Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .github/workflows/flet-build-test-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ env:
PYTHONUTF8: 1

# https://flet.dev/docs/reference/environment-variables
FLET_CLI_NO_RICH_OUTPUT: 1
# Plain output plus GitHub Actions groups/annotations for each build step.
FLET_CLI_LOG_FORMAT: github

FLET_BUILD_EXTRA_ARGS: ${{ inputs.extra_build_args }}

Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/flet-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ env:
PYTHONUTF8: 1

# https://flet.dev/docs/reference/environment-variables
FLET_CLI_NO_RICH_OUTPUT: 1
# Plain output plus GitHub Actions groups/annotations for each build step.
FLET_CLI_LOG_FORMAT: github

# Extra args passed via workflow_dispatch (empty for push/PR)
FLET_PACK_EXTRA_ARGS: ${{ github.event_name == 'workflow_dispatch' && inputs.extra_pack_args || '' }}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* Add `Theme.surfaces` and the `ThemeSurfaces` enum (`NEUTRAL`, `TONAL`) to choose whether surface colors are neutral or tinted with the seed color ([#6921](https://github.com/flet-dev/flet/issues/6921)) by @FeodorFitsner.
* Add `flet test --update-failed-goldens` (`FLET_TEST_GOLDEN=failed`): compares screenshots as usual and overwrites only golden images that are missing or no longer match, so unchanged goldens stay untouched ([#6921](https://github.com/flet-dev/flet/issues/6921)) by @FeodorFitsner.
* Add `bgcolor` to `FletTestApp.take_page_controls_screenshot()`, `assert_control_screenshot()` and `wrap_page_controls_in_screenshot()` to paint an opaque background behind the captured controls instead of a transparent one ([#6918](https://github.com/flet-dev/flet/pull/6918)) by @InesaFitsner.
* Add `--log-format {rich,plain,github}` (`FLET_CLI_LOG_FORMAT`) to `flet build`, `flet debug` and `flet test`. `github` prints plain output plus GitHub Actions workflow commands: a collapsible group per build step and annotations for warnings and errors; `plain` is the same as `--no-rich-output`. See [Log formats](/docs/cli/flet-build#log-formats) ([#6947](https://github.com/flet-dev/flet/issues/6947)) by @FeodorFitsner.
* Install `uvicorn[standard]` with `flet-web` again on Windows on ARM with Python 3.12 or later, now that `httptools` 0.9.0 ships wheels for it; Python 3.10 and 3.11 there keep plain `uvicorn`, because PyYAML has no wheels for them ([#6927](https://github.com/flet-dev/flet/pull/6927)) by @ndonkoHenri.

### Bug fixes
Expand Down
201 changes: 107 additions & 94 deletions sdk/python/packages/flet-cli/src/flet_cli/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
from pathlib import Path
from typing import Callable, Optional

from rich.console import Group
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
Expand Down Expand Up @@ -94,11 +91,9 @@ def handle(self, options: argparse.Namespace) -> None:

super().handle(options)
assert self.target_platform
self.status = console.status(
f"[bold blue]Initializing {self.target_platform} build...",
spinner="bouncingBall",
)
with Live(Group(self.status, self.progress), console=console) as self.live:
with self.live_status(
f"[bold blue]Initializing {self.target_platform} build..."
):
self.initialize_command()
self.validate_target_platform()
self.validate_entry_point()
Expand All @@ -119,6 +114,21 @@ def handle(self, options: argparse.Namespace) -> None:
if self.target_platform == "macos":
self.sign_macos_app()

unsigned_ipa = self.target_platform == "ipa" and not self.built_ipa()
if self.no_rich_output:
# Non-interactive: the exit code is the result, no banner; only
# the missing .ipa is worth a warning.
if unsigned_ipa:
self.warn(
"No .ipa was produced: Xcode exports one only for a "
"signed app. Configure a provisioning profile and a "
"signing certificate to get an uploadable bundle: "
"https://flet.dev/docs/publish/ios",
title="iOS signing",
)
self.cleanup(0)
return

self.cleanup(
0,
message=(
Expand All @@ -139,7 +149,7 @@ def handle(self, options: argparse.Namespace) -> None:
"[cyan]provisioning profile[/cyan] and a "
"[cyan]signing certificate[/cyan] to get an uploadable "
"bundle: https://flet.dev/docs/publish/ios"
if self.target_platform == "ipa" and not self.built_ipa()
if unsigned_ipa
else ""
)
),
Expand Down Expand Up @@ -219,33 +229,32 @@ def run_flutter(self):
assert self.platforms
assert self.target_platform

self.update_status(
with self.step(
f"[bold blue]Building [cyan]"
f"{self.platforms[self.target_platform]['status_text']}[/cyan]..."
)

# Clear the build output directories of artifacts from previous runs. Flutter
# only ever adds files to them, and copy_build_output harvests them wholesale —
# so without this, a previous build with different options (e.g. --arch,
# --split-per-abi, or a renamed product) would leak its artifacts into the
# user's output directory.
assert self.flutter_dir
flutter_dir = self.flutter_dir.resolve()
for output in self.platforms[self.target_platform]["outputs"]:
output_dir = Path(
os.path.dirname(self.resolve_output_path(output))
).resolve()
# only delete directories that are strictly inside the generated Flutter
# project (and never the project directory itself).
if output_dir != flutter_dir and output_dir.is_relative_to(flutter_dir):
shutil.rmtree(output_dir, ignore_errors=True)

self._run_flutter_command()
):
# Clear the build output directories of artifacts from previous runs.
# Flutter only ever adds files to them, and copy_build_output harvests
# them wholesale — so without this, a previous build with different
# options (e.g. --arch, --split-per-abi, or a renamed product) would
# leak its artifacts into the user's output directory.
assert self.flutter_dir
flutter_dir = self.flutter_dir.resolve()
for output in self.platforms[self.target_platform]["outputs"]:
output_dir = Path(
os.path.dirname(self.resolve_output_path(output))
).resolve()
# only delete directories that are strictly inside the generated Flutter
# project (and never the project directory itself).
if output_dir != flutter_dir and output_dir.is_relative_to(flutter_dir):
shutil.rmtree(output_dir, ignore_errors=True)

self._run_flutter_command()

console.log(
f"Built [cyan]{self.describe_build_output()}"
f"[/cyan] {self.emojis['checkmark']}",
)
console.log(
f"Built [cyan]{self.describe_build_output()}"
f"[/cyan] {self.emojis['checkmark']}",
)

def built_ipa(self) -> bool:
"""
Expand Down Expand Up @@ -646,58 +655,60 @@ 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:
# Each lane scopes resolution to the certificate type Apple's
# services accept for it — which also lets an unset identity
# auto-discover the only candidate. The plain lane stays
# unscoped: Apple Development or corporate certificates are
# legitimate there.
if distribution == "app-store":
resolved = resolve_identity(identity, types=APP_STORE_CERTIFICATE_TYPES)
elif distribution == "developer-id":
resolved = resolve_identity(
identity, types=DEVELOPER_ID_CERTIFICATE_TYPES
)
else:
resolved = resolve_identity(identity)
if not identity:
console.log(f"Signing identity: {resolved.name}")
if distribution == "developer-id" and resolved.is_adhoc:
self.cleanup(
1,
"Developer ID distribution requires a Developer ID "
'identity; ad-hoc ("-") signed apps cannot be notarized.',
)

if distribution == "app-store":
self._sign_macos_app_store(app_path, resolved, entitlements, log)
return
with self.step(f"[bold blue]Signing [cyan]{app_path.name}[/cyan]..."):
try:
# Each lane scopes resolution to the certificate type Apple's
# services accept for it — which also lets an unset identity
# auto-discover the only candidate. The plain lane stays
# unscoped: Apple Development or corporate certificates are
# legitimate there.
if distribution == "app-store":
resolved = resolve_identity(
identity, types=APP_STORE_CERTIFICATE_TYPES
)
elif distribution == "developer-id":
resolved = resolve_identity(
identity, types=DEVELOPER_ID_CERTIFICATE_TYPES
)
else:
resolved = resolve_identity(identity)
if not identity:
console.log(f"Signing identity: {resolved.name}")
if distribution == "developer-id" and resolved.is_adhoc:
self.cleanup(
1,
"Developer ID distribution requires a Developer ID "
'identity; ad-hoc ("-") signed apps cannot be notarized.',
)

signed_count = sign_app(
app_path,
resolved,
entitlements=entitlements,
log=log,
)
console.log(
f"Signed [cyan]{app_path.name}[/cyan] ({signed_count} binaries, "
f"identity: {resolved.description}) {self.emojis['checkmark']}"
)
if distribution == "app-store":
self._sign_macos_app_store(app_path, resolved, entitlements, log)
return

if distribution == "developer-id":
credentials = self._macos_notary_credentials()
self.update_status(
f"[bold blue]Notarizing [cyan]{app_path.name}[/cyan] "
"(this can take a few minutes)...",
signed_count = sign_app(
app_path,
resolved,
entitlements=entitlements,
log=log,
)
notarize_and_staple(app_path, credentials, log=log)
console.log(
f"Notarized and stapled [cyan]{app_path.name}[/cyan] "
f"{self.emojis['checkmark']}"
f"Signed [cyan]{app_path.name}[/cyan] ({signed_count} binaries, "
f"identity: {resolved.description}) {self.emojis['checkmark']}"
)
except MacOSSigningError as e:
self.cleanup(1, str(e))

if distribution == "developer-id":
credentials = self._macos_notary_credentials()
with self.step(
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 _sign_macos_app_store(
self,
Expand Down Expand Up @@ -797,12 +808,14 @@ def _sign_macos_app_store(
"or `[tool.flet.macos.info]` in pyproject.toml.",
)
if "ITSAppUsesNonExemptEncryption" not in info:
console.log(
"[yellow]Warning: ITSAppUsesNonExemptEncryption is not set in "
self.warn(
"ITSAppUsesNonExemptEncryption is not set in "
"Info.plist — App Store Connect will ask the export-compliance "
"question manually for every build. Set it with --info-plist "
"ITSAppUsesNonExemptEncryption=False if your app only uses "
"standard encryption.[/yellow]"
"standard encryption.",
title="App Store",
style="yellow",
)

profile_app_id = profile_application_identifier(profile_path)
Expand Down Expand Up @@ -845,17 +858,17 @@ def _sign_macos_app_store(
f"{self.emojis['checkmark']}"
)

self.update_status(f"[bold blue]Packaging [cyan]{app_path.stem}.pkg[/cyan]...")
pkg = build_pkg(
app_path,
installer,
self.out_dir / f"{app_path.stem}.pkg",
log=log,
)
console.log(
f"Packaged [cyan]{pkg.name}[/cyan] for App Store Connect "
f"(installer identity: {installer.name}) {self.emojis['checkmark']}"
)
with self.step(f"[bold blue]Packaging [cyan]{app_path.stem}.pkg[/cyan]..."):
pkg = build_pkg(
app_path,
installer,
self.out_dir / f"{app_path.stem}.pkg",
log=log,
)
console.log(
f"Packaged [cyan]{pkg.name}[/cyan] for App Store Connect "
f"(installer identity: {installer.name}) {self.emojis['checkmark']}"
)

def _macos_notary_credentials(self) -> NotaryCredentials:
"""
Expand Down
Loading
Loading