[K/N] Remove legacy executors.

This commit is contained in:
Alexander Shabalin
2023-09-03 19:57:29 +02:00
committed by Space Team
parent 78ef58f59d
commit 6a19c4ce96
4 changed files with 9 additions and 427 deletions
@@ -6285,25 +6285,6 @@ dependencies {
api RepoDependencies.commonDependency(project, "junit")
}
// Configure build for iOS device targets.
if (UtilsKt.supportsRunningTestsOnDevice(target) && !UtilsKt.getCompileOnlyTests(project)) {
project.tasks
.withType(KonanTestExecutable.class)
.configureEach {
ExecutorServiceKt.configureXcodeBuild((KonanTestExecutable) it)
}
// Exclude tasks that cannot be run on device
project.tasks
.withType(KonanTest.class)
.matching { (it instanceof KonanLocalTest && ((KonanLocalTest) it).useTestData) ||
// Filter out tests that depend on libs. TODO: copy them too
it instanceof KonanInteropTest ||
it instanceof KonanDynamicTest ||
it instanceof KonanLinkTest
}
.configureEach { it.enabled = false }
}
project.afterEvaluate {
// Don't treat any task as up-to-date, no matter what.
// Note: this project should contain only test tasks, including ones that build binaries,
@@ -17,7 +17,6 @@
@file:OptIn(kotlin.time.ExperimentalTime::class)
package org.jetbrains.kotlin
import com.google.gson.annotations.Expose
import groovy.lang.Closure
import org.gradle.api.Action
import org.gradle.api.Project
@@ -31,12 +30,7 @@ import org.jetbrains.kotlin.native.executors.*
import java.io.*
import java.nio.file.Path
import java.nio.file.Paths
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.TimeUnit
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
@@ -99,21 +93,21 @@ fun create(project: Project): ExecutorService {
val testTarget = project.testTarget
val configurables = project.testTargetConfigurables
return when {
project.compileOnlyTests -> NoOpExecutor(explanation = "compile-only tests").service(project)
project.hasProperty("remote") -> sshExecutor(project, testTarget)
configurables is WasmConfigurables -> WasmExecutor(configurables).service(project)
configurables is ConfigurablesWithEmulator && testTarget != HostManager.host -> EmulatorExecutor(configurables).service(project)
val executor = when {
project.compileOnlyTests -> NoOpExecutor(explanation = "compile-only tests")
testTarget == HostManager.host -> HostExecutor()
configurables is ConfigurablesWithEmulator && testTarget != HostManager.host -> EmulatorExecutor(configurables)
configurables is AppleConfigurables && configurables.targetTriple.isSimulator -> XcodeSimulatorExecutor(configurables).apply {
// Property can specify device identifier to be run on. For example, `com.apple.CoreSimulator.SimDeviceType.iPhone-11`
project.findProperty("iosDevice")?.toString()?.let {
deviceId = it
}
}.service(project)
supportsRunningTestsOnDevice(testTarget) -> deviceLauncher(project)
configurables is AppleConfigurables && RosettaExecutor.availableFor(configurables) -> RosettaExecutor(configurables).service(project)
else -> HostExecutor().service(project)
}
configurables is AppleConfigurables && RosettaExecutor.availableFor(configurables) -> RosettaExecutor(configurables)
else -> error("Cannot run for target $testTarget")
}
return executor.service(project)
}
data class ProcessOutput(var stdOut: String, var stdErr: String, var exitCode: Int)
@@ -230,354 +224,3 @@ fun localExecutorService(project: Project): ExecutorService = object : ExecutorS
override val project: Project get() = project
override fun execute(action: Action<in ExecSpec>): ExecResult? = project.exec(action)
}
/**
* Remote process executor.
*
* @param remote makes binaries be executed on a remote host
* Specify it as -Premote=user@host
*/
@Suppress("KDocUnresolvedReference")
private fun sshExecutor(project: Project, testTarget: KonanTarget): ExecutorService = object : ExecutorService {
private val remote: String = project.property("remote").toString()
private val sshArgs: List<String> = System.getenv("SSH_ARGS")?.split(" ") ?: emptyList()
private val sshHome = System.getenv("SSH_HOME") ?: "/usr/bin"
// Unique remote dir name to be used in the target host
private val remoteDir = run {
val date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
Paths.get(project.findProperty("remoteRoot").toString(), "tmp",
System.getProperty("user.name") + "_" + date).toString()
}
private val environmentArgs: List<String> = when {
testTarget.family.isAppleFamily ->
listOf("export DYLD_LIBRARY_PATH=$remoteDir:\$DYLD_LIBRARY_PATH;")
testTarget.family in listOf(Family.LINUX, Family.ANDROID) ->
listOf("export LD_LIBRARY_PATH=$remoteDir:\$LD_LIBRARY_PATH;")
else -> emptyList()
}
override val project: Project get() = project
override fun execute(action: Action<in ExecSpec>): ExecResult {
lateinit var executableWithLibs: ExecutableWithDynamicLibs
createRemoteDir()
val execResult = project.exec {
action.execute(this)
executableWithLibs = ExecutableWithDynamicLibs(executable)
executableWithLibs.upload()
this.executable = executableWithLibs.remoteExecutable
commandLine = arrayListOf("$sshHome/ssh") + sshArgs + remote + environmentArgs + commandLine
}
executableWithLibs.cleanup()
return execResult
}
private fun createRemoteDir() {
project.exec {
commandLine = arrayListOf("$sshHome/ssh") + sshArgs + remote + "mkdir" + "-p" + remoteDir
}
}
private fun upload(fileName: String) {
project.exec {
commandLine = arrayListOf("$sshHome/scp") + sshArgs + fileName + "$remote:$remoteDir"
}
}
private fun cleanup(fileName: String) {
project.exec {
commandLine = arrayListOf("$sshHome/ssh") + sshArgs + remote + "rm" + fileName
}
}
inner class ExecutableWithDynamicLibs(val localExecutable: String) {
val remoteExecutable: String = "$remoteDir/${File(localExecutable).name}"
val localDynamicLibs: List<String> = collectLocalDynamicLibs()
val remoteDynamicLibs: List<String> = localDynamicLibs.map { "$remoteDir/${File(it).name}" }
fun upload() {
upload(localExecutable)
localDynamicLibs.forEach(::upload)
}
fun cleanup() {
cleanup(remoteExecutable)
remoteDynamicLibs.forEach(::cleanup)
}
private val String.remotePath
get() = "$remoteDir/${File(this).name}"
private fun collectLocalDynamicLibs(): List<String> {
val dynamicSuffix = CompilerOutputKind.DYNAMIC.suffix(testTarget)
val directory = File(localExecutable).parentFile?.takeIf { it.isDirectory }
?: return emptyList()
return directory.listFiles()
.filter { it.isFile && it.name.endsWith(dynamicSuffix) }
.map { it.path }
}
}
}
internal data class DeviceTarget(
@Expose val name: String,
@Expose val udid: String,
@Expose val state: String,
@Expose val type: String
)
private fun deviceLauncher(project: Project) = object : ExecutorService {
private val xcProject = Paths.get(project.testOutputRoot, "launcher")
private val idb = project.findProperty("idb_path") as? String ?: "idb"
private val deviceName = project.findProperty("device_name") as? String
private val bundleID = "org.jetbrains.kotlin.KonanTestLauncher"
override val project: Project get() = project
override fun execute(action: Action<in ExecSpec>): ExecResult? {
val result: ExecResult?
try {
val udid = targetUDID()
println("Found device UDID: $udid")
install(udid, xcProject.resolve("build/KonanTestLauncher.ipa").toString())
val commands = startDebugServer(udid)
.split("\n")
.filter { it.isNotBlank() }
.flatMap { listOf("-o", it) }
var savedOut: OutputStream? = null
val out = ByteArrayOutputStream()
result = project.exec {
action.execute(this)
executable = "lldb"
args = commands + "-b" + "-o" + "command script import ${pythonScript()}" +
"-o" + ("process launch" +
(args.takeUnless { it.isEmpty() }
?.let { " -- ${it.joinToString(" ")}" }
?: "")) +
"-o" + "get_exit_code" +
"-k" + "get_exit_code" +
"-k" + "exit -1"
// A test task that uses project.exec { } sets the stdOut to parse the result,
// but the test executable is being run under debugger that has its own output mixed with the
// output from the test. Save the stdOut from the test to write the parsed output to it.
savedOut = this.standardOutput
standardOutput = out
}
out.toString()
.also { if (project.verboseTest) println(it) }
.split("\n")
.dropWhile { s -> !s.startsWith("(lldb) process launch") }
.drop(1) // drop 'process launch' also
.dropLastWhile { !it.matches(".*Process [0-9]* exited with status .*".toRegex()) }
.joinToString("\n") {
it.replace("Process [0-9]* exited with status .*".toRegex(), "")
.replace("\r", "") // TODO: investigate: where does the \r comes from
}
.also {
savedOut?.write(it.toByteArray())
}
uninstall(udid)
} catch (exc: Exception) {
throw RuntimeException("iOS-device execution failed", exc)
} finally {
kill()
}
return result
}
/*
* This script kills the target process in case it has been stopped,
* and exists lldb with the same exit code as a target process.
*/
private fun pythonScript(): String = xcProject.resolve("lldb_cmd.py").toFile().run {
writeText( // language=Python
"""
import lldb
def exit_code(debugger, command, exe_ctx, result, internal_dict):
process = exe_ctx.GetProcess()
state = process.GetState()
if state == lldb.eStateStopped:
# Call flush method, otherwise some output isn't shown in the debugger
debugger.HandleCommand("expression -- (int) fflush(NULL)")
debugger.HandleCommand("bt all")
process.Kill()
code = process.GetExitStatus()
debugger.HandleCommand("exit %d" % code)
def __lldb_init_module(debugger, _):
debugger.HandleCommand('command script add -f lldb_cmd.exit_code get_exit_code')
""".trimIndent())
absolutePath
}
private fun kill() = project.exec { commandLine(idb, "kill") }
private inline fun tryUntilTrue(times: Int = 3, f: () -> Boolean) {
for (i in 1..times) {
if (f()) break
else TimeUnit.SECONDS.sleep(i.toLong())
}
}
private fun targetUDID(): String {
val out = ByteArrayOutputStream()
// idb launches idb_companion but doesn't wait for it and just exits.
// So relaunch `list-targets` again.
tryUntilTrue {
project.exec {
commandLine(idb, "list-targets", "--json")
standardOutput = out
}.assertNormalExitValue()
out.toString().trim().isNotEmpty()
}
return out.toString().run {
check(isNotEmpty())
split("\n")
.filter { it.isNotEmpty() }
.map {
gson.fromJson(it, DeviceTarget::class.java)
}
.first {
it.type == "device" && deviceName?.run { this == it.name } ?: true
}
.udid
}
}
private fun install(udid: String, bundlePath: String) {
val out = ByteArrayOutputStream()
lateinit var result: ExecResult
tryUntilTrue {
result = project.exec {
workingDir = xcProject.toFile()
commandLine = listOf(idb, "install", "--udid", udid, bundlePath)
standardOutput = out
errorOutput = out
isIgnoreExitValue = true
}
println(out.toString())
result.exitValue == 0
}
check(result.exitValue == 0) { "Installation of $bundlePath failed: $out" }
}
private fun uninstall(udid: String) {
val out = ByteArrayOutputStream()
project.exec {
workingDir = xcProject.toFile()
commandLine = listOf(idb, "uninstall", "--udid", udid, bundleID)
standardOutput = out
errorOutput = out
isIgnoreExitValue = true
}
println(out.toString())
}
private fun startDebugServer(udid: String): String {
val out = ByteArrayOutputStream()
val result = project.exec {
workingDir = xcProject.toFile()
commandLine = listOf(idb, "debugserver", "start", "--udid", udid, bundleID)
standardOutput = out
errorOutput = out
isIgnoreExitValue = true
}
check(result.exitValue == 0) { "Failed to start debug server: $out" }
return out.toString()
}
}
fun KonanTestExecutable.configureXcodeBuild() {
this.doBeforeRun = Action {
val signIdentity = project.findProperty("sign_identity") as? String ?: "iPhone Developer"
val developmentTeam = project.findProperty("development_team") as? String
requireNotNull(developmentTeam) { "Specify '-Pdevelopment_team=' with the your team id" }
val xcProject = Paths.get(project.testOutputRoot, "launcher")
val shellScript: String = // language=Bash
mutableListOf("""
set -x
# Copy executable to the build dir.
COPY_TO="${"$"}TARGET_BUILD_DIR/${"$"}EXECUTABLE_PATH"
cp "${project.file(executable).absolutePath}" "${"$"}COPY_TO"
# copy dSYM if it exists
DSYM_DIR="${project.file("$executable.dSYM").absolutePath}"
if [ -d "${"$"}DSYM_DIR" ]; then
cp -r "${"$"}DSYM_DIR" "${"$"}TARGET_BUILD_DIR/${"$"}EXECUTABLE_FOLDER_PATH/"
fi
""".trimIndent()).also {
if (this is FrameworkTest) {
// Create a Frameworks folder inside the build dir.
it += "mkdir -p \"\$TARGET_BUILD_DIR/\$FRAMEWORKS_FOLDER_PATH\""
// Copy each framework to the Frameworks dir.
it += frameworks.filter { framework -> !framework.isStatic }
.map { framework ->
val artifact = framework.artifact
"cp -r \"$testOutput/${this.name}/${project.testTarget.name}/$artifact.framework\" " +
"\"\$TARGET_BUILD_DIR/\$FRAMEWORKS_FOLDER_PATH/$artifact.framework\""
}
}
}.joinToString(separator = "\\n") { it.replace("\"", "\\\"") }
// Copy template xcode project.
project.file("iosLauncher").copyRecursively(xcProject.toFile(), overwrite = true)
xcProject.resolve("KonanTestLauncher.xcodeproj/project.pbxproj")
.toFile()
.apply {
val text = readLines().joinToString("\n") {
when {
it.contains("CODE_SIGN_IDENTITY") ->
it.replaceAfter("= ", "\"$signIdentity\";")
it.contains("DEVELOPMENT_TEAM") || it.contains("DevelopmentTeam") ->
it.replaceAfter("= ", "$developmentTeam;")
it.contains("shellScript = ") ->
it.replaceAfter("= ", "\"$shellScript\";")
else -> it
}
}
writeText(text)
}
val sdk = when (project.testTarget) {
KonanTarget.IOS_ARM32, KonanTarget.IOS_ARM64 -> Xcode.findCurrent().iphoneosSdk
else -> error("Unsupported target: ${project.testTarget}")
}
fun xcodebuild(vararg elements: String) {
val xcode = listOf("/usr/bin/xcrun", "-sdk", sdk, "xcodebuild")
val out = ByteArrayOutputStream()
val result = project.exec {
workingDir = xcProject.toFile()
commandLine = xcode + elements.toList()
standardOutput = out
}
println(out.toString("UTF-8"))
result.assertNormalExitValue()
}
xcodebuild("-workspace", "KonanTestLauncher.xcodeproj/project.xcworkspace",
"-scheme", "KonanTestLauncher", "-allowProvisioningUpdates", "-destination",
"generic/platform=iOS", "build")
val archive = xcProject.resolve("build/KonanTestLauncher.xcarchive").toString()
xcodebuild("-workspace", "KonanTestLauncher.xcodeproj/project.xcworkspace",
"-scheme", "KonanTestLauncher", "archive", "-archivePath", archive)
xcodebuild("-exportArchive", "-archivePath", archive, "-exportOptionsPlist", "KonanTestLauncher/Info.plist",
"-exportPath", xcProject.resolve("build").toString())
}
}
@@ -156,12 +156,6 @@ fun codesign(project: Project, path: String) {
fun isSimulatorTarget(project: Project, target: KonanTarget): Boolean =
project.platformManager.platform(target).targetTriple.isSimulator
/**
* Check that [target] is an Apple device.
*/
fun supportsRunningTestsOnDevice(target: KonanTarget): Boolean =
target == KonanTarget.IOS_ARM32 || target == KonanTarget.IOS_ARM64
/**
* Creates a list of file paths to be compiled from the given [compile] list with regard to [exclude] list.
*/
@@ -1,36 +0,0 @@
/*
* 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))
})
}
}