[K/N] Make executors be separate included project
This change will make possible use Executors in old, gtest and new test infrastructures. Merge-request: KT-MR-8964
This commit is contained in:
committed by
Space Team
parent
c105e63bbb
commit
76ab130011
@@ -0,0 +1,53 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath("com.google.code.gson:gson:2.8.9")
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.google.code.gson:gson:2.8.9")
|
||||
configurations.all {
|
||||
resolutionStrategy.eachDependency {
|
||||
if (requested.group == "com.google.code.gson" && requested.name == "gson") {
|
||||
useVersion("2.8.9")
|
||||
because("Force using same gson version because of https://github.com/google/gson/pull/1991")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0")
|
||||
if (kotlinBuildProperties.isKotlinNativeEnabled) {
|
||||
implementation(project(":kotlin-native-shared"))
|
||||
} else {
|
||||
implementation(project(":native:kotlin-native-utils"))
|
||||
}
|
||||
}
|
||||
|
||||
group = "org.jetbrains.kotlin"
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(8))
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += listOf(
|
||||
"-Xskip-prerelease-check",
|
||||
"-Xsuppress-version-warnings",
|
||||
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||
"-opt-in=kotlin.RequiresOptIn"
|
||||
)
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2010-2023 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.native.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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2010-2023 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.native.executors
|
||||
|
||||
import java.io.*
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
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<String> = 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<String, String> = 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for [ExecuteRequest] creation.
|
||||
* Allows to create it without opting in [kotlin.time.ExperimentalTime] for API < 1.6.
|
||||
*/
|
||||
fun executeRequest(executableAbsolutePath: String, args: MutableList<String> = mutableListOf()): ExecuteRequest =
|
||||
ExecuteRequest(executableAbsolutePath, args)
|
||||
|
||||
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
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2010-2023 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.native.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<Job> = 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.inWholeMilliseconds, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2010-2023 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.native.executors
|
||||
|
||||
import java.io.File
|
||||
import java.util.logging.Logger
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
/**
|
||||
* [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)
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2010-2023 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.native.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))
|
||||
})
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2010-2023 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.native.executors
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.annotations.Expose
|
||||
import org.jetbrains.kotlin.konan.target.Family
|
||||
import org.jetbrains.kotlin.konan.target.Xcode
|
||||
import kotlin.math.min
|
||||
|
||||
internal val gson: Gson by lazy { GsonBuilder().excludeFieldsWithoutExposeAnnotation().create()!! }
|
||||
|
||||
/**
|
||||
* Compares two strings assuming that both are representing numeric version strings.
|
||||
* Examples of numeric version strings: "12.4.1.2", "9", "0.5".
|
||||
*/
|
||||
private fun compareStringsAsVersions(version1: String, version2: String): Int {
|
||||
val splitVersion1 = version1.split('.').map { it.toInt() }
|
||||
val splitVersion2 = version2.split('.').map { it.toInt() }
|
||||
val minimalLength = min(splitVersion1.size, splitVersion2.size)
|
||||
for (index in 0 until minimalLength) {
|
||||
if (splitVersion1[index] < splitVersion2[index]) return -1
|
||||
if (splitVersion1[index] > splitVersion2[index]) return 1
|
||||
}
|
||||
return splitVersion1.size.compareTo(splitVersion2.size)
|
||||
}
|
||||
|
||||
internal fun simulatorOsName(family: Family): String {
|
||||
return when (family) {
|
||||
Family.IOS -> "iOS"
|
||||
Family.WATCHOS -> "watchOS"
|
||||
Family.TVOS -> "tvOS"
|
||||
else -> error("Unexpected simulator OS: $family")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns parsed output of `xcrun simctl list runtimes -j`.
|
||||
*/
|
||||
private fun Xcode.getSimulatorRuntimeDescriptors(): List<SimulatorRuntimeDescriptor> =
|
||||
gson.fromJson(simulatorRuntimes, ListRuntimesReport::class.java).runtimes
|
||||
private fun getSimulatorRuntimeDescriptors(json: String): List<SimulatorRuntimeDescriptor> =
|
||||
gson.fromJson(json, ListRuntimesReport::class.java).runtimes
|
||||
|
||||
private fun getLatestSimulatorRuntimeFor(
|
||||
descriptors: List<SimulatorRuntimeDescriptor>,
|
||||
family: Family,
|
||||
osMinVersion: String
|
||||
): SimulatorRuntimeDescriptor? {
|
||||
val osName = simulatorOsName(family)
|
||||
return descriptors.firstOrNull {
|
||||
it.checkAvailability() && it.name.startsWith(osName) && compareStringsAsVersions(it.version, osMinVersion) >= 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSimulatorRuntimesFor(
|
||||
descriptors: List<SimulatorRuntimeDescriptor>,
|
||||
family: Family,
|
||||
osMinVersion: String
|
||||
): List<SimulatorRuntimeDescriptor> {
|
||||
val osName = simulatorOsName(family)
|
||||
return descriptors.filter {
|
||||
it.checkAvailability() && it.name.startsWith(osName) && compareStringsAsVersions(it.version, osMinVersion) >= 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns first available simulator runtime for [target] with at least [osMinVersion] OS version.
|
||||
* */
|
||||
fun Xcode.getLatestSimulatorRuntimeFor(family: Family, osMinVersion: String): SimulatorRuntimeDescriptor? =
|
||||
getLatestSimulatorRuntimeFor(getSimulatorRuntimeDescriptors(), family, osMinVersion)
|
||||
|
||||
fun getLatestSimulatorRuntimeFor(json: String, family: Family, osMinVersion: String): SimulatorRuntimeDescriptor? =
|
||||
getLatestSimulatorRuntimeFor(getSimulatorRuntimeDescriptors(json), family, osMinVersion)
|
||||
|
||||
fun getSimulatorRuntimesFor(json: String, family: Family, osMinVersion: String): List<SimulatorRuntimeDescriptor> =
|
||||
getSimulatorRuntimesFor(getSimulatorRuntimeDescriptors(json), family, osMinVersion)
|
||||
|
||||
// Result of `xcrun simctl list runtimes -j`.
|
||||
data class ListRuntimesReport(
|
||||
@Expose val runtimes: List<SimulatorRuntimeDescriptor>
|
||||
)
|
||||
|
||||
data class SimulatorRuntimeDescriptor(
|
||||
@Expose val version: String,
|
||||
// bundlePath field may not exist in the old Xcode (prior to 10.3).
|
||||
@Expose val bundlePath: String? = null,
|
||||
@Expose val isAvailable: Boolean? = null,
|
||||
@Expose val availability: String? = null,
|
||||
@Expose val name: String,
|
||||
@Expose val identifier: String,
|
||||
@Expose val buildversion: String,
|
||||
@Expose val supportedDeviceTypes: List<DeviceType>
|
||||
) {
|
||||
/**
|
||||
* Different Xcode/macOS combinations give different fields that checks
|
||||
* runtime availability. This method is an umbrella for these fields.
|
||||
*/
|
||||
fun checkAvailability(): Boolean {
|
||||
if (isAvailable == true) return true
|
||||
if (availability?.contains("unavailable") == true) return false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
data class DeviceType(
|
||||
@Expose val bundlePath: String,
|
||||
@Expose val name: String,
|
||||
@Expose val identifier: String,
|
||||
@Expose val productFamily: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns map of simulator devices from the json input
|
||||
*/
|
||||
fun getSimulatorDevices(json: String): Map<String, List<SimulatorDeviceDescriptor>> =
|
||||
gson.fromJson(json, ListDevicesReport::class.java).devices
|
||||
|
||||
// Result of `xcrun simctl list devices -j`
|
||||
data class ListDevicesReport(
|
||||
@Expose val devices: Map<String, List<SimulatorDeviceDescriptor>>
|
||||
)
|
||||
|
||||
data class SimulatorDeviceDescriptor(
|
||||
@Expose val lastBootedAt: String?,
|
||||
@Expose val dataPath: String,
|
||||
@Expose val dataPathSize: Long,
|
||||
@Expose val logPath: String,
|
||||
@Expose val udid: String,
|
||||
@Expose val isAvailable: Boolean?,
|
||||
@Expose val deviceTypeIdentifier: String,
|
||||
@Expose val state: String,
|
||||
@Expose val name: String
|
||||
)
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2010-2023 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.native.executors
|
||||
|
||||
import org.jetbrains.kotlin.konan.target.*
|
||||
import org.jetbrains.kotlin.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.util.logging.Logger
|
||||
|
||||
private fun defaultDeviceId(target: KonanTarget) = when (target.family) {
|
||||
Family.TVOS -> "com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-4K"
|
||||
Family.IOS -> "com.apple.CoreSimulator.SimDeviceType.iPhone-14"
|
||||
Family.WATCHOS -> "com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-6-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 deviceId: String = defaultDeviceId(configurables.target),
|
||||
) : Executor {
|
||||
private val hostExecutor: Executor = HostExecutor()
|
||||
|
||||
private val logger = Logger.getLogger(this::class.java.name)
|
||||
|
||||
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 fun simctl(vararg args: String): String {
|
||||
val out = hostExecutor.run("/usr/bin/xcrun", *arrayOf("simctl", *args))
|
||||
return out.toString("UTF-8").trim()
|
||||
}
|
||||
|
||||
private var deviceChecked: SimulatorDeviceDescriptor? = null
|
||||
|
||||
private fun ensureSimulatorExists() {
|
||||
// Already ensured that simulator for `deviceId` exists.
|
||||
if (deviceId == deviceChecked?.deviceTypeIdentifier) {
|
||||
logger.info("Device already exists: ${deviceChecked?.deviceTypeIdentifier} with name ${deviceChecked?.name}")
|
||||
return
|
||||
}
|
||||
logger.info("Device was not cheked before. Find the device with id: $deviceId")
|
||||
val simulatorRuntime = getSimulatorRuntime()
|
||||
logger.info("Runtime used for the $deviceId is $simulatorRuntime")
|
||||
|
||||
val device = getDeviceFor(simulatorRuntime)
|
||||
logger.info("Found device: $device")
|
||||
// If successfully found, remember that.
|
||||
deviceChecked = device
|
||||
}
|
||||
|
||||
private fun getDeviceFor(simulatorRuntime: SimulatorRuntimeDescriptor): SimulatorDeviceDescriptor {
|
||||
val runtimeIdentifier = simulatorRuntime.identifier
|
||||
val deviceOrNull = {
|
||||
getSimulatorDevices(simctl("list", "devices", "--json"))[runtimeIdentifier]
|
||||
?.find { it.deviceTypeIdentifier == deviceId && it.isAvailable == true }
|
||||
}
|
||||
// If the device already exists, nothing to do.
|
||||
deviceOrNull()?.let { return it }
|
||||
|
||||
// Create the device
|
||||
logger.info("Current runtime doesn't have $deviceId available")
|
||||
val deviceType = simulatorRuntime.supportedDeviceTypes.find { it.identifier == deviceId }
|
||||
checkNotNull(deviceType) {
|
||||
"""
|
||||
Default device $deviceId is not available for the runtime ${simulatorRuntime.name}
|
||||
Supported devices: ${simulatorRuntime.supportedDeviceTypes.map { it.identifier }.joinToString(", ")}
|
||||
""".trimIndent()
|
||||
}
|
||||
val out = simctl("create", deviceType.name, deviceType.identifier, runtimeIdentifier)
|
||||
logger.info("Create device $deviceId: simctl output is: $out")
|
||||
|
||||
// Return freshly created device if it fits
|
||||
deviceOrNull()?.let { return it }
|
||||
|
||||
// Looks like device is unavailable. This may happen if
|
||||
// `~/Library/Developer/CoreSimulator/RuntimeMap.plist` has empty preferred runtimes
|
||||
logger.info("Runtimes are available but devices can't use it. Download runtimes again with Xcode")
|
||||
downloadRuntimeFor(simulatorOsName(target.family))
|
||||
|
||||
return checkNotNull(deviceOrNull()) { "Unable to get or create simulator device $deviceId for $target with runtime ${simulatorRuntime.name}" }
|
||||
}
|
||||
|
||||
private fun getSimulatorRuntime(): SimulatorRuntimeDescriptor {
|
||||
val simulatorRuntimeOrNull = {
|
||||
getSimulatorRuntimesFor(
|
||||
json = simctl("list", "runtimes", "--json"),
|
||||
family = target.family,
|
||||
osMinVersion = configurables.osVersionMin
|
||||
).firstOrNull {
|
||||
it.supportedDeviceTypes.any { deviceType -> deviceType.identifier == deviceId }
|
||||
}
|
||||
}
|
||||
// If the simulator runtime already exists, nothing to do.
|
||||
simulatorRuntimeOrNull()?.let { return it }
|
||||
|
||||
// Download this platform if not available
|
||||
logger.info("Runtime for the $deviceId is not available. Downloading runtimes for $target with Xcode")
|
||||
downloadRuntimeFor(simulatorOsName(target.family))
|
||||
|
||||
return checkNotNull(simulatorRuntimeOrNull()) { "Runtime is not available for the selected $deviceId. Check Xcode installation" }
|
||||
}
|
||||
|
||||
private fun downloadRuntimeFor(osName: String) {
|
||||
val version = Xcode.findCurrent().version
|
||||
val versionSplit = version.split(".")
|
||||
check(versionSplit.size >= 2) {
|
||||
"Unrecognised version of Xcode $version was split to $versionSplit"
|
||||
}
|
||||
val major = versionSplit[0].toInt()
|
||||
val minor = versionSplit[1].toInt()
|
||||
check(major >= 14) {
|
||||
"Was unable to get the required runtimes running on Xcode $version. Check the Xcode installation"
|
||||
}
|
||||
if (minor >= 1) {
|
||||
// Option -downloadPlatform NAME available only since 14.1
|
||||
hostExecutor.run("/usr/bin/xcrun", "xcodebuild", "-downloadPlatform", osName)
|
||||
} else {
|
||||
// Have to download all platforms :(
|
||||
hostExecutor.run("/usr/bin/xcrun", "xcodebuild", "-downloadAllPlatforms")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
val name = deviceChecked?.name ?: error("No device available for $deviceId")
|
||||
// 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, name, executable))
|
||||
this.environment.clear()
|
||||
this.environment.putAll(env)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user