diff --git a/kotlin-native/backend.native/tests/build.gradle b/kotlin-native/backend.native/tests/build.gradle index 712804e8d85..525588518f1 100644 --- a/kotlin-native/backend.native/tests/build.gradle +++ b/kotlin-native/backend.native/tests/build.gradle @@ -53,6 +53,7 @@ ext.target = platformManager.targetManager(project.testTarget).target ext.testLibraryDir = "${ext.testOutputRoot}/klib/platform/${project.target.name}" // Add executor to run tests depending on a target +// NOTE: If this persists in a gradle daemon, environment update (e.g. an Xcode update) may lead to execution failures. project.extensions.executor = ExecutorServiceKt.create(project) diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/ExecutorService.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/ExecutorService.kt index 7188e1276bf..90465e96b90 100644 --- a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/ExecutorService.kt +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/ExecutorService.kt @@ -20,9 +20,12 @@ import com.google.gson.annotations.Expose import groovy.lang.Closure import org.gradle.api.Action import org.gradle.api.Project -import org.gradle.process.ExecResult -import org.gradle.process.ExecSpec +import org.gradle.kotlin.dsl.* +import org.gradle.process.* +import org.gradle.process.internal.DefaultExecSpec +import org.gradle.process.internal.ExecException import org.jetbrains.kotlin.konan.target.* +import org.jetbrains.kotlin.executors.* import java.io.* @@ -42,6 +45,47 @@ interface ExecutorService { fun execute(action: Action): ExecResult? } +private fun Executor.service(project: Project) = object: ExecutorService { + override val project + get() = project + + override fun execute(action: Action): ExecResult? { + val execSpec = project.objects.newInstance().apply { + action.execute(this) + } + val request = ExecuteRequest( + executableAbsolutePath = execSpec.executable, + args = execSpec.args, + ).apply { + execSpec.standardInput?.let { + stdin = it + } + execSpec.standardOutput?.let { + stdout = it + } + execSpec.errorOutput?.let { + stderr = it + } + environment.putAll(execSpec.environment.mapValues { it.toString() }) + } + val response = this@service.execute(request) + return object : ExecResult { + override fun getExitValue() = response.exitCode ?: -1 + + override fun assertNormalExitValue(): ExecResult { + if (exitValue != 0) { + throw ExecException("Failed with exit code $exitValue") + } + return this + } + + override fun rethrowFailure(): ExecResult { + return this + } + } + } +} + /** * Creates an ExecutorService depending on a test target -Ptest_target */ @@ -50,40 +94,17 @@ fun create(project: Project): ExecutorService { val configurables = project.testTargetConfigurables return when { - project.compileOnlyTests -> noopExecutor(project) + project.compileOnlyTests -> NoOpExecutor(explanation = "compile-only tests").service(project) project.hasProperty("remote") -> sshExecutor(project, testTarget) - configurables is WasmConfigurables -> wasmExecutor(project) - configurables is ConfigurablesWithEmulator && testTarget != HostManager.host -> emulatorExecutor(project, testTarget) - configurables.targetTriple.isSimulator -> simulator(project) + configurables is WasmConfigurables -> WasmExecutor(configurables).service(project) + configurables is ConfigurablesWithEmulator && testTarget != HostManager.host -> EmulatorExecutor(configurables).service(project) + configurables is AppleConfigurables && configurables.targetTriple.isSimulator -> XcodeSimulatorExecutor(configurables).apply { + project.findProperty("iosDevice")?.toString()?.let { + deviceName = it + } + }.service(project) supportsRunningTestsOnDevice(testTarget) -> deviceLauncher(project) - else -> localExecutorService(project) - } -} - -private fun noopExecutor(project: Project) = object : ExecutorService { - override val project: Project - get() = project - - override fun execute(action: Action): ExecResult? = object : ExecResult { - override fun getExitValue(): Int = 0 - - override fun assertNormalExitValue(): ExecResult = this - - override fun rethrowFailure(): ExecResult = this - } -} - -private fun wasmExecutor(project: Project) = object : ExecutorService { - val absoluteTargetToolchain = project.testTargetConfigurables.absoluteTargetToolchain - - override val project: Project get() = project - override fun execute(action: Action): ExecResult? = project.exec { - action.execute(this) - val exe = executable - val d8 = "$absoluteTargetToolchain/bin/d8" - val launcherJs = "$executable.js" - this.executable = d8 - this.args = listOf("--expose-wasm", launcherJs, "--", exe) + args + else -> HostExecutor().service(project) } } @@ -202,118 +223,6 @@ fun localExecutorService(project: Project): ExecutorService = object : ExecutorS override fun execute(action: Action): ExecResult? = project.exec(action) } -private fun emulatorExecutor(project: Project, target: KonanTarget) = object : ExecutorService { - val configurables = project.testTargetConfigurables as? ConfigurablesWithEmulator - ?: error("$target does not support emulation!") - val absoluteTargetSysRoot = configurables.absoluteTargetSysRoot - - override val project: Project get() = project - - override fun execute(action: Action): ExecResult? = project.exec { - action.execute(this) - - val exe = executable - // TODO: Move these to konan.properties when when it will be possible - // to represent absolute path there. - val qemuSpecificArguments = listOf("-L", absoluteTargetSysRoot) - val targetSpecificArguments = when (target) { - KonanTarget.LINUX_MIPS32, - KonanTarget.LINUX_MIPSEL32 -> { - // This is to workaround an endianess issue. - // See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=731082 for details. - listOf("$absoluteTargetSysRoot/lib/ld.so.1", "--inhibit-cache") - } - else -> emptyList() - } - executable = configurables.absoluteEmulatorExecutable - args = qemuSpecificArguments + targetSpecificArguments + exe + args - } -} - -/** - * Executes a given action with iPhone Simulator. - * - * The test target should be specified with -Ptest_target=ios_x64 - * @see KonanTarget.IOS_X64 - * @param iosDevice an optional project property used to control simulator's device type - * Specify -PiosDevice=iPhone X to set it - */ -@Suppress("KDocUnresolvedReference") -private fun simulator(project: Project): ExecutorService = object : ExecutorService { - - private val target = project.testTarget - val configurables = project.testTargetConfigurables as AppleConfigurables - - init { - require(configurables.targetTriple.isSimulator) { - "${configurables.target} is not a simulator." - } - val compatibleArchs = when (HostManager.host.architecture) { - Architecture.X64 -> listOf(Architecture.X64, Architecture.X86) - Architecture.ARM64 -> listOf(Architecture.ARM64, Architecture.ARM32) - else -> throw IllegalStateException("$target is not supported by the simulator") - } - require(configurables.target.architecture in compatibleArchs) { - "Can't run simulator with ${configurables.target.architecture} architecture." - } - } - - private val simctl by lazy { - val sdk = Xcode.findCurrent().pathToPlatformSdk(configurables.platformName()) - val out = ByteArrayOutputStream() - val result = project.exec { - commandLine("/usr/bin/xcrun", "--find", "simctl", "--sdk", sdk) - standardOutput = out - } - result.assertNormalExitValue() - out.toString("UTF-8").trim() - } - - private val device by lazy { - val version = Xcode.findCurrent().version - val default = project.findProperty("iosDevice")?.toString() ?: when (target.family) { - Family.TVOS -> "Apple TV 4K" - Family.IOS -> "iPhone 11" - Family.WATCHOS -> "Apple Watch Series 6 " + (if (version.startsWith("14")) "(40mm)" else "- 40mm") - else -> error("Unexpected simulation target: $target") - } - - // Find out if the default device is available - val out = ByteArrayOutputStream() - var result = project.exec { - commandLine("/usr/bin/xcrun", "simctl", "list", "devices", "available") - standardOutput = out - } - result.assertNormalExitValue() - // Create if it's not available - if (!out.toString("UTF-8").trim().contains(default)) { - out.reset() - result = project.exec { - commandLine("/usr/bin/xcrun", "simctl", "create", default, default) - standardOutput = out - } - result.assertNormalExitValue() - } - - default - } - - private val archSpecification = when (target.architecture) { - Architecture.X86 -> listOf("-a", "i386") - Architecture.X64 -> listOf() // x86-64 is used by default on Intel Macs. - Architecture.ARM64 -> listOf() // arm64 is used by default on Apple Silicon. - else -> error("${target.architecture} can't be used in simulator.") - }.toTypedArray() - - override val project: Project get() = project - - override fun execute(action: Action): ExecResult? = project.exec { - action.execute(this) - // Starting Xcode 11 `simctl spawn` requires explicit `--standalone` flag. - commandLine = listOf(simctl, "spawn", "--standalone", *archSpecification, device, executable) + args - } -} - /** * Remote process executor. * diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/bitcode/CompileToBitcodePlugin.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/bitcode/CompileToBitcodePlugin.kt index 2658e05ed84..53acc812281 100644 --- a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/bitcode/CompileToBitcodePlugin.kt +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/bitcode/CompileToBitcodePlugin.kt @@ -306,6 +306,7 @@ open class CompileToBitcodeExtension @Inject constructor(val project: Project) : reportFile.set(project.layout.buildDirectory.file("testReports/$testName/report-with-prefixes.xml")) filter.set(project.findProperty("gtest_filter") as? String) tsanSuppressionsFile.set(project.layout.projectDirectory.file("tsan_suppressions.txt")) + this.target.set(target) usesService(runGTestSemaphore) } diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/RunGTest.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/RunGTest.kt index 0bb370bffed..7fbc2c46810 100644 --- a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/RunGTest.kt +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/cpp/RunGTest.kt @@ -9,10 +9,13 @@ import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.* +import org.gradle.kotlin.dsl.getByType import org.gradle.process.ExecOperations import org.gradle.workers.WorkAction import org.gradle.workers.WorkParameters import org.gradle.workers.WorkerExecutor +import org.jetbrains.kotlin.executors.* +import org.jetbrains.kotlin.konan.target.* import javax.inject.Inject private abstract class RunGTestJob : WorkAction { @@ -23,29 +26,40 @@ private abstract class RunGTestJob : WorkAction { val reportFileUnprocessed: RegularFileProperty val filter: Property val tsanSuppressionsFile: RegularFileProperty + val platformManager: Property + // TODO: Figure out a way to pass KonanTarget, but it is used as a key into PlatformManager, + // so object identity matters, and platform managers are different between project and worker sides. + val targetName: Property } - @get:Inject - abstract val execOperations: ExecOperations + // The `Executor` is created for every `RunGTest` task execution. It's okay, testing tasks are few-ish and big. + private val executor: Executor by lazy { + val platformManager = parameters.platformManager.get() + val target = platformManager.targetByName(parameters.targetName.get()) + val configurables = platformManager.platform(target).configurables + val hostTarget = HostManager.host + when { + target == hostTarget -> HostExecutor() + configurables is AppleConfigurables && configurables.targetTriple.isSimulator -> XcodeSimulatorExecutor(configurables) + else -> error("Cannot run for target $target") + } + } override fun execute() { - // TODO: Use ExecutorService to allow running it on targets other than host. // TODO: Try to make it like other gradle test tasks: report progress in a way gradle understands instead of dumping stdout of gtest. with(parameters) { reportFileUnprocessed.asFile.get().parentFile.mkdirs() - execOperations.exec { - executable = this@with.executable.asFile.get().absolutePath - + executor.execute(ExecuteRequest(this@with.executable.asFile.get().absolutePath).apply { + this.args.add("--gtest_output=xml:${reportFileUnprocessed.asFile.get().absolutePath}") filter.orNull?.also { - args("--gtest_filter=${it}") + this.args.add("--gtest_filter=${it}") } - args("--gtest_output=xml:${reportFileUnprocessed.asFile.get().absolutePath}") tsanSuppressionsFile.orNull?.also { - environment("TSAN_OPTIONS", "suppressions=${it.asFile.absolutePath}") + this.environment.put("TSAN_OPTIONS", "suppressions=${it.asFile.absolutePath}") } - } + }).assertSuccess() reportFile.asFile.get().parentFile.mkdirs() @@ -65,6 +79,7 @@ private abstract class RunGTestJob : WorkAction { * * @see CompileToBitcodePlugin */ +@UntrackedTask(because = "Test executables must always run when asked to") abstract class RunGTest : DefaultTask() { /** * Decorating test names in the report. @@ -117,6 +132,9 @@ abstract class RunGTest : DefaultTask() { @get:Inject protected abstract val workerExecutor: WorkerExecutor + @get:Input + abstract val target: Property + @TaskAction fun run() { val workQueue = workerExecutor.noIsolation() @@ -128,6 +146,8 @@ abstract class RunGTest : DefaultTask() { reportFileUnprocessed.set(this@RunGTest.reportFileUnprocessed) filter.set(this@RunGTest.filter) tsanSuppressionsFile.set(this@RunGTest.tsanSuppressionsFile) + platformManager.set(project.extensions.getByType()) + targetName.set(this@RunGTest.target.get().name) } } } diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/EmulatorExecutor.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/EmulatorExecutor.kt new file mode 100644 index 00000000000..61309c0b53a --- /dev/null +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/EmulatorExecutor.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.executors + +import org.jetbrains.kotlin.konan.target.Configurables +import org.jetbrains.kotlin.konan.target.ConfigurablesWithEmulator +import org.jetbrains.kotlin.konan.target.KonanTarget +import java.io.File + +/** + * [Executor] that runs the process in an emulator (e.g. qemu). + * + * @param configurables [Configurables] for emulated target + */ +class EmulatorExecutor( + private val configurables: ConfigurablesWithEmulator, +) : Executor { + private val hostExecutor: Executor = HostExecutor() + + override fun execute(request: ExecuteRequest): ExecuteResponse { + val absoluteTargetSysRoot = configurables.absoluteTargetSysRoot + val workingDirectory = request.workingDirectory ?: File(request.executableAbsolutePath).parentFile + + return hostExecutor.execute(request.copying { + this.executableAbsolutePath = configurables.absoluteEmulatorExecutable + this.workingDirectory = workingDirectory + this.args.add(0, request.executableAbsolutePath) + when (configurables.target) { + KonanTarget.LINUX_MIPS32, + KonanTarget.LINUX_MIPSEL32 -> { + // This is to workaround an endianess issue. + // See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=731082 for details. + this.args.addAll(0, listOf("$absoluteTargetSysRoot/lib/ld.so.1", "--inhibit-cache")) + } + else -> Unit + } + // TODO: Move these to konan.properties when when it will be possible + // to represent absolute path there. + this.args.addAll(0, listOf("-L", absoluteTargetSysRoot)) + }) + } +} \ No newline at end of file diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/Executor.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/Executor.kt new file mode 100644 index 00000000000..892169ae477 --- /dev/null +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/Executor.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +@file:OptIn(kotlin.time.ExperimentalTime::class) + +package org.jetbrains.kotlin.executors + +import java.io.* +import kotlin.time.Duration + +private class CloseProtectedOutputStream(stream : OutputStream) : FilterOutputStream(stream) { + override fun close() { + // Make sure all the data is written out. + super.flush() + // Exchange out for a new dummy stream. + this.out = ByteArrayOutputStream() + // And now properly close the stream from above. + super.close() + } +} + +data class ExecuteRequest( + /** + * Path to the executable. + */ + var executableAbsolutePath: String, + /** + * Command line args. + */ + val args: MutableList = mutableListOf(), + /** + * Optional working directory. By default its the parent directory of [executableAbsolutePath]. + */ + var workingDirectory: File? = null, + /** + * Will be sent to the executable input and then closed. By default an empty stream. + */ + var stdin: InputStream = ByteArrayInputStream(byteArrayOf()), + /** + * The output stream of the executable will be sent to this stream and then this stream will be closed. By default stdout of current process. + */ + var stdout: OutputStream = CloseProtectedOutputStream(System.out), + /** + * The error stream of the executable will be sent to this stream and then this stream will be closed. By default stderr of current process. + */ + var stderr: OutputStream = CloseProtectedOutputStream(System.err), + /** + * Additional environment variables. + */ + val environment: MutableMap = mutableMapOf(), + /** + * Bound execution time of the process. By default it's [Duration.INFINITE] meaning it's unbounded. + */ + val timeout: Duration = Duration.INFINITE +) { + /** + * Create a copy of this [ExecuteRequest], modify the copy by running [block] on it, and return that copy. + */ + inline fun copying(block: ExecuteRequest.() -> Unit): ExecuteRequest = copy().apply(block) +} + +data class ExecuteResponse( + /** + * Process exit code if it exited by itself, or `null` if it was killed by a timeout. + */ + val exitCode: Int?, + /** + * Execution time of the process. Can be a bit bigger than [ExecuteRequest.timeout]. + */ + val executionTime: Duration, +) { + /** + * Checks that [exitCode] is `0`. + * + * @throws IllegalStateException if [exitCode] is not 0. + */ + fun assertSuccess(): ExecuteResponse { + check(exitCode == 0) { "Exited with code $exitCode" } + return this + } +} + +/** + * Run a process in a specific execution environment. + * + * See subclasses for supported execution environments. + * + * NOTE: an [Executor] may cache some details about the environment (e.g. availability of an Xcode simulator). + * So, if the [Executor] lives in a long-running process (e.g. gradle daemon), it will miss environment changes. In that case, + * consider just recreating [Executor]. + */ +interface Executor { + /** + * Run the process and wait for its completion. + */ + fun execute(request: ExecuteRequest): ExecuteResponse +} \ No newline at end of file diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/HostExecutor.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/HostExecutor.kt new file mode 100644 index 00000000000..095b22b450b --- /dev/null +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/HostExecutor.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +@file:OptIn(ExperimentalTime::class) + +package org.jetbrains.kotlin.executors + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.io.File +import java.util.concurrent.TimeUnit +import java.util.logging.Logger +import kotlin.time.* + +/** + * [Executor] that runs the process on the host system. + */ +class HostExecutor : Executor { + private val logger = Logger.getLogger(HostExecutor::class.java.name) + + override fun execute(request: ExecuteRequest): ExecuteResponse { + return runBlocking(Dispatchers.IO) { + val workingDirectory = request.workingDirectory ?: File(request.executableAbsolutePath).parentFile + val commandLine = "${request.executableAbsolutePath}${request.args.joinToString(separator = " ", prefix = " ")}" + logger.info(""" + |Starting command: $commandLine + |In working directory: ${workingDirectory.absolutePath} + |With additional environment: ${request.environment.entries.joinToString(prefix = "{", postfix = "}") { "\"${it.key}\": \"${it.value}\"" }} + |And timeout: ${request.timeout} + """.trimMargin()) + val exitCodeWithTime = measureTimedValue { + val process = ProcessBuilder(listOf(request.executableAbsolutePath) + request.args).apply { + directory(workingDirectory) + environment().putAll(request.environment) + }.start() + val jobs: MutableList = mutableListOf() + jobs.add(launch { + request.stdin.apply { + copyTo(process.outputStream) + close() + } + process.outputStream.close() + }) + jobs.add(launch { + request.stdout.apply { + process.inputStream.copyTo(this) + close() + } + process.inputStream.close() + }) + jobs.add(launch { + request.stderr.apply { + process.errorStream.copyTo(this) + close() + } + process.errorStream.close() + }) + + if (!process.waitFor(request.timeout.toLong(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS)) { + logger.warning("Timeout running $commandLine") + // Cancel every stream, no need to wait for them. + jobs.forEach { + it.cancel() + } + process.destroyForcibly() + null + } else { + // Drain every stream. + jobs.forEach { + it.join() + } + process.exitValue() + } + } + ExecuteResponse(exitCodeWithTime.value, exitCodeWithTime.duration) + } + } +} \ No newline at end of file diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/NoOpExecutor.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/NoOpExecutor.kt new file mode 100644 index 00000000000..344270a5550 --- /dev/null +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/NoOpExecutor.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +@file:OptIn(kotlin.time.ExperimentalTime::class) + +package org.jetbrains.kotlin.executors + +import java.io.File +import java.util.logging.Logger +import kotlin.time.Duration + +/** + * [Executor] that does not run the process and immediately returns a successful response. + * + * Can be used for "compile-only" tests that don't need to actually execute the binary. + * + * @param explanation optional explanation for skipping execution for the logs + */ +class NoOpExecutor( + private var explanation: String? = null, +) : Executor { + private val logger = Logger.getLogger(NoOpExecutor::class.java.name) + + override fun execute(request: ExecuteRequest): ExecuteResponse { + val workingDirectory = request.workingDirectory ?: File(request.executableAbsolutePath).parentFile + logger.info(""" + |Skipping execution${explanation?.let { " ($it)" } ?: ""} + |Command: ${request.executableAbsolutePath}${request.args.joinToString(separator = " ", prefix = " ")} + |In working directory: ${workingDirectory.absolutePath} + |With additional environment: ${request.environment.entries.joinToString(prefix = "{", postfix = "}") { "\"${it.key}\": \"${it.value}\"" }} + """.trimMargin()) + return ExecuteResponse(exitCode = 0, executionTime = Duration.ZERO) + } +} \ No newline at end of file diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/WasmExecutor.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/WasmExecutor.kt new file mode 100644 index 00000000000..8bb04a11309 --- /dev/null +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/WasmExecutor.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.executors + +import org.jetbrains.kotlin.konan.target.Configurables +import org.jetbrains.kotlin.konan.target.WasmConfigurables +import java.io.File + +/** + * [Executor] that runs the wasm process via `d8`. + * + * NOTE: Apart from [ExecuteRequest.executableAbsolutePath] a JS launcher is required, + * it must be located in the same folder with the same name and `.js` suffix appended. + * + * @param configurables [Configurables] for the wasm target + */ +class WasmExecutor( + private val configurables: WasmConfigurables, +) : Executor { + private val hostExecutor: Executor = HostExecutor() + + override fun execute(request: ExecuteRequest): ExecuteResponse { + val absoluteTargetToolchain = configurables.absoluteTargetToolchain + val workingDirectory = request.workingDirectory ?: File(request.executableAbsolutePath).parentFile + val executable = request.executableAbsolutePath + val launcherJs = "$executable.js" + return hostExecutor.execute(request.copying { + this.executableAbsolutePath = "$absoluteTargetToolchain/bin/d8" + this.workingDirectory = workingDirectory + this.args.addAll(0, listOf("--expose-wasm", launcherJs, "--", executable)) + }) + } +} \ No newline at end of file diff --git a/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/XcodeSimulatorExecutor.kt b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/XcodeSimulatorExecutor.kt new file mode 100644 index 00000000000..b60a88c20e1 --- /dev/null +++ b/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/executors/XcodeSimulatorExecutor.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.executors + +import org.jetbrains.kotlin.konan.target.* +import java.io.ByteArrayOutputStream +import java.io.File + +private fun defaultDeviceName(target: KonanTarget) = when (target.family) { + Family.TVOS -> "Apple TV 4K" + Family.IOS -> "iPhone 11" + Family.WATCHOS -> "Apple Watch Series 6 " + (if (Xcode.findCurrent().version.startsWith("14")) "(40mm)" else "- 40mm") + else -> error("Unexpected simulation target: $target") +} + +private fun Executor.run(executableAbsolutePath: String, vararg args: String) = ByteArrayOutputStream().let { + this.execute(ExecuteRequest(executableAbsolutePath).apply { + this.args.addAll(args) + stdout = it + workingDirectory = File("").absoluteFile + }).assertSuccess() + it +} + +/** + * [Executor] that runs the process in an Xcode simulator. + * + * @param configurables [Configurables] for simulated target + * @property deviceName which simulator to use (optional). When not provided the default simulator for [configurables.target] is used + */ +class XcodeSimulatorExecutor( + private val configurables: AppleConfigurables, + var deviceName: String = defaultDeviceName(configurables.target), +) : Executor { + private val hostExecutor: Executor = HostExecutor() + + private val target by configurables::target + + init { + require(configurables.targetTriple.isSimulator) { + "$target is not a simulator." + } + val hostArch = HostManager.host.architecture + val targetArch = target.architecture + val compatibleArchs = when (hostArch) { + Architecture.X64 -> listOf(Architecture.X64, Architecture.X86) + Architecture.ARM64 -> listOf(Architecture.ARM64, Architecture.ARM32) + else -> throw IllegalStateException("$hostArch is not a supported host architecture for the simulator") + } + require(targetArch in compatibleArchs) { + "Can't run simulator for $targetArch architecture on $hostArch host architecture" + } + } + + private val archSpecification = when (target.architecture) { + Architecture.X86 -> listOf("-a", "i386") + Architecture.X64 -> listOf() // x86-64 is used by default on Intel Macs. + Architecture.ARM64 -> listOf() // arm64 is used by default on Apple Silicon. + else -> error("${target.architecture} can't be used in simulator.") + }.toTypedArray() + + private var _deviceNameChecked: String? = null + private fun ensureSimulatorExists() { + // Already ensured that simulator for `deviceName` exists. + if (deviceName == _deviceNameChecked) + return + // Find out if the default device is available + val out = hostExecutor.run("/usr/bin/xcrun", "simctl", "list", "devices", "available") + val deviceNameExists = out.toString("UTF-8").trim().contains(deviceName) + // Create if it's not available + if (!deviceNameExists) { + hostExecutor.run("/usr/bin/xcrun", "simctl", "create", deviceName, deviceName) + } + // If successfully created, remember that. + _deviceNameChecked = deviceName + } + + override fun execute(request: ExecuteRequest): ExecuteResponse { + ensureSimulatorExists() + val executable = request.executableAbsolutePath + val env = request.environment.mapKeys { + "SIMCTL_CHILD_" + it.key + } + val workingDirectory = request.workingDirectory ?: File(request.executableAbsolutePath).parentFile + // Starting Xcode 11 `simctl spawn` requires explicit `--standalone` flag. + return hostExecutor.execute(request.copying { + this.executableAbsolutePath = "/usr/bin/xcrun" + this.workingDirectory = workingDirectory + this.args.addAll(0, listOf("simctl", "spawn", "--standalone", *archSpecification, deviceName, executable)) + this.environment.clear() + this.environment.putAll(env) + }) + } +} \ No newline at end of file