Repository navigation
Building from Source
This page takes you from a clean machine to a running Moonfin build on every platform the app ships on. For pre-built packages instead, see Installation. For how to get a change merged, see Development.
If you only want one platform, read Step 1 and Step 2, then jump to that platform under Step 3. Each platform section has a plain path first and an Advanced block at the end for release packaging and signing.
| You want to build | You need to build on |
|---|---|
| Android and Android TV | Linux, macOS or Windows |
| iOS | macOS |
| macOS | macOS |
| Apple TV (tvOS) | macOS, Apple silicon recommended (the tvOS simulator engine is arm64 only) |
| Windows | Windows 10 or 11 (x64 or ARM64, no cross-building) |
| Linux | Linux, x86_64 or aarch64 |
| Web | Any of the above |
Samsung (Tizen) and LG (webOS) TVs aren't built from this repo. Those TVs run the separate Smart-TV client.
These are the versions the official releases are built with. They come from .github/workflows/build.yml, pubspec.yaml and the Android Gradle files. Newer stable versions normally work, older ones often do not.
| Tool | Version | Needed for |
|---|---|---|
| Flutter |
3.47.2 stable (3.47.0 is the minimum in pubspec.yaml) |
Everything |
| Dart | 3.13 (ships inside Flutter, nothing to install) | Everything |
| Xcode | 26.3 (Flutter itself accepts 15 and recommends 16 or newer) | iOS, macOS, tvOS |
| CocoaPods | 1.16.2 or newer | iOS, macOS, tvOS. Swift packages resolve through Xcode, nothing extra to install |
| flutter-tvos toolchain | tag v3.47.2-tvos.1.10.0 | tvOS |
| JDK | 17 (Temurin). Gradle 8.14 also accepts 18 to 24 | Android |
| Android Studio | Current stable. Narwhal 3 Feature Drop (2025.1.3) or newer opens the project without an AGP warning | Android (easiest way to get the SDK) |
| Android Gradle Plugin | 8.13.2, pinned in android/settings.gradle.kts
|
Android |
| Gradle | 8.14.5, the wrapper downloads it for you | Android |
| Kotlin | 2.2.20, pinned | Android |
| Android SDK | compileSdk 36, minSdk 24, NDK 28.2.13676358, CMake 3.22.1 | Android |
| Visual Studio | 2022 with the Desktop development with C++ workload | Windows |
| vcpkg with libarchive | Current | Windows |
| Inno Setup | 6 | Windows installer only |
| Clang, CMake 3.13+, Ninja, pkg-config | Whatever your distro ships | Linux |
Flutter includes Dart, so this is the only SDK you install by hand.
- Download Flutter 3.47.2 stable for your OS from the Flutter SDK archive. The latest stable also works, 3.47.2 is the tested one.
- Extract it to a path without spaces. Good choices:
~/flutteron Linux and macOS,C:\flutteron Windows. Do not put it insideProgram Files. - Add the
binfolder to your PATH.- Linux and macOS: add
export PATH="$HOME/flutter/bin:$PATH"to~/.bashrcor~/.zshrc, then open a new terminal. - Windows: Settings > System > About > Advanced system settings > Environment Variables > Path > New >
C:\flutter\bin. Open a new PowerShell window afterwards.
- Linux and macOS: add
- Check it works:
flutter --version
flutter doctorflutter --version must say 3.47.x and channel stable. If it shows another channel run flutter channel stable and then flutter upgrade.
flutter doctor prints one line per platform. Only the platform you want to build needs a green checkmark. Warnings for the others are fine to ignore.
If you already use Flutter through Homebrew, snap or FVM that is fine too, as long as flutter --version prints 3.47.x. FVM users can run fvm use 3.47.2 in the repo.
git clone https://github.com/Moonfin-Client/Moonfin-Core.git
cd Moonfin-Core
flutter pub getflutter pub get downloads every Dart package, including a few forks that live on GitHub (listed under dependency_overrides in pubspec.yaml). Nothing needs patching by hand. The comment above each override in pubspec.yaml says what the fork changes.
It needs internet, and so does the first build on most platforms, since several plugins download prebuilt pieces while they compile.
If git shows lib/l10n/app_localizations.dart modified after this, discard the change unless you edited app_en.arb. It is regenerated on every pub get.
Builds on Linux, macOS and Windows.
- Install Android Studio. Start it once and let the setup wizard install the SDK (it lands in
~/Android/Sdk,~/Library/Android/sdkor%LOCALAPPDATA%\Android\Sdk). - In Android Studio open More Actions > SDK Manager.
- SDK Platforms tab: tick Android 16 (API 36).
- SDK Tools tab: tick Show Package Details at the bottom right, then tick Android SDK Build-Tools, Android SDK Command-line Tools, Android SDK Platform-Tools, NDK (Side by side) 28.2.13676358 and CMake 3.22.1. Apply.
- Java: Android Studio ships a JDK and Flutter finds it on its own. If
flutter doctorcomplains about Java, install Temurin 17 and point Flutter at it withflutter config --jdk-dir <path to the jdk>. - Accept the licenses:
flutter doctor --android-licensesAnswer y to everything. flutter doctor should now show Android toolchain with a checkmark.
Without Android Studio you can install the command-line tools and run sdkmanager "platforms;android-36" "build-tools;36.0.0" "platform-tools" "ndk;28.2.13676358" "cmake;3.22.1", then flutter config --android-sdk <path>.
Two things you do not need to do: the Firebase file android/app/google-services.json is already in the repo, and Gradle downloads itself through the wrapper.
The Android project defines product flavors, so every Android build and run command needs --flavor. Without it Flutter stops with an error that lists the flavors.
| Flavor | What it is | Application id |
|---|---|---|
mobile |
Phones and tablets | org.moonfin.androidtv |
androidTv |
Android TV, Google TV, Fire TV | org.moonfin.androidtv |
mobile-beta, androidTv-beta
|
Same apps, installable next to the release app | org.moonfin.androidtv.beta |
The TV flavors also need --dart-define=MOONFIN_FORCE_TV=true, which turns on the TV interface at compile time.
# Phones and tablets
flutter build apk --release --flavor mobile --dart-define=DISTRIBUTION_CHANNEL=apk
# Android TV, Google TV, Fire TV
flutter build apk --release --flavor androidTv --dart-define=MOONFIN_FORCE_TV=true --dart-define=DISTRIBUTION_CHANNEL=android_tv_apkThe APKs land in build/app/outputs/flutter-apk/ as app-mobile-release.apk and app-androidtv-release.apk. All Gradle output goes to the repo's build/ folder, not android/app/build.
Each APK contains arm64-v8a, armeabi-v7a and x86_64. Add --split-per-abi to get one smaller APK per architecture.
Without a release keystore the APK is signed with the debug key. It installs and works, but Android refuses to install it over a Play Store build or any build signed with another key. Uninstall the old one first.
- On the phone: Settings > About phone > tap Build number seven times, then Settings > Developer options > USB debugging on. Plug it in and accept the prompt.
- On a TV box: Settings > Device Preferences > About > tap Build seven times, turn on USB debugging or Network debugging, then
adb connect <tv ip>:5555. - Check the device shows up, then run:
flutter devices
flutter run --flavor mobile
flutter run --flavor androidTv --dart-define=MOONFIN_FORCE_TV=trueHot reload works as usual. flutter run builds in debug mode, so it is slower than a release APK. Add --release for a fair speed test.
If Gradle dies with Java heap space or the daemon disappears, lower -Xmx8G in android/gradle.properties to fit your RAM. Do not commit that change.
Advanced: release APKs, App Bundles and signing
./build-android.sh (or .\build-android.ps1 on Windows) is the release recipe. It runs flutter clean, builds both flavors as APK and as App Bundle, copies them to the repo root as Moonfin_Android_v<version>.apk, .aab, Moonfin_AndroidTV_v<version>.apk and .aab, and runs scripts/check-android-16kb-pages.sh over the result. Pass mobile or tv to ./build-android.sh to build just that flavor, which is how CI runs it.
To sign with your own key, put the keystore at android/app/release.keystore and create android/keystore.properties:
storePassword=...
keyAlias=...
keyPassword=...
# optional, for example pkcs12
storeType=Both files are ignored by git. When they are missing the release build falls back to the debug key.
Version numbers: version: x.y.z+build in pubspec.yaml feeds the mobile flavors. The TV flavors read android_tv_version and android_tv_build_number from the moonfin: block in the same file.
Android-side unit tests run from the android/ folder with ./gradlew testAndroidTvDebugUnitTest. Results land under the repo's build/ folder.
Impeller is switched off for the TV flavors through a manifest placeholder, because the GLES fallback stutters on TV box GPUs.
Needs a Mac.
- Install Xcode from the Mac App Store. Releases use 26.3, anything from 16 up works. Open it once and accept the license, then in Terminal:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch- Add an iOS simulator runtime: Xcode > Settings > Components > pick an iOS version > download.
- Install CocoaPods:
brew install cocoapods
pod --versionpod --version should print 1.16 or newer. If you do not use Homebrew, sudo gem install cocoapods works too.
-
flutter doctorshould show Xcode with a checkmark.
No Apple account and no signing needed:
open -a Simulator
flutter runOr open ios/Runner.xcworkspace in Xcode (always the .xcworkspace, never the .xcodeproj), pick a simulator and press Run.
To just produce the app without running it: flutter build ios --simulator --debug. The result is build/ios/iphonesimulator/Runner.app.
The emulator cores are not in git. The app builds and runs without them, only the Retro Games feature has nothing to play. To bundle them for a device build, run this once before building:
./ios/game_host/fetch_cores.sh
./ios/game_host/wrap_frameworks.shThis downloads prebuilt arm64 cores from the libretro buildbot, so it is for development builds on a real device. If a simulator build afterwards complains about frameworks built for iOS, delete the *.framework folders in ios/game_host/cores/ and build again.
Advanced: running on a physical iPhone or iPad
A physical device needs signing, and the app ships two entitlements Apple grants only on request, multicast and CarPlay. A normal developer account can't sign with either. To put a build on your own device, change three things in Xcode under Signing and Capabilities:
- Set the team to your own
- Change the bundle identifier, since
org.moonfin.appisn't yours to sign - Remove the multicast and CarPlay entitlements
Xcode writes the first two into ios/Runner.xcodeproj/project.pbxproj, which is tracked, so check that file before you commit and leave those edits out.
Push notifications won't work in a build like this, and Firebase logs a bundle identifier mismatch once the identifier is no longer org.moonfin.app. Both are expected.
Then plug the device in, trust the Mac on the device, and flutter run picks it up. Or run from Xcode.
Advanced: the release IPA
./build-ios.sh is the release recipe and is not the way to build for local testing. Copy build-ios.private.env.example to build-ios.private.env first and edit it. That file is ignored by git.
- With the default
IOS_CODESIGN="0"it producesMoonfin_iOS_v<version>_unsigned.ipain the repo root, the same file the releases ship for sideloading with AltStore, Sideloadly and friends. -
IOS_CODESIGN="1"plusIOS_EXPORT_METHOD="development"builds a signed IPA for your own registered devices.IOS_EXPORT_OPTIONS_PLISTpoints at a custom export plist instead. - Cores: by default the script compiles the cores from pinned sources with no JIT (
IOS_CORES_MODE=build, slow, the App Store path).IOS_CORES_MODE=fetchdownloads prebuilt ones instead.IOS_CORES_FORCE=1redoes the provisioning when cores already exist.
The ios job in .github/workflows/build.yml is the same flow on CI, plus certificate handling through .github/actions/apple-signing.
Needs a Mac. Same Xcode and CocoaPods setup as iOS.
flutter config --enable-macos-desktop
flutter run -d macos
flutter build macos --releaseThe release app is build/macos/Build/Products/Release/Moonfin.app. Double-click it or drag it into Applications.
Xcode signs it with your local development identity, or ad hoc when there is none, so it runs on your own Mac. Another Mac's Gatekeeper will refuse it, that needs Developer ID signing and notarization (see Advanced).
There is no Firebase on macOS, so there is no plist to add. The retro game cores are optional here as well and not in git. ./macos/game_host/fetch_cores.sh downloads universal (arm64 and x86_64) cores into macos/game_host/cores/.
Intel Macs draw with Skia. macos/Runner/MainFlutterWindow.swift turns Impeller off there, because the Impeller default in Flutter 3.47 garbles text and lags on Intel GPUs. Apple silicon stays on Impeller.
Advanced: DMG, App Store PKG and notarization
./build-macos.sh builds the DMG and the App Store PKG. It takes no arguments, everything is configured through environment variables, best placed in build-macos.private.env in the repo root (ignored by git).
| Variable | Meaning |
|---|---|
DEVELOPER_ID |
Developer ID Application: Your Name (TEAMID). Empty means ad hoc signing, which still yields a DMG for your own use |
NOTARYTOOL_PROFILE |
Keychain profile created with xcrun notarytool store-credentials. Set it to notarize the DMG |
TEAM_ID |
Your team id, needed for the PKG |
BUILD_APPSTORE_PKG |
0 skips the PKG |
EXPORT_DESTINATION |
export (the default) writes the PKG to disk, upload sends it straight to App Store Connect instead |
BUILD_DMG_GITHUB |
0 skips the DMG |
FETCH_CORES |
0 skips the core download |
CLEAN_FLUTTER |
1 runs flutter clean first |
ASC_KEY_PATH, ASC_KEY_ID, ASC_ISSUER_ID
|
App Store Connect API key, lets xcodebuild fetch profiles on a machine without an Apple ID in Xcode |
The output is Moonfin_macOS_v<version>.dmg in the repo root. The macos job in .github/workflows/build.yml runs this same script.
Needs a Mac, Apple silicon recommended. Regular Flutter cannot build for Apple TV. Moonfin uses the open source flutter-tvos toolchain, a drop-in flutter-tvos command with its own engine. You use flutter-tvos wherever you would normally type flutter.
- Xcode as described under iOS, plus the tvOS platform: Xcode > Settings > Components > tvOS, or
xcodebuild -downloadPlatform tvOS. Add a tvOS simulator runtime while you are there. - CocoaPods as described under iOS. Most tvOS plugins come in through Swift Package Manager, which is part of Xcode, but CocoaPods still installs the rest, and it brings the
xcodeprojRuby gem that the wiring scripts use. Python 3 comes with the Xcode command line tools, check withpython3 --version. - The toolchain, at the exact tag the releases use (
FLUTTER_TVOS_REFat the top of.github/workflows/build.yml):
git clone --depth 1 --branch v3.47.2-tvos.1.10.0 https://github.com/fluttertv/flutter-tvos.git ~/flutter-tvos
export PATH="$HOME/flutter-tvos/bin:$PATH"
flutter-tvos precache
flutter-tvos doctorAdd the export PATH line to ~/.zshrc too. Keep the clone at ~/flutter-tvos, the helper scripts look there. If you put it elsewhere, set FLUTTER_TVOS_ROOT to that path.
Open the simulator first: Xcode > Open Developer Tool > Simulator, then File > Open Simulator > tvOS. Then:
flutter-tvos pub get
./tvos/scripts/use_engine_slice.sh sim
flutter-tvos devices
flutter-tvos run -d <simulator id from the list above> --dart-define=MOONFIN_TVOS=true--dart-define=MOONFIN_TVOS=true is required on every tvOS build. It is how the app knows it is on Apple TV. Without it you get the iPhone interface on the TV.
- Pair the Apple TV with Xcode: on the Apple TV go to Settings > Remotes and Devices > Remote App and Devices, in Xcode open Window > Devices and Simulators and pair it. Both must be on the same network.
- Signing works like an iPhone device build: set your own team and a bundle identifier of your own in
tvos/Runner.xcodeproj, for both the Runner target and the MoonfinTopShelf extension, and leave those edits out of your commits. - Swap the engine to a device slice and run:
./tvos/scripts/use_engine_slice.sh debug # hot reload over the network
flutter-tvos run -d <apple tv> --dart-define=MOONFIN_TVOS=true
./tvos/scripts/use_engine_slice.sh release # AOT, what users get
flutter-tvos run -d <apple tv> --release --dart-define=MOONFIN_TVOS=true-
Use
flutter-tvos, notflutter, while working on tvOS. Plainflutter pub get,flutter analyzeor anyflutter buildrewrites.flutter-plugins-dependencieswithout the tvOS entries, and a build made from that loses its tvOS plugins. It cannot even store a login. Thetvos/Podfilerunstvos/scripts/restore_tvos_plugins.pyto put the entries back and stops with an error if it can't, and you can run it by hand beforecd tvos && pod install. -
The engine framework is not in git.
tvos/Flutter/Flutter.frameworkis ignored, andflutter-tvos precacheleaves the simulator slice there. Runuse_engine_slice.shevery time you switch between simulator and device.
The tvOS game cores are committed under tvos/Frameworks/Cores/, so unlike iOS there is nothing to fetch.
Advanced: how the tvOS plugins are linked
The tvOS project uses Swift Package Manager for every plugin that ships a Package.swift, which today is flutter_tvos, path_provider_tvos, shared_preferences_tvos and flutter_secure_storage_tvos. flutter-tvos generates them into FlutterGeneratedPluginSwiftPackage under tvos/Flutter/ephemeral/Packages/, and tvos/Runner.xcodeproj references that package. The Podfile skips any plugin with a Package.swift so nothing is linked twice, which leaves CocoaPods installing only the plugins without one, currently just sqflite_tvos. AetherEngine and libass come in as Swift packages as well.
Advanced: the unsigned tvOS IPA and the core scripts
The tvos job in .github/workflows/build.yml is the reference recipe and worth reading in full. In short:
flutter-tvos pub get
flutter-tvos build tvos --release --no-tree-shake-icons --dart-define=MOONFIN_TVOS=true
python3 tvos/scripts/restore_tvos_plugins.py
(cd tvos && pod install)
# copy build/tvos/**/Release-appletvos/Runner.app/flutter_assets to tvos/Flutter/flutter_assets
xcodebuild -workspace tvos/Runner.xcworkspace -scheme Runner -configuration Release \
-sdk appletvos -derivedDataPath /tmp/moonfin-dd \
CODE_SIGNING_ALLOWED=NO CODE_SIGN_IDENTITY="" buildThe resulting Runner.app goes into a Payload/ folder and is zipped as Moonfin_tvOS_v<version>_unsigned.ipa. The workflow also checks that flutter_assets/fonts/MaterialIcons-Regular.otf and AssetManifest.bin made it into the app, which is the usual thing to go wrong.
Core scripts under tvos/scripts/cores/, needed only when the bundled cores change:
-
fetch_cores.shdownloads prebuilt tvOS cores for development. -
build_cores.shbuilds them from pinned sources, interpreter only, for the App Store. -
wrap_frameworks.shturns the staged dylibs into frameworks and refuses any core that links the JIT toggle. -
tvos/scripts/wire_tvos_cores.rbre-embeds the frameworks in the Xcode project, andwire_tvos_player.rbre-adds the native player sources after files are added or removed undertvos/Runner/Playback.
The core list has to match appleBundledCores in lib/util/game_cores.dart, and game_cores_test.dart fails when they drift.
Needs Windows 10 or 11. An ARM64 PC builds the ARM64 app, an x64 PC builds the x64 app, there is no cross-building.
- Git for Windows with the default options.
- Turn on Developer Mode: Settings > System > For developers > Developer Mode. Flutter needs it to create symlinks for plugins.
start ms-settings:developersopens that page. - Visual Studio 2022 Community. In the installer tick the workload Desktop development with C++. On an ARM64 PC also open Individual components and tick MSVC v143 - VS 2022 C++ ARM64/ARM64EC build tools (Latest). Visual Studio Code is a different product and does not count.
- Flutter as in Step 1, extracted to
C:\flutter. Open a new PowerShell window and run:
flutter config --enable-windows-desktop
flutter doctorflutter doctor should show Visual Studio - develop Windows apps with a checkmark.
- libarchive through vcpkg. One plugin needs it and the build stops with libarchive not found for Windows plugin without it:
git clone https://github.com/microsoft/vcpkg C:\vcpkg
C:\vcpkg\bootstrap-vcpkg.bat
C:\vcpkg\vcpkg install libarchive:x64-windows
[Environment]::SetEnvironmentVariable('VCPKG_ROOT', 'C:\vcpkg', 'User')On an ARM64 PC use libarchive:arm64-windows. Close and reopen the terminal so VCPKG_ROOT is visible. If you only ever use build-windows.ps1 you can skip this step, that script sets vcpkg up on its own.
Use PowerShell or cmd for Flutter commands, not Git Bash.
flutter run -d windows
flutter build windows --releaseThe release build is the folder build\windows\x64\runner\Release\ (build\windows\arm64\runner\Release\ on ARM64). Run moonfin.exe from there. The whole folder is the app, move or zip it as one piece. libmpv and the rest of the video stack are downloaded by flutter pub get. A plain flutter build windows folder does rely on the Visual C++ runtime being installed on the machine that runs it. build-windows.ps1 copies the runtime DLLs from your Visual Studio install next to moonfin.exe, so its builds start without the redistributable.
The runner turns Impeller off in windows/runner/main.cpp, so Windows draws with Skia. Impeller, the Flutter 3.47 desktop default, made library scrolling lag.
Keep the checkout close to the drive root, for example C:\src\Moonfin-Core. Deep paths hit the Windows path length limit during the build.
Advanced: the installer, ARM64 and MSIX
- Install Inno Setup 6:
winget install JRSoftware.InnoSetup(orchoco install innosetup). - If PowerShell refuses to run scripts:
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass. - Run the script:
.\build-windows.ps1 # x64 PC, produces Moonfin_Windows_v<version>.exe
.\build-windows.ps1 -Architecture arm64 # ARM64 PC, produces Moonfin_WindowsARM64_v<version>.exeThe script finds or bootstraps vcpkg (into a vcpkg folder inside the repo when none is installed), installs libarchive for the target architecture, checks the Flutter version, builds with --dart-define=DISTRIBUTION_CHANNEL=windows and compiles the Inno Setup installer into the repo root.
Release installers are signed through SignPath on CI. A local installer is unsigned and SmartScreen will warn about it.
.\build-windows.ps1 -Store (add -Architecture arm64 for ARM64) builds the Microsoft Store package instead of the installer: MoonfinStore_Windows_v<version>.msix or MoonfinStore_WindowsARM64_v<version>.msix in the repo root. It builds with DISTRIBUTION_CHANNEL=msstore, which turns the in-app updater off, and packs the Release folder with dart run msix:create --store using the msix_config block in pubspec.yaml. That way the vcpkg and Visual C++ runtime DLLs come along, which msix's own build would leave out. The package is unsigned, since the Store signs it, so it only installs through the Store. CI builds both on tags and manual runs and keeps them as workflow artifacts rather than release assets. In the packaged app moonfin:// comes from protocol_activation in msix_config, because registry writes from a packaged app land in a private hive.
Builds on x86_64 and aarch64. The releases are built on Ubuntu 24.04 and Ubuntu 22.04 (ARM64).
Debian and Ubuntu:
sudo apt-get install -y clang cmake ninja-build pkg-config \
libgtk-3-dev libglib2.0-dev libwebkit2gtk-4.1-dev \
libmpv-dev libepoxy-dev libsecret-1-dev libasound2-dev libarchive-devFedora:
sudo dnf install -y clang cmake ninja-build pkgconf-pkg-config \
gtk3-devel glib2-devel webkit2gtk4.1-devel \
mpv-libs-devel libepoxy-devel libsecret-devel alsa-lib-devel libarchive-develArch:
sudo pacman -S --needed clang cmake ninja pkgconf \
gtk3 glib2 webkit2gtk-4.1 mpv libepoxy libsecret alsa-lib libarchiveWhat each one is for, in case your distro names them differently:
| Library | Used by |
|---|---|
| GTK 3, GLib | Flutter itself |
| webkit2gtk 4.1 (4.2 and 4.3 also work) | In-app web views |
| libmpv, libepoxy | Video playback |
| libsecret | Credential storage |
| ALSA | Volume control |
| libarchive | Comic and archive extraction |
Then:
flutter config --enable-linux-desktop
flutter doctorflutter doctor should show Linux toolchain with a checkmark. The first build downloads a few prebuilt pieces (SQLite, pdfium, mimalloc), so it needs internet.
flutter run -d linux
flutter build linux --releaseThe release build is the folder build/linux/x64/release/bundle/ (arm64 on aarch64). Run bundle/moonfin. The bundle folder is the app, move it as a whole. libmpv must be installed on any machine that runs it. The release packages bundle it, a plain flutter build linux does not.
The runner turns Impeller off in linux/runner/my_application.cc, so Linux draws with Skia. Impeller, the Flutter 3.47 desktop default, kept the GPU busy while idle and made Home and library scrolling lag.
Things that bite:
-
After installing a missing package, delete
build/linuxbefore retrying. CMake caches the failed lookup and keeps failing with the same message. -
pkg_check_modules: gtk+-3.0 not foundwhile the dev package is installed meanspkg-configis not looking in the system directories.pkg-config --modversion gtk+-3.0must print a version. Homebrew on Linux shadowing the systempkg-configis the usual cause. -
Black screen right after start on a minimal system: install
xdg-user-dirs. The app cannot find its data directory without it. -
Immutable distros (Silverblue, Kinoite, Bazzite): the easy path is a
distroboxortoolboxcontainer with the Fedora package list above and Flutter installed inside it. Building on the host works but needs the mpv and ALSA headers placed by hand.
Advanced: AppImage, deb, rpm, Flatpak, Snap and tarball
./build-linux.sh # every format the installed tools allow
./build-linux.sh appimage # or tarball, deb, rpm, snap, flatpakExtra tools per format. A missing tool skips that format with a message instead of failing:
| Format | Needs |
|---|---|
| AppImage |
appimagetool on PATH, zsync for the embedded update info, patchelf, and libfuse2 to run the result |
| deb | dpkg-deb |
| rpm |
rpm / rpmbuild
|
| Flatpak |
flatpak and flatpak-builder. The script installs the GNOME 50 runtime and SDK from Flathub into your user installation |
| Snap |
snapcraft, Ubuntu only. The base is picked from your Ubuntu release |
| tarball | Nothing extra |
Output files are Moonfin_Linux_v<version>.<ext> in the repo root, Moonfin_LinuxARM64_... on aarch64. The AppImage bundles libmpv and everything it pulls in, so it runs on systems without mpv installed.
The script builds with --dart-define=DISTRIBUTION_CHANNEL=linux. The AUR package uses linux_aur, which turns the in-app updater off. If flutter build linux fails on the pdfium download, the script purges that cache and retries on its own.
Any host. Install Chrome for the debug loop.
flutter run -d chrome
flutter build web --wasm --release --base-href "/Moonfin/Web/"The output is build/web. The web app is served by the Moonbase server plugin at /Moonfin/Web/ on Jellyfin and Emby, which is why the base href is fixed.
./build-web-plugin.sh builds once and syncs the result into a Plugin checkout next to this repo (../Plugin/Jellyfin/frontend and ../Plugin/Emby/web). Pass one or more target folders to sync somewhere else, -h shows the options.
After bumping drift or sqlite3 in pubspec.yaml, run scripts/regen-web-assets.sh to regenerate web/sqlite3.wasm and web/drift_worker.js.
flutter analyze
flutter test- Do not run
dart formatover whole files. The repo is not format-clean and it churns hundreds of unrelated lines. Format only the lines you touched. - Three generated files are committed:
lib/data/database/offline_database.g.dartand the two Seerr model files next toseerr_models.dart. After changing their sources rundart run build_runner build --delete-conflicting-outputsand commit the result. - Translations: edit
lib/l10n/app_en.arbonly, then runflutter gen-l10n. Every other language comes from Weblate, see Development. - Try the change on at least one real platform.
CI runs the same checks on every pull request, for the app and for several of the packages under packages/, such as server_core, server_jellyfin and server_emby. Only analyzer errors fail it, since the tree carries some long-standing infos and warnings. Every failing test does.
Everything below is passed as --dart-define=NAME=value. A local build can leave all of them out except MOONFIN_TVOS on Apple TV and MOONFIN_FORCE_TV for the TV flavors on Android.
| Flag | Values | Effect |
|---|---|---|
DISTRIBUTION_CHANNEL |
apk, aab, android_tv_apk, android_tv_aab, ios_signed, ios_unsigned, macos_dmg, macos_pkg, windows, linux, linux_aur
|
Tells the app how it was distributed, which drives the in-app update check. Unset means unknown |
MOONFIN_FORCE_TV |
true |
Compiles the TV interface in. Used by the androidTv flavors |
MOONFIN_TVOS |
true |
Marks an Apple TV build. Required on every tvOS build |
| Where | Feeds |
|---|---|
version: x.y.z+build in pubspec.yaml
|
Every platform except Android TV, including the Store MSIX |
moonfin.android_tv_version and moonfin.android_tv_build_number in pubspec.yaml
|
The Android TV flavors |
dependency_overrides in pubspec.yaml swaps a few packages for forks: media_kit_video, media_kit_libs_windows_video and background_downloader come from GitHub at pinned commits, while media_kit_libs_android_video (as media_kit_libs_android_video_full), flutter_inappwebview_windows and gamepads_linux live under packages/. The rest of that block pins versions. The comment above each fork says what it changes and when it can go, and the vendored packages carry their reasoning inline at the changed code, so on an upgrade you re-vendor the matching release and diff those files against it. flutter pub get handles all of it, there is nothing to run by hand.
.github/workflows/build.yml builds every platform on every release tag and on every pull request to main. FLUTTER_VERSION and FLUTTER_TVOS_REF at the top are the versions to match. When a build works there and not on your machine, diff your setup against that job.
| Job | Runner |
|---|---|
| android | ubuntu-latest, Temurin JDK 17 |
| ios, macos, tvos | macos-26 with Xcode 26.3 |
| windows, windows-arm64 | windows-latest, windows-11-arm |
| linux | ubuntu-24.04 (x64), ubuntu-22.04-arm (ARM64) |
| tests | ubuntu-latest, runs the analyzer and every test suite |
On a pull request, a bot comment lists how each platform build and the tests went, and it's rewritten in place on every new commit.
Android uses the keystore files described above, fed from repository secrets. iOS, macOS and tvOS go through .github/actions/apple-signing with a distribution certificate and an App Store Connect API key. Windows installers are signed through SignPath after the build. A manual run of the workflow can send the Apple builds to TestFlight instead of keeping them as artifacts, and then skips every other platform.
| Symptom | Fix |
|---|---|
Android: You must specify a --flavor option or the build lists product flavors |
Add --flavor mobile or --flavor androidTv
|
Android: Java heap space, Gradle daemon disappears |
Lower -Xmx8G in android/gradle.properties
|
Android: sdkmanager not found, licenses not accepted |
Install Android SDK Command-line Tools in the SDK Manager, then flutter doctor --android-licenses
|
Android: INSTALL_FAILED_UPDATE_INCOMPATIBLE
|
The installed app has a different signing key. Uninstall it first |
| Android: NDK or CMake version errors | Install exactly NDK 28.2.13676358 and CMake 3.22.1 in the SDK Manager |
iOS: Unable to find a destination or iOS N is not installed
|
Download the runtime in Xcode > Settings > Components |
iOS or macOS: CocoaPods not installed although it is |
pod --version must work in the same terminal. Reinstall with brew install cocoapods
|
iOS: Signing for "Runner" requires a development team
|
Simulator: pick a simulator device. Device: see the iOS Advanced block |
iOS or macOS: No such module Flutter in Xcode |
Run flutter build ios or flutter build macos once from the terminal, then open the .xcworkspace
|
tvOS: moonfin: no tvos plugins to install
|
flutter-tvos pub get, then python3 tvos/scripts/restore_tvos_plugins.py, then pod install in tvos/
|
tvOS: engine slice ... not found
|
flutter-tvos precache --force, then use_engine_slice.sh again |
| tvOS: the app runs but looks like the iPhone app | You forgot --dart-define=MOONFIN_TVOS=true
|
Windows: Building with plugins requires symlink support
|
Turn on Developer Mode |
Windows: libarchive not found for Windows plugin
|
vcpkg install libarchive:x64-windows, set VCPKG_ROOT, open a new terminal |
Windows: Unable to find suitable Visual Studio toolchain
|
Install the Desktop development with C++ workload, run flutter doctor
|
Linux: pkg_check_modules ... gtk+-3.0 not found
|
Install the GTK dev package, check pkg-config --modversion gtk+-3.0, delete build/linux
|
Linux: Could NOT find ALSA
|
Install libasound2-dev or alsa-lib-devel, delete build/linux
|
| Linux: window opens black | Install xdg-user-dirs
|
| Any platform: odd errors after changing Flutter versions or machines | flutter clean && flutter pub get |
Using Moonfin
Going deeper
For developers