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 280 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: e99b7e550c
ℹ️ 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".
The Cloudflare Pages step required both an immutable URL and a "Deployment alias URL:" line in wrangler's output, and failed the job when either was absent. wrangler does not print the alias line reliably -- one deploy of this branch printed it and the next, to the same preview branch, did not -- while the upload, the alias and the deployment had all succeeded. The job went red over a line of console output. The alias still existed: the branch URL this build could not scrape serves the site. PREVIEW_BRANCH is built from a PR number, so it is always lowercase, hyphenated and well inside the 28 characters Cloudflare truncates an alias to, and the URL can simply be rebuilt from it. Only accepted if it responds; otherwise the immutable deployment URL is used and a warning is emitted, since that is a working preview link too. A missing immutable URL is still fatal -- there is nothing to fall back to. 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: cd639b4b63
ℹ️ 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".
requestReplayAfterDrain ran the replay inline whenever a data listener was registered, including from inside the drain it would re-enter. Past MAX_PENDING tracked transfers that recursed: the re-offer parked, parking evicted a one-shot, the eviction asked for a replay, and the listener was still there so it ran -- a frame deeper each time. It now defers while a drain is in flight or anything is parked, and drainPending will not finish with a request outstanding, since a replay that parked nothing would otherwise be stranded. That is not the same question as "has anyone started listening", and the Android spool needs the second one. Its drain runs in the bridge constructor, before the app's init() has registered anything, so a cold launch correctly leaves every record on disk -- and nothing but inbound traffic ever re-ran it, so with no new traffic the previous run's one-shot messages and removals stayed there. Asking through requestReplayAfterDrain cannot fix it: a spooled MESSAGE record on a process holding only a DATA listener would drain, find nothing deliverable, re-arm and run again without bound. runWhenListenerRegisters never runs inline and fires from both registrations. Two in the generated script. A disjunction with a watchOS operand is unconditionally true on the watch, so `#if os(watchOS) || FEATURE` selects that arm however FEATURE evaluates; leaving it undecided kept an #else that imported an iOS-only product into the watch target. And a regression from the parenthesis normalizer added last round: it stripped the directive before the selection test, so `#ifndef TARGET_OS_WATCH` -- FALSE on the watch, whose arm is the #else -- read as a positive watch test and the watch's own arm was suppressed. Definedness is read from the raw text now, ahead of any normalization, and never selects. 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: 37365dd87b
ℹ️ 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".
A message the port has written down was parked as an ordinary Runnable, which is what evictOne discards FIRST and silently. A spool drain that queued more than the cap while a listener existed, and then lost that listener before the EDT ran the batch -- an app deregistering on pause -- had every re-parked message reach that path. Neither callback fired: not delivered, because nothing was, and not dropped, because there was none. The record stayed on disk still marked in flight, unclaimable for the life of the process, having spent an attempt from a budget meant to bound poison payloads. Such a message is a one-shot by definition -- this process holds the only in-memory copy and the record behind it is claimed -- so it is marked as one, which also moves it behind everything replaceable in the eviction order, and it carries a dropped callback. The re-park keeps both; it used to strip them. The removal path had the same hole by another route. An evicted removal is re-announced, so the LISTENER recovers, but that re-announcement goes through the plain entry point carrying no callback, so a port holding the removal durably heard neither outcome. Replicated carries the release too now, returned from both eviction branches -- superseded included, since superseded is still not delivered. On the port side unclaimSpooled gives the record back without deleting it, and refunds the attempt. The budget bounds a record that keeps killing the process; an eviction is an observed event on a process that is plainly still alive, and charging for it is what let a burst past the cap delete a record that had never once been seen. 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: 07fd3ee194
ℹ️ 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".
Routing both message call sites through the form that carries onDropped left the three-argument deliver with no callers, and SpotBugs is a zero-findings gate -- UPM_UNCALLED_PRIVATE_METHOD failed build-test (8). Its javadoc explained why a delivery is parked rather than dropped, which is still worth saying, so it moves onto the overload that survives. The gate command in CLAUDE.md is the reason this was reported clean locally. core-unittests is declared inside the unittests profile, so -am never reaches it from the android/ios/plugin invocation the file documents, and the spotbugsXml.xml left in its target is whatever an earlier run wrote. Reading that file after the documented command answers a question about some previous build. The missing invocation is now documented next to it, along with the fact that the module analyses the CORE classes -- so removing a caller anywhere in core can make a private method dead without the phone or plugin analysis ever mentioning it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… be read A truncated or bad-Base64 spool entry left replaySpooled without calling any of its completion callbacks, and claimSpooled had already put the key in SPOOL_IN_FLIGHT. spoolBusy() then stayed true for the life of the process, so deliverableNow answered no to everything after it and every reply-bearing request was downgraded to a plain spooled message whose sender timed out. One unreadable entry disabled live requests entirely. Such a record is now discarded, which means deleted and unclaimed rather than abandoned where it sits, and the contract that every exit runs exactly one callback is written down. Apple Watch published no safe area at all. getDisplaySafeArea() reads globals that viewSafeAreaInsetsDidChange fills, and the watch host does not run it, so every layout was handed the full rectangle and corner controls sat under the rounded bezel. The host derives and publishes insets when the surface starts or resizes, from the corner ratio the generated skins are drawn with so the simulator and the device agree. A rounded RECTANGLE, not the inscribed circle a round Wear face needs -- that rule would throw away a third of the screen. On Wear, the API 23-27 branch applies the round inset from a posted callback, after the form is already laid out, and the surface callback that would re-lay it returns early on unchanged dimensions. It now revalidates when the rectangle moved, as the API 28 branch does. On a round face this branch is the only source of inset, so it always moved and was always ignored. 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: 7aa95e0ed5
ℹ️ 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".
…tring The companion passed -Dskin to guarantee it comes up on a watch skin at all, which CN.isWatch() and the "watch" override layer both depend on. But a reload runs the same code in the same JVM and a permanent property outranks the preference the skin menu writes, so selecting Apple Watch 41mm, Wear Round or Wear Square saved the choice, reloaded, and came straight back up on 45mm -- which made every watch form factor except the default unusable. The property is cleared after the first load: loadSkinFile has just written that skin to the preference, so a reload with no choice made still finds a watch skin, and dskin stays set so hasSkins() is unaffected. The watch HealthKit gate collapsed the detected usage to one boolean and then accepted either purpose string. Apple wants the disclosure for the operation: a watch that only reads and declares only NSHealthUpdateUsageDescription is refused at authorization exactly as if it had declared nothing. Read and write are now carried separately from the same root the entitlement decision uses, each requiring its own string, and the error names the one that is missing rather than offering a choice that does not exist. A hint-only answer still has no direction, so either string remains acceptable there. Basic settings declared a four-row grid for nine fields. GridLayout takes its preferred height from the declared count, so the row the watch and TV entry points forced was compressed or clipped. 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: 6a14dbd110
ℹ️ 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".
bondedNodeIds() asks the capability client, so it lists peers that actually have this app; connectedNodes() is the unscoped Data Layer list and includes any watch paired to the phone. Seeding the quorum from the first and then adding the second back put devices in it that cannot publish an acknowledgement even in principle -- no app, no listener, nothing to write one -- so allTook never became true and a file every real recipient had taken stayed published until the seven-day hard cap. Not asked yet is a different answer from nobody has it, and an empty quorum retires a transfer nothing acknowledged. The sweep runs on the transfer worker, which may block, so bondedNodeIds() has just made the blocking query by the time the quorum is built -- if bondedKnown is still false that query failed, and the sweep defers to the coalescing interval instead of deciding on nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An iOS AppIcon set declares iPhone and iPad idioms, which is why it is filtered out of the catalog staged for the watch -- and nothing replaced it, so every watch product built here shipped with no icon. That does not affect building, running or testing; it fails App Store submission, which is the one place it cannot be worked around, and it left the standalone watch product unshippable without hand-editing the generated project. The icon is scaled from the project's own, as the phone's is, into a WatchImages catalog the phone never sees. One 1024x1024 image in the single-size form Xcode has accepted since 14: the per-device idiom list it replaces has to enumerate every watch size ever shipped and needs a new entry for each new one, and the deployment floor here is watchOS 10. Compiling the catalog is not enough on its own -- actool promotes a set to the app icon only when the target names it -- so the watch target now sets ASSETCATALOG_COMPILER_APPICON_NAME. 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: 945bce5c1a
ℹ️ 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".
The Apple models were 352x430 and 396x484, labelled logical points and in fact Retina pixels. The watch host reports points -- scaleValue is 1 on that slice, so getDisplayWidthImpl returns CN1WatchRenderingView.logicalWidth() unscaled -- so the simulator offered twice the space on each axis and a layout that fitted in it could reflow or clip on the device. 176x215 and 198x242 are what the device gives you. The corner radius follows, as a ratio rather than a count: the skins are no longer in one coordinate space, with the Apple models in points and the Wear ones in pixels, so a fixed 28 was twice the drawn corner on one of them. The ratio is the one CN1WatchHost derives the device safe area from, which is why the regenerated 45mm skin advertises the same five-point inset the watch does. Bezel margins are proportional now too; 70/90 was chosen against a 396-wide model and framed a 198-wide one in a third of an image of border. Helvetica is what makes loadSkinFile set its internal isIOS flag, whatever platformName says, so a Wear skin declaring it resolved native:* faces through the iOS font candidates -- Apple typography and Apple metrics in a preview of an archive carrying the Android Material theme. The Wear models ask for Roboto. 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: b937b02ae9
ℹ️ 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".
…the docs writeWatchAppIcon puts the catalog in <Main>-src, the same place the watch Info.plist and the staged watch sources go. The script looked for it beside the project, which is one level up in dist, so the directory test failed silently: the catalog was never added to the watch's resources phase and ASSETCATALOG_COMPILER_APPICON_NAME named a set the target did not contain. The icon fix therefore did nothing until now. The path convention is pinned by a test, because a wrong string in generated ruby has no compiler to catch it. Two documents describing behaviour this branch has already changed. The guide told the reader no watch icon is generated and to add an AppIcon set by hand, which would now mean replacing a valid generated catalog. WATCHOS_PORT.md still said the watch compiles the phone's single translation and that watchMain is not the entry point -- so a maintainer diagnosing missing watch output would look at the Core Graphics backend when the honest answer is now that the watch root may simply not reach the code, and would put watch UI in the phone lifecycle where it will not run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Publishing a real safe area on the watch changed where bottom-anchored content sits, which is the entire point of it: getDisplaySafeArea() used to return the full rectangle, so a component honouring it drew into the rounded bezel. Nine tests moved and 208 did not, and the nine are all bottom-anchored -- the lightweight pickers, the sheets, the tab bars. That distribution is the evidence this is the intended change rather than a rendering regression: nothing that ignores the safe area moved at all. Checked rather than accepted. TabsTheme_light now sits the tab labels above the curve instead of on it, and the clipped glyph fragments the old golden carried just above them -- content drawn where the bezel eats it -- are gone. Sheet is inset the same way with its content intact. Recorded here rather than refreshed quietly, because a golden that changes for a reason nobody wrote down is indistinguishable from one that regressed. 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: e4d3ebb95e
ℹ️ 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".
… its say The API 28+ branch tested for a change before applyRoundScreenInset ran, and that runs at the end of the method for every branch. On a round watch reporting no cutout and no system bars the branch concluded nothing had moved -- its own guard skips it when all four insets are zero -- and the round inset then changed all four with no revalidation asked for. The form stayed sized to the full rectangle with its corners under the bezel, which is the fault the API 23-27 branch already had fixed one round ago and this one still had. There is now a single comparison at the end, against a snapshot taken before any branch runs, so the round inset is part of the question rather than applied after it was answered. The posted API 23-27 branch keeps its own: it runs after the method has returned, so the end-of-method test has already happened. 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: 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>
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