Skip to content

Native Windows (Win32 + Direct2D/DirectWrite) port foundation - #5144

Merged
shai-almog merged 103 commits into
masterfrom
windows-port-foundation
Jun 8, 2026
Merged

Native Windows (Win32 + Direct2D/DirectWrite) port foundation#5144
shai-almog merged 103 commits into
masterfrom
windows-port-foundation

Conversation

@shai-almog

@shai-almog shai-almog commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Foundation for a native Windows desktop/tablet port built on ParparVM's clean C target: Java/Kotlin is translated to C, compiled with clang-cl, and linked into a standalone native .exe (no JVM). Rendering is Direct2D, text DirectWrite, imaging WIC, networking WinHTTP, storage Win32. Includes a proper WindowsNativeBuilder (the Windows analog of IPhoneBuilder) with x64 + arm64 support.

This compiles, links, and renders correct pixels on real ARM64 Windows, validated by tests in the VM and on CI.

What's here

  • BuilderWindowsNativeBuilder extends Executor: build(sourceZip, request) unzips the app, stages the WindowsPort native layer from the codenameone-windows bundle (classpath resource, same mechanism as the parparvm bundle), runs the ParparVM translator with the windows app type, then configures + builds the generated CMake project with clang-cl/Ninja inside the matching VS developer environment, and collects the .exe.
    • Architecture: windows.arch selects x64 (x86-64, default) or arm64; normalizeArch/targetTriple/vcvarsArchArg map to the clang-cl triple and the vcvarsall arch (cross form host_target, e.g. arm64_x64, when host≠target). arm64 is the tested path; x64 is wired but unverifiable on the Apple-Silicon dev VM (supported "in theory"). Arch logic is unit tested.
    • Wiring: Executor adds windows-device / windows-source targets; CN1BuildMojo dispatches them to doWindowsNativeLocalBuild (kept distinct from the JVM-bundled windows-desktop/javase target); BuildWindowsDeviceMojo is the cloud wrapper.
  • ParparVM windows app type (ByteCodeTranslator): emits a standalone add_executable linking the Direct2D/DirectWrite/WIC/WinHTTP/Win32 stack, compiling the C++ COM layer (LANGUAGES C CXX, globs *.cpp). The default clean/ios path (add_library) is unchanged.
  • maven/windows module + Ports/WindowsPort/: WindowsImplementation implements all 84 CodenameOneImplementation hooks via a WindowsNative bridge; shadowing ImplementationFactory; native-backed streams + HTTP connection.
  • Native layer (Ports/WindowsPort/nativeSources/): window + Direct2D HWND target + EDT-driven message pump + input (cn1_windows_window.cpp); drawing primitives/images (cn1_windows_graphics.cpp); DirectWrite text behind a plain-C facade (cn1_windows_text.c + cn1_windows_dwrite.cpp); WIC images (cn1_windows_image.cpp); filesystem/storage/clipboard (cn1_windows_io.c); WinHTTP (cn1_windows_net.c); offscreen render + PNG (cn1_windows_screenshot.cpp).
  • Input dispatched into Display via the EDT idle hook.
  • CI: parparvm-tests-windows.yml is a matrix over windows-latest (x64) + windows-11-arm (arm64, experimental).

Why C++

