flet-cli 1.0.2__tar.gz → 1.0.4__tar.gz
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-1.0.2 → flet_cli-1.0.4}/PKG-INFO +3 -3
- {flet_cli-1.0.2 → flet_cli-1.0.4}/pyproject.toml +3 -3
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/build_base.py +80 -3
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/flutter_base.py +48 -9
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/run.py +27 -6
- flet_cli-1.0.4/src/flet_cli/version.py +1 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli.egg-info/PKG-INFO +3 -3
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli.egg-info/SOURCES.txt +4 -1
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli.egg-info/requires.txt +2 -2
- flet_cli-1.0.4/tests/test_build_copy_output.py +118 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_build_template.py +35 -0
- flet_cli-1.0.4/tests/test_build_windows_arm64.py +231 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_run_desktop_dependency.py +2 -0
- flet_cli-1.0.4/tests/test_run_watcher_unavailable.py +155 -0
- flet_cli-1.0.2/src/flet_cli/version.py +0 -1
- {flet_cli-1.0.2 → flet_cli-1.0.4}/README.md +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/setup.cfg +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/__pyinstaller/__init__.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/__pyinstaller/config.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/__pyinstaller/hook-flet.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/__pyinstaller/macos_utils.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/__pyinstaller/rthooks/pyi_rth_localhost_fletd.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/__pyinstaller/rthooks.dat +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/__pyinstaller/utils.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/__pyinstaller/win_utils.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/cli.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/base.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/build.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/clean.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/create.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/debug.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/devices.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/doctor.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/emulators.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/mcp.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/options.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/pack.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/publish.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/serve.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/test.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/commands/test_host.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/android.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/android_sdk.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/app_excludes.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/cli.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/distros.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/flutter.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/hash_stamp.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/ios_sign.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/jdk.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/linux_deps.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/macos_sign.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/merge.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/plist.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/processes.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/project_dependencies.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/pyodide.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/pyproject_toml.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/python_versions.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/utils/template_cache.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli.egg-info/dependency_links.txt +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli.egg-info/entry_points.txt +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli.egg-info/top_level.txt +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_app_excludes.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_build_app_path.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_build_icons.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_build_ios.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_build_linux.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_build_list_options.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_build_macos_signing.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_build_platform_settings.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_build_splash.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_clean.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_cli_pass_through_args.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_cli_positionals.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_create.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_find_platform_image.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_flutter.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_macos_sign.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_plist.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_project_dependencies.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_python_versions.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_run_assets_dir.py +0 -0
- {flet_cli-1.0.2 → flet_cli-1.0.4}/tests/test_run_script_args.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flet-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Flet CLI
|
|
5
5
|
Author-email: "Appveyor Systems Inc." <hello@flet.dev>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -9,8 +9,8 @@ Project-URL: Repository, https://github.com/flet-dev/flet
|
|
|
9
9
|
Project-URL: Documentation, https://flet.dev/docs
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
|
-
Requires-Dist: flet==1.0.
|
|
13
|
-
Requires-Dist: flet-platform-assets==1.0.
|
|
12
|
+
Requires-Dist: flet==1.0.4
|
|
13
|
+
Requires-Dist: flet-platform-assets==1.0.4
|
|
14
14
|
Requires-Dist: watchdog>=4.0.0
|
|
15
15
|
Requires-Dist: packaging>=25.0
|
|
16
16
|
Requires-Dist: qrcode>=7.4.2
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flet-cli"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.4"
|
|
4
4
|
description = "Flet CLI"
|
|
5
5
|
authors = [{ name = "Appveyor Systems Inc.", email = "hello@flet.dev" }]
|
|
6
6
|
license = "Apache-2.0"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
9
|
dependencies = [
|
|
10
|
-
"flet==1.0.
|
|
11
|
-
"flet-platform-assets==1.0.
|
|
10
|
+
"flet==1.0.4",
|
|
11
|
+
"flet-platform-assets==1.0.4",
|
|
12
12
|
"watchdog >=4.0.0",
|
|
13
13
|
"packaging >=25.0",
|
|
14
14
|
"qrcode >=7.4.2",
|
|
@@ -50,6 +50,7 @@ from flet_cli.utils.android import (
|
|
|
50
50
|
)
|
|
51
51
|
from flet_cli.utils.app_excludes import find_default_excludes
|
|
52
52
|
from flet_cli.utils.cli import parse_cli_bool_value
|
|
53
|
+
from flet_cli.utils.flutter import get_flutter_dir
|
|
53
54
|
from flet_cli.utils.hash_stamp import HashStamp
|
|
54
55
|
from flet_cli.utils.merge import merge_dict
|
|
55
56
|
from flet_cli.utils.plist import is_supported_plist_value, parse_cli_plist_value
|
|
@@ -159,7 +160,7 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
159
160
|
"config_platform": "windows",
|
|
160
161
|
"flutter_build_command": "windows",
|
|
161
162
|
"status_text": "Windows app",
|
|
162
|
-
"outputs": ["build/windows/
|
|
163
|
+
"outputs": ["build/windows/{arch}/runner/Release/*"],
|
|
163
164
|
"dist": "windows",
|
|
164
165
|
"can_be_run_on": ["Windows"],
|
|
165
166
|
},
|
|
@@ -547,6 +548,14 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
547
548
|
required=False,
|
|
548
549
|
help="Default color for your web app's user interface (web only)",
|
|
549
550
|
)
|
|
551
|
+
parser.add_argument(
|
|
552
|
+
"--pwa-short-name",
|
|
553
|
+
dest="pwa_short_name",
|
|
554
|
+
required=False,
|
|
555
|
+
help="Short name of your web app, shown under its icon when "
|
|
556
|
+
"installed, e.g. on the iOS Home Screen. Defaults to the product "
|
|
557
|
+
"name (web only)",
|
|
558
|
+
)
|
|
550
559
|
parser.add_argument(
|
|
551
560
|
"--no-wasm",
|
|
552
561
|
dest="no_wasm",
|
|
@@ -1543,6 +1552,14 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
1543
1552
|
self.options.pwa_theme_color
|
|
1544
1553
|
or self.get_pyproject("tool.flet.web.pwa_theme_color")
|
|
1545
1554
|
),
|
|
1555
|
+
# The label an installed PWA gets under its icon. It used to be
|
|
1556
|
+
# the project name, a slug such as `my_app`, and iOS shows it on
|
|
1557
|
+
# the Home Screen instead of the `apple-mobile-web-app-title` meta.
|
|
1558
|
+
"pwa_short_name": (
|
|
1559
|
+
self.options.pwa_short_name
|
|
1560
|
+
or self.get_pyproject("tool.flet.web.pwa_short_name")
|
|
1561
|
+
or product_name
|
|
1562
|
+
),
|
|
1546
1563
|
"no_wasm": (
|
|
1547
1564
|
self.options.no_wasm
|
|
1548
1565
|
or self.get_pyproject("tool.flet.web.wasm") == False # noqa: E712
|
|
@@ -3125,6 +3142,49 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
3125
3142
|
)
|
|
3126
3143
|
self.cleanup(build_result.returncode if build_result.returncode else 1)
|
|
3127
3144
|
|
|
3145
|
+
def flutter_sdk_supported(self) -> bool:
|
|
3146
|
+
"""
|
|
3147
|
+
Check whether the Flutter SDK found can build for the target platform.
|
|
3148
|
+
|
|
3149
|
+
Flet's Python runtime for Windows is x64-only, so an ARM64 Flutter SDK,
|
|
3150
|
+
as installed natively on Windows on ARM, would build an ARM64 app that
|
|
3151
|
+
can't load it. The Flutter SDK that Flet installs is x64, runs under
|
|
3152
|
+
emulation there, and builds an x64 app that runs on Windows on ARM.
|
|
3153
|
+
|
|
3154
|
+
Returns:
|
|
3155
|
+
`False` for an ARM64 Flutter SDK when building for Windows,
|
|
3156
|
+
otherwise `True`.
|
|
3157
|
+
"""
|
|
3158
|
+
|
|
3159
|
+
if (
|
|
3160
|
+
self.current_platform != "Windows"
|
|
3161
|
+
or self.package_platform != "Windows"
|
|
3162
|
+
or self.flutter_arch() != "arm64"
|
|
3163
|
+
):
|
|
3164
|
+
return True
|
|
3165
|
+
|
|
3166
|
+
flet_flutter_dir = get_flutter_dir(str(self.required_flutter_version))
|
|
3167
|
+
if os.path.normcase(str(self.flutter_exe)).startswith(
|
|
3168
|
+
os.path.normcase(flet_flutter_dir) + os.sep
|
|
3169
|
+
):
|
|
3170
|
+
self.skip_flutter_doctor = True
|
|
3171
|
+
self.cleanup(
|
|
3172
|
+
1,
|
|
3173
|
+
f"Flet's Flutter SDK at {escape(flet_flutter_dir)} builds ARM64 "
|
|
3174
|
+
"Windows apps, but Flet's Python runtime for Windows is x64-only. "
|
|
3175
|
+
"Delete that directory and run the build again to install the x64 "
|
|
3176
|
+
"Flutter SDK.",
|
|
3177
|
+
)
|
|
3178
|
+
|
|
3179
|
+
console.log(
|
|
3180
|
+
f"The Flutter SDK at {escape(str(self.flutter_exe))} builds ARM64 "
|
|
3181
|
+
"Windows apps, but Flet's Python runtime for Windows is x64-only. "
|
|
3182
|
+
f"Flet will use its own Flutter {self.required_flutter_version} (x64) "
|
|
3183
|
+
"to build an x64 app, which runs on Windows on ARM.",
|
|
3184
|
+
style=warning_style,
|
|
3185
|
+
)
|
|
3186
|
+
return False
|
|
3187
|
+
|
|
3128
3188
|
def resolve_output_path(self, build_output: str) -> str:
|
|
3129
3189
|
"""
|
|
3130
3190
|
Resolve a platform `outputs` glob to an absolute path inside the
|
|
@@ -3137,7 +3197,11 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
3137
3197
|
assert self.flutter_dir
|
|
3138
3198
|
assert self.template_data
|
|
3139
3199
|
|
|
3140
|
-
arch =
|
|
3200
|
+
arch = (
|
|
3201
|
+
self.package_platform == "Windows"
|
|
3202
|
+
and "{arch}" in build_output
|
|
3203
|
+
and self.flutter_arch()
|
|
3204
|
+
) or platform.machine().lower()
|
|
3141
3205
|
if arch in {"x86_64", "amd64"}:
|
|
3142
3206
|
arch = "x64"
|
|
3143
3207
|
elif arch in {"arm64", "aarch64"}:
|
|
@@ -3183,8 +3247,11 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
3183
3247
|
|
|
3184
3248
|
return ignore
|
|
3185
3249
|
|
|
3250
|
+
searched_outputs = []
|
|
3251
|
+
copied = False
|
|
3186
3252
|
for build_output in self.platforms[self.target_platform]["outputs"]:
|
|
3187
3253
|
build_output_dir = self.resolve_output_path(build_output)
|
|
3254
|
+
searched_outputs.append(build_output_dir)
|
|
3188
3255
|
|
|
3189
3256
|
if self.verbose > 0:
|
|
3190
3257
|
console.log(
|
|
@@ -3194,8 +3261,11 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
3194
3261
|
|
|
3195
3262
|
build_output_glob = os.path.basename(build_output_dir)
|
|
3196
3263
|
build_output_dir = os.path.dirname(build_output_dir)
|
|
3197
|
-
if not os.path.
|
|
3264
|
+
if not os.path.isdir(build_output_dir) or not any(
|
|
3265
|
+
build_output_glob in ("*", f) for f in os.listdir(build_output_dir)
|
|
3266
|
+
):
|
|
3198
3267
|
continue
|
|
3268
|
+
copied = True
|
|
3199
3269
|
|
|
3200
3270
|
if self.out_dir.exists():
|
|
3201
3271
|
rmtree(str(self.out_dir))
|
|
@@ -3208,6 +3278,13 @@ class BaseBuildCommand(BaseFlutterCommand):
|
|
|
3208
3278
|
ignore=make_ignore_fn(build_output_dir, build_output_glob),
|
|
3209
3279
|
)
|
|
3210
3280
|
|
|
3281
|
+
if not copied:
|
|
3282
|
+
self.cleanup(
|
|
3283
|
+
1,
|
|
3284
|
+
"Build output not found in "
|
|
3285
|
+
+ ", ".join(escape(path) for path in searched_outputs),
|
|
3286
|
+
)
|
|
3287
|
+
|
|
3211
3288
|
if self.target_platform == "web":
|
|
3212
3289
|
self.prune_cdn_assets()
|
|
3213
3290
|
if self.assets_path.exists():
|
|
@@ -59,6 +59,7 @@ class BaseFlutterCommand(BaseCommand):
|
|
|
59
59
|
self.emojis = {}
|
|
60
60
|
self.dart_exe = None
|
|
61
61
|
self.flutter_exe = None
|
|
62
|
+
self._flutter_arch: Optional[tuple[str, Optional[str]]] = None
|
|
62
63
|
self.required_flutter_version: Optional[version.Version] = None
|
|
63
64
|
self.verbose = False
|
|
64
65
|
self.require_android_sdk = False
|
|
@@ -155,16 +156,16 @@ class BaseFlutterCommand(BaseCommand):
|
|
|
155
156
|
self.flutter_exe = self.find_flutter_batch("flutter")
|
|
156
157
|
self.dart_exe = self.find_flutter_batch("dart")
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
):
|
|
159
|
+
sdk_found = bool(
|
|
160
|
+
self.flutter_exe and self.dart_exe and self.flutter_version_valid()
|
|
161
|
+
)
|
|
162
|
+
if not sdk_found or not self.flutter_sdk_supported():
|
|
163
163
|
if not self.assume_yes:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
if not sdk_found:
|
|
165
|
+
console.log(
|
|
166
|
+
"Flutter SDK not found or invalid version installed.",
|
|
167
|
+
style=warning_style,
|
|
168
|
+
)
|
|
168
169
|
prompt = (
|
|
169
170
|
f"Flutter SDK {self.required_flutter_version} is required. "
|
|
170
171
|
f"It will be installed now. Proceed? [y/n] "
|
|
@@ -227,6 +228,44 @@ class BaseFlutterCommand(BaseCommand):
|
|
|
227
228
|
console.log(1, "Failed to validate Flutter version.")
|
|
228
229
|
return False
|
|
229
230
|
|
|
231
|
+
def flutter_sdk_supported(self) -> bool:
|
|
232
|
+
"""
|
|
233
|
+
Check whether the Flutter SDK found can build for this command's target.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
`True` when the SDK can be used, otherwise `False`, in which case the
|
|
237
|
+
required Flutter SDK is installed and used instead.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
def flutter_arch(self) -> Optional[str]:
|
|
243
|
+
"""
|
|
244
|
+
Return the CPU architecture of the Flutter SDK's Dart, such as `x64` or
|
|
245
|
+
`arm64`. On Windows, Flutter builds desktop apps for this architecture.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The architecture reported by `dart --version`, or `None` when it can't
|
|
249
|
+
be determined.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
if not self.dart_exe:
|
|
253
|
+
return None
|
|
254
|
+
if self._flutter_arch and self._flutter_arch[0] == self.dart_exe:
|
|
255
|
+
return self._flutter_arch[1]
|
|
256
|
+
|
|
257
|
+
arch = None
|
|
258
|
+
result = self.run(
|
|
259
|
+
[self.dart_exe, "--version"], cwd=os.getcwd(), capture_output=True
|
|
260
|
+
)
|
|
261
|
+
output = f"{result.stdout or ''}{result.stderr or ''}"
|
|
262
|
+
# e.g. `Dart SDK version: 3.12.2 (stable) (...) on "windows_arm64"`
|
|
263
|
+
match = re.search(r'on "[a-z]+_([a-z0-9]+)"', output)
|
|
264
|
+
if match:
|
|
265
|
+
arch = match.group(1)
|
|
266
|
+
self._flutter_arch = (self.dart_exe, arch)
|
|
267
|
+
return arch
|
|
268
|
+
|
|
230
269
|
def install_flutter(self):
|
|
231
270
|
"""
|
|
232
271
|
Install required Flutter SDK and update command environment.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import errno
|
|
2
3
|
import logging
|
|
3
4
|
import os
|
|
4
5
|
import platform
|
|
@@ -20,6 +21,7 @@ from flet.app import DEFAULT_ASSETS_DIR
|
|
|
20
21
|
from flet.utils import (
|
|
21
22
|
get_free_tcp_port,
|
|
22
23
|
get_local_ip,
|
|
24
|
+
is_linux,
|
|
23
25
|
is_windows,
|
|
24
26
|
open_in_browser,
|
|
25
27
|
random_string,
|
|
@@ -201,8 +203,12 @@ class Command(BaseCommand):
|
|
|
201
203
|
|
|
202
204
|
This method prepares the script/module path, selects transport
|
|
203
205
|
configuration (port or UDS), resolves assets and ignore directories,
|
|
204
|
-
starts the child app process through
|
|
205
|
-
observer running until termination.
|
|
206
|
+
starts file watching, then starts the child app process through
|
|
207
|
+
`Handler`, and keeps the observer running until termination.
|
|
208
|
+
|
|
209
|
+
If starting the observer raises `OSError`, for example because the
|
|
210
|
+
Linux inotify watch or instance limit is reached, a warning is printed
|
|
211
|
+
and the app runs without reloading on changes.
|
|
206
212
|
|
|
207
213
|
Args:
|
|
208
214
|
options: Parsed command options produced by :meth:`add_arguments`.
|
|
@@ -361,7 +367,22 @@ class Command(BaseCommand):
|
|
|
361
367
|
|
|
362
368
|
my_observer = Observer()
|
|
363
369
|
my_observer.schedule(my_event_handler, script_dir, recursive=options.recursive)
|
|
364
|
-
|
|
370
|
+
try:
|
|
371
|
+
my_observer.start()
|
|
372
|
+
except OSError as e:
|
|
373
|
+
hint = (
|
|
374
|
+
" To enable it, raise the inotify limits: "
|
|
375
|
+
"https://flet.dev/docs/getting-started/running-app#linux-inotify-limits"
|
|
376
|
+
if is_linux() and e.errno in (errno.ENOSPC, errno.EMFILE)
|
|
377
|
+
else ""
|
|
378
|
+
)
|
|
379
|
+
print(
|
|
380
|
+
f"Warning: file watching is unavailable ({e.strerror or e}), "
|
|
381
|
+
f"so the app won't reload on changes.{hint}"
|
|
382
|
+
)
|
|
383
|
+
my_observer = None
|
|
384
|
+
|
|
385
|
+
my_event_handler.start_process()
|
|
365
386
|
|
|
366
387
|
try:
|
|
367
388
|
while True:
|
|
@@ -377,8 +398,9 @@ class Command(BaseCommand):
|
|
|
377
398
|
|
|
378
399
|
close_flet_view(my_event_handler.pid_file)
|
|
379
400
|
|
|
380
|
-
my_observer
|
|
381
|
-
|
|
401
|
+
if my_observer is not None:
|
|
402
|
+
my_observer.stop()
|
|
403
|
+
my_observer.join()
|
|
382
404
|
|
|
383
405
|
|
|
384
406
|
class Handler(FileSystemEventHandler):
|
|
@@ -435,7 +457,6 @@ class Handler(FileSystemEventHandler):
|
|
|
435
457
|
self.flet_app_temp_dir = flet_app_temp_dir
|
|
436
458
|
self.verbose = verbose
|
|
437
459
|
self.terminate = threading.Event()
|
|
438
|
-
self.start_process()
|
|
439
460
|
|
|
440
461
|
def start_process(self):
|
|
441
462
|
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = "1.0.4"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flet-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Flet CLI
|
|
5
5
|
Author-email: "Appveyor Systems Inc." <hello@flet.dev>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -9,8 +9,8 @@ Project-URL: Repository, https://github.com/flet-dev/flet
|
|
|
9
9
|
Project-URL: Documentation, https://flet.dev/docs
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
|
-
Requires-Dist: flet==1.0.
|
|
13
|
-
Requires-Dist: flet-platform-assets==1.0.
|
|
12
|
+
Requires-Dist: flet==1.0.4
|
|
13
|
+
Requires-Dist: flet-platform-assets==1.0.4
|
|
14
14
|
Requires-Dist: watchdog>=4.0.0
|
|
15
15
|
Requires-Dist: packaging>=25.0
|
|
16
16
|
Requires-Dist: qrcode>=7.4.2
|
|
@@ -55,6 +55,7 @@ src/flet_cli/utils/python_versions.py
|
|
|
55
55
|
src/flet_cli/utils/template_cache.py
|
|
56
56
|
tests/test_app_excludes.py
|
|
57
57
|
tests/test_build_app_path.py
|
|
58
|
+
tests/test_build_copy_output.py
|
|
58
59
|
tests/test_build_icons.py
|
|
59
60
|
tests/test_build_ios.py
|
|
60
61
|
tests/test_build_linux.py
|
|
@@ -63,6 +64,7 @@ tests/test_build_macos_signing.py
|
|
|
63
64
|
tests/test_build_platform_settings.py
|
|
64
65
|
tests/test_build_splash.py
|
|
65
66
|
tests/test_build_template.py
|
|
67
|
+
tests/test_build_windows_arm64.py
|
|
66
68
|
tests/test_clean.py
|
|
67
69
|
tests/test_cli_pass_through_args.py
|
|
68
70
|
tests/test_cli_positionals.py
|
|
@@ -75,4 +77,5 @@ tests/test_project_dependencies.py
|
|
|
75
77
|
tests/test_python_versions.py
|
|
76
78
|
tests/test_run_assets_dir.py
|
|
77
79
|
tests/test_run_desktop_dependency.py
|
|
78
|
-
tests/test_run_script_args.py
|
|
80
|
+
tests/test_run_script_args.py
|
|
81
|
+
tests/test_run_watcher_unavailable.py
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""How `flet build` copies Flutter's build output into the output directory."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from flet_cli.commands.build_base import BaseBuildCommand
|
|
9
|
+
|
|
10
|
+
WINDOWS_RELEASE = "build/windows/x64/runner/Release"
|
|
11
|
+
MACOS_RELEASE = "build/macos/Build/Products/Release"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _command(
|
|
15
|
+
tmp_path: Path, target_platform: str, artifact_name: str = "MyApp"
|
|
16
|
+
) -> BaseBuildCommand:
|
|
17
|
+
"""
|
|
18
|
+
Build a command that copies from `tmp_path/flutter` into `tmp_path/out`.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
tmp_path: pytest tmp dir.
|
|
22
|
+
target_platform: Key of `BaseBuildCommand.platforms`.
|
|
23
|
+
artifact_name: Value substituted for `{artifact_name}` in outputs.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
A command whose `cleanup` raises `SystemExit` with the exit code and
|
|
27
|
+
records the message in `cleanup_messages`.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
cmd = BaseBuildCommand(argparse.ArgumentParser())
|
|
31
|
+
cmd.options = argparse.Namespace()
|
|
32
|
+
cmd.verbose = 0
|
|
33
|
+
cmd.flutter_dir = tmp_path / "flutter"
|
|
34
|
+
cmd.out_dir = tmp_path / "out"
|
|
35
|
+
cmd.rel_out_dir = "out"
|
|
36
|
+
cmd.assets_path = tmp_path / "assets"
|
|
37
|
+
cmd.target_platform = target_platform
|
|
38
|
+
cmd.package_platform = cmd.platforms[target_platform]["package_platform"]
|
|
39
|
+
cmd.template_data = {
|
|
40
|
+
"artifact_name": artifact_name,
|
|
41
|
+
"project_name": "my_app",
|
|
42
|
+
"product_name": "My App",
|
|
43
|
+
}
|
|
44
|
+
cmd.emojis = {"checkmark": ""}
|
|
45
|
+
cmd.update_status = lambda *_: None
|
|
46
|
+
cmd.flutter_arch = lambda: "x64"
|
|
47
|
+
cmd.cleanup_messages = []
|
|
48
|
+
|
|
49
|
+
def cleanup(exit_code, message=None, no_border=False):
|
|
50
|
+
cmd.cleanup_messages.append(message)
|
|
51
|
+
raise SystemExit(exit_code)
|
|
52
|
+
|
|
53
|
+
cmd.cleanup = cleanup
|
|
54
|
+
return cmd
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_windows_release_contents_are_copied(tmp_path):
|
|
58
|
+
"""Every entry of the Windows `Release` directory lands in the output dir."""
|
|
59
|
+
release = tmp_path / "flutter" / WINDOWS_RELEASE
|
|
60
|
+
(release / "data").mkdir(parents=True)
|
|
61
|
+
(release / "my_app.exe").write_text("exe")
|
|
62
|
+
cmd = _command(tmp_path, "windows")
|
|
63
|
+
|
|
64
|
+
cmd.copy_build_output()
|
|
65
|
+
|
|
66
|
+
assert sorted(p.name for p in cmd.out_dir.iterdir()) == ["data", "my_app.exe"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_macos_copies_only_the_app_bundle(tmp_path):
|
|
70
|
+
"""Only `{artifact_name}.app` is copied out of the macOS `Release` directory."""
|
|
71
|
+
release = tmp_path / "flutter" / MACOS_RELEASE
|
|
72
|
+
(release / "MyApp.app" / "Contents").mkdir(parents=True)
|
|
73
|
+
(release / "FlutterMacOS.framework").mkdir()
|
|
74
|
+
cmd = _command(tmp_path, "macos")
|
|
75
|
+
|
|
76
|
+
cmd.copy_build_output()
|
|
77
|
+
|
|
78
|
+
assert [p.name for p in cmd.out_dir.iterdir()] == ["MyApp.app"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_macos_app_name_is_matched_literally(tmp_path):
|
|
82
|
+
"""An artifact name with glob characters still matches its `.app` bundle."""
|
|
83
|
+
release = tmp_path / "flutter" / MACOS_RELEASE
|
|
84
|
+
(release / "My App [Beta].app" / "Contents").mkdir(parents=True)
|
|
85
|
+
cmd = _command(tmp_path, "macos", artifact_name="My App [Beta]")
|
|
86
|
+
|
|
87
|
+
cmd.copy_build_output()
|
|
88
|
+
|
|
89
|
+
assert [p.name for p in cmd.out_dir.iterdir()] == ["My App [Beta].app"]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.parametrize(
|
|
93
|
+
("target_platform", "make_tree"),
|
|
94
|
+
[
|
|
95
|
+
pytest.param("windows", lambda f: None, id="windows-no-release-dir"),
|
|
96
|
+
pytest.param(
|
|
97
|
+
"windows",
|
|
98
|
+
lambda f: (f / WINDOWS_RELEASE).mkdir(parents=True),
|
|
99
|
+
id="windows-empty-release-dir",
|
|
100
|
+
),
|
|
101
|
+
pytest.param(
|
|
102
|
+
"macos",
|
|
103
|
+
lambda f: (f / MACOS_RELEASE / "Other.app").mkdir(parents=True),
|
|
104
|
+
id="macos-release-dir-without-app",
|
|
105
|
+
),
|
|
106
|
+
],
|
|
107
|
+
)
|
|
108
|
+
def test_missing_build_output_fails(tmp_path, target_platform, make_tree):
|
|
109
|
+
"""The build fails, naming the searched path, when no output matches."""
|
|
110
|
+
make_tree(tmp_path / "flutter")
|
|
111
|
+
cmd = _command(tmp_path, target_platform)
|
|
112
|
+
|
|
113
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
114
|
+
cmd.copy_build_output()
|
|
115
|
+
|
|
116
|
+
assert exc_info.value.code == 1
|
|
117
|
+
assert "Build output not found in" in cmd.cleanup_messages[-1]
|
|
118
|
+
assert not cmd.out_dir.exists()
|
|
@@ -105,3 +105,38 @@ class TestBuildTemplateContract:
|
|
|
105
105
|
)
|
|
106
106
|
parsed = yaml.safe_load(rendered)
|
|
107
107
|
assert parsed["description"] == re.sub(r"[\x00-\x1f]", " ", description)
|
|
108
|
+
|
|
109
|
+
@pytest.mark.parametrize("product", ["Flet Studio", 'My "App"', "back\\slash"])
|
|
110
|
+
def test_manifest_names_come_from_product(self, product):
|
|
111
|
+
"""iOS labels an installed PWA with the manifest's `short_name`, which
|
|
112
|
+
was the project name slug (`client`) instead of the product name."""
|
|
113
|
+
with open(BUILD_TEMPLATE_DIR.parent / "cookiecutter.json") as f:
|
|
114
|
+
default_short_name = json.load(f)["pwa_short_name"]
|
|
115
|
+
context = {
|
|
116
|
+
"project_name": "client",
|
|
117
|
+
"product_name": product,
|
|
118
|
+
"pwa_background_color": "#FFFFFF",
|
|
119
|
+
"pwa_theme_color": "#FF005F",
|
|
120
|
+
"project_description": "",
|
|
121
|
+
}
|
|
122
|
+
context["pwa_short_name"] = (
|
|
123
|
+
Environment().from_string(default_short_name).render(cookiecutter=context)
|
|
124
|
+
)
|
|
125
|
+
manifest = json.loads(
|
|
126
|
+
_render_template(BUILD_TEMPLATE_DIR / "web" / "manifest.json", **context)
|
|
127
|
+
)
|
|
128
|
+
assert manifest["name"] == product
|
|
129
|
+
assert manifest["short_name"] == product
|
|
130
|
+
|
|
131
|
+
def test_manifest_short_name_override(self):
|
|
132
|
+
rendered = _render_template(
|
|
133
|
+
BUILD_TEMPLATE_DIR / "web" / "manifest.json",
|
|
134
|
+
product_name="Flet Studio",
|
|
135
|
+
pwa_short_name="Studio",
|
|
136
|
+
pwa_background_color="#FFFFFF",
|
|
137
|
+
pwa_theme_color="#FF005F",
|
|
138
|
+
project_description="",
|
|
139
|
+
)
|
|
140
|
+
manifest = json.loads(rendered)
|
|
141
|
+
assert manifest["name"] == "Flet Studio"
|
|
142
|
+
assert manifest["short_name"] == "Studio"
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flutter SDK architecture handling for `flet build windows` on Windows on ARM.
|
|
3
|
+
|
|
4
|
+
Flet's Python runtime for Windows is x64-only, so an ARM64 Flutter SDK, which
|
|
5
|
+
builds ARM64 Windows apps, must not be used for a Windows build. The SDK's
|
|
6
|
+
architecture comes from `dart --version`, which is stubbed here, so no Flutter
|
|
7
|
+
SDK is needed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import os
|
|
12
|
+
from types import SimpleNamespace
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
from packaging import version
|
|
16
|
+
|
|
17
|
+
from flet_cli.commands.build import Command
|
|
18
|
+
from flet_cli.utils.flutter import get_flutter_dir
|
|
19
|
+
|
|
20
|
+
FLUTTER_VERSION = "3.44.8"
|
|
21
|
+
EXTERNAL_FLUTTER_EXE = os.path.join(os.sep + "opt", "flutter", "bin", "flutter")
|
|
22
|
+
# `dart --version` output, formatted with the `<os>_<arch>` Dart runs on.
|
|
23
|
+
DART_VERSION = (
|
|
24
|
+
'Dart SDK version: 3.12.2 (stable) (Tue Jun 9 01:11:39 2026 -0700) on "{}"\n'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Exit(Exception):
|
|
29
|
+
"""Captures `cleanup(1, message)` calls, which normally sys.exit."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, code, message):
|
|
32
|
+
self.code = code
|
|
33
|
+
self.message = message
|
|
34
|
+
super().__init__(f"cleanup({code}): {message}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def make_command(**attrs) -> Command:
|
|
38
|
+
"""
|
|
39
|
+
Build a `flet build windows` command on Windows without parsing a command line.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
attrs: Attributes to set on the command, overriding the defaults.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
A command whose `cleanup` raises `Exit` instead of exiting.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
cmd = Command(argparse.ArgumentParser())
|
|
49
|
+
cmd.required_flutter_version = version.Version(FLUTTER_VERSION)
|
|
50
|
+
cmd.current_platform = "Windows"
|
|
51
|
+
cmd.package_platform = "Windows"
|
|
52
|
+
cmd.target_platform = "windows"
|
|
53
|
+
|
|
54
|
+
def cleanup(code, message=None, **kwargs):
|
|
55
|
+
raise Exit(code, message)
|
|
56
|
+
|
|
57
|
+
cmd.cleanup = cleanup
|
|
58
|
+
for name, value in attrs.items():
|
|
59
|
+
setattr(cmd, name, value)
|
|
60
|
+
return cmd
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def dart_version_runner(stdout="", stderr=""):
|
|
64
|
+
"""
|
|
65
|
+
Return a stub for `run()` answering `dart --version`, recording its calls.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
stdout: Standard output of the stubbed `dart --version`.
|
|
69
|
+
stderr: Standard error of the stubbed `dart --version`.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
A `run()` replacement whose `calls` lists the arguments of each call.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def run(args, cwd, env=None, capture_output=True):
|
|
76
|
+
run.calls.append(args)
|
|
77
|
+
return SimpleNamespace(returncode=0, stdout=stdout, stderr=stderr)
|
|
78
|
+
|
|
79
|
+
run.calls = []
|
|
80
|
+
return run
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def unexpected_dart_call():
|
|
84
|
+
"""Fail the test if the SDK architecture is queried."""
|
|
85
|
+
raise AssertionError("`dart --version` must not run")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestFlutterArch:
|
|
89
|
+
"""How `flutter_arch()` reads the SDK architecture from `dart --version`."""
|
|
90
|
+
|
|
91
|
+
@pytest.mark.parametrize(
|
|
92
|
+
("stdout", "stderr", "expected"),
|
|
93
|
+
[
|
|
94
|
+
(DART_VERSION.format("windows_arm64"), "", "arm64"),
|
|
95
|
+
(DART_VERSION.format("windows_x64"), "", "x64"),
|
|
96
|
+
("", DART_VERSION.format("windows_arm64"), "arm64"),
|
|
97
|
+
],
|
|
98
|
+
ids=["windows_arm64", "windows_x64", "on-stderr"],
|
|
99
|
+
)
|
|
100
|
+
def test_parses_dart_version(self, stdout, stderr, expected):
|
|
101
|
+
"""The architecture suffix of the `on "<os>_<arch>"` part is returned."""
|
|
102
|
+
cmd = make_command(dart_exe="dart", run=dart_version_runner(stdout, stderr))
|
|
103
|
+
|
|
104
|
+
assert cmd.flutter_arch() == expected
|
|
105
|
+
|
|
106
|
+
def test_unparsable_output_is_none(self):
|
|
107
|
+
"""Output without an `on "<os>_<arch>"` part gives `None`."""
|
|
108
|
+
cmd = make_command(
|
|
109
|
+
dart_exe="dart", run=dart_version_runner("Dart SDK version: 3.12.2\n")
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
assert cmd.flutter_arch() is None
|
|
113
|
+
|
|
114
|
+
def test_cached_per_dart_executable(self):
|
|
115
|
+
"""Dart runs once per executable, so switching SDKs reads the new one."""
|
|
116
|
+
cmd = make_command(
|
|
117
|
+
dart_exe="arm64-dart",
|
|
118
|
+
run=dart_version_runner(DART_VERSION.format("windows_arm64")),
|
|
119
|
+
)
|
|
120
|
+
assert cmd.flutter_arch() == "arm64"
|
|
121
|
+
assert cmd.flutter_arch() == "arm64"
|
|
122
|
+
assert len(cmd.run.calls) == 1
|
|
123
|
+
|
|
124
|
+
cmd.dart_exe = "x64-dart"
|
|
125
|
+
cmd.run = dart_version_runner(DART_VERSION.format("windows_x64"))
|
|
126
|
+
assert cmd.flutter_arch() == "x64"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TestFlutterSdkSupported:
|
|
130
|
+
"""Which Flutter SDKs `flet build` accepts, by host, target and architecture."""
|
|
131
|
+
|
|
132
|
+
@pytest.mark.parametrize(
|
|
133
|
+
"flutter_exe",
|
|
134
|
+
[
|
|
135
|
+
EXTERNAL_FLUTTER_EXE,
|
|
136
|
+
os.path.join(
|
|
137
|
+
get_flutter_dir(FLUTTER_VERSION) + "-custom", "bin", "flutter"
|
|
138
|
+
),
|
|
139
|
+
],
|
|
140
|
+
ids=["outside-home", "sibling-of-managed-dir"],
|
|
141
|
+
)
|
|
142
|
+
def test_external_arm64_sdk_is_replaced(self, flutter_exe):
|
|
143
|
+
"""An ARM64 SDK outside Flet's own directory is swapped for Flet's x64 SDK."""
|
|
144
|
+
cmd = make_command(flutter_exe=flutter_exe, flutter_arch=lambda: "arm64")
|
|
145
|
+
|
|
146
|
+
assert cmd.flutter_sdk_supported() is False
|
|
147
|
+
|
|
148
|
+
def test_managed_arm64_sdk_fails(self):
|
|
149
|
+
"""An ARM64 SDK in Flet's own directory fails, asking to delete it."""
|
|
150
|
+
cmd = make_command(
|
|
151
|
+
flutter_exe=os.path.join(
|
|
152
|
+
get_flutter_dir(FLUTTER_VERSION), "bin", "flutter.bat"
|
|
153
|
+
),
|
|
154
|
+
flutter_arch=lambda: "arm64",
|
|
155
|
+
skip_flutter_doctor=False,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
with pytest.raises(Exit) as exc_info:
|
|
159
|
+
cmd.flutter_sdk_supported()
|
|
160
|
+
|
|
161
|
+
assert exc_info.value.code == 1
|
|
162
|
+
assert "Delete that directory" in exc_info.value.message
|
|
163
|
+
|
|
164
|
+
def test_x64_sdk_is_supported(self):
|
|
165
|
+
"""An x64 SDK builds Windows apps on any Windows host."""
|
|
166
|
+
cmd = make_command(flutter_exe=EXTERNAL_FLUTTER_EXE, flutter_arch=lambda: "x64")
|
|
167
|
+
|
|
168
|
+
assert cmd.flutter_sdk_supported() is True
|
|
169
|
+
|
|
170
|
+
@pytest.mark.parametrize(
|
|
171
|
+
("host", "package_platform"),
|
|
172
|
+
[
|
|
173
|
+
("Windows", "Android"),
|
|
174
|
+
("Darwin", "Darwin"),
|
|
175
|
+
("Linux", "Linux"),
|
|
176
|
+
("Darwin", "Windows"),
|
|
177
|
+
("Linux", "Windows"),
|
|
178
|
+
],
|
|
179
|
+
ids=[
|
|
180
|
+
"android-on-windows",
|
|
181
|
+
"macos-on-macos",
|
|
182
|
+
"linux-on-linux",
|
|
183
|
+
"windows-on-macos",
|
|
184
|
+
"windows-on-linux",
|
|
185
|
+
],
|
|
186
|
+
)
|
|
187
|
+
def test_other_builds_are_supported_without_running_dart(
|
|
188
|
+
self, host, package_platform
|
|
189
|
+
):
|
|
190
|
+
"""Builds other than Windows-on-Windows accept any SDK without asking Dart."""
|
|
191
|
+
cmd = make_command(
|
|
192
|
+
current_platform=host,
|
|
193
|
+
package_platform=package_platform,
|
|
194
|
+
flutter_exe=EXTERNAL_FLUTTER_EXE,
|
|
195
|
+
flutter_arch=unexpected_dart_call,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
assert cmd.flutter_sdk_supported() is True
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class TestWindowsBuildOutput:
|
|
202
|
+
"""Where `flet build windows` looks for the app Flutter built."""
|
|
203
|
+
|
|
204
|
+
@pytest.mark.parametrize(
|
|
205
|
+
("sdk_arch", "host_machine", "expected"),
|
|
206
|
+
[
|
|
207
|
+
("x64", "ARM64", "x64"),
|
|
208
|
+
("arm64", "AMD64", "arm64"),
|
|
209
|
+
(None, "AMD64", "x64"),
|
|
210
|
+
],
|
|
211
|
+
ids=["x64-sdk-on-arm64", "arm64-sdk", "unknown-sdk-arch"],
|
|
212
|
+
)
|
|
213
|
+
def test_output_follows_sdk_arch(
|
|
214
|
+
self, tmp_path, monkeypatch, sdk_arch, host_machine, expected
|
|
215
|
+
):
|
|
216
|
+
"""The output directory follows the SDK's architecture, not the host's."""
|
|
217
|
+
monkeypatch.setattr("platform.machine", lambda: host_machine)
|
|
218
|
+
cmd = make_command(
|
|
219
|
+
flutter_dir=tmp_path,
|
|
220
|
+
template_data={
|
|
221
|
+
"artifact_name": "app",
|
|
222
|
+
"project_name": "app",
|
|
223
|
+
"product_name": "app",
|
|
224
|
+
},
|
|
225
|
+
flutter_arch=lambda: sdk_arch,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
(output,) = cmd.platforms["windows"]["outputs"]
|
|
229
|
+
assert cmd.resolve_output_path(output) == str(
|
|
230
|
+
tmp_path / "build" / "windows" / expected / "runner" / "Release" / "*"
|
|
231
|
+
)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""`flet run` runs the app without hot reload when file watching can't start.
|
|
2
|
+
|
|
3
|
+
On Linux the watchdog observer is backed by inotify, and starting it raises
|
|
4
|
+
`OSError` with `ENOSPC` or `EMFILE` once the user's inotify watch or instance
|
|
5
|
+
limit is used up. `flet run` then prints a warning, links the docs on raising
|
|
6
|
+
those limits, and starts the app anyway.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import errno
|
|
10
|
+
import inspect
|
|
11
|
+
import threading
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from flet_cli.cli import parse_command_line
|
|
16
|
+
|
|
17
|
+
DOCS_URL = "https://flet.dev/docs/getting-started/running-app#linux-inotify-limits"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def run_flet(monkeypatch, tmp_path):
|
|
22
|
+
from flet_cli.commands import run as run_module
|
|
23
|
+
|
|
24
|
+
def invoke(observer_error=None, linux=None):
|
|
25
|
+
events = []
|
|
26
|
+
monkeypatch.setattr(
|
|
27
|
+
"flet.utils.pip.ensure_flet_desktop_package_installed", lambda: None
|
|
28
|
+
)
|
|
29
|
+
monkeypatch.setattr(
|
|
30
|
+
"flet.utils.pip.ensure_flet_web_package_installed", lambda: None
|
|
31
|
+
)
|
|
32
|
+
# Belt and braces: should the patches above ever stop applying, fail
|
|
33
|
+
# loudly instead of quietly pip-installing into the developer's venv.
|
|
34
|
+
monkeypatch.setattr(
|
|
35
|
+
"flet.utils.pip.install_flet_package",
|
|
36
|
+
lambda name: pytest.fail(f"test tried to install {name!r}"),
|
|
37
|
+
)
|
|
38
|
+
if linux is not None:
|
|
39
|
+
monkeypatch.setattr(run_module, "is_linux", lambda: linux)
|
|
40
|
+
|
|
41
|
+
real_signature = inspect.signature(run_module.Handler.__init__)
|
|
42
|
+
|
|
43
|
+
class StubHandler:
|
|
44
|
+
def __init__(self, **kwargs):
|
|
45
|
+
real_signature.bind(self, **kwargs)
|
|
46
|
+
self.pid_file = None
|
|
47
|
+
# Return control to `handle()` immediately.
|
|
48
|
+
self.terminate = threading.Event()
|
|
49
|
+
self.terminate.set()
|
|
50
|
+
|
|
51
|
+
def start_process(self):
|
|
52
|
+
events.append("start_process")
|
|
53
|
+
|
|
54
|
+
class StubObserver:
|
|
55
|
+
"""Tears down like a watchdog observer, which is a `Thread`."""
|
|
56
|
+
|
|
57
|
+
started = False
|
|
58
|
+
|
|
59
|
+
def schedule(self, *args, **kwargs): ...
|
|
60
|
+
|
|
61
|
+
def start(self):
|
|
62
|
+
events.append("start")
|
|
63
|
+
if observer_error is not None:
|
|
64
|
+
raise observer_error
|
|
65
|
+
self.started = True
|
|
66
|
+
|
|
67
|
+
def is_alive(self):
|
|
68
|
+
return self.started
|
|
69
|
+
|
|
70
|
+
def stop(self):
|
|
71
|
+
events.append("stop")
|
|
72
|
+
|
|
73
|
+
def join(self):
|
|
74
|
+
if not self.started:
|
|
75
|
+
raise RuntimeError("cannot join thread before it is started")
|
|
76
|
+
events.append("join")
|
|
77
|
+
|
|
78
|
+
monkeypatch.setattr(run_module, "Handler", StubHandler)
|
|
79
|
+
monkeypatch.setattr(run_module, "Observer", StubObserver)
|
|
80
|
+
|
|
81
|
+
script = tmp_path / "main.py"
|
|
82
|
+
script.write_text("import flet as ft\n", encoding="utf-8")
|
|
83
|
+
monkeypatch.chdir(tmp_path)
|
|
84
|
+
|
|
85
|
+
args = parse_command_line([str(script)])
|
|
86
|
+
args.handler(args)
|
|
87
|
+
return events
|
|
88
|
+
|
|
89
|
+
return invoke
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestTheAppRunsWithoutFileWatching:
|
|
93
|
+
"""An observer that fails to start costs hot reload, not the run."""
|
|
94
|
+
|
|
95
|
+
def test_app_is_started(self, run_flet):
|
|
96
|
+
"""The app starts, and the never-started observer is not joined."""
|
|
97
|
+
events = run_flet(OSError(errno.ENOSPC, "inotify watch limit reached"))
|
|
98
|
+
assert "start_process" in events
|
|
99
|
+
|
|
100
|
+
def test_watching_starts_before_the_app(self, run_flet):
|
|
101
|
+
"""An observer error is raised before any app process exists."""
|
|
102
|
+
events = run_flet(OSError(errno.ENOSPC, "inotify watch limit reached"))
|
|
103
|
+
assert events.index("start") < events.index("start_process")
|
|
104
|
+
|
|
105
|
+
def test_started_observer_is_torn_down(self, run_flet):
|
|
106
|
+
"""An observer that did start is stopped and joined when the run ends."""
|
|
107
|
+
assert run_flet()[-2:] == ["stop", "join"]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestTheWarning:
|
|
111
|
+
"""The warning names the error and links the docs for inotify limits only."""
|
|
112
|
+
|
|
113
|
+
@pytest.mark.parametrize(
|
|
114
|
+
("linux", "error", "detail", "hinted"),
|
|
115
|
+
[
|
|
116
|
+
(
|
|
117
|
+
True,
|
|
118
|
+
OSError(errno.ENOSPC, "inotify watch limit reached"),
|
|
119
|
+
"inotify watch limit reached",
|
|
120
|
+
True,
|
|
121
|
+
),
|
|
122
|
+
(
|
|
123
|
+
True,
|
|
124
|
+
OSError(errno.EMFILE, "inotify instance limit reached"),
|
|
125
|
+
"inotify instance limit reached",
|
|
126
|
+
True,
|
|
127
|
+
),
|
|
128
|
+
(
|
|
129
|
+
True,
|
|
130
|
+
OSError(errno.EPERM, "Operation not permitted"),
|
|
131
|
+
"Operation not permitted",
|
|
132
|
+
False,
|
|
133
|
+
),
|
|
134
|
+
(
|
|
135
|
+
False,
|
|
136
|
+
OSError(errno.ENOSPC, "No space left on device"),
|
|
137
|
+
"No space left on device",
|
|
138
|
+
False,
|
|
139
|
+
),
|
|
140
|
+
(True, OSError("watcher failed"), "watcher failed", False),
|
|
141
|
+
],
|
|
142
|
+
ids=[
|
|
143
|
+
"linux-watch-limit",
|
|
144
|
+
"linux-instance-limit",
|
|
145
|
+
"linux-other-errno",
|
|
146
|
+
"not-linux",
|
|
147
|
+
"no-errno",
|
|
148
|
+
],
|
|
149
|
+
)
|
|
150
|
+
def test_docs_link(self, run_flet, capsys, linux, error, detail, hinted):
|
|
151
|
+
"""Only a Linux inotify watch or instance limit error links the docs."""
|
|
152
|
+
run_flet(error, linux=linux)
|
|
153
|
+
out = capsys.readouterr().out
|
|
154
|
+
assert f"file watching is unavailable ({detail})" in out
|
|
155
|
+
assert (DOCS_URL in out) is hinted
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
version = "1.0.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flet_cli-1.0.2 → flet_cli-1.0.4}/src/flet_cli/__pyinstaller/rthooks/pyi_rth_localhost_fletd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|