FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(cli)!: overhaul ios embedding, devices, and release builds (#18) · pythonnative/pythonnative@c200d2a · GitHub

Commit c200d2a

Browse files
authored
feat(cli)!: overhaul ios embedding, devices, and release builds (#18)
1 parent 25f5985 commit c200d2a

52 files changed

Lines changed: 2675 additions & 1069 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/release.yml‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,31 @@ jobs:
3030
- name: Install build tools
3131
run: python -m pip install -U pip build
3232

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.
3342
- name: Python Semantic Release
3443
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
3858
3959
- name: Build package
4060
if: steps.release.outputs.released == 'true'

‎docs/api/cli.md‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@ the documented behavior never drifts from the code.
1515
with Fast Refresh, the fastest way to iterate on UI. Flags:
1616
`--width`, `--height`, `--title`, `--no-hot-reload`. See the
1717
[Desktop preview guide](../guides/desktop-preview.md).
18+
- `pn devices [android|ios]`: list connected devices, emulators, and
19+
simulators with the identifiers `--device` accepts.
1820
- `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.
2025
- `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
2228
[Building for release](../guides/building-for-release.md).
2329
- `pn app-id android|ios`: print the resolved application id (Android)
2430
or bundle id (iOS), handy for scripts and CI.

‎docs/concepts/architecture.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ See [Mental model](mental-model.md) for a wider comparison table.
335335

336336
## iOS flow (rubicon-objc)
337337

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
339339
[`create_screen`][pythonnative.create_screen] internally with the
340340
current `UIViewController` pointer.
341341
- The reconciler creates UIKit views and attaches them to the

‎docs/guides/android.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ directly; just export your component and configure the entry point in
2323
pn run android
2424
```
2525

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+
2633
Or to only prepare the project without building:
2734

2835
```bash
@@ -46,6 +53,7 @@ the template use, so you get Python output without the usual logcat noise:
4653

4754
Press Ctrl+C to stop streaming. Pass `--no-logs` to skip log streaming
4855
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`.
4957

5058
If you need unfiltered output, run `adb logcat` yourself in another terminal.
5159

‎docs/guides/building-for-release.md‎

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,35 @@ the release build so the resulting APK/AAB are signed and upload-ready.
8989

9090
## iOS
9191

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:
9596

9697
```
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
99100
```
100101

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.
105121

106122
### Signing
107123

@@ -127,13 +143,13 @@ iOS has no system Python, so PythonNative embeds CPython from the
127143
[Python-Apple-support](https://github.com/beeware/Python-Apple-support)
128144
project. On the first iOS build, `pn` downloads the pinned, checksum-
129145
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`.
134149

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.
137153

138154
---
139155

‎docs/guides/configuration.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ build = 1 # integer build number
1717
python_version = "3.11" # embedded CPython version
1818
orientation = "portrait" # portrait | landscape | all
1919
entry_point = "app/main.py" # module whose `App` is mounted
20+
url_schemes = ["myapp"] # deep-link schemes the app handles
2021

2122
[permissions]
2223
camera = "Scan receipts with your camera."
@@ -42,7 +43,7 @@ export_method = "development" # development | ad-hoc | app-store | enterprise
4243
[android]
4344
min_sdk = 24
4445
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
4647

4748
[android.signing]
4849
# keystore = "release.keystore"
@@ -66,8 +67,9 @@ Core identity, shared by both platforms.
6667
| `display_name` | string | `name` | The label shown under the icon on the home screen. |
6768
| `version` | string | `"1.0.0"` | Marketing version (`CFBundleShortVersionString` / `versionName`). One to four dot-separated numbers. |
6869
| `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. |
7071
| `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`. |
7173
| `entry_point` | string | `"app/main.py"` | The module whose top-level `App` component is mounted. `app/main.py` → imported as `app.main`. |
7274

7375
!!! tip "Per-platform id overrides"
@@ -180,7 +182,7 @@ provisioning_profile = "My App Distribution"
180182
| `target_sdk` | integer | `34` | Target API level. Must be ≥ `min_sdk`. |
181183
| `compile_sdk` | integer | `34` | SDK level the project compiles against. |
182184
| `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). |
184186
| `permissions` | list of strings | `[]` | Extra **raw** Android permission strings appended to the ones derived from `[permissions]`. |
185187

186188
### `[android.signing]`

‎docs/guides/ios.md‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ Basic steps to build and run an iOS project generated by `pn`.
44

55
## What gets generated
66

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:
88

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.
1014

1115
## Component model
1216

@@ -23,6 +27,16 @@ directly; just export your component and configure the entry point in
2327
pn run ios
2428
```
2529

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+
2640
Or prepare without building:
2741

2842
```bash
@@ -47,6 +61,15 @@ interacting with the app from Xcode or Console.app, or when running in
4761
a non-interactive context like CI (see the `e2e.yml` workflow for an
4862
example).
4963

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+
5073
### Requirements
5174

5275
- **Xcode 14 or newer.** `simctl launch --console-pty` was added in

‎docs/guides/native-modules.md‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,18 @@ async def setup_reminder():
166166
`request_permission()` is required on iOS and on Android 13+. On
167167
older Android the call returns `True` without prompting.
168168

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+
169181
## Clipboard
170182

171183
[`Clipboard`][pythonnative.Clipboard] reads and writes the system
@@ -193,7 +205,7 @@ async def share_link():
193205
## Linking
194206

195207
[`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.
197209

198210
```python
199211
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
203215
pn.Linking.open_settings() # this app's entry in Settings
204216
```
205217

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+
206234
## Permissions (runtime)
207235

208236
[`Permissions`][pythonnative.Permissions] normalizes the iOS/Android

‎docs/guides/permissions.md‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ permissions, and (where relevant) iOS background modes.
5353
| `face_id` | Biometric auth | `NSFaceIDUsageDescription` | `USE_BIOMETRIC` |
5454
| `bluetooth` | Nearby Bluetooth devices | `NSBluetoothAlwaysUsageDescription` | `BLUETOOTH_CONNECT`, `BLUETOOTH_SCAN` |
5555
| `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` |
5758
| `vibration` | Haptics / vibration | – | `VIBRATE` |
5859
| `background_audio` | Play audio in background | – (adds `audio` background mode) | `FOREGROUND_SERVICE` |
5960
| `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.
6465

6566
### Background modes
6667

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.)
7081

7182
## Always-on permissions
7283

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL