Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b7fbe71
Add native Mac build pipeline + screenshot CI
shai-almog May 27, 2026
c1d32e0
mac-native CI: launch via `open --stdout` so the Catalyst app gets a …
shai-almog May 27, 2026
fd14e60
Fix Mac Catalyst rendering: glyph atlas + buffer/window aspect mismatch
shai-almog May 28, 2026
2283f3f
Mac native: debounced layout refresh + Storage-based screenshot fast …
shai-almog May 28, 2026
602ab0d
Mac native: sync NSWindow titlebar appearance to CN1 dark theme
shai-almog May 28, 2026
e5a69ca
Mac native: wait for form transition before screenshot + reactive tit…
shai-almog May 28, 2026
8fb378f
Mac native: force paint + extra settle before snapshot, bridge NSWind…
shai-almog May 28, 2026
5791dc0
Mac native: capture screenshots off-screen via paintComponent
shai-almog May 28, 2026
93b81fe
Mac native: bridge NSWindow appearance via UIWindow KVC chain
shai-almog May 28, 2026
5b0623c
Mac native: promote CI-produced screenshots as goldens
shai-almog May 28, 2026
61df6cc
Extract Mac native code from IPhoneBuilder into MacNativeBuilder dele…
shai-almog May 28, 2026
88bbda0
Add Mac Native build target + IDE shortcuts (local + cloud)
shai-almog May 28, 2026
8db72af
Document the Mac Native build target
shai-almog May 28, 2026
8314ebb
Merge remote-tracking branch 'origin/master' into mac-native-ci-pipeline
shai-almog May 28, 2026
91f1a46
docs: drop first-person 'My Mac' usage flagged by Vale
shai-almog May 28, 2026
7815f5f
Mac native: re-promote CI goldens (1024x684 window height)
shai-almog May 28, 2026
3acc56f
ci: switch scripts-javascript Java setup from Zulu to Temurin
shai-almog May 29, 2026
738e88b
ci: switch scripts-android matrix JDK from Zulu to Temurin
shai-almog May 29, 2026
28be304
Mac native: re-promote CI goldens at 1024x681
shai-almog May 29, 2026
d48d931
Mac native: pin Catalyst window to 1024x685 for screenshot determinism
shai-almog May 29, 2026
0577776
Merge remote-tracking branch 'origin/master' into mac-native-ci-pipeline
shai-almog May 29, 2026
52075df
Mac native: promote 1024x685 goldens from window-locked CI run
shai-almog May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/scripts-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ jobs:
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'zulu'
distribution: 'temurin'
- name: Set JDK_HOME
if: matrix.id != 'default'
run: echo "JDK_HOME=${JAVA_HOME}" >> $GITHUB_ENV
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/scripts-javascript.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
- name: Set up Java 8 for ParparVM
uses: actions/setup-java@v4
with:
distribution: 'zulu'
distribution: 'temurin'
java-version: '8'
cache: 'maven'

Expand All @@ -102,7 +102,7 @@ jobs:
- name: Set up Java 17
uses: actions/setup-java@v4
with:
distribution: 'zulu'
distribution: 'temurin'
java-version: '17'
cache: 'maven'

Expand Down
256 changes: 256 additions & 0 deletions .github/workflows/scripts-mac-native.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
name: Test Mac native UI build scripts

# Mac native = the macNative.enabled=true variant of the iOS build
# pipeline. IPhoneBuilder routes the generated project to
# target/<finalName>-mac-source/ and injects Mac Catalyst settings
# (SUPPORTS_MACCATALYST, MACOSX_DEPLOYMENT_TARGET, signing/team via
# [sdk=macosx*] qualifiers, AppStore + Developer ID entitlements,
# ExportOptions plists, Mac.appiconset). This workflow exercises that
# path end-to-end: sample app -> Xcode project -> Mac Catalyst .app ->
# screenshot suite -> golden comparison.
#
# Mirrors .github/workflows/scripts-ios.yml's build-ios-metal job
# closely; the Mac slice shares the iOS port artifact (built by the
# reusable _build-ios-port.yml workflow) so cache hits across the three
# Mac/iOS workflows on the same SHA stay fast.

on:
pull_request:
paths:
- '.github/workflows/scripts-mac-native.yml'
- '.github/workflows/_build-ios-port.yml'
- 'scripts/setup-workspace.sh'
- 'scripts/build-ios-port.sh'
- 'scripts/build-mac-native-app.sh'
- 'scripts/run-mac-native-ui-tests.sh'
- 'scripts/hellocodenameone/**'
- 'scripts/ios/tests/**'
- 'scripts/mac-native/**'
- 'scripts/templates/**'
- '!scripts/templates/**/*.md'
- 'scripts/common/java/**'
- 'scripts/lib/cn1ss.sh'
- 'CodenameOne/src/**'
- '!CodenameOne/src/**/*.md'
- 'Ports/iOSPort/**'
- '!Ports/iOSPort/**/*.md'
- 'native-themes/ios-modern/**'
- '!native-themes/ios-modern/**/*.md'
- 'vm/**'
- '!vm/**/*.md'
- 'tests/**'
- '!tests/**/*.md'
- 'maven/**'
- '!maven/core-unittests/**'
- '!docs/**'
push:
branches: [ master ]
paths:
- '.github/workflows/scripts-mac-native.yml'
- '.github/workflows/_build-ios-port.yml'
- 'scripts/setup-workspace.sh'
- 'scripts/build-ios-port.sh'
- 'scripts/build-mac-native-app.sh'
- 'scripts/run-mac-native-ui-tests.sh'
- 'scripts/hellocodenameone/**'
- 'scripts/ios/tests/**'
- 'scripts/mac-native/**'
- 'scripts/templates/**'
- '!scripts/templates/**/*.md'
- 'scripts/common/java/**'
- 'scripts/lib/cn1ss.sh'
- 'CodenameOne/src/**'
- 'Ports/iOSPort/**'
- 'native-themes/ios-modern/**'
- 'vm/**'
- 'tests/**'
- 'maven/**'
- '!maven/core-unittests/**'
workflow_dispatch:

jobs:
build-port:
# Shared with scripts-ios.yml / scripts-ios-native.yml / ios-packaging.yml
# via the cn1-built cache; first runner to land a fresh SHA populates it
# and the others skip the rebuild.
uses: ./.github/workflows/_build-ios-port.yml

build-mac-native:
needs: build-port
permissions:
contents: read
pull-requests: write
issues: write
runs-on: macos-15
timeout-minutes: 45
concurrency:
group: mac-ci-${{ github.workflow }}-mac-native-${{ github.ref_name }}
cancel-in-progress: true

env:
GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}
GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}

steps:
- uses: actions/checkout@v4

- name: Cache CocoaPods and user gems
uses: actions/cache@v4
with:
path: |
~/.gem
~/Library/Caches/CocoaPods
~/.cocoapods/repos
key: ${{ runner.os }}-pods-v1-${{ hashFiles('scripts/setup-workspace.sh') }}
restore-keys: |
${{ runner.os }}-pods-v1-

- name: Ensure CocoaPods / xcodeproj tooling
run: |
mkdir -p ~/.codenameone
cp maven/UpdateCodenameOne.jar ~/.codenameone/
set -euo pipefail
if ! command -v ruby >/dev/null; then
echo "ruby not found"; exit 1
fi
GEM_USER_DIR="$(ruby -e 'print Gem.user_dir')"
export PATH="$GEM_USER_DIR/bin:$PATH"
# The macNative path uses xcodeproj unconditionally to inject the
# Catalyst build settings (see applyMacNativeXcodeSettings in
# IPhoneBuilder.java). cocoapods comes along because the iOS
# pipeline shares the same gem cache key and we want one warm
# cache across both workflows.
if ! command -v pod >/dev/null 2>&1; then
gem install cocoapods xcodeproj --no-document --user-install
else
gem list xcodeproj | grep -q xcodeproj || gem install xcodeproj --no-document --user-install
fi
pod --version
ruby -e "require 'xcodeproj'; puts Xcodeproj::VERSION"

- name: Compute setup-workspace hash
id: setup_hash
run: |
set -euo pipefail
echo "hash=$(shasum -a 256 scripts/setup-workspace.sh | awk '{print $1}')" >> "$GITHUB_OUTPUT"

- name: Set TMPDIR
run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV

- name: Cache codenameone-tools
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/codenameone-tools
key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }}
restore-keys: |
${{ runner.os }}-cn1-tools-

- name: Cache Maven repository
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-m2-

- name: Restore cn1-binaries cache
uses: actions/cache@v4
with:
path: ../cn1-binaries
key: cn1-binaries-${{ runner.os }}-${{ steps.setup_hash.outputs.hash }}
restore-keys: |
cn1-binaries-${{ runner.os }}-

