Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fa72d04
Add macOS code signing and notarization to `flet build macos`
ndonkoHenri Jul 21, 2026
3fcafa3
Improve documentation of the macOS signing code
ndonkoHenri Jul 21, 2026
74e2189
Read `FLET_WEB_*` env vars in web builds; align publish env-var docs
ndonkoHenri Jul 21, 2026
d57e027
Changelog: macOS code signing and notarization under 1.0.0
ndonkoHenri Jul 21, 2026
16629c0
Harden macOS signing: wheel frameworks, helper-bundle entitlements, p…
ndonkoHenri Jul 21, 2026
4092806
Fix unreachable route-url-strategy fallbacks in `flet publish`
ndonkoHenri Jul 21, 2026
9515580
Add Mac App Store / TestFlight support to `flet build macos`
ndonkoHenri Jul 22, 2026
78cbc21
Polish macOS signing after full review
ndonkoHenri Jul 22, 2026
33906ad
Type-scoped identity resolution with per-lane auto-discovery
ndonkoHenri Jul 23, 2026
80455d6
Document resolve_identity's algorithm in its docstring
ndonkoHenri Jul 23, 2026
19d4e01
Mention auto-discovery in the identity option help texts
ndonkoHenri Jul 23, 2026
011a585
Detail the macOS signing docs to the iOS page's standard
ndonkoHenri Jul 23, 2026
0fa489c
Phrase auto-discovery by build mode, not by CLI flag
ndonkoHenri Jul 23, 2026
ffdbcfe
Link forward references in the App Store build section
ndonkoHenri Jul 23, 2026
18e94c3
Order the App Store section as a pipeline: options before build
ndonkoHenri Jul 23, 2026
532f75e
Link every option mention; give APPLE_API_* their reference entries
ndonkoHenri Jul 23, 2026
573090c
Replace lane booleans with a single --macos-distribution selector
ndonkoHenri Jul 23, 2026
fa29ac3
Per-lane signing subtables: [tool.flet.macos.signing.<lane>]
ndonkoHenri Jul 24, 2026
208c181
Normalize web option case on the resolved value, not per source
ndonkoHenri Jul 24, 2026
3f90368
macOS publish docs: custom DMG recipe (dmgbuild) in Distributing
ndonkoHenri Jul 24, 2026
6662b2d
Document Flet build action in publishing docs
ndonkoHenri Jul 24, 2026
756c235
macOS publish docs: credentials/CI walkthroughs, troubleshooting expa…
ndonkoHenri Jul 24, 2026
6948d2d
Publish docs: troubleshooting tables for windows/linux/ios/android
ndonkoHenri Jul 24, 2026
bf18e87
macOS publish docs: code annotations for CI and DMG blocks
ndonkoHenri Jul 24, 2026
34eede7
Revive PyInstaller packaging docs as publish/using-pyinstaller
ndonkoHenri Jul 24, 2026
5e1b245
update
ndonkoHenri Jul 24, 2026
a934f5c
Merge branch 'release/flet-1.0' into fix/macos-sign-notarize
ndonkoHenri Jul 24, 2026
6637e16
update docs
ndonkoHenri Jul 24, 2026
3499364
Address Copilot review findings on signing preflight and credentials
ndonkoHenri Jul 28, 2026
cbca319
Merge branch 'release/flet-1.0' into fix/macos-sign-notarize
FeodorFitsner Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions sdk/python/packages/flet-cli/src/flet_cli/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@
from rich.live import Live

from flet_cli.commands.build_base import BaseBuildCommand, console
from flet_cli.commands.flutter_base import verbose1_style
from flet_cli.utils.android import flutter_target_platforms
from flet_cli.utils.macos_sign import (
MacOSSigningError,
NotaryCredentials,
notarize_and_staple,
resolve_identity,
sign_app,
)


class Command(BaseBuildCommand):
Expand Down Expand Up @@ -84,6 +92,8 @@ def handle(self, options: argparse.Namespace) -> None:
self.customize_splash_images()
self.run_flutter()
self.copy_build_output()
if self.target_platform == "macos":
self.sign_macos_app()

self.cleanup(
0,
Expand Down Expand Up @@ -207,3 +217,145 @@ def run_flutter(self):
f"Built [cyan]{self.platforms[self.target_platform]['status_text']}"
f"[/cyan] {self.emojis['checkmark']}",
)

def sign_macos_app(self):
"""
Code-sign — and optionally notarize — the built macOS app bundle.

Runs after `copy_build_output()` and operates on the final `.app`
in the output directory, i.e. the artifact users distribute.

No-op unless a signing identity is configured; without one, the app keeps the
default ad-hoc signature produced by the Flutter build. Notarization is
additionally gated and requires a real (non-ad-hoc) identity plus notary
credentials.

The app-bundle signature re-applies the entitlements from the
template-generated `Release.entitlements` — re-signing replaces the
signature Xcode embedded them in, so they must be supplied again.

Exits via `cleanup(1, ...)` with an actionable message on any
configuration or signing failure.
"""

