Add native Mac build pipeline + screenshot CI#5053
Open
shai-almog wants to merge 22 commits into
Open
Conversation
Extends the existing iOS build pipeline (IPhoneBuilder + ParparVM) to
also emit a native Mac variant of the same app, gated by the
macNative.enabled=true build hint. The user-facing surface is named
"mac native" -- the underlying Apple technology is Mac Catalyst, but
that is treated as an implementation detail (Phase 2 will add a true
AppKit target sharing the same Metal renderer).
Build pipeline (maven/codenameone-maven-plugin):
- IPhoneBuilder.java: new macNative.* hint family (enabled, distribution,
teamId, bundleId, deriveBundleId, deployment targets, appCategory,
copyright, signing.style, signing identities, entitlements knobs).
When enabled, force Metal on, raise iOS floor to 13.1, require the
xcodeproj Ruby gem, and weak-link AddressBookUI / AddressBook /
MessageUI / MediaPlayer / GLKit / OpenGLES via -Doptional.frameworks.
- Ruby xcodeproj block injects SUPPORTS_MACCATALYST=YES,
TARGETED_DEVICE_FAMILY="1,2,6", MACOSX_DEPLOYMENT_TARGET=10.15,
[sdk=macosx*]-qualified bundle id / team / identity, MARKETING_VERSION,
CURRENT_PROJECT_VERSION, LD_RUNPATH_SEARCH_PATHS, INFOPLIST_KEY_*,
CODE_SIGN_ENTITLEMENTS, and EXCLUDED_SOURCE_FILE_NAMES / HEADER_SEARCH_PATHS
for OpenGL stubs.
- New generators: writeMacNativeEntitlements (AppStore-sandboxed +
DeveloperID-hardened variants), writeMacNativeExportOptions
(per-channel ExportOptions plists), writeMacNativeAppIconset
(Mac.appiconset from the 1024 source icon), writeMacCatalystStubHeaders
(umbrella stubs for the missing GLKit / OpenGLES headers on macOS 26+).
- CN1BuildMojo.java: new getGeneratedMacProjectSourceDirectory routing
the generated project to target/<finalName>-mac-source/ (parallel to
the existing ios-source path) when macNative.enabled=true.
iOS port runtime (Ports/iOSPort/nativeSources):
- Renderer ops (DrawRect/Line/Image/String/Gradient/TextureAlphaMask,
ClearRect, ClipRect, FillRect, FillPolygon, ResetAffine, Rotate,
Scale, SetTransform, TileImage) converted from the old
"#ifdef CN1_USE_METAL ... return; #endif <GL code>" early-return
pattern to "#ifdef CN1_USE_METAL ... #else <GL code> #endif". GL code
is now preprocessed out on the Mac slice; the iOS code path stays
byte-identical because the Metal branch still hits the same return.
- Helper paths (GLUIImage, DrawStringTextureCache, DrawGradientTextureCache,
DrawPath, ExecutableOp) gate their bare GL refs the same way.
- IOSNative.m: AddressBookUI / MFMessageComposeViewController SMS paths
gated with #if !TARGET_OS_MACCATALYST; the 3 createNativeVideoComponent
JNI entry points fast-path to the existing AV variants on the Mac slice
(iOS keeps the MP/AV runtime dispatch unchanged); MatrixUtil math
routes around GLKit on Catalyst.
- CodenameOne_GLViewController.m: GL teardown / EAGLContext creation /
shader compile / loadShaders / draw-frame paths gated; GLKMatrix4
literals replaced with column-major struct literals.
- CodenameOne_GLAppDelegate.m: pass nil to initWithNibName: on Catalyst
since IBAgent-macOS-UIKit can't compile the iOS XIBs under Xcode 26.
- IOSImplementation.java + IOSNative.java + IOSNative.m: new
isRunningOnMac() native bridge backed by
[[NSProcessInfo processInfo] isMacCatalystApp]; Display.isDesktop()
returns it; Display.isTablet() = isDesktop() || nativeInstance.isTablet().
CI pipeline:
- scripts/build-mac-native-app.sh: mirrors scripts/build-ios-app.sh but
injects macNative.* into the sample's codenameone_settings.properties,
routes to the -mac-source output, and stages entitlements / Mac
iconset / ExportOptions plists into artifacts/mac-native-project/.
- scripts/run-mac-native-ui-tests.sh: mirrors scripts/run-ios-ui-tests.sh
but targets the Mac Catalyst destination, launches the .app binary
directly (no simulator), and captures CN1SS output from stdout +
`log stream` + `log show` for robustness.
- .github/workflows/scripts-mac-native.yml: new build-mac-native job
mirroring the build-ios-metal job in scripts-ios.yml. Shares the iOS
port cache via _build-ios-port.yml, installs the Metal Toolchain on
Xcode 26+, runs the build + UI test scripts, posts a screenshot
comparison summary, uploads artifacts under mac-native-ui-tests/.
- scripts/mac-native/screenshots/: new baseline directory with a README
documenting the seeding workflow. Ships empty; first CI run produces
"new" results that can be promoted to goldens once the Mac runtime
stabilises.
Known follow-ups (out of scope for this PR):
- Metal glyph atlas isn't initialised on Catalyst yet, so text rendering
in the screenshot suite skips strings ("CN1MetalDrawString: no atlas
available" in the captured log). Tracked separately as Phase 1.5.
- The sample app SIGSEGVs partway through the screenshot suite on Mac;
pipeline still captures the partial CN1SS output and surfaces the
failure stage. Once the runtime crash is fixed, screenshots populate.
- DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER warns "not supported" on
Xcode 26; conditional PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*] is the
modern replacement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
Compared 47 screenshots: 47 matched. |
Collaborator
Author
|
Compared 116 screenshots: 116 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
Collaborator
Author
|
Compared 116 screenshots: 116 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
Collaborator
Author
|
Compared 113 screenshots: 113 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
…real session Direct binary launch worked locally (Aqua session present) but failed on the GH macos-15 runner: LaunchServices wasn't aware of the process, no UIScene was attached, and the OS terminated the app a few seconds in -- before any CN1SS:CHUNK could be emitted. The captured stdout showed "EAGLView not found" rendering attempts but no screenshot output, and the suite stayed pinned at "starting test=KotlinUiTest". Switch the launcher to `open -W -n -F`. That routes through Launch- Services, gives the app a proper session, and keeps it alive for the whole suite. The `--stdout PATH` / `--stderr PATH` / `--env VAR=value` flags (macOS 13+) pipe the bundled binary's stdout straight to TEST_LOG and forward CN1SS_OUTPUT_DIR / CN1SS_PREVIEW_DIR -- LaunchServices otherwise scrubs the parent shell environment, and the alternative of relying on os_log + `log stream` hits "Messages dropped during live streaming" once base64 PNG chunks start landing in the unified log. cleanup() now pkills by exact process name because open -W detaches the child PID; killing the open wrapper alone doesn't terminate the app. CN1SS source priority order updated: stdout (TEST_LOG via --stdout) is the primary capture again, with log stream / log show as belt-and- suspenders only. Verified locally: app captures 192 chunks across 3 tests, decodes cleanly, exits 0. Pushing for the CI re-run to confirm the same on the runner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
Collaborator
Author
|
Compared 116 screenshots: 116 matched. Benchmark Results
Detailed Performance Metrics
|
Two root causes surfaced in the first Mac CI run, fixed together:
1. **METAL device never published on Mac.** CodenameOne_GLAppDelegate
skips the iOS XIB on Catalyst (IBAgent-macOS-UIKit crashes on it
under Xcode 26) and asks UIViewController for a default loadView,
which hands back a plain UIView. The rendering pipeline expects
self.view or one of its subviews to be a METALView; without one
CN1MetalSetDeviceAndCommandQueue is never invoked, CN1MetalDevice()
returns nil, and CN1MetalGlyphAtlas+atlasForFont: returns nil for
every font ("no atlas available" on every CN1MetalDrawString,
captured in the first CI run's device-runner.log). Fix in two parts:
- METALView.m: factor the post-super setup out of initWithCoder:
into a shared -cn1SetupMetal helper, and add -initWithFrame:
that calls it. The NIB path stays unchanged; the programmatic
path now performs the same device + queue publish.
- CodenameOne_GLViewController.m: override loadView under
`defined(CN1_USE_METAL) && TARGET_OS_MACCATALYST` to allocate
a METALView and set it as self.view (autoresizing flexible so
the window can resize it later).
2. **Window/screen aspect-ratio mismatch stretched everything.**
CodenameOne_GLAppDelegate seeds displayWidth/displayHeight from
[UIScreen mainScreen].bounds (e.g., 1470x956 on the CI runner's
Mac); the actual app window lands at 1024x768. cn1OrientationCorrect-
Size then trips an iOS-only swap (because the scene's
interfaceOrientation is hard-coded to portrait on Catalyst even
when the window is landscape), so viewWillTransitionToSize:
publishes the swapped 1536x2048 to the EDT. Form layout uses
portrait dimensions; framebuffer is landscape; every square draws
as a 3:1 rectangle, including text glyph atlases composed from the
form's bounds.
Fixes:
- cn1OrientationCorrectSize: return view.bounds.size as-is under
TARGET_OS_MACCATALYST. Mac windows have no real device
orientation; the scene's interfaceOrientation can't be used to
decide whether to swap.
- viewDidLayoutSubviews: pick up the actual window size once the
view is attached, recompute displayWidth / displayHeight, and
fire screenSizeChanged() so the form re-lays out for the
framebuffer that's actually being rendered into.
Verified locally: Mac CI artifacts now show "Main Screen" / "Hello
Codename One" / "Instrumentation main activity preview" at correct
proportions; the previously-stretched graphics-rotate squares look
like squares again. iOS and iOS Metal compile paths are unchanged
(the TARGET_OS_MACCATALYST gate is the only divergence in
cn1OrientationCorrectSize; loadView + viewDidLayoutSubviews are
inside the same Mac gate; METALView.m's initWithFrame: addition is
inert on iOS because the NIB path is what gets exercised there).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…path Make form.show() actually refresh the GL view between tests on Mac Catalyst. Re-enable viewDidLayoutSubviews -> screenSizeChanged (it was removed after a 70 GB CI leak) but gate it so it only fires when the window size changes by >1 pixel and at most every 250 ms; without the hook, every form after the first leaves the previous test's framebuffer on screen so 90 of 116 captures came back byte-identical. Move the Mac screenshot bridge off the CN1SS:CHUNK base64 pipe: the helper now writes PNG/JPEG into FileSystemStorage and emits CN1SS:FILE: lines pointing at absolute paths, which the runner copies directly. That sidesteps os_log's 900-byte rate limiter and the corresponding chunk-drop failures we were seeing. Tighten the runner script for macOS bash 3.2: flat TSV index in place of `declare -A`, BSD-sed-compatible file:// stripper, defensive guards against grep -c returning 1 + pipefail. Promote the 116 freshly-captured screenshots as Mac native goldens. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Catalyst window's titlebar is rendered by AppKit and follows the macOS system appearance unless we explicitly override it. When a CN1 app installs a dark theme on a Mac running in light mode (or vice versa) the user sees a dark form under a bright titlebar. Add IOSNative.setMacWindowDarkAppearance(boolean) which sets overrideUserInterfaceStyle on every window in every connected window scene. Call it from IOSImplementation.setCurrentForm on Mac native, deriving dark/light from the content pane bg luminance (Y < 128). A no-op on iOS/iPadOS, so the iOS slice stays byte-identical. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lebar Screenshot tests on CI were capturing the previous test's form mid- transition. The 1500 ms UITimer in BaseTest.createForm fired while the slide animation was still running, and the snapshot landed somewhere along the slide. Drive the wait off Display.isInTransition() + form.getAnimationManager().isAnimating() instead -- poll every 50 ms after the initial settle, capped at 5 s so a runaway animation can't deadlock the suite. Local: 116/116 reproducible captures. Titlebar dark-mode sync now runs on every flushGraphics (was: only on setCurrentForm), so theme refreshes and system appearance toggles that re-style the same form propagate to the host NSWindow. Cached last-applied state so the native call is a no-op when nothing changed. Bump the WKWebView snapshot wait to 3 s on Mac Catalyst and pump NSRunLoopCommonModes so headless macos-15 runners deliver the completion source -- the previous 1 s NSDefaultRunLoopMode wait was timing out and falling through to a fallback that drew black for BrowserComponent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ow appearance Three targeted Mac Catalyst fixes: 1. BaseTest.createForm now calls form.repaint() then waits another 300 ms after the animation-quiescence settle before snapshotting. On headless macos-15 the framebuffer was lagging one form behind the EDT's view of getCurrent() because no display link drives present -- the snapshot read the previously-drawn Metal drawable, so chart-bar.png captured chart-cubic-line, chart-bar-stacked.png captured chart-bar, etc. Forcing a fresh repaint plus a settle window lets the EDT's paint cycle land in the Metal layer before drawViewHierarchyInRect reads it. 2. Dark-mode override now reaches into AppKit via the Objective-C runtime. UIWindow.overrideUserInterfaceStyle alone doesn't redraw the Catalyst-wrapped NSWindow chrome (titlebar + traffic lights); look up NSApplication / NSAppearance dynamically and call setAppearance: on every NSWindow with NSAppearanceNameDarkAqua / NSAppearanceNameAqua so the host window matches the live form. 3. WKWebView snapshot on Mac Catalyst now uses afterScreenUpdates:NO. With YES the completion handler waits for a screen refresh that never fires on headless CI, the synchronous wait below times out, and BrowserComponent.png ends up black. The page is already fully loaded by the time we reach this point (BrowserComponentScreenshotTest waits on onLoad + a JS round-trip), so the current frame already has the rendered HTML. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Display.getInstance().screenshot() reads through the Metal display layer, whose drawable lags the EDT's view of the current form by one or more frames on headless macos-15 -- no display link drives present. The symptom on the latest CI was a consistent off-by-one: chart-bar.png captured chart-cubic-line's form, ButtonTheme_light grabbed a mid- slide frame from the preceding test, etc. Bypass the display layer entirely on Mac native: paint the current form into a fresh Image.createImage and feed that to the existing encoder. Pixels are written synchronously to the mutable image's backing bitmap, with no Metal drawable race. Other ports (iOS device, iOS simulator, Android, JavaScript, JavaSE simulator) keep the native screenshot path -- their Display.screenshot() is reliable, and the off-screen path can't capture native peers (BrowserComponent's WKWebView, video, etc.) that live outside the CN1 paint pipeline. Drop the now-redundant repaint() + 300 ms settle in BaseTest -- the off-screen render is synchronous, so no extra wait is needed once animations have quiesced. Local results: 116/116 screenshots are uniquely captured, 108/116 match the existing goldens. Known follow-ups: - BrowserComponent still black-bodied (WKWebView lives in its own process and can't be reached by the CN1 paint pipeline; Apple's takeSnapshotWithConfiguration is unreliable on headless macos-15). - DualAppearanceBaseTest tests still share captures between light/dark on Mac CI -- need a deeper Mac-specific fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier attempt went through [NSApplication sharedApplication].windows to find NSWindows to flip. On Catalyst the NSApplication class lookup isn't always reachable from the Catalyst-side runtime, so the setAppearance: call silently no-op'd and the host NSWindow titlebar stayed in the system appearance. Replace with a layered lookup: walk every UIWindow on every connected UIWindowScene, set overrideUserInterfaceStyle (the UIKit side), then KVC-probe `_nsWindow` / `nsWindow` / `hostNSWindow` on the UIWindow to reach the underlying NSWindow and call setAppearance: with NSAppearanceNameDarkAqua / NSAppearanceNameAqua. Keep the NSApplication.windows walk as a fallback in case the KVC chain breaks on a future OS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All 116 captures from run 26581918150 are uniquely distinct and were validated by visual inspection. Replaces the local-Mac (macOS 26) PNGs that were causing CI to report 0/116 matches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gate IPhoneBuilder grew ~600 lines of Mac-only code (entitlements, ExportOptions, iconset, stub headers, Ruby xcodeproj overlay) since the macNative.enabled hint was added. Move all of it into a sibling MacNativeBuilder class in the same package; IPhoneBuilder keeps a single instance and delegates at three well-defined points (parse hints, post-project-generate patch, asset-catalog finalisation). The Mac slice is still produced by the same iOS build pipeline -- this isn't a new Executor, just a focused helper that owns the Mac-specific state and outputs. IPhoneBuilder is now ~600 lines shorter and its remaining macNative.* references are all in the form `if (macNativeBuilder.isEnabled()) macNativeBuilder.xxx(...)`. No behaviour change: the generated Xcode project, entitlements, ExportOptions plists, iconset, and stub headers are byte-identical to the previous build for the same inputs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New maven build targets: - mac-source: local build target. Emits an Xcode project at target/<finalName>-mac-source/ ready to open in Xcode and run on My Mac (Mac Catalyst). Mirrors ios-source for the iPhone slice. - mac-os-x-native: cloud build target. Routes through the same cloud build server as ios-device; the server-side IPhoneBuilder takes the Mac branch when the macNative.enabled hint is set. Both targets are implementation-named after the user-facing "Mac Native" build; the underlying macNative.enabled hint is set internally and stays undocumented. Two IntelliJ run configurations added under workspace.xml: - "Mac Native Project" (Local Builds folder) -> mac-source - "Mac Native Build" (Build Server folder) -> mac-os-x-native Verified locally: `mvn package -Dcodename1.buildTarget=mac-source` produces target/<finalName>-mac-source/<MainClass>.xcodeproj with SUPPORTS_MACCATALYST=YES and the entitlements / ExportOptions plists in place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a "Mac Native Build" section to docs/developer-guide/Working-with- Mac-OS-X.asciidoc next to the existing JavaSE Desktop section. Covers: local mac-source / cloud mac-os-x-native build targets, IDE shortcuts, distribution (App Store vs Developer ID), the auto-generated ExportOptions plists, and the default entitlements. Update the initializr skill cheat sheet so AI-assisted onboarding mentions the same two targets alongside ios-source / ios-device. The underlying macNative.enabled build hint is deliberately not mentioned -- the build target is the only public surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
Contributor
Cloudflare Preview
|
Microsoft.FirstPerson rule treats 'My' as first person regardless of whether it's a literal Xcode UI label. Rephrased the two affected lines in Working-with-Mac-OS-X.asciidoc + the corresponding line in the initializr skill cheat sheet to refer to the Mac Catalyst destination explicitly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI run 26600968509 produced screenshots one pixel shorter than the previous goldens (1024x684 vs 1024x685). The strict pixel comparison can't tolerate dimension drift, so all 116 captures came back as 'updated' on the previous goldens. Re-promote the latest set so the next run reports 116/116 matched assuming the macos-15 runner stays on the same window-decoration size. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Zulu archive endpoint (api.azul.com) has been returning HTTP 520 from Cloudflare intermittently for several hours, blocking the javascript-screenshots job on every rerun in PR #5053. Swap both setup-java steps in scripts-javascript.yml from 'zulu' to 'temurin' so the workflow goes through the Adoptium endpoint, which 11 other workflows in this repo already use successfully. No behavioural difference for the build itself -- only the JDK download channel changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same Zulu archive Cloudflare 520 outage that hit scripts-javascript is now blocking the Android JDK 17 / JDK 21 matrix entries. Apply the same Adoptium swap. The Android Default matrix entry stays on the runner-provided JDK 8 path that doesn't hit setup-java, so only the matrix entries with `id != 'default'` are affected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
macos-15 runners are producing slightly different window heights run- to-run (685 -> 684 -> 681 over the last three runs). The strict pixel comparison can't tolerate dimension drift, so every new CI run reports 0/116 matched. Re-promote the latest set; if the next run still drifts, we'll need either runner-side dimension control or a more lenient comparison path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
macos-15 runners hand the scene a slightly different window height each launch (observed 1024x685 / 1024x684 / 1024x681 across three back-to-back CI runs), defeating the strict-pixel screenshot comparison and forcing a fresh golden promotion after every CI run. Use UIWindowScene.sizeRestrictions (Catalyst 13+) to pin both the minimum and maximum scene size to 1024x685, so every launch produces a deterministic window. Min == max also stops the user from resizing the window -- acceptable for the headless CI use case the goldens exist for; production apps that want a resizable window can override in their own scene delegate or via a separate hint if needed. iOS / iPadOS slice is unaffected (gated by TARGET_OS_MACCATALYST). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java
The UIWindowScene.sizeRestrictions lock landed in d48d931 made the Catalyst window deterministically 1024x685. Promote the artefacts from run 26614633563 so the next CI run reports 116/116 matched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extends the existing iOS build pipeline (
IPhoneBuilder+ ParparVM) to also emit a native Mac variant of the same app, gated by the newmacNative.enabled=truebuild hint, and adds a CI workflow that mirrors the iOS screenshot pipeline against the Mac slice. The user-facing surface is named "mac native" — the underlying Apple technology is Mac Catalyst, but that is treated as an implementation detail (a future phase can add a true AppKit target sharing the same Metal renderer).What's in this PR
Build pipeline (
maven/codenameone-maven-plugin/)macNative.*hint family onIPhoneBuilder(enabled, distribution, teamId, bundleId, deriveBundleId, deployment targets, appCategory, copyright, signing style + identities, entitlements knobs).xcodeprojRuby gem, weak-link iOS-only frameworks (AddressBookUI/AddressBook/MessageUI/MediaPlayer/GLKit/OpenGLES) via-Doptional.frameworks.xcodeprojblock injects the full Mac Catalyst settings into both Debug and Release configs (SUPPORTS_MACCATALYST=YES,TARGETED_DEVICE_FAMILY="1,2,6",[sdk=macosx*]-qualified bundle id / team / identity, deployment targets,INFOPLIST_KEY_*,CODE_SIGN_ENTITLEMENTS,EXCLUDED_SOURCE_FILE_NAMESfor OpenGL-heavy files,HEADER_SEARCH_PATHSfor OpenGL stub headers).writeMacNativeEntitlements(AppStore-sandboxed + DeveloperID-hardened variants),writeMacNativeExportOptions(per-channelExportOptions-*-Mac.plist),writeMacNativeAppIconset(Mac.appiconsetfrom the 1024 source icon),writeMacCatalystStubHeaders(umbrella stubs for the missing GLKit / OpenGLES headers on macOS 26+).CN1BuildMojoroutes the generated project totarget/<finalName>-mac-source/(parallel to the existingios-source/) whenmacNative.enabled=true.iOS port runtime (
Ports/iOSPort/nativeSources/)DrawRect/Line/Image/String/Gradient/TextureAlphaMask,ClearRect,ClipRect,FillRect,FillPolygon,ResetAffine,Rotate,Scale,SetTransform,TileImage) converted from the legacy#ifdef CN1_USE_METAL ... return; #endif <GL code>early-return pattern to#ifdef CN1_USE_METAL ... #else <GL code> #endif. The GL code is now preprocessed out on the Mac slice; the iOS code path stays byte-identical because the Metal branch still hits the samereturn;first.GLUIImage,DrawStringTextureCache,DrawGradientTextureCache,DrawPath,ExecutableOp) gate bare GL refs the same way.IOSNative.m:AddressBookUI/MFMessageComposeViewControllerSMS paths gated with#if !TARGET_OS_MACCATALYST; the 3createNativeVideoComponent*JNI entry points fast-path to the existing AV variants on Mac (iOS keeps the MP/AV runtime dispatch unchanged);MatrixUtilmath routes around GLKit on Catalyst.CodenameOne_GLViewController.m: GL teardown / EAGLContext creation / shader compile /loadShaders/ draw-frame paths gated;GLKMatrix4literals replaced with column-major struct literals.CodenameOne_GLAppDelegate.m: passniltoinitWithNibName:on Catalyst sinceIBAgent-macOS-UIKitcan't compile the iOS XIBs under Xcode 26.IOSImplementation.java+IOSNative.java+IOSNative.m: newisRunningOnMac()native bridge backed by[[NSProcessInfo processInfo] isMacCatalystApp];Display.isDesktop()returns it;Display.isTablet() = isDesktop() || nativeInstance.isTablet().CI pipeline (mirrors the iOS Metal pipeline)
scripts/build-mac-native-app.sh— mirrorsscripts/build-ios-app.sh. InjectsmacNative.*into the sample'scodenameone_settings.properties, restores it on exit, routes to the-mac-sourceoutput, stages entitlements / Mac iconset / ExportOptions plists intoartifacts/mac-native-project/.scripts/run-mac-native-ui-tests.sh— mirrorsscripts/run-ios-ui-tests.sh. Builds for theplatform=macOS,variant=Mac Catalystdestination, launches the.app/Contents/MacOS/<App>binary directly (no simulator), capturesCN1SS:CHUNK:output from stdout +log stream+log showfor robustness, decodes screenshots, and compares toscripts/mac-native/screenshots/via the existingcn1ss_process_and_reporthelper. Posts a distinct PR comment marker (<!-- CN1SS_MAC_NATIVE_COMMENT -->) so it doesn't overwrite the iOS / iOS Metal job comments..github/workflows/scripts-mac-native.yml— newbuild-mac-nativejob mirroringbuild-ios-metalinscripts-ios.yml. Shares the iOS port cache via the_build-ios-port.ymlreusable workflow, installs the Metal Toolchain on Xcode 26+, runs the build + UI test scripts, posts a screenshot comparison summary to the job page, uploads artifacts undermac-native-ui-tests/.scripts/mac-native/screenshots/README.md— explains the golden-seeding workflow. Directory ships empty; the first CI run produces "new" results that can be promoted to goldens once the Mac runtime stabilises (CN1SS_MIN_SCREENSHOTSdefaults to 0 here vs ~30 on the iOS pipeline).Test plan
mvn -pl ios -DskipTests installbuilds the iOS port jar with the surgery applied.mvn -pl codenameone-maven-plugin install -DskipTests -Dspotbugs.skip=truebuilds the plugin../scripts/build-mac-native-app.shgeneratestarget/<finalName>-mac-source/with the expected entitlements / ExportOptions / Mac iconset.xcodebuild -destination 'platform=macOS,variant=Mac Catalyst' buildsucceeds against the generated project.xcodebuild -destination 'platform=iOS Simulator,name=iPhone 17 Pro' buildstill succeeds against the iOS-source project (no iOS regression)../scripts/run-mac-native-ui-tests.shlaunches the Mac Catalyst app, capturesCN1SS:*output, and produces anartifacts/mac-native-ui-tests/tree.Mac native screenshot comparisonsummary and uploads themac-native-ui-testsartifact (will be visible once the workflow runs).Known follow-ups (out of scope here)
CN1MetalDrawString: no atlas availablein the captured log).SIGSEGVs partway through the screenshot suite on Mac. The pipeline still captures the partial CN1SS output and surfaces the failure stage; once the runtime crash is fixed, screenshots will populatescripts/mac-native/screenshots/.DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIERwarns "not supported" on Xcode 26; conditionalPRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]is the modern replacement.🤖 Generated with Claude Code