- name: Restore built CN1 + iOS port artifacts
# The build-port reusable workflow populates this cache; reuse its
# exact key (same trick scripts-ios.yml uses) to avoid recomputing
# the hash on this runner and producing a spurious miss.
uses: actions/cache/restore@v4
with:
path: |
~/.m2/repository/com/codenameone
Themes
Ports/iOSPort/nativeSources
key: ${{ needs.build-port.outputs.cn1_built_cache_key }}
fail-on-cache-miss: true

- name: Install Metal Toolchain
# Xcode 26+ requires the Metal Toolchain component for .metal
# shader compilation. The Mac Catalyst slice always uses Metal
# (Mac Catalyst doesn't have OpenGL ES), so this download is
# required even though we're not setting ios.metal=true here.
run: |
set -euo pipefail
XCODE_APP="$(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -V | tail -n 1 || true)"
if [ ! -x "$XCODE_APP/Contents/Developer/usr/bin/xcodebuild" ]; then
echo "Xcode 26 not found under /Applications. Cannot install Metal Toolchain." >&2
exit 1
fi
echo "Using $XCODE_APP"
export DEVELOPER_DIR="$XCODE_APP/Contents/Developer"
"$DEVELOPER_DIR/usr/bin/xcodebuild" -downloadComponent MetalToolchain
timeout-minutes: 10

- name: Build sample Mac native app and compile workspace
id: build-mac-native-app
run: ./scripts/build-mac-native-app.sh -q -DskipTests
timeout-minutes: 30

- name: Run Mac native UI screenshot tests
env:
ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/mac-native-ui-tests
run: |
set -euo pipefail
mkdir -p "${ARTIFACTS_DIR}"

echo "workspace='${{ steps.build-mac-native-app.outputs.workspace }}'"
echo "scheme='${{ steps.build-mac-native-app.outputs.scheme }}'"

./scripts/run-mac-native-ui-tests.sh \
"${{ steps.build-mac-native-app.outputs.workspace }}" \
"" \
"${{ steps.build-mac-native-app.outputs.scheme }}"
timeout-minutes: 30

- name: Publish Mac native screenshot summary
# Surfaces run-mac-native-ui-tests.sh's comparison result in the
# job's GitHub Actions summary page so the Mac slice status is
# visible at a glance without digging into the artifact zip.
# Reuses the existing metal-screenshot-summary.py helper because
# the JSON schema is identical -- the summary text says "iOS
# Metal" so the wrapper here overrides the headline manually.
if: always()
env:
COMPARE_JSON: ${{ github.workspace }}/artifacts/mac-native-ui-tests/screenshot-compare.json
COMMENT_MD: ${{ github.workspace }}/artifacts/mac-native-ui-tests/screenshot-comment.md
run: |
set -eu
{
echo "## Mac native screenshot comparison"
echo
echo "Ran against \`scripts/hellocodenameone\` as a Mac Catalyst build (\`macNative.enabled=true\`)."
echo "Golden images: \`scripts/mac-native/screenshots/\` (see the README there for the seeding workflow)."
echo
if [ -s "$COMPARE_JSON" ]; then
python3 scripts/ci/metal-screenshot-summary.py --markdown "$COMPARE_JSON"
elif [ -s "$COMMENT_MD" ]; then
cat "$COMMENT_MD"
else
echo "_No screenshot comparison artifact was produced. See the upload step output for details._"
fi
} >> "$GITHUB_STEP_SUMMARY"
if [ -s "$COMPARE_JSON" ]; then
NOTICE="$(python3 scripts/ci/metal-screenshot-summary.py --headline "$COMPARE_JSON" || true)"
if [ -n "$NOTICE" ]; then
echo "::notice title=Mac native screenshot comparison::${NOTICE}"
fi
fi

- name: Upload Mac native artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: mac-native-ui-tests
path: artifacts
if-no-files-found: warn
retention-days: 14
4 changes: 4 additions & 0 deletions Ports/iOSPort/nativeSources/CN1ES2compat.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ enum CN1GLenum {
};

#ifdef USE_ES2
// On Mac Catalyst the GLKit/OpenGLES headers resolve to stub headers under
// macCatalystStubs/ via HEADER_SEARCH_PATHS[sdk=macosx*] (set by
// IPhoneBuilder when macNative.enabled=true). On iOS the real SDK headers
// are picked up.
#import <GLKit/GLKit.h>
#import <OpenGLES/ES2/gl.h>
#import "ExecutableOp.h"
Expand Down
2 changes: 2 additions & 0 deletions Ports/iOSPort/nativeSources/ClearRect.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#endif

