Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/scripts-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ on:
- 'scripts/setup-workspace.sh'
- 'scripts/build-ios-port.sh'
- 'scripts/build-ios-app.sh'
- 'scripts/check-ios-framework-links.py'
- 'scripts/run-ios-ui-tests.sh'
- 'scripts/run-watch-ui-tests.sh'
- 'scripts/run-tv-ui-tests.sh'
Expand Down Expand Up @@ -56,6 +57,7 @@ on:
- 'scripts/setup-workspace.sh'
- 'scripts/build-ios-port.sh'
- 'scripts/build-ios-app.sh'
- 'scripts/check-ios-framework-links.py'
- 'scripts/run-ios-ui-tests.sh'
- 'scripts/run-watch-ui-tests.sh'
- 'scripts/run-tv-ui-tests.sh'
Expand Down Expand Up @@ -241,6 +243,22 @@ jobs:
run: ./scripts/build-ios-app.sh -q -DskipTests
timeout-minutes: 30

# Every framework the port's natives need must be one the project DECLARES.
# A framework that only reaches the link through an autolink hint -- a Swift
# file's `import SwiftUI`, a CocoaPods dependency, `@import` under the Metal
# build's CLANG_ENABLE_MODULES -- links this sample and no plain Objective-C
# application. That is how `_OBJC_CLASS_$_UTType` stayed undefined in every
# customer archive while this job was green for five days.
#
# Runs on the project the step above already generated, so it costs seconds
# and it never links a Swift object, a pod or an extension.
- name: Check the port links only frameworks the project declares
run: |
set -euo pipefail
scripts/check-ios-framework-links.py \
"$(dirname "${{ steps.build-ios-app.outputs.workspace }}")"
timeout-minutes: 10

- name: Run iOS UI screenshot tests
env:
ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/ios-ui-tests
Expand Down Expand Up @@ -461,6 +479,22 @@ jobs:
run: ./scripts/build-ios-app.sh -q -DskipTests
timeout-minutes: 30

# Every framework the port's natives need must be one the project DECLARES.
# A framework that only reaches the link through an autolink hint -- a Swift
# file's `import SwiftUI`, a CocoaPods dependency, `@import` under the Metal
# build's CLANG_ENABLE_MODULES -- links this sample and no plain Objective-C
# application. That is how `_OBJC_CLASS_$_UTType` stayed undefined in every
# customer archive while this job was green for five days.
#
# Runs on the project the step above already generated, so it costs seconds
# and it never links a Swift object, a pod or an extension.
- name: Check the port links only frameworks the project declares (Metal)
run: |
set -euo pipefail
scripts/check-ios-framework-links.py \
"$(dirname "${{ steps.build-ios-app.outputs.workspace }}")"
timeout-minutes: 10

- name: Run iOS UI screenshot tests (Metal)
env:
ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/ios-ui-tests-metal
Expand Down
54 changes: 54 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,60 @@ calls -- is a warning: it is dead code, not a broken build. Note the offline gat
reads `target/classes`, so **a stale port build reports natives that no longer
exist**; rebuild the module before believing a finding.

#### A framework an app gets by accident is a framework it can lose

The iOS port referenced the `UTType` class while `ByteCodeTranslator` linked
`UniformTypeIdentifiers.framework` only on the macOS branch -- and *every* iOS CI
job stayed green for five days while *every* customer archive failed with
`Undefined symbols: "_OBJC_CLASS_$_UTType"`.

Nothing declared the framework in either case. The sample application CI builds
simply supplies it by accident, three different ways, none of which a plain
application has:

- **A Swift autolink hint.** `import SwiftUI` makes swiftc stamp
`-framework UniformTypeIdentifiers` into the object as an `LC_LINKER_OPTION`,
and the linker honours it. The sample has Swift in its app target (App Intents,
Live Activity surfaces); an ordinary Codename One app has none.
- **`CLANG_ENABLE_MODULES`.** `IPhoneBuilder` turns it on for the Metal build,
for pods, the watch and the VPN extension. With modules on, a plain
`#import <Framework/Header.h>` autolinks too -- so the *same source* links on
the Metal build and fails on the GL one.
- **CocoaPods.** A pod's own dependencies are on the link line either way.

So the rule is: **a framework the port's natives reference must be declared in
`ByteCodeTranslator.includeFrameworks`**, never left to an autolink hint. Weakly
(`optionalFrameworks`, which emits `ATTRIBUTES = (Weak, )`) when the API is newer
than the lowest `ios.deployment_target`, so a device without the framework still
launches -- the uses are `@available` fenced. Classify it in `WatchNativeBuilder`
in the same change, the way the watch partition test asks.

`scripts/check-ios-framework-links.py` gates this. It runs over the project
`build-ios-app.sh` already generated, so it costs seconds, and it answers one
question: *every symbol our natives reference that an SDK framework defines -- is
it defined by a framework this project declares?* It compiles the port's natives
with the project's own settings (prefix header, defines, per-file `-fobjc-arc`,
modules where the project asks) and then resolves the undefined symbols against
the declared set alone. **Autolink cannot hide anything from it**: a hint is a
load command, not a definition, so `nm` still reports the symbol undefined, and
the probe it links to confirm a finding carries no hints at all.

Two properties worth keeping if you touch it. A native that fails to compile is
**fatal**, not skipped -- a partial set reporting success is the exact failure
this replaces. And the SDK symbol index self-tests (a symbol floor, plus two
known symbols that must resolve to `Foundation` and `UIKit`), because a text
index that silently parses to nothing reports every project clean.

```bash
scripts/check-ios-framework-links.py <generated-ios-source-dir>
scripts/check-ios-framework-links.py <dir> --sdk iphonesimulator --verbose
```

Scope is the **iOS application target and our own port natives**. Translated C
only calls into natives, and a cn1lib's natives are its author's to declare. The
watch, tv and macOS slices are not covered yet.


### Integration Tests

Located in `maven/integration-tests/`:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,12 @@ class WatchNativeBuilder {
+ "LocalAuthentication.framework;MobileCoreServices.framework;"
+ "NaturalLanguage.framework;NetworkExtension.framework;PhotosUI.framework;"
+ "QuartzCore.framework;Security.framework;UserNotifications.framework;"
// In every project's link phase (ByteCodeTranslator.includeFrameworks), and present in
// BOTH watch SDKs -- verified against WatchOS26.2 and WatchSimulator26.2, the pair
// BackgroundTasks taught us to check separately. The watch slice really does reference
// it: CN1DragAndDrop.m's MIME-to-identifier mapping is outside the UIKit guard, so the
// UTType class is compiled for the watch the same as everywhere else.
+ "UniformTypeIdentifiers.framework;"
// Linked on the iOS slice alongside ContactsUI when the app references
// com.codename1.contacts.ContactPicker. Present in both watch SDKs, and the watch
// never calls into it -- but a framework that IS there links harmlessly, and
Expand Down
Loading
Loading