Skip to content

Commit 869e977

Browse files
authored
feat!: add pythonnative.toml config and build system (#10)
1 parent 2a153af commit 869e977

45 files changed

Lines changed: 5364 additions & 1071 deletions

Some content is hidden

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

docs/api/cli.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@ the documented behavior never drifts from the code.
77
## Subcommands
88

99
- `pn init [name]`: scaffold a new project (creates `app/`,
10-
`pythonnative.json`, `requirements.txt`, `.gitignore`).
10+
`pythonnative.toml`, `.gitignore`). Flag: `--force` to overwrite
11+
existing files. See [Configuration](../guides/configuration.md).
12+
- `pn doctor [android|ios]`: diagnose the local toolchain and validate
13+
`pythonnative.toml`. Exits non-zero when something will block a build.
1114
- `pn preview [component]`: render the app in a desktop (Tkinter) window
1215
with Fast Refresh — the fastest way to iterate on UI. Flags:
1316
`--width`, `--height`, `--title`, `--no-hot-reload`. See the
1417
[Desktop preview guide](../guides/desktop-preview.md).
1518
- `pn run android|ios`: build and run on a connected device or
1619
simulator. Flags: `--prepare-only`, `--hot-reload`, `--no-logs`.
20+
- `pn build android|ios`: build distributable artifacts (release by
21+
default). Flag: `--debug` for the debug variant. See
22+
[Building for release](../guides/building-for-release.md).
23+
- `pn app-id android|ios`: print the resolved application id (Android)
24+
or bundle id (iOS) — handy for scripts and CI.
1725
- `pn clean`: remove the local `build/` directory.
1826

1927
::: pythonnative.cli.pn

docs/concepts/components.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,8 @@ def App():
187187
The entry point [`create_screen`][pythonnative.create_screen] is called
188188
internally by native templates to bootstrap your root component. You
189189
don't call it directly: name your top-level component `App` (so the
190-
templates can find it by convention) and `pythonnative.json` points
191-
at the module that defines it.
190+
templates can find it by convention) and `app.entry_point` in
191+
`pythonnative.toml` points at the module that defines it.
192192

193193
## State and re-rendering
194194

docs/getting-started.md

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ pn init MyApp
1414
This scaffolds:
1515

1616
- `app/` with a minimal `main.py`
17-
- `pythonnative.json` project config
18-
- `requirements.txt`
17+
- `pythonnative.toml` — your project configuration: app id, version,
18+
permissions, assets, and signing. See
19+
[Configuration](guides/configuration.md).
1920
- `.gitignore`
2021

2122
A minimal `app/main.py` looks like:
@@ -66,6 +67,34 @@ Key ideas:
6667

6768
When the root `Stack.Navigator` is rendered inside the host's first screen, `navigate(...)` and `go_back()` drive the **native** navigation controller (UINavigationController on iOS, AndroidX Navigation Component on Android). Each pushed screen runs in its own reconciler host, so state on the previous screen is preserved by the platform stack.
6869

70+
## Configure your app
71+
72+
Everything about your app's *identity* — its bundle/application id,
73+
display name, version, the device permissions it requests, its icon and
74+
splash, third-party packages, and signing — lives in a single
75+
`pythonnative.toml` at the project root:
76+
77+
```toml
78+
[app]
79+
id = "com.example.myapp"
80+
name = "myapp"
81+
display_name = "My App"
82+
version = "1.0.0"
83+
build = 1
84+
85+
[permissions]
86+
camera = "Scan receipts with your camera."
87+
notifications = true
88+
89+
[assets]
90+
icon = "assets/icon.png"
91+
```
92+
93+
The build system reads this file for every command, so `pn run`,
94+
`pn build`, `pn doctor`, and `pn app-id` all stay in sync. See the full
95+
[Configuration reference](guides/configuration.md) and the
96+
[Permissions guide](guides/permissions.md).
97+
6998
## Preview on your desktop
7099

