| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -30,11 +30,31 @@ jobs: | |||
| 30 | 30 | - name: Install build tools | |
| 31 | 31 | run: python -m pip install -U pip build | |
| 32 | 32 | ||
| 33 | + # TEMPORARY: inlined replacement for the | ||
| 34 | + # python-semantic-release/python-semantic-release@v9 action. That | ||
| 35 | + # action builds its Docker image at job time with GitPython | ||
| 36 | + # unpinned, and GitPython 3.1.60 (2026-08-25) removed | ||
| 37 | + # Actor.name_email_regex, which crashes every semantic-release | ||
| 38 | + # config load (python-semantic-release issue #1475). This step | ||
| 39 | + # mirrors the action: same git identity, same `version` command, | ||
| 40 | + # and a `released` output for the steps below. Restore the action | ||
| 41 | + # once the fix (python-semantic-release PR #1477) is released. | ||
| 33 | 42 | - name: Python Semantic Release | |
| 34 | 43 | id: release | |
| 35 | - uses: python-semantic-release/python-semantic-release@v9 | ||
| 36 | - with: | ||
| 37 | - github_token: ${{ secrets.GITHUB_TOKEN }} | ||
| 44 | + env: | ||
| 45 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| 46 | + run: | | ||
| 47 | + git config user.name "github-actions" | ||
| 48 | + git config user.email "actions@github.com" | ||
| 49 | + pip install "python-semantic-release==9.21.2" "gitpython<3.1.60" | ||
| 50 | + tags_before=$(git tag | wc -l) | ||
| 51 | + semantic-release -v version | ||
| 52 | + tags_after=$(git tag | wc -l) | ||
| 53 | + if [ "$tags_after" -gt "$tags_before" ]; then | ||
| 54 | + echo "released=true" >> "$GITHUB_OUTPUT" | ||
| 55 | + else | ||
| 56 | + echo "released=false" >> "$GITHUB_OUTPUT" | ||
| 57 | + fi | ||
| 38 | 58 | ||
| 39 | 59 | - name: Build package | |
| 40 | 60 | if: steps.release.outputs.released == 'true' | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,10 +15,16 @@ the documented behavior never drifts from the code. | |||
| 15 | 15 | with Fast Refresh, the fastest way to iterate on UI. Flags: | |
| 16 | 16 | `--width`, `--height`, `--title`, `--no-hot-reload`. See the | |
| 17 | 17 | [Desktop preview guide](../guides/desktop-preview.md). | |
| 18 | + - `pn devices [android|ios]`: list connected devices, emulators, and | ||
| 19 | + simulators with the identifiers `--device` accepts. | ||
| 18 | 20 | - `pn run android|ios`: build and run on a connected device or | |
| 19 | - simulator. Flags: `--prepare-only`, `--hot-reload`, `--no-logs`. | ||
| 21 | + simulator. Flags: `--device` (target a specific device by identifier | ||
| 22 | + or name), `--prepare-only`, `--hot-reload`, `--no-logs`. | ||
| 23 | + - `pn logs android|ios`: stream logs from the running app without | ||
| 24 | + rebuilding. | ||
| 20 | 25 | - `pn build android|ios`: build distributable artifacts (release by | |
| 21 | - default). Flag: `--debug` for the debug variant. See | ||
| 26 | + default). Flags: `--debug` for the debug variant, `--upload` to send | ||
| 27 | + an iOS release build to App Store Connect. See | ||
| 22 | 28 | [Building for release](../guides/building-for-release.md). | |
| 23 | 29 | - `pn app-id android|ios`: print the resolved application id (Android) | |
| 24 | 30 | or bundle id (iOS), handy for scripts and CI. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -335,7 +335,7 @@ See [Mental model](mental-model.md) for a wider comparison table. | |||
| 335 | 335 | ||
| 336 | 336 | ## iOS flow (rubicon-objc) | |
| 337 | 337 | ||
| 338 | - - The iOS template (Swift plus PythonKit) boots Python and calls | ||
| 338 | + - The iOS template (Swift with CPython embedded through the C API) boots Python and calls | ||
| 339 | 339 | [`create_screen`][pythonnative.create_screen] internally with the | |
| 340 | 340 | current `UIViewController` pointer. | |
| 341 | 341 | - The reconciler creates UIKit views and attaches them to the | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -23,6 +23,13 @@ directly; just export your component and configure the entry point in | |||
| 23 | 23 | pn run android | |
| 24 | 24 | ``` | |
| 25 | 25 | ||
| 26 | + With several devices or emulators connected, list them and target one: | ||
| 27 | + | ||
| 28 | + ```bash | ||
| 29 | + pn devices android | ||
| 30 | + pn run android --device emulator-5554 | ||
| 31 | + ``` | ||
| 32 | + | ||
| 26 | 33 | Or to only prepare the project without building: | |
| 27 | 34 | ||
| 28 | 35 | ```bash | |
@@ -46,6 +53,7 @@ the template use, so you get Python output without the usual logcat noise: | |||
| 46 | 53 | ||
| 47 | 54 | Press Ctrl+C to stop streaming. Pass `--no-logs` to skip log streaming | |
| 48 | 55 | entirely (useful in CI or when you'd rather watch logs from Android Studio). | |
| 56 | + To re-attach later without rebuilding, run `pn logs android`. | ||
| 49 | 57 | ||
| 50 | 58 | If you need unfiltered output, run `adb logcat` yourself in another terminal. | |
| 51 | 59 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -89,19 +89,35 @@ the release build so the resulting APK/AAB are signed and upload-ready. | |||
| 89 | 89 | ||
| 90 | 90 | ## iOS | |
| 91 | 91 | ||
| 92 | - `pn build ios` archives the app for a device with `xcodebuild archive`, | ||
| 93 | - embeds the device CPython slice into the archive, and exports a signed | ||
| 94 | - `.ipa` with `xcodebuild -exportArchive`. Outputs: | ||
| 92 | + `pn build ios` archives the app with `xcodebuild archive` and exports a | ||
| 93 | + signed `.ipa` with `xcodebuild -exportArchive`. The Python framework, | ||
| 94 | + standard library, and app code are installed and signed during the | ||
| 95 | + Xcode build itself, so the archive needs no post-processing. Outputs: | ||
| 95 | 96 | ||
| 96 | 97 | ``` | |
| 97 | - build/ios/ios_template/build/export/*.ipa | ||
| 98 | - build/ios/ios_template/build/ios_template.xcarchive | ||
| 98 | + build/ios/export/*.ipa | ||
| 99 | + build/ios/ios_template.xcarchive | ||
| 99 | 100 | ``` | |
| 100 | 101 | ||
| 101 | - !!! warning "Experimental" | ||
| 102 | - The device archive/export path embeds and re-signs the embedded | ||
| 103 | - Python framework. Treat it as experimental and verify on a real | ||
| 104 | - device before relying on it for store submission. | ||
| 102 | + To send the build straight to App Store Connect instead of exporting a | ||
| 103 | + local `.ipa`, set `export_method = "app-store"` and pass `--upload`: | ||
| 104 | + | ||
| 105 | + ```bash | ||
| 106 | + pn build ios --upload | ||
| 107 | + ``` | ||
| 108 | + | ||
| 109 | + The upload uses the App Store Connect credentials Xcode has stored; in | ||
| 110 | + CI, provide an ASC API key through Xcode's standard mechanisms or | ||
| 111 | + upload the exported `.ipa` with `xcrun altool`/Transporter instead. | ||
| 112 | + | ||
| 113 | + ### Bytecode-only bundles | ||
| 114 | + | ||
| 115 | + Release builds byte-compile your `app/` sources and every bundled | ||
| 116 | + package to `.pyc` and drop the `.py` files, which shrinks the bundle | ||
| 117 | + and avoids shipping plain-text source. Because bytecode is | ||
| 118 | + version-specific, this requires the Python running `pn` to match | ||
| 119 | + `app.python_version`; otherwise `pn` prints a notice and ships `.py` | ||
| 120 | + sources instead. | ||
| 105 | 121 | ||
| 106 | 122 | ### Signing | |
| 107 | 123 | ||
@@ -127,13 +143,13 @@ iOS has no system Python, so PythonNative embeds CPython from the | |||
| 127 | 143 | [Python-Apple-support](https://github.com/beeware/Python-Apple-support) | |
| 128 | 144 | project. On the first iOS build, `pn` downloads the pinned, checksum- | |
| 129 | 145 | verified runtime for your `app.python_version` and caches it under | |
| 130 | - `build/ios/ios_runtime/`. The correct slice (Simulator vs. device) is | ||
| 131 | - embedded into the app bundle along with the standard library, your | ||
| 132 | - `app/` sources, the bundled `pythonnative` package, and any pure-Python | ||
| 133 | - `[requirements].packages`. | ||
| 146 | + `build/ios/ios_runtime/`. The Xcode build links `Python.xcframework`, | ||
| 147 | + installs the standard library, and bundles your `app/` sources, the | ||
| 148 | + `pythonnative` package, and any pure-Python `[requirements].packages`. | ||
| 134 | 149 | ||
| 135 | - iOS currently ships a verified runtime for **Python 3.11**; set | ||
| 136 | - `python_version = "3.11"` in `[app]` for device builds. | ||
| 150 | + Pinned, verified runtimes exist for **Python 3.10, 3.11, and 3.12**; | ||
| 151 | + set `python_version` in `[app]` accordingly. Unpinned versions are | ||
| 152 | + rejected rather than fetched unverified. | ||
| 137 | 153 | ||
| 138 | 154 | --- | |
| 139 | 155 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -17,6 +17,7 @@ build = 1 # integer build number | |||
| 17 | 17 | python_version = "3.11" # embedded CPython version | |
| 18 | 18 | orientation = "portrait" # portrait | landscape | all | |
| 19 | 19 | entry_point = "app/main.py" # module whose `App` is mounted | |
| 20 | + url_schemes = ["myapp"] # deep-link schemes the app handles | ||
| 20 | 21 | ||
| 21 | 22 | [permissions] | |
| 22 | 23 | camera = "Scan receipts with your camera." | |
@@ -42,7 +43,7 @@ export_method = "development" # development | ad-hoc | app-store | enterprise | |||
| 42 | 43 | [android] | |
| 43 | 44 | min_sdk = 24 | |
| 44 | 45 | target_sdk = 34 | |
| 45 | - abi_filters = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"] | ||
| 46 | + # abi_filters = ["arm64-v8a", "x86_64"] # the default; add 32-bit ABIs if needed | ||
| 46 | 47 | ||
| 47 | 48 | [android.signing] | |
| 48 | 49 | # keystore = "release.keystore" | |
@@ -66,8 +67,9 @@ Core identity, shared by both platforms. | |||
| 66 | 67 | | `display_name` | string | `name` | The label shown under the icon on the home screen. | | |
| 67 | 68 | | `version` | string | `"1.0.0"` | Marketing version (`CFBundleShortVersionString` / `versionName`). One to four dot-separated numbers. | | |
| 68 | 69 | | `build` | integer | `1` | Build number (`CFBundleVersion` / `versionCode`). Must be a positive integer; bump it for every store upload. | | |
| 69 | - | `python_version` | string | `"3.11"` | Embedded CPython version. One of `3.10`, `3.11`, `3.12`. iOS currently ships a pinned, verified build for `3.11`. | | ||
| 70 | + | `python_version` | string | `"3.11"` | Embedded CPython version. One of `3.10`, `3.11`, `3.12`; every listed version has a pinned, checksum-verified iOS runtime. | | ||
| 70 | 71 | | `orientation` | string | `"portrait"` | `portrait`, `landscape`, or `all`. | | |
| 72 | + | `url_schemes` | list of strings | `[]` | Custom deep-link URL schemes (e.g. `["myapp"]` handles `myapp://…`). Wired into `CFBundleURLTypes` on iOS and a `VIEW` intent filter on Android; inbound URLs reach `pn.Linking`. | | ||
| 71 | 73 | | `entry_point` | string | `"app/main.py"` | The module whose top-level `App` component is mounted. `app/main.py` → imported as `app.main`. | | |
| 72 | 74 | ||
| 73 | 75 | !!! tip "Per-platform id overrides" | |
@@ -180,7 +182,7 @@ provisioning_profile = "My App Distribution" | |||
| 180 | 182 | | `target_sdk` | integer | `34` | Target API level. Must be ≥ `min_sdk`. | | |
| 181 | 183 | | `compile_sdk` | integer | `34` | SDK level the project compiles against. | | |
| 182 | 184 | | `application_id` | string | `app.id` | Override the Android application id (and package). | | |
| 183 | - | `abi_filters` | list of strings | all four | Native ABIs to include: `armeabi-v7a`, `arm64-v8a`, `x86`, `x86_64`. Trim to shrink the APK. | | ||
| 185 | + | `abi_filters` | list of strings | `["arm64-v8a", "x86_64"]` | Native ABIs to include. The default covers 64-bit devices and emulators; add `armeabi-v7a` or `x86` only if you must support 32-bit hardware (each ABI adds roughly 30 MB). | | ||
| 184 | 186 | | `permissions` | list of strings | `[]` | Extra **raw** Android permission strings appended to the ones derived from `[permissions]`. | | |
| 185 | 187 | ||
| 186 | 188 | ### `[android.signing]` | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,9 +4,13 @@ Basic steps to build and run an iOS project generated by `pn`. | |||
| 4 | 4 | ||
| 5 | 5 | ## What gets generated | |
| 6 | 6 | ||
| 7 | - `pn run ios` unpacks the bundled iOS template (Swift + PythonKit, with optional Rubicon-ObjC) into `build/ios/ios_template` and copies your `app/` under `build/ios/app/` for later integration steps. The template zip is bundled with the package, so no network is required to scaffold. | ||
| 7 | + `pn run ios` unpacks the bundled iOS template (a Swift Xcode project) into `build/ios/ios_template` and stages three things at the project root: | ||
| 8 | 8 | ||
| 9 | - The default `ViewController.swift` initializes PythonKit, prints the Python version, and attempts to import `rubicon.objc` if present. | ||
| 9 | + - `Python.xcframework`: the embedded CPython runtime for your `app.python_version` (downloaded once, checksum-verified, and cached under `build/ios/ios_runtime/`). | ||
| 10 | + - `app/`: your project's Python sources. | ||
| 11 | + - `app_packages/`: the `pythonnative` package, `rubicon-objc`, and any `[requirements].packages`. | ||
| 12 | + | ||
| 13 | + The Xcode project links the framework at build time and embeds CPython through its C API (`PythonRuntime.swift`); an "Install Python runtime" build phase installs the standard library and converts binary modules into signed frameworks. The built `.app` is therefore complete as soon as `xcodebuild` finishes, with no post-build patching. If Python fails to start or your root component fails to mount, the app shows a full-screen error report with the Python traceback instead of failing silently. | ||
| 10 | 14 | ||
| 11 | 15 | ## Component model | |
| 12 | 16 | ||
@@ -23,6 +27,16 @@ directly; just export your component and configure the entry point in | |||
| 23 | 27 | pn run ios | |
| 24 | 28 | ``` | |
| 25 | 29 | ||
| 30 | + To target a specific Simulator, or a physical device, list the available targets and pass one to `--device`: | ||
| 31 | + | ||
| 32 | + ```bash | ||
| 33 | + pn devices ios | ||
| 34 | + pn run ios --device "iPhone 15 Pro" | ||
| 35 | + pn run ios --device 00008120-001234567890ABCD | ||
| 36 | + ``` | ||
| 37 | + | ||
| 38 | + Running on a physical device requires `[ios].development_team` in `pythonnative.toml` (for code signing), a paired device, and Developer Mode enabled on the device (Settings > Privacy & Security > Developer Mode). | ||
| 39 | + | ||
| 26 | 40 | Or prepare without building: | |
| 27 | 41 | ||
| 28 | 42 | ```bash | |
@@ -47,6 +61,15 @@ interacting with the app from Xcode or Console.app, or when running in | |||
| 47 | 61 | a non-interactive context like CI (see the `e2e.yml` workflow for an | |
| 48 | 62 | example). | |
| 49 | 63 | ||
| 64 | + To re-attach to a running app later without rebuilding, use: | ||
| 65 | + | ||
| 66 | + ```bash | ||
| 67 | + pn logs ios | ||
| 68 | + ``` | ||
| 69 | + | ||
| 70 | + For a physical device, use Console.app or Xcode's Devices and | ||
| 71 | + Simulators window; `simctl` console attach is Simulator-only. | ||
| 72 | + | ||
| 50 | 73 | ### Requirements | |
| 51 | 74 | ||
| 52 | 75 | - **Xcode 14 or newer.** `simctl launch --console-pty` was added in | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -166,6 +166,18 @@ async def setup_reminder(): | |||
| 166 | 166 | `request_permission()` is required on iOS and on Android 13+. On | |
| 167 | 167 | older Android the call returns `True` without prompting. | |
| 168 | 168 | ||
| 169 | + For server-sent (remote) pushes, declare the `remote_notifications` | ||
| 170 | + capability in `pythonnative.toml`, then register with APNs: | ||
| 171 | + | ||
| 172 | + ```python | ||
| 173 | + token = await pn.Notifications.get_device_token() # APNs hex token | ||
| 174 | + ``` | ||
| 175 | + | ||
| 176 | + Your server passes the token to APNs to address this install. The | ||
| 177 | + simulator has no APNs connection, so test on a real device. Android | ||
| 178 | + remote push requires Firebase Cloud Messaging, which the built-in | ||
| 179 | + module doesn't wire up; `get_device_token()` returns `None` there. | ||
| 180 | + | ||
| 169 | 181 | ## Clipboard | |
| 170 | 182 | ||
| 171 | 183 | [`Clipboard`][pythonnative.Clipboard] reads and writes the system | |
@@ -193,7 +205,7 @@ async def share_link(): | |||
| 193 | 205 | ## Linking | |
| 194 | 206 | ||
| 195 | 207 | [`Linking`][pythonnative.Linking] opens URLs, deep links, and the app's | |
| 196 | - Settings page, and reports the URL that launched the app. | ||
| 208 | + Settings page, and delivers inbound deep links to your app. | ||
| 197 | 209 | ||
| 198 | 210 | ```python | |
| 199 | 211 | if pn.Linking.can_open_url("tel:+15551234567"): | |
@@ -203,6 +215,22 @@ launch_url = pn.Linking.get_initial_url() # deep link the app opened with | |||
| 203 | 215 | pn.Linking.open_settings() # this app's entry in Settings | |
| 204 | 216 | ``` | |
| 205 | 217 | ||
| 218 | + To receive deep links, declare your schemes in `pythonnative.toml`: | ||
| 219 | + | ||
| 220 | + ```toml | ||
| 221 | + [app] | ||
| 222 | + url_schemes = ["myapp"] | ||
| 223 | + ``` | ||
| 224 | + | ||
| 225 | + Opening `myapp://orders/42` now launches (or foregrounds) your app on | ||
| 226 | + both platforms. The URL that cold-started the app is returned by | ||
| 227 | + `get_initial_url()`; URLs that arrive while the app is running reach | ||
| 228 | + subscribers: | ||
| 229 | + | ||
| 230 | + ```python | ||
| 231 | + unsubscribe = pn.Linking.add_listener(lambda url: navigate_to(url)) | ||
| 232 | + ``` | ||
| 233 | + | ||
| 206 | 234 | ## Permissions (runtime) | |
| 207 | 235 | ||
| 208 | 236 | [`Permissions`][pythonnative.Permissions] normalizes the iOS/Android | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -53,7 +53,8 @@ permissions, and (where relevant) iOS background modes. | |||
| 53 | 53 | | `face_id` | Biometric auth | `NSFaceIDUsageDescription` | `USE_BIOMETRIC` | | |
| 54 | 54 | | `bluetooth` | Nearby Bluetooth devices | `NSBluetoothAlwaysUsageDescription` | `BLUETOOTH_CONNECT`, `BLUETOOTH_SCAN` | | |
| 55 | 55 | | `speech_recognition` | Speech recognition (iOS) | `NSSpeechRecognitionUsageDescription` | – | | |
| 56 | - | `notifications` | Local/push notifications | – | `POST_NOTIFICATIONS` | | ||
| 56 | + | `notifications` | Show local notifications | – | `POST_NOTIFICATIONS` | | ||
| 57 | + | `remote_notifications` | Server-sent (remote) push | – (adds the `aps-environment` entitlement and `remote-notification` background mode) | `POST_NOTIFICATIONS` | | ||
| 57 | 58 | | `vibration` | Haptics / vibration | – | `VIBRATE` | | |
| 58 | 59 | | `background_audio` | Play audio in background | – (adds `audio` background mode) | `FOREGROUND_SERVICE` | | |
| 59 | 60 | | `background_fetch` | Periodic background fetch | – (adds `fetch` background mode) | – | | |
@@ -64,9 +65,19 @@ need no iOS usage string, so declaring them as `true` is enough. | |||
| 64 | 65 | ||
| 65 | 66 | ### Background modes | |
| 66 | 67 | ||
| 67 | - `location_always`, `background_audio`, and `background_fetch` also add | ||
| 68 | - the corresponding values to the iOS `UIBackgroundModes` array | ||
| 69 | - (`location`, `audio`, `fetch`) so the OS keeps your app scheduled. | ||
| 68 | + `location_always`, `background_audio`, `background_fetch`, and | ||
| 69 | + `remote_notifications` also add the corresponding values to the iOS | ||
| 70 | + `UIBackgroundModes` array (`location`, `audio`, `fetch`, | ||
| 71 | + `remote-notification`) so the OS keeps your app scheduled. | ||
| 72 | + | ||
| 73 | + ### Entitlements | ||
| 74 | + | ||
| 75 | + Some capabilities need a code-signing entitlement rather than an | ||
| 76 | + `Info.plist` key. Declaring `remote_notifications` generates an | ||
| 77 | + `.entitlements` file with `aps-environment` and wires it into the | ||
| 78 | + build, so `await pn.Notifications.get_device_token()` can register with | ||
| 79 | + APNs. (Distribution builds get the production APNs environment | ||
| 80 | + automatically when Xcode re-signs the archive for the store.) | ||
| 70 | 81 | ||
| 71 | 82 | ## Always-on permissions | |
| 72 | 83 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments