flet-cli 0.85.3.dev0__py3-none-any.whl → 0.86.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flet_cli/cli.py +50 -7
- flet_cli/commands/build.py +36 -1
- flet_cli/commands/build_base.py +552 -45
- flet_cli/commands/clean.py +76 -0
- flet_cli/commands/create.py +4 -0
- flet_cli/commands/mcp.py +67 -0
- flet_cli/commands/publish.py +31 -1
- flet_cli/commands/run.py +89 -3
- flet_cli/commands/test.py +292 -0
- flet_cli/commands/test_host.py +11 -0
- flet_cli/utils/android.py +53 -0
- flet_cli/utils/android_sdk.py +1 -0
- flet_cli/utils/distros.py +6 -3
- flet_cli/utils/flutter.py +50 -20
- flet_cli/utils/linux_deps.py +36 -0
- flet_cli/utils/pyodide.py +140 -0
- flet_cli/utils/python_versions.py +213 -0
- flet_cli/utils/template_cache.py +48 -0
- flet_cli/version.py +1 -1
- {flet_cli-0.85.3.dev0.dist-info → flet_cli-0.86.0.dev0.dist-info}/METADATA +5 -2
- {flet_cli-0.85.3.dev0.dist-info → flet_cli-0.86.0.dev0.dist-info}/RECORD +24 -15
- {flet_cli-0.85.3.dev0.dist-info → flet_cli-0.86.0.dev0.dist-info}/WHEEL +0 -0
- {flet_cli-0.85.3.dev0.dist-info → flet_cli-0.86.0.dev0.dist-info}/entry_points.txt +0 -0
- {flet_cli-0.85.3.dev0.dist-info → flet_cli-0.86.0.dev0.dist-info}/top_level.txt +0 -0
flet_cli/commands/build_base.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import base64
|
|
2
3
|
import copy
|
|
3
4
|
import glob
|
|
5
|
+
import json
|
|
4
6
|
import os
|
|
5
7
|
import platform
|
|
6
8
|
import shutil
|
|
@@ -15,6 +17,7 @@ from rich.table import Column, Table
|
|
|
15
17
|
import flet.version
|
|
16
18
|
import flet_cli.utils.processes as processes
|
|
17
19
|
from flet.utils import copy_tree, slugify
|
|
20
|
+
from flet.utils.deprecated import deprecated_warning
|
|
18
21
|
from flet_cli.commands.flutter_base import (
|
|
19
22
|
BaseFlutterCommand,
|
|
20
23
|
console,
|
|
@@ -23,6 +26,10 @@ from flet_cli.commands.flutter_base import (
|
|
|
23
26
|
verbose2_style,
|
|
24
27
|
warning_style,
|
|
25
28
|
)
|
|
29
|
+
from flet_cli.utils.android import (
|
|
30
|
+
ANDROID_ARCH_TO_FLUTTER_TARGET_PLATFORM,
|
|
31
|
+
excluded_android_abis,
|
|
32
|
+
)
|
|
26
33
|
from flet_cli.utils.cli import parse_cli_bool_value
|
|
27
34
|
from flet_cli.utils.hash_stamp import HashStamp
|
|
28
35
|
from flet_cli.utils.merge import merge_dict
|
|
@@ -32,12 +39,28 @@ from flet_cli.utils.project_dependencies import (
|
|
|
32
39
|
get_project_dependencies,
|
|
33
40
|
)
|
|
34
41
|
from flet_cli.utils.pyproject_toml import load_pyproject_toml
|
|
42
|
+
from flet_cli.utils.python_versions import (
|
|
43
|
+
UnsupportedPythonVersionError,
|
|
44
|
+
resolve_python_version,
|
|
45
|
+
)
|
|
35
46
|
|
|
36
47
|
DEFAULT_TEMPLATE_URL = (
|
|
37
48
|
"https://github.com/flet-dev/flet/releases/download/"
|
|
38
49
|
"v{version}/flet-build-template.zip"
|
|
39
50
|
)
|
|
40
51
|
|
|
52
|
+
# Android (serious_python native-mmap packaging): pure Python ships in stored zips
|
|
53
|
+
# read via zipimport, which breaks packages that read bundled data through a real
|
|
54
|
+
# filesystem path (__file__ / pkg_resources) instead of importlib.resources. Such
|
|
55
|
+
# packages are shipped extracted to disk via --android-extract-packages or
|
|
56
|
+
# [tool.flet.android].extract_packages.
|
|
57
|
+
#
|
|
58
|
+
# The default set is empty: the common offenders read their data via
|
|
59
|
+
# importlib.resources, which is zip-safe (e.g. certifi.where() works from the zip —
|
|
60
|
+
# importlib.resources.as_file() extracts cacert.pem to a temp file on demand). Add
|
|
61
|
+
# real offenders here as they are found.
|
|
62
|
+
ANDROID_DEFAULT_EXTRACT_PACKAGES: list[str] = []
|
|
63
|
+
|
|
41
64
|
|
|
42
65
|
class BaseBuildCommand(BaseFlutterCommand):
|
|
43
66
|
"""
|
|
@@ -244,10 +267,13 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
244
267
|
parser.add_argument(
|
|
245
268
|
"--arch",
|
|
246
269
|
dest="target_arch",
|
|
270
|
+
action="extend",
|
|
247
271
|
nargs="+",
|
|
248
272
|
default=[],
|
|
249
273
|
help="Build for specific CPU architectures "
|
|
250
|
-
"(used in macOS and Android builds only).
|
|
274
|
+
"(used in macOS and Android builds only). "
|
|
275
|
+
"Android: arm64-v8a, armeabi-v7a, x86_64; macOS: arm64, x64. "
|
|
276
|
+
"Example: `--arch arm64-v8a`",
|
|
251
277
|
)
|
|
252
278
|
parser.add_argument(
|
|
253
279
|
"--exclude",
|
|
@@ -263,7 +289,8 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
263
289
|
dest="clear_cache",
|
|
264
290
|
action="store_true",
|
|
265
291
|
default=None,
|
|
266
|
-
help="Remove any existing build cache before starting the build process"
|
|
292
|
+
help="Remove any existing build cache before starting the build process. "
|
|
293
|
+
"Deprecated: use the `flet clean` command instead",
|
|
267
294
|
)
|
|
268
295
|
parser.add_argument(
|
|
269
296
|
"--project",
|
|
@@ -448,16 +475,30 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
448
475
|
parser.add_argument(
|
|
449
476
|
"--compile-app",
|
|
450
477
|
dest="compile_app",
|
|
451
|
-
action=
|
|
478
|
+
action=argparse.BooleanOptionalAction,
|
|
452
479
|
default=None,
|
|
453
|
-
help="Pre-compile app's `.py` files to `.pyc`"
|
|
480
|
+
help="Pre-compile app's `.py` files to `.pyc` (on by default; "
|
|
481
|
+
"use --no-compile-app to disable)",
|
|
454
482
|
)
|
|
455
483
|
parser.add_argument(
|
|
456
484
|
"--compile-packages",
|
|
457
485
|
dest="compile_packages",
|
|
458
|
-
action=
|
|
486
|
+
action=argparse.BooleanOptionalAction,
|
|
487
|
+
default=None,
|
|
488
|
+
help="Pre-compile site packages' `.py` files to `.pyc` (on by default; "
|
|
489
|
+
"use --no-compile-packages to disable)",
|
|
490
|
+
)
|
|
491
|
+
parser.add_argument(
|
|
492
|
+
"--swift-package-manager",
|
|
493
|
+
dest="swift_package_manager",
|
|
494
|
+
action=argparse.BooleanOptionalAction,
|
|
459
495
|
default=None,
|
|
460
|
-
help="
|
|
496
|
+
help="Integrate the embedded Python runtime via Swift Package Manager "
|
|
497
|
+
"(default) or CocoaPods for iOS/macOS builds. On by default, matching "
|
|
498
|
+
"Flutter 3.44+ which uses SPM by default (other non-SPM plugins still "
|
|
499
|
+
"build with CocoaPods alongside it). Use --no-swift-package-manager (or "
|
|
500
|
+
"`swift_package_manager = false` under [tool.flet]) only if you've "
|
|
501
|
+
"disabled Swift Package Manager in Flutter.",
|
|
461
502
|
)
|
|
462
503
|
parser.add_argument(
|
|
463
504
|
"--cleanup-app",
|
|
@@ -497,10 +538,28 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
497
538
|
parser.add_argument(
|
|
498
539
|
"--source-packages",
|
|
499
540
|
dest="source_packages",
|
|
541
|
+
action="extend",
|
|
500
542
|
nargs="+",
|
|
501
543
|
default=[],
|
|
502
544
|
help="The list of Python packages to install from source distributions",
|
|
503
545
|
)
|
|
546
|
+
parser.add_argument(
|
|
547
|
+
"--android-extract-packages",
|
|
548
|
+
dest="android_extract_packages",
|
|
549
|
+
nargs="+",
|
|
550
|
+
default=[],
|
|
551
|
+
help="Android only: Python packages (relative paths) to ship extracted "
|
|
552
|
+
"to disk instead of inside the app zip — for packages that read bundled "
|
|
553
|
+
"data via __file__ / pkg_resources rather than importlib.resources",
|
|
554
|
+
)
|
|
555
|
+
parser.add_argument(
|
|
556
|
+
"--python-version",
|
|
557
|
+
dest="python_version",
|
|
558
|
+
type=str,
|
|
559
|
+
default=None,
|
|
560
|
+
help="Python version to bundle (e.g. 3.13). Defaults to the latest "
|
|
561
|
+
"supported version, or is parsed from project.requires-python.",
|
|
562
|
+
)
|
|
504
563
|
parser.add_argument(
|
|
505
564
|
"--info-plist",
|
|
506
565
|
dest="info_plist",
|
|
@@ -552,6 +611,7 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
552
611
|
"--permissions",
|
|
553
612
|
dest="permissions",
|
|
554
613
|
type=str.lower,
|
|
614
|
+
action="extend",
|
|
555
615
|
nargs="+",
|
|
556
616
|
default=[],
|
|
557
617
|
choices=["location", "camera", "microphone", "photo_library"],
|
|
@@ -647,6 +707,22 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
647
707
|
"""
|
|
648
708
|
|
|
649
709
|
super().handle(options)
|
|
710
|
+
|
|
711
|
+
if getattr(self.options, "clear_cache", None):
|
|
712
|
+
deprecated_warning(
|
|
713
|
+
name="--clear-cache",
|
|
714
|
+
reason="Use the `flet clean` command instead.",
|
|
715
|
+
version="0.86.0",
|
|
716
|
+
delete_version="0.89.0",
|
|
717
|
+
type="flag",
|
|
718
|
+
)
|
|
719
|
+
console.print(
|
|
720
|
+
"Warning: the `--clear-cache` flag is deprecated since version "
|
|
721
|
+
"0.86.0 and will be removed in version 0.89.0. "
|
|
722
|
+
"Use the `flet clean` command instead.",
|
|
723
|
+
style=warning_style,
|
|
724
|
+
)
|
|
725
|
+
|
|
650
726
|
if "target_platform" in self.options:
|
|
651
727
|
self.target_platform = self.options.target_platform
|
|
652
728
|
|
|
@@ -693,6 +769,29 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
693
769
|
self.pubspec_path = str(self.flutter_dir.joinpath("pubspec.yaml"))
|
|
694
770
|
self.get_pyproject = load_pyproject_toml(self.python_app_path)
|
|
695
771
|
|
|
772
|
+
try:
|
|
773
|
+
self.python_release = resolve_python_version(
|
|
774
|
+
self.options.python_version, self.get_pyproject
|
|
775
|
+
)
|
|
776
|
+
except UnsupportedPythonVersionError as e:
|
|
777
|
+
self.cleanup(1, str(e))
|
|
778
|
+
|
|
779
|
+
# Changing the bundled Python version invalidates the compiled bytecode
|
|
780
|
+
# baked into the previous build's native bundles (stdlib/site-packages
|
|
781
|
+
# .pyc). Reusing the build directory would mix versions and crash at
|
|
782
|
+
# runtime with "bad magic number". Force a clean rebuild on a switch.
|
|
783
|
+
version_marker = self.build_dir / ".python-version"
|
|
784
|
+
if self.build_dir.exists() and version_marker.exists():
|
|
785
|
+
previous = version_marker.read_text(encoding="utf-8").strip()
|
|
786
|
+
if previous and previous != self.python_release.short:
|
|
787
|
+
console.log(
|
|
788
|
+
f"Bundled Python version changed ({previous} -> "
|
|
789
|
+
f"{self.python_release.short}); cleaning the build directory."
|
|
790
|
+
)
|
|
791
|
+
shutil.rmtree(self.build_dir, ignore_errors=True)
|
|
792
|
+
self.build_dir.mkdir(parents=True, exist_ok=True)
|
|
793
|
+
version_marker.write_text(self.python_release.short, encoding="utf-8")
|
|
794
|
+
|
|
696
795
|
def validate_target_platform(self):
|
|
697
796
|
"""
|
|
698
797
|
Validate whether current host OS can build the selected target platform.
|
|
@@ -794,6 +893,13 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
794
893
|
or self.get_pyproject("project.name")
|
|
795
894
|
or self.python_app_path.name
|
|
796
895
|
)
|
|
896
|
+
# Under integration test, `flutter test -d <desktop>` launches the built
|
|
897
|
+
# binary by the project name (the Flutter pubspec `name`), but the
|
|
898
|
+
# Windows/Linux runner sets the executable's OUTPUT_NAME to artifact_name.
|
|
899
|
+
# When they differ (e.g. `artifact = "my-app"` vs project `my_app`) the
|
|
900
|
+
# test host can't find the binary. Pin them equal in test mode.
|
|
901
|
+
if getattr(self, "test_mode", False):
|
|
902
|
+
artifact_name = project_name
|
|
797
903
|
product_name = (
|
|
798
904
|
self.options.product_name
|
|
799
905
|
or self.get_pyproject("tool.flet.product")
|
|
@@ -826,6 +932,7 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
826
932
|
"android.hardware.touchscreen": False,
|
|
827
933
|
}
|
|
828
934
|
android_meta_data = {}
|
|
935
|
+
android_providers = {}
|
|
829
936
|
|
|
830
937
|
# merge values from "--permissions" arg:
|
|
831
938
|
for p in (
|
|
@@ -974,6 +1081,88 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
974
1081
|
else:
|
|
975
1082
|
self.cleanup(1, f"Invalid Android meta-data option: {p}")
|
|
976
1083
|
|
|
1084
|
+
android_providers = merge_dict(
|
|
1085
|
+
android_providers,
|
|
1086
|
+
self.get_pyproject("tool.flet.android.provider") or {},
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
def _xml_attr_value(v):
|
|
1090
|
+
# Android XML expects lowercase booleans.
|
|
1091
|
+
if isinstance(v, bool):
|
|
1092
|
+
return "true" if v else "false"
|
|
1093
|
+
return v
|
|
1094
|
+
|
|
1095
|
+
normalized_providers = {}
|
|
1096
|
+
for key, value in android_providers.items():
|
|
1097
|
+
if value is False or value == {}:
|
|
1098
|
+
continue
|
|
1099
|
+
if value is True:
|
|
1100
|
+
self.cleanup(
|
|
1101
|
+
1,
|
|
1102
|
+
f"Invalid Android provider value for {key}: 'true' is not "
|
|
1103
|
+
"supported. Use an inline table of attributes, or 'false' "
|
|
1104
|
+
"to skip.",
|
|
1105
|
+
)
|
|
1106
|
+
if not isinstance(value, dict):
|
|
1107
|
+
self.cleanup(
|
|
1108
|
+
1,
|
|
1109
|
+
f"Invalid Android provider value for {key}: "
|
|
1110
|
+
f"{type(value).__name__}. Expected boolean or inline table.",
|
|
1111
|
+
)
|
|
1112
|
+
normalized = {}
|
|
1113
|
+
for ak, av in value.items():
|
|
1114
|
+
if ak == "name":
|
|
1115
|
+
self.cleanup(
|
|
1116
|
+
1,
|
|
1117
|
+
f"Invalid Android provider attribute for {key}: "
|
|
1118
|
+
"'name' is reserved and is taken from the table key.",
|
|
1119
|
+
)
|
|
1120
|
+
if ak == "meta_data":
|
|
1121
|
+
if not isinstance(av, dict):
|
|
1122
|
+
self.cleanup(
|
|
1123
|
+
1,
|
|
1124
|
+
f"Invalid Android provider meta_data for {key}: "
|
|
1125
|
+
f"{type(av).__name__}. Expected inline table.",
|
|
1126
|
+
)
|
|
1127
|
+
normalized_meta = {}
|
|
1128
|
+
for mk, mv in av.items():
|
|
1129
|
+
if isinstance(mv, (str, bool, int, float)):
|
|
1130
|
+
normalized_meta[mk] = _xml_attr_value(mv)
|
|
1131
|
+
continue
|
|
1132
|
+
if isinstance(mv, dict):
|
|
1133
|
+
normalized_attrs = {}
|
|
1134
|
+
for attr_key, attr_value in mv.items():
|
|
1135
|
+
if not isinstance(attr_value, (str, bool, int, float)):
|
|
1136
|
+
self.cleanup(
|
|
1137
|
+
1,
|
|
1138
|
+
f"Invalid Android provider meta-data "
|
|
1139
|
+
f"attribute value for "
|
|
1140
|
+
f"{key}.meta_data.{mk}.{attr_key}: "
|
|
1141
|
+
f"{type(attr_value).__name__}. "
|
|
1142
|
+
"Expected string, boolean, or number.",
|
|
1143
|
+
)
|
|
1144
|
+
normalized_attrs[attr_key] = _xml_attr_value(attr_value)
|
|
1145
|
+
normalized_meta[mk] = normalized_attrs
|
|
1146
|
+
continue
|
|
1147
|
+
self.cleanup(
|
|
1148
|
+
1,
|
|
1149
|
+
f"Invalid Android provider meta-data value for "
|
|
1150
|
+
f"{key}.meta_data.{mk}: {type(mv).__name__}. "
|
|
1151
|
+
"Expected string, boolean, number, or inline table.",
|
|
1152
|
+
)
|
|
1153
|
+
normalized["meta_data"] = normalized_meta
|
|
1154
|
+
continue
|
|
1155
|
+
if not isinstance(av, (str, bool, int, float)):
|
|
1156
|
+
self.cleanup(
|
|
1157
|
+
1,
|
|
1158
|
+
f"Invalid Android provider attribute value for "
|
|
1159
|
+
f"{key}.{ak}: {type(av).__name__}. "
|
|
1160
|
+
"Expected string, boolean, or number.",
|
|
1161
|
+
)
|
|
1162
|
+
normalized[ak] = _xml_attr_value(av)
|
|
1163
|
+
normalized_providers[key] = normalized
|
|
1164
|
+
android_providers = normalized_providers
|
|
1165
|
+
|
|
977
1166
|
deep_linking_scheme = (
|
|
978
1167
|
self.get_pyproject("tool.flet.ios.deep_linking.scheme")
|
|
979
1168
|
if self.package_platform == "iOS"
|
|
@@ -1003,6 +1192,40 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1003
1192
|
or self.get_pyproject(f"tool.flet.{self.config_platform}.target_arch")
|
|
1004
1193
|
or self.get_pyproject("tool.flet.target_arch")
|
|
1005
1194
|
)
|
|
1195
|
+
target_arch = (
|
|
1196
|
+
target_arch
|
|
1197
|
+
if isinstance(target_arch, list)
|
|
1198
|
+
else [target_arch]
|
|
1199
|
+
if isinstance(target_arch, str)
|
|
1200
|
+
else []
|
|
1201
|
+
)
|
|
1202
|
+
if self.package_platform == "Android":
|
|
1203
|
+
invalid_archs = [
|
|
1204
|
+
arch
|
|
1205
|
+
for arch in target_arch
|
|
1206
|
+
if arch not in ANDROID_ARCH_TO_FLUTTER_TARGET_PLATFORM
|
|
1207
|
+
]
|
|
1208
|
+
if invalid_archs:
|
|
1209
|
+
self.cleanup(
|
|
1210
|
+
1,
|
|
1211
|
+
f"Invalid Android architecture(s): {', '.join(invalid_archs)}.\n"
|
|
1212
|
+
f"Supported: "
|
|
1213
|
+
f"{', '.join(ANDROID_ARCH_TO_FLUTTER_TARGET_PLATFORM)}.\n"
|
|
1214
|
+
f"Docs: https://flet.dev/docs/publish/android#supported-target-architectures",
|
|
1215
|
+
)
|
|
1216
|
+
python_abis = list(self.python_release.android_abis)
|
|
1217
|
+
unsupported_archs = [a for a in target_arch if a not in python_abis]
|
|
1218
|
+
if unsupported_archs:
|
|
1219
|
+
self.cleanup(
|
|
1220
|
+
1,
|
|
1221
|
+
f"Architecture(s) not supported by Python "
|
|
1222
|
+
f"{self.python_release.short}: {', '.join(unsupported_archs)}.\n"
|
|
1223
|
+
f"Supported: {', '.join(python_abis)}.\n"
|
|
1224
|
+
f"Docs: https://flet.dev/docs/publish/android#supported-target-architectures",
|
|
1225
|
+
)
|
|
1226
|
+
if not target_arch:
|
|
1227
|
+
# Build only for the ABIs the bundled Python supports.
|
|
1228
|
+
target_arch = python_abis
|
|
1006
1229
|
|
|
1007
1230
|
ios_export_method = (
|
|
1008
1231
|
self.options.ios_export_method
|
|
@@ -1044,6 +1267,7 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1044
1267
|
self.target_platform in ["ipa"]
|
|
1045
1268
|
and not ios_provisioning_profile
|
|
1046
1269
|
and not self.debug_platform
|
|
1270
|
+
and not getattr(self, "test_mode", False)
|
|
1047
1271
|
):
|
|
1048
1272
|
console.print(
|
|
1049
1273
|
Panel(
|
|
@@ -1084,8 +1308,16 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1084
1308
|
"no_cdn": (
|
|
1085
1309
|
self.options.no_cdn or self.get_pyproject("tool.flet.web.cdn") == False # noqa: E712
|
|
1086
1310
|
),
|
|
1311
|
+
# Surface the resolved Pyodide release to the cookiecutter
|
|
1312
|
+
# context so the web template's index.html can wire the
|
|
1313
|
+
# correct jsdelivr URL when CDN mode is on.
|
|
1314
|
+
"pyodide_version": self.python_release.pyodide,
|
|
1087
1315
|
"base_url": f"/{base_url}/" if base_url else "/",
|
|
1088
1316
|
"split_per_abi": split_per_abi,
|
|
1317
|
+
# Enabled by `flet test` to scaffold integration-test wiring
|
|
1318
|
+
# (integration_test/ + flutter_test dev deps). Default False so
|
|
1319
|
+
# normal `flet build`/`flet debug` output is unaffected.
|
|
1320
|
+
"test_mode": getattr(self, "test_mode", False),
|
|
1089
1321
|
"project_name": project_name,
|
|
1090
1322
|
"project_name_slug": project_name_slug,
|
|
1091
1323
|
"artifact_name": artifact_name,
|
|
@@ -1114,11 +1346,11 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1114
1346
|
"options": {
|
|
1115
1347
|
"package_platform": self.package_platform,
|
|
1116
1348
|
"config_platform": self.config_platform,
|
|
1117
|
-
"
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
if
|
|
1349
|
+
"python_version": self.python_release.short,
|
|
1350
|
+
"target_arch": target_arch,
|
|
1351
|
+
"android_excluded_abis": (
|
|
1352
|
+
excluded_android_abis(target_arch)
|
|
1353
|
+
if self.package_platform == "Android"
|
|
1122
1354
|
else []
|
|
1123
1355
|
),
|
|
1124
1356
|
"info_plist": info_plist,
|
|
@@ -1126,6 +1358,7 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1126
1358
|
"android_permissions": android_permissions,
|
|
1127
1359
|
"android_features": android_features,
|
|
1128
1360
|
"android_meta_data": android_meta_data,
|
|
1361
|
+
"android_providers": android_providers,
|
|
1129
1362
|
"deep_linking": {
|
|
1130
1363
|
"scheme": deep_linking_scheme,
|
|
1131
1364
|
"host": deep_linking_host,
|
|
@@ -1137,9 +1370,77 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1137
1370
|
),
|
|
1138
1371
|
},
|
|
1139
1372
|
"flutter": {"dependencies": list(self.flutter_dependencies.keys())},
|
|
1373
|
+
"boot_screen": self._resolve_boot_screen(),
|
|
1140
1374
|
"pyproject": self.get_pyproject(),
|
|
1141
1375
|
}
|
|
1142
1376
|
|
|
1377
|
+
def _resolve_boot_screen(self):
|
|
1378
|
+
"""
|
|
1379
|
+
Resolve the boot screen configuration from pyproject.toml.
|
|
1380
|
+
|
|
1381
|
+
Merges the global `[tool.flet.boot_screen]` with the platform-specific
|
|
1382
|
+
`[tool.flet.<platform>.boot_screen]` (platform wins per key), resolves
|
|
1383
|
+
the selected screen `name` (default "flet") and its options table.
|
|
1384
|
+
|
|
1385
|
+
Falls back to the legacy `[tool.flet[.<platform>].app.boot_screen]` /
|
|
1386
|
+
`app.startup_screen` (`show`/`message`) settings, mapping them onto the
|
|
1387
|
+
built-in "flet" screen with a deprecation warning.
|
|
1388
|
+
|
|
1389
|
+
Returns a dict with `name` and `options_b64` (base64-encoded JSON of the
|
|
1390
|
+
options table) for the cookiecutter template.
|
|
1391
|
+
"""
|
|
1392
|
+
config_platform = self.config_platform
|
|
1393
|
+
|
|
1394
|
+
def merged(key):
|
|
1395
|
+
result = {}
|
|
1396
|
+
merge_dict(
|
|
1397
|
+
result, copy.deepcopy(self.get_pyproject(f"tool.flet.{key}") or {})
|
|
1398
|
+
)
|
|
1399
|
+
merge_dict(
|
|
1400
|
+
result,
|
|
1401
|
+
copy.deepcopy(
|
|
1402
|
+
self.get_pyproject(f"tool.flet.{config_platform}.{key}") or {}
|
|
1403
|
+
),
|
|
1404
|
+
)
|
|
1405
|
+
return result
|
|
1406
|
+
|
|
1407
|
+
boot_screen = merged("boot_screen")
|
|
1408
|
+
|
|
1409
|
+
if boot_screen:
|
|
1410
|
+
name = boot_screen.get("name", "flet")
|
|
1411
|
+
options = boot_screen.get(name) or {}
|
|
1412
|
+
else:
|
|
1413
|
+
# backward compatibility with the legacy app.boot_screen /
|
|
1414
|
+
# app.startup_screen settings
|
|
1415
|
+
name = "flet"
|
|
1416
|
+
options = {}
|
|
1417
|
+
legacy_boot = merged("app.boot_screen")
|
|
1418
|
+
legacy_startup = merged("app.startup_screen")
|
|
1419
|
+
if legacy_boot or legacy_startup:
|
|
1420
|
+
console.log(
|
|
1421
|
+
"[tool.flet.app.boot_screen] and "
|
|
1422
|
+
"[tool.flet.app.startup_screen] are deprecated; use "
|
|
1423
|
+
"[tool.flet.boot_screen] with a named screen instead.",
|
|
1424
|
+
style=warning_style,
|
|
1425
|
+
)
|
|
1426
|
+
if legacy_boot.get("show"):
|
|
1427
|
+
options["spinner_size"] = 30
|
|
1428
|
+
message = legacy_boot.get("message")
|
|
1429
|
+
if message:
|
|
1430
|
+
options["prepare_message"] = message
|
|
1431
|
+
if legacy_startup.get("show"):
|
|
1432
|
+
options["spinner_size"] = 30
|
|
1433
|
+
message = legacy_startup.get("message")
|
|
1434
|
+
if message:
|
|
1435
|
+
options["startup_message"] = message
|
|
1436
|
+
|
|
1437
|
+
return {
|
|
1438
|
+
"name": name,
|
|
1439
|
+
"options_b64": base64.b64encode(json.dumps(options).encode("utf-8")).decode(
|
|
1440
|
+
"ascii"
|
|
1441
|
+
),
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1143
1444
|
def create_flutter_project(self, second_pass=False):
|
|
1144
1445
|
"""
|
|
1145
1446
|
Render Flutter bootstrap project from template if template inputs changed.
|
|
@@ -1174,6 +1475,9 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1174
1475
|
template_ref = flet.version.flet_version
|
|
1175
1476
|
|
|
1176
1477
|
is_local_dev = False
|
|
1478
|
+
# Identity printed in status / hashed for invalidation; may differ from
|
|
1479
|
+
# the path cookiecutter actually reads when caching kicks in below.
|
|
1480
|
+
template_source = template_url
|
|
1177
1481
|
if template_url:
|
|
1178
1482
|
# User-provided template (git repo or local path) — use checkout
|
|
1179
1483
|
checkout = template_ref
|
|
@@ -1182,13 +1486,19 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1182
1486
|
local_tpl = Path(__file__).resolve().parents[5] / "templates" / "build"
|
|
1183
1487
|
if local_tpl.is_dir():
|
|
1184
1488
|
template_url = str(local_tpl)
|
|
1489
|
+
template_source = template_url
|
|
1185
1490
|
checkout = None
|
|
1186
1491
|
is_local_dev = True
|
|
1187
1492
|
else:
|
|
1188
|
-
|
|
1493
|
+
from flet_cli.utils.template_cache import get_cached_template_zip
|
|
1494
|
+
|
|
1495
|
+
template_source = DEFAULT_TEMPLATE_URL.format(version=template_ref)
|
|
1496
|
+
template_url = str(
|
|
1497
|
+
get_cached_template_zip(template_source, template_ref)
|
|
1498
|
+
)
|
|
1189
1499
|
checkout = None
|
|
1190
1500
|
|
|
1191
|
-
hash.update(
|
|
1501
|
+
hash.update(template_source)
|
|
1192
1502
|
hash.update(template_ref)
|
|
1193
1503
|
|
|
1194
1504
|
template_dir = self.options.template_dir or self.get_pyproject(
|
|
@@ -1214,7 +1524,7 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1214
1524
|
# create a new Flutter bootstrap project directory, if non-existent
|
|
1215
1525
|
if not second_pass:
|
|
1216
1526
|
self.flutter_dir.mkdir(parents=True, exist_ok=True)
|
|
1217
|
-
status = f"[bold blue]Creating app shell from {
|
|
1527
|
+
status = f"[bold blue]Creating app shell from {template_source}"
|
|
1218
1528
|
if checkout:
|
|
1219
1529
|
status += f' with ref "{template_ref}"'
|
|
1220
1530
|
status += "..."
|
|
@@ -1239,6 +1549,8 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1239
1549
|
self.cleanup(1, f"{e}")
|
|
1240
1550
|
|
|
1241
1551
|
# For local development, override flet dependency with path
|
|
1552
|
+
repo_root = None
|
|
1553
|
+
pubspec = None
|
|
1242
1554
|
if is_local_dev:
|
|
1243
1555
|
repo_root = flet.version.find_repo_root(Path(__file__).resolve().parent)
|
|
1244
1556
|
if repo_root:
|
|
@@ -1248,7 +1560,36 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1248
1560
|
pubspec.setdefault("dependency_overrides", {})["flet"] = {
|
|
1249
1561
|
"path": flet_pkg_path
|
|
1250
1562
|
}
|
|
1251
|
-
|
|
1563
|
+
|
|
1564
|
+
# In test mode, inject the integration-test driver (and flutter_test)
|
|
1565
|
+
# as dev dependencies. They are intentionally NOT in the template
|
|
1566
|
+
# pubspec: that keeps it valid YAML for the release patch tooling and
|
|
1567
|
+
# ensures a normal `flet build` never pulls them. flet_integration_test
|
|
1568
|
+
# is publish_to:none, so for local dev it resolves to the in-repo
|
|
1569
|
+
# package by path, and for an end user it is a git dependency pinned to
|
|
1570
|
+
# this flet version's tag.
|
|
1571
|
+
if getattr(self, "test_mode", False):
|
|
1572
|
+
if pubspec is None:
|
|
1573
|
+
pubspec = self.load_yaml(self.pubspec_path)
|
|
1574
|
+
dev_deps = pubspec.setdefault("dev_dependencies", {})
|
|
1575
|
+
dev_deps["flutter_test"] = {"sdk": "flutter"}
|
|
1576
|
+
if is_local_dev and repo_root:
|
|
1577
|
+
fit_pkg_path = str(repo_root / "packages" / "flet_integration_test")
|
|
1578
|
+
dev_deps["flet_integration_test"] = {"path": fit_pkg_path}
|
|
1579
|
+
pubspec.setdefault("dependency_overrides", {})[
|
|
1580
|
+
"flet_integration_test"
|
|
1581
|
+
] = {"path": fit_pkg_path}
|
|
1582
|
+
else:
|
|
1583
|
+
dev_deps["flet_integration_test"] = {
|
|
1584
|
+
"git": {
|
|
1585
|
+
"url": "https://github.com/flet-dev/flet.git",
|
|
1586
|
+
"ref": f"v{flet.version.flet_version}",
|
|
1587
|
+
"path": "packages/flet_integration_test",
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
if pubspec is not None:
|
|
1592
|
+
self.save_yaml(self.pubspec_path, pubspec)
|
|
1252
1593
|
|
|
1253
1594
|
pyproject_pubspec = self.get_pyproject("tool.flet.flutter.pubspec")
|
|
1254
1595
|
|
|
@@ -1291,10 +1632,15 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1291
1632
|
assert self.template_data
|
|
1292
1633
|
assert self.build_dir
|
|
1293
1634
|
|
|
1635
|
+
# Replace the permanent flutter-packages copy with this build's set. The
|
|
1636
|
+
# temp dir is populated by serious_python's package step and is ABSENT
|
|
1637
|
+
# when the app has no Flutter extensions — so always clear the old copy
|
|
1638
|
+
# first, otherwise an extension removed since the previous build (e.g.
|
|
1639
|
+
# dropping flet-video) would linger here and stay in the built app.
|
|
1640
|
+
if self.flutter_packages_dir.exists():
|
|
1641
|
+
shutil.rmtree(self.flutter_packages_dir, ignore_errors=True)
|
|
1294
1642
|
if self.flutter_packages_temp_dir.exists():
|
|
1295
1643
|
# copy packages from temp to permanent location
|
|
1296
|
-
if self.flutter_packages_dir.exists():
|
|
1297
|
-
shutil.rmtree(self.flutter_packages_dir, ignore_errors=True)
|
|
1298
1644
|
shutil.move(self.flutter_packages_temp_dir, self.flutter_packages_dir)
|
|
1299
1645
|
|
|
1300
1646
|
if self.flutter_packages_dir.exists():
|
|
@@ -1756,6 +2102,25 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1756
2102
|
d[pp[-1]] = f"{images_dir}/{image}"
|
|
1757
2103
|
return
|
|
1758
2104
|
|
|
2105
|
+
def _darwin_spm_active(self) -> bool:
|
|
2106
|
+
"""Whether to stage serious_python for Swift Package Manager (vs CocoaPods).
|
|
2107
|
+
|
|
2108
|
+
On by default, matching Flutter 3.44+ (SPM enabled by default). Because
|
|
2109
|
+
`serious_python_darwin` ships a `Package.swift`, Flutter always builds it
|
|
2110
|
+
as an SPM plugin when SPM is enabled — even in a hybrid app where other,
|
|
2111
|
+
non-SPM plugins (e.g. `flet-video`/media_kit) build with CocoaPods at the
|
|
2112
|
+
same time. So serious_python must stage for SPM to match; it is NOT tied
|
|
2113
|
+
to whether the app also pulls in non-SPM plugins. Users force CocoaPods
|
|
2114
|
+
with `--no-swift-package-manager` (or `swift_package_manager = false` under
|
|
2115
|
+
`[tool.flet]`) only when they've disabled SPM in Flutter itself. Flet does
|
|
2116
|
+
not change Flutter's global SPM configuration.
|
|
2117
|
+
"""
|
|
2118
|
+
if self.package_platform not in ("iOS", "Darwin"):
|
|
2119
|
+
return False
|
|
2120
|
+
return self.get_bool_setting(
|
|
2121
|
+
self.options.swift_package_manager, "swift_package_manager", True
|
|
2122
|
+
)
|
|
2123
|
+
|
|
1759
2124
|
def package_python_app(self):
|
|
1760
2125
|
"""
|
|
1761
2126
|
Package Python app and dependencies into Flutter-consumable app archive.
|
|
@@ -1786,14 +2151,24 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1786
2151
|
str(self.package_app_path),
|
|
1787
2152
|
"--platform",
|
|
1788
2153
|
self.package_platform,
|
|
2154
|
+
"--python-version",
|
|
2155
|
+
self.python_release.short,
|
|
1789
2156
|
]
|
|
1790
2157
|
|
|
1791
2158
|
if self.template_data["options"]["target_arch"]:
|
|
2159
|
+
# serious_python's --arch is a Dart multi-option: values must be
|
|
2160
|
+
# comma-separated or the flag repeated. Space-separated values
|
|
2161
|
+
# after the first are silently treated as positional arguments.
|
|
1792
2162
|
package_args.extend(
|
|
1793
|
-
["--arch"
|
|
2163
|
+
["--arch", ",".join(self.template_data["options"]["target_arch"])]
|
|
1794
2164
|
)
|
|
1795
2165
|
|
|
1796
|
-
|
|
2166
|
+
# Only the short version is passed; serious_python derives the full
|
|
2167
|
+
# version, python-build date, and dart_bridge version from its own
|
|
2168
|
+
# committed snapshot of the manifest.
|
|
2169
|
+
package_env = {
|
|
2170
|
+
"SERIOUS_PYTHON_VERSION": self.python_release.short,
|
|
2171
|
+
}
|
|
1797
2172
|
|
|
1798
2173
|
# requirements
|
|
1799
2174
|
requirements_txt = self.python_app_path.joinpath("requirements.txt")
|
|
@@ -1826,7 +2201,13 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1826
2201
|
if not dev_path.is_absolute():
|
|
1827
2202
|
dev_path = (self.python_app_path / dev_path).resolve()
|
|
1828
2203
|
if dev_path.exists():
|
|
1829
|
-
|
|
2204
|
+
# Use Path.as_uri() so Windows drive paths render as
|
|
2205
|
+
# `file:///D:/a/...` rather than `file://D:\a\...`,
|
|
2206
|
+
# which pip otherwise treats as a UNC path and fails
|
|
2207
|
+
# to resolve.
|
|
2208
|
+
toml_dependencies[i] = (
|
|
2209
|
+
f"{package_name} @ {dev_path.as_uri()}"
|
|
2210
|
+
)
|
|
1830
2211
|
else:
|
|
1831
2212
|
toml_dependencies[i] = (
|
|
1832
2213
|
f"{package_name} @ {package_location}"
|
|
@@ -1856,6 +2237,23 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1856
2237
|
package_env["SERIOUS_PYTHON_SITE_PACKAGES"] = str(
|
|
1857
2238
|
self.build_dir / "site-packages"
|
|
1858
2239
|
)
|
|
2240
|
+
# app staging dir: serious_python's `package` places the processed
|
|
2241
|
+
# app here (no app.zip on native); the platform native build copies
|
|
2242
|
+
# it into the bundle (Android zips it as a stored asset).
|
|
2243
|
+
package_env["SERIOUS_PYTHON_APP"] = str(self.build_dir / "python-app")
|
|
2244
|
+
|
|
2245
|
+
# Swift Package Manager (darwin): tell serious_python's package command to
|
|
2246
|
+
# do the host-side SPM staging (the podspec prepare_command doesn't run
|
|
2247
|
+
# under SPM) and write the SP_NATIVE_SET cache-bust key to this file.
|
|
2248
|
+
# serious_python defaults to SPM staging, so be explicit either way — set
|
|
2249
|
+
# it false for the CocoaPods cases (e.g. an app using flet-video).
|
|
2250
|
+
if self.package_platform in ("iOS", "Darwin"):
|
|
2251
|
+
spm = self._darwin_spm_active()
|
|
2252
|
+
package_env["SERIOUS_PYTHON_DARWIN_SPM"] = "true" if spm else "false"
|
|
2253
|
+
if spm:
|
|
2254
|
+
package_env["SERIOUS_PYTHON_SPM_KEY_FILE"] = str(
|
|
2255
|
+
self.build_dir / ".serious_python_spm_key"
|
|
2256
|
+
)
|
|
1859
2257
|
|
|
1860
2258
|
# flutter-packages variable
|
|
1861
2259
|
if self.flutter_packages_temp_dir.exists():
|
|
@@ -1891,11 +2289,31 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1891
2289
|
source_packages
|
|
1892
2290
|
)
|
|
1893
2291
|
|
|
1894
|
-
|
|
2292
|
+
# android-extract-packages: path-hungry packages shipped extracted to disk
|
|
2293
|
+
# instead of inside the zip (serious_python Android native-mmap packaging).
|
|
2294
|
+
# A built-in default set covers commonly-broken packages; the user list
|
|
2295
|
+
# (CLI / pyproject) is merged on top. Consumed by the serious_python_android
|
|
2296
|
+
# Gradle split during `flutter build`, so the env var is set on build_env
|
|
2297
|
+
# (see _run_flutter_command), not on the package step.
|
|
2298
|
+
self.android_extract_packages: list[str] = []
|
|
2299
|
+
if self.package_platform == "Android":
|
|
2300
|
+
user_extract_packages = (
|
|
2301
|
+
self.options.android_extract_packages
|
|
2302
|
+
or self.get_pyproject(
|
|
2303
|
+
f"tool.flet.{self.config_platform}.extract_packages"
|
|
2304
|
+
)
|
|
2305
|
+
or self.get_pyproject("tool.flet.extract_packages")
|
|
2306
|
+
or []
|
|
2307
|
+
)
|
|
2308
|
+
self.android_extract_packages = list(
|
|
2309
|
+
dict.fromkeys(ANDROID_DEFAULT_EXTRACT_PACKAGES + user_extract_packages)
|
|
2310
|
+
)
|
|
2311
|
+
|
|
2312
|
+
if self.get_bool_setting(self.options.compile_app, "compile.app", True):
|
|
1895
2313
|
package_args.append("--compile-app")
|
|
1896
2314
|
|
|
1897
2315
|
if self.get_bool_setting(
|
|
1898
|
-
self.options.compile_packages, "compile.packages",
|
|
2316
|
+
self.options.compile_packages, "compile.packages", True
|
|
1899
2317
|
):
|
|
1900
2318
|
package_args.append("--compile-packages")
|
|
1901
2319
|
|
|
@@ -1982,13 +2400,36 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1982
2400
|
|
|
1983
2401
|
hash.commit()
|
|
1984
2402
|
|
|
1985
|
-
#
|
|
1986
|
-
|
|
1987
|
-
if
|
|
1988
|
-
self.
|
|
2403
|
+
# verify the package output: web ships app/app.zip; native platforms
|
|
2404
|
+
# stage the unpacked app to build/app for the native build to bundle.
|
|
2405
|
+
if self.package_platform == "Emscripten":
|
|
2406
|
+
app_zip_path = self.flutter_dir.joinpath("app", "app.zip")
|
|
2407
|
+
if not os.path.exists(app_zip_path):
|
|
2408
|
+
self.cleanup(1, "Flet app package app/app.zip was not created.")
|
|
2409
|
+
else:
|
|
2410
|
+
app_staging_dir = self.build_dir / "python-app"
|
|
2411
|
+
if not app_staging_dir.exists():
|
|
2412
|
+
self.cleanup(
|
|
2413
|
+
1, f"Flet app package was not staged to {app_staging_dir}."
|
|
2414
|
+
)
|
|
1989
2415
|
|
|
1990
2416
|
console.log(f"Packaged Python app {self.emojis['checkmark']}")
|
|
1991
2417
|
|
|
2418
|
+
# Drop the matching Pyodide runtime into the Flutter project's web/
|
|
2419
|
+
# directory so it ships in `flutter build web` output. Cached
|
|
2420
|
+
# per-version under ~/.flet/pyodide/<version>/ so subsequent builds
|
|
2421
|
+
# are no-ops.
|
|
2422
|
+
if self.package_platform == "Emscripten":
|
|
2423
|
+
from flet_cli.utils.pyodide import ensure_pyodide
|
|
2424
|
+
|
|
2425
|
+
self.update_status("[bold blue]Preparing Pyodide runtime...")
|
|
2426
|
+
pyodide_dest = self.flutter_dir / "web" / "pyodide"
|
|
2427
|
+
ensure_pyodide(self.python_release.pyodide, pyodide_dest)
|
|
2428
|
+
console.log(
|
|
2429
|
+
f"Pyodide {self.python_release.pyodide} ready "
|
|
2430
|
+
f"{self.emojis['checkmark']}"
|
|
2431
|
+
)
|
|
2432
|
+
|
|
1992
2433
|
def get_bool_setting(self, cli_option, pyproj_setting, default_value):
|
|
1993
2434
|
"""
|
|
1994
2435
|
Resolve a boolean setting with precedence: CLI option, pyproject, default.
|
|
@@ -2038,6 +2479,60 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
2038
2479
|
|
|
2039
2480
|
self._run_flutter_command()
|
|
2040
2481
|
|
|
2482
|
+
def _serious_python_build_env(self) -> dict:
|
|
2483
|
+
"""
|
|
2484
|
+
serious_python environment for the platform NATIVE build (the Gradle /
|
|
2485
|
+
CMake / podspec steps run by `flutter build`).
|
|
2486
|
+
|
|
2487
|
+
These tell the native build where the `package` step staged the app and
|
|
2488
|
+
site-packages and which embedded Python runtime to bundle. `flet build`
|
|
2489
|
+
applies them via `_run_flutter_command`; `flet test` applies the SAME set
|
|
2490
|
+
to the `flutter test` it spawns (see test.py `_flutter_path_env`) so both
|
|
2491
|
+
bundle an identical app. In particular, without `SERIOUS_PYTHON_APP` the
|
|
2492
|
+
Android `packageApp` Gradle task early-returns and a stale `app.zip` (e.g.
|
|
2493
|
+
an old-Python `main.pyc`) survives in the APK — `ImportError: bad magic
|
|
2494
|
+
number`. Built defensively so it is safe to call before the full build
|
|
2495
|
+
pipeline has populated every attribute.
|
|
2496
|
+
"""
|
|
2497
|
+
|
|
2498
|
+
env: dict = {}
|
|
2499
|
+
python_release = getattr(self, "python_release", None)
|
|
2500
|
+
if python_release is not None:
|
|
2501
|
+
# Only the short version is passed; serious_python derives the rest
|
|
2502
|
+
# from its committed manifest snapshot.
|
|
2503
|
+
env["SERIOUS_PYTHON_VERSION"] = python_release.short
|
|
2504
|
+
|
|
2505
|
+
build_dir = getattr(self, "build_dir", None)
|
|
2506
|
+
package_platform = getattr(self, "package_platform", None)
|
|
2507
|
+
if build_dir is not None and package_platform != "Emscripten":
|
|
2508
|
+
env["SERIOUS_PYTHON_SITE_PACKAGES"] = str(build_dir / "site-packages")
|
|
2509
|
+
# app staging dir: read by the platform native build (CMake / podspec
|
|
2510
|
+
# / Android Gradle) at `flutter build` time to place the unpacked app
|
|
2511
|
+
# into the bundle.
|
|
2512
|
+
env["SERIOUS_PYTHON_APP"] = str(build_dir / "python-app")
|
|
2513
|
+
|
|
2514
|
+
# Swift Package Manager (darwin): export the cache-bust key the package
|
|
2515
|
+
# step computed so the plugin's Package.swift re-resolves when the staged
|
|
2516
|
+
# native set changes (SwiftPM caches its graph on manifest text + env).
|
|
2517
|
+
if (
|
|
2518
|
+
build_dir is not None
|
|
2519
|
+
and package_platform in ("iOS", "Darwin")
|
|
2520
|
+
and self._darwin_spm_active()
|
|
2521
|
+
):
|
|
2522
|
+
spm_key_file = build_dir / ".serious_python_spm_key"
|
|
2523
|
+
if spm_key_file.exists():
|
|
2524
|
+
env["SP_NATIVE_SET"] = spm_key_file.read_text().strip()
|
|
2525
|
+
|
|
2526
|
+
# Path-hungry packages to ship extracted to disk: consumed by the
|
|
2527
|
+
# serious_python_android Gradle split during `flutter build`.
|
|
2528
|
+
if package_platform == "Android" and getattr(
|
|
2529
|
+
self, "android_extract_packages", None
|
|
2530
|
+
):
|
|
2531
|
+
env["SERIOUS_PYTHON_ANDROID_EXTRACT_PACKAGES"] = ",".join(
|
|
2532
|
+
self.android_extract_packages
|
|
2533
|
+
)
|
|
2534
|
+
return env
|
|
2535
|
+
|
|
2041
2536
|
def _run_flutter_command(self):
|
|
2042
2537
|
"""
|
|
2043
2538
|
Build final Flutter CLI command, configure environment, and run it.
|
|
@@ -2059,13 +2554,10 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
2059
2554
|
]
|
|
2060
2555
|
)
|
|
2061
2556
|
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
#
|
|
2065
|
-
|
|
2066
|
-
build_env["SERIOUS_PYTHON_SITE_PACKAGES"] = str(
|
|
2067
|
-
self.build_dir / "site-packages"
|
|
2068
|
-
)
|
|
2557
|
+
# serious_python env for the native build, shared verbatim with `flet
|
|
2558
|
+
# test` (which spawns its own `flutter test`) so both bundle an identical
|
|
2559
|
+
# app — see `_serious_python_build_env`.
|
|
2560
|
+
build_env = self._serious_python_build_env()
|
|
2069
2561
|
|
|
2070
2562
|
if self.package_platform == "Emscripten" and not self.template_data["no_wasm"]:
|
|
2071
2563
|
build_args.append("--wasm")
|
|
@@ -2139,6 +2631,32 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
2139
2631
|
console.log(build_result.stderr, style=error_style)
|
|
2140
2632
|
self.cleanup(build_result.returncode if build_result.returncode else 1)
|
|
2141
2633
|
|
|
2634
|
+
def resolve_output_path(self, build_output: str) -> str:
|
|
2635
|
+
"""
|
|
2636
|
+
Resolve a platform `outputs` glob to an absolute path inside the
|
|
2637
|
+
Flutter project, substituting the `{arch}` and name placeholders.
|
|
2638
|
+
|
|
2639
|
+
Args:
|
|
2640
|
+
build_output: An entry of `self.platforms[...]["outputs"]`.
|
|
2641
|
+
"""
|
|
2642
|
+
|
|
2643
|
+
assert self.flutter_dir
|
|
2644
|
+
assert self.template_data
|
|
2645
|
+
|
|
2646
|
+
arch = platform.machine().lower()
|
|
2647
|
+
if arch in {"x86_64", "amd64"}:
|
|
2648
|
+
arch = "x64"
|
|
2649
|
+
elif arch in {"arm64", "aarch64"}:
|
|
2650
|
+
arch = "arm64"
|
|
2651
|
+
|
|
2652
|
+
return (
|
|
2653
|
+
str(self.flutter_dir.joinpath(build_output))
|
|
2654
|
+
.replace("{arch}", arch)
|
|
2655
|
+
.replace("{artifact_name}", self.template_data["artifact_name"])
|
|
2656
|
+
.replace("{project_name}", self.template_data["project_name"])
|
|
2657
|
+
.replace("{product_name}", self.template_data["product_name"])
|
|
2658
|
+
)
|
|
2659
|
+
|
|
2142
2660
|
def copy_build_output(self):
|
|
2143
2661
|
"""
|
|
2144
2662
|
Copy generated platform artifacts into the requested output directory.
|
|
@@ -2154,11 +2672,6 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
2154
2672
|
self.update_status(
|
|
2155
2673
|
f"[bold blue]Copying build to [cyan]{self.rel_out_dir}[/cyan] directory...",
|
|
2156
2674
|
)
|
|
2157
|
-
arch = platform.machine().lower()
|
|
2158
|
-
if arch in {"x86_64", "amd64"}:
|
|
2159
|
-
arch = "x64"
|
|
2160
|
-
elif arch in {"arm64", "aarch64"}:
|
|
2161
|
-
arch = "arm64"
|
|
2162
2675
|
|
|
2163
2676
|
def make_ignore_fn(out_dir, out_glob):
|
|
2164
2677
|
"""
|
|
@@ -2177,13 +2690,7 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
2177
2690
|
return ignore
|
|
2178
2691
|
|
|
2179
2692
|
for build_output in self.platforms[self.target_platform]["outputs"]:
|
|
2180
|
-
build_output_dir = (
|
|
2181
|
-
str(self.flutter_dir.joinpath(build_output))
|
|
2182
|
-
.replace("{arch}", arch)
|
|
2183
|
-
.replace("{artifact_name}", self.template_data["artifact_name"])
|
|
2184
|
-
.replace("{project_name}", self.template_data["project_name"])
|
|
2185
|
-
.replace("{product_name}", self.template_data["product_name"])
|
|
2186
|
-
)
|
|
2693
|
+
build_output_dir = self.resolve_output_path(build_output)
|
|
2187
2694
|
|
|
2188
2695
|
if self.verbose > 0:
|
|
2189
2696
|
console.log(
|