Skip to content

feat(flet build): macOS code signing, notarization and AppStore support#6702

Open
ndonkoHenri wants to merge 28 commits into
release/flet-1.0from
fix/macos-sign-notarize
Open

feat(flet build): macOS code signing, notarization and AppStore support#6702
ndonkoHenri wants to merge 28 commits into
release/flet-1.0from
fix/macos-sign-notarize

Conversation

@ndonkoHenri

@ndonkoHenri ndonkoHenri commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

flet build macos can now produce a Developer ID-signed, notarized, and stapled app for direct distribution — and a Mac App Store / TestFlight-ready .pkg — selected by a single distribution lane switch. Previously the output was ad-hoc signed with no hardened runtime, which macOS 15+ effectively blocks for downloaded apps, and native .so files from dependencies triggered "library load disallowed by system policy". Signing is a no-op unless configured, so existing builds are unaffected.

Closes #2347
Closes #4543

Usage

The minimal form — the signing identity is auto-discovered from the keychain:

flet build macos --macos-distribution developer-id --macos-notary-profile flet-notary

One pyproject.toml can hold both lanes; lane-specific settings live in per-lane subtables and don't collide:

[tool.flet.macos.signing]
distribution = "developer-id"                     # default lane

[tool.flet.macos.signing.developer-id]
notary_profile = "flet-notary"

[tool.flet.macos.signing.app-store]
provisioning_profile = "certs/MyApp_MacAppStore.provisionprofile"
flet build macos                                          # notarized app for your website
flet build macos --macos-distribution app-store -o build/macos-store   # store .pkg

Every setting resolves CLI > per-lane subtable > flat pyproject key > env var (FLET_MACOS_*, APPLE_API_* for notary credentials in CI).

What it does

New flet_cli/utils/macos_sign.py, orchestrated from the build command:

  • Distribution lanes--macos-distribution {none,developer-id,app-store} replaces per-feature toggles, so conflicting configurations are inexpressible. Identity resolution is scoped to the certificate type each lane requires, and auto-discovers the certificate when the keychain holds exactly one of that type.
  • developer-id: every bundled Mach-O (found by magic bytes, not extension) is signed inside-out with the hardened runtime and secure timestamp; entitlements applied to the app and helper executables; verified with codesign --verify --deep --strict plus a per-binary coverage assertion; then notarytool submit --wait (keychain profile or ASC API key), Apple's per-file log on rejection, staple + validate.
  • app-store: signs with Apple Distribution — sandboxed, no hardened runtime, store-mandated application-identifier/team-identifier entitlements, helpers get the sandbox inherit pair, all com.apple.security.cs.* exceptions stripped — embeds the provisioning profile (scrubbed of the quarantine xattr that ASC processing rejects as error 91109; --validate-app misses it), and packages an installer-signed .pkg verified with pkgutil.
  • Pre-build preflight: the entire signing configuration is validated before the Flutter build starts — typo'd/expired identity (called out by name and status), missing notary credentials, missing profile, profile/bundle-id mismatch (the slow-to-surface ITMS-90889), missing LSApplicationCategoryType — all fail in seconds with the exact setting to fix.

Breaking change

iOS per-method signing settings in [tool.flet.ios.export_methods.<method>] now override the flat [tool.flet.ios] keys instead of losing to them (previously the subtables were dead config whenever a flat key existed). Same rule as the new macOS per-lane subtables. See the migration guide.

Also in this PR

  • Default entitlement added: com.apple.security.cs.allow-unsigned-executable-memory — required for ctypes/cffi callbacks on x86_64 under the hardened runtime (overridable).
  • Template fix: entitlements emitted <true /> (with a space), accepted by Xcode/plutil but rejected by codesign's AMFI parser; both templates fixed and the signer normalizes entitlements through plistlib.
  • Web env fixes: FLET_WEB_RENDERER / FLET_WEB_ROUTE_URL_STRATEGY / FLET_WEB_NO_CDN fallbacks now reachable in flet build and flet publish; case normalized on the resolved value.
  • Docs: the macOS publish guide gained Code signing, Notarization (credential walkthroughs for both notary auth flavors), Mac App Store (portal walkthroughs), CI setup incl. certificate export, Distributing (DMG/zip), and troubleshooting; symptom/fix troubleshooting tables added across the windows/linux/ios/android publish pages; the PyInstaller (flet pack) guide revived as Using PyInstaller; new env vars in the reference page.

