Skip to content

Commit e64f23e

Browse files
committed
[docs] require iOS 15 deployment floor
Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent 1b89a05 commit e64f23e

5 files changed

Lines changed: 128 additions & 4 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# ADR-0045 — Adopt iOS and iPadOS 15 as the app deployment floor
2+
3+
- Status: **Accepted**
4+
- Date: 2026-08-27
5+
- Extends: [ADR-0044](0044-start-app-v020-beta-train-at-build-5.md)
6+
7+
## Context
8+
9+
The retained PyBLE `0.2.0 (5)` TestFlight artifact was compiled with an
10+
iOS/iPadOS deployment target of `13.0`. Transporter accepted the package far
11+
enough to report advisory `90068`: Apple announced that App Store Connect will
12+
require `MinimumOSVersion` `15.0` or later beginning in Spring 2027.
13+
14+
The existing IPA validator proved signing, entitlements, nested-code safety,
15+
and Xcode software-information integrity, but did not enforce a deployment
16+
floor. Its successful result was therefore compatible with the genuine `13.0`
17+
value encoded in the app, frameworks, resource bundles, and Mach-O load
18+
commands. Re-exporting or editing a signed archive cannot change that compiled
19+
contract safely.
20+
21+
The project can retain iOS/iPadOS 13 and 14 compatibility temporarily, or move
22+
the maintained source floor to 15 before Apple begins rejecting uploads. The
23+
maintainer chose the latter so future features and distribution artifacts use
24+
one forward-compatible default instead of deferring the migration to a release
25+
deadline.
26+
27+
## Decision
28+
29+
1. **Raise the maintained source floor to 15.0.** Every Runner Xcode build
30+
configuration MUST set `IPHONEOS_DEPLOYMENT_TARGET` to `15.0`. Generated
31+
Flutter and Swift-package configuration must inherit that floor rather than
32+
introducing another authored version literal.
33+
34+
2. **Make the source setting executable.** A repository test MUST enumerate
35+
every authored Xcode deployment-target declaration, require the expected
36+
configuration count, and require exact value `15.0`. A missing, duplicate,
37+
lower, or independently changed configuration fails before merge.
38+
39+
3. **Enforce the compiled application payload.** The exported-IPA gate MUST
40+
require the top-level application's `MinimumOSVersion` and main Mach-O
41+
build-version command to encode an iOS minimum of at least `15.0`. A bundled
42+
dependency may retain a lower compatibility floor because it can still run
43+
inside a higher-floor application; it does not lower the application's
44+
installation requirement. This extends rather than replaces the existing
45+
signature, nested-code, and Xcode software-information checks.
46+
47+
4. **Require a clean rebuild.** A deployment-floor change MUST rebuild and
48+
re-archive the application and all embedded content from source. Editing an
49+
`Info.plist`, Mach-O load command, archive, or IPA after compilation/signing,
50+
or merely re-exporting an older archive, is prohibited.
51+
52+
5. **Preserve build 5 as history.** The retained `0.2.0 (5)` IPA, checksum,
53+
archive, source commit, and TestFlight plan continue to describe the exact
54+
iOS/iPadOS 13 artifact submitted through Transporter. They MUST NOT be
55+
relabelled as iOS/iPadOS 15.
56+
57+
6. **Do not create build 6 in this change.** The package identity remains
58+
`0.2.0+5` because this source-only change is not an external candidate or
59+
store upload. A later external artifact MUST receive the next globally
60+
monotonic build number under ADR-0044 and complete its own validation and
61+
handoff record.
62+
63+
## Consequences
64+
65+
- Future PyBLE iOS and iPadOS builds install only on version 15.0 or later;
66+
devices limited to versions 13 and 14 cannot install or update to those
67+
builds.
68+
- The existing build-5 TestFlight artifact remains eligible on its encoded
69+
platform range and remains distinguishable from any later candidate.
70+
- Store validation will fail locally if a project configuration or the
71+
compiled top-level application regresses below the maintained floor.
72+
- Raising the deployment floor changes no PBLE/1 behavior, firmware baseline,
73+
application feature, Android minimum, or current app version/build identity.
74+
75+
## Alternatives considered
76+
77+
- **Wait until Spring 2027.** Temporarily preserves iOS/iPadOS 13 and 14 for
78+
new builds, but leaves a known distribution migration on the critical path
79+
and permits new features to keep treating the older floor as the default.
80+
- **Patch or re-export build 5.** Rejected because the minimum is compiled into
81+
Mach-O load commands as well as bundle metadata, and because an uploaded
82+
build identity and signed historical artifact must remain immutable.
83+
- **Upload build 6 immediately.** Rejected for this change because the
84+
maintainer requested a future-source default only; external candidate
85+
creation, physical acceptance, and TestFlight upload remain separate work.
86+
- **Set only Release to 15.0.** Rejected because Debug/Profile divergence would
87+
hide compatibility failures during development and testing.
88+
89+
## Related
90+
91+
- [App build and distribution requirements](../specifications/App/specs.md#9-build-versioning--distribution--bld)
92+
- [App test design](../specifications/App/TDD.md#159-ios-and-ipados-deployment-floor-gate)
93+
- [Build 5 TestFlight handoff](../testing/testflight/0.2.0-build-5.md)
94+
- [Apple `MinimumOSVersion`](https://developer.apple.com/documentation/BundleResources/Information-Property-List/MinimumOSVersion)
95+
96+
<!-- SPDX-License-Identifier: MIT -->

‎docs/decisions/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,6 @@ Each ADR captures one significant decision: its context, the choice, and the con
4848
| [0042](0042-prefill-official-examples-and-discover-branches.md) | Prefill the editable official examples repository and provide bounded, branch-only discovery while retaining advanced explicit refs | Accepted |
4949
| [0043](0043-session-bound-visible-file-multi-delete.md) | Bind visible regular-file multi-delete to one board session with sequential fail-fast execution and honest partial results | Accepted |
5050
| [0044](0044-start-app-v020-beta-train-at-build-5.md) | Start the app 0.2.0 beta train at build 5 with synchronized package, PBLE/1 HELLO, and GitHub request identities | Accepted |
51+
| [0045](0045-adopt-ios-15-deployment-floor.md) | Adopt iOS and iPadOS 15 as the maintained source and compiled distribution floor while preserving build 5 as historical evidence | Accepted |
5152

52-
Decided 2026-06-30 (0001–0005); 0006 accepted 2026-07-01; 0007, 0008, 0009 and 0010 accepted 2026-07-02; 0011 accepted 2026-07-03; 0012 accepted 2026-07-25; 0013 accepted 2026-07-27; 0014, 0015, 0016, and 0017 accepted 2026-07-28; 0018–0022 accepted 2026-07-29; 0023 and 0024 accepted 2026-08-01; 0025 and 0026 accepted 2026-08-02; 0027–0029 accepted 2026-08-03; 0030 accepted 2026-08-11; 0031–0033 accepted 2026-08-12; 0034 accepted 2026-08-13; 0035 and 0036 accepted 2026-08-16; 0037 and 0038 accepted 2026-08-20; 0039 accepted 2026-08-21; 0040 accepted 2026-08-22; 0041 accepted 2026-08-25; 0042–0044 accepted 2026-08-27.
53+
Decided 2026-06-30 (0001–0005); 0006 accepted 2026-07-01; 0007, 0008, 0009 and 0010 accepted 2026-07-02; 0011 accepted 2026-07-03; 0012 accepted 2026-07-25; 0013 accepted 2026-07-27; 0014, 0015, 0016, and 0017 accepted 2026-07-28; 0018–0022 accepted 2026-07-29; 0023 and 0024 accepted 2026-08-01; 0025 and 0026 accepted 2026-08-02; 0027–0029 accepted 2026-08-03; 0030 accepted 2026-08-11; 0031–0033 accepted 2026-08-12; 0034 accepted 2026-08-13; 0035 and 0036 accepted 2026-08-16; 0037 and 0038 accepted 2026-08-20; 0039 accepted 2026-08-21; 0040 accepted 2026-08-22; 0041 accepted 2026-08-25; 0042–0045 accepted 2026-08-27.

‎docs/specifications/App/TDD.md‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2239,6 +2239,30 @@ records the clean source commit, version name/code, bundle SHA-256, and public
22392239
upload-certificate SHA-256; the two certificate fingerprints extracted from
22402240
the AAB and upload keystore MUST match before upload (BLD-12).
22412241

2242+
### 15.9 iOS and iPadOS deployment-floor gate
2243+
2244+
The authored Runner project owns one deployment floor: every Debug, Release,
2245+
and Profile `IPHONEOS_DEPLOYMENT_TARGET` declaration is exactly `15.0`.
2246+
`app_identity_manifest_test.dart` parses the project, pins the expected three
2247+
declarations, and rejects a missing, duplicate, lower, or divergent value.
2248+
The project does not hand-author `MinimumOSVersion` in Runner's `Info.plist`;
2249+
Xcode derives it from the deployment target (BLD-13).
2250+
2251+
After a clean archive and App Store export, `validate_ios_ipa.sh` reads the
2252+
top-level application `MinimumOSVersion` and rejects a value below `15.0`. It
2253+
also reads the main executable's iOS `LC_BUILD_VERSION` minimum with Xcode
2254+
`vtool` and rejects a compiled minimum below `15.0`. A bundled dependency may
2255+
retain a lower compatibility floor without lowering the containing app's
2256+
installation floor. These checks run before the existing signature,
2257+
nested-code, and Xcode software-information gates report success. An old
2258+
archive cannot satisfy a new source floor by re-export or plist mutation
2259+
because the compiled load command remains authoritative.
2260+
2261+
The retained `0.2.0 (5)` handoff stays bound to its iOS/iPadOS 13 archive and
2262+
checksum. This source change leaves `pubspec.yaml` at `0.2.0+5` and produces no
2263+
new external candidate; ADR-0044 assigns the next build number only when a
2264+
later artifact is prepared for external handoff (BLD-3/13).
2265+
22422266
## 16. Traceability
22432267

22442268
Design element → satisfied requirement IDs. Each `FR-*`/`NFR-*`/`CON-*`/`DAT-*` family has at least one design home.
@@ -2265,13 +2289,13 @@ Design element → satisfied requirement IDs. Each `FR-*`/`NFR-*`/`CON-*`/`DAT-*
22652289
| Error handling & mapping ([§14](#14-error-handling--mapping)) | lib/pble + UI | FR-PBLE-13, FR-ERR-*, FR-FILES-3, NFR-USE-3, NFR-REL-4 |
22662290
| Reliability (resume, CRC, preserve-on-drop) ([§8.4](#84-file-transfer-state-machine), [§9.2](#92-migrations--hydration)) | lib/pble, lib/data | NFR-REL-1..4, NFR-PERF-1/2, FR-PROJ-6 |
22672291
| Test design, shared corpus, gates ([§15](#15-test-design)) | all | NFR-MAINT-1/3/4, BLD-5/8, CON-6/8 |
2268-
| Build/versioning/distribution ([§15.6](#156-import-boundary--no-leak-gates), [§15.7](#157-launcher-identity-and-platform-assets), [§15.8](#158-android-release-signing-and-app-bundle-gate), [§8.6](#86-hello--capabilities)) | all | BLD-1..12, IF-6, NFR-COMPAT-1 |
2292+
| Build/versioning/distribution ([§15.6](#156-import-boundary--no-leak-gates), [§15.7](#157-launcher-identity-and-platform-assets), [§15.8](#158-android-release-signing-and-app-bundle-gate), [§15.9](#159-ios-and-ipados-deployment-floor-gate), [§8.6](#86-hello--capabilities)) | all | BLD-1..13, IF-6, NFR-COMPAT-1 |
22692293
| Security & privacy ([§5.1](#51-the-connection-interface), [§6.3](#63-single-active-writer--serialization), [§8.9](#89-screenless-identity--identify-control-commands), [§9.1](#91-schema), [§10.2](#102-canonical-input-and-immutable-snapshot-resolution), [§11.5](#115-screenless-identity--rename--identify)) | lib/pble, lib/connect, lib/data, lib/github_import | SEC-1..10 |
22702294

22712295
**Requirements with no dedicated design element:** none functional. Notes:
22722296

22732297
- **NFR-PERF-3/4** and **OI-4** have design *levers* (MTU 247, window `W`, bounded console buffer, time-to-connect via saved `board_ref`) but the concrete ceilings are HIL-frozen later — by intent, not a gap.
2274-
- **BLD-1/2/3/4/6/10/11/12** (single Flutter codebase, `pubspec` governance + lock, SemVer, free dual-store-at-parity distribution, generated notices + in-app Open-Source Notices screen / IF-6, platform launcher packaging, store-signable embedded data, and fail-closed Android upload signing) are project/build-pipeline obligations: this TDD honors them (single codebase, ASCII identifiers, notices screen surfaced in UI, shared vector launcher source, deterministic Blockly asset transform, and the signed AAB gate) but the release pipeline is owned by the build/infra stories (X-03/X-11). Flagged here for completeness.
2298+
- **BLD-1/2/3/4/6/10/11/12/13** (single Flutter codebase, `pubspec` governance + lock, SemVer, free dual-store-at-parity distribution, generated notices + in-app Open-Source Notices screen / IF-6, platform launcher packaging, store-signable embedded data, fail-closed Android upload signing, and the compiled iOS/iPadOS deployment floor) are project/build-pipeline obligations: this TDD honors them (single codebase, ASCII identifiers, notices screen surfaced in UI, shared vector launcher source, deterministic Blockly asset transform, signed AAB gate, and source-plus-IPA iOS floor gates) but the release pipeline is owned by the build/infra stories (X-03/X-11). Flagged here for completeness.
22752299
- **BLD-9** (previous-protocol-major compatibility window) is forward-looking: no PBLE/2 exists yet, but its design home is the version-negotiation seam ([§8.6](#86-hello--capabilities)) — the same HELLO `proto_versions[]` exchange that refuses an unsupported version (FR-PBLE-5/6) is where a future app would select a previous major, so deployed boards are never bricked by an app update.
22762300
- **OI-1** (editor widget) is closed by ADR-0012, and **OI-2** (state management) is closed by ADR-0007. Their choices are reflected in the Riverpod state graph and the executable `EditorSurface` fallback ([§11.1](#111-editor)); R1 remains a measured runtime risk rather than an undecided architecture.
22772301

‎docs/specifications/App/specs.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,7 @@ The local Drift database ([PRD §12.1](../prd.md), `lib/data/`) MUST model exact
11521152
- **BLD-10 — Launcher identity (FROZEN · X-11 · `[docs]` 2026-07-29).** iPadOS and Android MUST ship the same original PyBLE **Prompt Chip** identity from an MIT, grid-defined vector source: a full-bleed flat deep-navy `#081B35` field; one electric-blue `#2F8CFF` rounded microcontroller frame with exactly two pins on its top, bottom, and left edges and no right-edge pins; one large near-white `#F4F7FF` terminal prompt; and one subordinate blue radio arc on the right. The complete foreground MUST stay inside Android's 66×66 dp guaranteed safe zone. The production artwork MUST NOT contain text, a connection-status indicator, the Bluetooth or Python figure mark, extra radio arcs, generated lighting, a baked platform mask, or platform-specific geometry. Repeatable exports MUST provide an opaque 1024×1024 iOS default icon plus deliberate dark and grayscale-tinted appearances; opaque Android legacy density icons plus adaptive background/foreground and monochrome layers; and a 512×512 RGBA Google Play listing icon. Unit tests MUST verify catalog/resource linkage, canonical vector parity, formats, and dimensions; release builds plus 20 px and circle/squircle/rounded-square visual checks verify compilation, legibility, and mask safety. MUST (*source: PRD §13.6, §15.3, §18.2; verify: unit/build/manual; story: X-11*)
11531153
- **BLD-11 — Store-signable embedded assets (FROZEN · X-11 · `[docs]` 2026-07-29).** Every non-Mach-O file embedded in an iOS distribution archive MUST remain a sealed data resource and MUST NOT be classifiable as an unsigned nested executable. In particular, generated Blockly JavaScript that contains multiline Python templates MUST be transformed deterministically at vendoring time so the packaged file is ordinary JavaScript/data rather than a Python script while preserving byte-for-byte generated Python at runtime. The asset-policy unit gate MUST pin that transform; the vendoring build and exported-IPA gate MUST reject any Flutter asset identified as `script text executable`; and any correction MUST be followed by a clean archive/export rather than mutation of a signed bundle. MUST (*source: PRD §18.2; Apple Code Signing Guide / TN2206; verify: unit/build/manual; story: X-11*)
11541154
- **BLD-12 — Android release signing (FROZEN · X-11 · `[docs]` 2026-08-07).** Every Android `release` variant MUST fail closed unless an explicit non-debug signing identity is supplied through the four secret-manager environment variables `PYBLE_ANDROID_KEYSTORE_PATH`, `PYBLE_ANDROID_KEYSTORE_PASSWORD`, `PYBLE_ANDROID_KEY_ALIAS`, and `PYBLE_ANDROID_KEY_PASSWORD`; it MUST never fall back to the Android debug key or an unsigned artifact. The production Play artifact MUST be an upload-key-signed Android App Bundle for `dev.pyble.pyble`. Public CI MUST exercise this exact release-signing path with a disposable CI-only key, build both the production APK and AAB, verify the AAB JAR signature, and never receive the production upload key. The final release gate MUST compare the AAB signer SHA-256 fingerprint with the owner-controlled upload certificate and record the AAB SHA-256 plus source commit and version. MUST (*source: PRD §15.3, §18.2; Android app-signing requirements; verify: unit/build/manual; story: X-11*)
1155+
- **BLD-13 — iOS/iPadOS 15 deployment floor (FROZEN · X-11 · `[docs]` 2026-08-27, [ADR-0045](../../decisions/0045-adopt-ios-15-deployment-floor.md)).** Every authored Runner Xcode build configuration MUST set `IPHONEOS_DEPLOYMENT_TARGET` exactly to `15.0`. Every later exported top-level application `MinimumOSVersion` and main Mach-O build-version command MUST encode an iOS minimum of at least `15.0`; the exported-IPA gate MUST reject any regression. A bundled dependency may retain a lower compatible minimum without lowering the containing application's installation floor. A floor change requires a clean source rebuild/archive and MUST NOT patch or merely re-export an older signed artifact. The retained `0.2.0 (5)` iOS/iPadOS 13 TestFlight handoff remains immutable; this source-only change creates or uploads no build 6 and changes no Android, PBLE/1, or firmware baseline. MUST (*source: PRD §18.2; Apple `MinimumOSVersion`; verify: unit/build/manual; story: X-11*)
11551156

11561157
## 10. Security & Privacy — SEC
11571158

‎docs/specifications/prd.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1613,7 +1613,9 @@ v1.0 is accepted when **all** of the following pass. The first block folds in th
16131613
9. The **no-leak CI gate** is green on a clean tree and demonstrably fails on a fixture token (stories X-02).
16141614
10. `THIRD_PARTY_LICENSES` ships with each firmware artifact and the in-app Open-Source Notices screen renders on both iPadOS and Android (§15.2).
16151615
11. The **esp-web-tools** web flasher at `pyble.dev/flash` flashes each chip target from a supported browser (story X-10).
1616-
12. Free App Store + Play builds are submitted at parity, and firmware bins are published via GitHub Releases (story X-11).
1616+
12. Free App Store + Play builds are submitted at parity, the iOS/iPadOS build
1617+
encodes a minimum deployment target of at least `15.0`, and firmware bins
1618+
are published via GitHub Releases (story X-11).
16171619
13. **Localization parity** holds: `en` is complete and CI blocks any merge that breaks ARB parity (story X-12).
16181620
14. Every shipped behavior has a test (no behavior without a test); the PBLE/1 conformance suite (frame round-trip, CRC, fragmentation, window/resume, error mapping) is green.
16191621

0 commit comments

Comments
 (0)