[K/N] Refactor Executor out of ExecutorService without gradle dependency
Merge-request: KT-MR-7622 Merged-by: Alexander Shabalin <Alexander.Shabalin@jetbrains.com>
This commit is contained in:
committed by
Space Team
parent
e16cfd5485
commit
d512420204
@@ -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)
|
||||
|
||||
|
||||
|
||||
+55
-146
@@ -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<in ExecSpec>): ExecResult?
|
||||
}
|
||||
|
||||
private fun Executor.service(project: Project) = object: ExecutorService {
|
||||
override val project
|
||||
get() = project
|
||||
|
||||
override fun execute(action: Action<in ExecSpec>): ExecResult? {
|
||||
val execSpec = project.objects.newInstance<DefaultExecSpec>().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<in ExecSpec>): 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<in ExecSpec>): 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<in ExecSpec>): 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<in ExecSpec>): 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<in ExecSpec>): 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.
|
||||
*
|
||||
|
||||
+1
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<RunGTestJob.Parameters> {
|
||||
@@ -23,29 +26,40 @@ private abstract class RunGTestJob : WorkAction<RunGTestJob.Parameters> {
|
||||
val reportFileUnprocessed: RegularFileProperty
|
||||
val filter: Property<String>
|
||||
val tsanSuppressionsFile: RegularFileProperty
|
||||
val platformManager: Property<PlatformManager>
|
||||
// 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<String>
|
||||
}
|
||||
|
||||
@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<RunGTestJob.Parameters> {
|
||||
*
|
||||
* @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<KonanTarget>
|
||||
|
||||
@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<PlatformManager>())
|
||||
targetName.set(this@RunGTest.target.get().name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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<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)
|
||||
}
|
||||
|
||||
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-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<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.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -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)
|
||||
}
|
||||
}
|
||||
+36
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
+97
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user