Watch apps: one entry point, a phone-watch channel, and a simulator that can run the pair - #5487
Watch apps: one entry point, a phone-watch channel, and a simulator that can run the pair#5487shai-almog wants to merge 295 commits into
Conversation
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
Cloudflare Preview
|
|
Compared 12 screenshots: 12 matched. |
|
Compared 151 screenshots: 151 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
|
Compared 181 screenshots: 181 matched. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f10016b1e4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…e in Moving those models to logical points left ppi at the physical Retina 326. JavaSEPort turns it into pixelMilliRatio = ppi / 25.4, which every millimetre-based size is converted through, so the simulator made each one twice the size the device gives it: IOSImplementation.getDeviceDensity() answers DENSITY_MEDIUM for a display narrower than 500, and the watch now reports 198. 163 is that density. The Wear models are still in pixels and keep 326. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Probing usr/lib/swift/<M>.swiftmodule and usr/include/<M> found the Swift overlays -- Darwin, Dispatch and ObjectiveC each have one, which is why the three modules that got tested passed -- and missed nearly everything else. Apple declares its C modules in shared maps, and of the 78 the watchOS SDK's usr/include/module.modulemap names, 66 matched no path at all: SQLite3, zlib, MachO, notify, os_object, TargetConditionals among them. A staged source importing any of those was called unattributed, and one unattributed import switches strict package filtering off wholesale, mirroring every phone package product into the watch target. The maps are now parsed, along with the toolchain's own swiftmodule directories, and memoised: about 2800 names in roughly a second, once per build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…inst theGeneratedPredicatesDecideCorrectly failed on build-linux-jdk8: these plugin tests run on Linux too, where there is no xcrun to ask for an SDK path. The guard I wrote for that -- `|| sdk_names.empty?` -- cannot fire, because the helper seeds the Swift standard names before it reads anything and so never returns an empty map. On Linux it therefore asked whether a host with no Apple SDK knows about SQLite3, and failed. Guarded on whether an SDK path was actually found, with the lookup itself rescued: a missing xcrun raises rather than returning empty. Verified both ways -- against the real SDKs, and against an xcrun that exits non-zero, which takes the skip. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ly be said The notice naming kinds that will appear on no platform was guarded on widgetExtensionBuilder existing, and parseSurfacesManifest clears surfacesExtensionEnabled before that builder is created whenever every declared kind is watch-only. The one case the notice exists for is therefore the one case it could not reach: the build discarded every declared surface and reported only that the iOS lowering had been skipped. It is emitted from parseSurfacesManifest now, naming the kinds. The block at the extension-generation site keeps the mixed case -- watch-only kinds alongside iOS ones, where the extension is built and quietly carries none of them -- and its comment no longer claims to cover both, which was how this hid. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f45a720626
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
putSecondaryEntryPointArguments read only the unprefixed project setting, while the cloud mirror leaves an existing codename1.arg.* value alone -- so -Dcodename1.arg.watchMain=... reached the daemon and was ignored locally, and one invocation produced two different products. An absent project setting also disabled the watch target outright, whatever the command line asked for. That is worse than an override that works nowhere, because nothing in the local build says the flag was dropped. The overlaid argument comes first now, the project setting behind it, which is the order the mirror already resolves them in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`== 1` and `!= 0` say exactly what the bare macro says, and the atom matcher accepted only the bare form -- so `#if TARGET_OS_WATCH == 1` was left undecided, its #else survived, and a phone-only package imported there was mirrored into the watch target. The mirror of the `== 0` reading already in cn1_watch_excludes_watch: that side learned two rounds ago that a zero comparison is a negation, and this side was not taught the same thing about a one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Requiring both a device and a simulator slice rejected a valid watchOS-only archive: the framework phase then skipped it and the watch target failed on undefined symbols, over a simulator slice nothing in that build was going to load. The same conjunction was in the bundle check, over CFBundleSupportedPlatforms and SupportedPlatformVariant, and is corrected with it -- it is the same rule about the same artifacts, and only the archive half was reported. EITHER slice here, deliberately, rather than a specific one. This generator hands the developer an Xcode project and does not know which destination they will build for. The cloud builder does -- it selects the SDK and the destination itself -- so it asks for exactly the matching slice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A reply on a review thread is not visible to whatever reads the diff next, so two rejections that were argued in comment threads are written where the decision lives. The watch target is created :application, not :watch2_app. com.apple.product-type.application.watchapp2 is the LEGACY WatchKit App and is valid only paired with a watchkit2-extension carrying the code. This generates the modern single-target app that replaced it -- WKApplication, SwiftUI @main with @WKApplicationDelegateAdaptor, watchOS 10 floor, one target -- and Xcode's own watchOS App.xctemplate declares com.apple.product-type.application with no mention of watchapp2. The only watchapp2 in that template directory is the legacy iOS container. The watch translation is passed no separate reflective-keep-roots channel because the stub IS that channel: it gets the same svgRegistryInstall and route and annotation install snippets the phone stub does, so SVGRegistry.installGlobal runs there too and the generated classes it names are hard references the translator follows. The watch suite builds a distinct watchMain and SVGStatic and SVGAnimated match their goldens every run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5a563336dd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…lishes CN1WatchHost derives one geometric inset from the corner radius and applies it to all four sides. The skins took the vertical inset as 6% of the display height instead, so the Apple models advertised 13 and 15 points against the host's 4 and 5 -- three times the inset, on the axis a watch layout is tightest on. A layout honouring the safe area reflowed in the simulator and fitted on the device, which is the simulator lying in the direction that wastes the most time. Nothing on a rounded-rectangle face intrudes vertically beyond the corner arc, so the extra modelled nothing. A circular face keeps its own rule: inscribing a rectangle in a circle really does cost about 15% on both axes. Correcting the record as well as the code -- an earlier commit claimed the regenerated 45mm skin "advertises the same five-point inset the watch does", which was true of the horizontal axis and stated as though it were the whole rectangle. The new test mirrors the host's formula rather than hard-coding 4 and 5, so the two sides cannot drift apart again without failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It did not do what I said it did. The justification was that filtering
NativeLookup.register out of the watch stub stops a phone-only native's
Objective-C being compiled for watchOS, where a UIKit import is a build failure.
ByteCodeTranslator.execute copies every non-class file on the classpath into its
output verbatim -- `if (name.endsWith(".class")) Parser.parse(f); else copy(f)`
-- so that .m is staged and compiled for whichever target lists it whether or
not any Java stub names it. The filter never prevented that failure. A native
that cannot build for watchOS is guarded with TARGET_OS_WATCH in its own source,
which is how the port handles its own in 71 files, and the developer guide now
says so.
So the walk went, and with it: reachableClasses, classReferences,
installedRegistryRoots, reachesHealth, resolveHealthUsagePerRoot,
healthListenersReachableFrom, nativeRegistrationsReachableFrom, the four
per-root health fields, the sensorWriteThroughCallers attribution and the
ClassScanner.scanningClass hook added to feed it. Roughly 700 lines across the
two builders and their tests.
What replaces it is what was already there: the app-wide API scan decides
whether the app uses HealthKit, both targets get the same answer, and
watchNative.health overrides it when the scan is wrong. That is one boolean and
an existing hint instead of a bytecode walk.
The cost is honest and small: a watch that does not use health carries the
listener registry unless the project says watchNative.health=false, and the
watch binary is slightly larger than a per-root filter would leave it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…or them WATCH_OPTIONAL_FRAMEWORKS already names every framework the port links that watchOS does not have -- it is what ParparVM weak-links through -Doptional.frameworks, and the port's sources are #ifdef'd around them. That is the conditional-system-library arrangement every other platform here uses, and the frameworks phase now follows the same declaration instead of probing SDK directories for each entry. The probe was the wrong shape, not merely mis-tuned. It asked whether a framework exists in the watch SDKs, which has no single right answer: requiring both dropped a device-only framework such as BackgroundTasks from a device archive, and requiring only the active one makes the generated project depend on which destination produced it. A declaration has neither problem, is reviewable in one place, and cannot disagree with the weak-link list because it IS the weak-link list. The SDK lookup that remains answers a different question -- whether an import in staged Swift names something the SDK vends or a package product -- and it now covers frameworks too, which is where the framework directories went. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The declaration names what the PORT links, so it can say whether the watch has CarPlay or OpenGLES and nothing at all about a dylib the developer dropped into the project. Routing both through it accepted any raw library whose basename happened not to be on the list, and an iOS-only one was then weak-linked into the watch target -- weak linkage does not save a platform mismatch at link time. So the raw-library branch now draws the distinction the .framework branch already drew: from the SDK, the declaration decides; vendored, the binary does. A .dylib is Mach-O and answers the same slice test the static archives use. A .tbd is text that names its platforms, and unreadable is answered no, as the plist and archive checks answer it -- a library left out costs a link error naming it, one wrongly linked costs a mismatch deep in a build with no reason to involve it. Checked against the real stubs: watchOS libz.tbd reads as watch, the iPhoneOS one does not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing is both iOS and watchOS, so `#if os(watchOS) && os(iOS)` is false on every platform and that arm never compiles anywhere. Treating the presence of os(watchOS) as evidence the arm might compile on the watch kept an unreachable arm alive and mirrored the iOS-only package it imports into the watch target. The dual of the disjunction rule added earlier: there every operand had to exclude the watch for the whole to exclude it, here any one operand does. Split at the top level and asked recursively, so an operand that is itself a disjunction gets the whole test, and `os(watchOS) && FEATURE` is still not excluded -- a feature flag is unknown, not false. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 721b548915
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Two findings, one in each half of the watch build. **The local node is not a peer.** Every module the build emits declares the same `cn1_wearable` capability, so this device satisfies the capability query it issues, and FILTER_ALL is an unfiltered set. Keeping our own id made isPaired() and isCompanionAppInstalled() answer true on a device with no counterpart at all, purely on the strength of seeing ourselves. Filtered where the cache is WRITTEN -- all four paths -- rather than where each question is asked, so there is one rule instead of one per caller. The transfer quorum already stripped the local node after the fact; that line goes, because filtering there left every other reader of the same cache still seeing us. The async refresh asks getLocalNode() alongside the capability query instead of resolving it in the callback: that listener runs on the main thread unless given an executor, and a blocking identity query there is an ANR -- the trap bondedNodeIds() already routes latency-sensitive callers around. Both tasks have completed by the time the combined listener runs, so neither read blocks. **A parenthesized disjunction is punctuation, not nesting.** In `#if (os(iOS) || os(macOS)) && FEATURE` the top-level `||` split yields one operand, and returning "cannot tell" there settled the condition before the conjunction rule could observe that the group is false on watchOS -- retaining an arm the compiler drops and mirroring its iOS/macOS-only package into the watch target. The splitter now strips parentheses that wrap the whole expression, balance-checked so `(a) && (b)` keeps its `&&`, and a single top-level operand falls through instead of answering. Falling through makes the rest of the function reachable for conditions it never saw before, and every scan below reads a positive platform mention as exclusion. `#if !(os(iOS))` is TRUE on watchOS -- that arm is the one the watch compiles -- so a negated group is now explicitly undecidable, which keeps it. That was already wrong before this change; it just could not be reached. 27 predicate assertions, including both over-exclusion guards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…amed `ld: framework 'SystemConfiguration' not found` on the unsigned device archive. watchOS has no SystemConfiguration -- and no AudioToolbox and no QuickLook -- yet ByteCodeTranslator puts all three in every generated project's link phase, and WATCH_OPTIONAL_FRAMEWORKS did not name them, so they survived into the watch target. The source half of the arrangement was already right: every one of these is imported under `#if !TARGET_OS_WATCH` in the port. That is why it went unnoticed -- the watch target compiled cleanly and only failed at the link, so nothing short of an archive got far enough to see it. Weak-linking cannot cover an absent framework: `ATTRIBUTES = (Weak, )` still resolves against the SDK, so a framework the SDK does not have has to be left out of the phase entirely. Under-declaring is a link error; over-declaring one watchOS does have is merely tidy. The two are worth telling apart, so the constant now says which of its entries are which and how to check: ls "$(xcrun --sdk watchos --show-sdk-path)/System/Library/Frameworks" | grep X I audited the translator's whole base list that way rather than adding back the single name the linker happened to print first; AudioToolbox and QuickLook came out of that audit, not out of the log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
decode() calls Integer.parseInt on the four bytes after a '%', so a stored path containing something like `%zzzz` threw NumberFormatException. In replaySpooled that call sat OUTSIDE the try, so the throw went past all three completion callbacks: the key stayed in SPOOL_IN_FLIGHT for the life of the process, spoolBusy() stayed true, and every later reply-bearing request was silently downgraded to a plain spooled message. It also aborted the rest of that drain pass, so records queued behind the bad one were never looked at either. Fixed at both levels, because either alone leaves the other exposed. The try now covers the WHOLE body of replaySpooled rather than the delivery alone. Every step in there parses bytes some earlier build wrote -- the framing search, the substrings, the percent decode, the Base64 -- and guarding only the ones already seen to fail is what let the next one strand a claim. The raw path is kept before decoding so the handler can still name the record when decoding is what failed, and both delivery calls queue rather than run listeners inline, so a throw from one has not already spent `delivered` or `dropped`. decode() itself no longer throws: a '%' not followed by four hex digits is literal text. That is the same hazard at two more call sites -- both enumerate peer-published item paths, where one corrupt byte took out the whole enumeration -- and wrapping each caller would have left the third to be found later. encode() escapes everything outside [A-Za-z0-9_-] and always writes four hex digits, so nothing it produces reaches the lenient branch; verified by round-tripping the encoder and by feeding the decoder every malformed shape I could construct (bare %, short %0, non-hex %zzzz, doubled %%, trailing %). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 62e51c9d56
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Rejecting a P1 that says an untrusted local app can bind the exported WearableListenerService and hand us a forged message or deletion. It cannot, and our own comments are what said otherwise -- so the fix is to correct them rather than to add a check. Verified against play-services-wearable 18.2.0, not from memory: WearableListenerService.onBind is FINAL. There is no override point, which is also why the suggested remedy -- authenticate the binder caller -- cannot be written. It is final because the library does it for us. The binder it returns (com.google.android.gms.wearable.zzag) starts every dispatch with Binder.getCallingUid() and, for any uid not already accepted, calls UidVerifier.isGooglePlayServicesUid. That requires the calling uid to own the com.google.android.gms package AND that package to pass GoogleSignatureVerifier.isGooglePublicSignedPackage. A uid that fails logs "Caller is not GooglePlayServices; caller UID: n" and the dispatch returns having invoked nothing. WearableListenerService declares no onStartCommand, so the started-intent route delivers nothing either, and we override neither method. A local app can therefore bind or start this service and still never reach onMessageReceived, onDataChanged or ensureAppRunning. What the node checks actually do is bound which PEER an authentic Play services delivery may claim to be from -- a weaker and different question, and the one dataItemExists answers properly for data changes. Saying they were the barrier against a local attacker overstated both the threat and the mitigation, and is what generated this finding; it would have generated it again. The argument lives in the class javadoc, in the manifest comment that exports the service, and next to isKnownNode, because those are the three places someone asks the question. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… parser Three things, one of them mine to answer for. **NativeLookup has no business in our own code.** registeredInterfaceName() parsed `NativeLookup.register(...)` lines out of the generated stub. It had no callers -- it was left behind when the reachability walk was deleted -- and it should never have existed: NativeLookup is the user-level native-interface mechanism, not something the framework reasons about. Gone, along with the orphaned javadoc the same deletion stranded above writeHealthBindings and the dead Executor.scanningClass hook that walk was the only consumer of. **`ld: framework 'Vision' not found`, the third round of this.** The watch target mirrors the app target's frameworks phase and drops what WATCH_OPTIONAL_FRAMEWORKS names. Last time I audited ByteCodeTranslator's base list and said so; that was the wrong set. IPhoneBuilder's API scan adds more -- Vision, CoreImage, CoreNFC, CoreTelephony, JavascriptCore, AdSupport -- and each only reaches the watch link in a project that uses the feature, so they arrive one CI round at a time. All six are absent from both watch SDKs, checked, and all six are now declared. CN1Vision.m was already `#if !TARGET_OS_WATCH`; as before, only the link list lagged. A deny list is silent about what nobody classified, so it will keep doing this. WATCH_LINKABLE_FRAMEWORKS now names the ones the watch may link, and a test asserts the two lists PARTITION every quoted `*.framework` literal IPhoneBuilder can emit. Adding a framework without classifying it fails that test on the commit that adds it instead of in an iOS job forty minutes later. Verified by removing Vision and watching it fail with the framework named. It cannot see ios.add_libs or the other module's base list, which is stated where it matters. Matching is now case-insensitive: IPhoneBuilder writes "JavascriptCore" with a lowercase s, which resolves only because macOS filesystems are, and an exact match would have missed it. Both SDKROOT branches -- frameworks and raw libraries -- were changed; only fixing the first would have left the dylib arm reading the old way. **#if TARGET_OS_WATCH == 0 excludes the watch** (BuildDaemon#164 r3781548389). `== 0` and `!= 1` are the unary `!` written out, and the same reading already existed for the other platforms, where it means the opposite -- there a false test is the arm the watch DOES compile. Only the unary spelling was recognized here, so a branch that cannot compile on watchOS was kept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f0599e0857
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
watchUsesHealth's javadoc still described the per-root walk that was deleted -- "when the watch has its own watchMain it shakes from its own root and the phone's usage says nothing". The body says the opposite and has since the deletion. A reviewer read the doc and filed the walk's removal as a bug. The doc now states the decision that is implemented: app-wide, with watchNative.health overriding either way. The concern it used to describe is real and still answered -- a non-health watch app in a project whose phone uses HealthKit sets watchNative.health=false rather than having it guessed -- and the reason inference lost is recorded: the walk did not work, and guessing wrong the other way omits the entitlement from a watch app that does use HealthKit, which fails at runtime instead of at signing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…me out ensureAppRunning() calls startActivity() from a service, which Android 10 and later refuse. The comment there was right that nothing is LOST -- messages and removals are spooled before it is attempted -- but wrong by omission about requests: a cold process has no Display and no registered listener, so spoolOrDeliverRequest degraded every reply-bearing request to a plain message and the sender sat out the full 30-second timeout for an answer that provably was not coming. Reachable-but-not-running is the normal state of a phone app whose watch just asked it something, so this was the common case, not the edge. The receiver now says so. A new /cn1/replyerr/<token> path carries a reason back to the asking node, and the requester turns it into the error arm of deliverReply -- the same arm the timeout would eventually have taken, minus the thirty seconds. The request is still spooled and still replays as a plain message on the next launch, which is what it has become by then. Sent BEFORE the spool write, so a process killed mid-write still frees the sender, and best-effort: if it cannot be sent the deadline is still there, which is exactly today's behaviour. Only ever travels between two devices running this bridge -- the Data Layer pairs Android with Wear, and the Apple side answers through WatchConnectivity's own reply handler -- so no iOS or JavaSE change is implied. A peer on an older build does not match the path, ignores the message and waits out its timeout as before, so this degrades rather than breaks. What it is NOT: a way to run the app headlessly. Registering the app's listeners means running app code, and there is no permitted background path to that without a new callback interface. Reporting the truth quickly is the fix that does not invent API. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 79c5580ed1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… no companion
`ld: framework 'VisionKit' not found` -- the fourth of these, and the guard I
added last time did not catch it. It scanned IPhoneBuilder for quoted
"X.framework" literals, but PlatformFeatureCatalog names frameworks BARE and
IPhoneBuilder appends the suffix, so VisionKit and Speech had no literal to
find. Both are absent from both watch SDKs; both are now declared.
The test now reads all three sources a framework can arrive from --
IPhoneBuilder, MapsProviderInjector, and the catalog in the other module -- and
matches the catalog's `.iosFrameworks("VisionKit", ...)` form as well as
literals. Verified by removing VisionKit and watching it fail, naming it.
That verification failed the first two times I ran it, and the reason is worth
recording: WATCH_OPTIONAL_FRAMEWORKS is a static final String, so javac inlines
it into the test class. Editing the list without touching the test left a stale
copy compiled in and the test passed on a list it was no longer reading. The
test now reads both constants reflectively. CI builds clean and would never have
seen this, which is exactly why it was worth ruling out.
Also, the guide's "by default the watch app is a companion" is only true on
Apple Watch (#5487 r3787443009). AndroidGradleBuilder builds the Wear app in the
standalone branch only, and the builder already logs that at build time; the
introduction now says so instead of contradicting its own section 391-398 four
hundred lines later.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
windows-latest failed with three 504s from community.chocolatey.org over about seven minutes, then the verify step correctly refused to continue without ffmpeg. The retry-and-verify hardening did its job -- it named the cause in the log instead of letting the smoke test die later with a FileNotFoundError that mentions neither ffmpeg nor the feed -- but retrying cannot help when the outage outlasts the retry budget. So after the retries, download a build from GitHub instead. That is not a second flaky mirror: if github.com is unreachable the job could not have checked the repository out, so it is the one download whose failure is never a mystery. The directory goes on GITHUB_PATH as well as $env:PATH, because the smoke test runs in a later step and only sees the former. Chocolatey is still tried first, and still preferred when it answers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
When the user touches the watch immediately after onAppear, cn1_watch_runtime_start() has only launched the VM on a background pthread, so cn1WatchJavaLifecycleReady may still be false. The generated Swift gesture is already active, however, and this call reaches IOSImplementation.pointerPressedCallback(), which dereferences the still-null static instance; drag and release have the same issue. Ignore or queue pointer events until the lifecycle-ready flag is set, as the phase-delivery path already does.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
isWatch()existed without a product on top of it. This turns the watch renderslice into a watch app: one setting builds it on both platforms, the two apps can
talk, and you can develop the pair on your desktop.
Bugs this fixes
Wearables are new and nothing depends on them, so these are fixed rather than
preserved:
codename1.watchMainbecame abuild argument only on the local path; the server lifts only
codename1.arg.*keys out of the uploaded settings file, so the daemon asked for
watchMainandgot nothing.
companiondefault never embedded the watch app.watchNative.embedCompaniondefaulted tofalse, so the "Embed Watch Content"phase was actively removed even in companion mode.
watchMainreached only iOS. Wear OS was enabled by an unrelatedandroid.wearhint, so a project had to declare the same intent twice.JavaSEPortnever overrodeisWatch(), so the guide's advice to iterate on a watch layout locally wasuntrue.
onGenericMotionEventread only the mouseaxes; rotary input arrives on
SOURCE_ROTARY_ENCODER/AXIS_SCROLL.so the safe area came back zero.
What is new
One setting.
codename1.watchMainis the entire opt-in on both platforms.Nine build hints are deleted; bundle id, deployment target, team id and display
name are derived.
codename1.watchStandaloneis the only other setting — the onething not inferable from the project. Net new hints: zero.
com.codename1.wearable— the phone↔watch channel, same API on Apple Watchand Wear OS, modelled on
com.codename1.car(portable API, SPI bridge, inertwhen there is nothing on the other end). It exposes the three transports the
platforms actually give us, because picking the wrong one is the usual reason a
watch app "never gets the update":
sendMessagefor a live answer,putDataforstate that survives sleep and relaunch,
transferFilefor bulk. Callbacks arriveon the EDT and are queued across a cold start — the platform starts an app purely
to hand it a payload. Backed by
WCSessionon Apple and the Wearable Data Layeron Android, both gated by API scan so apps that never talk to a watch link
nothing.
A simulator that runs the pair. Four generated watch skins (Apple Watch 41/45,
Wear round, Wear square),
isWatch()and the"watch"override layer, and aWatch menu that launches
watchMainin a second process wired to the first — sosendMessageandputDatagenuinely round-trip on the desktop. Two processes,not two windows:
Displayis a singleton and sharing it would hide the bugs thatonly appear once the pair is real.
Complications as surfaces families. A complication is a WidgetKit widget in
an accessory family, so
WATCH_CIRCULAR/RECTANGULAR/INLINE/CORNERjoinWidgetSizerather than getting an API of their own.The guide, rewritten around the two-app model, with the data-sharing decision
table as its centre.
Not yet done, and stated as such
that render the watch families are not generated yet. The guide says so.
codename1.watchMain, and boots a watch stub. Verified end to end: the watchtarget compiles, links and renders on the watchOS simulator (166 pass / 3 fail
/ 2 skip). Tree-shaking is real -- the phone tree carries 24 generated SVG
classes, the watch tree none -- and that is also the one known regression:
those classes are reached reflectively, so
SVGStatic,SVGAnimatedScreenshotTestandLottieAnimatedScreenshotTestfall back to aplaceholder render. The watch pass needs the same reflective roots the phone
pass keeps. A project whose watch entry point IS the phone main keeps a single
translation and is unaffected.
Verification
CI is the gate. Everything below is what was additionally checked locally before
pushing, and the numbers move as the branch does.
codenameone-maven-plugin: 466 pass, 1 skippedcore-unittests: 4754 pass, with SpotBugs, PMD and Checkstyle at zerotype-check against a stub harness (nothing in CI compiles them) and the
WatchConnectivity native passes
clang -fsyntax-onlyfor iphoneos/arm64--failure-level WARN, Vale, capitalizationbuild-ios-watchdoes run in CI -- it is a job in Test iOS UI buildscripts, on macos-15 with
DEVELOPER_DIRpointed at Xcode 26, and this PRtouches paths that trigger it. It is the real gate for anything that alters
the generated Xcode project. It is not run on the authoring machine, and
during the rapid review cycle several of its runs were cancelled by
supersession, so check it against the CURRENT head rather than an older run.
Server-side half: codenameone/BuildDaemon#watch-apps-product
🤖 Generated with Claude Code