fpack 1.1.13
fpack: ^1.1.13 copied to clipboard
Package Flutter apps into release artifacts for every platform (apk, aab, ipa, dmg, exe, msix, deb, rpm, AppImage, web) with one command. Native Go core, thin Dart launcher.
fpack #
English · 中文
📖 Documentation → matkurban.github.io/fpack #
Guides for every platform (signing, notarization, installers), every command and config key, CI recipes and troubleshooting. A complete sample app with a commented fpack.yaml lives in example/.
One command turns a Flutter project into release artifacts for every platform. Android (APK / per-ABI APKs / AAB), iOS (IPA), macOS (.app zip / DMG / pkg installer, signed + notarized), Windows (zip / Inno Setup installer / MSIX), Linux (tar.gz / deb / rpm / AppImage / Snap) and Web (zip).
dart pub global activate fpack # install (git / local path also work, see below)
cd my_flutter_app
fpack doctor # what can this machine build? what is missing and how to install it?
fpack build apk aab # → dist/1.0.0+1/my_app-1.0.0+1-android-universal.apk …
fpack build --all # everything this machine can build; the rest is skipped with a reason
- Works with zero configuration: reads
pubspec.yaml, Gradle and the Xcode projects; thefpack.yamlgenerated byfpack initis entirely optional. - Never modifies your project files: fpack only drives the Flutter toolchain and packaging tools. Signing data is injected through environment variables, Gradle files are never edited. The only file it ever writes into the project is the
fpack.yamlfromfpack init; artifacts go todist/, temporary files tobuild/fpack/. - Native core: the core is written in Go and runs as a native binary (≈10 ms startup, no runtime dependencies); the Dart package is a thin launcher that finds or prepares the binary of exactly the same version.
- Everything is configurable: 211 fpack.yaml keys (installer metadata, signing, DMG layout, Inno Setup languages/privileges, MSIX, deb/rpm metadata and scripts, hooks, file name templates…), any value can come from the environment (
${VAR}/${VAR:-default}, also for booleans and numbers) and the build, signing and notarization settings have dedicatedFPACK_*variables; the configuration is strictly validated (unknown keys get a "did you mean …", type errors point to the line) and a JSON Schema gives editor completion. - Built for humans: colored output and spinners (plain text in CI), failures show the key error excerpt + a fix + the full log path,
--dry-runshows every exact command,--jsonfor machines, English/Chinese chosen automatically.
Contents #
- Install
- Quick start
- Commands
- Targets and artifacts
- Configuration: fpack.yaml
- Signing (Android · iOS · macOS · getting macOS credentials)
- CI example
- AI agent skill
- Environment variables
- Exit codes
- FAQ
- Architecture
- Development and releases
Install #
Requires Dart 3.8+ (bundled with Flutter). Pick one:
| Method | Command | Notes |
|---|---|---|
| pub.dev | dart pub global activate fpack |
recommended |
| git | dart pub global activate --source git https://github.com/Matkurban/fpack |
the first run downloads the core from the GitHub Release (SHA-256 verified); builds it with local Go only if the download is unavailable |
| local path | dart pub global activate --source path /path/to/fpack |
works offline: the release tarball contains prebuilt binaries for all 6 platforms |
Make sure ~/.pub-cache/bin (Windows: %LOCALAPPDATA%\Pub\Cache\bin) is on your PATH:
echo 'export PATH="$PATH:$HOME/.pub-cache/bin"' >> ~/.zshrc && source ~/.zshrc
fpack --version
How the launcher finds the native core #
On every run fpack looks for an fpack-core whose version exactly matches the Dart package (checked with fpack-core --core-version), in this order. A verified prebuilt binary always wins; a local build is only the last resort:
-
the
FPACK_COREenvironment variable (development) -
the user cache: macOS
~/Library/Caches/fpack/<version>/<os>-<arch>/, Linux$XDG_CACHE_HOME/fpack/…(default~/.cache/fpack), Windows%LOCALAPPDATA%\fpack\…(override withFPACK_HOME) -
the bundled prebuilt binary
prebuilt/<os>-<arch>/(when present): verified against the SHA-256 inprebuilt/manifest.json, then copied into the cache -
a download of this platform's binary (≈4 MB) from the GitHub Release, verified against
checksums.txt— a mismatching file is never executed -
only if the download fails or is disabled: a local build from the bundled Go sources (
go build -trimpath, dependencies are vendored, no network needed), with a clear note that it fell back to a local build and which Go it used:fpack: note: no verified prebuilt core available (download: …connection refused…); fpack: falling back to a local build from the bundled sources with go1.27.2 (/usr/local/go/bin/go) – first run only, ~30s…
Controls:
| Variable | Effect |
|---|---|
FPACK_NO_DOWNLOAD=1 |
offline / air-gapped: never download; use the bundled binary or build locally with Go |
FPACK_DOWNLOAD_URL |
download from a mirror (a directory with checksums.txt and fpack-core-<os>-<arch> (.exe on Windows), laid out like the GitHub Release) |
FPACK_GO=/path/to/go / FPACK_GO=none |
the Go used for the fallback build / never build locally |
FPACK_REBUILD=1 |
development: ignore the cache and build with local Go first (after changing the Go sources) |
FPACK_HOME |
cache directory |
A cached core is reused until the version changes; to replace a locally built core with the downloaded binary, delete the cache directory (fpack --wrapper-info shows its path).
fpack --wrapper-info shows each step's path and the binary that is finally used.
pub.dev package size trade-off: the binaries for 6 platforms are ≈23 MB (≈10 MB compressed). Bundling them in the pub package → works offline right after install, but every user downloads every platform's binary; not bundling → a tiny package (≈350 KB) that downloads this platform's ≈4 MB binary from the GitHub Release on first run (and only builds with local Go, ≈30 s, if that is unavailable). The release workflow's repository variable
BUNDLE_BINARIESdecides; thefpack-vX.Y.Z.tar.gzon the GitHub Release always contains all binaries for offline / intranet use.
Quick start #
cd my_flutter_app
fpack doctor # checks the environment, with an install/fix command per item
fpack build apk --dry-run # shows the commands that would run (runs nothing)
fpack build apk # actually build
fpack init # (optional) commented fpack.yaml, interactive; --yes uses the defaults
Artifacts go to dist/<version>+<build>/ by default, together with SHA256SUMS (check with shasum -a 256 -c SHA256SUMS):
dist/1.0.0+1/
my_app-1.0.0+1-android-universal.apk
my_app-1.0.0+1-android-arm64-v8a.apk
my_app-1.0.0+1-android.aab
my_app-1.0.0+1-ios-arm64.ipa
my_app-1.0.0+1-macos-universal.dmg
my_app-1.0.0+1-macos.pkg
SHA256SUMS
Existing artifacts are never overwritten by default: bump the version, use --build-number N, or pass --force.
Commands #
fpack build [targets…] [options] [-- args passed to flutter]
fpack doctor check the prerequisites of every target, with fixes
fpack list list all targets, their artifact formats and whether this machine can build them
fpack init write an fpack.yaml listing every key with English/Chinese comments (-y/--yes uses the
detected defaults; if it exists, writes fpack.yaml.new and shows a diff, --force overwrites)
fpack schema print the JSON Schema of fpack.yaml (-o FILE writes a file)
fpack notarize status [dir|ID] query the state of macOS notary submissions (reads dist/…/notarization.json)
fpack notarize finish [dir] wait for the notary result, staple the ticket, update SHA256SUMS
fpack clean remove build/fpack; --dist also removes dist/; --flutter-clean runs flutter clean; --all everything
fpack version version information (same as --version / -V)
fpack help <command> help for a command
Global options: -C, --project DIR, --config FILE, --flutter SDK, --lang zh|en, -v, --verbose, --json, --no-color, -y, --yes.
build options #
| Option | Description |
|---|---|
-a, --all |
build every target this machine can build; skip the rest with a reason |
-n, --dry-run |
print the full plan and exact commands, run nothing |
-m, --mode / --release --profile --debug |
build mode (default release) |
--flavor NAME |
Android productFlavor / Xcode scheme. A name that is not among the detected flavors is an error (exit 3, with a suggestion); if no flavors are detected it is only a warning |
-t, --target FILE |
entry point, e.g. lib/main_prod.dart |
--dart-define K=V (repeatable) / --dart-define-from-file FILE |
compile-time variables |
--build-name X.Y.Z / --build-number N |
override the pubspec version |
--split-per-abi[=true|both] / --abis LIST |
per-ABI APKs; both also keeps the universal APK |
--obfuscate / --split-debug-info DIR |
obfuscation; symbols default to <output>/debug-info/<platform> |
-o, --output DIR / -f, --force |
output directory / allow overwriting |
--export-method M / --export-options-plist FILE / --no-codesign |
iOS export |
--sign --no-sign --sign-identity ID --installer-identity ID --notarize --no-notarize --notarize-no-wait --notary-profile NAME --dmg-tool T |
macOS signing (app / pkg), notarization (--notarize-no-wait: submit without waiting), DMG tool |
--base-href PATH / --wasm |
Web |
-- … |
everything after is passed to flutter build, e.g. -- --no-tree-shake-icons |
Examples:
fpack build apk aab --flavor prod --dart-define-from-file env/prod.json
fpack build apk --split-per-abi=both
fpack build ipa --export-method ad-hoc
fpack build ipa --no-codesign # unsigned IPA (Payload layout)
fpack build macos dmg # signs/notarizes per fpack.yaml macos.sign (unsigned if not configured)
fpack build dmg --no-notarize # quick local package
fpack build dmg --notarize-no-wait # submit and exit; later: fpack notarize finish
fpack build macos dmg pkg # zip + DMG + pkg installer from a single flutter build
fpack build --all --json > result.json
fpack -C apps/client build web --base-href /app/
Precedence: command-line flags > FPACK_* environment variables > fpack.yaml > defaults.
No targets: fpack build without targets builds build.targets from fpack.yaml; without that key it stops with exit code 2 and asks which targets to build.
Interrupting: one Ctrl-C stops the running flutter/gradle/xcodebuild gracefully (the whole process group), a second one kills it; exit code 130.
Monorepos: in a project subdirectory fpack walks up to pubspec.yaml; at the repository root it lists the Flutter projects it found — pick one with -C <dir>.
Flutter SDK lookup: --flutter → FPACK_FLUTTER → flutter.sdk in fpack.yaml → FVM (.fvm/flutter_sdk, .fvmrc) → FLUTTER_ROOT → PATH → common install locations (~/develop/flutter, ~/flutter, …).
Targets and artifacts #
The default file name template is {app}{-flavor}-{version}{+build}-{platform}{-arch}{-variant}{-mode} (no mode for release).
| Target | Host OS | Artifact | Tools |
|---|---|---|---|
apk |
any | -android-universal.apk, -android-arm64-v8a.apk … |
flutter, apksigner (signature check) |
aab |
any | -android.aab |
flutter, keytool (signature check) |
ipa |
macOS | -ios-arm64.ipa (unsigned: -ios-arm64-unsigned.ipa) |
flutter/xcodebuild, ditto with --no-codesign |
macos |
macOS | -macos-universal.zip |
flutter, codesign, ditto, notarytool |
dmg |
macOS | -macos-universal.dmg |
hdiutil or create-dmg, codesign, notarytool, stapler |
pkg |
macOS | -macos.pkg |
pkgbuild + productbuild (installs to /Applications), Developer ID Installer signing, notarytool, stapler |
windows |
Windows | -windows-x64-portable.zip |
flutter |
exe |
Windows | -windows-x64-setup.exe |
Inno Setup (ISCC) |
msix |
Windows | -windows-x64.msix |
the msix dev dependency (dart run msix:create) |
linux |
Linux | -linux-x64.tar.gz |
flutter |
deb |
Linux | -linux-x64.deb |
dpkg-deb |
rpm |
Linux | -linux-x64.rpm |
rpmbuild |
appimage |
Linux | -linux-x64.AppImage |
appimagetool (skipped by --all when missing) |
snap |
Linux | -linux-x64.snap |
snapcraft (skipped by --all when missing) |
web |
any | -web.zip |
flutter |
Flutter cannot cross-compile iOS/macOS/Windows/Linux desktop apps. fpack build --all skips targets this machine cannot build and says why; naming one explicitly (e.g. fpack build ipa on Linux) is an error that points you to a CI runner of that OS. --dry-run still shows a reference plan for such targets.
One flutter build is shared per run: macos + dmg + pkg build once, linux + deb + rpm + appimage + snap build once, windows + exe + msix build once.
Target aliases work on the command line and in build.targets: bundle/appbundle → aab, android → apk, ios → ipa, mac/osx/app → macos, win/zip/portable → windows, setup/installer/inno → exe, tar/tgz/tar.gz → linux, debian → deb, fedora → rpm, image/AppImage → appimage, snapcraft → snap.
fpack doctor lists a target as "ready to build" only when nothing it needs is missing; for apk/aab that includes Java (JDK 17+), even though a build still tries without it (Gradle may find a JDK fpack cannot see).
Configuration: fpack.yaml #
📖 Full configuration reference (every fpack.yaml key, every
FPACK_*variable, every flag, precedence rules and a complete example): doc/configuration.md · website
Every key is optional. fpack init writes a file that lists every key, each with an English (--lang zh: Chinese) comment describing what it does, its allowed values, default, an example and the matching environment variable / flag, pre-filled with values detected in the project (application ID, flavors, version, team ID, Inno AppId…); leave the keys you do not need commented out.
- Validation: unknown keys (with "did you mean …"), type errors (
line 12: android.signing.v1: expected true or false, got "maybe"), allowed values and ranges, conflicting combinations; files used by the selected targets are checked before building. - Editor completion: the first line of the generated file is
# yaml-language-server: $schema=…/schema/fpack.schema.json, so VS Code (YAML extension) / JetBrains give completion, hover docs (English/Chinese) and validation.fpack schema -o fpack.schema.jsonexports a local copy. - Environment variables:
${VAR}/${VAR:-default}are expanded on load in any value (obfuscate: ${OBF:-false}works too); build, signing and notarization settings also have dedicatedFPACK_*variables (see the full list).
# yaml-language-server: $schema=https://raw.githubusercontent.com/Matkurban/fpack/main/schema/fpack.schema.json
app:
display_name: XueHua IM
publisher: XueHua Tech # default: CompanyName in windows/runner/Runner.rc
homepage: https://xuehua.example.com
license: MIT
build:
targets: [apk, aab, ipa, dmg] # built by `fpack build` without targets
flavor: prod
dart_define_from_file: [config/prod.json]
output:
dir: "dist/{version}{+build}"
names: { exe: "{app}-setup-{version}", web: "{app}-web" } # per-target names; {target} {date} also work
checksum_algorithm: sha512 # SHA512SUMS
hooks:
pre_build: [dart run build_runner build --delete-conflicting-outputs]
post_package: { dmg: ['./scripts/upload.sh "$FPACK_ARTIFACT"'] }
android:
split_per_abi: both
signing:
store_file: ~/keys/upload.jks
store_password: ${FPACK_ANDROID_KEYSTORE_PASSWORD}
key_alias: upload
v4: true # any of v1–v4 re-signs with apksigner; v4 also writes the .idsig
ios:
export_method: ad-hoc
team_id: ABCDE12345 # fpack then generates ExportOptions.plist
provisioning_profiles: { com.xuehua.im: XueHua AdHoc }
macos:
sign:
identity: "Developer ID Application: XueHua Tech (ABCDE12345)"
notary_profile: XueHua # or notary_api_key/_id/_issuer, or notary_apple_id/_team_id/_password
notarize:
wait: true # false: submit without waiting (staple later with fpack notarize finish)
dmg: { background: macos/dmg/bg.png, window_size: [660, 400], format: ULFO }
pkg: { min_os: "11.0", postinstall: macos/installer/postinstall.sh }
windows:
inno_setup: { languages: [zh-CN, en], privileges: ask, desktop_icon: checked }
sign: { certificate: C:/certs/codesign.pfx, password: ${WINDOWS_CERT_PASSWORD} }
msix: { publisher: "CN=XueHua Tech", capabilities: [internetClient] }
linux:
categories: [Network, InstantMessaging]
metainfo: linux/com.xuehua.im.metainfo.xml
deb: { depends: ["libgtk-3-0 | libgtk-3-0t64"], postinst: linux/postinst }
rpm: { requires: [gtk3], license: MIT }
web:
base_href: /app/
source_maps: true
Hooks (hooks): pre_build (once before the first flutter build; a failure stops the run), pre_package.<target> / post_package.<target> (around packaging, with FPACK_TARGET, FPACK_ARTIFACT, FPACK_ARTIFACTS), post_build (once at the end, with FPACK_ARTIFACTS, FPACK_SUCCESS = 1/0; it also runs after build failures, but is skipped when every target already failed its checks, e.g. a missing tool or unknown flavor). All hooks run in the project root through sh -c (Windows: cmd /S /C, so quoted paths work as typed) with FPACK_PROJECT_ROOT, FPACK_OUTPUT_DIR, FPACK_VERSION, FPACK_BUILD_NUMBER, FPACK_MODE, FPACK_FLAVOR; write $VAR (Windows cmd: %VAR%) for variables that only exist while the hook runs (${VAR} is expanded when the configuration is loaded). A failing hook means exit code 1.
Signing #
Android signing #
fpack does not edit Gradle files. It signs release builds through the Android Gradle plugin's standard android.injected.signing.* properties (passed as ORG_GRADLE_PROJECT_* environment variables); passwords never appear on the command line, in logs or in --dry-run output (shown as ***).
export FPACK_ANDROID_KEYSTORE=~/keys/upload-keystore.jks
export FPACK_ANDROID_KEYSTORE_PASSWORD='…'
export FPACK_ANDROID_KEY_ALIAS=upload
export FPACK_ANDROID_KEY_PASSWORD='…' # defaults to the keystore password
fpack build apk aab
- In CI, put the base64 of the keystore into
FPACK_ANDROID_KEYSTORE_BASE64; fpack writes it tobuild/fpack/secrets/(mode 0600) and deletes it after the build. - Before building,
keytoolchecks the password and alias and tells you which one is wrong. - After building,
apksigner/keytool -printcertverify the artifacts and show the signer; a remaining Android Debug certificate triggers a warning. - If the project already has its own
key.properties+signingConfigs, just don't configure fpack signing and the project's setup is used. - Without signing configured and with release using the debug key (the
flutter createdefault) fpack notes: fine for testing, Google Play will reject it.
iOS export and signing #
fpack build ipa # Flutter default (App Store Connect)
fpack build ipa --export-method ad-hoc
fpack build ipa --export-options-plist ios/ExportOptions.plist
fpack build ipa --no-codesign # unsigned IPA for re-signing later
doctor checks Xcode, CocoaPods (when there is a Podfile), the project's DEVELOPMENT_TEAM and signing certificates; on failure the key Xcode errors (certificates, provisioning profiles, team, pods…) are translated into concrete fixes.
macOS signing and notarization #
For distribution outside the App Store (Developer ID). Enabled only through fpack's own settings (low → high precedence):
macos.signinfpack.yaml(fpack initwrites a commented section with placeholders and lists the Developer ID certificates found in your keychain)- environment variables
FPACK_MACOS_SIGN,FPACK_MACOS_SIGN_IDENTITY,FPACK_MACOS_INSTALLER_IDENTITY,FPACK_MACOS_NOTARIZE,FPACK_MACOS_NOTARY_PROFILE - flags
--sign/--no-sign,--sign-identity,--installer-identity,--notarize/--no-notarize,--notary-profile
macos:
sign:
identity: "Developer ID Application: Your Name (TEAMID)" # turns signing on
notary_profile: XueHua # turns notarization on
One notary credential (highest priority first): keychain profile notary_profile (recommended locally) → App Store Connect API key notary_api_key + notary_api_key_id (+ notary_api_issuer, recommended for CI) → Apple ID notary_apple_id + notary_team_id + notary_password (app-specific password, redacted in logs). Signing uses the Hardened Runtime by default (hardened_runtime, required for notarization).
Rules: an identity turns signing on; any notary credential turns notarization on; turning signing off also turns notarization off; --sign without an identity picks the first "Developer ID Application" certificate in the keychain; --notarize (or notarize: true) without any credential uses the keychain profile NotaryProfile; a missing identity lists the available ones. With nothing configured the .app keeps the Xcode project's own signature, the DMG is unsigned, and the artifact notes explain how to configure signing. fpack does not read other tools' settings in pubspec.yaml (such as the dmg: section of the dmg package).
DMG flow: copy the .app → codesign --deep the nested code → re-sign the app with the Hardened Runtime + macos/Runner/Release.entitlements → verify → hdiutil (or create-dmg, with background image, window size/position, icon size and positions, volume icon, license; format/filesystem work with both tools) builds the DMG → sign the DMG → xcrun notarytool submit → wait for the result → stapler staple → spctl assessment.
Waiting for notarization: Apple usually needs a few minutes. As soon as the upload has a submission ID, fpack writes NOTARIZATION.md (language follows --lang) and notarization.json into the output directory: artifact path and SHA-256, submission ID, submit time, the credential used (keychain profile name / API key ID / Apple ID — never a password) and copy-paste commands (xcrun notarytool info/wait/log …, xcrun stapler staple/validate …, spctl -a -vv …, fpack notarize status/finish), and prints the file's path. While waiting it shows the elapsed time and the hint: Notarization usually takes a few minutes. Ctrl-C stops waiting — Apple keeps processing; query it later with the commands in NOTARIZATION.md. The file is updated with the final state (Accepted / Invalid); on rejection fpack downloads Apple's log (notary-log-<file>.json), summarizes the issues and suggests fixes.
- No waiting:
macos.notarize.wait: false(orFPACK_NOTARIZE_WAIT=false,--notarize-no-wait) submits, writes the record and finishes; the summary and--jsonreport the artifact's notarization state assubmitted(not stapled yet). - Continue later:
fpack notarize status [dir|ID]asks Apple about all pending submissions;fpack notarize finish [dir]waits for the results, staples accepted DMGs/pkgs (an app zip is unpacked, the .app stapled and re-zipped), rewritesSHA256SUMSand updates the record; rejected ones get their log downloaded and summarized. Also works after a Ctrl-C.
How to obtain the certificates and notary credentials is described in the next section, Getting macOS signing certificates and notarization credentials.
fpack build macos produces a signed zip; with notarization on, the zip is notarized too (submit the zip → staple the .app → re-zip).
Getting macOS signing certificates and notarization credentials #
Distributing a macOS app outside the App Store needs a Developer ID Application certificate (signs the .app / zip / DMG), optionally a Developer ID Installer certificate (signs the pkg), and one kind of notarization credential. All of this requires a paid Apple Developer Program membership, and Developer ID certificates can only be created by the team's Account Holder.
1. Create the Developer ID certificates (on a Mac)
-
Xcode → Settings… → Accounts → select the team → Manage Certificates… → + → Developer ID Application (and Developer ID Installer if you build pkgs). Certificate and private key go straight into the login keychain.
-
Or on the web: Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority… (save the CSR to disk) → developer.apple.com/account/resources/certificates → + → Developer ID Application / Developer ID Installer → upload the CSR → download the
.cerand double-click it — on the Mac that created the CSR, which holds the private key. -
Check:
security find-identity -v -p codesigning # "Developer ID Application: Your Name (ABCDE12345)" security find-identity -v -p basic # also lists "Developer ID Installer: …" -
Tell fpack (use the quoted names exactly as printed; the 10 characters in parentheses are your Team ID):
Purpose fpack.yaml Environment Flag App signing identity macos.sign.identityFPACK_MACOS_SIGN_IDENTITY--sign-identitypkg signing identity macos.sign.installer_identityFPACK_MACOS_INSTALLER_IDENTITY--installer-identity
2. Notarization credentials (pick one)
a) Keychain profile (recommended on your own Mac): Apple ID + app-specific password + Team ID, stored once.
-
account.apple.com → Sign-In and Security → App-Specific Passwords → generate one (looks like
abcd-efgh-ijkl-mnop). -
Team ID: developer.apple.com/account → Membership details, or the 10 characters in the certificate name.
-
Store it in the keychain (omit
--passwordto be prompted):xcrun notarytool store-credentials fpack-notary \ --apple-id you@example.com --team-id ABCDE12345 --password abcd-efgh-ijkl-mnop xcrun notarytool history --keychain-profile fpack-notary # check -
fpack:
macos.sign.notary_profile: fpack-notary(orFPACK_MACOS_NOTARY_PROFILE,--notary-profile).
Without a stored profile you can pass the Apple ID directly: notary_apple_id / notary_team_id / notary_password (FPACK_NOTARY_APPLE_ID, FPACK_NOTARY_TEAM_ID, FPACK_NOTARY_PASSWORD). Keep the password in an environment variable or CI secret; fpack redacts it in logs, --dry-run output and NOTARIZATION.md.
b) App Store Connect API key (recommended for CI): not tied to a person's Apple ID, revocable at any time.
-
App Store Connect → Users and Access → Integrations → App Store Connect API → Team Keys → +, access Developer (or higher).
-
Download
AuthKey_<KEY_ID>.p8— it can be downloaded only once, keep it safe — and note the Key ID and the Issuer ID shown on the page (individual keys have no issuer; leave it out). -
fpack:
Value fpack.yaml Environment path of the .p8filemacos.sign.notary_api_keyFPACK_NOTARY_API_KEYKey ID macos.sign.notary_api_key_idFPACK_NOTARY_API_KEY_IDIssuer ID macos.sign.notary_api_issuerFPACK_NOTARY_API_ISSUERCheck:
xcrun notarytool history --key AuthKey_ABC123DEF4.p8 --key-id ABC123DEF4 --issuer <Issuer ID>.
Priority: notary_profile → API key → Apple ID; any of them turns notarization on.
3. Export the certificates (.p12) for CI
-
Keychain Access → login keychain → My Certificates → select "Developer ID Application: …" (expand it: the private key must be underneath) → File → Export Items… → format Personal Information Exchange (.p12) → choose an export password. Repeat for "Developer ID Installer: …" if you build pkgs (or select both and export them into one .p12).
-
Base64-encode it into CI secrets (e.g.
MACOS_CERTS_P12_BASE64,MACOS_CERTS_P12_PASSWORD), the API key likewise (NOTARY_API_KEY_P8_BASE64):base64 -i DeveloperID.p12 | pbcopy base64 -i AuthKey_ABC123DEF4.p8 | pbcopy -
Import them into a temporary keychain on the runner (GitHub Actions):
- name: import Developer ID certificates env: P12_BASE64: ${{ secrets.MACOS_CERTS_P12_BASE64 }} P12_PASSWORD: ${{ secrets.MACOS_CERTS_P12_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} # any random string API_KEY_BASE64: ${{ secrets.NOTARY_API_KEY_P8_BASE64 }} run: | KEYCHAIN="$RUNNER_TEMP/signing.keychain-db" echo "$P12_BASE64" | base64 --decode > "$RUNNER_TEMP/certs.p12" security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" security set-keychain-settings -lut 21600 "$KEYCHAIN" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" security import "$RUNNER_TEMP/certs.p12" -k "$KEYCHAIN" -P "$P12_PASSWORD" \ -T /usr/bin/codesign -T /usr/bin/productbuild -T /usr/bin/pkgbuild security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"') security find-identity -v -p codesigning "$KEYCHAIN" echo "$API_KEY_BASE64" | base64 --decode > "$RUNNER_TEMP/AuthKey.p8" rm "$RUNNER_TEMP/certs.p12" - name: fpack build macos dmg pkg env: FPACK_MACOS_SIGN_IDENTITY: "Developer ID Application: Your Name (ABCDE12345)" FPACK_MACOS_INSTALLER_IDENTITY: "Developer ID Installer: Your Name (ABCDE12345)" FPACK_NOTARY_API_KEY: ${{ runner.temp }}/AuthKey.p8 FPACK_NOTARY_API_KEY_ID: ${{ secrets.NOTARY_API_KEY_ID }} FPACK_NOTARY_API_ISSUER: ${{ secrets.NOTARY_API_ISSUER }} run: fpack build macos dmg pkg - name: clean up keychain if: always() run: security delete-keychain "$RUNNER_TEMP/signing.keychain-db" || trueset-key-partition-listlets codesign / productbuild use the private key without a prompt;-lut 21600keeps the keychain unlocked for 6 hours. Never commit certificates, .p12/.p8 files or passwords.
macOS installer (pkg) #
fpack build pkg uses pkgbuild to create a component package that installs the app into /Applications (not relocatable, so upgrades always replace the copy in /Applications), then productbuild to create a distribution package (Apple silicon and Intel, no Rosetta prompt). macos.pkg sets identifier, install_location, title and the installer's welcome / readme / license / conclusion pages (.html/.rtf/.txt) and background (image).
- Signing: a pkg needs a separate "Developer ID Installer" certificate (not the "Developer ID Application" one that signs the app), set with
macos.sign.installer_identity/FPACK_MACOS_INSTALLER_IDENTITY/--installer-identity; the signed pkg is checked withpkgutil --check-signature. Without it an unsigned pkg is produced with a note;--no-signturns pkg signing off as well. - App signing: with
macos.sign.identityset, the app inside the pkg is re-signed with Developer ID just like for the zip/DMG. - Notarization: shares the DMG settings; only signed pkgs are submitted and stapled, then assessed with
spctl --assess --type install. - More options:
min_os(refuse to install below this version; default is the project'sMACOSX_DEPLOYMENT_TARGET),require_restart,relocatable,preinstall/postinstallscripts (made executable automatically),version. doctorchecks pkgbuild/productbuild and lists the installer certificates in the keychain.
Windows signing and installers #
Code signing is optional. Choose one certificate source in windows.sign:
- a
.pfxfile:certificate+password(FPACK_WINDOWS_CERTIFICATE/FPACK_WINDOWS_CERTIFICATE_PASSWORD; the password is never printed), - the certificate store:
thumbprint(/sha1) orsubject(/n), withstore(/s, defaultMy) andmachine_store: true(/sm), - a signtool plugin for cloud signing, e.g. Azure Trusted Signing:
dlib+dlib_metadata(/dlib//dmdf).
Right after flutter build windows, fpack signs files of the Release folder (default *.exe and *.dll), so the portable zip, the installer and the MSIX all contain signed binaries. The Inno Setup installer and uninstaller are signed through SignTool=; the MSIX is signed by msix:create with the same certificate (with dlib, fpack signs the finished MSIX). Every signature uses SHA-256 and an RFC 3161 timestamp (timestamp_url, default http://timestamp.digicert.com), with description (/d) and url (/du); afterwards signtool verify /pa checks it (verify: false turns that off; a failure, e.g. a self-signed test certificate, is only a warning). signtool is found on PATH or in the newest Windows SDK (or set signtool); fpack doctor reports it and how to install the SDK (winget install Microsoft.WindowsSDK.10.0.26100).
windows:
sign:
certificate: ${WINDOWS_PFX} # or thumbprint: / subject: / dlib:
password: ${WINDOWS_PFX_PASSWORD}
url: https://example.com
Inno Setup installers support publisher/URL, copyright, install directory, Start menu group, desktop shortcut, license/info pages, icon and wizard images, minimum Windows version, privileges (user without admin / admin / ask) and multiple languages (languages: [zh-CN, en], the first is the default; fpack ships the Chinese language file older Inno Setup versions lack). MSIX display name, publisher, capabilities, file associations, protocols, version etc. can be set directly in windows.msix and take precedence over pubspec's msix_config.
Linux packages #
deb/rpm/AppImage install into linux.prefix (default /opt/<package>, with a symlink in /usr/bin); the .desktop file has Name/GenericName/Comment/Categories/Keywords/MimeType/StartupWMClass, the icon is scaled into the hicolor theme per icon_sizes, and an AppStream metainfo and copyright can be included. deb supports Depends/Recommends/Suggests/Conflicts/Section/Priority and maintainer scripts, rpm supports Requires/Group/License/URL and %pre/%post/%preun/%postun, and AppImage supports embedded update information (update_information, with the .zsync file written to the output directory).
The snap target packs the already built bundle with snapcraft. fpack generates snap/snapcraft.yaml (base core24, strict confinement, the gnome extension for GTK, plugs desktop, desktop-legacy, wayland, x11, opengl, audio-playback, network, home, and a dump part over the bundle, so Flutter is not rebuilt inside snapcraft) plus a .desktop file and the icon in snap/gui/. Name, summary, description, base, grade, confinement, license, plugs, extensions, stage packages, extra apps and the .desktop file can be set under linux.snap; linux.snap.template uses your own snapcraft.yaml instead. linux.snap.build_mode (or FPACK_SNAP_BUILD_MODE) chooses how snapcraft builds: destructive (directly on this machine, which must run the Ubuntu release of the base, 24.04 for core24; fastest, good for CI; snapcraft installs build dependencies here, so fpack runs it with sudo -n unless you are root: passwordless sudo as on CI, or run sudo -v first), lxd or multipass (isolated build environments). The default auto uses destructive on a matching Ubuntu and lxd elsewhere. Install snapcraft with sudo snap install snapcraft --classic, then test the snap with sudo snap install --dangerous <file>.snap. The snap contains the bundle Flutter built on this machine, so it is for this machine's architecture (amd64 or arm64), whatever the build mode: LXD and Multipass isolate the build but do not cross-compile Flutter. Build an arm64 snap on an arm64 Linux machine or runner (for example ubuntu-24.04-arm).
CI example #
jobs:
android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: subosito/flutter-action@v2
- run: dart pub global activate fpack 1.1.13 && echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH
- run: fpack build apk aab --split-per-abi=both
env:
FPACK_ANDROID_KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
FPACK_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
FPACK_ANDROID_KEY_ALIAS: upload
- uses: actions/upload-artifact@v7
with: { name: android, path: dist/ }
apple:
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- uses: subosito/flutter-action@v2
- run: dart pub global activate fpack 1.1.13 && echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH
- run: fpack build ipa dmg --json > result.json
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
- uses: subosito/flutter-action@v2
- run: dart pub global activate fpack 1.1.13
- run: fpack build windows exe
linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: subosito/flutter-action@v2
- run: sudo apt-get install -y ninja-build libgtk-3-dev rpm
- run: dart pub global activate fpack 1.1.13 && echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH
- run: fpack build linux deb rpm web
For signed + notarized macOS builds in CI, see the certificate import steps above.
Without a TTY, spinners and colors are turned off and a heartbeat line is printed every 60 seconds so CI does not time out on silence.
AI agent skill #
fpack ships a package skill, fpack-cli, that teaches AI coding agents (Cursor, Claude Code, Gemini, Copilot, Cline, Codex…) how to use fpack: install, init, doctor, build targets, config keys and FPACK_* variables, signing, macOS notarization, CI and troubleshooting. Install it into your project's agent folder:
dart pub add dev:fpack # skills are installed from dependencies
dart run skills@ get --package fpack # choose your agent, or add --agent cursor|claude|codex|…
dart run skills@ get --package fpack --skill fpack-cli # non-interactive (CI/scripts)
Re-run after upgrading fpack. Details: AI agent skill.
Environment variables #
Full details (type, default, affected targets): doc/configuration.md.
| Variable | Effect |
|---|---|
FPACK_FLUTTER |
Flutter SDK path |
FPACK_CONFIG |
configuration file path |
FPACK_MODE FPACK_FLAVOR FPACK_ENTRY FPACK_BUILD_NAME FPACK_BUILD_NUMBER |
build parameters |
FPACK_OUTPUT_DIR FPACK_OVERWRITE FPACK_SPLIT_PER_ABI FPACK_OBFUSCATE |
output / Android / obfuscation |
FPACK_ANDROID_KEYSTORE FPACK_ANDROID_KEYSTORE_BASE64 FPACK_ANDROID_KEYSTORE_PASSWORD FPACK_ANDROID_KEY_ALIAS FPACK_ANDROID_KEY_PASSWORD |
Android signing |
FPACK_IOS_EXPORT_METHOD FPACK_IOS_EXPORT_OPTIONS_PLIST FPACK_IOS_CODESIGN |
iOS export |
FPACK_MACOS_SIGN FPACK_MACOS_SIGN_IDENTITY FPACK_MACOS_INSTALLER_IDENTITY FPACK_MACOS_NOTARIZE FPACK_MACOS_NOTARY_PROFILE FPACK_NOTARIZE_WAIT FPACK_DMG_TOOL |
macOS |
FPACK_NOTARY_APPLE_ID FPACK_NOTARY_TEAM_ID FPACK_NOTARY_PASSWORD FPACK_NOTARY_API_KEY FPACK_NOTARY_API_KEY_ID FPACK_NOTARY_API_ISSUER |
macOS notary credentials (Apple ID / API key) |
FPACK_WINDOWS_CERTIFICATE FPACK_WINDOWS_CERTIFICATE_PASSWORD FPACK_WINDOWS_CERT_THUMBPRINT FPACK_WINDOWS_CERT_SUBJECT FPACK_WINDOWS_SIGN_DLIB FPACK_WINDOWS_SIGN_DLIB_METADATA |
Windows code signing (.pfx, certificate store, or a signtool dlib such as Azure Trusted Signing) |
FPACK_LANG |
zh / en (default: the OS language – macOS UI language, Windows display language, Linux LC_ALL/LC_MESSAGES/LANGUAGE/LANG; Chinese → Chinese, anything else → English; details) |
NO_COLOR / FPACK_NO_COLOR / FORCE_COLOR |
colors |
FPACK_CORE |
use this native core binary (development) |
FPACK_HOME |
core cache directory |
FPACK_REBUILD=1 |
build the core with local Go first (after changing the Go sources) |
FPACK_GO |
the Go used for the fallback build (none disables local builds) |
FPACK_NO_DOWNLOAD=1 / FPACK_DOWNLOAD_URL |
never download (build locally) / download mirror (intranet); see how the launcher finds the native core |
Exit codes #
| Code | Meaning |
|---|---|
| 0 | success (--dry-run: the plan was produced) |
| 1 | at least one target failed to build/package, or a hook failed; fpack doctor: problems were found; fpack notarize: a submission was rejected or a query failed |
| 2 | usage or configuration error (unknown flag or target, no targets and no build.targets, invalid fpack.yaml, no Flutter project found) |
| 3 | prerequisites missing / artifact already exists / nothing buildable / Flutter SDK not found; fpack notarize: no notarization record found |
| 130 | interrupted by Ctrl-C (an interrupted notarization wait keeps processing on Apple's side) |
With --json, stdout carries only the JSON result (targets, states, artifact paths, sizes, SHA-256/512, notarization state notarization.state, durations, error excerpts, fixes); all human-readable output goes to stderr.
FAQ #
How do I read a failed build? fpack shows the key error excerpt (Gradle's "What went wrong", Xcode error lines, …), one concrete fix, and the full log path build/fpack/logs/…. Add -v to stream the full output.
A path dependency is missing (e.g. xue_hua_sdk: path: ../xue_hua_sdk): fpack checks before building and names the missing package; put it at the expected location relative to the project.
macOS says the developer cannot be verified: fpack removes the quarantine attribute when it installs the core binary; if it is still blocked, run FPACK_REBUILD=1 fpack --version to build it with local Go.
With a local path install every run starts with "Resolving dependencies…": that is pub's fixed behavior for path-activated packages (git / pub.dev installs don't do it). If another program consumes the --json output, install from git or pub.dev, or filter those lines.
Will it change my project? No. fpack only reads project files; flutter_launcher_icons.yaml, flutter_native_splash.yaml, distribute_options.yaml, package_rename_config.yaml, pubspec.yaml and the like are never modified. Consider adding dist/ to .gitignore.
Architecture #
fpack/
bin/fpack.dart Dart entry point (pub executable)
lib/src/ launcher: platform detection, core resolution (cache → bundled → download → local go build), SHA-256, signal forwarding
go/ native core (Go, dependencies vendored)
cmd/fpack-core/ main
internal/cli argument parsing, commands, help
internal/build orchestration: classify → preflight → shared flutter steps → package → checksums → summary
internal/targets per-target preflight, flutter args, artifact lookup, packaging commands (dry-run and real run share the code)
internal/config key registry (drives env vars, validation, JSON Schema, init template and doc tables), fpack.yaml parsing
internal/project pubspec / Gradle / Xcode project info (read-only)
internal/flutter Flutter SDK lookup (FVM / PATH / …)
internal/runner subprocesses (process groups, Ctrl-C, logs, secret redaction)
internal/hints common errors → fixes
internal/ui, i18n, pack, host, doctor, version
schema/ fpack.schema.json (generated from the key registry)
prebuilt/ prebuilt binaries + manifest.json (release artifacts, not in git)
scripts/ build_binaries.sh, check_versions.sh, package_dist.sh, e2e_*.py + fake-apple/ (E2E)
website/ documentation site (Jaspr; generated tables come from the Go tests)
.github/workflows/ ci.yml (tests + lint), e2e.yml, docs.yml, release.yml
Development and releases #
cd go && go vet ./... && go test ./... # Go core
FPACK_UPDATE=1 go test ./internal/config # after changing the key registry: regenerate the schema and doc/configuration.md tables
dart pub get && dart analyze && dart test # Dart launcher
scripts/build_binaries.sh # cross-compile 6 platforms → prebuilt/
scripts/package_dist.sh ../fpack-dist.tar.gz # self-contained offline bundle
FPACK_CORE=$PWD/prebuilt/darwin-arm64/fpack-core fpack doctor # debug with a specific core
Releasing: bump the version in pubspec.yaml, lib/src/version.dart and go/internal/version/version.go together (scripts/check_versions.sh checks them), update CHANGELOG.md, push a vX.Y.Z tag. release.yml cross-compiles, creates the GitHub Release (binaries + checksums.txt + offline bundle) and optionally publishes to pub.dev.
License #
MIT