Testing

  • 130 unit tests in flet-cli: Mach-O detection (incl. fat binaries), identity resolution against canned keychain output (dedupe, ambiguity, expiry, type scoping, auto-discovery), lane/subtable precedence, preflight failures, MAS entitlement/profile handling, plus a certificate-free ad-hoc end-to-end on a real bundle (CI-safe).
  • Developer ID E2E (real Team ID): 216 binaries signed in one build; notarization accepted; spctl reports "Notarized Developer ID"; a quarantined download of the app launches cleanly and imports pydantic/numpy/cryptography/pillow/opencv under the hardened runtime — the macOS app throws "library load disallowed by system policy" error while loading 3rd-party Python .so modules #4543 scenario, dead.
  • App Store E2E: .pkg from a plain flet build macos passed altool --validate-app and --upload-package, survived ASC processing, and the TestFlight-installed app runs all native imports under the App Sandbox.
  • An edge-case wheel (non-canonical framework, extensionless dylib, helper executable) also signs, notarizes, and loads — validated by Apple's own notary service.

New `--macos-signing-identity`, `--macos-notarize` and
`--macos-notary-profile` options, configurable via
`[tool.flet.macos.signing]` and `FLET_MACOS_*` environment variables:

- Signs every bundled Mach-O inside-out (discovered by magic bytes, not
  extension) with hardened runtime and secure timestamp; entitlements are
  applied to the app bundle and standalone helper executables, libraries
  are signed bare per Apple guidance. Identity is resolved against the
  keychain up front (name, SHA-1, or substring; deduplicated across
  keychains) and every failure is per-file and fatal.
- Verifies with `codesign --verify --deep --strict` plus a per-binary
  coverage assertion.
- Optionally notarizes (`notarytool submit --wait` with a keychain
  profile or App Store Connect API key env vars), surfaces Apple's
  notarization log on rejection, then staples and validates the ticket.
- Adds `com.apple.security.cs.allow-unsigned-executable-memory` to the
  default entitlements: Apple's libffi allocates W+X closure memory on
  x86_64, breaking ctypes/cffi callbacks under the hardened runtime.
- Fixes the entitlements templates emitting self-closing tags with a
  space (`<true />`), which Xcode tolerates but codesign's AMFI parser
  rejects; the signer additionally normalizes any entitlements file
  through plistlib before use.
- Documents signing, notarization, credentials, CI setup and
  troubleshooting in the macOS publish guide, and adds the new
  environment variables to the reference page (now fully sorted).

Ref #2347
Expand every function, method, and dataclass in flet_cli/utils/macos_sign.py,
the sign_macos_app/_macos_notary_credentials methods, and the test suite to
full Google-style docstrings (Args/Returns/Raises), including the rationale
behind the non-obvious decisions: the inside-out signing order and why it
protects resource seals, the entitlements policy (app bundle + MH_EXECUTE
helpers only), fingerprint-based identity selection, the AMFI plist
normalization, and the multi-keychain deduplication.

Also refines the macOS publish guide's CI section heading and links the
apple-actions/import-codesign-certs action in the troubleshooting table.
`flet build web` and `flet publish` now resolve `FLET_WEB_RENDERER`, `FLET_WEB_ROUTE_URL_STRATEGY` and `FLET_WEB_NO_CDN` from the environment (precedence: CLI flag > pyproject.toml > env var > default), making the `[env: ...]` markers already shown in `--help` accurate. Previously the build ignored them and only the runtime web server read them.

Docs: add "env var" example tabs and environment-variables reference links to the Android, macOS and web publish pages; rename the former ".env" tab label to "env var" (flet reads the process environment, it does not auto-load a .env file). Also fix the Android key-alias resolution order, which listed the env var above pyproject.toml though the code resolves pyproject.toml first.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 security issue

Security issues:

  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="sdk/python/packages/flet-cli/src/flet_cli/utils/macos_sign.py" line_range="154-159" />