71100
The fastest way to iterate is `pn preview`, which renders your app in a
@@ -177,6 +206,36 @@ pn run android --no-logs
177206
pn run ios --no-logs
178207
```
179208

209+
## Check your toolchain
210+
211+
Before your first build, run `pn doctor` to verify the local toolchain
212+
(Java/Android SDK for Android; Xcode/Simulator and a signing team for
213+
iOS) and validate your `pythonnative.toml`:
214+
215+
```bash
216+
pn doctor # check everything
217+
pn doctor android # only Android-relevant checks
218+
pn doctor ios # only iOS-relevant checks
219+
```
220+
221+
It prints `[ok]` / `[!]` / `[x]` for each check and exits non-zero when
222+
something will block a build, so it's safe to run in CI.
223+
224+
## Build for release
225+
226+
When you're ready to ship, `pn build` produces signed, distributable
227+
artifacts:
228+
229+
```bash
230+
pn build android # release APK + AAB
231+
pn build ios # signed .ipa via xcodebuild archive/export
232+
```
233+
234+
Release builds need signing configured in `pythonnative.toml` (a
235+
keystore for Android, a development team for iOS). See
236+
[Building for release](guides/building-for-release.md) for the full
237+
walkthrough.
238+
180239
## Clean
181240

182241
Remove the build artifacts safely:

docs/guides/android.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The native Android template uses
1515
[`create_screen`][pythonnative.create_screen] internally to bootstrap your
1616
root component inside a `ScreenFragment`. You don't call `create_screen`
1717
directly; just export your component and configure the entry point in
18-
`pythonnative.json`.
18+
`pythonnative.toml` (`app.entry_point`).
1919

2020
## Run
2121

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# Building for release
2+
3+
`pn run` is for iterating on a device or simulator. When you're ready to
4+
ship, `pn build` produces standalone, distributable artifacts:
5+
6+
```bash
7+
pn build android # release APK + AAB
8+
pn build ios # signed .ipa via xcodebuild archive + export
9+
```
10+
11+
Pass `--debug` to build the debug variant instead (a debug APK on
12+
Android; a Simulator `.app` on iOS):
13+
14+
```bash
15+
pn build android --debug
16+
pn build ios --debug
17+
```
18+
19+
`pn build` reads identity, permissions, assets, and signing from
20+
[`pythonnative.toml`](configuration.md). Run [`pn doctor`](#check-first-pn-doctor)
21+
first to confirm the toolchain is ready.
22+
23+
---
24+
25+
## Check first: `pn doctor`
26+
27+
```bash
28+
pn doctor android
29+
pn doctor ios
30+
```
31+
32+
`pn doctor` validates `pythonnative.toml` and checks the platform
33+
toolchain — for Android: `adb`, a JDK, and whether release signing is
34+
configured; for iOS: macOS, Xcode, `simctl`, and a development team. It
35+
exits non-zero on anything that will block a build, so you can gate CI
36+
on it.
37+
38+
---
39+
40+
## Android
41+
42+
`pn build android` runs `assembleRelease` and `bundleRelease`, producing
43+
both an APK and a Play-ready AAB. Artifacts are reported at the end of
44+
the build and live under the staged project:
45+
46+
```
47+
build/android/android_template/app/build/outputs/apk/release/app-release.apk
48+
build/android/android_template/app/build/outputs/bundle/release/app-release.aab
49+
```
50+
51+
### Signing
52+
53+
Without signing configured, Gradle emits an **unsigned** release APK
54+
(`app-release-unsigned.apk`) — fine for inspection, not for the store.
55+
To produce signed artifacts, create a keystore once:
56+
57+
```bash
58+
keytool -genkeypair -v -keystore release.keystore \
59+
-alias myapp -keyalg RSA -keysize 2048 -validity 10000
60+
```
61+
62+
…then point `pythonnative.toml` at it:
63+
64+
```toml
65+
[android.signing]
66+
keystore = "release.keystore"
67+
key_alias = "myapp"
68+
# store_password_env / key_password_env default to
69+
# PN_ANDROID_KEYSTORE_PASSWORD / PN_ANDROID_KEY_PASSWORD
70+
```
71+
72+
Passwords are **never** stored in the config — only the names of the
73+
environment variables that hold them. Provide the secrets at build time:
74+
75+
```bash
76+
export PN_ANDROID_KEYSTORE_PASSWORD=…
77+
export PN_ANDROID_KEY_PASSWORD=…
78+
pn build android
79+
```
80+
81+
When signing is configured, `pn` injects a Gradle `signingConfig` into
82+
the release build so the resulting APK/AAB are signed and upload-ready.
83+
84+
!!! tip "Keep keystores out of git"
85+
Commit neither the keystore nor the passwords. Store the keystore as
86+
a CI secret/file and inject the passwords via environment variables.
87+
88+
---
89+
90+
## iOS
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:
95+
96+
```
97+
build/ios/ios_template/build/export/*.ipa
98+
build/ios/ios_template/build/ios_template.xcarchive
99+
```
100+
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.
105+
106+
### Signing
107+
108+
Set your Apple Developer Team ID and an export method:
109+
110+
```toml
111+
[ios]
112+
development_team = "ABCDE12345"
113+
114+
[ios.signing]
115+
export_method = "app-store" # development | ad-hoc | app-store | enterprise
116+
provisioning_profile = "My App Distribution"
117+
```
118+
119+
`development_team` drives signing during `archive`; `export_method` and
120+
the optional `provisioning_profile` are written into the
121+
`exportOptions.plist` that `xcodebuild -exportArchive` consumes. If
122+
export fails, the error points you back at `[ios.signing]`.
123+
124+
### Embedded Python runtime
125+
126+
iOS has no system Python, so PythonNative embeds CPython from the
127+
[Python-Apple-support](https://github.com/beeware/Python-Apple-support)
128+
project. On the first iOS build, `pn` downloads the pinned, checksum-
129+
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`.
134+
135+
iOS currently ships a verified runtime for **Python 3.11**; set
136+
`python_version = "3.11"` in `[app]` for device builds.
137+
138+
---
139+
140+
## App icon and splash
141+
142+
Provide a single high-resolution source image and PythonNative renders
143+
every per-platform, per-density variant at build time:
144+
145+
```toml
146+
[assets]
147+
icon = "assets/icon.png" # 1024x1024 PNG
148+
splash = "assets/splash.png"
149+
```
150+
151+
- **iOS** — a universal `AppIcon.appiconset` (Xcode resizes the rest)
152+
and a `Splash` image set referenced by the generated launch screen.
153+
- **Android**`mipmap-*` launcher icons at every density plus a round
154+
variant, and a centered icon for the Android 12+ splash screen.
155+
156+
Image processing needs [Pillow](https://python-pillow.org/), an optional
157+
dependency:
158+
159+
```bash
160+
pip install 'pythonnative[build]'
161+
```
162+
163+
If Pillow isn't installed the build still succeeds — it just keeps the
164+
template's default assets. `pn doctor` reports whether Pillow is
165+
available.
166+
167+
---
168+
169+
## Versioning
170+
171+
Two fields in `[app]` drive the store-visible version and the internal
172+
build number:
173+
174+
```toml
175+
[app]
176+
version = "1.2.0" # CFBundleShortVersionString / versionName
177+
build = 7 # CFBundleVersion / versionCode (bump every upload)
178+
```
179+
180+
Both stores reject a new upload that reuses an existing build number, so
181+
increment `build` for each submission.
182+
183+
---
184+
185+
## Continuous integration
186+
187+
A typical release job:
188+
189+
```bash
190+
pip install 'pythonnative[build]'
191+
pn doctor android # fail fast on a misconfigured runner
192+
export PN_ANDROID_KEYSTORE_PASSWORD=$KEYSTORE_PW
193+
export PN_ANDROID_KEY_PASSWORD=$KEY_PW
194+
pn build android
195+
```
196+
197+
`pn app-id android` / `pn app-id ios` print the resolved id, which is
198+
handy for downstream steps (uploaders, smoke tests) that need it without
199+
re-parsing the config.
200+
201+
---
202+
203+
## Prepare without building
204+
205+
To hand off to Android Studio or Xcode instead of building from the CLI,
206+
stage and configure the native project without compiling:
207+
208+
```bash
209+
pn run android --prepare-only
210+
pn run ios --prepare-only
211+
```
212+
213+
This writes a fully configured project (identity, permissions, icons,
214+
relocated Android package) under `build/`, which you can open and build
215+
with the native IDE — useful for debugging signing or build issues with
216+
the platform's own tooling.

0 commit comments

Comments
 (0)