diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml index 6a80f31e..7d6003ba 100644 --- a/.github/workflows/desktop.yml +++ b/.github/workflows/desktop.yml @@ -51,6 +51,31 @@ jobs: # Using --console=plain so logs are readable in GitHub Actions run: ./gradlew :python-multiplatform:desktopTest --console=plain + desktop-native-linux: + name: Desktop Kotlin/Native Tests (Linux x86_64) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'zulu' + cache: 'gradle' + + - name: Cache Konan + uses: actions/cache@v4 + with: + path: ~/.konan + key: ${{ runner.os }}-konan-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-konan- + + - name: Run Linux x86_64 Kotlin/Native Tests + run: ./gradlew :python-multiplatform:linuxX64Test --rerun --console=plain + # The dev agent's protocol on the other desktop OSes (#238). Its socket options differ per platform # (SO_EXCLUSIVEADDRUSE on Windows, SO_REUSEADDR on POSIX), and the port-takeover and restart tests are # what check them. Stdlib only: no JDK, no Gradle. diff --git a/docs/SPEC.md b/docs/SPEC.md index 2e65b6f4..13e3a29f 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -26,12 +26,13 @@ fail, then implement. | Platform | Kotlin target(s) | Downcall mechanism | Upcall entry | Test path | |---|---|---|---|---| | Desktop JVM (macOS; Linux/Windows wired, unverified) | `jvm("desktop")` | Panama FFM, `invokeExact` | FFM upcall stub | `desktopTest → jvmTest → commonTest` | +| Desktop native (macOS Arm64, Linux x86_64) | `macosArm64`, `linuxX64` | cinterop | `@CName` symbol | `{macosArm64Test, linuxX64Test} → desktopNativeTest → nativeTest → commonTest` | | Android (ART) | `androidTarget` | JNI via `RegisterNatives` | JNI upcall | `androidInstrumentedTest → jvmTest → commonTest` (device) | | iOS | `iosArm64`, `iosSimulatorArm64`, `iosX64` | cinterop | `@CName` symbol | `iosSimulatorArm64Test → nativeTest → commonTest` | | Android native | `androidNativeArm64`, `androidNativeX64` | cinterop | `@CName` symbol | `androidNativeArm64Test → artTest → nativeTest → commonTest` (device, via adb) | | Web (experimental) | `wasmJs` (Node, browser) | JS / Emscripten CPython | `@WasmExport` | `wasmJsTest` (Node suite; browser subset) | -- **P-1** The library compiles for every target above. `Status: implemented` for desktop, Android, +- **P-1** The library compiles for every target above. `Status: implemented` for desktop, `macosArm64`, `linuxX64`, Android, iosSimulatorArm64, androidNativeArm64 and wasmJs (each has a test source set). `partial` for `iosArm64`, `iosX64`, `androidNativeX64` (compile-wired, no tests run there). - **P-2** Linux and Windows desktop: the build downloads and links the right CPython archive. diff --git a/python-multiplatform/build.gradle.kts b/python-multiplatform/build.gradle.kts index 46e46c00..bb3ac482 100644 --- a/python-multiplatform/build.gradle.kts +++ b/python-multiplatform/build.gradle.kts @@ -89,6 +89,7 @@ buildkonfig { } val libVersion = pythonVersion.split('.').subList(0, 2).joinToString(".") +val abiSuffix = if (pythonFreeThreaded) "t" else "" /** * The `ftp/python//` directory holding a release, which drops any pre-release suffix: @@ -1245,7 +1246,8 @@ kotlin { */ listOf( iosArm64(), iosSimulatorArm64(), iosX64(), - androidNativeArm64(), androidNativeX64() + androidNativeArm64(), androidNativeX64(), + macosArm64(), linuxX64() ).forEach { nativeTarget -> nativeTarget.apply { val targetABI = when(konanTarget) { @@ -1254,16 +1256,20 @@ kotlin { IOS_ARM64 -> "ios-arm64" IOS_X64 -> "ios-arm64_x86_64-simulator" IOS_SIMULATOR_ARM64 -> "ios-arm64_x86_64-simulator" + MACOS_ARM64 -> "macos-aarch64" + LINUX_X64 -> "linux-x86_64" else -> throw RuntimeException("Unsupported ABI: $konanTarget") } val targetExtractDir = when(konanTarget.family) { Family.ANDROID -> "$extractedDir/android-${if (targetABI == "arm64-v8a") "aarch64" else "x86_64"}/prefix" Family.IOS -> "$extractedDir/ios/Python.xcframework" + Family.OSX, Family.LINUX -> "$extractedDir/$targetABI$desktopFlavourSuffix/python" else -> throw RuntimeException("Unsupported target family: ${konanTarget.family}") } val targetIncludePath = when(konanTarget.family) { Family.ANDROID -> "$targetExtractDir/include/python$libVersion" Family.IOS -> "$targetExtractDir/$targetABI/Python.framework/Headers" + Family.OSX, Family.LINUX -> "$targetExtractDir/include/python$libVersion$abiSuffix" else -> throw RuntimeException("Unsupported target family: ${konanTarget.family}") } @@ -1331,6 +1337,28 @@ kotlin { "-framework", "Python", "-F$targetExtractDir/$targetABI", "-rpath", "$targetExtractDir/$targetABI" )) + } else if (konanTarget.family == Family.OSX) { + getTest(NativeBuildType.DEBUG).linkerOpts.addAll(listOf( + "-L$targetExtractDir/lib", + "-lpython$libVersion$abiSuffix", + "-Wl,-rpath,$targetExtractDir/lib" + )) + } else if (konanTarget.family == Family.LINUX) { + // python-build-standalone ships libpythonX.Y.so as a symlink to libpythonX.Y.so.1.0, + // which Gradle's tarTree extracts as a 0-byte file; stageDesktopLibraries resolves + // it into a real libpythonX.Y.so under build/desktop-libs/lib/linux-x86_64. + val stagedLinuxLibDir = layout.buildDirectory.dir("desktop-libs/lib/linux-x86_64").get().asFile.absolutePath + getTest(NativeBuildType.DEBUG).apply { + linkerOpts.addAll(listOf( + "-L$stagedLinuxLibDir", + "-L$targetExtractDir/lib", + "-lpython$libVersion$abiSuffix", + "-Wl,-rpath,$targetExtractDir/lib" + )) + linkTaskProvider.configure { + dependsOn("stageDesktopLibraries") + } + } } } } @@ -1426,6 +1454,15 @@ kotlin { androidNativeX64Main.dependsOn(artMain) androidNativeArm64Main.dependsOn(artMain) + val desktopNativeMain by creating { + kotlin.srcDir("src/desktopNativeMain/kotlin") + } + val macosArm64Main by getting + val linuxX64Main by getting + desktopNativeMain.dependsOn(nativeMain) + macosArm64Main.dependsOn(desktopNativeMain) + linuxX64Main.dependsOn(desktopNativeMain) + // `src/nativeTest` existed as a directory long before this line, and nothing pointed at // it -- so its `GCLeakTest.native.kt` was dead source, and the same file had been copied // byte-for-byte into all five target test source sets to compensate. This is the mirror of @@ -1439,6 +1476,13 @@ kotlin { "androidNativeX64Test", "androidNativeArm64Test", ).forEach { getByName(it).dependsOn(nativeTest) } + val desktopNativeTest by creating { + kotlin.srcDir("src/desktopNativeTest/kotlin") + } + desktopNativeTest.dependsOn(nativeTest) + listOf("macosArm64Test", "linuxX64Test") + .forEach { getByName(it).dependsOn(desktopNativeTest) } + // ...and `artTest` completes the mirror for the other half: things true of androidNative // but not of iOS. Without it such a test has nowhere to live except duplicated into both // androidNative target source sets, which is the arrangement the comment above describes @@ -1452,6 +1496,18 @@ kotlin { } } +tasks.named("macosArm64Test") { + dependsOn("downloadPython_macos_aarch64") + val pythonHome = file("$extractedDir/macos-aarch64$desktopFlavourSuffix/python").absolutePath + environment("PYTHONHOME", pythonHome) +} + +tasks.named("linuxX64Test") { + dependsOn("downloadPython_linux_x86_64") + val pythonHome = file("$extractedDir/linux-x86_64$desktopFlavourSuffix/python").absolutePath + environment("PYTHONHOME", pythonHome) +} + /** * The iOS `Python.framework` ships only the interpreter binary and headers, it carries no * standard library. `Py_Initialize()` therefore aborts the process with diff --git a/python-multiplatform/src/desktopNativeMain/kotlin/python/multiplatform/env/PackagedPythonHome.desktopNative.kt b/python-multiplatform/src/desktopNativeMain/kotlin/python/multiplatform/env/PackagedPythonHome.desktopNative.kt new file mode 100644 index 00000000..a338dc59 --- /dev/null +++ b/python-multiplatform/src/desktopNativeMain/kotlin/python/multiplatform/env/PackagedPythonHome.desktopNative.kt @@ -0,0 +1,100 @@ +package python.multiplatform.env + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import platform.posix.S_IFDIR +import platform.posix.S_IFMT +import platform.posix.stat + +/** + * The directory containing the running executable, or null when the OS cannot report it. + */ +internal expect fun executableDirectory(): String? + +/** + * Where a desktop Kotlin/Native application (`macosArm64`, `linuxX64`) finds its packaged CPython + * prefix (SPEC L-9 / P-1, #191). + * + * A desktop native binary has no JVM, no system properties and no launcher to set `PYTHONHOME`: + * like a GraalVM native image (#246), its prefix ships beside the executable under + * [DIRECTORY_NAME] (or, inside a macOS `.app` bundle, under `../Resources/`[DIRECTORY_NAME]). + */ +internal object DesktopNativePythonHome { + + /** Matches `PackagedPythonHome.DIRECTORY_NAME` (`jvmMain`) and `IosPythonHome.DIRECTORY_NAME` (`iosMain`). */ + const val DIRECTORY_NAME = "python-multiplatform-home" + + /** A resolved prefix and a human-readable account of where it came from, for error messages. */ + data class Resolved(val path: String, val source: String) + + /** [resolve] against this process's environment and executable directory. */ + fun resolve(): Resolved? = resolve( + environmentHome = readEnvVar("PYTHONHOME"), + executableDir = executableDirectory(), + isDirectory = ::isDirectory, + ) + + /** + * The packaged prefix, or null when there is none to apply. + * + * 1. A non-blank `PYTHONHOME` in the environment wins, and the answer is null: CPython reads it + * itself and [PythonHomeCheck] already checked it. This keeps the Gradle host test tasks + * (`macosArm64Test`, `linuxX64Test`) and an explicit caller environment working unchanged. + * 2. `/`[DIRECTORY_NAME], when that directory exists. + * 3. `/../Resources/`[DIRECTORY_NAME], when that directory exists (a macOS + * `.app` bundle places the binary in `Contents/MacOS/` and resources in `Contents/Resources/`). + */ + fun resolve( + environmentHome: String?, + executableDir: String?, + isDirectory: (String) -> Boolean, + ): Resolved? { + if (!environmentHome.isNullOrBlank()) return null + val dir = executableDir?.takeIf { it.isNotBlank() }?.trimEnd('/') ?: return null + val beside = "$dir/$DIRECTORY_NAME" + if (isDirectory(beside)) { + return Resolved(beside, "$DIRECTORY_NAME beside the executable ($dir)") + } + val parent = dir.substringBeforeLast('/', missingDelimiterValue = "") + if (parent.isNotEmpty()) { + val resources = "$parent/Resources/$DIRECTORY_NAME" + if (isDirectory(resources)) { + return Resolved(resources, "$DIRECTORY_NAME in the app bundle's Resources directory ($parent/Resources)") + } + } + return null + } + + /** `stat(2)`'s answer: a directory exists at [path] (a regular file there is not one). */ + @OptIn(ExperimentalForeignApi::class) + internal fun isDirectory(path: String): Boolean = memScoped { + val info = alloc() + if (stat(path, info.ptr) != 0) return false + (info.st_mode.toInt() and S_IFMT) == S_IFDIR + } +} + +/** + * Hands a packaged prefix to CPython with `Py_SetPythonHome` before `Py_Initialize()`. + * + * `Py_DecodeLocale` is documented by CPython as safe to call before initialisation and produces + * storage that lives for the duration of the process, as `Py_SetPythonHome` requires. + */ +@OptIn(ExperimentalForeignApi::class) +@Suppress("DEPRECATION") +internal actual fun applyPackagedPythonHome() { + val home = DesktopNativePythonHome.resolve() ?: return + try { + PythonHomeCheck.verifyOrThrow(home.path) + } catch (e: IllegalStateException) { + throw IllegalStateException( + "The Python home this application was packaged with (${home.source}) is unusable: ${e.message}", + e, + ) + } + val wide = python.native.ffi.bindings.Py_DecodeLocale(home.path, null) + ?: throw IllegalStateException("Py_DecodeLocale could not decode the packaged Python home '${home.path}'.") + python.native.ffi.bindings.Py_SetPythonHome(wide) +} diff --git a/python-multiplatform/src/desktopNativeMain/kotlin/python/multiplatform/env/PythonPayload.desktopNative.kt b/python-multiplatform/src/desktopNativeMain/kotlin/python/multiplatform/env/PythonPayload.desktopNative.kt new file mode 100644 index 00000000..cb3c3d1e --- /dev/null +++ b/python-multiplatform/src/desktopNativeMain/kotlin/python/multiplatform/env/PythonPayload.desktopNative.kt @@ -0,0 +1,24 @@ +package python.multiplatform.env + +/** + * Desktop Kotlin/Native's answer to "where did the payload land": `python/` beside the executable + * or, in a macOS `.app` bundle, under `../Resources/python`. + */ +internal actual fun discoverStagedPayloadRoots(): List = + discoverStagedPayloadRoots(executableDirectory(), ::pathIsAccessible) + +internal fun discoverStagedPayloadRoots( + executableDir: String?, + isAccessible: (String) -> Boolean, +): List { + val dir = executableDir?.takeIf { it.isNotBlank() }?.trimEnd('/') ?: return emptyList() + val roots = mutableListOf() + val beside = "$dir/${PythonPayload.PAYLOAD_ROOT}" + if (isAccessible(beside)) roots += beside + val parent = dir.substringBeforeLast('/', missingDelimiterValue = "") + if (parent.isNotEmpty()) { + val resources = "$parent/Resources/${PythonPayload.PAYLOAD_ROOT}" + if (resources != beside && isAccessible(resources)) roots += resources + } + return roots +} diff --git a/python-multiplatform/src/desktopNativeTest/kotlin/python/multiplatform/env/DesktopNativePythonHomeTest.kt b/python-multiplatform/src/desktopNativeTest/kotlin/python/multiplatform/env/DesktopNativePythonHomeTest.kt new file mode 100644 index 00000000..28fb4543 --- /dev/null +++ b/python-multiplatform/src/desktopNativeTest/kotlin/python/multiplatform/env/DesktopNativePythonHomeTest.kt @@ -0,0 +1,93 @@ +package python.multiplatform.env + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Where a desktop Kotlin/Native application (`macosArm64`, `linuxX64`) resolves its packaged + * CPython prefix and payload from. + */ +class DesktopNativePythonHomeTest { + + private val besideBinary = "/apps/demo/python-multiplatform-home" + private val inAppBundle = "/apps/Demo.app/Contents/Resources/python-multiplatform-home" + + @Test + fun theDirectoryNameMatchesThePluginAndDesktopAndIos() { + assertEquals("python-multiplatform-home", DesktopNativePythonHome.DIRECTORY_NAME) + } + + @Test + fun anEnvironmentPythonHomeWinsAndNothingIsApplied() { + assertNull( + DesktopNativePythonHome.resolve( + environmentHome = "/from/env", + executableDir = "/apps/demo", + isDirectory = { true }, + ) + ) + } + + @Test + fun thePrefixBesideTheExecutableIsUsedWhenPresent() { + for (environment in listOf(null, "", " ")) { + val resolved = DesktopNativePythonHome.resolve( + environmentHome = environment, + executableDir = "/apps/demo", + isDirectory = { it == besideBinary }, + ) + assertEquals(besideBinary, resolved?.path, "PYTHONHOME='$environment'") + assertTrue("beside the executable" in resolved!!.source, resolved.source) + } + } + + @Test + fun anAppBundleResourceDirectoryIsCheckedWhenNothingSitsBesideTheBinary() { + val resolved = DesktopNativePythonHome.resolve( + environmentHome = null, + executableDir = "/apps/Demo.app/Contents/MacOS", + isDirectory = { it == inAppBundle }, + ) + assertEquals(inAppBundle, resolved?.path) + assertTrue("Resources" in resolved!!.source, resolved.source) + } + + @Test + fun anUnpackagedExecutableBehavesAsBefore() { + assertNull(DesktopNativePythonHome.resolve(null, "/apps/demo") { false }) + assertNull(DesktopNativePythonHome.resolve(null, null) { true }) + assertNull(DesktopNativePythonHome.resolve(null, "") { true }) + } + + @Test + fun theHostTestTaskPassesPythonHomeAndExecutableDirectoryResolves() { + val home = assertNotNull(readEnvVar("PYTHONHOME"), "the host test task must pass PYTHONHOME") + assertNull(DesktopNativePythonHome.resolve()) + val exeDir = assertNotNull(executableDirectory(), "executableDirectory() returned null") + assertTrue(DesktopNativePythonHome.isDirectory(exeDir), exeDir) + assertTrue(DesktopNativePythonHome.isDirectory(home), home) + assertFalse(DesktopNativePythonHome.isDirectory("$home/does-not-exist")) + } + + @Test + fun payloadDiscoveryFindsBesideExecutableAndBundleResources() { + assertEquals( + listOf("/apps/demo/python"), + discoverStagedPayloadRoots("/apps/demo") { it == "/apps/demo/python" }, + ) + assertEquals( + listOf("/apps/Demo.app/Contents/Resources/python"), + discoverStagedPayloadRoots("/apps/Demo.app/Contents/MacOS") { + it == "/apps/Demo.app/Contents/Resources/python" + }, + ) + assertEquals( + emptyList(), + discoverStagedPayloadRoots("/apps/demo") { false }, + ) + } +} diff --git a/python-multiplatform/src/linuxX64Main/kotlin/python/multiplatform/Platform.linuxX64.kt b/python-multiplatform/src/linuxX64Main/kotlin/python/multiplatform/Platform.linuxX64.kt new file mode 100644 index 00000000..dfa1251a --- /dev/null +++ b/python-multiplatform/src/linuxX64Main/kotlin/python/multiplatform/Platform.linuxX64.kt @@ -0,0 +1,39 @@ +package python.multiplatform + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.toKString +import platform.posix.uname +import platform.posix.utsname + +/** + * The Linux Kotlin/Native (`linuxX64`) view of the running host. + */ +object LinuxNativePlatform : Platform { + override val os = OSType.Linux + override val arch: String = machineName() + override val version: String = kernelRelease() + override val versionCode: Int? = null + override val platformType = PlatformType.Native + override val platformVersion: String? = null + + override fun toString(): String = name +} + +@OptIn(ExperimentalForeignApi::class) +private fun machineName(): String = memScoped { + val info = alloc() + if (uname(info.ptr) != 0) return "unknown" + info.machine.toKString().ifEmpty { "unknown" } +} + +@OptIn(ExperimentalForeignApi::class) +private fun kernelRelease(): String = memScoped { + val info = alloc() + if (uname(info.ptr) != 0) return "unknown" + info.release.toKString().ifEmpty { "unknown" } +} + +actual val currentPlatform: Platform = LinuxNativePlatform diff --git a/python-multiplatform/src/linuxX64Main/kotlin/python/multiplatform/ThreadId.linuxX64.kt b/python-multiplatform/src/linuxX64Main/kotlin/python/multiplatform/ThreadId.linuxX64.kt new file mode 100644 index 00000000..94cbe50d --- /dev/null +++ b/python-multiplatform/src/linuxX64Main/kotlin/python/multiplatform/ThreadId.linuxX64.kt @@ -0,0 +1,9 @@ +package python.multiplatform + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.convert + +/** glibc's `pthread_t` is a plain unsigned integral typedef (`unsigned long`), not a pointer. */ +@OptIn(ExperimentalForeignApi::class) +internal actual fun currentThreadId(): Long = + platform.posix.pthread_self().convert() diff --git a/python-multiplatform/src/linuxX64Main/kotlin/python/multiplatform/env/ExecutableDir.linuxX64.kt b/python-multiplatform/src/linuxX64Main/kotlin/python/multiplatform/env/ExecutableDir.linuxX64.kt new file mode 100644 index 00000000..72e10a9e --- /dev/null +++ b/python-multiplatform/src/linuxX64Main/kotlin/python/multiplatform/env/ExecutableDir.linuxX64.kt @@ -0,0 +1,21 @@ +package python.multiplatform.env + +import kotlinx.cinterop.ByteVar +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.convert +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.set +import kotlinx.cinterop.toKString +import platform.posix.PATH_MAX +import platform.posix.readlink + +@OptIn(ExperimentalForeignApi::class) +internal actual fun executableDirectory(): String? = memScoped { + val buf = allocArray(PATH_MAX) + val len = readlink("/proc/self/exe", buf, (PATH_MAX - 1).convert()) + if (len <= 0) return null + buf[len.toInt()] = 0 + val path = buf.toKString() + path.substringBeforeLast('/', missingDelimiterValue = "").takeIf { it.isNotEmpty() } +} diff --git a/python-multiplatform/src/macosArm64Main/kotlin/python/multiplatform/Platform.macosArm64.kt b/python-multiplatform/src/macosArm64Main/kotlin/python/multiplatform/Platform.macosArm64.kt new file mode 100644 index 00000000..6be1e9ab --- /dev/null +++ b/python-multiplatform/src/macosArm64Main/kotlin/python/multiplatform/Platform.macosArm64.kt @@ -0,0 +1,49 @@ +package python.multiplatform + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.toKString +import kotlinx.cinterop.useContents +import platform.Foundation.NSProcessInfo +import platform.posix.uname +import platform.posix.utsname + +/** + * The macOS Kotlin/Native (`macosArm64`) view of the running host. + */ +object MacosNativePlatform : Platform { + override val os = OSType.MacOS + override val arch: String = getDeviceArch() + override val version: String = getOSVersionString() + override val versionCode: Int = getOSVersionCode() + override val versionText = "SDK $versionCode" + override val platformType = PlatformType.Native + override val platformVersion: String? = null + + override fun toString(): String = name +} + +@OptIn(ExperimentalForeignApi::class) +private fun getDeviceArch(): String = memScoped { + val systemInfo = alloc() + if (uname(systemInfo.ptr) != 0) { + throw RuntimeException("Failed to get device arch info") + } + systemInfo.machine.toKString() +} + +@OptIn(ExperimentalForeignApi::class) +private fun getOSVersionString(): String = + NSProcessInfo.processInfo.operatingSystemVersion().useContents { + "$majorVersion.$minorVersion.$patchVersion" + } + +@OptIn(ExperimentalForeignApi::class) +private fun getOSVersionCode(): Int = + NSProcessInfo.processInfo.operatingSystemVersion().useContents { + majorVersion.toInt() * 10_000 + minorVersion.toInt() * 100 + patchVersion.toInt() + } + +actual val currentPlatform: Platform = MacosNativePlatform diff --git a/python-multiplatform/src/macosArm64Main/kotlin/python/multiplatform/ThreadId.macosArm64.kt b/python-multiplatform/src/macosArm64Main/kotlin/python/multiplatform/ThreadId.macosArm64.kt new file mode 100644 index 00000000..d1906274 --- /dev/null +++ b/python-multiplatform/src/macosArm64Main/kotlin/python/multiplatform/ThreadId.macosArm64.kt @@ -0,0 +1,8 @@ +package python.multiplatform + +import kotlinx.cinterop.ExperimentalForeignApi + +/** Darwin's `pthread_self()` returns a pointer; its bit pattern is the thread identity. */ +@OptIn(ExperimentalForeignApi::class) +internal actual fun currentThreadId(): Long = + platform.posix.pthread_self()?.rawValue?.toLong() ?: 0L diff --git a/python-multiplatform/src/macosArm64Main/kotlin/python/multiplatform/env/ExecutableDir.macosArm64.kt b/python-multiplatform/src/macosArm64Main/kotlin/python/multiplatform/env/ExecutableDir.macosArm64.kt new file mode 100644 index 00000000..38fb7176 --- /dev/null +++ b/python-multiplatform/src/macosArm64Main/kotlin/python/multiplatform/env/ExecutableDir.macosArm64.kt @@ -0,0 +1,27 @@ +package python.multiplatform.env + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.free +import kotlinx.cinterop.nativeHeap +import kotlinx.cinterop.toKString +import platform.Foundation.NSBundle +import platform.Foundation.NSProcessInfo +import platform.posix.realpath + +@OptIn(ExperimentalForeignApi::class) +internal actual fun executableDirectory(): String? { + val raw = NSBundle.mainBundle.executablePath + ?: (NSProcessInfo.processInfo.arguments.firstOrNull() as? String) + ?: return null + val resolved = realpath(raw, null) + val path = if (resolved != null) { + try { + resolved.toKString() + } finally { + nativeHeap.free(resolved) + } + } else { + raw + } + return path.substringBeforeLast('/', missingDelimiterValue = "").takeIf { it.isNotEmpty() } +}