From 092f72f2d9f3e18032012418d1b5595408e912a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 11 Feb 2026 20:26:37 +0100 Subject: [PATCH 1/6] Refactor to call createAppleFramework with versioned: true for Darwin --- packages/ferric/src/build.ts | 49 +++++++++++++++++---------- packages/ferric/src/targets.ts | 60 ++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index 36a673ad..56291aa2 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -31,9 +31,11 @@ import { ANDROID_TARGETS, AndroidTargetName, APPLE_TARGETS, + AppleOperatingSystem, AppleTargetName, ensureAvailableTargets, filterTargetsByPlatform, + parseAppleTargetName, } from "./targets.js"; import { generateTypeScriptDeclarations } from "./napi-rs.js"; import { getBlockComment } from "./banner.js"; @@ -299,16 +301,16 @@ export const buildCommand = new Command("build") } if (appleLibraries.length > 0) { - const libraryPaths = await combineLibraries(appleLibraries); + const libraries = await combineAppleLibraries(appleLibraries); const frameworkPaths = await oraPromise( Promise.all( - libraryPaths.map((libraryPath) => + libraries.map((library) => limit(() => - // TODO: Pass true as `versioned` argument for -darwin targets createAppleFramework({ - libraryPath, + libraryPath: library.path, bundleIdentifier: appleBundleIdentifier, + versioned: library.os === "darwin", }), ), ), @@ -389,16 +391,23 @@ export const buildCommand = new Command("build") ), ); -async function createUniversalAppleLibraries(libraryPathGroups: string[][]) { +async function createUniversalAppleLibraries( + groups: { os: AppleOperatingSystem; paths: string[] }[], +): Promise<{ os: AppleOperatingSystem; path: string }[]> { const result = await oraPromise( Promise.all( - libraryPathGroups.map(async (libraryPaths) => { - if (libraryPaths.length === 0) { + groups.map(async ({ os, paths }) => { + if (paths.length === 0) { return []; - } else if (libraryPaths.length === 1) { - return libraryPaths; + } else if (paths.length == 1) { + return [{ os, path: paths[0] }]; } else { - return [await createUniversalAppleLibrary(libraryPaths)]; + return [ + { + os, + path: await createUniversalAppleLibrary(paths), + }, + ]; } }), ), @@ -412,15 +421,21 @@ async function createUniversalAppleLibraries(libraryPathGroups: string[][]) { return result.flat(); } -async function combineLibraries( +type CombinedAppleLibrary = { + path: string; + os: AppleOperatingSystem; +}; + +async function combineAppleLibraries( libraries: Readonly<[AppleTargetName, string]>[], -): Promise { +): Promise { const result = []; const darwinLibraries = []; const iosSimulatorLibraries = []; const tvosSimulatorLibraries = []; for (const [target, libraryPath] of libraries) { - if (target.endsWith("-darwin")) { + const { os } = parseAppleTargetName(target); + if (os === "darwin") { darwinLibraries.push(libraryPath); } else if ( target === "aarch64-apple-ios-sim" || @@ -433,14 +448,14 @@ async function combineLibraries( ) { tvosSimulatorLibraries.push(libraryPath); } else { - result.push(libraryPath); + result.push({ os, path: libraryPath }); } } const combinedLibraryPaths = await createUniversalAppleLibraries([ - darwinLibraries, - iosSimulatorLibraries, - tvosSimulatorLibraries, + { os: "darwin", paths: darwinLibraries }, + { os: "ios", paths: iosSimulatorLibraries }, + { os: "tvos", paths: tvosSimulatorLibraries }, ]); return [...result, ...combinedLibraryPaths]; diff --git a/packages/ferric/src/targets.ts b/packages/ferric/src/targets.ts index 513c27e1..173b1707 100644 --- a/packages/ferric/src/targets.ts +++ b/packages/ferric/src/targets.ts @@ -1,3 +1,4 @@ +import assert from "node:assert"; import cp from "node:child_process"; import { assertFixable } from "@react-native-node-api/cli-utils"; @@ -48,6 +49,65 @@ export const APPLE_TARGETS = [ ] as const; export type AppleTargetName = (typeof APPLE_TARGETS)[number]; +const APPLE_ARCHITECTURES = [ + "aarch64", + "arm64_32", + "arm64e", + "armv7", + "armv7s", + "x86_64", + "x86_64h", + "i386", + "i686", +] as const; +export type AppleArchitecture = (typeof APPLE_ARCHITECTURES)[number]; +export function isAppleArchitecture( + architecture: string, +): architecture is AppleArchitecture { + return (APPLE_ARCHITECTURES as readonly string[]).includes(architecture); +} + +const APPLE_OPERATING_SYSTEMS = [ + "darwin", + "ios", + "tvos", + "visionos", + "watchos", +] as const; +export type AppleOperatingSystem = (typeof APPLE_OPERATING_SYSTEMS)[number]; +export function isAppleOperatingSystem(os: string): os is AppleOperatingSystem { + return (APPLE_OPERATING_SYSTEMS as readonly string[]).includes(os); +} + +const APPLE_VARIANTS = ["sim", "macabi"] as const; +export type AppleVariant = (typeof APPLE_VARIANTS)[number]; +export function isAppleVariant(variant: string): variant is AppleVariant { + return (APPLE_VARIANTS as readonly string[]).includes(variant); +} + +export function parseAppleTargetName(target: AppleTargetName): { + architecture: AppleArchitecture; + os: AppleOperatingSystem; + variant?: AppleVariant; +} { + const [architecture, vendor, os, variant] = target.split("-"); + assert(vendor === "apple", "Expected vendor to be apple"); + assert( + isAppleArchitecture(architecture), + `Unexpected architecture: ${architecture}`, + ); + assert(isAppleOperatingSystem(os), `Unexpected operating system: ${os}`); + assert( + typeof variant === "undefined" || isAppleVariant(variant), + `Unexpected variant: ${variant}`, + ); + return { + architecture, + os, + variant, + }; +} + export const ALL_TARGETS = [...ANDROID_TARGETS, ...APPLE_TARGETS] as const; export type TargetName = (typeof ALL_TARGETS)[number]; From c75f551ad570410d09a2f2f17eef96f10872647c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 11 Feb 2026 21:10:17 +0100 Subject: [PATCH 2/6] Add support for versioned frameworks --- .changeset/rich-weeks-cry.md | 7 + packages/cmake-rn/src/platforms/apple.ts | 2 +- packages/ferric/src/build.ts | 2 +- packages/host/src/node/prebuilds/apple.ts | 200 +++++++++++++++++----- 4 files changed, 169 insertions(+), 42 deletions(-) create mode 100644 .changeset/rich-weeks-cry.md diff --git a/.changeset/rich-weeks-cry.md b/.changeset/rich-weeks-cry.md new file mode 100644 index 00000000..11855a92 --- /dev/null +++ b/.changeset/rich-weeks-cry.md @@ -0,0 +1,7 @@ +--- +"cmake-rn": minor +"ferric-cli": minor +"react-native-node-api": minor +--- + +Add support for building versioned frameworks for Apple Darwin / macOS diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index cb2e7f1b..3f00431c 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -383,7 +383,7 @@ export const platform: Platform = { const [artifact] = artifacts; await createAppleFramework({ libraryPath: path.join(buildPath, artifact.path), - versioned: triplet.endsWith("-darwin"), + kind: triplet.endsWith("-darwin") ? "versioned" : "flat", bundleIdentifier: appleBundleIdentifier, }); } diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index 56291aa2..c3e3e30f 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -309,8 +309,8 @@ export const buildCommand = new Command("build") limit(() => createAppleFramework({ libraryPath: library.path, + kind: library.os === "darwin" ? "versioned" : "flat", bundleIdentifier: appleBundleIdentifier, - versioned: library.os === "darwin", }), ), ), diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index 6e3bfb53..c87313c9 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -22,64 +22,184 @@ export function escapeBundleIdentifier(input: string) { return input.replace(/[^A-Za-z0-9-.]/g, "-"); } +/** Serialize a plist object and write it to the given path. */ +async function writeInfoPlist( + infoPlistPath: string, + plistDict: Record, +) { + await fs.promises.writeFile(infoPlistPath, plist.build(plistDict), "utf8"); +} + +/** Build and write the framework Info.plist to the given path. */ +async function writeFrameworkInfoPlist( + infoPlistPath: string, + libraryName: string, + bundleIdentifier?: string, +) { + await writeInfoPlist(infoPlistPath, { + CFBundleDevelopmentRegion: "en", + CFBundleExecutable: libraryName, + CFBundleIdentifier: escapeBundleIdentifier( + bundleIdentifier ?? `com.callstackincubator.node-api.${libraryName}`, + ), + CFBundleInfoDictionaryVersion: "6.0", + CFBundleName: libraryName, + CFBundlePackageType: "FMWK", + CFBundleShortVersionString: "1.0", + CFBundleVersion: "1", + NSPrincipalClass: "", + }); +} + +/** Update the library binary’s install name so it resolves correctly at load time. */ +async function updateLibraryInstallName( + binaryPath: string, + libraryName: string, + cwd: string, +) { + await spawn( + "install_name_tool", + ["-id", `@rpath/${libraryName}.framework/${libraryName}`, binaryPath], + { outputMode: "buffered", cwd }, + ); +} + type CreateAppleFrameworkOptions = { libraryPath: string; - versioned?: boolean; + kind: "flat" | "versioned"; bundleIdentifier?: string; }; -export async function createAppleFramework({ +/** + * Creates a flat (non-versioned) .framework bundle: + * MyFramework.framework/MyFramework, Info.plist, Headers/ + */ +async function createFlatFramework({ libraryPath, - versioned = false, + frameworkPath, + libraryName, bundleIdentifier, -}: CreateAppleFrameworkOptions) { - if (versioned) { - // TODO: Add support for generating a Versions/Current/Resources/Info.plist convention framework - throw new Error("Creating versioned frameworks is not supported yet"); - } - assert(fs.existsSync(libraryPath), `Library not found: ${libraryPath}`); - // Write a info.plist file to the framework - const libraryName = path.basename(libraryPath, path.extname(libraryPath)); - const frameworkPath = path.join( - path.dirname(libraryPath), - `${libraryName}.framework`, - ); - // Create the framework from scratch - await fs.promises.rm(frameworkPath, { recursive: true, force: true }); +}: { + libraryPath: string; + frameworkPath: string; + libraryName: string; + bundleIdentifier?: string; +}): Promise { await fs.promises.mkdir(frameworkPath); await fs.promises.mkdir(path.join(frameworkPath, "Headers")); - // Create an empty Info.plist file - await fs.promises.writeFile( + await writeFrameworkInfoPlist( path.join(frameworkPath, "Info.plist"), - plist.build({ - CFBundleDevelopmentRegion: "en", - CFBundleExecutable: libraryName, - CFBundleIdentifier: escapeBundleIdentifier( - bundleIdentifier ?? `com.callstackincubator.node-api.${libraryName}`, - ), - CFBundleInfoDictionaryVersion: "6.0", - CFBundleName: libraryName, - CFBundlePackageType: "FMWK", - CFBundleShortVersionString: "1.0", - CFBundleVersion: "1", - NSPrincipalClass: "", - }), - "utf8", + libraryName, + bundleIdentifier, ); const newLibraryPath = path.join(frameworkPath, libraryName); // TODO: Consider copying the library instead of renaming it await fs.promises.rename(libraryPath, newLibraryPath); - // Update the name of the library - await spawn( - "install_name_tool", - ["-id", `@rpath/${libraryName}.framework/${libraryName}`, newLibraryPath], - { - outputMode: "buffered", - }, + await updateLibraryInstallName(libraryName, libraryName, frameworkPath); + return frameworkPath; +} + +/** + * Version identifier for the single version we create. + * Apple uses A, B, ... for major versions; we only ever create one version. + */ +const VERSIONED_FRAMEWORK_VERSION = "A"; + +/** + * Creates a versioned .framework bundle (Versions/Current convention): + * MyFramework.framework/ + * MyFramework -> Versions/Current/MyFramework + * Resources -> Versions/Current/Resources + * Headers -> Versions/Current/Headers + * Versions/ + * A/MyFramework, Resources/Info.plist, Headers/ + * Current -> A + * See: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html + */ +async function createVersionedFramework({ + libraryPath, + frameworkPath, + libraryName, + bundleIdentifier, +}: { + libraryPath: string; + frameworkPath: string; + libraryName: string; + bundleIdentifier?: string; +}): Promise { + const versionsDir = path.join(frameworkPath, "Versions"); + const versionDir = path.join(versionsDir, VERSIONED_FRAMEWORK_VERSION); + const versionResourcesDir = path.join(versionDir, "Resources"); + const versionHeadersDir = path.join(versionDir, "Headers"); + + await fs.promises.mkdir(versionResourcesDir, { recursive: true }); + await fs.promises.mkdir(versionHeadersDir, { recursive: true }); + + await writeFrameworkInfoPlist( + path.join(versionResourcesDir, "Info.plist"), + libraryName, + bundleIdentifier, + ); + + const versionBinaryPath = path.join(versionDir, libraryName); + await fs.promises.rename(libraryPath, versionBinaryPath); + await updateLibraryInstallName( + path.join("Versions", VERSIONED_FRAMEWORK_VERSION, libraryName), + libraryName, + frameworkPath, + ); + + const currentLink = path.join(versionsDir, "Current"); + await fs.promises.symlink(VERSIONED_FRAMEWORK_VERSION, currentLink); + + await fs.promises.symlink( + "Versions/Current/Resources", + path.join(frameworkPath, "Resources"), + ); + await fs.promises.symlink( + "Versions/Current/Headers", + path.join(frameworkPath, "Headers"), ); + await fs.promises.symlink( + path.join("Versions", "Current", libraryName), + path.join(frameworkPath, libraryName), + ); + return frameworkPath; } +export async function createAppleFramework({ + libraryPath, + kind, + bundleIdentifier, +}: CreateAppleFrameworkOptions) { + assert(fs.existsSync(libraryPath), `Library not found: ${libraryPath}`); + const libraryName = path.basename(libraryPath, path.extname(libraryPath)); + const frameworkPath = path.join( + path.dirname(libraryPath), + `${libraryName}.framework`, + ); + await fs.promises.rm(frameworkPath, { recursive: true, force: true }); + + if (kind === "versioned") { + return createVersionedFramework({ + libraryPath, + frameworkPath, + libraryName, + bundleIdentifier, + }); + } else if (kind === "flat") { + return createFlatFramework({ + libraryPath, + frameworkPath, + libraryName, + bundleIdentifier, + }); + } else { + throw new Error(`Unexpected framework kind: ${kind as string}`); + } +} + export async function createXCframework({ frameworkPaths, outputPath, From f174f19ca68ab2f7efdde0033f47cc29096f974d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Feb 2026 20:28:46 +0000 Subject: [PATCH 3/6] Refactor helper functions to use object arguments instead of positional strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kræn Hansen --- packages/host/src/node/prebuilds/apple.ts | 92 ++++++++++++++--------- 1 file changed, 55 insertions(+), 37 deletions(-) diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index c87313c9..df87f7c8 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -23,40 +23,54 @@ export function escapeBundleIdentifier(input: string) { } /** Serialize a plist object and write it to the given path. */ -async function writeInfoPlist( - infoPlistPath: string, - plistDict: Record, -) { +async function writeInfoPlist({ + path: infoPlistPath, + plist: plistDict, +}: { + path: string; + plist: Record; +}) { await fs.promises.writeFile(infoPlistPath, plist.build(plistDict), "utf8"); } /** Build and write the framework Info.plist to the given path. */ -async function writeFrameworkInfoPlist( - infoPlistPath: string, - libraryName: string, - bundleIdentifier?: string, -) { - await writeInfoPlist(infoPlistPath, { - CFBundleDevelopmentRegion: "en", - CFBundleExecutable: libraryName, - CFBundleIdentifier: escapeBundleIdentifier( - bundleIdentifier ?? `com.callstackincubator.node-api.${libraryName}`, - ), - CFBundleInfoDictionaryVersion: "6.0", - CFBundleName: libraryName, - CFBundlePackageType: "FMWK", - CFBundleShortVersionString: "1.0", - CFBundleVersion: "1", - NSPrincipalClass: "", +async function writeFrameworkInfoPlist({ + path: infoPlistPath, + libraryName, + bundleIdentifier, +}: { + path: string; + libraryName: string; + bundleIdentifier?: string; +}) { + await writeInfoPlist({ + path: infoPlistPath, + plist: { + CFBundleDevelopmentRegion: "en", + CFBundleExecutable: libraryName, + CFBundleIdentifier: escapeBundleIdentifier( + bundleIdentifier ?? `com.callstackincubator.node-api.${libraryName}`, + ), + CFBundleInfoDictionaryVersion: "6.0", + CFBundleName: libraryName, + CFBundlePackageType: "FMWK", + CFBundleShortVersionString: "1.0", + CFBundleVersion: "1", + NSPrincipalClass: "", + }, }); } /** Update the library binary’s install name so it resolves correctly at load time. */ -async function updateLibraryInstallName( - binaryPath: string, - libraryName: string, - cwd: string, -) { +async function updateLibraryInstallName({ + binaryPath, + libraryName, + cwd, +}: { + binaryPath: string; + libraryName: string; + cwd: string; +}) { await spawn( "install_name_tool", ["-id", `@rpath/${libraryName}.framework/${libraryName}`, binaryPath], @@ -87,15 +101,19 @@ async function createFlatFramework({ }): Promise { await fs.promises.mkdir(frameworkPath); await fs.promises.mkdir(path.join(frameworkPath, "Headers")); - await writeFrameworkInfoPlist( - path.join(frameworkPath, "Info.plist"), + await writeFrameworkInfoPlist({ + path: path.join(frameworkPath, "Info.plist"), libraryName, bundleIdentifier, - ); + }); const newLibraryPath = path.join(frameworkPath, libraryName); // TODO: Consider copying the library instead of renaming it await fs.promises.rename(libraryPath, newLibraryPath); - await updateLibraryInstallName(libraryName, libraryName, frameworkPath); + await updateLibraryInstallName({ + binaryPath: libraryName, + libraryName, + cwd: frameworkPath, + }); return frameworkPath; } @@ -135,19 +153,19 @@ async function createVersionedFramework({ await fs.promises.mkdir(versionResourcesDir, { recursive: true }); await fs.promises.mkdir(versionHeadersDir, { recursive: true }); - await writeFrameworkInfoPlist( - path.join(versionResourcesDir, "Info.plist"), + await writeFrameworkInfoPlist({ + path: path.join(versionResourcesDir, "Info.plist"), libraryName, bundleIdentifier, - ); + }); const versionBinaryPath = path.join(versionDir, libraryName); await fs.promises.rename(libraryPath, versionBinaryPath); - await updateLibraryInstallName( - path.join("Versions", VERSIONED_FRAMEWORK_VERSION, libraryName), + await updateLibraryInstallName({ + binaryPath: path.join("Versions", VERSIONED_FRAMEWORK_VERSION, libraryName), libraryName, - frameworkPath, - ); + cwd: frameworkPath, + }); const currentLink = path.join(versionsDir, "Current"); await fs.promises.symlink(VERSIONED_FRAMEWORK_VERSION, currentLink); From b09267e958d5271dfb87bf796f3066f43e86afff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 11 Feb 2026 22:03:44 +0100 Subject: [PATCH 4/6] Removing cwd from install_name_tool call --- packages/host/src/node/prebuilds/apple.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index df87f7c8..082e56d7 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -65,16 +65,14 @@ async function writeFrameworkInfoPlist({ async function updateLibraryInstallName({ binaryPath, libraryName, - cwd, }: { binaryPath: string; libraryName: string; - cwd: string; }) { await spawn( "install_name_tool", ["-id", `@rpath/${libraryName}.framework/${libraryName}`, binaryPath], - { outputMode: "buffered", cwd }, + { outputMode: "buffered" }, ); } @@ -110,9 +108,8 @@ async function createFlatFramework({ // TODO: Consider copying the library instead of renaming it await fs.promises.rename(libraryPath, newLibraryPath); await updateLibraryInstallName({ - binaryPath: libraryName, + binaryPath: newLibraryPath, libraryName, - cwd: frameworkPath, }); return frameworkPath; } @@ -162,9 +159,8 @@ async function createVersionedFramework({ const versionBinaryPath = path.join(versionDir, libraryName); await fs.promises.rename(libraryPath, versionBinaryPath); await updateLibraryInstallName({ - binaryPath: path.join("Versions", VERSIONED_FRAMEWORK_VERSION, libraryName), + binaryPath: versionBinaryPath, libraryName, - cwd: frameworkPath, }); const currentLink = path.join(versionsDir, "Current"); From e5d1e7f8ca792b20f70af42b9bfc6449f519323f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 11 Feb 2026 22:04:07 +0100 Subject: [PATCH 5/6] Use path.join in symlink targets --- packages/host/src/node/prebuilds/apple.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index 082e56d7..525e3a9b 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -167,11 +167,11 @@ async function createVersionedFramework({ await fs.promises.symlink(VERSIONED_FRAMEWORK_VERSION, currentLink); await fs.promises.symlink( - "Versions/Current/Resources", + path.join("Versions", "Current", "Resources"), path.join(frameworkPath, "Resources"), ); await fs.promises.symlink( - "Versions/Current/Headers", + path.join("Versions", "Current", "Headers"), path.join(frameworkPath, "Headers"), ); await fs.promises.symlink( From 712d3bcecbb66b8df37362ab870bf60073585d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 12 Feb 2026 07:24:42 +0100 Subject: [PATCH 6/6] Try bumping cmake version --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 99fa9365..2a514763 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -4,7 +4,7 @@ env: # Version here should match the one in React Native template and packages/cmake-rn/src/cli.ts NDK_VERSION: 27.1.12297006 # Building Hermes from source doesn't support CMake v4 - CMAKE_VERSION: 3.31.6 + CMAKE_VERSION: 4.2.2 # Enabling the Gradle test on CI (disabled by default because it downloads a lot) ENABLE_GRADLE_TESTS: true