diff --git a/gradle.properties b/gradle.properties index 9e53dbcaf0..af79919e9b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,7 @@ kotlin.code.style=official kotlin.incremental.js=true kotlin.incremental.multiplatform=true kotlin.mpp.stability.nowarn=true +kotlin.mpp.enableCInteropCommonization=true kotlin.native.ignoreDisabledTargets=true # atomicfu diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index b864b68dce..6ccc7cb1e2 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -2383,9 +2383,12 @@ public final class aws/smithy/kotlin/runtime/util/OperatingSystem { public final class aws/smithy/kotlin/runtime/util/OsFamily : java/lang/Enum { public static final field Android Laws/smithy/kotlin/runtime/util/OsFamily; public static final field Ios Laws/smithy/kotlin/runtime/util/OsFamily; + public static final field IpadOs Laws/smithy/kotlin/runtime/util/OsFamily; public static final field Linux Laws/smithy/kotlin/runtime/util/OsFamily; public static final field MacOs Laws/smithy/kotlin/runtime/util/OsFamily; + public static final field TvOs Laws/smithy/kotlin/runtime/util/OsFamily; public static final field Unknown Laws/smithy/kotlin/runtime/util/OsFamily; + public static final field WatchOs Laws/smithy/kotlin/runtime/util/OsFamily; public static final field Windows Laws/smithy/kotlin/runtime/util/OsFamily; public static fun getEntries ()Lkotlin/enums/EnumEntries; public fun toString ()Ljava/lang/String; diff --git a/runtime/runtime-core/build.gradle.kts b/runtime/runtime-core/build.gradle.kts index 8fac2377f8..7a4bdd17fa 100644 --- a/runtime/runtime-core/build.gradle.kts +++ b/runtime/runtime-core/build.gradle.kts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + plugins { alias(libs.plugins.kotlinx.serialization) } @@ -54,4 +56,15 @@ kotlin { languageSettings.optIn("aws.smithy.kotlin.runtime.InternalApi") } } + + targets.withType { + compilations["main"].cinterops { + val interopDir = "$projectDir/native/src/nativeInterop/cinterop" + create("environ") { + includeDirs(interopDir) + packageName("aws.smithy.platform.posix") + headers(listOf("$interopDir/environ.h")) + } + } + } } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt index 684949943a..b2b56a11b7 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt @@ -58,6 +58,9 @@ public enum class OsFamily { Windows, Android, Ios, + IpadOs, + TvOs, + WatchOs, Unknown, ; @@ -67,6 +70,9 @@ public enum class OsFamily { Windows -> "windows" Android -> "android" Ios -> "ios" + IpadOs -> "ipados" + TvOs -> "tvos" + WatchOs -> "watchos" Unknown -> "unknown" } } diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt new file mode 100644 index 0000000000..734f788e49 --- /dev/null +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.util + +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class SystemPlatformProviderTest { + @Test + fun testReadWriteFile() = runTest { + val ps = PlatformProvider.System + + val tempDir = if (ps.osInfo().family == OsFamily.Windows) { + requireNotNull(ps.getenv("TEMP")) { "%TEMP% unexpectedly null" } + } else { + "/tmp" + } + val path = "$tempDir/testReadWriteFile-${Uuid.random()}.txt" + + val expected = "Hello, File!".encodeToByteArray() + + ps.writeFile(path, expected) + assertTrue(ps.fileExists(path)) + + val actual = ps.readFileOrNull(path) + assertContentEquals(expected, actual) + } + + @Test + fun testGetEnv() = runTest { + val envVarKeys = listOf("PATH", "USERPROFILE") // PATH is not set on Windows CI + assertNotNull( + envVarKeys.firstNotNullOfOrNull { PlatformProvider.System.getenv(it) }, + ) + + assertNull(PlatformProvider.System.getenv("THIS_ENV_VAR_IS_NOT_SET")) + } + + @Test + fun testGetAllEnvVars() = runTest { + val allEnv = PlatformProvider.System.getAllEnvVars() + assertTrue(allEnv.isNotEmpty()) + + val envVarKeys = listOf("PATH", "USERPROFILE") // PATH is not set on Windows CI + assertTrue( + envVarKeys.any { allEnv.contains(it) }, + ) + } + + @Test + fun testOsInfo() = runTest { + val osInfo = PlatformProvider.System.osInfo() + assertNotEquals(OsFamily.Unknown, osInfo.family) + } +} diff --git a/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt b/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt new file mode 100644 index 0000000000..f6ca8da166 --- /dev/null +++ b/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt @@ -0,0 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.util + +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class SystemPlatformProviderLinuxX64Test { + @Test + fun testOsInfo() = runTest { + val osInfo = PlatformProvider.System.osInfo() + assertEquals(OsFamily.Linux, osInfo.family) + } +} diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt index bb435ccaea..220e66fde5 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt @@ -4,50 +4,104 @@ */ package aws.smithy.kotlin.runtime.util +import aws.smithy.kotlin.runtime.io.IOException +import aws.smithy.kotlin.runtime.io.internal.SdkDispatchers +import aws.smithy.platform.posix.get_environ_ptr +import kotlinx.cinterop.* +import kotlinx.coroutines.withContext +import platform.posix.* + internal actual object SystemDefaultProvider : PlatformProvider { - actual override fun getAllEnvVars(): Map { - TODO("Not yet implemented") + actual override fun getAllEnvVars(): Map = memScoped { + val environ = get_environ_ptr() + generateSequence(0) { it + 1 } + .map { idx -> environ?.get(idx)?.toKString() } + .takeWhile { it != null } + .associate { env -> + val parts = env?.split("=", limit = 2) + check(parts?.size == 2) { "Environment entry \"$env\" is malformed" } + parts[0] to parts[1] + } } - actual override fun getenv(key: String): String? { - TODO("Not yet implemented") - } + actual override fun getenv(key: String): String? = platform.posix.getenv(key)?.toKString() actual override val filePathSeparator: String - get() = TODO("Not yet implemented") + get() = when (osInfo().family) { + OsFamily.Windows -> "\\" + else -> "/" + } - actual override suspend fun readFileOrNull(path: String): ByteArray? { - TODO("Not yet implemented") - } + actual override suspend fun readFileOrNull(path: String): ByteArray? = withContext(SdkDispatchers.IO) { + try { + val file = fopen(path, "rb") ?: return@withContext null - actual override suspend fun writeFile(path: String, data: ByteArray) { - TODO("Not yet implemented") - } + try { + // Get file size + fseek(file, 0L, SEEK_END) + val size = ftell(file) + fseek(file, 0L, SEEK_SET) - actual override fun fileExists(path: String): Boolean { - TODO("Not yet implemented") + // Read file content + val buffer = ByteArray(size.toInt()).pin() + val rc = fread(buffer.addressOf(0), 1uL, size.toULong(), file) + if (rc == size.toULong()) buffer.get() else null + } finally { + fclose(file) + } + } catch (_: Exception) { + null + } } - actual override fun osInfo(): OperatingSystem { - TODO("Not yet implemented") + actual override suspend fun writeFile(path: String, data: ByteArray) = withContext(SdkDispatchers.IO) { + val file = fopen(path, "wb") ?: throw IOException("Cannot open file for writing: $path") + try { + val wc = fwrite(data.refTo(0), 1uL, data.size.toULong(), file) + if (wc != data.size.toULong()) { + throw IOException("Failed to write all bytes to file $path, expected ${data.size.toLong()}, wrote $wc") + } + } finally { + fclose(file) + } } - actual override val isJvm: Boolean - get() = TODO("Not yet implemented") - actual override val isAndroid: Boolean - get() = TODO("Not yet implemented") - actual override val isBrowser: Boolean - get() = TODO("Not yet implemented") - actual override val isNode: Boolean - get() = TODO("Not yet implemented") - actual override val isNative: Boolean - get() = TODO("Not yet implemented") - - actual override fun getAllProperties(): Map { - TODO("Not yet implemented") - } + actual override fun fileExists(path: String): Boolean = access(path, F_OK) == 0 - actual override fun getProperty(key: String): String? { - TODO("Not yet implemented") + actual override fun osInfo(): OperatingSystem = memScoped { + val utsname = alloc() + uname(utsname.ptr) + + val sysName = utsname.sysname.toKString().lowercase() + val version = utsname.release.toKString() + val machine = utsname.machine.toKString().lowercase() // Helps differentiate Apple platforms + + val family = when { + sysName.contains("darwin") -> { + when { + machine.startsWith("iphone") -> OsFamily.Ios + // TODO Validate that iPadOS/tvOS/watchOS resolves correctly on each of these devices + machine.startsWith("ipad") -> OsFamily.IpadOs + machine.startsWith("tv") -> OsFamily.TvOs + machine.startsWith("watch") -> OsFamily.WatchOs + else -> OsFamily.MacOs + } + } + sysName.contains("linux") -> OsFamily.Linux + sysName.contains("windows") -> OsFamily.Windows + else -> OsFamily.Unknown + } + + return OperatingSystem(family, version) } + + actual override val isJvm: Boolean = false + actual override val isAndroid: Boolean = false + actual override val isBrowser: Boolean = false + actual override val isNode: Boolean = false + actual override val isNative: Boolean = true + + // Kotlin/Native doesn't have system properties + actual override fun getAllProperties(): Map = emptyMap() + actual override fun getProperty(key: String): String? = null } diff --git a/runtime/runtime-core/native/src/nativeInterop/cinterop/environ.h b/runtime/runtime-core/native/src/nativeInterop/cinterop/environ.h new file mode 100644 index 0000000000..6262167426 --- /dev/null +++ b/runtime/runtime-core/native/src/nativeInterop/cinterop/environ.h @@ -0,0 +1,12 @@ +#ifndef ENVIRON_H +#define ENVIRON_H + +// External declaration to get environment variables +extern char **environ; + +// Helper function to get the environ pointer +char** get_environ_ptr() { + return environ; +} + +#endif