Flutter Local Build
Flutter Local Build is a command-line tool for building and distributing Flutter applications from a developer machine or CI runner. It supports ordinary single-app repositories, flavor-based apps, and multi-app monorepos from one YAML configuration.
It can:
- build Android App Bundles and iOS IPA files with Flutter or FVM;
- create Shorebird releases and patches;
- resolve the next build number from local pubspecs, Google Play, and App Store Connect;
- upload artifacts to Google Drive, Google Play, TestFlight, and Firebase App Distribution; and
- report successful and failed jobs through Slack and SMTP.
Requirements
- Dart 3.6 or later;
- a Flutter SDK available through
flutterCommand; - Xcode on macOS for iOS builds and TestFlight uploads; and
- the Shorebird CLI on
PATHwhen using Shorebird. - the Firebase CLI on
PATH(or configured throughcommand) when using Firebase App Distribution.
Provider accounts and credentials are only required for publishers you configure and use.
Install
dart pub global activate local_build
local_build --help
If local_build is not found, add Dart's global executable directory to your
PATH. Dart prints the appropriate directory when activating the package.
Update or remove the CLI with:
dart pub global activate local_build
dart pub global deactivate local_build
Quick start
Create .local-build.yaml next to the Flutter application's pubspec.yaml:
version: 1
apps:
app:
path: .
androidPackage: com.example.app
iosBundleId: com.example.app
profiles:
development:
flavor: development
target: lib/main_development.dart
dartDefines: config/development.json
production:
flavor: production
target: lib/main_production.dart
obfuscate: true
splitDebugInfo: build/symbols
extraArgs: [--tree-shake-icons]
androidArgs: [--split-per-abi]
iosArgs: [--export-method, app-store]
versioning:
buildNumber:
strategy: pubspec
Preview the production jobs and generated commands:
local_build build --profile production --dry-run
Build both configured platforms:
local_build build --profile production --platform all --build-name 1.4.0
Commands
local_build build --profile <name> [options]
local_build patch --profile <name> [options]
build runs flutter build, or shorebird release when
--enable-shorebird is supplied. patch always runs shorebird patch.
Command options
| Option | Required | Default | Description |
|---|---|---|---|
--config <path> |
No | .local-build.yaml |
YAML configuration file. Relative paths are resolved from the current shell directory. |
--profile <name> |
Yes | None | Root profile to use. The name must exist under profiles. |
--app <name> |
No | All apps | Select an app declared under apps. Repeat the option to select multiple apps. |
--platform <value> |
No | all |
Build android, ios, or all. Jobs are only created where the selected app has the corresponding store identifier. |
--android-artifact <value> |
No | aab |
Build Android as an App Bundle (aab) or Android Package (apk). The selected file is passed to Google Drive, Firebase App Distribution, and Play Console. Shorebird Android requires aab. |
--build-name <version> |
No | Flutter project value | Pass a semantic version such as 1.4.0 to Flutter or Shorebird as --build-name. Flutter Local Build does not calculate build names. |
--build-number <number> |
No | Resolved by configuration | Positive integer override. It takes precedence over versioning.buildNumber.strategy. |
--enable-shorebird |
No | Off | Use shorebird release for a build command. It does not change a patch command, which already uses Shorebird. |
--publish |
No | Off | Run every configured publisher that supports each successfully built artifact. |
--firebase-group-aliases <alias> |
No | Publisher setting | Override publishers.firebaseAppDistribution.groupAliases for this invocation. Repeat it or provide comma-separated aliases. |
--no-notify |
No | Off | Disable all configured Slack and SMTP notifications for this invocation. |
--dry-run |
No | Off | Print build, publishing, and notification plans without executing those actions. See Dry-run behavior. |
-h, --help |
No | Off | Print top-level usage and examples. |
Examples:
# Build and publish an APK for one Android app.
local_build build --profile production --app customer --platform android \
--android-artifact apk --publish
# Build and publish all eligible applications and platforms.
local_build build --profile production --publish
# Create a Shorebird Android release.
local_build build --profile production --platform android --enable-shorebird
# Patch one iOS application through Shorebird.
local_build patch --profile production --app customer --platform ios
Configuration file
The configuration root must be a YAML map. Paths in the following sections are relative to the directory containing the configuration unless noted otherwise.
Root fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
version |
Integer | Yes | None | Configuration schema version. The only supported value is 1. |
flutterCommand |
List of strings | No | [flutter] |
Executable and any fixed arguments used for non-Shorebird builds. It must contain at least one item. |
apps |
Map | Yes | None | Named application definitions. At least one app is required. |
profiles |
Map | Yes | None | Named shared build profiles. --profile selects one of these names. |
versioning |
Map | No | {} |
Automatic versioning settings. Currently only buildNumber is read. |
publishers |
Map | No | {} |
Artifact-publisher settings for Google Drive, Play Console, TestFlight, and Firebase App Distribution. |
notifiers |
Map | No | {} |
Slack and SMTP notification settings. Each notifier is enabled unless its enabled field is false. |
Use FVM by changing the Flutter command:
flutterCommand: [fvm, flutter]
flutterCommand is ignored for Shorebird jobs; those invoke the shorebird
executable directly.
apps
Each key under apps is the name accepted by --app.
apps:
customer:
path: apps/customer
androidPackage: com.example.customer
iosBundleId: com.example.customer
firebaseAndroidAppId: 1:1234567890:android:abcdef
firebaseIosAppId: 1:1234567890:ios:abcdef
| App field | Type | Required | Default | Description |
|---|---|---|---|---|
path |
String | No | . |
Flutter application directory, relative to the configuration file. Build commands run from this directory. |
androidPackage |
String | For Android jobs | None | Google Play package name, for example com.example.customer. It may instead come from the selected root or app-specific profile. |
iosBundleId |
String | For iOS jobs | None | App Store bundle identifier. It may instead come from the selected root or app-specific profile. |
firebaseAndroidAppId |
String | For Android Firebase uploads | None | Firebase Android App ID. This is the value shown in Firebase project settings, not the Android package name. |
firebaseIosAppId |
String | For iOS Firebase uploads | None | Firebase iOS App ID. This is the value shown in Firebase project settings, not the bundle identifier. |
profiles |
Map | No | {} |
App-specific overrides keyed by a root profile name. These use the same fields as a root profile. |
An app may intentionally support only one platform. The selected profile is merged before platform eligibility is checked, so identifiers can live at the app or profile level. If the effective configuration has no identifier for a selected platform, that app/platform is skipped. If the complete selection produces no jobs, the command fails with a configuration error.
profiles
A root profile describes the arguments shared by every selected app. All profile fields are optional.
profiles:
production:
flavor: production
target: lib/main_production.dart
dartDefines:
API_URL: https://api.example.com
ENABLE_LOGGING: false
obfuscate: true
splitDebugInfo: build/symbols
| Profile field | Type | Default | Generated argument / behavior |
|---|---|---|---|
flavor |
String | None | --flavor <value> |
target |
String | Flutter default | --target <value>; normally an app-relative Dart entrypoint. |
dartDefines |
Map, string, or list of strings | None | Explicit --dart-define values or one or more --dart-define-from-file paths. |
obfuscate |
Boolean | false |
Adds --obfuscate only when the YAML value is exactly true. |
splitDebugInfo |
String | None | --split-debug-info=<path>; typically used with obfuscate. |
extraArgs |
String or list of strings | None | Additional arguments appended to both Flutter and Shorebird commands. A string is exactly one argument; use a list to preserve multiple argument boundaries. |
androidArgs |
String or list of strings | None | Additional arguments appended only to Android commands. |
iosArgs |
String or list of strings | None | Additional arguments appended only to iOS commands. |
androidPackage |
String | App value | Overrides the app's package name for this profile. It affects job selection, store lookup, and Google Play publishing, not the Flutter command itself. |
iosBundleId |
String | App value | Overrides the app's bundle identifier for this profile. It affects job selection, store lookup, and TestFlight publishing, not the Flutter command itself. |
firebaseAndroidAppId |
String | App value | Overrides the Firebase Android App ID for this profile or tenant. |
firebaseIosAppId |
String | App value | Overrides the Firebase iOS App ID for this profile or tenant. |
Paths passed as profile arguments are interpreted by Flutter or Shorebird from the app's working directory.
Pass-through arguments are supplied directly to the child process without
shell parsing or evaluation. Write a flag and its separate value as two list
items, or use the tool's --flag=value form:
profiles:
production:
extraArgs:
- --tree-shake-icons
androidArgs:
- --analyze-size
- --code-size-directory
- build/code-size/android
iosArgs:
- --export-method=app-store
extraArgs are appended after Flutter Local Build's generated arguments,
followed by the matching platform-specific arguments. The same arguments are
also passed to shorebird release and shorebird patch, so only configure
flags accepted by every action that uses that profile. Use androidArgs and
iosArgs when a flag is platform-specific.
App-specific profile overrides
Flavor-specific store identifiers and settings can be placed below an app:
apps:
customer:
path: apps/customer
androidPackage: com.example.customer
iosBundleId: com.example.customer
profiles:
development:
androidPackage: com.example.customer.dev
iosBundleId: com.example.customer.dev
dartDefines:
TENANT: customer
An override is applied only when its key matches the selected root profile.
Scalar values replace their root-profile value. Explicit Dart-define maps are
merged, with app values winning duplicate keys. Define-file lists are appended.
extraArgs, androidArgs, and iosArgs are also appended, with shared-profile
arguments first and app-specific arguments last.
obfuscate: true in either profile enables obfuscation; an app override cannot
turn off obfuscation already enabled by the root profile.
Dart defines
dartDefines accepts exactly one of three shapes in each profile.
Explicit key-value pairs
dartDefines:
API_URL: https://dev.example.com
ENABLE_LOGGING: true
RETRY_COUNT: 3
Keys and values are converted to strings and emitted as:
--dart-define=API_URL=https://dev.example.com
--dart-define=ENABLE_LOGGING=true
--dart-define=RETRY_COUNT=3
One define file
dartDefines: config/development.json
Multiple define files
dartDefines:
- config/common.json
- config/development.env
Each path must be a string and is emitted as
--dart-define-from-file=<path>. The file contents and supported formats are
interpreted by the installed Flutter or Shorebird version.
One profile node cannot directly mix a map and file list because YAML gives
dartDefines one value. A shared root profile and an app-specific override can
combine them. Define files are emitted first and explicit values afterward, so
explicit values take precedence when Flutter encounters duplicate keys.
Automatic build numbers
Configure build-number resolution under versioning.buildNumber:
versioning:
buildNumber:
strategy: max-stores-plus-one
fallback: pubspec
| Field | Accepted values | Default | Description |
|---|---|---|---|
strategy |
manual, pubspec, stores, max-stores-plus-one |
pubspec |
Selects how the next number is obtained. stores and max-stores-plus-one are aliases. |
fallback |
pubspec or fail |
fail |
Store strategies use local pubspecs if set to pubspec; otherwise lookup errors stop the command. Values other than pubspec behave as fail. |
Strategy behavior:
manualrequires--build-number.pubspecreadsversion: <name>+<number>from every selected app'spubspec.yaml, takes the highest number, and adds one. Missing files or versions do not contribute; if none contain a number, the result is1.storesandmax-stores-plus-oneinspect every selected app/platform, calculate one global maximum, and add one. This prevents collisions across a multi-app release.
Google Play lookup checks uploaded bundles and releases in all tracks. App
Store Connect lookup finds the app by bundle identifier and checks all paginated
build records. The relevant playConsole or testFlight publisher must be
configured for every selected platform.
An explicit --build-number always wins and must be a positive integer.
Shorebird patch jobs skip build-number resolution.
Publishers
Artifact publishers are optional and live under publishers.
Merely configuring a publisher does not upload anything; publication also
requires --publish. Slack and SMTP live separately under the root-level
notifiers section and run by default unless --no-notify is supplied.
Every publisher and notifier accepts enabled, a boolean that defaults to
true:
publishers:
playConsole:
enabled: false
serviceAccountEnv: LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT
track: internal
When enabled: false, Flutter Local Build ignores that publisher or notifier
completely. It does not create its publisher/notifier, read its credentials, or
use it as a store build-number source. This allows configuration to remain in
the file while being temporarily inactive. Omitting enabled is equivalent to
enabled: true.
Google service-account credentials
Google Drive and Google Play share the same credential format:
serviceAccountEnv: LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT
The field names an environment variable—it is not the credential itself. The
variable may contain the service-account JSON text or a path to its JSON file.
Use an absolute file path in CI and when running the command outside the config
directory. The default variable name is
LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT.
The service account must have access to the target Drive folder or Play Console application as appropriate.
publishers.googleDrive
Uploads every successful Android and iOS artifact to one Drive folder.
publishers:
googleDrive:
enabled: true
serviceAccountEnv: LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT
folderId: 1AbCdEfGhIjKlMnOp
| Field | Required | Default | Description |
|---|---|---|---|
enabled |
No | true |
Set to false to ignore this publisher, including its credentials and uploads. |
folderId |
Yes | None | Google Drive destination folder ID. The folder must be shared with the service account. |
serviceAccountEnv |
No | LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT |
Name of the environment variable containing Google credentials or a credentials-file path. |
The uploaded object keeps the artifact's filename.
publishers.playConsole
Supports Android artifacts only. It creates a Play edit, uploads the AAB/APK, assigns it to a track as a completed release, and commits the edit.
publishers:
playConsole:
enabled: true
serviceAccountEnv: LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT
track: internal
| Field | Required | Default | Description |
|---|---|---|---|
enabled |
No | true |
Set to false to ignore Play uploads and Google Play build-number lookup. |
serviceAccountEnv |
No | LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT |
Name of the environment variable containing Google credentials or a credentials-file path. |
track |
No | internal |
Google Play target track, such as internal, alpha, beta, or production. Custom track names are passed through unchanged. |
The Play package name comes from the selected app's effective
androidPackage, including any profile override.
The Play release name is <build-name>+<build-number>, such as
1.0.0+128. When --build-name is omitted, the name is read from that app's
pubspec.yaml, so each app in a monorepo keeps its own version name. The CLI
also adds an en-US release note in the form
Local <profile> build <build-name>+<build-number>.
publishers.testFlight
Supports iOS artifacts only and uploads through xcrun altool. Uploading
requires macOS and Xcode.
publishers:
testFlight:
enabled: true
keyIdEnv: APP_STORE_CONNECT_KEY_ID
issuerIdEnv: APP_STORE_CONNECT_ISSUER_ID
privateKeyEnv: APP_STORE_CONNECT_PRIVATE_KEY_PATH
| Field | Required | Default environment-variable name | Description |
|---|---|---|---|
enabled |
No | true |
Set to false to ignore TestFlight uploads and App Store build-number lookup. |
keyIdEnv |
No | APP_STORE_CONNECT_KEY_ID |
Environment variable containing the App Store Connect API key ID. |
issuerIdEnv |
No | APP_STORE_CONNECT_ISSUER_ID |
Environment variable containing the issuer ID. |
privateKeyEnv |
No | APP_STORE_CONNECT_PRIVATE_KEY_PATH |
Environment variable containing the private-key value described below. |
For TestFlight publication, privateKeyEnv must resolve to the path of the
App Store Connect .p8 file because its parent directory is supplied to
xcrun through API_PRIVATE_KEYS_DIR. An absolute path is recommended.
For App Store build-number lookup, the same variable may instead contain either the EC private-key PEM text or a path to the key. If the same configuration is used for both lookup and publication, use a file path so both operations work.
The effective iosBundleId selects the App Store Connect application during
build-number lookup. TestFlight upload itself uses the signed IPA metadata.
publishers.firebaseAppDistribution
Uploads Android AAB/APK and iOS IPA artifacts with the Firebase CLI. Install
the Firebase CLI and authenticate it, or provide a service-account credential
file. Each app (or its selected profile override) needs the matching
firebaseAndroidAppId or firebaseIosAppId.
apps:
customer:
path: apps/customer
androidPackage: com.example.customer
iosBundleId: com.example.customer
firebaseAndroidAppId: 1:1234567890:android:abcdef
firebaseIosAppId: 1:1234567890:ios:abcdef
publishers:
firebaseAppDistribution:
enabled: true
command: [npx, firebase]
serviceAccountEnv: LOCAL_BUILD_FIREBASE_SERVICE_ACCOUNT
groupAliases: [internal-testers, qa]
releaseNotesFile: release-notes/firebase.txt
testers: [developer@example.com]
testDevices:
- model=shiba,version=34,locale=en,orientation=portrait
testUsername: qa@example.com
testPasswordEnv: FIREBASE_TEST_PASSWORD
testUsernameResource: username
testPasswordResource: password
testNonBlocking: false
resultsBucket: gs://my-test-results
debug: false
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
Boolean | true |
Set to false to ignore Firebase uploads. |
command |
String or list | firebase |
Firebase CLI executable and optional fixed arguments. For example, [npx, firebase]. |
serviceAccountEnv |
String | GOOGLE_APPLICATION_CREDENTIALS |
Name of an environment variable containing the absolute path to a Google credential JSON file. The path is forwarded to the Firebase CLI as GOOGLE_APPLICATION_CREDENTIALS. If the default variable is unset, the Firebase CLI may use its existing login or Application Default Credentials. |
groupAliases |
String or list | None | Tester group aliases. A string is comma-separated. Overridden by --firebase-group-aliases. Cannot be combined with groupsFile. |
groupsFile |
String | None | Config-relative file containing comma-separated group aliases. Cannot be combined with groupAliases. |
testers |
String or list | None | Tester email addresses. A string is comma-separated. Cannot be combined with testersFile. |
testersFile |
String | None | Config-relative file containing comma-separated tester emails. Cannot be combined with testers. |
releaseNotes |
String | None | Inline release notes. Cannot be combined with releaseNotesFile. |
releaseNotesFile |
String | None | Config-relative plain-text release-notes file. Cannot be combined with releaseNotes. |
debug |
Boolean | false |
Enable verbose Firebase CLI output. |
testDevices |
String or list | None | Automated App Testing device specifications. Separate multiple inline specifications with semicolons or use a YAML list. Cannot be combined with testDevicesFile. |
testDevicesFile |
String | None | Config-relative file containing automated-test device specifications. Cannot be combined with testDevices. |
testUsername |
String | None | Username used for automatic login during an automated test. |
testPasswordEnv |
String | None | Environment-variable name containing the automated-test password. The secret is not read during a dry run. Cannot be combined with testPasswordFile. |
testPasswordFile |
String | None | Config-relative password file passed to Firebase. Cannot be combined with testPasswordEnv. |
testUsernameResource |
String | None | Android resource name of the login username field. |
testPasswordResource |
String | None | Android resource name of the login password field. |
testNonBlocking |
Boolean | false |
Start automated tests without waiting for their results. |
resultsBucket |
String | None | Google Cloud Storage bucket used for automated-test results. |
For a one-off group selection, the CLI value replaces the configured aliases:
local_build build --profile production --app customer --platform android \
--publish --firebase-group-aliases internal-testers,qa
File paths in this publisher are resolved relative to .local-build.yaml.
Direct and file forms of the same setting are mutually exclusive, so choose
one of releaseNotes/releaseNotesFile, testers/testersFile,
groupAliases/groupsFile, testDevices/testDevicesFile, and
testPasswordEnv/testPasswordFile.
Notifiers
Slack and SMTP configurations live under the root-level notifiers map, not
under publishers.
notifiers.slack
notifiers:
slack:
enabled: true
webhookEnv: LOCAL_BUILD_SLACK_WEBHOOK
| Field | Required | Default | Description |
|---|---|---|---|
enabled |
No | true |
Set to false to disable Slack without deleting its settings. |
webhookEnv |
No | LOCAL_BUILD_SLACK_WEBHOOK |
Name of the environment variable containing a Slack incoming-webhook URL. |
Slack sends one message after each successful job and one message for a failed job. A non-2xx webhook response fails the command.
notifiers.smtp
notifiers:
smtp:
enabled: true
events: both
host: smtp.example.com
port: 587
security: starttls
usernameEnv: LOCAL_BUILD_SMTP_USERNAME
passwordEnv: LOCAL_BUILD_SMTP_PASSWORD
from: builds@example.com
fromName: Flutter Local Build
to:
- developer@example.com
- release-team@example.com
cc: engineering@example.com
bcc: [audit@example.com]
subjectPrefix: "[Flutter Local Build]"
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled |
Boolean | No | true |
Set to false to disable SMTP without deleting its settings. No other SMTP fields are required while disabled. |
events |
String | No | both |
success, failure, both, or none. neither is accepted as an alias for none. |
host |
String | Yes when sending | None | SMTP server hostname. |
port |
Integer or numeric string | No | 587 |
SMTP port from 1 through 65535. |
security |
String | No | starttls |
starttls, ssl, or insecure. Use insecure only for a trusted local relay. |
usernameEnv |
String | No | No authentication | Name of the environment variable containing the SMTP username. |
passwordEnv |
String | No | No authentication | Name of the environment variable containing the SMTP password. |
from |
String | Yes when sending | None | Sender email address. |
fromName |
String | No | None | Display name for the sender. |
to |
String or list of strings | Yes when sending | None | At least one recipient. A string may contain comma-separated addresses. |
cc |
String or list of strings | No | None | Carbon-copy recipients. A string may contain comma-separated addresses. |
bcc |
String or list of strings | No | None | Blind-carbon-copy recipients. A string may contain comma-separated addresses. |
subjectPrefix |
String | No | None | Text placed before generated success/failure subjects. Blank text is ignored. |
SMTP sends plain-text messages. Success messages contain the app, platform,
profile, version values, and artifact path. Failure messages contain the job
identity and error. Fields marked "when sending" are not accessed for events
disabled by events or during a dry run.
Publishing behavior
With --publish, every applicable configured publisher runs in this order:
| Publisher | Android | iOS |
|---|---|---|
| Google Drive | Yes | Yes |
| Google Play | Yes | No |
| TestFlight | No | Yes |
| Firebase App Distribution | Yes | Yes |
Android format support is:
| Publisher | AAB | APK |
|---|---|---|
| Google Drive | Yes | Yes |
| Google Play | Yes | Yes |
| Firebase App Distribution | Yes | Yes |
--android-artifact selects one Android output for the invocation. The same
artifact is sent to all applicable enabled publishers; it does not build a
different format per publisher. The default is aab, which preserves prior
behavior. TestFlight is unaffected because iOS always produces an IPA.
If multiple publishers support an artifact, all of them run. Jobs are processed sequentially in configuration order, with Android before iOS for each app. A build, upload, or notification error stops the remaining work and returns a non-zero exit code.
Shorebird patches are delivered by Shorebird and cannot be combined with
--publish. Shorebird releases may use the configured artifact publishers.
Notifications
Slack and SMTP are independent. Both run when both are configured. Successful notifications are sent after publishing completes for an artifact. On a job error, failure notifications run immediately and the original error is then reported.
Use --no-notify to disable all notifiers for one invocation. To keep SMTP
configured but silence it persistently, use events: none. Slack has no event
filter; remove its configuration or use --no-notify when it should not send.
Dry-run behavior
--dry-run prints generated build commands, prospective uploads, and
notification summaries without running Flutter, Shorebird, uploads, Slack, or
SMTP. It uses conventional artifact paths in its output.
Build-number resolution happens before execution. Consequently, a dry run with
the stores or max-stores-plus-one strategy still calls Google Play and/or
App Store Connect. Use --build-number, the pubspec strategy, or a separate
dry-run configuration when a completely offline preview is required.
Artifact discovery
Flutter Android builds use build appbundle for --android-artifact aab and
build apk for --android-artifact apk; iOS builds use build ipa.
After a real build, Flutter Local Build searches recursively below:
build/app/outputs/bundlefor the most recently modified.aab; andbuild/app/outputs/flutter-apkfor the most recently modified.apk; andbuild/ios/ipafor the most recently modified.ipa.
If the directory or matching artifact does not exist, the job fails. Dry runs
and patch jobs report the conventional paths
build/app/outputs/bundle/release/app-release.aab,
build/app/outputs/flutter-apk/app-release.apk, and
build/ios/ipa/app.ipa without checking that files exist.
Complete multi-app example
version: 1
flutterCommand: [fvm, flutter]
apps:
customer:
path: apps/customer
androidPackage: com.example.customer
iosBundleId: com.example.customer
profiles:
development:
androidPackage: com.example.customer.dev
iosBundleId: com.example.customer.dev
dartDefines:
TENANT: customer
vendor:
path: apps/vendor
androidPackage: com.example.vendor
iosBundleId: com.example.vendor
profiles:
development:
androidPackage: com.example.vendor.dev
iosBundleId: com.example.vendor.dev
dartDefines:
TENANT: vendor
profiles:
development:
flavor: development
target: lib/main_development.dart
dartDefines: config/development.json
production:
flavor: production
target: lib/main_production.dart
dartDefines:
API_URL: https://api.example.com
obfuscate: true
splitDebugInfo: build/symbols
versioning:
buildNumber:
strategy: max-stores-plus-one
fallback: pubspec
publishers:
googleDrive:
serviceAccountEnv: LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT
folderId: your-drive-folder-id
playConsole:
serviceAccountEnv: LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT
track: internal
testFlight:
keyIdEnv: APP_STORE_CONNECT_KEY_ID
issuerIdEnv: APP_STORE_CONNECT_ISSUER_ID
privateKeyEnv: APP_STORE_CONNECT_PRIVATE_KEY_PATH
notifiers:
slack:
webhookEnv: LOCAL_BUILD_SLACK_WEBHOOK
smtp:
events: failure
host: smtp.example.com
port: 587
security: starttls
usernameEnv: LOCAL_BUILD_SMTP_USERNAME
passwordEnv: LOCAL_BUILD_SMTP_PASSWORD
from: builds@example.com
to: [release-team@example.com]
Build both apps for Android while overriding the automatically resolved number:
local_build build \
--profile production \
--app customer \
--app vendor \
--platform android \
--build-name 1.4.0 \
--build-number 104 \
--publish
Credentials and security
Store credentials in environment variables or a secret manager. Never commit service-account JSON, App Store private keys, SMTP passwords, tokens, or Slack webhooks. Prefer absolute paths for credential files because those paths are read by the CLI process, not resolved relative to each app.
Use narrowly scoped provider accounts, grant Drive access only to the target folder, and limit Play/App Store permissions to the applications and actions required by the build workflow.
To report a vulnerability, use the repository's private security advisory form.
Extending Flutter Local Build
The Dart package exports the build models and provider interfaces. A fork can
add a Publisher, Notifier, or BuildNumberSource implementation and
register it in the provider registry. Keep provider-specific credentials and
API behavior outside the planner and executor so the CLI remains portable.
See the architecture guide and contribution guide.
Troubleshooting
- Unknown profile or app: verify names exactly match the YAML keys.
- No build jobs: the selected apps lack
androidPackage/iosBundleIdfor the selected platform, or the selection is empty. - Missing environment variable: configuration fields such as
serviceAccountEnvcontain variable names, not secret values. - Artifact not found: confirm the Flutter flavor writes an
.aabor.ipabelow the documented output directories. - Store lookup fails in a preview: store-based build-number resolution also
runs during
--dry-run; usefallback: pubspecor an explicit number. - TestFlight key cannot be found: use an absolute
.p8path for publication and ensure Xcode's command-line tools are installed.
Argument and configuration errors return exit code 64. Failed builds,
publishing operations, notifications, and unexpected errors return a non-zero
exit code and print details to standard error.
Project
Flutter Local Build is maintained by Pycify. Contributor: GabbyGreat.
Libraries
- local_build
- Extensible local Flutter build and release orchestration.