#ifdef USE_ES2
#ifndef CN1_USE_METAL
extern GLKMatrix4 CN1modelViewMatrix;
extern GLKMatrix4 CN1projectionMatrix;
extern GLKMatrix4 CN1transformMatrix;
Expand Down Expand Up @@ -88,6 +89,7 @@ static GLuint getOGLProgram(){
return program;
}

#endif // !CN1_USE_METAL
#endif


Expand Down
3 changes: 2 additions & 1 deletion Ports/iOSPort/nativeSources/ClipRect.m
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,14 @@ +(void)updateClipToScale {
if ( clipIsTexture ){
return;
}
#ifndef CN1_USE_METAL
int displayHeight = [CodenameOne_GLViewController instance].view.bounds.size.height * scaleValue;
if(currentScaleX == 1 && currentScaleY == 1) {
//_glEnable(GL_SCISSOR_TEST);
//CN1Log(@"Updating clip to scale");
glScissor(clipX, displayHeight - clipY - clipH, clipW, clipH);
}

#endif // !CN1_USE_METAL
}

#ifndef CN1_USE_ARC
Expand Down
16 changes: 14 additions & 2 deletions Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,22 @@ @implementation CodenameOne_GLAppDelegate
- (CodenameOne_GLViewController *)cn1EnsureViewController
{
if (self.viewController == nil) {
// The iOS XIB-based instantiation breaks under Mac Catalyst on
// Xcode 26: IBAgent-macOS-UIKit crashes compiling the GL/Metal
// view-controller XIBs, so the file is excluded from the Mac
// slice via EXCLUDED_SOURCE_FILE_NAMES[sdk=macosx*]. Pass nil as
// the NIB name on Mac so UIViewController synthesises a plain
// UIView; the Metal layer is attached programmatically further
// down the init chain, so the XIB's IBOutlet wiring isn't needed.
#if TARGET_OS_MACCATALYST
NSString *cn1NibName = nil;
#else
NSString *cn1NibName = @"CodenameOne_GLViewController";
#endif
#ifdef CN1_USE_ARC
self.viewController = [[CodenameOne_GLViewController alloc] initWithNibName:@"CodenameOne_GLViewController" bundle:nil];
self.viewController = [[CodenameOne_GLViewController alloc] initWithNibName:cn1NibName bundle:nil];
#else
CodenameOne_GLViewController *viewController = [[CodenameOne_GLViewController alloc] initWithNibName:@"CodenameOne_GLViewController" bundle:nil];
CodenameOne_GLViewController *viewController = [[CodenameOne_GLViewController alloc] initWithNibName:cn1NibName bundle:nil];
self.viewController = viewController;
[viewController release];
#endif
Expand Down
20 changes: 20 additions & 0 deletions Ports/iOSPort/nativeSources/CodenameOne_GLSceneDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session op
[window release];
#endif

#if TARGET_OS_MACCATALYST
// Mac Catalyst window-size determinism for CI screenshot tests:
// macos-15 runners (and SwiftUI auto-fit on the iPhone/iPad sim too)
// hand the scene a slightly different window height each launch
// (observed 1024x685, 1024x684, 1024x681 across three CI runs in a
// row), which makes the strict-pixel screenshot comparison
// permanently mismatch. Pin the window to an exact 1024x685 via the
// sizeRestrictions API so every launch produces byte-identical
// captures. Min == max forces the user can't resize either; for the
// headless CI use case that's the right trade-off.
if (@available(macCatalyst 13.0, *)) {
UIWindowScene *ws = (UIWindowScene *)scene;
if (ws.sizeRestrictions != nil) {
CGSize fixed = CGSizeMake(1024.0, 685.0);
ws.sizeRestrictions.minimumSize = fixed;
ws.sizeRestrictions.maximumSize = fixed;
}
}
#endif

UIOpenURLContext *urlContext = [connectionOptions.URLContexts anyObject];
if (urlContext != nil) {
[appDelegate cn1OpenURL:[UIApplication sharedApplication] url:urlContext.URL sourceApplication:urlContext.options.sourceApplication annotation:urlContext.options.annotation];
Expand Down
Loading
Loading