assert self.options
assert self.get_pyproject
assert self.out_dir
assert self.flutter_dir

identity = (
self.options.macos_signing_identity
or self.get_pyproject("tool.flet.macos.signing.identity")
or os.getenv("FLET_MACOS_SIGNING_IDENTITY")
)
notarize = (
self.options.macos_notarize
if self.options.macos_notarize is not None
else bool(self.get_pyproject("tool.flet.macos.signing.notarize"))
)

if not identity:
if notarize:
self.cleanup(
1,
"Notarization requires a code-signing identity. Pass "
"--macos-signing-identity or set "
"`[tool.flet.macos.signing].identity` in pyproject.toml.",
)
return

apps = sorted(self.out_dir.glob("*.app"))
if len(apps) != 1:
self.cleanup(
1,
f"Expected exactly one .app bundle in {self.rel_out_dir}, "
f"found {len(apps)}.",
)
app_path = apps[0]

# Release.entitlements is the single merged source of entitlements.
entitlements = self.flutter_dir / "macos" / "Runner" / "Release.entitlements"

def log(message: str):
if self.verbose > 0:
console.log(message, style=verbose1_style)

self.update_status(f"[bold blue]Signing [cyan]{app_path.name}[/cyan]...")
try:
resolved = resolve_identity(identity)
if notarize and resolved.is_adhoc:
self.cleanup(
1,
"Notarization requires a Developer ID identity; "
'ad-hoc ("-") signed apps cannot be notarized.',
)
signed_count = sign_app(
app_path,
resolved,
entitlements=entitlements if entitlements.is_file() else None,
log=log,
)
console.log(
f"Signed [cyan]{app_path.name}[/cyan] ({signed_count} binaries, "
f"identity: {resolved.description}) {self.emojis['checkmark']}"
)

if notarize:
credentials = self._macos_notary_credentials()
self.update_status(
f"[bold blue]Notarizing [cyan]{app_path.name}[/cyan] "
"(this can take a few minutes)...",
)
notarize_and_staple(app_path, credentials, log=log)
console.log(
f"Notarized and stapled [cyan]{app_path.name}[/cyan] "
f"{self.emojis['checkmark']}"
)
except MacOSSigningError as e:
self.cleanup(1, str(e))

def _macos_notary_credentials(self) -> NotaryCredentials:
"""
Resolve Apple notary service credentials.

A keychain profile is looked up first — `--macos-notary-profile`,
then `[tool.flet.macos.signing].notary_profile`, then the
`FLET_MACOS_NOTARY_PROFILE` environment variable — and only then
the `APPLE_API_KEY`/`APPLE_API_KEY_ID`/`APPLE_API_ISSUER` App Store
Connect API key variables (all three required). A configured
profile deliberately outranks the `APPLE_API_*` variables, which
other tooling (Fastlane, CI images) may have exported ambiently,
possibly for a different Apple team.

Returns:
Credentials for `notarytool`; exits via `cleanup(1, ...)` with
setup instructions when nothing is configured.
"""

assert self.options
assert self.get_pyproject

profile = (
self.options.macos_notary_profile
or self.get_pyproject("tool.flet.macos.signing.notary_profile")
or os.getenv("FLET_MACOS_NOTARY_PROFILE")
)
if profile:
return NotaryCredentials(keychain_profile=profile)

api_key = os.getenv("APPLE_API_KEY")
api_key_id = os.getenv("APPLE_API_KEY_ID")
api_issuer = os.getenv("APPLE_API_ISSUER")
if api_key and api_key_id and api_issuer:
return NotaryCredentials(
api_key=api_key, api_key_id=api_key_id, api_issuer=api_issuer
)