Both Direct2D (d2d1.h) and DirectWrite (dwrite.h) ship only a C++ binding (in C, d2d1.h yields incomplete types and dwrite.h won't parse). The COM translation units are compiled as C++ with extern "C" bridge functions (keeping C linkage for the translated runtime); text/io/net stay C. cn1_globals.h is included extern "C" under C++.

Validation (real ARM64 Windows, Parallels VM)

Windows-only tests in CleanTargetIntegrationTest, all green:

  • compilesWindowsPortNativeLayer — compiles every native source (~3k lines of COM) with clang-cl in real runtime context.
  • rendersOffscreenToPngWithDirect2D — translates an app, links the exe with clang-cl, runs it headless to render an offscreen frame (fills + DirectWrite text), and verifies the encoded PNG's pixels.

Plus WindowsNativeBuilderArchTest (arch→triple/vcvars) on every platform, and the existing clean-target tests remain green on Linux/macOS.

Limitations / follow-ups

  • End-to-end builder run (a full project through WindowsNativeBuilder producing a packaged installer) is exercised on the Windows CI/build-VM path, not locally on macOS; the rendering + clang-cl link it relies on are proven.
  • Full Display/Form app integration (app stub + theme .res + EDT bootstrap) builds on this.
  • Native text editing / IME, native peer components (e.g. WebView2 BrowserComponent), file dialogs, and an installer/packaging step — minimal/stubbed.
  • The clean target's String[] main-args marshalling crashes when an app reads args[] (latent ParparVM/runtime issue, independent of the port).
  • The windows-11-arm CI leg is continue-on-error until its toolchain is confirmed.
  • scripts/windows/* are VM dev helpers.

🤖 Generated with Claude Code

shai-almog and others added 13 commits June 1, 2026 13:12
…pp type

The native Windows desktop port links the translated runtime into a single
executable, with the Direct2D/DirectWrite rendering and Win32 windowing layer
supplied by the port's bundled nativeSources. Thread the appType through
handleCleanOutput into writeCmakeProject and, when it is "windows", emit
add_executable() plus the platform link libraries (Direct2D/DirectWrite/DXGI,
WIC, WinHTTP and the core Win32 user32/gdi32/ole32) behind a portable
if(WIN32)/else libm guard. Every other consumer of the clean target keeps the
existing add_library() output, so the contract for the iOS/embedding path and
the existing integration tests is unchanged.

Add a CleanTargetIntegrationTest case that drives the new "windows" app type
end to end (translate -> cmake -> build -> run) without the
replaceLibraryWithExecutableTarget post-processing the other tests rely on,
proving the translator now emits a runnable native binary directly. A
non-breaking 4-arg runTranslator overload backs it; the existing 3-arg callers
delegate with the "ios" app type. Verified on macOS (26/26, libm branch); the
Win32 link set is exercised on the windows-latest CI leg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eton

First slice of the native Windows (Win32 desktop/tablet) port. Adds the
maven/windows module (codenameone-windows), modeled on the iOS port: it compiles
the port's Java against codenameone-core and bundles the C nativeSources into
nativewindows.jar plus a -bundle.jar (WindowsPort.jar + nativewindows.jar) for
the build pipeline, mirroring the iOS bundle. Wired into the reactor after ios.

WindowsImplementation extends CodenameOneImplementation and implements every
platform hook so the port is concrete and compiles against the core; the hooks
throw UnsupportedOperationException for now and are filled in across the
windowing, graphics, input and platform-service phases. The stub set was
generated from the core's abstract declarations so it tracks the surface
exactly. ImplementationFactory shadows the core factory to return the Win32
implementation (same pattern as the Android port), and WindowsNative declares
the Java side of the Win32 native bridge (bodies live in nativeSources, compiled
by ParparVM's "windows" clean-target build).

nativeSources/windows_bootstrap.c is a _WIN32-gated skeleton carrying only the
early-boot log helper; WinMain and the message loop land with the windowing
phase to avoid colliding with the entry point the clean target emits from the
app's Java main().

Verified on macOS: mvn -pl windows compile/package is green (all stubs match the
abstract surface) and the reactor validates with the new module.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Turn the single windows-latest job into a fail-fast:false matrix over both
Windows architectures so the native-port build is exercised where it actually
ships and is developed:

  * x64   (windows-latest)  -- unchanged, the reliable gate.
  * arm64 (windows-11-arm)  -- matches Apple-Silicon Parallels dev VMs and Arm
                               desktops.

windows-11-arm is a public-preview runner (free on public repos only) whose
image differs from x64: Temurin publishes no Windows/aarch64 JDK 8 or 11, and
the LLVM/Ninja tooling availability still needs confirming. The arm64 leg is
therefore marked experimental via continue-on-error so it yields signal without
blocking merges, builds native arm64 by passing arch to msvc-dev-cmd, and runs
the JDK 17/21/25 configs (Maven driven by JDK 17 instead of 8). It will be
hardened once real CI output shows the image's toolchain.

Also trigger on Ports/WindowsPort/** so the native layer feeding the
clean-target build re-runs this workflow. The "windows" app-type case in
CleanTargetIntegrationTest validates the Direct2D/DirectWrite/Win32 link set on
both legs. The GUI golden-image job is deferred until the Direct2D rendering
phase lands something to capture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the throwing stubs in WindowsImplementation with real implementations
of all 84 platform hooks, wired through a new WindowsNative bridge to the Win32
/ Direct2D / DirectWrite / WIC / WinHTTP native layer. Graphics, font, image and
connection peers cross the boundary as opaque native long pointers boxed as
Long.

Adds the supporting Java types: WindowsNative (the full static-native bridge
surface), WindowsInputStream / WindowsOutputStream (backed by a native file
handle or HTTP body), and WindowsHttpConnection. Storage maps onto the Win32
filesystem under %LOCALAPPDATA%, networking onto WinHTTP, and getLocalization
Manager returns an L10NManager using the JDK locale defaults. Native text
editing / IME and richer key/game-action mapping are intentionally minimal in
this first cut and noted for follow-up.

Verified on macOS: mvn -pl windows compile/package is green (the Java side binds
the bridge and matches the core's abstract surface exactly). The native bodies
build and are validated on the Windows CI legs / dev VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the WindowsNative bridge in C across six translation units sharing
cn1_windows.h (the context, peer structs and cross-file helpers). All bodies are
gated on _WIN32 and use the COM C-binding (COBJMACROS) so Direct2D/DirectWrite/
WIC are callable without a C++ compiler:

  cn1_windows_window.c    window + HWND render target, the EDT-driven Win32
                          message pump, input event ring buffer (WM_* -> CN1
                          pointer/key events), display metrics, present/flush.
  cn1_windows_graphics.c  Direct2D state (color/alpha/clip/font) and all drawing
                          primitives, drawImage and drawRGB.
  cn1_windows_text.c      DirectWrite font creation, measurement and drawString.
  cn1_windows_image.c     WIC decode, ARGB/mutable images, scale, getRGB.
  cn1_windows_io.c        Win32 filesystem, storage dir, roots, clipboard.
  cn1_windows_net.c       WinHTTP client with lazy send-once semantics.

The bridge function names were generated from the WindowsNative signatures using
ParparVM's exact mangling (verified: 76 functions, each defined once, names
matching). Adds uuid to the "windows" app-type link set for the COM IID/CLSID
symbols.

This native layer cannot be compiled on macOS; it builds with clang-cl on the
Windows CI legs / Parallels dev VM and is expected to need a first-build
iteration pass there. The Java side that drives it is already verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the native Win32 event ring buffer into Codename One. WindowsImplementation
now overrides edtIdle: when the EDT is about to idle it blocks briefly on
waitForEvent, then drainInput pulls each queued event via pollEvent and
translates it into the inherited pointerPressed/Released/Dragged, keyPressed/
Released, sizeChanged and exitApplication calls (which re-enqueue onto the EDT).
Event type codes mirror the CN1EventType enum in cn1_windows.h.

This makes pointer and key input functional rather than merely queued. Native
text editing / IME remains a follow-up. Verified on macOS: mvn -pl windows
compile is green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One-shot elevated PowerShell script to provision the Windows build VM: VS Build
Tools (MSVC ARM64 toolset, Windows SDK, clang-cl, CMake, Ninja) plus CMake/Ninja/
Maven/Temurin JDK 17 via Chocolatey, and the JDK_*_HOME machine env vars the
translator test harness reads. The repo is read live from the Parallels shared
folder (Y:), so nothing is cloned. Run once in the guest; builds are then driven
from the Mac via `prlctl exec --current-user`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onent add)

The Chocolatey visualstudio2022-workload-vctools package reports success but its
--add parameter passthrough silently drops the components, leaving an empty VS
shell (no cl.exe / clang-cl / Windows SDK). Drive vs_installer.exe modify
directly with --wait instead, which actually lays the MSVC ARM64/x64 toolsets,
clang-cl, the Windows SDK and CMake/Ninja down. Accept exit 0/3010/1641 as
success, make the script idempotent, and verify clang-cl/MSVC/SDK at the end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…taller

vs_installer.exe rejects --wait (exit 87 / invalid parameter). Download and run
the VS Build Tools bootstrapper (vs_BuildTools.exe) instead, which supports
--wait + --installPath and modifies the existing instance, actually laying down
the MSVC ARM64/x64 toolsets, clang-cl, the Windows SDK and CMake/Ninja.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Batch helper run inside the build VM (off the Y: shared repo) that initialises
the ARM64 VS developer environment (clang-cl + Windows SDK on PATH) and runs the
same Maven invocation as the Windows CI legs. Driven from the Mac via
prlctl exec --current-user.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Batch helper that compile-checks the WindowsPort nativeSources with clang-cl in
the build VM. Note: a full check needs a translated "windows" app-type dist
(cn1_globals.h includes the generated cn1_class_method_index.h), so this is used
against a generated srcRoot rather than standalone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…C++-only)

A real clang-cl build in the Windows VM showed that BOTH Direct2D (d2d1.h) and
DirectWrite (dwrite.h) ship only a C++ binding: in C, d2d1.h yields incomplete
interface types and dwrite.h does not parse at all. Only WIC and Win32 have C
bindings. So the COM rendering translation units are now compiled as C++:

  * window/graphics/image -> .cpp, bodies wrapped in extern "C" so the ParparVM
    bridge functions and shared helpers keep C linkage for the translated C
    runtime. text/io/net stay C (they never call a Direct2D method).
  * cn1_windows.h defines CINTERFACE only for C (so d2d1.h parses there as opaque
    pointers), includes cn1_globals.h inside extern "C" under C++ (verified it
    compiles as C++), and pulls cn1_windows_comc.h under C++.
  * cn1_windows_comc.h maps the COBJMACROS-style call sites (Interface_Method(p,
    ..)) to plain C++ method calls, so the existing call sites compile unchanged.
  * DirectWrite stays isolated in cn1_windows_dwrite.cpp behind the plain-C
    facade cn1_windows_dwrite.h; cn1_windows_text.c is a thin C bridge over it.
  * The translator's "windows" clean target now enables CXX and globs *.cpp.
  * Fixed C++ COM call conventions: REFIID/REFCLSID/REFGUID are references (drop
    &), and IDC_ARROW is cast for LoadCursorW (no UNICODE define).

Adds the compilesWindowsPortNativeLayer test (Windows-only): it translates a
windows app-type dist and compiles every native source with clang-cl in the real
runtime context, reporting all failures at once. Verified in the Apple-Silicon
Parallels VM on Windows 11 ARM64: the whole native layer (~3k lines) compiles
clean. Includes the VM build/compile helper scripts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings up the full build+run pipeline for the native Windows port and proves the
rendering works on real hardware. Adds two WindowsNative hooks --
createOffscreenGraphics (a WIC-backed Direct2D render target, lazily creating the
D2D/WIC factories so it works headless without a window) and saveGraphicsToPng
(flush + WIC PNG encode) -- implemented in the new C++ unit
cn1_windows_screenshot.cpp. CN1Graphics gains a wicBitmap handle for offscreen
targets; cn1WindowsLog now flushes.

Adds rendersOffscreenToPngWithDirect2D (Windows-only): it compiles a tiny app
that drives the bridge, translates it with the "windows" app type plus the whole
WindowsPort native layer, links the executable with clang-cl, runs it headless to
render an offscreen frame (fills + DirectWrite text), and verifies the encoded
PNG (dimensions + the red rect and white-background pixels).

Verified in the Apple-Silicon Parallels VM on Windows 11 ARM64: translate -> link
-> run -> Direct2D/DirectWrite/WIC render -> PNG with correct pixels, all green.
Removed the temporary bisection logging (kept error-path logs).

Note: the clean target's String[] main-args marshalling crashes when an app reads
args[] (latent: HelloWorld never touched it); the render app avoids args. That is
a translator/runtime issue independent of the port, to be fixed separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shai-almog

shai-almog commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 124 screenshots: 124 matched.

Native Android coverage

  • 📊 Line coverage: 13.30% (7970/59918 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 10.79% (39511/366206), branch 4.60% (1585/34466), complexity 5.63% (1860/33062), method 9.88% (1528/15469), class 16.09% (349/2169)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 13.30% (7970/59918 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 10.79% (39511/366206), branch 4.60% (1585/34466), complexity 5.63% (1860/33062), method 9.88% (1528/15469), class 16.09% (349/2169)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 961.000 ms
Base64 CN1 encode 266.000 ms
Base64 encode ratio (CN1/native) 0.277x (72.3% faster)
Base64 native decode 776.000 ms
Base64 CN1 decode 304.000 ms
Base64 decode ratio (CN1/native) 0.392x (60.8% faster)
Image encode benchmark status skipped (SIMD unsupported)

…spatch

Adds the missing builder architecture: WindowsNativeBuilder extends Executor (the
Windows analog of IPhoneBuilder). build(sourceZip, request) unzips the app, stages
the WindowsPort native layer (platform classes + C/C++ nativeSources) from the
codenameone-windows 'bundle' classpath resources (same mechanism as the parparvm
bundle), runs the ParparVM translator with the "windows" app type, then configures
and builds the generated CMake project with clang-cl + Ninja inside the matching
Visual Studio developer environment, and collects the .exe.

Architecture support: windows.arch selects x64 (x86-64, default) or arm64.
normalizeArch/targetTriple/vcvarsArchArg map to the clang-cl target triple
(x86_64/aarch64-pc-windows-msvc) and the vcvarsall arch arg, using the cross form
(host_target, e.g. arm64_x64) when host != target. x64 is wired but unverifiable
on the Apple-Silicon arm64 dev VM, so it is supported "in theory" per the design;
arm64 is the tested path. The arch logic is unit tested (platform independent).

Wiring: Executor gains BUILD_TARGET_WINDOWS_NATIVE ("windows-device") and
BUILD_TARGET_WINDOWS_NATIVE_PROJECT ("windows-source"); CN1BuildMojo dispatches the
native targets to doWindowsNativeLocalBuild (kept distinct from the JVM-bundled
"windows-desktop"/javase target); BuildWindowsDeviceMojo is the cloud wrapper. The
plugin depends on the codenameone-windows bundle so the builder can load the port
resources.

Verified on macOS: plugin compiles, WindowsNativeBuilderArchTest 3/3. The native
compile/link runs on the Windows CI legs / build VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shai-almog

Copy link
Copy Markdown
Collaborator Author

Added the missing builder architecture: WindowsNativeBuilder extends Executor (Windows analog of IPhoneBuilder) with x64 + arm64 target support (windows.arch), wired into CN1BuildMojo dispatch (windows-device/windows-source, distinct from the JVM windows-desktop) plus a BuildWindowsDeviceMojo cloud wrapper, and a unit test for the arch→triple/vcvars logic. x64 is wired but can't be tested on the Apple-Silicon arm64 dev VM (supported in theory); arm64 is the validated path. Pushed in acba795.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 124 screenshots: 124 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 112 seconds

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 834.000 ms
Base64 CN1 encode 1502.000 ms
Base64 encode ratio (CN1/native) 1.801x (80.1% slower)
Base64 native decode 439.000 ms
Base64 CN1 decode 1170.000 ms
Base64 decode ratio (CN1/native) 2.665x (166.5% slower)
Base64 SIMD encode 465.000 ms
Base64 encode ratio (SIMD/native) 0.558x (44.2% faster)
Base64 encode ratio (SIMD/CN1) 0.310x (69.0% faster)
Base64 SIMD decode 411.000 ms
Base64 decode ratio (SIMD/native) 0.936x (6.4% faster)
Base64 decode ratio (SIMD/CN1) 0.351x (64.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 70.000 ms
Image createMask (SIMD on) 11.000 ms
Image createMask ratio (SIMD on/off) 0.157x (84.3% faster)
Image applyMask (SIMD off) 160.000 ms
Image applyMask (SIMD on) 80.000 ms
Image applyMask ratio (SIMD on/off) 0.500x (50.0% faster)
Image modifyAlpha (SIMD off) 191.000 ms
Image modifyAlpha (SIMD on) 111.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.581x (41.9% faster)
Image modifyAlpha removeColor (SIMD off) 190.000 ms
Image modifyAlpha removeColor (SIMD on) 88.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.463x (53.7% faster)
Image PNG encode (SIMD off) 1203.000 ms
Image PNG encode (SIMD on) 983.000 ms
Image PNG encode ratio (SIMD on/off) 0.817x (18.3% faster)
Image JPEG encode 625.000 ms

@shai-almog

shai-almog commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 124 screenshots: 124 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 273 seconds

Build and Run Timing

Metric Duration
Simulator Boot 87000 ms
Simulator Boot (Run) 1000 ms
App Install 14000 ms
App Launch 9000 ms
Test Execution 257000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 775.000 ms
Base64 CN1 encode 1875.000 ms
Base64 encode ratio (CN1/native) 2.419x (141.9% slower)
Base64 native decode 413.000 ms
Base64 CN1 decode 1025.000 ms
Base64 decode ratio (CN1/native) 2.482x (148.2% slower)
Base64 SIMD encode 477.000 ms
Base64 encode ratio (SIMD/native) 0.615x (38.5% faster)
Base64 encode ratio (SIMD/CN1) 0.254x (74.6% faster)
Base64 SIMD decode 509.000 ms
Base64 decode ratio (SIMD/native) 1.232x (23.2% slower)
Base64 decode ratio (SIMD/CN1) 0.497x (50.3% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 71.000 ms
Image createMask (SIMD on) 15.000 ms
Image createMask ratio (SIMD on/off) 0.211x (78.9% faster)
Image applyMask (SIMD off) 158.000 ms
Image applyMask (SIMD on) 72.000 ms
Image applyMask ratio (SIMD on/off) 0.456x (54.4% faster)
Image modifyAlpha (SIMD off) 624.000 ms
Image modifyAlpha (SIMD on) 84.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.135x (86.5% faster)
Image modifyAlpha removeColor (SIMD off) 262.000 ms
Image modifyAlpha removeColor (SIMD on) 133.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.508x (49.2% faster)
Image PNG encode (SIMD off) 1911.000 ms
Image PNG encode (SIMD on) 1256.000 ms
Image PNG encode ratio (SIMD on/off) 0.657x (34.3% faster)
Image JPEG encode 754.000 ms

@shai-almog

shai-almog commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 73 screenshots: 73 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 124 seconds

Build and Run Timing

Metric Duration
Simulator Boot 59000 ms
Simulator Boot (Run) 1000 ms
App Install 23000 ms
App Launch 14000 ms
Test Execution 1501000 ms

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

✅ ByteCodeTranslator Quality Report

Test & Coverage

  • Tests: 734 total, 0 failed, 10 skipped

Benchmark Results

  • Execution Time: 10231 ms

  • Hotspots (Top 20 sampled methods):

    • 22.15% java.lang.String.indexOf (389 samples)
    • 21.18% com.codename1.tools.translator.Parser.isMethodUsed (372 samples)
    • 19.48% java.util.ArrayList.indexOf (342 samples)
    • 5.13% java.lang.Object.hashCode (90 samples)
    • 5.01% com.codename1.tools.translator.BytecodeMethod.addToConstantPool (88 samples)
    • 2.11% java.lang.System.identityHashCode (37 samples)
    • 1.99% com.codename1.tools.translator.ByteCodeClass.calcUsedByNative (35 samples)
    • 1.71% com.codename1.tools.translator.ByteCodeClass.markDependent (30 samples)
    • 1.59% com.codename1.tools.translator.ByteCodeClass.updateAllDependencies (28 samples)
    • 1.25% com.codename1.tools.translator.Parser.generateClassAndMethodIndexHeader (22 samples)
    • 1.20% com.codename1.tools.translator.BytecodeMethod.appendMethodSignatureSuffixFromDesc (21 samples)
    • 1.08% com.codename1.tools.translator.Parser.cullMethods (19 samples)
    • 0.85% java.lang.StringBuilder.append (15 samples)
    • 0.85% com.codename1.tools.translator.BytecodeMethod.isMethodUsedByNative (15 samples)
    • 0.57% com.codename1.tools.translator.Parser.getClassByName (10 samples)
    • 0.51% java.util.TreeMap.getEntry (9 samples)
    • 0.51% com.codename1.tools.translator.BytecodeMethod.optimize (9 samples)
    • 0.51% com.codename1.tools.translator.BytecodeMethod.appendCMethodPrefix (9 samples)
    • 0.51% com.codename1.tools.translator.BytecodeMethod.equals (9 samples)
    • 0.51% sun.nio.fs.UnixNativeDispatcher.open0 (9 samples)
  • ⚠️ Coverage report not generated.

Static Analysis

  • ✅ SpotBugs: no findings (report was not generated by the build).
  • ⚠️ PMD report not generated.
  • ⚠️ Checkstyle report not generated.

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

JavaScript port screenshot updates

Compared 96 screenshots: 94 matched, 2 updated.

  • TabsTheme_dark — updated screenshot. Screenshot differs (375x667 px, bit depth 8).

    TabsTheme_dark
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as TabsTheme_dark.png in workflow artifacts.

  • TabsTheme_light — updated screenshot. Screenshot differs (375x667 px, bit depth 8).

    TabsTheme_light
    Preview info: JPEG preview quality 70; JPEG preview quality 70.
    Full-resolution PNG saved as TabsTheme_light.png in workflow artifacts.

- Security (CodeQL Zip Slip): WindowsNativeBuilder.extractJarResource now
  rejects archive entries whose canonical path escapes the target directory
  (path traversal via '..' / absolute names).
- CI (windows clean-target x64): the "windows" app type emits a LANGUAGES C CXX
  project, but cmakeToolchainArgs() only set CMAKE_C_COMPILER, so cmake failed
  enabling CXX (generatesRunnableExecutableForWindowsAppType). Always select a
  C++ compiler too (clang-cl on Windows, clang++ elsewhere). Verified on macOS.
- CI (windows clean-target arm64): Temurin publishes Windows/aarch64 only from
  JDK 21, so setup-java for 17 failed on the arm64 leg. The arm64 leg now uses
  JDK 21; x64 keeps 8/11/17/21/25.

The failing Android (emulator boot timeout) and javascript-screenshots
(screenshot mismatch / broken pipe) jobs are pre-existing infra/flaky failures
unrelated to this PR (it touches neither Android nor JavaScript).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shai-almog

Copy link
Copy Markdown
Collaborator Author

CI/security fixes pushed (f512e86):

  • CodeQL Zip Slip (WindowsNativeBuilder.extractJarResource): now rejects any archive entry whose canonical path escapes the destination dir, before writing. The alert should clear on the next CodeQL scan.
  • clean-target (x64): the windows app type emits a LANGUAGES C CXX project; cmakeToolchainArgs() only set the C compiler, so cmake failed enabling CXX. It now also selects clang-cl (Windows) / clang++ (else). Verified green on macOS.
  • clean-target (arm64): Temurin ships Windows/aarch64 only from JDK 21, so setup-java for 17 failed — the arm64 leg now uses JDK 21.

The Build Android (emulator boot timeout, port 5554) and javascript-screenshots (screenshot mismatch / broken pipe) failures are pre-existing infra/flaky jobs unrelated to this PR, which touches neither Android nor JavaScript.

shai-almog and others added 4 commits June 1, 2026 21:38
…DT message loop)

Brings up a full Codename One Display/Form app as a native Windows .exe
through the ParparVM clean target + WindowsPort, end-to-end:

- Fonts: implement native/TrueType font support so native: fonts resolve
  instead of crashing. WindowsImplementation now overrides isTrueTypeSupported,
  isNativeFontSchemeSupported, loadTrueTypeFont and deriveTrueTypeFont; the
  native layer maps native:* names to the Segoe UI family (weight/slant from
  the suffix) via DirectWrite and carries an owned family on CN1Font so derive
  can re-resolve. This fixes the UIManager startup path that called
  Font.createTrueTypeFont(native:..., size).derive(...) on a null font.

- Threading: the Win32 message loop now runs on the app main thread
  (runMessageLoop) after Display.init, while the EDT renders concurrently;
  pollEvent only drains the queue and waitForEvent blocks on the event signal.
  The Direct2D factory is created MULTI_THREADED (window thread creates, EDT
  draws), matching the offscreen/screenshot path.

- Translator: a non-iOS native build can set -Dcn1.concreteImplementation to
  bind the abstract CodenameOneImplementation @concrete hint to the Windows
  impl and drop the other iOS specializations, so core translates without the
  iOS port. WindowsNativeBuilder passes this through.

- Diagnostics: a last-resort unhandled-exception logger (with an ARM64 frame
  walk) and a few lifecycle breadcrumbs; per-call logging removed.

- Test: buildsFullFormAppNative translates app + full core + WindowsPort +
  JavaAPI + nativeSources with the "windows" app type and links the .exe with
  clang-cl, proving the builder pipeline on an actual Form (not just the
  bridge). Verified rendering live in the Windows 11 VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bSocket + WIC image encode)

Adds the subsystems the unified cn1ss screenshot pipeline needs so the native
Windows port participates the same way Android/iOS/JavaSE do -- streaming a
rendered Form over a WebSocket to Cn1ssScreenshotServer, rather than a bespoke
local capture.

- Raw TCP sockets (WinSock): cn1_windows_socket.c implements connect/read/
  write/available/close over ws2_32; WindowsSocket wraps the native peer with
  blocking InputStream/OutputStream; WindowsImplementation overrides the socket
  SPI (connectSocket, readFromSocketStream, writeToSocketStream, getSocket*,
  disconnectSocket, isSocketAvailable, getHostOrIP). ws2_32 added to the
  Windows clean-target link set.

- WebSocket: WindowsWebSocketImpl is an RFC 6455 client over WindowsSocket,
  written against the translated ParparVM runtime (com.codename1.util.Base64,
  java.util.Random, manual ws:// parse; no java.util.Base64 / java.net.URI /
  MessageDigest). isWebSocketSupported()/createWebSocketImpl() wired up.

- Image encode: getImageIO() returns a WIC-backed PNG ImageIO
  (encodeArgbToPng native); captureWindowToPngBytes snapshots the offscreen
  Direct2D/WIC window target to PNG bytes -- the proven headless render path --
  for a deterministic UI capture independent of the mutable-image GetRGB path.

- Misc port natives: exitProcess, sleepMillis, parkMainThread.

- Translator: cn1_win_compat.h guards its struct timeval against winsock2.h's
  (only the port's socket TU pulls both into one translation unit).

- Tests: capturesFormScreenshotOverWebSocket builds the app, starts a
  Cn1ssScreenshotServer and runs the exe, asserting the PNG arrives over the
  WebSocket; verified end-to-end in the Windows 11 VM (the captured Form renders
  correctly, server reports status=ok). CI: a windows-latest job builds core +
  port, runs the capture test and uploads the PNG; a companion Linux job posts
  it to the PR via the shared cn1ss machinery, like the other ports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…plit fix)

The screenshot-capture job's core build passed -Dmaven.javadoc.skip=true
unquoted under pwsh, which split it at the dots and treated ".javadoc.skip=true"
as a bogus Maven lifecycle phase (BUILD FAILURE before Maven ran). Single-quote
it (and -Plocal-dev-javase) like the sibling test step already does.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
shai-almog and others added 14 commits June 6, 2026 21:19
editString: while a TextField/TextArea is edited it is now overlaid by a real
Win32 EDIT control (native caret, selection, keyboard, IME) at the component's
bounds; on commit (Enter on a single-line field, or focus loss) the text is read
back and delivered via Display.onEditingComplete, then the control is torn down.
isNativeInputSupported() now returns true so TextArea routes here instead of the
lightweight editor; editing is synchronous (the EDT parks in invokeAndBlock while
the control, on the window's pump thread, owns the keystrokes). The control is
created/destroyed on the pump thread via a new WM_CN1_EDIT marshal (mirroring the
WebView2 peer); cn1_windows_edit.c holds the control + subclass proc. The window
gains WS_CLIPCHILDREN so the Direct2D present does not paint over the native child
controls (this EDIT and the WebView2 browser).

clipboard: the native CF_UNICODETEXT get/set (cn1_windows_io.c) existed but was
never wired -- copyToClipboard/getPasteDataFromClipboard now route through it so
copy/paste interoperate with other Windows apps (non-string objects keep the
in-memory lightweight clipboard via super).

Native editing has no automated coverage: the headless screenshot path has no
real window, and a child HWND is not part of the Direct2D capture -- it needs
interactive verification on a real Windows desktop (tracked in status.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three fixes found running the native initializr app:

1. Overscroll "smear" -- the D2D HWND render target was created with
   D2D1_PRESENT_OPTIONS_NONE, which discards the back buffer after each Present.
   Codename One repaints only the dirty region per frame and relies on the rest
   of the surface being preserved, so every area the EDT did not repaint showed
   stale pixels (the smear at the form edges during overscroll; a window resize
   recreated the target and briefly hid it). Use
   D2D1_PRESENT_OPTIONS_RETAIN_CONTENTS so partial repaints are correct.

2. Native editing never stopped -- editing used a synchronous invokeAndBlock loop,
   which froze the EDT and bypassed the framework's async-mode scroll-hide hook, so
   the overlaid EDIT control floated, stuck, when you scrolled or switched fields.
   Switch to async edit mode: editString returns immediately, a UITimer polls the
   control for commit (Enter / focus loss), hideTextEditor() (which the core invokes
   from Component.setScrollY when the editing field scrolls away) commits and tears
   the control down, and starting a new edit finishes the previous one first -- so
   one control is ever live and it never detaches from its field.

3. Native editing did not look like the field -- the EDIT used the default Win32
   chrome (sunken border, Segoe UI). Style it to match: overlay it on the field's
   text area inside the padding (so the CN1 border/background still frame it),
   borderless, with a GDI font built from the field's CN1Font (same family + pixel
   size) and the field's foreground/background colours applied via WM_CTLCOLOREDIT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…omplete does not)

In async edit mode TextArea.onEditComplete intentionally does NOT setText -- it
assumes the port streams the value to the field as the user types (the way iOS
does via editingUpdate). The port only called onEditingComplete, so every edit
was dropped: switching fields lost everything typed. Mirror the native control's
text into the field live from the edit poller, and set it again on commit, so the
field tracks the control and fires its data-change events as you type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…parent console from cmd

Link the native Windows exe as a GUI-subsystem app
(/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup in writeCmakeProject) so
double-clicking it never pops a stray console window in front of the UI.
Previously the exe linked as a console app and initDisplay hid the
console window with ShowWindow(SW_HIDE) -- which also hid the user's own
cmd window when launched from a terminal.

GUI-subsystem processes do not get their CRT stdout/stderr wired to a
parent console automatically, so initDisplay now AttachConsole(
ATTACH_PARENT_PROCESS) and reopens stdout/stderr onto CONOUT$ only when
they are not already redirected to a file/pipe. Result: launched from
cmd the app's logs/exceptions appear in that console; double-clicked it
stays silent (no console, no exceptions on screen); a redirected stdout
pipe (the screenshot CI harness) is preserved. cn1WindowsLog continues
to mirror to %TEMP%\cn1windows.log regardless.

Verified: CleanTargetIntegrationTest#compilesWindowsPortNativeLayer
compiles every nativeSource with clang-cl (BUILD SUCCESS).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…trap stub

The native Windows builder previously passed the app's Lifecycle main class
straight to the translator and never registered native interfaces. The clean
target requires a class with main(), which a CN1 Lifecycle app does not have,
and NativeLookup had nothing registered -- so a real app could neither build
nor resolve its @NativeInterface implementations.

Mirror the iOS builder's native-interface mechanism, adapted to the clean C
target:

- generateNativeInterfaceAndBootstrapStubs() scans the app classes for
  NativeInterface implementors and, via registerNativeImplementationsAndCreate-
  Stubs(), generates an XxxStub bridge per interface (the NativeLookup target)
  plus an XxxImplCodenameOne declaring the actual native methods. The translator
  emits one C function per native method; the app defines them in its own
  nativeSources C/C++. This is how a native interface "resolves to actual C++
  native code" on the clean target.

- PeerComponent returns/params are bridged through a long[] holding the native
  widget handle (getImplSuffix=ImplCodenameOne, create(new long[]{...}),
  ((long[])p.getNativePeer())[0]) -- identical to IPhoneBuilder -- so a returned
  native widget becomes a real PeerComponent. Fixes the prior overrides which
  autoboxed a long to Object and would not have compiled for long-returning
  native methods.

- A generated <MainClass>Stub provides the executable entry point: its main()
  runs the NativeLookup.register(...) calls and boots the Lifecycle app windowed
  (Display.init + init/start on the EDT + runMainEventLoop), plus the SVGRegistry
  install weave. The clean target auto-detects it as the C main (sole main()).

All generated stubs are compiled (javac from java.home, source/target chosen for
the running JDK) into classesDir, a translator source root.

Plugin compiles (mvn -pl codenameone-maven-plugin compile: BUILD SUCCESS).
End-to-end binding with a real native-interface app, and generic HWND peer
placement in WindowsImplementation, remain to verify (tracked in status.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The DirectWrite text path (drawString -> cn1dwDrawText -> DrawTextLayout) was
the one draw operation that did not push the current clip; every graphics
primitive (lines, rects, images, shapes) wraps its draw in cn1WinPushClip/
cn1WinPopClip, but text did not. DrawTextLayout ignores the render target's
state clip, so glyphs drew unclipped.

During a scroll/overscroll, Codename One draws the scrollable rows translated
past the viewport. With the HwndRenderTarget presenting the whole surface every
frame under D2D1_PRESENT_OPTIONS_RETAIN_CONTENTS, that unclipped text landed in
the retained margins outside the dirty region (e.g. up into the toolbar band)
and was never cleared -- the smeared, overlapping text trails. The diagnostic
build confirmed the dirty region itself was correct (the full scroll viewport,
10,64,764x435 on a 784x561 surface); the bug was purely the missing text clip.

Fix: expose cn1WinPushClip/cn1WinPopClip (extern "C", declared in cn1_windows.h)
and wrap the cn1dwDrawText call in drawString with them, so glyphs clip to g's
current clip exactly like every other primitive. They honour both the axis-
aligned rect clip and the shape-clip layer, so clipped text under a setClip(Shape)
is correct too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WM_MOUSEWHEEL is not handled in the window proc yet (touch/drag scrolling
works). Documents the wiring approach (synthetic pointer drag through CN1's
scroll logic, mirroring JavaSEPort.mouseWheelMoved) for the follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…11 chrome note

Native in-place text editing is no longer a stub -- editString overlays a styled
Win32 EDIT control, streams text back live, and commits on blur/scroll (verified
interactively). Rewrite gap #1 to reflect that, keeping the real remaining item:
no automated/headless coverage, and IME/bidi unverified.

Remove the "Dialog / Win11 chrome polish" note: using the material theme as the
native base is an intentional design choice, not a gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The drawString clip fix (glyphs now clip to g's clip like every other primitive)
also changes static renders wherever text sat at a component/tile edge -- toolbar
titles, sticky headers, lightweight picker rows, gradient tile labels, the
draw-string and clip-under-rotation probes. Promote the post-fix x64 captures
(16 tiles) as the new baseline; verified the renders are correct (text correctly
clipped, no truncation or corruption).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rop the hack)

The native Windows build previously bound CodenameOneImplementation to its
concrete WindowsImplementation via a global -Dcn1.concreteImplementation system
property, which the translator's @concrete parser special-cased ("if the iOS
name ends with IOSImplementation, swap in the override"). That was a hack: a
process-global string, an iOS-name string match, and duplicated in the builder
and every test.

Replace it with a first-class annotation attribute. @concrete now has win() in
addition to name(): name() is the iOS pipeline target, win() the native Windows
one. CodenameOneImplementation is annotated
@concrete(name=IOSImplementation, win=WindowsImplementation). The translator sets
a per-run concrete target from the app type (ByteCodeClass.setConcreteTarget,
"win" for the windows app type) and the parser honours win() over name() for that
target; when win() is empty (e.g. Simd, which has only an iOS specialization) the
concrete is left unset so the portable base class is translated instead of
pulling in the absent iOS class -- exactly what the old override did, without the
string matching.

Drop the -Dcn1.concreteImplementation plumbing from WindowsNativeBuilder and the
integration tests. Also fix WindowsNativeBuilder's translator invocation: the
output-type arg must be "clean" (the windows app type, passed separately,
specializes it); it was incorrectly passing "windows" as the output type, which
the translator does not recognize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These were added as a literal (non-regex) fallback back when the native Windows
sample was built by hand, on the false premise that "the clean target does not
run the replaceAll->RE rewrite". That premise no longer holds: the sample (and
any initializr project) now builds with the codenameone-maven-plugin, whose
bytecode-compliance goal rewrites String.replaceAll/replaceFirst to
JdkApiRewriteHelper (the CN1 RE engine) at process-classes -- so these JavaAPI
methods are never referenced, and worse, the literal fallback gave wrong results
for real regex patterns. Verified: rebuilt hellocodenameone-common references
JdkApiRewriteHelper.replaceAll (0 classes reference java.lang.String.replaceAll).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ne yet' note

Every captured tile now has a baseline in scripts/windows/screenshots (incl.
SVGStatic, whose gradient fills are fixed). Update the PR-comment message + code
comment so they stop claiming tiles lack a baseline and post as "new".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ows.debug hint

The native Windows exe was built RelWithDebInfo unconditionally -- the translator
always emitted clang-cl /Zi + linker /DEBUG regardless of config, so even a
"release" build carried full debug info and dragged a large PDB. The 14.8 MB
figure people saw was that debug build (it also produced a ~97 MB .pdb).

Make the shipping build optimized and stripped by default:
- ByteCodeTranslator.writeCmakeProject now gates /Zi + /DEBUG on Debug/
  RelWithDebInfo, and for Release adds /OPT:REF (dead-strip unreferenced code) +
  /OPT:ICF (fold identical COMDATs).
- WindowsNativeBuilder configures CMAKE_BUILD_TYPE=Release by default and exposes
  a windows.debug build hint; windows.debug=true switches to RelWithDebInfo so a
  native crash can still be symbolized during development.

Measured on the Initializr sample: Release = ~13 MB exe, no PDB;
RelWithDebInfo = ~14 MB exe + ~97 MB PDB. Optimizations (/O2) stay on in both.
Documented the two hints in the build-hints table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Model it after the other port chapters (Working with iOS, etc.). Covers the
technology stack (ParparVM clean C -> clang-cl native exe, concurrent GC,
Direct2D/DirectWrite/WIC, Media Foundation, WebView2, WinHTTP, single self-
contained exe with PE-embedded resources), how to build (Windows + VS toolchain,
x64/arm64), the windows.arch / windows.debug build hints, the optimized+stripped
default, and a practical comparison table of the native .exe vs. the JVM desktop
app vs. the executable jar (JVM dependency, artifact size, startup, arch, look,
portability, maturity). Includes an illustrative screenshot of the Initializr app
running as a native exe, and is wired into developer-guide.asciidoc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

shai-almog and others added 8 commits June 7, 2026 11:30
The Build Developer Guide Docs job failed on the new Windows chapter: Vale (7
Microsoft-style issues) and LanguageTool (2). Use contractions (it's/can't/don't),
drop the "roughly" adverb and the "fully-featured" hyphen, rename the "What it is"
table row to "Summary", and reword "produce an x64 binary" (the a-vs-an check).
Add "Winsock" to languagetool-accept.txt. Vale now reports 0/0/0 locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nload

The clean-target job failed at "Set up Ninja": seanmiddleditch/gha-setup-ninja
downloaded a corrupted ninja-win.zip ("Invalid or unsupported zip format. No END
header found"). Replace all three Ninja setup steps with `pip install ninja`
(wheels for win_amd64 and win_arm64) wrapped in a 3x retry, which avoids the
GitHub-release download path entirely and absorbs transient network blips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cn1_windows_resources.c passed RT_RCDATA (which expands to the narrow
MAKEINTRESOURCEA, an LPSTR) to FindResourceW, whose lpType is LPCWSTR. MSVC only
warns (C4133) so the native build was unaffected, but a stricter clang-cl cross-
compile errors on the incompatible pointer type. Cast to (LPCWSTR) RT_RCDATA --
the value is an integer-encoded resource type, so the cast is safe and now
compiles on both. Surfaced by the new Linux cross-compile check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…x runner

Add a Linux GitHub Actions job (windows-cross-compile.yml) that builds the native
Windows port into a Windows x64 PE without a Windows machine, using clang-cl +
lld-link + llvm-rc against a Windows SDK laid out by xwin. clang is a cross-
compiler, so the binary it emits for x86_64-pc-windows-msvc is the same one a
Windows host produces -- this is a fast, Windows-free pre-check that the port +
the Codename One core + a real Form app translate and link into a Windows binary.

The work is driven by a new CleanTargetIntegrationTest#crossCompilesWindowsExe-
WithXwin: it translates a Form app with the "windows" app type (host-agnostic),
then configures CMake for the cross target (CMAKE_SYSTEM_NAME=Windows so the
generated CMakeLists' if(WIN32) Direct2D/DirectWrite link set activates; clang-cl
with /imsvc onto the xwin includes; lld-link via -fuse-ld=lld with /libpath onto
the xwin libs; llvm-rc with the SDK include path, which on Windows comes from
%INCLUDE%), builds, and asserts a non-trivial PE ('MZ') is produced. It is
compile-and-link only -- the PE cannot run here -- and is gated on
CN1_XWIN_SYSROOT, so it skips on Windows and where the sysroot is absent.

Validated locally on macOS (also a non-Windows host): produces a PE32+ x86-64 GUI
exe. The authoritative run/render gate stays on the Windows runners.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e Linux build cloud)

The builder ran its CMake step only inside a Visual Studio developer environment
(vswhere -> vcvarsall), so the native Windows target could be produced only on a
Windows machine. Add a non-Windows-host path so the Linux build cloud can emit
Windows x64/arm64 binaries with no Windows machine in the loop.

On a non-Windows host the builder now cross-compiles with clang-cl + lld-link +
llvm-rc against a Windows SDK laid out by `xwin splat` -- the exact recipe the
windows-cross-compile CI job and the crossCompilesWindowsExeWithXwin test already
prove green: CMAKE_SYSTEM_NAME=Windows (so the generated CMakeLists' if(WIN32)
Direct2D/DirectWrite link set activates), /imsvc onto the SDK headers, -fuse-ld=lld
with /libpath onto the SDK libs, and llvm-rc with the SDK include that %INCLUDE%
provides on Windows. The SDK arch subdir follows windows.arch, so both x64 and
arm64 build from either host. CMake steps run directly (no vcvarsall) on the cross
host; Windows hosts keep the existing Visual Studio path unchanged.

The sysroot comes from the windows.sdkRoot build hint or CN1_XWIN_SYSROOT, with a
clear error if it is missing or not an xwin splat. clang-cl/llvm-rc honour
CN1_CLANG_CL/CN1_LLVM_RC overrides, else resolve on PATH.

Documented in the developer guide (build hints windows.sdkRoot, the "Building"
section, the build-hints table) and status.md. The cross recipe itself is CI-
validated; this commit wires the identical flags into the builder (plugin
compiles; the builder is exercised end-to-end by the build cloud).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the native Windows port a first-class cloud build target named win32:

- buildxml-template.xml: a "windows-device" cloud target uploads targetType=win32,
  the queue the Linux win32 build daemon (Android/JavaScript group) polls. Distinct
  from windows-desktop (the JVM/JavaSE bundle).
- BuildWin32Mojo: a cn1:buildWin32 convenience goal (platform=windows, buildTarget=
  windows-device), the user-facing name for the target (BuildWindowsDeviceMojo
  stays as the legacy alias).
- Document the win32 cloud build in the initializr build reference and the "Working
  with the native Windows port" developer-guide chapter: a regular build returns
  x64 + arm64 release exes, the windows.debug hint returns a single x64 debug exe.

Server-side handling (BuildCloud routing, BuildDaemon win32 builder) is in separate
PRs on those repos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t-in)

Native Windows exes run unsigned, but an unsigned download trips SmartScreen and
shows "Unknown publisher" in UAC, so anything distributed should be Authenticode-
signed. Add opt-in signing with osslsigncode, which signs Windows PE files on any
OS -- so it works in the Linux build cloud with no Windows machine, and signs both
the x64 and arm64 release exes (the daemon delegates each arch to this builder).

Signing runs only when a code-signing certificate is provided (default = unsigned):
- windows.signing.pkcs12 (path to a .pfx/.p12), falling back to the build request's
  uploaded certificate; windows.signing.password (falls back to the request cert
  password).
- windows.signing.timestampUrl (RFC 3161, default http://timestamp.digicert.com;
  empty disables), windows.signing.digest (default sha256),
  windows.signing.name/url (signature description + URL), windows.signing=false to
  force-skip. osslsigncode resolved on PATH or via CN1_OSSLSIGNCODE. Missing
  tool/cert fails the build with a clear message rather than shipping unsigned.

Validated the exact command end-to-end (osslsigncode 2.13: PKCS#12 sign + sha256 +
RFC 3161 timestamp from DigiCert, verified) on a real cross-compiled PE. Documented
the build hints + a signing tutorial (incl. the post-2023 hardware-key / cloud
signing-service path via PKCS#11) in the developer guide. Plugin compiles; guide
Vale-clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gates

Code-signing certificate handling for the native Windows (win32) target:
- The certificate is configured through project settings
  (codename1.windows.signing.certificate = path to the .p12/.pfx, and
  codename1.windows.signing.password); it is not a user-facing "build request
  channel". The cloud build's codeNameOne ant task now uploads it with the
  request (certificate/certPassword attributes), exactly like the iOS/Android
  certificates, and doWindowsNativeLocalBuild attaches the same certificate for
  local builds -- so local and cloud sign from one configuration.

Developer-guide fixes (the guide quality gate was failing):
- Working-With-Windows: rewrite the signing section around the settings-property
  certificate; correct the comparison table -- the JVM desktop app is Intel x64
  only and ships a bundled Intel Java 8 JRE (no arm64).
- Resolve the failing quality gates: capitalize the "or the convenience goal"
  paragraph (paragraph-cap + LanguageTool UPPERCASE_SENTENCE_START), reword
  "SSL.com eSigner" -> "eSigner from SSL.com" and accept-list "eSigner"
  (LanguageTool MORFOLOGIK), and fix two Microsoft.Contractions Vale errors.
  Verified locally: asciidoctor lint, Vale, paragraph-cap and LanguageTool all 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shai-almog
shai-almog merged commit 6d73d72 into master Jun 8, 2026
39 of 41 checks passed
@shai-almog
shai-almog deleted the windows-port-foundation branch June 8, 2026 01:54
shai-almog added a commit that referenced this pull request Jun 12, 2026
* Weekly release blog: native Win32, 3D, gaming, printing, Wallet

Friday weekly index plus four daily follow-up tutorials, shipped as one
batch PR per the blog-release-series convention (future-dated posts
render on the PR preview via HUGO_BUILD_FUTURE and appear on the live
site on their dates):

- Fri: Native Java Win32, 3D Gaming, Printing and Wallet (index, with
  fleshed-out teasers, the Baeldung article, the build-cloud rebuild
  heads-up, and the simulator UX / compliance-check / UIScene-launch /
  browser-appearance items)
- Sat: portable 3D graphics API, #5151
- Sun: game development API + Box2D physics, #5166
- Mon: native Windows port (no JVM), #5144 #5209
- Tue: printing API + Apple Wallet extensions, #5217 #5227

Screenshots come from the real CI screenshot baselines (iOS Metal, Mac
native, Windows). The gaming post's animated GIF was captured from the
actual physics demo running in the simulator. Hero images are composed
from those same renders; prose gate (Vale) and hugo --buildFuture pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* blog: fix prose-gate findings on the weekly batch

frame rate as two words, drop the uncomparable 'more complete', avoid
the 'Your' false positive, 'the surrounding form', American 'afterward',
hyphenate '100-millisecond', and accept-list Francesco Galgani.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* blog: editorial round on the weekly batch

- gaming post: personal history opening (Jane's USAF, why gaming was
  off the table for years, the Minecraft argument, royalty-free indie
  angle), credit Erin Catto's Box2D + JBox2D explicitly, explain that
  sprites composite on the GPU backends, add a "What can you build?"
  section with all six game samples and fresh simulator captures
- 3D post: JOGL is the simulator default with the software rasterizer
  as fallback, deeper two-level API section (command layer, pipeline
  cache, on-demand vs continuous), link the developer guide chapter
- windows post: no-Swing/no-AWT clarification, tie the narrative to the
  Mac native post (WORA desktop), precise GraalVM comparison, real
  Initializr-on-Windows screenshot, accurate dial()/sms handler story,
  drop the "honest" phrasing
- printing post: drop the "honest caveat" phrasing
- new post-link shortcode: links to future-dated posts render as plain
  text in production and become real links when the daily rebuild picks
  up the published post; used for every forward link in the batch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* blog: in-game animated GIFs for all five game samples + prose-gate round 2

Replace the five static sample screenshots in the gaming post with
animated GIFs captured from scripted gameplay in the simulator (joystick
patrol, run/jump/coin run, card flips with a match and a mismatch,
checkers moves against the AI, orbiting 3D camera). Also lands the
second prose-gate round: accept-list entries and rephrased sentences so
post-link shortcodes no longer open a sentence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
shai-almog added a commit that referenced this pull request Sep 2, 2026
…s per parent

check-copyright-headers went red once #5671 merged in: BuildWin32Mojo and
BuildWindowsDeviceMojo have carried no header since they were added in #5144, and
editing them brought them into the diff the gate examines. Both take the Codename
One GPL + Classpath header, copied from BuildMacNativeMojo in the same package.

I had run that gate locally and read "8 file(s) passed", which was worthless: I
ran it BEFORE `git add`, and it diffs committed refs, so the two files it needed
to see were still sitting unstaged in the working tree. A gate run against HEAD
while the change is uncommitted answers a question about the previous commit.

Separately, review is right that duplicate detection only covered the manifest.
The counter was keyed by target and incremented for root edges alone, so a nested
include repeated in the SAME parent was invisible -- and _visit() returns early on
a revisit, so the second edge left no trace at all. The outcome check then saw one
declaration against two rendered titles and passed, because it only asks whether a
title appears AT LEAST as often as it is declared.

Now keyed by (parent, target) and counted for every unconditional edge at any
depth. That keeps the distinction that matters: one parent including one file
twice renders it twice and is a defect; two different parents including the same
fragment is what a fragment is for. Conditional edges stay excluded, at the root
and nested, since two mutually exclusive branches are one rendering.

Probed all three:

    nested duplicate, same parent     old 0 -> new 1
    root duplicate                    old 1 -> new 1   (regression intact)
    same fragment from a 2nd parent   new 0            (still allowed)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shai-almog added a commit that referenced this pull request Sep 2, 2026
…s per parent

check-copyright-headers went red once #5671 merged in: BuildWin32Mojo and
BuildWindowsDeviceMojo have carried no header since they were added in #5144, and
editing them brought them into the diff the gate examines. Both take the Codename
One GPL + Classpath header, copied from BuildMacNativeMojo in the same package.

I had run that gate locally and read "8 file(s) passed", which was worthless: I
ran it BEFORE `git add`, and it diffs committed refs, so the two files it needed
to see were still sitting unstaged in the working tree. A gate run against HEAD
while the change is uncommitted answers a question about the previous commit.

Separately, review is right that duplicate detection only covered the manifest.
The counter was keyed by target and incremented for root edges alone, so a nested
include repeated in the SAME parent was invisible -- and _visit() returns early on
a revisit, so the second edge left no trace at all. The outcome check then saw one
declaration against two rendered titles and passed, because it only asks whether a
title appears AT LEAST as often as it is declared.

Now keyed by (parent, target) and counted for every unconditional edge at any
depth. That keeps the distinction that matters: one parent including one file
twice renders it twice and is a defect; two different parents including the same
fragment is what a fragment is for. Conditional edges stay excluded, at the root
and nested, since two mutually exclusive branches are one rendering.

Probed all three:

    nested duplicate, same parent     old 0 -> new 1
    root duplicate                    old 1 -> new 1   (regression intact)
    same fragment from a 2nd parent   new 0            (still allowed)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shai-almog added a commit that referenced this pull request Sep 3, 2026
* Gates for the guide defects Asciidoctor cannot see

Four checks, split out of the structural repairs they were written alongside so
each can be reviewed on its own terms. Every one was verified to fail when the
defect it describes is reintroduced, and to pass again when it is reverted.

check-guide-structure.py -- every document is in the book or declared out of it;
no chapter silently becomes a part, a subsection, or a duplicate; and, by
rendering the book and reading its headings back, every included chapter's title
survives into the output. That last check is what would have caught a whole
chapter rendering as subsections of the one before it.

check-guide-xrefs.py -- every internal link resolves, and none renders as a bare
"[some-id]". Asciidoctor reports neither: a reference carrying link text renders
as an ordinary link to nowhere.

check-missing-code-blocks.py -- prose that promises a listing where a hole
follows. 406 exist, from the snippet extraction in bbdc6058f0, recoverable from
bbdc6058f0~1.

check-guide-links.py -- links checked against the paths the site actually
serves, derived from _redirects and the Hugo content tree and followed through
every redirect rule to what it produces, rather than accepted for matching one.

The two ratchets may only shrink; growing one needs --allow-new, so recording
debt is deliberate rather than a side effect of regenerating.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Bank the four holes the constraints chapter no longer has

The stacked content change restored the video capture constraints examples, so
their baseline entries went stale and the ratchet refused to pass until they
were removed. 406 to 402.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* An unrelated subsection must not stand in for a swallowed chapter

The outcome check compared counts of normalized headings at any depth, which a
collision satisfies: "Analytics" is a chapter and also a subsection of Commerce,
so the rendered book contains it twice. If the chapter were swallowed, the
subsection kept the count at one and the check -- the central safeguard of this
change -- passed. Measured on the current book: analytics appears twice at any
level and once at chapter level; "getting started" four times at any level and
never at chapter level.

Manifest entries are now counted against chapter-level headings only. Nested
fragments still use the any-level count, because their depth is whatever their
parent gives them.

Second finding: resolves() tried every matching redirect rule and passed if any
chain worked. The host applies the FIRST match and stops, so a link matching an
early rule that leads somewhere deleted would have passed on the strength of a
later rule the reader never reaches. It now returns the first match's result. No
guide link changes verdict today; the point is that the model matches the
behaviour it claims to model.

Also declined, with the reasoning left in the code rather than a review thread:
adding Markdown's three-backtick fence to the literal-block list. The guide
contains none, and validate-guide-snippets.py requires every listing to be
[source,LANG] with a bare include:: inside ---- delimiters, so such a block
would fail that gate before reaching this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* The ifeval branch of the conditional test could never match

The pattern required empty brackets, which is right for ifdef and ifndef -- with
content they are the single-line form and guard only that line -- but wrong for
ifeval, which has no single-line form and always carries its expression in the
brackets. So the directive was listed and never recognised.

Latent: the manifest contains no ifeval, and its only conditionals are two
single-line ifdef attribute assignments, which are still correctly treated as
not opening a block. Fixed because a pattern that names three directives and
handles two is a bug in what the code claims, not a missing feature.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Record why commented-out URLs are checked like any other

Review asked for AsciiDoc comment-state tracking so that a URL inside a `//`
line or a `////` block is skipped. Declining, and recording it where the next
reader of this loop will look rather than in a review thread nobody reads.

The guide has 120 files, zero commented-out URLs and zero `////` blocks, so the
tracking would govern nothing that exists. It would also weaken the gate: with
comments skipped, commenting a line out makes its finding disappear and lets the
ratchet shrink, banking a "fix" while the dead link stays in the source waiting
to be uncommented. Deleting the link is the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Four links left the book to re-enter it, and nothing checked the anchor

Review asked for fragment validation on same-site URLs. Measuring first: the
guide holds 42 same-site URLs with a fragment, and 38 are /javadoc/ -- the one
tree this script cannot enumerate, because it is generated from the framework
sources at build time. The other four all pointed at
`developer-guide.html#<anchor>`: this book, linking into its own body.

Those are the same defect as the fifteen `/manual/*.html` deep links already
converted in this stack, missed because they use a different path. They send a
reader out to the web to fetch the page they are reading, they do not work in
the PDF, and a renamed section breaks them silently -- the anchor lives in the
rendered book, so no amount of site-path checking can see it.

So the fix is not to validate the fragment, it is to stop writing the link:
all four become `<<anchor,text>>` xrefs, which check-guide-xrefs.py already
resolves against the rendered anchors. All three targets exist today, so this
repairs no live 404 -- it moves four unguarded links under a gate.

The rule closes the class rather than the four instances: any codenameone.com
link whose path is one of this book's own routes and which carries a fragment is
now reported. Verified non-vacuous by restoring one of the four and watching it
fail, then reverting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Two silent blind spots in the guards become loud refusals

Both findings describe constructs the guide does not contain, so neither is a
live defect. Both are real holes in the checkers though, and each closes for a
few lines, so they close rather than getting argued with.

Conditional includes (check-guide-structure.py). A manifest entry inside an
ifdef/ifeval was skipped by the duplicate count -- correctly, since the same
chapter under two exclusive branches is one chapter in the output -- and also by
the rendered-title check, since only one branch renders. Skipping both means a
chapter included twice inside a single ACTIVE branch would pass silently. Rather
than teach the checker to distinguish exclusive branches for a construct that
does not exist (measured: zero include:: lines sit inside a conditional anywhere
in the guide), a conditional manifest entry is now refused outright, with a
message saying what to extend if one is ever wanted.

Trailing slashes on file paths (check-guide-links.py). Site paths were compared
with the trailing slash stripped, so a link to `/x.jar/` was validated against
`/x.jar`. The reviewer's premise checks out: _redirects declares the two forms as
separate routes and spells both out where both work, 32 such pairs. Preserving
the slash everywhere would break the many legitimate directory links, so the
narrow case is reported instead -- a last path segment containing a dot, wearing
a trailing slash. Directory routes such as /blog/ and /javadoc/com/codename1/io/
have no dot in the last segment and are untouched.

Both rules verified non-vacuous by introducing an instance and reading the real
exit status rather than a pipeline's: structure and links each exit 1 on the
probe and 0 once it is reverted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Check what swallows a heading, not whether its title turns up somewhere

Review is right that the rendered-title check has slack: it counts normalized
titles across the whole book, so a nested fragment losing its heading can be
covered by an identical heading elsewhere. Confirmed rather than accepted --
removing the leveloffset from both Maven includes takes "Getting started" from 4
rendered headings to 3, while only one file declares it, so `3 >= 1` and the gate
passed a genuinely swallowed section.

Rather than give nested fragments a rendered identity, which the HTML does not
carry, this checks the cause. The spacing rule already existed but ran only on
the root manifest, and its condition was "a blank line follows". Both were wrong.
Reproduced minimally to find the real rule -- a heading is absorbed only when ALL
of these hold:

  * the include is followed immediately by content, and
  * neither this include nor the following one carries leveloffset (asciidoctor
    brackets the content with :leveloffset: attribute entries, and an attribute
    entry closes the paragraph -- the one on the FOLLOWING include lands between
    the paragraph and the heading, so it protects just as well), and
  * the included file does not end on a blank line, and
  * its last line is ordinary paragraph text -- a delimiter, table row, heading,
    attribute entry or comment all close the paragraph. That is why
    _generated-build-hints.adoc, which ends on "|===", does not eat the
    "Versioned builds" heading directly after it.

Against the real book, all four combinations now agree with what asciidoctor
actually renders:

    both leveloffset    4 headings   renders ok   gate passes
    only current        4 headings   renders ok   gate passes
    only following      4 headings   renders ok   gate passes
    neither             3 headings   SWALLOWED    gate fails

An earlier draft exempted only the current include and failed the third row --
a false positive on markup that renders correctly. Every guide include is now
checked, not just the manifest's: measured across all 120 files, this leaves zero
findings, because the five adjacent includes in Maven-Project-Workflow.asciidoc
are protected by leveloffset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* The slash rule rejected routes the site declares, and Hugo could move the rest

Three link findings, and only the first was a live defect -- in the rule I added
one commit earlier.

The trailing-slash rule was unconditional, so it reported "/videos.html/" as
broken even though _redirects declares that exact source and the site serves it.
The rules here all compile slash-insensitively ("^...$/?"), which is right for
matching but discards the distinction the site actually draws, so the raw sources
are now collected alongside them and a slashed file path is reported only when
_redirects does not spell it out. Probed both ways: "/videos.html/" passes,
"/files/CodenameOneBuildClient.jar/" fails.

Hugo route overrides. hugo.toml sets neither [permalinks] nor uglyURLs today, so
deriving a route as "section path + slug" is currently correct; if either
appears, every route underneath moves and this would go on accepting links to
paths Hugo no longer publishes. Modelling a configuration that is not there would
be guesswork, and reading the built public/ tree makes a local run depend on a
tree that may be stale or absent. So it now notices instead: either key present
aborts the check with what to do about it. Probed with each key in turn.

Protocol-relative links and fragments on ordinary same-site pages are declined,
with the measurements recorded in the code rather than in a review thread nobody
reads. The guide has no `link://` macro at all, and its one bare "//host/path" is
a JavaScript string inside a source block, so widening URL_RE to match "//" would
start reporting code as a broken link. And of the 42 same-site URLs carrying a
fragment, 38 are /javadoc/ -- generated at build time, exempt for that reason --
while the other four pointed into this book and are now xrefs, which
check-guide-xrefs.py resolves against the rendered anchors. Nothing is left that
this script could check without building the Hugo site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* The PDF branch was never rendered, and an admonition counted as a listing

Two more holes in the guards. Neither has an instance in the guide today, and
both close cheaply enough that arguing about them would cost more.

check-guide-xrefs.py rendered the book once, with the default backend, so
anything inside ifdef::backend-pdf[] was dropped before a single reference in it
could be examined -- while the workflow publishes an asciidoctor-pdf build from
the same source. Native-Themes.asciidoc already has two such branches. It now
renders twice, the second time with -a backend-pdf, which selects exactly the
content the PDF build includes without needing asciidoctor-pdf here. The two runs
are checked SEPARATELY rather than pooled: an anchor that exists only in the HTML
branch must not satisfy a reference made in the PDF branch. Findings name the
branch they came from.

A/B with a dangling xref planted inside the backend-pdf branch of
Native-Themes.asciidoc:

    old   "Cross-references OK: 1741 anchors" -- exit 0
    new   "Native-Themes.asciidoc:506: <<definitely-not-a-real-anchor>> ...
           (1 reference(s), pdf render)" -- exit 1

check-missing-code-blocks.py accepted any `[attribute]` line as the start of the
promised block, so an admonition standing where a listing used to be hid the
hole. An admonition is prose and can never be the listing a sentence promised, so
the five names are excluded. Excluded by name rather than by whitelisting the
kinds that ARE code: measured, the bracket lines legitimately answering a
promising sentence already span [source] (582), [cols=...] and [options=...] (56),
[listing] (3), [quote] (2) and an anchored image, and a whitelist would report the
next kind nobody anticipated.

A/B with each block planted after a promising sentence, using the real signature
of a removed listing -- the two blank lines it leaves behind:

    [NOTE]          old: none new     new: reported as a hole
    [source,java]   old: none new     new: none new

The baseline is unchanged at 402, because the guide currently has no admonition
sitting in a listing's place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Three links from the old wiki went nowhere, and a directive is not a separator

Two findings, and this time the first one is live.

The rendered book contains exactly three relative hrefs, and all three are
broken: css.asciidoc carried link:Images[], link:Fonts[] and
link:Supported-Properties#text-decoration[] over from the wiki this guide
replaced, naming pages that were never carried across with them. The guide
renders as a single page, so a relative href resolves against wherever that page
is served and reaches nothing that ships with it -- the reader gets a 404 while
every gate reported success. All three targets are sections in that same file:
[[Images]] already existed, [[Fonts]] is added to match it, and
[[text-decoration]] was already there and already used by a sibling reference.

check-guide-xrefs.py now reports any relative href, which after this change has
zero instances. Absolute URLs stay with check-guide-links.py, and root-relative
paths and fragments are excluded. A/B with one link restored: old printed
"Cross-references OK" and exited 0, new reports it and exits 1.

The second is a wrong claim in my own comment. check-guide-structure.py returned
early when an include was followed by ifdef/ifndef/ifeval/endif, on the reasoning
that "a preprocessor directive is not content and cannot absorb a paragraph".
True, and irrelevant: asciidoctor REMOVES the directive during preprocessing, so
it does not separate anything either. Measured -- with `ifdef::backend-html5[]`
between an include ending in prose and a following heading, the render contains
zero heading tags and the paragraph reads "...preferred tools. === A parent-local
heading". The rule now scans past directive lines to the first line that survives
preprocessing. A/B on that construct: old passed it, new reports it.

Note the rendered-title check cannot cover this case either, because a
parent-local heading is not the first heading of any included file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* A Cloudflare function serves /files and /demos, and the PDF surrogate has a limit

The first finding was live: the link ratchet held a working link.

docs/website/functions/[[path]].js runs after context.next() has already 404ed
and then redirects anything under /files/ or /demos/ to
download.codenameone.com. Those paths are served, so the model was missing a
whole class of route and had banked a real one -- /files/iOS_UI-Kit.psd -- as
broken. The destination is off-site, which puts it in the same bucket as every
other off-site redirect: reachable, not verifiable from this repository. The
rules are appended AFTER the _redirects rules because the function is a fallback
and the first matching rule wins, mirroring how the host evaluates. The baseline
shrinks 37 -> 36, and a new /files/ link is now accepted rather than failing CI.

The second is a real limit of the PDF surrogate, and one the command line cannot
remove. Setting backend-pdf makes ifndef::backend-pdf[] content disappear and
ifdef::backend-pdf[] content appear -- verified, and that is the only form this
guide uses. It does not undefine backend-html5: the HTML converter sets that
itself, after command-line attributes are applied, so even `-a backend-html5!`
leaves it defined. Measured both ways. Content guarded on the HTML backend would
therefore survive into the surrogate render and could satisfy a PDF-only
reference that the real asciidoctor-pdf build leaves dangling.

The guide has no such conditional, so rather than model a construct that is not
there -- or drive asciidoctor-pdf and try to read anchors back out of a PDF --
the checker refuses one if it appears, naming the alternative the rest of the
guide already uses. Probed: planting an ifdef::backend-html5[] block aborts the
check with that message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* A five-dash block is still a literal block, and both scanners read inside one

Both scanners matched a delimiter of exactly four characters, so a block opened
with ----- was invisible to them and its contents were scanned as live markup.
The guide has 20 such lines -- eight in Desktop-Integration, six in
Working-With-iOS, six in appendix_goal_generate_archetype -- every one an
ordinary [source] block wrapping an include::. validate-guide-snippets.py already
accepted -{4,}; these two had drifted from it.

Nothing is misread today, because those includes name .java, .xml and .properties
files and the reachability walk skips non-asciidoc targets. The failure mode is
still the interesting one though: it is a FALSE POSITIVE generator, not a missed
defect. A/B with each construct planted inside a five-dash block:

  a displayed include::Working-with-UWP.asciidoc[]
      old  reported a swallowed-heading error against markup that only appears
           on the page as text
      new  clean

  a displayed "The wrapper looks like this:" followed by blank lines
      old  reported it as a missing code block
      new  clean

Both now track the delimiter that opened the block and close on one of the same
character AND length, which is what asciidoctor does -- so a four-dash line
inside a five-dash block is content rather than the close. The set stays limited
to literal blocks: an example (====) or sidebar (****) block contains live
markup, so headings and includes inside one are real and must keep counting.

Baselines unchanged: 119 included documents, 402 holes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Guide figures render with a bundled font instead of the host's (#5668)

* Guide figures render with a bundled font instead of the host's

None of the 24 generated figures could be reproduced outside CI. Running the
generator on a Mac reproduces 0 of 24 byte-for-byte, with 3-20% of pixels
differing. Two consecutive local runs are byte-identical, so the generator is
deterministic on one host; the variable is the font.

`PreAdvancedThemingScreenshots` styled with `Font.createSystemFont`, which
resolves through `JavaSEPort.fontFaceSystem` -- "Arial" on macOS and Linux
alike. Arial exists on a developer's Mac and not on a stock CI runner, so AWT
silently substitutes and every glyph changes. The differences are exactly that:
for `flow-layout.png` every differing pixel sits in y 10-130, the title and
label rows, while the colored blocks below match to the pixel.

So the byte-exact gate has only ever been green because CI both generates and
verifies. A developer regenerating locally could not match it, and at the scale
the guide's remaining ~260 app screenshots would need, that is untenable.

The figures now load `native:MainRegular`, which `JavaSEPort.loadTrueTypeFont`
reads from `/com/codename1/impl/javase/Roboto-Medium.ttf` on the classpath
rather than from an installed-font lookup. This is also what the project's font
rule requires everywhere: never `createSystemFont`, always the `native:` scheme.
A null return refuses loudly rather than falling back to a host font, because a
silent fallback would restore the exact non-determinism this removes.

The committed baselines are regenerated from a Mac. CI byte-compares them on
Linux, so the check either passes -- proving host independence rather than
asserting it -- or fails and says so immediately.

Composition, dimensions and colours are unchanged; only the typeface moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Pin the font on every component, not just the ones that get block colours

CI reported one stale figure out of 24: `guibuilder-2-insets-3.png`, the only
one containing a `TextField`. The other 23 matched Linux byte for byte, so the
bundled-font change worked -- it just did not reach far enough.

`applyBlockStyleToContent` styled `Label` and `Button`. Everything else kept the
theme's default font, which resolves through the host, so the text field and its
hint were still host-dependent. Enumerating the types that carry text would have
left the next one added broken the same way, so the walk now pins the face on
every component it visits, plus the hint label, which is painted by a `Label`
that is not in the component tree and so is never reached by the walk.

The first attempt used `BLOCK_FONT` for this and regressed the figure: at 29px
against the theme default's 13px the field grew and squeezed "Submit" down to a
clipped sliver. `FIELD_FONT` is sized to reproduce the original height, so the
composition is unchanged and only the typeface moves.

Verified locally by rendering twice, once with `JavaSEPort.setFontFaces` pointed
at a family that does not exist -- which is what a machine without Arial looks
like to the port. All 24 come out byte-identical, so nothing in these figures
reads an installed font any more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Publish what the runner actually rendered when a screenshot fails

"Committed screenshot is stale: <name>" names the file and nothing else, so
there is no way to tell a real regression from an environment difference
without adding a debugging round trip to CI. The generated directory is now
uploaded as an artifact when the step fails.

Needed immediately: one figure still differs between a Mac and the runner after
the font fix, and host fonts, JDK version and working directory have each been
ruled out locally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Pin MigLayout's platform, and stop demanding byte equality of a glyph edge

The artifact step paid for itself immediately. Downloading what the runner
rendered showed two figures differing, not the one the gate reported -- it exits
on the first mismatch -- and the two had nothing in common.

`mig-layout.png` differed by 12.8% of its pixels, a real layout shift.
MigLayout takes its default gaps from `PlatformDefaults`, which reads
`System.getProperty("os.name")` and answers MAC_OSX, GNOME or WINDOWS_XP, each
with different spacing. The figure was rendering with macOS gaps on a Mac and
GNOME gaps on the runner. Pinning the platform fixes it: with the pin, 23 of the
24 figures now match the runner's own output byte for byte.

`layered-layout.png` differed by 173 pixels, 0.113%, inside a 25x25 box. That
one is not fixable. Measured against the runner's bytes, the material glyph
lands at exactly the same size and the same origin -- a 55x49 bounding box --
and differs only in antialiased edge coverage, 946 fully-white pixels against
916. Java2D rasterizes the same glyph, from the same bundled font, at the same
size, slightly differently on the two platforms. Demanding byte equality there
would mean deleting legitimate content from the figure or carrying a
permanently red check.

So the comparison moves from `cmp -s` to a comparer that still requires byte
equality by default and accepts a bounded difference only where a figure carries
a `.tolerance` sidecar explaining itself, in the same key=value shape the CN1SS
suites already use. The area bound does the work: a per-pixel delta that large is
meaningless on its own, since a glyph edge flips between white and the block
behind it, but a regression that changed the icon would move far more than 0.3%
of the image.

Verified by running the comparer with the runner's own output against the
committed figures -- which is exactly what CI will do -- and by four probes: an
untoleranced figure that differs fails, the toleranced figure fails when changed
beyond its budget, a missing figure fails the count, and a clean run passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Guard the iOS font branch, and stop the comparator gating itself out

Two review findings on this PR.

The first says `native:MainRegular` does not give host-independent output,
because `loadTrueTypeFont` resolves native fonts to the first installed SF or
Helvetica family before reaching the bundled Roboto. The branch is real, but it
is reached only when `isIOS` is set, which `loadSkinFile` does for a skin whose
systemFontFamily contains "helvetica" -- and this generator never loads a skin.
The measurement agrees: figures rendered on a Mac match the Linux runner byte
for byte, which could not happen if one side were resolving Helvetica Neue and
the other Roboto.

So the conclusion does not hold today, but the risk is real for tomorrow: a
change that loads a skin here would put host fonts back into the output with no
other symptom. The generator now refuses to run under an iOS platform, and says
why, rather than leaving that to a comment nobody reads.

The second finding is straightforwardly right and is the more serious of the
two. `on.pull_request.paths` triggers on `scripts/developer-guide/**`, but the
`Determine changed components` filter named only two scripts, so a pull request
touching any other script here started the workflow with `docs` false and
skipped the steps that script governs. A change to compare-screenshots.py could
have merged without the screenshot check ever running it -- a gate that skips
itself. The filter now covers the whole directory, which also removes the
two-copies-drift the surrounding comment already warns about.

Verified the guard changes no output: all 24 figures are unchanged, and the
comparer still passes against the runner's own bytes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Count every changed pixel toward the area budget, not just the loud ones

The comparator incremented its mismatch counter only for pixels whose channel
delta EXCEEDED maxChannelDelta, which is what the CN1SS comparator does. With a
sidecar written the way this one was -- a large delta bound paired with a small
area bound -- that leaves an unbounded hole, and review gave the exploit in the
figures' own palette: recolouring the green #06a806 to #a608a6 moves every
channel by exactly 160, so with maxChannelDelta=160 not one pixel is counted and
a dramatically different image reports zero mismatches.

Reproduced it before fixing: the recolour changes 4191 pixels, 2.73% of the
image, at a worst delta of exactly 160 -- and passed.

The two bounds are now independent. maxMismatchPercent limits how much of the
image may change at all, counting every differing pixel; maxChannelDelta caps how
far any single pixel may move. The measured legitimate noise -- 173 pixels,
0.113%, worst delta 141 -- still passes, the recolour now fails on area, and a
five-pixel solid overwrite fails on delta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Correct the sidecar's rationale to match how the bounds now work

The previous commit changed the comparator so every differing pixel counts
toward the area budget and the channel delta is a separate ceiling. The sidecar
still explained the old behaviour -- that the area bound did the work and the
delta was meaningless on its own -- which is now wrong in a file whose whole
purpose is to justify the numbers beside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Compare figures in RGBA, because they have an alpha channel

The comparator converted both images to RGB before counting differences. The
committed figures are genuine RGBA PNGs -- four channels, currently opaque
everywhere -- so that dropped a real channel, and any change confined to alpha
was invisible to the tolerance path. A regression that turned the whole figure
transparent while leaving every colour channel intact reported zero changed
pixels and passed.

Byte equality, which every figure without a sidecar is still held to, always
caught this. Only a figure carrying a sidecar could reach the weakened path, so
today the exposure was one image -- but that image is exactly the one whose
comparison is relaxed.

Verified: making layered-layout.png fully transparent with its RGB channels
untouched now reports 100% of pixels changed, the real runner output still
passes, and the recolour and untoleranced-difference probes still fail as they
did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* A comment block is not content, and img/ really does ship beside the page

Two findings, one of them a false positive I introduced last round.

Comment blocks. Asciidoctor drops a //// block entirely, so an include inside
one does not put its target in the book and prose inside one promises the reader
nothing -- but both scanners read straight through. //// joins the fence set in
each. A/B with each construct planted inside a comment block: a displayed
include:: made the structure check fail, and a promising sentence made the
missing-block check fail; both are clean now.

Note this is deliberately NOT symmetric with check-guide-links.py, which still
reads URLs inside comments. The reasons differ and the code says so: a dead link
commented out is still dead debt in the source, and letting the ratchet shrink
for it would make "comment the line out" a way to silence that gate. Reachability
is the opposite -- calling a commented-out include reachable states something
about the book that is simply untrue.

Packaged assets. The relative-href rule added last round was too broad: the HTML
packaging step copies every subdirectory of docs/developer-guide next to
developer-guide.html, so link:img/example.png[] resolves for a reader who opens
the zip, and the gate would have rejected it. A relative href that names an
existing file under the guide directory is now allowed. sketch/ is not, because
that is the one directory the packaging loop skips.

Probed all four ways, since an exemption is only worth having if it still
rejects:

    link:img/flow-layout.png[]        exit 0   (real, packaged)
    link:img/does-not-exist.png[]     exit 1
    link:sketch/whatever.svg[]        exit 1   (not packaged)
    link:Supported-Properties[]       exit 1   (the wiki-era shape)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* The packaging exemption trusted the repository, not the published output

Both findings are defects in the two rules added in the previous commit.

packaged_asset() asked whether the file exists here, which is not the question. A
link only works if the published output carries it, and the two outputs filter
differently:

  * the HTML archive copies only the SUBDIRECTORIES of docs/developer-guide next
    to developer-guide.html and skips sketch, so a root-level file is never in the
    zip however much it exists in the tree;
  * the website rsync excludes sketch/, *.asciidoc and *.adoc, so a source file
    nested inside a packaged directory still does not reach the site.

So link:Introduction.asciidoc[] was being exempted and readers would follow it to
nothing. Both filters are now reproduced. The permissive direction is the
expensive one here -- it suppresses a finding for a link nobody can follow --
so all four cases are probed:

    img/flow-layout.png        exit 0   in both outputs
    Introduction.asciidoc      exit 1   in neither
    img/does-not-exist.png     exit 1
    sketch/foo.svg             exit 1   excluded by both

The backend-conditional rejection scanned raw lines, so a source block DISPLAYING
`ifdef::backend-html5[]` as example text aborted the whole gate. A displayed
directive takes no part in preprocessing. It now tracks literal and comment block
delimiters the way the other guide scanners do. A/B: displayed inside a source
block went 1 -> 0, while a genuinely active conditional still exits 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* The Introduction taught a dead IDE menu, an iTunes walkthrough and 2018 statistics (#5671)

* The Introduction taught a dead IDE menu, an iTunes walkthrough and 2018 statistics

Six corrections to the chapter a new developer reads first:

* The JavaScript target was described twice and contradicted itself: the
  overview said TeaVM did the translation, the port section said ParparVM with
  TeaVM as a fallback. The port section is right -- `javascript.port` is
  declared with `parparvm` as its default and `teavm` documented as "the
  original builder as a compatibility fallback".
* `http://teavm.org:[TeaVM-based builder]` -- the stray colon made the macro a
  bare host, and the site serves TLS. Removed from the link ratchet, which now
  stands at 37.
* Bitcode was cited as something ParparVM absorbed without modification. Apple
  has since withdrawn bitcode, which makes the point better than the arrival
  did, so the sentence now names both moves.
* The device-fragmentation section rested on two 2018 Android share numbers,
  followed by a sentence conceding they would be stale on arrival. The
  structural claim survives without them.
* The iOS developer fee was dated with "for 10 years at the time of this
  writing".
* The UDID instructions walked the reader through iTunes, with a screenshot of
  iTunes on iOS 9.3.5 that also exposed a real device serial and UDID. iTunes
  has not existed on macOS since Catalina, and the signing chapter already gives
  the current answer, so this now points there.
* The device build was "a right click away" via the IDE plugin, illustrated by a
  menu still offering Blackberry, J2ME and Windows Phone builds. Replaced with
  the `cn1:buildAndroid` and `cn1:buildIos` goals, taken from the mojo names.

Both screenshots are deleted, so find_unused_images.py stays satisfied.

Verified: asciidoctor at --failure-level WARN, Vale at suggestion level,
LanguageTool (status ok, 0 matches, run under JDK 17 rather than the JDK 8
false green), paragraph capitalization, snippets, xrefs, structure, links and
missing-code-blocks all clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* "-pl common" made a device build a no-op that reported success

Review caught the command I had just written. AbstractBuildWrapperMojo.execute()
opens with:

    if (!project.isExecutionRoot()) {
        getLog().info("Skipping execution for non-root project");
        return;
    }

and BuildAndroidMojo, BuildIosMojo and BuildWin32Mojo all inherit it unchanged.
Under `-pl common` the selected module is not the execution root, so the goal
logs that line and stops. The design is deliberate -- the wrapper re-invokes
Maven on the root pom itself (`request.setPomFile(new File("pom.xml"))`) -- so
naming a module both skips the wrapper and defeats its purpose.

Measured rather than reasoned, against scripts/hellocodenameone:

    mvn -pl common ...:buildWin32   -> "Skipping execution for non-root project"
                                       BUILD SUCCESS, nothing built
    mvn ...:buildWin32              -> no skip line, proceeds into the reactor

A silent success is the worst failure mode for a getting-started instruction, so
the Introduction now says where to run it and warns about the module form
explicitly.

Working-With-Windows carried the same `-pl common` shape and is fixed with it --
it is where I copied the form from, so leaving it would reintroduce the defect
the next time someone follows the pattern.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* The Windows build wrappers named a platform no profile answers to

Review checked the command this PR documents and found it still builds nothing.
The wrapper passes `codename1.platform` to its nested Maven run, and that
property is what activates the module profile in a generated project's root pom.
The profile is `win`, matching the value the win module itself declares:

    <profile><id>win</id>
      <activation><property>
        <name>codename1.platform</name><value>win</value>
      </property></activation>
      <modules><module>win</module></modules>

BuildWin32Mojo and BuildWindowsDeviceMojo both passed "windows", which matches no
profile at all. Nothing else in the plugin reads the platform as "windows", so
the value was simply inert: the win module never joined the reactor and the
nested build reported success having produced no Windows binary. Every other
wrapper already agrees with its profile -- android, ios, javascript, linux and
javase all match -- so these two were the only ones out of step.

Verified by A/B against scripts/hellocodenameone, reading the NESTED reactor
rather than the outer one:

    before   hellocodenameone, -common, -javase     <- no win module
    after    hellocodenameone, -common, -win

The build TARGET is a separate namespace and stays "windows-device"
(Executor.BUILD_TARGET_WINDOWS_NATIVE), which is correct.

SpotBugs over codenameone-maven-plugin regenerated: 0 findings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* The primary Win32 command had both defects the convenience goal just lost

Fixing BuildWin32Mojo made the sentence beside this snippet false: the goal now
submits a build, while the command the chapter leads with still could not. It
carried both defects at once --

    mvn -pl common package -Dcodename1.platform=windows ...

-- `-pl common` builds that module instead of the reactor, so the win module is
never reached, and `windows` matches no profile even when the reactor is whole.
The two mistakes hid each other: with only the module selected, the platform
value had nothing left to activate.

Corrected to what the wrapper actually runs, read off AbstractBuildWrapperMojo:
goal `package`, `codename1.platform=win`, `codename1.buildTarget=windows-device`,
from the project root.

A/B against scripts/hellocodenameone, reading the reactor, which Maven prints
before it compiles:

    old   no win module in the reactor at all
    new   hellocodenameone, -common, -win

Both invocations then fail identically on this machine with "invalid target
release: 17", because tools/env.sh pins JDK 8 while the demo targets 17. That is
environmental and equal on both sides, so it does not affect the comparison.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Copyright headers for the two Windows wrappers, and duplicate includes per parent

check-copyright-headers went red once #5671 merged in: BuildWin32Mojo and
BuildWindowsDeviceMojo have carried no header since they were added in #5144, and
editing them brought them into the diff the gate examines. Both take the Codename
One GPL + Classpath header, copied from BuildMacNativeMojo in the same package.

I had run that gate locally and read "8 file(s) passed", which was worthless: I
ran it BEFORE `git add`, and it diffs committed refs, so the two files it needed
to see were still sitting unstaged in the working tree. A gate run against HEAD
while the change is uncommitted answers a question about the previous commit.

Separately, review is right that duplicate detection only covered the manifest.
The counter was keyed by target and incremented for root edges alone, so a nested
include repeated in the SAME parent was invisible -- and _visit() returns early on
a revisit, so the second edge left no trace at all. The outcome check then saw one
declaration against two rendered titles and passed, because it only asks whether a
title appears AT LEAST as often as it is declared.

Now keyed by (parent, target) and counted for every unconditional edge at any
depth. That keeps the distinction that matters: one parent including one file
twice renders it twice and is a defect; two different parents including the same
fragment is what a fragment is for. Conditional edges stay excluded, at the root
and nested, since two mutually exclusive branches are one rendering.

Probed all three:

    nested duplicate, same parent     old 0 -> new 1
    root duplicate                    old 1 -> new 1   (regression intact)
    same fragment from a 2nd parent   new 0            (still allowed)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Refuse a conditional include at every depth, not only in the manifest

The previous commit refused conditional includes under `path == self.root` and
left exactly that hole one level down: a nested parent including the same file
twice inside one ACTIVE ifdef recorded neither edge, _visit() deduplicated the
second target, and the rendered-title check only requires the title once. Two
copies render and nothing objects.

The refusal now applies wherever the include sits. Nothing in the guide is
conditional -- still measured at zero include:: lines inside a conditional
anywhere -- and neither check that matters can validate one: the edge count has
to skip a conditional edge because the same file under two exclusive branches is
one rendering, and the rendered-title check has to skip it for the same reason.
Skipping both silently is what let the duplicate through, so the construct is
refused while nothing uses it.

The message also stops hardcoding "developer-guide.asciidoc" as the filename,
which was wrong for every nested parent.

    nested duplicate inside one conditional   old 0 -> new 1
    root conditional                          old 1 -> new 1   (regression intact)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* A typo in the authority hid behind a correct hostname

urlsplit().hostname drops the port whether or not it is a number, so
https://www.codenameone.com:notaport/getting-started/ presented itself as an
ordinary same-site URL and every check below passed on something no browser can
open. Reading .port is what surfaces it: urlsplit defers that parse until the
attribute is read, and then raises.

A numeric nonstandard port is the quieter version of the same problem. The route
model here is derived from _redirects, the Hugo content tree and the static tree,
and all of that describes the site on its default port; :8443 is a different
endpoint the model says nothing about, so validating the path against it would be
accepting an unchecked URL.

Local services keep their ports. The guide documents http://localhost:11434
deliberately -- it is the Ollama endpoint in the AI chapter, and the one URL in
the guide with a port at all -- so the rule is scoped to SITE_HOSTS and leaves
LOCAL_HOSTS alone.

    https://www.codenameone.com:notaport/getting-started/   old 0 -> new 1
    https://www.codenameone.com:8443/getting-started/       old 0 -> new 1
    https://www.codenameone.com/getting-started/            new 0
    http://localhost:11434/api/tags                         new 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* A listing hides content from the reader, not directives from the preprocessor

Three findings, and chasing the middle one turned up that I had been patching on
a false premise for several commits.

I had been treating a literal block as a place where an include:: or an ifdef::
is merely DISPLAYED. It is not. Measured against asciidoctor rather than assumed:

                        inside ---- / ----- / ....      inside ////
    include::           PROCESSED                       dropped
    ifdef:: / ifeval::  ACTIVE                          dropped
    heading / prose     literal text                    dropped

Preprocessor directives are resolved before block parsing, so one written inside
a listing fires exactly as it would outside it; only a comment block removes it.
A literal block hides CONTENT, which is a different question.

So the scanners now split the two. first_heading() and the missing-block scan
keep the full fence set -- a heading or a promising sentence inside a listing
really is only text. The include walk, the conditional test and the
backend-conditional rejection use a comment-block fence only. Two earlier commits
had extended literal-block skipping to those three, which made them disagree with
the renderer; verified against the real book:

    include inside ----     3 renders (a duplicate)   gate now objects
    include inside ////     2 renders (dropped)       gate now quiet

That also means the review premise here -- that a displayed conditional inside a
source block misclassifies a later include -- does not hold, and the code says so
where the next reader will look.

The other two findings stand and are fixed. An explicit default port reaches the
identical endpoint, so https://www.codenameone.com:443/ is accepted while :8443
is still rejected. And ifeval::["{backend}" == "html5"] is as unmodellable as the
boolean attribute: the surrogate sets backend-pdf on an HTML render, so
backend-html5 stays defined AND {backend} still reads "html5". Both spellings are
now refused.

One crash caught by probing rather than by the clean run: COMMENT_FENCE_RE had no
capture group while the caller reads group(1). The guide has no //// block, so
that branch never executed and every gate stayed green over a latent IndexError.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* The DNS root dot walked straight past the same-site checks

urlsplit lowercases a host but keeps the root label's trailing dot, so
"https://www.codenameone.com./does-not-exist" carried the hostname
"www.codenameone.com." -- which is not in SITE_HOSTS. It therefore skipped the
route model, the self-link rule, the slash rule and the port rule in one go, and
was accepted, while the identical path without the dot is reported. DNS treats
the two spellings as the same name, so the dot is stripped before the host is
classified.

The guide contains no such URL today; this closes the bypass rather than fixing
an instance.

    https://www.codenameone.com./does-not-exist     old 0 -> new 1
    https://www.codenameone.com/does-not-exist      old 1 -> new 1
    https://www.codenameone.com./getting-started/   new 0   (still fine)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Read the fallback routes from the Function, and close the authority family

Two findings, plus the structural change the last several implied.

The /files and /demos fallbacks were restated here as a literal pair, so deleting
one from docs/website/functions/[[path]].js would have left this accepting links
to a route that no longer exists. They are now read out of the Function itself.
Verified the derivation tracks the file rather than merely looking as though it
does: a /demos/ link passes today, and renaming that startsWith in the Function
makes the same link fail. If the Function is ever rewritten in a shape this
cannot read, the derivation yields nothing and links under those prefixes start
failing -- loudly, which is the safe direction.

asciidoctor-pdf defines basebackend-pdf alongside backend-pdf, and the surrogate
defined only the latter, so ifdef::basebackend-pdf[] content vanished from the
very render meant to inspect it. Both attributes are now set: a dangling
reference inside such a branch is reported.

And the reason for the third change: the last several findings here were all one
shape -- a spelling urlsplit tolerates that SITE_HOSTS membership does not see.
Malformed port, default port, DNS root dot, each fixed individually. That family
has no natural end (userinfo, IPv6 literals, mixed case, IDN), so it is now one
rule instead: a same-site classification requires the authority to be the bare
host and, at most, its own port. Anything else is reported rather than quietly
routed around the model.

    https://user@www.codenameone.com/getting-started/    reported
    https://www.codenameone.com:8443/getting-started/    reported
    https://www.codenameone.com/getting-started/         fine
    https://www.codenameone.com:443/getting-started/     fine
    https://www.codenameone.com./getting-started/        fine

Measured before adding it: the guide has no URL with userinfo, a non-ASCII host,
an IPv6 literal or mixed case in the authority, so this rejects nothing that
exists and forecloses the ones that do not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* An inline directive is content, and an attribute-built URL is unreadable

The spacing scan stepped over every preprocessor directive on its way to the
next surviving line. Only the BLOCK forms vanish, though: ifdef/ifndef with empty
brackets open a region and leave nothing behind, and endif closes one, but the
single-line form carries its content in the brackets and EXPANDS to it. Measured
-- `ifdef::feature[== Inline Chapter]` renders as a real chapter when the
attribute is set. So the scan was walking past a heading that the preceding
paragraph was about to swallow. It now steps over block forms only, and treats a
single-line directive as the content it becomes.

    inline ifdef right after an include    old 0 -> new 1

A URL assembled from an attribute is invisible to the link scan: the declaration
holds a valid site root and the use site holds only "{name}", so the path that
actually ships is never checked. Expanding attributes properly means
reimplementing asciidoctor's resolution, inheritance through includes included,
which is a great deal of machinery for a construct the guide does not use --
measured, zero URL-valued attribute declarations and zero link:{...} targets. So
both spellings are refused instead, which stops the gap opening quietly later.

    :site: https://www.codenameone.com     old 0 -> new 1
    link:{site}/missing[label]             old 0 -> new 1
    an ordinary URL                        new 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Three Javadoc links pointed at a package that is spelt lowercase

The /javadoc/ prefix was accepted wholesale, on the stated grounds that the tree
is generated at build time and cannot be enumerated from the repository. It can:
build_javadocs.sh runs over two fixed source roots, so a package is a directory
and a class page is a .java file. Behind that exemption sat three live 404s --
the guide linked to /javadoc/com/codename1/JavaScript/ three times, and the only
tracked package is lowercase com/codename1/javascript, so a case-sensitive host
served nothing while the gate reported success. macOS hides this: the checkout
answers to both spellings, and only `git ls-files` tells the truth.

All three links are corrected. The exemption is replaced by a derivation over
CodenameOne/src and Ports/CLDC11/src, which rejects an unknown package or class
and still accepts what javadoc emits beyond that model -- top-level index pages,
class-use/ trees, package-summary and nested Outer.Inner.html. Measured against
every javadoc link in the guide: 423 checked, 0 false positives.

    /javadoc/com/codename1/JavaScript/JSObject.html   reported
    /javadoc/com/codename1/ui/NoSuchClass.html        reported
    /javadoc/com/codename1/javascript/JSObject.html   fine
    /javadoc/com/codename1/ui/Form.Inner.html         fine
    /javadoc/com/codename1/ui/class-use/Form.html     fine
    /javadoc/index.html                               fine

An early draft let the wrong-case package through: the guard meant to admit
top-level index pages also admitted any unrecognised package. Probing is what
caught it -- the guide's own links all passed either way, because they had
already been fixed.

Root-relative targets are scanned too. `link:/does-not-exist[]` and
`href="/does-not-exist"` name a website route exactly as an absolute URL does,
but carry no scheme, so URL_RE never saw them, and check-guide-xrefs.py skips
hrefs starting with "/" because they are not same-page anchors. They now go
through the same route model. The guide has none today.

The link text says `com.codename1.javascript` in backticks: the package really is
lowercase, and unmarked it reads to LanguageTool as a misspelling of JavaScript.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* The Javadoc index documented a package the build deliberately removes

Two gaps between what the model claims and what the generators produce.

build_javadocs.sh filters /com/codename1/impl/ out of its source list, passes
-exclude for the package, and then guards that it never reached the output --
three separate refusals. The index built here recorded every .java under the
source roots, so a link to /javadoc/com/codename1/impl/ARImpl.html was accepted
against a page the build goes out of its way not to produce. The exclusion is now
mirrored, subpackages included.

    /javadoc/com/codename1/impl/ARImpl.html   old 0 -> new 1
    /javadoc/com/codename1/ui/Form.html       new 0

Note the first probe of this proved nothing: ImplementationFactory is written
into tempJavaSources by the generator itself, so it was absent from the old index
too and both versions reported it. A class that really is in CodenameOne/src was
needed to tell them apart.

The publication test read only `date`, so Hugo's publishDate and expiryDate were
invisible: scheduling a page forward or expiring it removes the route from the
built site while a guide link to it went on passing. Both are honoured now.
A date-only expiry lapses at midnight, so the day itself is already too late.

    no dates / past date / past publishDate / expires later   published
    future date / future publishDate / expired / draft        not published

Neither construct appears in the repository today -- no guide link into impl, no
page carrying either field -- so both close a gap rather than fix an instance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Javadoc documents public types, and the index was recording every source file

The generator runs javadoc with -protected, which documents public and protected
types only; a top-level type cannot be protected, so in practice it is public or
it gets no page. The index recorded a class for every .java under the source
roots, so a link to a package-private type was accepted against a page that is
never generated -- com.codename1.io.JSONSanitizer is declared `final class`, and
/javadoc/com/codename1/io/JSONSanitizer.html passed.

Only public top-level types are indexed now. Comments are stripped first, because
a sample inside a javadoc block can easily put the word "public" next to a class
name. package-info is dropped by name: it declares no type, and javadoc folds its
content into package-summary.html, which the package check already covers.

Validated against every javadoc link in the guide before trusting it -- 423
checked, 0 rejected -- so this narrows the index by 366 of 2211 sources without
touching a single real link.

    /javadoc/com/codename1/io/JSONSanitizer.html      old 0 -> new 1
    /javadoc/com/codename1/io/Log.html                new 0
    /javadoc/com/codename1/ui/Form.Inner.html         new 0
    /javadoc/com/codename1/io/package-summary.html    new 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* An uppercase scheme was invisible, and the two slash rules had drifted apart

URL_RE required a lowercase scheme, so link:HTTPS://www.codenameone.com/... was
extracted by nothing and skipped every check behind it. Schemes are
case-insensitive; the extraction is now too, and urlsplit already lowercases what
it returns, so nothing downstream changes.

    link:HTTPS://...            /does-not-exist    old 0 -> new 1
    link:HTTPS://...            /getting-started/  new 0
    lowercase, bad route                           unchanged

The trailing-slash rule existed only on the absolute-URL path, so a root-relative
link to a static file with a slash appended -- link:/favicon.ico/ -- was
normalised down to the existing asset and accepted, while the identical absolute
URL was reported. That is the sort of split that happens when the same rule is
written twice, so it is now written once and called from both.

    link:/favicon.ico/          old 0 -> new 1
    link:/videos.html/          new 0   (declared in _redirects)
    link:/developer-guide/      new 0   (a directory route)

Neither spelling appears in the guide today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* A nested type was taken on trust, and an include hid inside an inline conditional

The nested-type branch split Outer.Inner.html at the first dot and checked only
that Outer.java exists, so any invented page was accepted --
/javadoc/com/codename1/ui/Component.DoesNotExist.html passed against a page
javadoc cannot generate. The innermost name is now required to be declared in the
outer type's own file, which is the only place javadoc could find it. Nested
declarations may be protected as well as public, and -protected documents both,
so the visibility test differs from the top-level one.

Validated against the guide before trusting it: 423 javadoc links checked, 0
rejected, including all six distinct nested pages -- ActionEvent.Type,
URLImage.ImageAdapter, TableLayout.Constraint, BrowserComponent.JSRef,
BrowserComponent.JSProxy and the doubly nested
LayeredLayout.LayeredLayoutConstraint.Inset.

    Component.DoesNotExist.html                          old 0 -> new 1
    ActionEvent.Type.html                                new 0
    LayeredLayout.LayeredLayoutConstraint.Inset.html     new 0

Separately, `ifdef::feature[include::chapter.adoc[]]` is expanded and processed by
asciidoctor, but INCLUDE_RE is anchored at the start of the line, so the walker
saw nothing there and the edge went uncounted -- a chapter included both this way
and normally would render twice with the counter still reading one. Refused, for
the same reason every other conditional include is: neither the duplicate count
nor the rendered-title check can model one. The guide contains none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* An invented middle level passed, and xref: was a third spelling nobody read

The nested-type check verified the ends of the chain and discarded everything
between, so an invented middle level was accepted:
/javadoc/com/codename1/ui/CommonProgressAnimations.Fake.CircleProgress.html
passed because both CommonProgressAnimations and CircleProgress are real, while
Fake is not declared anywhere. Every component is checked now.

What this deliberately does not verify is that the names nest in the ORDER given.
That needs brace-depth parsing of the outer source, and a page naming two real
siblings the wrong way round is a much less likely mistake than naming a level
that does not exist. The limit is written down rather than left to be rediscovered.

    CommonProgressAnimations.Fake.CircleProgress.html   old 0 -> new 1
    CommonProgressAnimations.CircleProgress.html        new 0
    Component.DoesNotExist.html                         unchanged

And xref: is a third way to write a root-relative target, alongside link: and a
raw href=. Asciidoctor renders it as a root-relative href, which
check-guide-xrefs.py skips because it is not a same-page anchor, so the route
went unchecked by both gates. Added to the same extraction.

    xref:/does-not-exist[label]     old 0 -> new 1
    xref:/developer-guide/[label]   new 0

Re-validated against the guide: 423 javadoc links, 0 rejected. The guide has no
xref: macro at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* The publication-date fix was inert: the parser never stored the keys

Two rounds ago is_published() learned to honour Hugo's publishDate and
expiryDate. It could not: front_matter() matches an ALLOWLIST --
url|slug|aliases|draft|date -- so neither key was ever stored and both lookups
read empty on every page. The check ran and decided nothing.

I verified that change by calling is_published() with a dict I wrote by hand,
which is exactly the half that already worked. The parser feeding it was never
exercised. Proven now by running both versions over a real file:

    with the fix     parsed ['publishdate'] -> published False
    without it       parsed []              -> published True

The keys are in the allowlist now, matched case-insensitively and stored
lowercased, because Hugo treats front-matter keys case-insensitively and this
tree mixes YAML and TOML. Eight cases pass end to end THROUGH front_matter --
YAML and TOML spellings, future and past publishDate, expired and not-yet-expired
expiryDate, a lowercase key, and draft. The site path count is unchanged at 4739,
so widening the allowlist moved nothing else.

The docstring now says the list is an allowlist, since that is the property that
made a whole check silently do nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Real nesting, real timestamps, and a comment stripper that ate braces

Four findings, and implementing the first one exposed a bug of my own that the
other checks had been quietly relying on not hitting.

Nesting is parsed now rather than approximated. Asking only whether each name is
declared SOMEWHERE in the outer file accepted two siblings written as if one
contained the other -- CommonProgressAnimations.CircleProgress.EmptyAnimation
names two real types, neither inside the other. The chain is walked with brace
depth and an enclosing stack, and recorded only when every level is public or
protected. I had deferred exactly this last round on the grounds that a wrong
ORDER was unlikely; the reviewer produced a concrete instance, so the argument
does not hold.

Doing it needed the sources parsed properly, which is where my comment stripping
turned out to be wrong: it blanked line comments BEFORE string literals, so the
"//" inside "https://..." ate the rest of that line -- opening braces included.
Brace depth went negative at URLImage.java:650 and the file's own class appeared
to close early, which is why three real nested types were rejected on the first
attempt. Blanking is now a single scan with no ordering to get wrong, and is
shared with is_public_type(), which had the same hazard. Checked across all 2211
sources: braces balance in every one.

    CircleProgress.EmptyAnimation (siblings)   reported
    Fake.CircleProgress (invented middle)      reported
    Component.DoesNotExist                     reported
    CircleProgress / ImageAdapter / JSRef      fine
    LayeredLayoutConstraint.Inset              fine

Publication now follows Hugo. publishDate DECIDES availability when set and date
is only the fallback, where before a future `date` alongside a past `publishDate`
was rejected -- a page Hugo publishes. And the comparison keeps the time of day:
the site rebuilds once daily, so a page scheduled for later today is genuinely
absent until then, and truncating to the calendar day called it live. Nine cases
pass end to end through front_matter, both fence styles included.

An attributed admonition is an admonition. [NOTE,caption="Aside"] fell out of the
lookahead, which required the bracket immediately after the name, so it read as
the listing the preceding colon promised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* The self-link rule existed on one branch and not the other

link:/developer-guide/#…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants