pythonnative 0.24.0__py3-none-any.whl → 0.26.0__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.
- pythonnative/__init__.py +16 -4
- pythonnative/animated.py +2 -2
- pythonnative/cli/pn.py +172 -12
- pythonnative/components.py +59 -0
- pythonnative/diagnostics.py +27 -0
- pythonnative/hooks.py +364 -81
- pythonnative/native_modules/app_state.py +2 -1
- pythonnative/native_modules/battery.py +3 -2
- pythonnative/native_modules/biometrics.py +2 -1
- pythonnative/native_modules/camera.py +16 -7
- pythonnative/native_modules/clipboard.py +3 -2
- pythonnative/native_modules/file_system.py +3 -2
- pythonnative/native_modules/haptics.py +7 -6
- pythonnative/native_modules/linking.py +62 -6
- pythonnative/native_modules/location.py +11 -8
- pythonnative/native_modules/net_info.py +2 -1
- pythonnative/native_modules/notifications.py +132 -9
- pythonnative/native_modules/permissions.py +27 -3
- pythonnative/native_modules/share.py +2 -1
- pythonnative/native_views/android.py +149 -124
- pythonnative/native_views/desktop.py +50 -49
- pythonnative/native_views/ios.py +240 -161
- pythonnative/project/android.py +15 -3
- pythonnative/project/builder.py +172 -70
- pythonnative/project/config.py +28 -6
- pythonnative/project/devices.py +291 -0
- pythonnative/project/doctor.py +5 -4
- pythonnative/project/ios.py +51 -77
- pythonnative/project/permissions.py +19 -1
- pythonnative/project/runtime_assets.py +78 -166
- pythonnative/reconciler.py +518 -37
- pythonnative/runtime.py +407 -215
- pythonnative/screen.py +53 -7
- pythonnative/storage.py +2 -2
- pythonnative/suspense.py +417 -0
- pythonnative/templates/android_template/app/build.gradle +5 -0
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +1 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +88 -12
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +41 -17
- pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
- pythonnative/templates/ios_template/ios_template/Info.plist +0 -2
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +397 -0
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +31 -20
- pythonnative/templates/ios_template/ios_template/ViewController.swift +77 -198
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +72 -47
- {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/METADATA +2 -1
- {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/RECORD +51 -48
- {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/WHEEL +1 -1
- pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -24
- {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.24.0.dist-info → pythonnative-0.26.0.dist-info}/top_level.txt +0 -0
pythonnative/project/android.py
CHANGED
|
@@ -255,7 +255,7 @@ def configure_strings(project_dir: Path, config: AppConfig) -> None:
|
|
|
255
255
|
|
|
256
256
|
|
|
257
257
|
def configure_manifest(project_dir: Path, config: AppConfig) -> None:
|
|
258
|
-
"""Inject
|
|
258
|
+
"""Inject permissions, the launch orientation, and deep-link filters.
|
|
259
259
|
|
|
260
260
|
Args:
|
|
261
261
|
project_dir: The staged Android project root.
|
|
@@ -272,11 +272,23 @@ def configure_manifest(project_dir: Path, config: AppConfig) -> None:
|
|
|
272
272
|
orientation_attr = _ANDROID_ORIENTATION.get(config.orientation)
|
|
273
273
|
if orientation_attr:
|
|
274
274
|
content = content.replace(
|
|
275
|
-
' android:exported="true"
|
|
276
|
-
f' android:exported="true"\n android:screenOrientation="{orientation_attr}"
|
|
275
|
+
' android:exported="true"',
|
|
276
|
+
f' android:exported="true"\n android:screenOrientation="{orientation_attr}"',
|
|
277
277
|
1,
|
|
278
278
|
)
|
|
279
279
|
|
|
280
|
+
if config.url_schemes:
|
|
281
|
+
schemes = "".join(f' <data android:scheme="{scheme}" />\n' for scheme in config.url_schemes)
|
|
282
|
+
deep_link_filter = (
|
|
283
|
+
" <intent-filter>\n"
|
|
284
|
+
' <action android:name="android.intent.action.VIEW" />\n'
|
|
285
|
+
' <category android:name="android.intent.category.DEFAULT" />\n'
|
|
286
|
+
' <category android:name="android.intent.category.BROWSABLE" />\n'
|
|
287
|
+
f"{schemes}"
|
|
288
|
+
" </intent-filter>\n"
|
|
289
|
+
)
|
|
290
|
+
content = content.replace(" </activity>", f"{deep_link_filter} </activity>", 1)
|
|
291
|
+
|
|
280
292
|
manifest_path.write_text(content, encoding="utf-8")
|
|
281
293
|
|
|
282
294
|
|
pythonnative/project/builder.py
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
The [`Builder`][pythonnative.project.builder.Builder] ties the pieces
|
|
4
4
|
together: it stages the bundled native template, runs the platform
|
|
5
|
-
[`configurators`][pythonnative.project.android],
|
|
6
|
-
|
|
7
|
-
the
|
|
5
|
+
[`configurators`][pythonnative.project.android], stages the Python
|
|
6
|
+
sources and (on iOS) the embedded CPython runtime into the native
|
|
7
|
+
project, and invokes the native toolchains (Gradle / Xcode).
|
|
8
|
+
|
|
9
|
+
On iOS, the staged project is self-contained: ``Python.xcframework`` is
|
|
10
|
+
linked at build time and an Xcode build phase installs the standard
|
|
11
|
+
library and app code into the bundle, so ``xcodebuild`` output is
|
|
12
|
+
directly runnable and archivable with no post-build patching.
|
|
8
13
|
|
|
9
14
|
All shell-outs go through a small
|
|
10
15
|
[`CommandRunner`][pythonnative.project.builder.CommandRunner] abstraction
|
|
@@ -20,7 +25,9 @@ builder stops at producing installable/archivable artifacts.
|
|
|
20
25
|
|
|
21
26
|
from __future__ import annotations
|
|
22
27
|
|
|
28
|
+
import compileall
|
|
23
29
|
import os
|
|
30
|
+
import platform as platform_module
|
|
24
31
|
import subprocess
|
|
25
32
|
import sys
|
|
26
33
|
from dataclasses import dataclass, field
|
|
@@ -251,11 +258,15 @@ class Builder:
|
|
|
251
258
|
|
|
252
259
|
# -- Preparation ----------------------------------------------------
|
|
253
260
|
|
|
254
|
-
def prepare(self, platform: str) -> PreparedProject:
|
|
261
|
+
def prepare(self, platform: str, *, release: bool = False) -> PreparedProject:
|
|
255
262
|
"""Stage and configure the native project for ``platform``.
|
|
256
263
|
|
|
257
264
|
Args:
|
|
258
265
|
platform: ``"android"`` or ``"ios"``.
|
|
266
|
+
release: Prepare for a release/store build. On iOS this
|
|
267
|
+
byte-compiles the staged Python code and drops the
|
|
268
|
+
``.py`` sources from the bundle. (Chaquopy does the
|
|
269
|
+
equivalent on Android automatically.)
|
|
259
270
|
|
|
260
271
|
Returns:
|
|
261
272
|
A [`PreparedProject`][pythonnative.project.builder.PreparedProject].
|
|
@@ -286,9 +297,8 @@ class Builder:
|
|
|
286
297
|
)
|
|
287
298
|
|
|
288
299
|
ios_layout = ios_config.configure(project_dir, self.config, log=self.log)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
self._stage_ios_python(build_dir)
|
|
300
|
+
self._stage_ios_python(project_dir, release=release)
|
|
301
|
+
self._link_ios_runtime(project_dir)
|
|
292
302
|
return PreparedProject(
|
|
293
303
|
platform=platform,
|
|
294
304
|
build_dir=build_dir,
|
|
@@ -297,25 +307,101 @@ class Builder:
|
|
|
297
307
|
ios=ios_layout,
|
|
298
308
|
)
|
|
299
309
|
|
|
300
|
-
def _stage_ios_python(self,
|
|
310
|
+
def _stage_ios_python(self, project_dir: Path, *, release: bool) -> None:
|
|
311
|
+
"""Stage ``app/`` and ``app_packages/`` at the Xcode project root.
|
|
312
|
+
|
|
313
|
+
The Xcode project references both folders as bundle resources
|
|
314
|
+
and the "Install Python runtime" build phase converts any
|
|
315
|
+
binary modules inside them into signed frameworks.
|
|
316
|
+
"""
|
|
301
317
|
import shutil
|
|
302
318
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
319
|
+
app_dir = project_dir / "app"
|
|
320
|
+
packages_dir = project_dir / "app_packages"
|
|
321
|
+
for directory in (app_dir, packages_dir):
|
|
322
|
+
if directory.exists():
|
|
323
|
+
shutil.rmtree(directory)
|
|
324
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
307
325
|
|
|
308
326
|
app_src = self.config.project_root / "app"
|
|
309
|
-
if app_src.is_dir():
|
|
310
|
-
|
|
327
|
+
if not app_src.is_dir():
|
|
328
|
+
raise BuildError(f"No app/ directory found at {app_src}; nothing to bundle.")
|
|
329
|
+
shutil.copytree(app_src, app_dir, dirs_exist_ok=True)
|
|
330
|
+
|
|
311
331
|
if self.dev_lib_root.is_dir():
|
|
312
332
|
shutil.copytree(
|
|
313
333
|
self.dev_lib_root,
|
|
314
|
-
|
|
334
|
+
packages_dir / "pythonnative",
|
|
315
335
|
dirs_exist_ok=True,
|
|
316
336
|
ignore=android_config.LIB_IGNORE,
|
|
317
337
|
)
|
|
318
|
-
|
|
338
|
+
|
|
339
|
+
# rubicon-objc supplies the iOS Objective-C bridge; user
|
|
340
|
+
# requirements ride along in the same site directory.
|
|
341
|
+
result = self.runner.run(
|
|
342
|
+
[sys.executable, "-m", "pip", "install", "--no-deps", "--upgrade", "rubicon-objc", "-t", str(packages_dir)],
|
|
343
|
+
capture=True,
|
|
344
|
+
)
|
|
345
|
+
if not result.ok:
|
|
346
|
+
raise BuildError(f"pip install rubicon-objc failed:\n{result.stderr}")
|
|
347
|
+
if self.config.requirements:
|
|
348
|
+
result = self.runner.run(
|
|
349
|
+
[sys.executable, "-m", "pip", "install", "-t", str(packages_dir), *self.config.requirements],
|
|
350
|
+
capture=True,
|
|
351
|
+
)
|
|
352
|
+
if not result.ok:
|
|
353
|
+
raise BuildError(f"pip install of [requirements].packages failed:\n{result.stderr}")
|
|
354
|
+
|
|
355
|
+
if release:
|
|
356
|
+
self._compile_ios_bytecode(app_dir, packages_dir)
|
|
357
|
+
|
|
358
|
+
def _compile_ios_bytecode(self, *roots: Path) -> None:
|
|
359
|
+
"""Byte-compile staged Python code and drop the ``.py`` sources.
|
|
360
|
+
|
|
361
|
+
Ships ``.pyc`` only: smaller bundles, faster cold start, and no
|
|
362
|
+
plain-text source in the store artifact. Requires the host
|
|
363
|
+
interpreter to match ``app.python_version``, since bytecode is
|
|
364
|
+
version-specific.
|
|
365
|
+
"""
|
|
366
|
+
host_version = f"{sys.version_info[0]}.{sys.version_info[1]}"
|
|
367
|
+
if host_version != self.config.python_version:
|
|
368
|
+
self.log(
|
|
369
|
+
f"Skipping bytecode compilation: the build host runs Python {host_version} "
|
|
370
|
+
f"but app.python_version is {self.config.python_version}, and bytecode is "
|
|
371
|
+
"version-specific. The app will ship .py sources. Run pn with Python "
|
|
372
|
+
f"{self.config.python_version} to ship bytecode."
|
|
373
|
+
)
|
|
374
|
+
return
|
|
375
|
+
for root in roots:
|
|
376
|
+
# legacy=True writes module.pyc next to module.py (no
|
|
377
|
+
# __pycache__), which the embedded interpreter imports
|
|
378
|
+
# directly once the .py files are gone.
|
|
379
|
+
if not compileall.compile_dir(str(root), quiet=1, legacy=True, optimize=1):
|
|
380
|
+
raise BuildError(f"Byte-compiling {root} failed; fix the syntax error above.")
|
|
381
|
+
for source in root.rglob("*.py"):
|
|
382
|
+
source.unlink()
|
|
383
|
+
self.log("Compiled Python sources to bytecode for release.")
|
|
384
|
+
|
|
385
|
+
def _link_ios_runtime(self, project_dir: Path) -> None:
|
|
386
|
+
"""Make ``Python.xcframework`` available at the project root.
|
|
387
|
+
|
|
388
|
+
A symlink into the shared runtime cache is preferred (the
|
|
389
|
+
framework is hundreds of megabytes); staging falls back to a
|
|
390
|
+
copy when symlinks are unavailable.
|
|
391
|
+
"""
|
|
392
|
+
import shutil
|
|
393
|
+
|
|
394
|
+
runtime = self._ios_runtime()
|
|
395
|
+
link = project_dir / "Python.xcframework"
|
|
396
|
+
if link.is_symlink() or link.is_file():
|
|
397
|
+
link.unlink()
|
|
398
|
+
elif link.is_dir():
|
|
399
|
+
shutil.rmtree(link)
|
|
400
|
+
try:
|
|
401
|
+
link.symlink_to(runtime.xcframework_dir, target_is_directory=True)
|
|
402
|
+
except OSError:
|
|
403
|
+
shutil.copytree(runtime.xcframework_dir, link)
|
|
404
|
+
self.log(f"Linked Python.xcframework (Python {self.config.python_version}).")
|
|
319
405
|
|
|
320
406
|
# -- Android builds -------------------------------------------------
|
|
321
407
|
|
|
@@ -378,21 +464,28 @@ class Builder:
|
|
|
378
464
|
# -- iOS builds -----------------------------------------------------
|
|
379
465
|
|
|
380
466
|
def build_ios_simulator(self, prepared: PreparedProject) -> Path:
|
|
381
|
-
"""Build the iOS app for the simulator
|
|
467
|
+
"""Build the iOS app for the simulator.
|
|
468
|
+
|
|
469
|
+
The Xcode build produces a complete, runnable ``.app``: the
|
|
470
|
+
Python framework is linked and the "Install Python runtime"
|
|
471
|
+
build phase embeds the standard library and staged sources.
|
|
382
472
|
|
|
383
473
|
Args:
|
|
384
474
|
prepared: A prepared iOS project.
|
|
385
475
|
|
|
386
476
|
Returns:
|
|
387
|
-
Path to the built ``.app
|
|
477
|
+
Path to the built ``.app``.
|
|
388
478
|
|
|
389
479
|
Raises:
|
|
390
|
-
BuildError: If the
|
|
391
|
-
fails.
|
|
480
|
+
BuildError: If the build fails.
|
|
392
481
|
"""
|
|
393
|
-
runtime = self._ios_runtime()
|
|
394
482
|
derived = prepared.project_dir / "build"
|
|
395
483
|
settings = ios_config.build_settings(self.config)
|
|
484
|
+
# The simulator runs on the host CPU, and BeeWare's runtime
|
|
485
|
+
# install script requires a single-architecture build (its
|
|
486
|
+
# stdlib layout is per-arch), so pin ARCHS to the host.
|
|
487
|
+
host_arch = "arm64" if platform_module.machine() == "arm64" else "x86_64"
|
|
488
|
+
settings.append(f"ARCHS={host_arch}")
|
|
396
489
|
result = self.runner.run(
|
|
397
490
|
[
|
|
398
491
|
"xcodebuild",
|
|
@@ -417,35 +510,72 @@ class Builder:
|
|
|
417
510
|
app_path = derived / "Build" / "Products" / "Debug-iphonesimulator" / ios_config.APP_BUNDLE_NAME
|
|
418
511
|
if not app_path.is_dir():
|
|
419
512
|
raise BuildError(f"Built app not found at {app_path}.")
|
|
420
|
-
|
|
421
|
-
site_packages = self._install_ios_site_packages(prepared.build_dir)
|
|
422
|
-
ios_config.embed_runtime(
|
|
423
|
-
app_path,
|
|
424
|
-
runtime=runtime,
|
|
425
|
-
destination="simulator",
|
|
426
|
-
python_sources=prepared.build_dir / "python",
|
|
427
|
-
site_packages=site_packages,
|
|
428
|
-
log=self.log,
|
|
429
|
-
)
|
|
430
513
|
return app_path
|
|
431
514
|
|
|
432
|
-
def
|
|
433
|
-
"""
|
|
515
|
+
def build_ios_device(self, prepared: PreparedProject) -> Path:
|
|
516
|
+
"""Build a signed Debug app for a physical iOS device.
|
|
434
517
|
|
|
435
|
-
|
|
436
|
-
the archive before export and relies on ``xcodebuild`` to re-sign
|
|
437
|
-
the embedded framework.
|
|
518
|
+
Requires ``[ios].development_team`` for automatic signing.
|
|
438
519
|
|
|
439
520
|
Args:
|
|
440
521
|
prepared: A prepared iOS project.
|
|
441
522
|
|
|
523
|
+
Returns:
|
|
524
|
+
Path to the built ``.app``.
|
|
525
|
+
|
|
526
|
+
Raises:
|
|
527
|
+
BuildError: If the build fails or signing is not set up.
|
|
528
|
+
"""
|
|
529
|
+
if not self.config.ios.development_team:
|
|
530
|
+
raise BuildError(
|
|
531
|
+
"Running on a physical iOS device requires code signing. "
|
|
532
|
+
"Set [ios].development_team in pythonnative.toml."
|
|
533
|
+
)
|
|
534
|
+
derived = prepared.project_dir / "build"
|
|
535
|
+
settings = ios_config.build_settings(self.config)
|
|
536
|
+
settings.append("-allowProvisioningUpdates")
|
|
537
|
+
result = self.runner.run(
|
|
538
|
+
[
|
|
539
|
+
"xcodebuild",
|
|
540
|
+
"-project",
|
|
541
|
+
ios_config.PROJECT_FILE,
|
|
542
|
+
"-scheme",
|
|
543
|
+
ios_config.PROJECT_NAME,
|
|
544
|
+
"-configuration",
|
|
545
|
+
"Debug",
|
|
546
|
+
"-destination",
|
|
547
|
+
"generic/platform=iOS",
|
|
548
|
+
"-derivedDataPath",
|
|
549
|
+
str(derived),
|
|
550
|
+
"build",
|
|
551
|
+
*settings,
|
|
552
|
+
],
|
|
553
|
+
cwd=prepared.project_dir,
|
|
554
|
+
)
|
|
555
|
+
if not result.ok:
|
|
556
|
+
raise BuildError("xcodebuild (device) failed. See output above.")
|
|
557
|
+
|
|
558
|
+
app_path = derived / "Build" / "Products" / "Debug-iphoneos" / ios_config.APP_BUNDLE_NAME
|
|
559
|
+
if not app_path.is_dir():
|
|
560
|
+
raise BuildError(f"Built app not found at {app_path}.")
|
|
561
|
+
return app_path
|
|
562
|
+
|
|
563
|
+
def build_ios_archive(self, prepared: PreparedProject, *, upload: bool = False) -> BuildArtifacts:
|
|
564
|
+
"""Archive the iOS app and export a signed IPA (or upload it).
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
prepared: A prepared iOS project (use ``release=True``).
|
|
568
|
+
upload: Upload directly to App Store Connect instead of
|
|
569
|
+
exporting a local ``.ipa`` (requires
|
|
570
|
+
``export_method = "app-store"`` and stored ASC
|
|
571
|
+
credentials in Xcode).
|
|
572
|
+
|
|
442
573
|
Returns:
|
|
443
574
|
The produced ``.ipa`` (and ``.xcarchive``) paths.
|
|
444
575
|
|
|
445
576
|
Raises:
|
|
446
577
|
BuildError: If archiving or export fails.
|
|
447
578
|
"""
|
|
448
|
-
runtime = self._ios_runtime()
|
|
449
579
|
archive_path = prepared.build_dir / "ios_template.xcarchive"
|
|
450
580
|
settings = ios_config.build_settings(self.config, for_archive=True)
|
|
451
581
|
result = self.runner.run(
|
|
@@ -461,6 +591,7 @@ class Builder:
|
|
|
461
591
|
"generic/platform=iOS",
|
|
462
592
|
"-archivePath",
|
|
463
593
|
str(archive_path),
|
|
594
|
+
"-allowProvisioningUpdates",
|
|
464
595
|
"archive",
|
|
465
596
|
*settings,
|
|
466
597
|
],
|
|
@@ -469,20 +600,10 @@ class Builder:
|
|
|
469
600
|
if not result.ok:
|
|
470
601
|
raise BuildError("xcodebuild archive failed. See output above.")
|
|
471
602
|
|
|
472
|
-
app_in_archive = archive_path / "Products" / "Applications" / ios_config.APP_BUNDLE_NAME
|
|
473
|
-
if app_in_archive.is_dir():
|
|
474
|
-
site_packages = self._install_ios_site_packages(prepared.build_dir)
|
|
475
|
-
ios_config.embed_runtime(
|
|
476
|
-
app_in_archive,
|
|
477
|
-
runtime=runtime,
|
|
478
|
-
destination="device",
|
|
479
|
-
python_sources=prepared.build_dir / "python",
|
|
480
|
-
site_packages=site_packages,
|
|
481
|
-
log=self.log,
|
|
482
|
-
)
|
|
483
|
-
|
|
484
603
|
export_dir = prepared.build_dir / "export"
|
|
485
|
-
options = ios_config.write_export_options(
|
|
604
|
+
options = ios_config.write_export_options(
|
|
605
|
+
self.config, prepared.build_dir / "exportOptions.plist", upload=upload
|
|
606
|
+
)
|
|
486
607
|
result = self.runner.run(
|
|
487
608
|
[
|
|
488
609
|
"xcodebuild",
|
|
@@ -493,6 +614,7 @@ class Builder:
|
|
|
493
614
|
str(options),
|
|
494
615
|
"-exportPath",
|
|
495
616
|
str(export_dir),
|
|
617
|
+
"-allowProvisioningUpdates",
|
|
496
618
|
],
|
|
497
619
|
cwd=prepared.project_dir,
|
|
498
620
|
)
|
|
@@ -508,26 +630,6 @@ class Builder:
|
|
|
508
630
|
except RuntimeError as exc:
|
|
509
631
|
raise BuildError(str(exc)) from exc
|
|
510
632
|
|
|
511
|
-
def _install_ios_site_packages(self, build_dir: Path) -> Optional[Path]:
|
|
512
|
-
import shutil
|
|
513
|
-
|
|
514
|
-
site_dir = build_dir / "platform-site"
|
|
515
|
-
if site_dir.exists():
|
|
516
|
-
shutil.rmtree(site_dir)
|
|
517
|
-
site_dir.mkdir(parents=True, exist_ok=True)
|
|
518
|
-
|
|
519
|
-
# rubicon-objc supplies the iOS Objective-C bridge.
|
|
520
|
-
self.runner.run(
|
|
521
|
-
[sys.executable, "-m", "pip", "install", "--no-deps", "--upgrade", "rubicon-objc", "-t", str(site_dir)],
|
|
522
|
-
capture=True,
|
|
523
|
-
)
|
|
524
|
-
if self.config.requirements:
|
|
525
|
-
self.runner.run(
|
|
526
|
-
[sys.executable, "-m", "pip", "install", "-t", str(site_dir), *self.config.requirements],
|
|
527
|
-
capture=True,
|
|
528
|
-
)
|
|
529
|
-
return site_dir
|
|
530
|
-
|
|
531
633
|
|
|
532
634
|
# ======================================================================
|
|
533
635
|
# Helpers
|
pythonnative/project/config.py
CHANGED
|
@@ -21,6 +21,7 @@ build = 1 # integer build number
|
|
|
21
21
|
python_version = "3.11" # embedded CPython version
|
|
22
22
|
orientation = "portrait" # portrait | landscape | all
|
|
23
23
|
entry_point = "app/main.py" # module whose `App` is mounted
|
|
24
|
+
url_schemes = ["myapp"] # custom deep-link URL schemes
|
|
24
25
|
|
|
25
26
|
[permissions] # see pythonnative.project.permissions
|
|
26
27
|
camera = "Scan receipts."
|
|
@@ -45,7 +46,7 @@ provisioning_profile = "My App Distribution"
|
|
|
45
46
|
[android]
|
|
46
47
|
min_sdk = 24
|
|
47
48
|
target_sdk = 34
|
|
48
|
-
abi_filters = ["arm64-v8a", "x86_64"]
|
|
49
|
+
abi_filters = ["arm64-v8a", "x86_64"] # the default; add armeabi-v7a/x86 if needed
|
|
49
50
|
|
|
50
51
|
[android.signing]
|
|
51
52
|
keystore = "release.keystore"
|
|
@@ -72,10 +73,12 @@ CONFIG_FILENAME = "pythonnative.toml"
|
|
|
72
73
|
"""The fixed config filename looked up at the project root."""
|
|
73
74
|
|
|
74
75
|
SUPPORTED_PYTHON_VERSIONS = ("3.10", "3.11", "3.12")
|
|
75
|
-
"""CPython versions accepted in ``app.python_version``.
|
|
76
|
+
"""CPython versions accepted in ``app.python_version``.
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
Every listed version has a pinned, checksum-verified iOS runtime (see
|
|
79
|
+
``runtime_assets.PINNED_ASSETS``) and is supported by Chaquopy on
|
|
80
|
+
Android.
|
|
81
|
+
"""
|
|
79
82
|
|
|
80
83
|
VALID_ORIENTATIONS = ("portrait", "landscape", "all")
|
|
81
84
|
"""Accepted values for ``app.orientation``."""
|
|
@@ -93,6 +96,7 @@ _JAVA_KEYWORDS = frozenset("""
|
|
|
93
96
|
|
|
94
97
|
_APP_ID_SEGMENT = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
95
98
|
_VERSION_RE = re.compile(r"^\d+(\.\d+){0,3}$")
|
|
99
|
+
_URL_SCHEME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*$")
|
|
96
100
|
|
|
97
101
|
|
|
98
102
|
class ConfigError(Exception):
|
|
@@ -193,7 +197,9 @@ class AndroidConfig:
|
|
|
193
197
|
target_sdk: int = 34
|
|
194
198
|
compile_sdk: int = 34
|
|
195
199
|
application_id: Optional[str] = None
|
|
196
|
-
|
|
200
|
+
# arm64 devices plus x86_64 emulators. 32-bit ABIs (armeabi-v7a, x86)
|
|
201
|
+
# roughly double the native payload and are opt-in.
|
|
202
|
+
abi_filters: List[str] = field(default_factory=lambda: ["arm64-v8a", "x86_64"])
|
|
197
203
|
permissions: List[str] = field(default_factory=list)
|
|
198
204
|
signing: AndroidSigning = field(default_factory=AndroidSigning)
|
|
199
205
|
|
|
@@ -222,6 +228,9 @@ class AppConfig:
|
|
|
222
228
|
python_version: Embedded CPython version (``app.python_version``).
|
|
223
229
|
orientation: ``"portrait"``, ``"landscape"``, or ``"all"``.
|
|
224
230
|
entry_point: Path to the entry module (``app.entry_point``).
|
|
231
|
+
url_schemes: Custom deep-link URL schemes the app handles
|
|
232
|
+
(``app.url_schemes``); wired into ``CFBundleURLTypes`` on
|
|
233
|
+
iOS and a ``VIEW`` intent filter on Android.
|
|
225
234
|
permissions: The declared capability map (``[permissions]``).
|
|
226
235
|
icon: Optional source icon path (``[assets].icon``).
|
|
227
236
|
splash: Optional splash image path (``[assets].splash``).
|
|
@@ -240,6 +249,7 @@ class AppConfig:
|
|
|
240
249
|
python_version: str = "3.11"
|
|
241
250
|
orientation: str = "portrait"
|
|
242
251
|
entry_point: str = "app/main.py"
|
|
252
|
+
url_schemes: List[str] = field(default_factory=list)
|
|
243
253
|
permissions: Dict[str, _permissions.PermissionValue] = field(default_factory=dict)
|
|
244
254
|
icon: Optional[str] = None
|
|
245
255
|
splash: Optional[str] = None
|
|
@@ -358,6 +368,7 @@ class AppConfig:
|
|
|
358
368
|
python_version=_opt_str(app, "python_version") or "3.11",
|
|
359
369
|
orientation=(_opt_str(app, "orientation") or "portrait").lower(),
|
|
360
370
|
entry_point=_opt_str(app, "entry_point") or "app/main.py",
|
|
371
|
+
url_schemes=_opt_str_list(app, "url_schemes"),
|
|
361
372
|
permissions=dict(_expect_table(data, "permissions", optional=True)),
|
|
362
373
|
ios=_parse_ios(_expect_table(data, "ios", optional=True)),
|
|
363
374
|
android=_parse_android(_expect_table(data, "android", optional=True)),
|
|
@@ -406,6 +417,13 @@ class AppConfig:
|
|
|
406
417
|
if not self.entry_point.strip():
|
|
407
418
|
raise ConfigError("app.entry_point must not be empty.")
|
|
408
419
|
|
|
420
|
+
for scheme in self.url_schemes:
|
|
421
|
+
if not _URL_SCHEME_RE.match(scheme):
|
|
422
|
+
raise ConfigError(
|
|
423
|
+
f"app.url_schemes entry {scheme!r} is invalid; schemes must start with a letter "
|
|
424
|
+
"and contain only letters, digits, '+', '-', and '.'."
|
|
425
|
+
)
|
|
426
|
+
|
|
409
427
|
unknown = _permissions.unknown_capabilities(self.permissions.keys())
|
|
410
428
|
if unknown:
|
|
411
429
|
known = ", ".join(sorted(_permissions.CAPABILITIES))
|
|
@@ -593,6 +611,8 @@ build = 1
|
|
|
593
611
|
python_version = "{python_version}"
|
|
594
612
|
orientation = "portrait" # portrait | landscape | all
|
|
595
613
|
entry_point = "app/main.py"
|
|
614
|
+
# Custom URL schemes for deep links (e.g. "{name}" opens {name}://...).
|
|
615
|
+
# url_schemes = ["{name}"]
|
|
596
616
|
|
|
597
617
|
# Declare the device capabilities your app needs. A string becomes the
|
|
598
618
|
# iOS permission prompt text; `true` uses a sensible default.
|
|
@@ -626,7 +646,9 @@ export_method = "development" # development | ad-hoc | app-store | enterprise
|
|
|
626
646
|
[android]
|
|
627
647
|
min_sdk = 24
|
|
628
648
|
target_sdk = 34
|
|
629
|
-
|
|
649
|
+
# arm64 devices + x86_64 emulators (the default). Add "armeabi-v7a" or
|
|
650
|
+
# "x86" only if you must support 32-bit hardware; each ABI adds ~30 MB.
|
|
651
|
+
# abi_filters = ["arm64-v8a", "x86_64"]
|
|
630
652
|
|
|
631
653
|
[android.signing]
|
|
632
654
|
# keystore = "release.keystore"
|