<code_context>
    return subprocess.run(
        args,
        capture_output=True,
        text=True,
        timeout=timeout,
    )
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +154 to +159
return subprocess.run(
args,
capture_output=True,
text=True,
timeout=timeout,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying flet-website-v2 with  Cloudflare Pages  Cloudflare Pages

Latest commit: a934f5c
Status: ✅  Deploy successful!
Preview URL: https://1ab67556.flet-website-v2.pages.dev
Branch Preview URL: https://test-macos-signing.flet-website-v2.pages.dev

View logs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds macOS code-signing + optional notarization support to flet build macos, including identity resolution, inside-out signing of all Mach-O binaries in the bundle, strict verification/coverage checks, and notarization + stapling via notarytool. The PR also updates templates, docs, and adds a test suite; additionally, web build/publish now honors new env vars for renderer/URL strategy/CDN.

Changes:

  • Introduce flet_cli.utils.macos_sign and integrate signing/notarization into the macOS build pipeline (new CLI flags + pyproject/env precedence).
  • Fix entitlements template boolean formatting (<true/> / <false/>) and document the new default entitlement for hardened runtime compatibility on Intel Macs.
  • Extend docs and behavior to include additional environment-variable configuration (macOS signing/notary + web options), plus new unit tests.

Reviewed changes

Copilot reviewed 10 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
website/docs/reference/environment-variables.md Adds/sorts documented env vars (Android signing, storage cache, macOS signing/notary, web options).
website/docs/publish/web/static-website/index.md Documents env-var precedence and adds env-var examples for web route strategy/renderer/CDN.
website/docs/publish/macos.md Adds code signing + notarization guidance, precedence rules, CI/troubleshooting, and notes about default entitlements.
website/docs/publish/android.md Aligns Android signing precedence docs and links env vars to the reference page.
sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/Release.entitlements Fixes boolean self-closing tag formatting for codesign compatibility.
sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/DebugProfile.entitlements Same entitlements boolean formatting fix for debug profile.
sdk/python/packages/flet-cli/tests/test_macos_sign.py Adds unit + macOS-only e2e tests for Mach-O detection, identity resolution, entitlements normalization, and ad-hoc signing verification.
sdk/python/packages/flet-cli/src/flet_cli/utils/macos_sign.py New implementation: identity resolution, Mach-O discovery/classification, signing, verification, notarization, stapling.
sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py Adds env-var support for web renderer/route strategy/no-cdn in flet publish web.
sdk/python/packages/flet-cli/src/flet_cli/commands/build.py Invokes macOS signing/notarization after copy_build_output() for flet build macos.
sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py Adds new macOS signing/notary flags; adds default entitlement; adds env-var support for web renderer/route strategy/no-cdn.
CHANGELOG.md Adds changelog entries describing macOS signing/notarization and entitlements-template fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +129 to +139
if self.keychain_profile:
return ["--keychain-profile", self.keychain_profile]
assert self.api_key and self.api_key_id and self.api_issuer
return [
"--key",
self.api_key,
"--key-id",
self.api_key_id,
"--issuer",
self.api_issuer,
]
Comment thread CHANGELOG.md Outdated

### Bug fixes

* Fix the generated `macos/Runner/*.entitlements` files being rejected by `codesign` with `AMFIUnserializeXML: syntax error` when used directly for re-signing: the template emitted boolean values as self-closing tags with a space (`<true />`), which Xcode and `plutil` accept but codesign's stricter AMFI plist parser does not. The templates now emit `<true/>`, and `flet build`'s own signing step additionally normalizes any entitlements file through `plistlib` before use, so plist formatting can never break signing by ([#6702](https://github.com/flet-dev/flet/pull/6702)) by @ndonkoHenri.
Comment on lines 372 to 383
web_renderer=WebRenderer(
options.web_renderer
or get_pyproject("tool.flet.web.renderer")
or os.getenv("FLET_WEB_RENDERER")
or "canvaskit"
),
route_url_strategy=RouteUrlStrategy(
options.route_url_strategy
or get_pyproject("tool.flet.web.route_url_strategy")
or os.getenv("FLET_WEB_ROUTE_URL_STRATEGY")
or "path"
),
Comment on lines 1314 to 1330
@@ -1298,6 +1325,7 @@ def _xml_attr_value(v):
"web_renderer": (
self.options.web_renderer
or self.get_pyproject("tool.flet.web.renderer")
or os.getenv("FLET_WEB_RENDERER")
or "canvaskit"
),
…rovenance verification

Fixes for three defects found by adversarial review of the signing code,
each empirically reproduced with codesign on macOS 15:

- Non-canonical frameworks (the only kind pip wheels can ship — the zip
  format cannot represent the Versions/Current symlink) aborted the whole
  signing run with 'bundle format unrecognized/ambiguous', or got a junk
  generic-bundle signature in hybrid layouts like PyQt6's. They are now
  detected via _is_signable_framework() and their Mach-O files are signed
  individually — which notarization and library validation accept — while
  canonical frameworks keep getting sealed as bundles.

- Nested helper bundles (.app/.appex/.xpc) were bundle-signed without
  entitlements, which both stripped the entitlements their main executable
  had just received (codesign --force discards them without
  --preserve-metadata) and left hardened-runtime helpers without the
  JIT/unsigned-memory exceptions Python needs. Their bundle signature now
  carries the same entitlements as the app.

- verify_app()'s per-file safety net used plain 'codesign --verify', which
  passes for the linker-signed stub every pip wheel ships with — so a file
  skipped by the signing passes sailed through to notarization rejection
  or a library-validation kill at runtime. Coverage now also checks
  provenance via 'codesign --display': linker-signed stubs always fail,
  real identities must appear in the Authority chain.

Also: recognize fat64 (0xcafebabf) universal binaries; anchor the
framework main-binary heuristic to the framework root; make a missing
Release.entitlements a hard error instead of silently signing without
entitlements; document the manual Mac App Store/TestFlight requirements
(sandbox entitlement, Apple Distribution cert, provisioning profile,
productbuild) the automated pipeline does not cover yet; expand the test
suite from 14 to 28 tests (signing order and entitlements routing, all
notarization failure paths, provenance verification, non-canonical
framework handling).
argparse's default="path" meant options.route_url_strategy was always
truthy, so the pyproject (tool.flet.web.route_url_strategy) and
FLET_WEB_ROUTE_URL_STRATEGY fallbacks documented for `flet publish` were
dead code. The default now lives at the end of the resolution chain, as
in `flet build`.
New --macos-app-store, --macos-provisioning-profile and
--macos-installer-identity options (pyproject: [tool.flet.macos.signing]
app_store / provisioning_profile / installer_identity; env:
FLET_MACOS_PROVISIONING_PROFILE / FLET_MACOS_INSTALLER_IDENTITY), verified
end-to-end against App Store Connect and TestFlight on 2026-07-22 — the
uploaded build installs via TestFlight and loads every bundled native
extension under the App Sandbox, closing the scenario of #4543.

The store lane differs from Developer ID signing in every dimension that
matters, all handled by the new mode:

- Apple Distribution identity without the hardened runtime (the store's
  containment model is the App Sandbox; Apple re-signs on delivery).
- Entitlements are the template's with every com.apple.security.cs.*
  hardened-runtime exception stripped, App Sandbox forced on, and the
  application-identifier / team-identifier pair injected — derived from
  the certificate's Team ID and the built app's CFBundleIdentifier.
  Helper executables and nested helper bundles carry exactly the
  sandbox-inherit pair.
- The provisioning profile is embedded at Contents/embedded.provisionprofile
  before signing so the app seal covers it, and its App ID is cross-checked
  against the bundle id up front — a mismatch otherwise surfaces only
  after upload as ITMS-90889.
- The deliverable is a .pkg built with productbuild and signed with the
  installer certificate — a certificate type invisible under the
  codesigning keychain policy, so resolve_identity() gained a policy
  parameter and the installer identity resolves (fail-fast) before any
  signing work.
- LSApplicationCategoryType is validated before signing (App Store
  validation hard-rejects without it — empirically a 409) and a missing
  ITSAppUsesNonExemptEncryption produces a warning, both pointing at
  --info-plist / [tool.flet.macos.info].
- app_store + notarize is rejected: store builds are not notarized.

verify_app_store_app() asserts the embedded profile and the sealed
application-identifier after signing (the TestFlight ITMS-90889
invariants), build_pkg() verifies the package signature with pkgutil, and
the docs' Mac App Store section now covers the automated flow, the
one-time portal setup, and the altool validate/upload commands.
Unit tests: 28 -> 34. ITMS added to the typos allowlist.

The provisioning profile is embedded BEFORE the xattr sweep: profiles are
browser downloads carrying com.apple.quarantine, macOS propagates the
attribute onto the embedded copy, and App Store Connect processing
rejects any quarantined file in the package (error 91109, observed on
the playground upload) — a check altool --validate-app does not perform,
so it only surfaces after upload.
Fixes and tightening from an adversarial review of the complete signing
change set (both distribution lanes), each finding verified before fixing:

Correctness:
- Prefix-wildcard provisioning profiles (TEAM.com.example.*) were falsely
  rejected by the profile/bundle-id cross-check, which only accepted exact
  and full-team wildcards. Wildcard matching now lives in
  profile_covers_application() with tests.
- An installer identity that resolved (under the basic keychain policy) to
  a non-installer certificate failed only later, deep inside productbuild;
  it is now rejected up front with the certificate named.
- Malformed Info.plist / Release.entitlements in the App Store lane raised
  raw tracebacks; both now produce clean, actionable errors.
- _normalized_entitlements() could silently collide when two entitlements
  files shared a stem; the normalized copy's name now includes a source-path
  digest.
- verify_app_store_app() misreported a codesign --display failure as an
  entitlement mismatch; the two cases are now distinct errors.
- FLET_WEB_RENDERER / FLET_WEB_ROUTE_URL_STRATEGY env fallbacks bypassed
  the CLI options' lowercasing; values are now lowered for parity.

Organization:
- The store entitlements policy moved from the CLI command into
  macos_sign.app_store_entitlements() (unit-testable), with the helper
  sandbox-inherit pair as APP_STORE_HELPER_ENTITLEMENTS.
- Module docstring now maps both distribution lanes; store-section
  divider added; duplicated cleanup blocks merged; missing type
  annotations added; over-explaining docstrings trimmed.

Tests (96 -> 107):
- --options runtime/--timestamp for real identities is now asserted (the
  property notarization depends on, previously untested).
- New darwin e2e signs an App Store-shaped bundle with real codesign and
  proves the xattr sweep strips quarantine from the embedded profile (the
  ASC error-91109 regression) and that the seal covers the profile.
- Previously untested error branches covered: missing/invalid entitlements
  and profile inputs, deep-verification failure, unreadable sealed
  entitlements, missing main executable, malformed profile plist, staple
  validation failure; notarize/productbuild happy paths now assert
  credential forwarding and the /Applications install root.
- resolve_identity's default policy pinned; xattr sweep deletion now fails
  a test; MAS naming unified to App Store; imports and identity fixtures
  consolidated.

Docs:
- FLET_MACOS_PROVISIONING_PROFILE and FLET_MACOS_INSTALLER_IDENTITY added
  to the environment-variables reference (they were undocumented);
  FLET_MACOS_SIGNING_IDENTITY entry updated for Apple Distribution.
- Mac App Store section links options/env vars instead of raw argparse
  notation; helper-bundle entitlements phrasing corrected; duplicate
  credentials explanation trimmed; missing bash fence tag added.
- Changelog: MAS bullet gains the quarantine/91109 scrub; entries added
  for the FLET_WEB_* env fallbacks and the flet publish
  route-url-strategy fix the branch also carries.
The build mode fully determines the certificate type Apple accepts —
Developer ID Application for notarized direct distribution, Apple
Distribution (or the legacy 3rd Party Mac Developer Application) for the
App Store, an installer certificate for the store .pkg — so
resolve_identity() now takes the lane's acceptable type prefixes and
scopes matching to them:

- A partial identity like a bare team ID stays unambiguous per lane even
  when the keychain holds certificates of several types (previously it
  matched all of them and errored).
- An explicit identity of the wrong type gets a precise error naming the
  matched certificate and the required type, replacing the store lane's
  after-the-fact warning and the installer lane's post-resolution name
  check.
- Lanes that require an identity anyway (--macos-notarize,
  --macos-app-store, and the store installer certificate) auto-discover
  it when exactly one certificate of the required type exists — the
  previous hard 'requires an identity' errors become successes in the
  common single-team case, with the chosen identity logged. Several
  candidates or none still fail with the candidate list (including the
  expired-certificate explanation). Discovery never triggers on a plain
  build: no identity there still means ad-hoc, unchanged.

The plain signing lane stays unscoped — Apple Development and corporate
certificates are legitimate for local/internal distribution.
Tests: 108 -> 115; docs and changelog updated.
Step-by-step map of the resolution algorithm (ad-hoc short-circuit,
validity filtering via find-identity -v, fingerprint dedup, type scoping,
selection precedence, failure diagnosis order), the rationale for never
auto-picking between two valid same-name certificates, and a corrected
Returns section (it returns the identity, not its fingerprint). Body
comments added for the load-bearing -v semantics and the exact-name-
before-substring precedence.
--macos-installer-identity's help still said 'required for App Store
builds', stale since auto-discovery; both identity options and the
FLET_MACOS_INSTALLER_IDENTITY reference entry now state the
when-unset behavior. (--macos-provisioning-profile stays 'required' —
profiles are not discoverable.)
The Mac App Store section now matches the page's own per-option
convention and the iOS page's depth: portal walkthroughs a newcomer can
follow (both distribution certificates with the find-identity -p basic
gotcha, explicit App ID registration, Mac App Store Connect profile
creation, the App Store Connect app record and where its numeric Apple
ID lives), a three-channel configuration example, and dedicated
sections with Resolution order for app_store, the provisioning profile,
and the installer identity.

A new Identity auto-discovery section states the semantics precisely —
an identity is 'not configured' only when the CLI option, pyproject key,
and environment variable are all unset; resolution always runs first and
configured values are never silently replaced — and the notarize
section, the signing-identity resolution default, and the CLI help texts
(which wrongly implied CLI-only 'unset', and --macos-notarize still
claimed to require --macos-signing-identity) now defer to it.

The Credentials section documents both notarytool store-credentials
flavors — App Store Connect API key (--key/--key-id/--issuer) and Apple
ID with app-specific password — including where each credential is
created and that the .p8 can be downloaded only once. The credentials
resolution order gains its missing terminal Default entry.

Proofread adversarially against the implementation: all eight
Resolution order lists, the discovery semantics, both credential
commands, the altool invocations, and every cross-page anchor verified.
'With --macos-notarize, the signing identity may be omitted' implied the
behavior was tied to the CLI flag, but notarization (and App Store mode)
can equally be enabled via their pyproject keys — the docs, the env-var
reference, and the --macos-signing-identity help now say 'notarizing /
App Store builds' instead of naming flags.
The build example and the identity-pinning paragraph referenced the
provisioning profile and installer identity before their sections;
they now link ahead. The pinning paragraph also links each identity's
section instead of enumerating two of its three configuration channels
(the env vars were missing) — the sections carry the full resolution
order.
'Building for the App Store' now follows the provisioning-profile and
installer-identity sections, so every setting the build example uses is
defined before it appears — prerequisites → options → build → upload —
and the 'detailed below' forward pointer becomes unnecessary.
Systematic sweep of macos.md for config options named in prose without a
link (the build-number sentence in Uploading was the trigger — it named
only the CLI flag; it now links the Versioning docs). Also linked: the
notarize/app-store toggles in the env-var tab notes, the compile flags in
the Troubleshooting table, and the APPLE_API_KEY / APPLE_API_KEY_ID /
APPLE_API_ISSUER trio, which had no environment-variables reference
entries at all and now do. Unlinked-by-convention cases left alone:
pyproject keys (no anchor target exists) and --no-* forms adjacent to
their linked positive option.
The notarize/app_store boolean pair encoded a four-state lane space
(ad-hoc, sign-only, Developer ID + notarize, App Store) as a 2x2 grid
with an invalid cell that had to be policed by a mutual-exclusion error,
and booleans compose badly across the CLI > pyproject > env layers:
turning one lane on could not turn the other lane's pyproject 'true'
off, so every lane flip needed a paired --no-* negation — and shipping
on both channels from one pyproject was impossible.

The lane is now one value — --macos-distribution {none,developer-id,
app-store} / [tool.flet.macos.signing].distribution — so conflicting
lanes are inexpressible, a CLI value wholesale-replaces the pyproject
value, and one pyproject can hold both channels' settings (they are
naturally lane-scoped) with a single flag flipping between them:

    flet build macos --macos-distribution app-store

The resolved value is validated wherever it came from — argparse
choices= only guards the CLI layer, and a pyproject typo must fail
loudly rather than fall through to a silently ad-hoc build.

The signing configuration is also validated BEFORE the build starts
(preflight_macos_signing): identity resolution against the keychain,
notary credentials, the provisioning profile, and the store category
now fail in seconds instead of after the multi-minute Flutter build.
The signing step re-checks everything; the preflight is purely an
early exit and a no-op for unsigned builds.

Both lanes re-verified end-to-end with pyproject-only distribution
config: developer-id (auto-discovered identity, 216 binaries,
notarized, stapled, 7/7 verification) and app-store (both identities
auto-discovered, store entitlements sealed, signed .pkg). Tests
115 -> 126 — the lane dispatch and preflight gain their first
coverage. Docs: new Distribution lanes section with the single
resolution order and a 'Shipping on both channels' recipe; notarize
and App Store sections, CI example, and changelog updated to the
lane vocabulary.
Every signing key except the lane selector itself may now live in a
per-lane subtable that overrides the flat key when its lane builds —
resolution is CLI option > lane subtable > flat key > environment
variable (the lane deliberately beats the flat key, the opposite of
iOS's export_methods, where per-lane values silently lose to flat ones).

This closes the one real gap auto-discovery leaves in the
one-pyproject-two-lanes workflow: keychains where discovery cannot pick
(e.g. certificates from several teams) can pin a different identity per
lane. For keys only one lane reads (notary_profile, provisioning
profile, installer identity) the subtable and flat forms are equivalent,
so configs can be grouped by lane for readability. The 'none' lane has
no subtable, and a misnamed subtable (e.g. signing.app_store) fails the
build instead of being silently ignored — same fail-loud philosophy as
the distribution value itself.

All five lookup sites route through one macos_signing_setting() helper.
Tests 126 -> 130: full precedence chain, per-lane identity pinning
flipped by --macos-distribution, the fully lane-organized pyproject, and
misnamed-subtable rejection. Docs: the Per-lane settings section
(tabbed, with the two-run CLI counterpart — invocations need no lane
syntax since an invocation is single-lane), lane steps in the four
affected resolution orders, and the dual-lane section renamed to
'Switching lanes'.
route_url_strategy and web_renderer previously lowercased only the CLI
option (type=str.lower) and the environment variable, letting a
pyproject value like renderer = "CanvasKit" through unlowered. Apply
.lower() once to whichever source wins instead; the or-"" guard on the
env var is no longer needed since the default keeps the chain non-None.

Also reflow an over-long comment in android_sdk.py (E501).
Add a third Distributing tab covering styled DMGs (background image,
app-left/Applications-right layout) via dmgbuild: verified settings file
matched to flet's build output, the >=1.6.7 version floor (older versions
produce blank backgrounds on macOS 26.2+, dmgbuild#273), the @2x Retina
background convention, and the same sign/notarize/staple closing sequence
as the plain-DMG tab. dmgbuild writes the Finder layout without a GUI
session, so the recipe works identically in CI.
…nsion

- Credentials split into two sequential subsections (Creating a
  credential / Providing it to Flet); note that only the keychain
  profile takes both credential kinds.
- CI section now walks the one-time setup: .p12 export from Keychain
  Access, why the .p12 is base64-encoded while the PEM .p8 goes in
  as-is, and gh secret set commands for all six secrets. Workflow
  writes the API key through $APPLE_API_KEY into runner.temp
  (printf, no path duplication, out of artifact-upload reach) with a
  note on why the key must be a file at all.
- Distributing: DMG (plain or dmgbuild custom look, sharing the
  sign/notarize/staple steps) vs zip as tabs, each step explained;
  zip's can't-be-stapled asymmetry called out.
- Troubleshooting: two new rows (codesign hang on keychain prompt,
  missing Apple intermediate CAs) and a Mac App Store table
  (ITMS-90889, 91109 quarantine, App Sandbox file access, TestFlight
  app relocation) — all hit during real-world verification.
Bring the Symptom | Cause-and-fix table format from the macOS page to
the other platform pages. Rows are symptom indexes: where a page already
documents a failure in depth (android's Extract packages, linux's
Wayland section), the row links there instead of duplicating it.

- windows: tabulate the Developer Mode item; add the missing
  C++-workload toolchain error.
- linux: linker error (lld), missing -dev packages, libmpv/GStreamer
  as *runtime* dependencies on users' machines, Wayland positioning.
- ios (had none): codesign hang on hidden keychain prompt, no valid
  signing certificates, profiles disappearing while Xcode runs.
- android: sitepackages.zip crash -> Extract packages, keytool not on
  PATH, manifest merger provider clash.

Also link the macOS CI section to the complete build workflow in the
CI/CD guide — its snippet shows only the signing-specific steps.
Use the expandable # (N)! annotations (remark-code-annotations, as on
the CI/CD guide) where explanations belong to specific lines:

- CI workflow: import-codesign-certs (with docs link), APPLE_API_KEY
  path/runner.temp rationale, optional identity secret (auto-discovery),
  printf materialization — absorbing the two after-block explainer
  paragraphs and the tip admonition.
- gh secret set: base64-for-binary .p12 vs as-is PEM .p8, interactive
  prompt note, which values come with the API key.
- DMG sign/notarize/staple: the three per-command bullets.

Left plain: altool (backslash-continued lines can't carry trailing
markers), dmg_settings.py (copied to disk, comments should survive),
and short config examples.
The flet pack command is supported (the planned deprecation was
reversed) but its only doc was the archived PyInstaller page, hidden
from the sidebar and opening with a danger banner steering readers to
flet build. Replace it with a live page under Publishing (sidebar:
'Using PyInstaller', below Web):

- Neutral 'How it relates to flet build' comparison table with
  when-to-pick-which guidance instead of the danger banner.
- Content refreshed against today's pack.py: platform-native icon
  formats (.ico/.icns/.png) replace the old PNG+Pillow advice, the
  Windows-only ';' add-data separator is gone, and --onedir, --yes,
  --company-name, --debug-console, --uac-admin, --codesign-identity
  and --pyinstaller-build-args are covered. Documents that icon and
  metadata are patched into the embedded Flet viewer too, and the
  build/dist deletion prompts.
- AppVeyor CI section (stale claims, PAT-in-yaml flow, missing
  images) replaced by a GitHub Actions three-OS matrix.
- Troubleshooting table (hidden imports, --debug-console, macOS
  Gatekeeper) in the same format as the platform pages.
- Every option mention links to its anchor in the flet pack CLI
  reference, per the house style.
- Alternative-note admonitions on the macOS/Windows/Linux pages and
  a pointer in the publish index; archived page removed (nothing
  linked to it).
@ndonkoHenri ndonkoHenri changed the title feat(flet build): macOS code signing and notarization feat(flet build): macOS code signing, notarization and AppStore support Jul 24, 2026
@ndonkoHenri ndonkoHenri linked an issue Jul 24, 2026 that may be closed by this pull request
@ndonkoHenri
ndonkoHenri requested a review from Copilot July 24, 2026 16:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 27 changed files in this pull request and generated 3 comments.

Comment on lines +275 to +276
for key, value in (self.get_pyproject("tool.flet.macos.signing") or {}).items():
if isinstance(value, dict) and key not in self.MACOS_DISTRIBUTIONS:
Comment on lines +386 to +402
if distribution == "app-store":
resolve_identity(identity, types=APP_STORE_CERTIFICATE_TYPES)
resolve_identity(
self.macos_signing_setting(
cli_value=self.options.macos_installer_identity,
distribution=distribution,
key="installer_identity",
env_var="FLET_MACOS_INSTALLER_IDENTITY",
),
policy="basic",
types=INSTALLER_CERTIFICATE_TYPES,
)
elif distribution == "developer-id":
resolve_identity(identity, types=DEVELOPER_ID_CERTIFICATE_TYPES)
self._macos_notary_credentials()
else:
resolve_identity(identity)
# Quarantine and Finder-info extended attributes make codesign fail with
# "resource fork, Finder information, or similar detritus not allowed",
# and App Store Connect rejects quarantined files outright.
_run(["xattr", "-cr", str(app_path)])
[skip ci]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add code signing option to flet build macos command

2 participants