self.cleanup(
1,
"Notary service credentials are missing. Either store an "
"App Store Connect API key or Apple ID app-specific password "
"with `xcrun notarytool store-credentials <profile>` and pass "
"--macos-notary-profile <profile>, or set the APPLE_API_KEY, "
"APPLE_API_KEY_ID and APPLE_API_ISSUER environment variables.",
)
46 changes: 39 additions & 7 deletions sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import flet.version
import flet_cli.utils.processes as processes
from flet.utils import copy_tree, slugify
from flet.utils import copy_tree, get_bool_env_var, slugify
from flet.utils.deprecated import deprecated_warning
from flet_cli.commands.flutter_base import (
BaseFlutterCommand,
Expand Down Expand Up @@ -655,6 +655,31 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
default=None,
help="Android signing key alias [env: FLET_ANDROID_SIGNING_KEY_ALIAS=]",
)
parser.add_argument(
"--macos-signing-identity",
dest="macos_signing_identity",
help='"Developer ID Application" certificate name, its SHA-1 '
'fingerprint, or "-" for ad-hoc, used to code-sign the app bundle '
"(macos only) [env: FLET_MACOS_SIGNING_IDENTITY=]",
)
parser.add_argument(
"--macos-notarize",
dest="macos_notarize",
action=argparse.BooleanOptionalAction,
default=None,
help="Submit the signed app to the Apple notary service and staple "
"the ticket; requires --macos-signing-identity and notary "
"credentials (macos only)",
)
parser.add_argument(
"--macos-notary-profile",
dest="macos_notary_profile",
help="Keychain profile name created with `xcrun notarytool "
"store-credentials` to authenticate with the Apple notary service; "
"alternatively set the APPLE_API_KEY, APPLE_API_KEY_ID and "
"APPLE_API_ISSUER environment variables (macos only) "
"[env: FLET_MACOS_NOTARY_PROFILE=]",
)
parser.add_argument(
"--build-number",
dest="build_number",
Expand Down Expand Up @@ -924,6 +949,7 @@ def setup_template_data(self):
macos_entitlements = {
"com.apple.security.app-sandbox": False,
"com.apple.security.cs.allow-jit": True,
"com.apple.security.cs.allow-unsigned-executable-memory": True,
"com.apple.security.network.client": True,
"com.apple.security.network.server": True,
"com.apple.security.files.user-selected.read-write": True,
Expand Down Expand Up @@ -1288,6 +1314,7 @@ def _xml_attr_value(v):
"route_url_strategy": (
self.options.route_url_strategy
or self.get_pyproject("tool.flet.web.route_url_strategy")
or os.getenv("FLET_WEB_ROUTE_URL_STRATEGY")
or "path"
),
# "canvaskit" (dart2js), not "auto": with "auto" Chromium browsers
Expand All @@ -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"
),
"pwa_background_color": (
Expand All @@ -1313,7 +1341,9 @@ def _xml_attr_value(v):
or self.get_pyproject("tool.flet.web.wasm") == False # noqa: E712
),
"no_cdn": (
self.options.no_cdn or self.get_pyproject("tool.flet.web.cdn") == False # noqa: E712
self.options.no_cdn
or self.get_pyproject("tool.flet.web.cdn") == False # noqa: E712
or bool(get_bool_env_var("FLET_WEB_NO_CDN"))
),
# Surface the resolved Pyodide release to the cookiecutter
# context so the web template's index.html can wire the
Expand Down Expand Up @@ -2853,11 +2883,13 @@ def find_platform_image(
# incompatible formats so flutter_launcher_icons gets a decodable file.
images = list(
filter(
lambda p: not (
(ext := Path(p).suffix.lower()) == ".icns"
and self.target_platform != "macos"
or ext == ".ico"
and self.target_platform != "windows"
lambda p: (
not (
(ext := Path(p).suffix.lower()) == ".icns"
and self.target_platform != "macos"
or ext == ".ico"
and self.target_platform != "windows"
)
),
glob.glob(str(src_path.joinpath(f"{image_name}.*"))),
)
Expand Down
18 changes: 15 additions & 3 deletions sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
from pathlib import Path

from flet.controls.types import RouteUrlStrategy, WebRenderer
from flet.utils import copy_tree, is_within_directory, random_string
from flet.utils import (
copy_tree,
get_bool_env_var,
is_within_directory,
random_string,
)
from flet_cli.commands.base import BaseCommand
from flet_cli.utils.project_dependencies import (
get_poetry_dependencies,
Expand Down Expand Up @@ -146,7 +151,8 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
action="store_true",
default=False,
help="Disable loading of CanvasKit, Pyodide, and fonts from CDNs. "
"Use this for full offline deployments or air-gapped environments",
"Use this for full offline deployments or air-gapped environments "
"[env: FLET_WEB_NO_CDN=]",
)

def handle(self, options: argparse.Namespace) -> None:
Expand Down Expand Up @@ -343,7 +349,11 @@ def filter_tar(tarinfo: tarfile.TarInfo):
"tool.flet.web.pwa_theme_color"
)

no_cdn = options.no_cdn or get_pyproject("tool.flet.web.cdn") == False # noqa: E712
no_cdn = (
options.no_cdn
or get_pyproject("tool.flet.web.cdn") == False # noqa: E712
or bool(get_bool_env_var("FLET_WEB_NO_CDN"))
)

print("Patching index.html")
patch_index_html(
Expand All @@ -362,11 +372,13 @@ def filter_tar(tarinfo: tarfile.TarInfo):
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 thread
ndonkoHenri marked this conversation as resolved.
no_cdn=no_cdn,
Expand Down
Loading
Loading