diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index e7232f5..13ed040 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -33,6 +33,8 @@ jobs: # Cross-compile native libraries - name: Setup Zig uses: goto-bus-stop/setup-zig@v2 + with: + version: 0.12.0 # Run js/ts scripts - name: Set Bun.js diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0f49609..cddebe3 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -11,9 +11,11 @@ jobs: build: strategy: matrix: - os: [ ubuntu-latest, macos-latest ] + os: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.os }} + env: + ORG_GRADLE_PROJECT_RELEASE_SIGNING_ENABLED: false steps: - uses: actions/checkout@v6 @@ -38,6 +40,8 @@ jobs: # Cross-compile native libraries - name: Setup Zig uses: goto-bus-stop/setup-zig@v2 + with: + version: 0.12.0 # Run js/ts scripts - name: Set Bun.js @@ -46,18 +50,18 @@ jobs: bun-version: latest - name: Install multiplatform JDKs - run: | - bun scripts/setupMultiplatformJdks.ts - printf "Exports:\n$(cat ~/java-home-vars-github.sh)" - source ~/java-home-vars-github.sh + run: bun scripts/setupMultiplatformJdks.ts - uses: gradle/actions/setup-gradle@v5 - name: Grant execute permission for scripts + if: runner.os != 'Windows' run: | chmod +x gradlew chmod +x quickjs/native/cmake/zig-ar.sh chmod +x quickjs/native/cmake/zig-ranlib.sh - name: Build - run: ./gradlew build + run: | + ./gradlew publishToMavenLocal + ./gradlew build diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index cc2b826..3155bde 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -30,6 +30,8 @@ jobs: # Cross-compile native libraries - name: Setup Zig uses: goto-bus-stop/setup-zig@v2 + with: + version: 0.12.0 # Run js/ts scripts - name: Set Bun.js diff --git a/buildSrc/src/main/kotlin/com/dokar/quickjs/applyQuickJsNativeBuildTasks.kt b/buildSrc/src/main/kotlin/com/dokar/quickjs/applyQuickJsNativeBuildTasks.kt index 09552d1..00d7df6 100644 --- a/buildSrc/src/main/kotlin/com/dokar/quickjs/applyQuickJsNativeBuildTasks.kt +++ b/buildSrc/src/main/kotlin/com/dokar/quickjs/applyQuickJsNativeBuildTasks.kt @@ -173,7 +173,16 @@ private fun Project.findBuildPlatformsFromStartTaskNames(): List { val isPublishing = taskNames.any { it.contains("publish", ignoreCase = true) } if (isPublishing) { - return Platform.values().toList() + val allPlatforms = Platform.values().toList() + return when (currentPlatform) { + Platform.linux_x64 -> allPlatforms.filter { + it == Platform.linux_x64 || it == Platform.linux_aarch64 + } + Platform.windows_x64 -> allPlatforms.filter { it == Platform.windows_x64 } + Platform.macos_x64, + Platform.macos_aarch64 -> allPlatforms + else -> allPlatforms + } } val isBuild = taskNames.any { it.contains("build", ignoreCase = true) } diff --git a/buildSrc/src/main/kotlin/com/dokar/quickjs/buildQuickJsNativeLibrary.kt b/buildSrc/src/main/kotlin/com/dokar/quickjs/buildQuickJsNativeLibrary.kt index 826538f..8232468 100644 --- a/buildSrc/src/main/kotlin/com/dokar/quickjs/buildQuickJsNativeLibrary.kt +++ b/buildSrc/src/main/kotlin/com/dokar/quickjs/buildQuickJsNativeLibrary.kt @@ -15,8 +15,22 @@ internal fun Project.buildQuickJsNativeLibrary( outputDir: File? = null, withPlatformSuffixIfCopy: Boolean = false, ) { - val libType = if (sharedLib) "shared" else "static" + if (withJni) { + val home = when (platform) { + Platform.windows_x64 -> windowX64JavaHome() + Platform.linux_x64 -> linuxX64JavaHome() + Platform.linux_aarch64 -> linuxAarch64JavaHome() + Platform.macos_x64 -> macosX64JavaHome() + Platform.macos_aarch64 -> macosAarch64JavaHome() + else -> error("Unsupported platform: '$platform'") + } + if (home == null) { + println("Skip building JNI library for '$platform' because JDK is not found.") + return + } + } + val libType = if (sharedLib) "shared" else "static" println("Building $libType native library for target '$platform'...") // Ensure scripts in native/cmake are executable @@ -50,11 +64,11 @@ internal fun Project.buildQuickJsNativeLibrary( val generateArgs = if (withJni) { when (platform) { - Platform.windows_x64 -> commonArgs + ninja + javaHomeArg(windowX64JavaHome()) - Platform.linux_x64 -> commonArgs + ninja + javaHomeArg(linuxX64JavaHome()) - Platform.linux_aarch64 -> commonArgs + ninja + javaHomeArg(linuxAarch64JavaHome()) - Platform.macos_x64 -> commonArgs + ninja + javaHomeArg(macosX64JavaHome()) - Platform.macos_aarch64 -> commonArgs + ninja + javaHomeArg(macosAarch64JavaHome()) + Platform.windows_x64 -> commonArgs + ninja + javaHomeArg(windowX64JavaHome()!!) + Platform.linux_x64 -> commonArgs + ninja + javaHomeArg(linuxX64JavaHome()!!) + Platform.linux_aarch64 -> commonArgs + ninja + javaHomeArg(linuxAarch64JavaHome()!!) + Platform.macos_x64 -> commonArgs + ninja + javaHomeArg(macosX64JavaHome()!!) + Platform.macos_aarch64 -> commonArgs + ninja + javaHomeArg(macosAarch64JavaHome()!!) else -> error("Unsupported platform: '$platform'") } } else { @@ -207,29 +221,19 @@ internal fun Project.buildQuickJsNativeLibrary( /// Multiplatform JDK locations private fun Project.windowX64JavaHome() = - requireNotNull(envVarOrLocalPropOf("JAVA_HOME_WINDOWS_X64")) { - "'JAVA_HOME_WINDOWS_X64' is not found in env vars or local.properties" - } + envVarOrLocalPropOf("JAVA_HOME_WINDOWS_X64") private fun Project.linuxX64JavaHome() = - requireNotNull(envVarOrLocalPropOf("JAVA_HOME_LINUX_X64")) { - "'JAVA_HOME_LINUX_X64' is not found in env vars or local.properties" - } + envVarOrLocalPropOf("JAVA_HOME_LINUX_X64") private fun Project.linuxAarch64JavaHome() = - requireNotNull(envVarOrLocalPropOf("JAVA_HOME_LINUX_AARCH64")) { - "'JAVA_HOME_LINUX_AARCH64' is not found env vars or in local.properties" - } + envVarOrLocalPropOf("JAVA_HOME_LINUX_AARCH64") private fun Project.macosX64JavaHome() = - requireNotNull(envVarOrLocalPropOf("JAVA_HOME_MACOS_X64")) { - "'JAVA_HOME_MACOS_X64' is not found in env vars or local.properties" - } + envVarOrLocalPropOf("JAVA_HOME_MACOS_X64") private fun Project.macosAarch64JavaHome() = - requireNotNull(envVarOrLocalPropOf("JAVA_HOME_MACOS_AARCH64")) { - "'JAVA_HOME_MACOS_AARCH64' is not found in env vars or local.properties" - } + envVarOrLocalPropOf("JAVA_HOME_MACOS_AARCH64") private fun Project.envVarOrLocalPropOf(key: String): String? { val localProperties = Properties() diff --git a/integration-test/.gitignore b/integration-test/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/integration-test/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/integration-test/build.gradle.kts b/integration-test/build.gradle.kts new file mode 100644 index 0000000..c2cc244 --- /dev/null +++ b/integration-test/build.gradle.kts @@ -0,0 +1,38 @@ +import com.dokar.quickjs.disableUnsupportedPlatformTasks + +plugins { + alias(libs.plugins.kotlinMultiplatform) +} + +val quickjsVersion: String = property("VERSION_NAME") as String + +kotlin { + jvm() + mingwX64() + linuxX64() + linuxArm64() + macosX64() + macosArm64() + + applyDefaultHierarchyTemplate() + + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + + sourceSets { + commonTest.dependencies { + implementation(kotlin("test")) + implementation("io.github.dokar3:quickjs-kt:$quickjsVersion") + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.test) + } + } +} + +repositories { + mavenLocal() + mavenCentral() +} + +disableUnsupportedPlatformTasks() diff --git a/integration-test/src/commonTest/kotlin/QuickJsIntegrationTest.kt b/integration-test/src/commonTest/kotlin/QuickJsIntegrationTest.kt new file mode 100644 index 0000000..f48b08f --- /dev/null +++ b/integration-test/src/commonTest/kotlin/QuickJsIntegrationTest.kt @@ -0,0 +1,23 @@ +import com.dokar.quickjs.QuickJs +import com.dokar.quickjs.quickJs +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class QuickJsIntegrationTest { + @Test + fun evalExpression() = runTest { + quickJs { + val result = evaluate("1 + 2") + assertEquals(3, result) + } + } + + @Test + fun evalString() = runTest { + quickJs { + val result = evaluate("'hello' + ' ' + 'world'") + assertEquals("hello world", result) + } + } +} diff --git a/quickjs/native/common/stack_chk.c b/quickjs/native/common/stack_chk.c index f9e96fa..4105cef 100644 --- a/quickjs/native/common/stack_chk.c +++ b/quickjs/native/common/stack_chk.c @@ -1,31 +1,23 @@ #include #include -#include #if defined(_WIN32) -// Default stack guard value -uintptr_t __stack_chk_guard = 0x595e9fbd94fda766; +// Generate a pseudo-random stack canary at compile time. +// Not cryptographically strong, but varies per build which is better +// than a fully static value. Runtime randomization via constructor is +// not viable because it runs before the Windows runtime is initialized +// in the Kotlin/Native static library context. +#define STACK_CHK_SEED ((uint64_t)(__LINE__) * 7 + __COUNTER__ * 13) +#define STACK_CHK_HASH(s) ((s) ^ ((s) >> 16) ^ ((s) << 32)) +uintptr_t __stack_chk_guard = STACK_CHK_HASH(STACK_CHK_SEED + \ + (uint64_t)(__DATE__[0]) * 31 + \ + (uint64_t)(__DATE__[2]) * 37 + \ + (uint64_t)(__TIME__[0]) * 41 + \ + (uint64_t)(__TIME__[1]) * 43); void __stack_chk_fail(void) { abort(); } -typedef BOOLEAN (WINAPI *RtlGenRandomFunc)(PVOID, ULONG); - -// Initialize the stack guard with a random value -__attribute__((constructor)) -static void __stack_chk_init(void) { - HMODULE hAdvApi32 = LoadLibraryA("advapi32.dll"); - if (hAdvApi32) { - RtlGenRandomFunc RtlGenRandom = (RtlGenRandomFunc)GetProcAddress(hAdvApi32, "SystemFunction036"); - if (RtlGenRandom) { - uintptr_t random_guard; - if (RtlGenRandom(&random_guard, sizeof(random_guard))) { - __stack_chk_guard = random_guard; - } - } - FreeLibrary(hAdvApi32); - } -} #endif diff --git a/scripts/setupMultiplatformJdks.ts b/scripts/setupMultiplatformJdks.ts index e76c63f..f0dad1c 100644 --- a/scripts/setupMultiplatformJdks.ts +++ b/scripts/setupMultiplatformJdks.ts @@ -1,4 +1,5 @@ -import { $, ShellOutput } from "bun"; +// @ts-nocheck +import { $ } from "bun"; import * as path from "path"; import * as fs from "fs/promises"; @@ -11,7 +12,10 @@ type Jdk = { sha256: string; }; -const USER_HOME = process.env.HOME!; +const USER_HOME = process.env.HOME || process.env.USERPROFILE; +if (!USER_HOME) { + throw new Error("Cannot determine home directory: neither HOME nor USERPROFILE is set"); +} const JDK_ROOT = path.join(USER_HOME, "jdks"); const JDK_LIST: Jdk[] = [ @@ -66,17 +70,16 @@ async function isFileExists(filepath: string) { } } -async function fileSha256(filepath: string) { - const out = await $`shasum -a 256 ${filepath}`.text(); - return out.split(" ")[0]; +async function fileSha256(filepath: string): Promise { + const file = Bun.file(filepath); + const buffer = await file.arrayBuffer(); + const hasher = new Bun.CryptoHasher("sha256"); + hasher.update(buffer); + return hasher.digest("hex"); } -async function throwIfStderrNotEmpty(output: ShellOutput) { - const err = output.stderr.toString().trim(); - if (err.length > 0) { - throw new Error(err); - } - return output; +async function downloadFile(url: string, destPath: string) { + await $`curl -fsSL -o ${destPath} ${url}`; } async function downloadAndExtractJdk(jdk: Jdk) { @@ -94,21 +97,25 @@ async function downloadAndExtractJdk(jdk: Jdk) { downloaded = true; } else { console.log(`Removing broken ${jdkFullName}...`); - throwIfStderrNotEmpty(await $`rm ${filepath}`); + await fs.unlink(filepath); } } if (!downloaded) { - throwIfStderrNotEmpty(await $`wget --quiet -P ${JDK_ROOT} ${jdk.url}`); + await downloadFile(jdk.url, filepath); } console.log(`Extracting ${jdkFullName}...`); const extractPath = path.join(JDK_ROOT, jdk.name); - await $`mkdir ${extractPath}`; + await fs.mkdir(extractPath, { recursive: true }); if (filename.endsWith(".gz")) { - throwIfStderrNotEmpty(await $`tar -xzf ${filepath} -C ${extractPath}`); + await $`tar -xzf ${filepath} -C ${extractPath}`; } else if (filename.endsWith(".zip")) { - throwIfStderrNotEmpty(await $`unzip -o -q ${filepath} -d ${extractPath}`); + if (process.platform === "win32") { + await $`tar -xf ${filepath} -C ${extractPath}`; + } else { + await $`unzip -o -q ${filepath} -d ${extractPath}`; + } } else { throw new Error(`Unknown JDK archive: ${filename}`); } @@ -117,22 +124,26 @@ async function downloadAndExtractJdk(jdk: Jdk) { } async function createEnvVars(vars: (readonly [string, string])[]) { + // Write directly to GITHUB_ENV if in CI + const githubEnvFile = process.env.GITHUB_ENV; + if (githubEnvFile) { + let content = ""; + for (const [varName, varValue] of vars) { + content += `${varName}=${varValue}\n`; + } + await fs.appendFile(githubEnvFile, content); + console.log("Exported env variables to GITHUB_ENV"); + return; + } + + // Otherwise write shell scripts for local use const varExportsFilepath = path.join(USER_HOME, "java-home-vars.sh"); - const ghVarsExportsFilepath = path.join( - USER_HOME, - "java-home-vars-github.sh" - ); let varExports = "# Generated by the JDK setup script."; - let ghVarExports = "# Generated by the JDK setup script."; - for (const envVar of vars) { - const varName = envVar[0]; - const varValue = envVar[1]; + for (const [varName, varValue] of vars) { varExports += `\nexport ${varName}=${varValue}`; - ghVarExports += `\necho "${varName}=${varValue}" >> $GITHUB_ENV`; } await fs.writeFile(varExportsFilepath, varExports); - await fs.writeFile(ghVarsExportsFilepath, ghVarExports); console.log(); console.log( @@ -141,10 +152,6 @@ async function createEnvVars(vars: (readonly [string, string])[]) { console.log(); console.log(`>>> source ~/${path.basename(varExportsFilepath)}`); console.log(); - console.log("Or export env variables in GitHub CI:"); - console.log(); - console.log(`>>> source ~/${path.basename(ghVarsExportsFilepath)}`); - console.log(); } const start = Date.now(); @@ -155,7 +162,7 @@ console.log(); console.log("JDK ROOT:", JDK_ROOT); console.log(); -await $`mkdir ${JDK_ROOT}`; +await fs.mkdir(JDK_ROOT, { recursive: true }); // Download and extract const envVars = await Promise.all( diff --git a/settings.gradle.kts b/settings.gradle.kts index b1a4549..b00e3fc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,3 +35,4 @@ include(":samples:repl") include(":samples:openai") include(":samples:openai-android") include(":benchmark") +include(":integration-test")