| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -57,7 +57,9 @@ jobs: | |||
| 57 | 57 | # The Swift half of the rendering core: decoders, component managers, | |
| 58 | 58 | # gestures, and module promises run under XCTest on a simulator. | |
| 59 | 59 | native-ios: | |
| 60 | - runs-on: macos-14 | ||
| 60 | + runs-on: macos-15 | ||
| 61 | + env: | ||
| 62 | + DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer | ||
| 61 | 63 | steps: | |
| 62 | 64 | - name: Checkout | |
| 63 | 65 | uses: actions/checkout@v4 | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -134,6 +134,8 @@ jobs: | |||
| 134 | 134 | # alerts present twice, pickers reopen after selecting. This suite | |
| 135 | 135 | # ran green on the macos-15 image for months. | |
| 136 | 136 | runs-on: macos-15 | |
| 137 | + env: | ||
| 138 | + DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer | ||
| 137 | 139 | # Budget for the worst case: ~8 min build/boot plus two full suite | |
| 138 | 140 | # attempts (run-e2e.sh retries the whole suite once after a driver | |
| 139 | 141 | # flake). The test step gets its own, shorter timeout so a hung | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -25,7 +25,7 @@ A surface commit has this shape: | |||
| 25 | 25 | ||
| 26 | 26 | ```json | |
| 27 | 27 | { | |
| 28 | - "version": 2, | ||
| 28 | + "version": 3, | ||
| 29 | 29 | "application": "unique-application-id", | |
| 30 | 30 | "surface": 1, | |
| 31 | 31 | "revision": 1, | |
@@ -40,7 +40,7 @@ A surface commit has this shape: | |||
| 40 | 40 | | Operation | Fields | | |
| 41 | 41 | | --- | --- | | |
| 42 | 42 | | `c` | tag, component type, props | | |
| 43 | - | `u` | tag, changed props; `null` removes a prop | | ||
| 43 | + | `u` | tag, changed props, removed property names; `null` remains a value | | ||
| 44 | 44 | | `i` | parent tag, child tag, insertion index | | |
| 45 | 45 | | `d` | tag; children must already be destroyed | | |
| 46 | 46 | | `f` | tag, x, y, width, height | | |
@@ -74,10 +74,15 @@ provenance. Python tests build a host binding, iOS and Android compile the same | |||
| 74 | 74 | core, and the browser preview uses the corresponding Yoga WebAssembly package. | |
| 75 | 75 | ||
| 76 | 76 | The application sends styles as props. Native leaf managers measure text, | |
| 77 | - controls, and images next to their widgets. One `Layout.compute` request returns | ||
| 78 | - a batch of changed frames with `application`, `surface`, and `revision`. | ||
| 77 | + controls, and images next to their widgets. When the viewport is known, a commit | ||
| 78 | + includes a `layout` object with `roots`, `width`, and `height`. Its acknowledgment | ||
| 79 | + includes the resulting frames, so mutation and required layout share one bridge | ||
| 80 | + crossing. `Layout.compute` remains available for viewport changes without | ||
| 81 | + mutations. Both return frames with `application`, `surface`, and `revision`. | ||
| 79 | 82 | Geometry from another surface or revision is ignored. Drawing-only prop updates | |
| 80 | - skip the request entirely. Python doesn't make one measurement RPC per leaf. | ||
| 83 | + skip a separate request. Yoga calculations reuse unchanged constraints, and | ||
| 84 | + frame collection visits only subtrees with new layout. Python doesn't make one | ||
| 85 | + measurement RPC per leaf. | ||
| 81 | 86 | ||
| 82 | 87 | Commit and required layout acknowledgments precede ref and effect publication. | |
| 83 | 88 | Native navigation animations and later platform layout changes may continue after | |
@@ -91,7 +96,12 @@ UIKit collection views and Android recycler views own physical cells; a bounded | |||
| 91 | 96 | window of ordinary keyed Python row components supplies their contents. Headless | |
| 92 | 97 | tests simulate the same viewport and row requests. Dataset revisions change when | |
| 93 | 98 | data or rendering inputs change; per-item revisions avoid reloading unchanged | |
| 94 | - cells. Global and section headers don't change public item indices. Row instances | ||
| 99 | + cells. Dataset indexing is memoized separately from viewport changes. Stable | ||
| 100 | + sequence identity, callbacks, and `data_revision` reuse that index even when a | ||
| 101 | + parent rerenders. Replace an edited sequence or increment `data_revision` after | ||
| 102 | + in-place edits. Native prefetch requests warm the bounded row window. Native | ||
| 103 | + containers preserve the first visible key and its offset across dataset changes; | ||
| 104 | + iOS also preserves it as measured row heights replace estimates. Global and section headers don't change public item indices. Row instances | ||
| 95 | 105 | can unmount outside the window, so persistent item state belongs in app data. | |
| 96 | 106 | ||
| 97 | 107 | ## Modules, contracts, and animations | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -30,7 +30,7 @@ the render loop. | |||
| 30 | 30 | | Op | Meaning | | |
| 31 | 31 | |---|---| | |
| 32 | 32 | | `CreateOp(tag, type_name, props)` | Create a native view for `tag`. Props are already *clean*: callables have been routed to the event registry. | | |
| 33 | - | `UpdateOp(tag, changed_props)` | Apply only the props that changed (removed props arrive as `None`). | | ||
| 33 | + | `UpdateOp(tag, changed_props)` | Apply only the props that changed (`UNSET` removes a prop; `None` is explicit null). | | ||
| 34 | 34 | | `InsertOp(parent_tag, child_tag, index)` | Place the child at `index` (move-aware: an attached child is repositioned, not duplicated). | | |
| 35 | 35 | | `DestroyOp(tag)` | Release the native view (detaching it from its parent) and drop the tag record. | | |
| 36 | 36 | | `SetFrameOp(tag, x, y, w, h)` | Apply a frame from headless layout or an explicit frame operation. Bridge renderers compute ordinary layout beside their widgets. | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -26,7 +26,7 @@ tree: | |||
| 26 | 26 | ||
| 27 | 27 | - If their `type` matches, **update**: emit an | |
| 28 | 28 | [`UpdateOp`][pythonnative.mutations.UpdateOp] carrying only the | |
| 29 | - props that changed (removed props arrive as `None`). | ||
| 29 | + props that changed (removed props arrive as `UNSET`; `None` is explicit null). | ||
| 30 | 30 | - If their `type` differs, **replace**: emit destroy ops for the old | |
| 31 | 31 | subtree, create ops for the new one, and recurse into its children. | |
| 32 | 32 | - For container elements, match children by `key` first and by | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -30,13 +30,30 @@ pn doctor ios | |||
| 30 | 30 | ``` | |
| 31 | 31 | ||
| 32 | 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 | ||
| 33 | + toolchain: Android needs JDK 17 through 23, the configured SDK (36 by | ||
| 34 | + default), NDK 28.2.13676358, and `adb`. iOS needs macOS, Xcode 26 or | ||
| 35 | + later, and `simctl`. The doctor also reports signing, lockfile, and | ||
| 36 | + privacy-manifest configuration. It | ||
| 35 | 37 | exits non-zero on anything that will block a build, so you can gate CI | |
| 36 | 38 | on it. | |
| 37 | 39 | ||
| 38 | 40 | --- | |
| 39 | 41 | ||
| 42 | + ## Frozen dependencies | ||
| 43 | + | ||
| 44 | + Release builds with Python dependencies require a current `pn.lock` covering | ||
| 45 | + all build targets. Resolve and review the lock before building: | ||
| 46 | + | ||
| 47 | + ```bash | ||
| 48 | + pn deps android --lock | ||
| 49 | + pn deps ios --lock | ||
| 50 | + ``` | ||
| 51 | + | ||
| 52 | + Commit the lock with your application. Changing requirements or replacing a | ||
| 53 | + local wheel requires refreshing it. Releases reject missing or stale locks; | ||
| 54 | + development builds can still resolve unlocked requirements. Native extension | ||
| 55 | + manifests pin SwiftPM and Maven dependencies to exact versions. | ||
| 56 | + | ||
| 40 | 57 | ## Android | |
| 41 | 58 | ||
| 42 | 59 | `pn build android` runs `assembleRelease` and `bundleRelease`, producing | |
@@ -48,6 +65,21 @@ build/android/android_template/app/build/outputs/apk/release/app-release.apk | |||
| 48 | 65 | build/android/android_template/app/build/outputs/bundle/release/app-release.aab | |
| 49 | 66 | ``` | |
| 50 | 67 | ||
| 68 | + ### Toolchain and packaged binaries | ||
| 69 | + | ||
| 70 | + The template uses compile/target SDK 36, Gradle 8.13, Android Gradle Plugin | ||
| 71 | + 8.11.1, Kotlin 2.2.21, and NDK 28.2.13676358. Release builds reject a target | ||
| 72 | + SDK below 36. These are PythonNative's supported build defaults, independent | ||
| 73 | + of store submission deadlines. | ||
| 74 | + | ||
| 75 | + After Gradle builds, PythonNative inspects the APK and AAB. Every packaged | ||
| 76 | + 64-bit ELF library must have 16 KB aligned LOAD segments, including Python | ||
| 77 | + extensions inside Chaquopy assets. Uncompressed APK libraries must also be | ||
| 78 | + ZIP aligned, and the AAB must request compatible generated APK alignment. | ||
| 79 | + An error names the offending dependency. Rebuild or replace that dependency; | ||
| 80 | + upgrading your app's NDK can't repair a prebuilt wheel. See Android's | ||
| 81 | + [16 KB page-size guidance](https://developer.android.com/guide/practices/page-sizes). | ||
| 82 | + | ||
| 51 | 83 | ### Signing | |
| 52 | 84 | ||
| 53 | 85 | Without signing configured, Gradle emits an **unsigned** release APK | |
@@ -110,6 +142,24 @@ The upload uses the App Store Connect credentials Xcode has stored; in | |||
| 110 | 142 | CI, provide an ASC API key through Xcode's standard mechanisms or | |
| 111 | 143 | upload the exported `.ipa` with `xcrun altool`/Transporter instead. | |
| 112 | 144 | ||
| 145 | + ### Privacy manifests | ||
| 146 | + | ||
| 147 | + The template includes an app `PrivacyInfo.xcprivacy`. Supply your app's own | ||
| 148 | + manifest with a path relative to the project: | ||
| 149 | + | ||
| 150 | + ```toml | ||
| 151 | + [ios] | ||
| 152 | + privacy_manifest = "assets/PrivacyInfo.xcprivacy" | ||
| 153 | + ``` | ||
| 154 | + | ||
| 155 | + PythonNativeKit ships its own resource-bundle manifest for preferences and | ||
| 156 | + cache-file timestamps. Archive validation checks that the app and runtime | ||
| 157 | + manifests are present and structurally valid before export or upload. It also | ||
| 158 | + parses manifests supplied by bundled dependencies. This check doesn't infer | ||
| 159 | + your application's data collection, tracking, or required-reason API usage. | ||
| 160 | + Declare those from your actual behavior using Apple's | ||
| 161 | + [privacy-manifest documentation](https://developer.apple.com/documentation/bundleresources/privacy-manifest-files). | ||
| 162 | + | ||
| 113 | 163 | ### Bytecode-only bundles | |
| 114 | 164 | ||
| 115 | 165 | Release builds byte-compile your `app/` sources and every bundled | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -38,6 +38,7 @@ packages = ["humanize", "httpx", "numpy"] | |||
| 38 | 38 | [ios] | |
| 39 | 39 | deployment_target = "13.0" | |
| 40 | 40 | development_team = "ABCDE12345" | |
| 41 | + privacy_manifest = "PrivacyInfo.xcprivacy" # optional app declarations | ||
| 41 | 42 | # bundle_id = "com.example.myapp" | |
| 42 | 43 | ||
| 43 | 44 | [ios.signing] | |
@@ -46,7 +47,7 @@ export_method = "development" # development | ad-hoc | app-store | enterprise | |||
| 46 | 47 | ||
| 47 | 48 | [android] | |
| 48 | 49 | min_sdk = 24 | |
| 49 | - target_sdk = 34 | ||
| 50 | + target_sdk = 36 | ||
| 50 | 51 | # abi_filters = ["arm64-v8a", "x86_64"] # the default; add 32-bit ABIs if needed | |
| 51 | 52 | ||
| 52 | 53 | [android.signing] | |
@@ -200,6 +201,7 @@ generated registration file calls every plugin's `register`. See | |||
| 200 | 201 | [ios] | |
| 201 | 202 | deployment_target = "13.0" | |
| 202 | 203 | development_team = "ABCDE12345" | |
| 204 | + privacy_manifest = "PrivacyInfo.xcprivacy" # optional app declarations | ||
| 203 | 205 | ||
| 204 | 206 | [ios.signing] | |
| 205 | 207 | export_method = "app-store" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -35,6 +35,19 @@ def Big(): | |||
| 35 | 35 | The list never holds 10,000 native views; only the window around the | |
| 36 | 36 | viewport ever exists. | |
| 37 | 37 | ||
| 38 | + ## Data snapshots | ||
| 39 | + | ||
| 40 | + Lists accept a `Sequence` and cache keys and row metadata separately from the | ||
| 41 | + scrolling window. Keep `render_item`, `key_extractor`, and height callbacks | ||
| 42 | + stable when the parent rerenders, using module-level functions or | ||
| 43 | + `use_callback`. Scrolling then does work proportional to the mounted window. | ||
| 44 | + | ||
| 45 | + Prefer replacing data with a new sequence. If you mutate a sequence in place, | ||
| 46 | + increment `data_revision` when passing it to `FlatList` or `SectionList`. | ||
| 47 | + The revision invalidates the cached snapshot. Native prefetch requests mount | ||
| 48 | + nearby rows, and keyed anchoring preserves the visible row as measurements or | ||
| 49 | + data updates change extents above it. | ||
| 50 | + | ||
| 38 | 51 | ## Row heights | |
| 39 | 52 | ||
| 40 | 53 | Three ways to tell the list how tall rows are, in order of preference: | |
| Back | FazBrowse Home | New Git URL |
0 commit comments