Prepare test infrastructure for Apple Silicon simulators.
* Get rid of explicit target enumerations by using TargetTriple. * Remove some code duplication with `project.testTargetConfigurables`
This commit is contained in:
committed by
TeamCityServer
parent
cebf815fed
commit
437a0aa450
@@ -46,42 +46,32 @@ interface ExecutorService {
|
|||||||
* Creates an ExecutorService depending on a test target -Ptest_target
|
* Creates an ExecutorService depending on a test target -Ptest_target
|
||||||
*/
|
*/
|
||||||
fun create(project: Project): ExecutorService {
|
fun create(project: Project): ExecutorService {
|
||||||
val platformManager = project.platformManager
|
|
||||||
val testTarget = project.testTarget
|
val testTarget = project.testTarget
|
||||||
val platform = platformManager.platform(testTarget)
|
val configurables = project.testTargetConfigurables
|
||||||
val absoluteTargetToolchain = platform.absoluteTargetToolchain
|
|
||||||
|
|
||||||
return if (project.hasProperty("remote")) {
|
|
||||||
sshExecutor(project)
|
|
||||||
} else when (testTarget) {
|
|
||||||
KonanTarget.WASM32 -> object : ExecutorService {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
KonanTarget.LINUX_MIPS32,
|
|
||||||
KonanTarget.LINUX_MIPSEL32,
|
|
||||||
KonanTarget.LINUX_ARM32_HFP,
|
|
||||||
KonanTarget.LINUX_ARM64 -> emulatorExecutor(project, testTarget)
|
|
||||||
|
|
||||||
KonanTarget.IOS_X64,
|
|
||||||
KonanTarget.TVOS_X64,
|
|
||||||
KonanTarget.WATCHOS_X86,
|
|
||||||
KonanTarget.WATCHOS_X64 -> simulator(project)
|
|
||||||
|
|
||||||
KonanTarget.IOS_ARM32,
|
|
||||||
KonanTarget.IOS_ARM64 -> deviceLauncher(project)
|
|
||||||
|
|
||||||
|
return when {
|
||||||
|
project.hasProperty("remote") -> sshExecutor(project)
|
||||||
|
configurables is WasmConfigurables -> wasmExecutor(project)
|
||||||
|
configurables is ConfigurablesWithEmulator && testTarget != HostManager.host -> emulatorExecutor(project, testTarget)
|
||||||
|
configurables.targetTriple.isSimulator -> simulator(project)
|
||||||
|
testTarget == KonanTarget.IOS_ARM32 || testTarget == KonanTarget.IOS_ARM64 -> deviceLauncher(project)
|
||||||
else -> localExecutorService(project)
|
else -> localExecutorService(project)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun wasmExecutor(project: Project) = object : ExecutorService {
|
||||||
|
val absoluteTargetToolchain = project.testTargetConfigurables.absoluteTargetToolchain
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class ProcessOutput(var stdOut: String, var stdErr: String, var exitCode: Int)
|
data class ProcessOutput(var stdOut: String, var stdErr: String, var exitCode: Int)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -196,8 +186,7 @@ fun localExecutorService(project: Project): ExecutorService = object : ExecutorS
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun emulatorExecutor(project: Project, target: KonanTarget) = object : ExecutorService {
|
private fun emulatorExecutor(project: Project, target: KonanTarget) = object : ExecutorService {
|
||||||
val platformManager = project.platformManager
|
val configurables = project.testTargetConfigurables as? ConfigurablesWithEmulator
|
||||||
val configurables = platformManager.platform(target).configurables as? ConfigurablesWithEmulator
|
|
||||||
?: error("$target does not support emulation!")
|
?: error("$target does not support emulation!")
|
||||||
val absoluteTargetSysRoot = configurables.absoluteTargetSysRoot
|
val absoluteTargetSysRoot = configurables.absoluteTargetSysRoot
|
||||||
|
|
||||||
@@ -234,15 +223,19 @@ private fun emulatorExecutor(project: Project, target: KonanTarget) = object : E
|
|||||||
private fun simulator(project: Project): ExecutorService = object : ExecutorService {
|
private fun simulator(project: Project): ExecutorService = object : ExecutorService {
|
||||||
|
|
||||||
private val target = project.testTarget
|
private val target = project.testTarget
|
||||||
|
val configurables = project.testTargetConfigurables as AppleConfigurables
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(configurables.targetTriple.isSimulator) {
|
||||||
|
"${configurables.target} is not a simulator."
|
||||||
|
}
|
||||||
|
require(HostManager.host.architecture == configurables.target.architecture) {
|
||||||
|
"Can't run simulator with ${configurables.target.architecture} architecture."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val simctl by lazy {
|
private val simctl by lazy {
|
||||||
val sdk = when (target) {
|
val sdk = Xcode.current.pathToPlatformSdk(configurables.platformName())
|
||||||
KonanTarget.TVOS_X64 -> Xcode.current.appletvsimulatorSdk
|
|
||||||
KonanTarget.IOS_X64 -> Xcode.current.iphonesimulatorSdk
|
|
||||||
KonanTarget.WATCHOS_X64,
|
|
||||||
KonanTarget.WATCHOS_X86 -> Xcode.current.watchsimulatorSdk
|
|
||||||
else -> error("Unexpected simulation target: $target")
|
|
||||||
}
|
|
||||||
val out = ByteArrayOutputStream()
|
val out = ByteArrayOutputStream()
|
||||||
val result = project.exec {
|
val result = project.exec {
|
||||||
commandLine("/usr/bin/xcrun", "--find", "simctl", "--sdk", sdk)
|
commandLine("/usr/bin/xcrun", "--find", "simctl", "--sdk", sdk)
|
||||||
@@ -282,7 +275,8 @@ private fun simulator(project: Project): ExecutorService = object : ExecutorServ
|
|||||||
|
|
||||||
private val archSpecification = when (target.architecture) {
|
private val archSpecification = when (target.architecture) {
|
||||||
Architecture.X86 -> listOf("-a", "i386")
|
Architecture.X86 -> listOf("-a", "i386")
|
||||||
Architecture.X64 -> listOf() // x86-64 is used by default.
|
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.")
|
else -> error("${target.architecture} can't be used in simulator.")
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
|
|
||||||
|
|||||||
@@ -178,26 +178,12 @@ open class FrameworkTest : DefaultTask(), KonanTestExecutable {
|
|||||||
* test target.
|
* test target.
|
||||||
*/
|
*/
|
||||||
private fun getSwiftLibsPathForTestTarget(): String {
|
private fun getSwiftLibsPathForTestTarget(): String {
|
||||||
val target = project.testTarget
|
val configs = project.testTargetConfigurables as AppleConfigurables
|
||||||
val platform = project.platformManager.platform(target)
|
val swiftPlatform = configs.platformName().toLowerCase()
|
||||||
val configs = platform.configurables as AppleConfigurables
|
val simulatorPath = when (configs.targetTriple.isSimulator) {
|
||||||
val swiftPlatform = when (target) {
|
true -> Xcode.current.getLatestSimulatorRuntimeFor(configs.target.family, configs.osVersionMin)
|
||||||
KonanTarget.IOS_X64 -> "iphonesimulator"
|
?.bundlePath
|
||||||
KonanTarget.IOS_ARM32, KonanTarget.IOS_ARM64 -> "iphoneos"
|
?.let { "$it/Contents/Resources/RuntimeRoot/usr/lib/swift" }
|
||||||
KonanTarget.TVOS_X64 -> "appletvsimulator"
|
|
||||||
KonanTarget.TVOS_ARM64 -> "appletvos"
|
|
||||||
KonanTarget.MACOS_X64, KonanTarget.MACOS_ARM64 -> "macosx"
|
|
||||||
KonanTarget.WATCHOS_ARM64 -> "watchos"
|
|
||||||
KonanTarget.WATCHOS_X64, KonanTarget.WATCHOS_X86 -> "watchsimulator"
|
|
||||||
else -> throw IllegalStateException("Test target $target is not supported")
|
|
||||||
}
|
|
||||||
val simulatorPath = when (target) {
|
|
||||||
KonanTarget.TVOS_X64,
|
|
||||||
KonanTarget.IOS_X64,
|
|
||||||
KonanTarget.WATCHOS_X86,
|
|
||||||
KonanTarget.WATCHOS_X64 -> Xcode.current.getLatestSimulatorRuntimeFor(target, configs.osVersionMin)?.bundlePath?.let {
|
|
||||||
"$it/Contents/Resources/RuntimeRoot/usr/lib/swift"
|
|
||||||
}
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
// Use default path from toolchain if we cannot get `bundlePath` for target.
|
// Use default path from toolchain if we cannot get `bundlePath` for target.
|
||||||
@@ -206,18 +192,16 @@ open class FrameworkTest : DefaultTask(), KonanTestExecutable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildEnvironment(): Map<String, String> {
|
private fun buildEnvironment(): Map<String, String> {
|
||||||
val target = project.testTarget
|
val configs = project.testTargetConfigurables
|
||||||
// Hopefully, lexicographical comparison will work.
|
// Hopefully, lexicographical comparison will work.
|
||||||
val newMacos = System.getProperty("os.version").compareTo("10.14.4") >= 0
|
val newMacos = System.getProperty("os.version").compareTo("10.14.4") >= 0
|
||||||
val dyldLibraryPathKey = when (target) {
|
val dyldLibraryPathKey = if (configs.targetTriple.isSimulator) {
|
||||||
KonanTarget.IOS_X64,
|
"SIMCTL_CHILD_DYLD_LIBRARY_PATH"
|
||||||
KonanTarget.WATCHOS_X86,
|
} else {
|
||||||
KonanTarget.WATCHOS_X64,
|
"DYLD_LIBRARY_PATH"
|
||||||
KonanTarget.TVOS_X64 -> "SIMCTL_CHILD_DYLD_LIBRARY_PATH"
|
|
||||||
else -> "DYLD_LIBRARY_PATH"
|
|
||||||
}
|
}
|
||||||
// TODO: macos_arm64?
|
// TODO: macos_arm64?
|
||||||
return if (newMacos && target == KonanTarget.MACOS_X64) emptyMap() else mapOf(
|
return if (newMacos && configs.target == KonanTarget.MACOS_X64) emptyMap() else mapOf(
|
||||||
dyldLibraryPathKey to getSwiftLibsPathForTestTarget()
|
dyldLibraryPathKey to getSwiftLibsPathForTestTarget()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -245,25 +229,14 @@ open class FrameworkTest : DefaultTask(), KonanTestExecutable {
|
|||||||
if (!fullBitcode) {
|
if (!fullBitcode) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val testTarget = project.testTarget
|
val configurables = project.testTargetConfigurables as AppleConfigurables
|
||||||
val configurables = project.platformManager.platform(testTarget).configurables as AppleConfigurables
|
|
||||||
|
|
||||||
val bitcodeBuildTool = "${configurables.absoluteAdditionalToolsDir}/bin/bitcode-build-tool"
|
val bitcodeBuildTool = "${configurables.absoluteAdditionalToolsDir}/bin/bitcode-build-tool"
|
||||||
val toolPath = "${configurables.absoluteTargetToolchain}/usr/bin/"
|
val toolPath = "${configurables.absoluteTargetToolchain}/usr/bin/"
|
||||||
val sdk = when (testTarget) {
|
if (configurables.targetTriple.isSimulator) {
|
||||||
KonanTarget.IOS_X64,
|
return // bitcode-build-tool doesn't support simulators.
|
||||||
KonanTarget.TVOS_X64,
|
|
||||||
KonanTarget.WATCHOS_X86,
|
|
||||||
KonanTarget.WATCHOS_X64 -> return // bitcode-build-tool doesn't support simulators.
|
|
||||||
KonanTarget.IOS_ARM64,
|
|
||||||
KonanTarget.IOS_ARM32 -> Xcode.current.iphoneosSdk
|
|
||||||
KonanTarget.MACOS_X64,
|
|
||||||
KonanTarget.MACOS_ARM64 -> Xcode.current.macosxSdk
|
|
||||||
KonanTarget.TVOS_ARM64 -> Xcode.current.appletvosSdk
|
|
||||||
KonanTarget.WATCHOS_ARM32,
|
|
||||||
KonanTarget.WATCHOS_ARM64 -> Xcode.current.watchosSdk
|
|
||||||
else -> error("Cannot validate bitcode for test target $testTarget")
|
|
||||||
}
|
}
|
||||||
|
val sdk = Xcode.current.pathToPlatformSdk(configurables.platformName())
|
||||||
|
|
||||||
val python3 = listOf("/usr/bin/python3", "/usr/local/bin/python3")
|
val python3 = listOf("/usr/bin/python3", "/usr/local/bin/python3")
|
||||||
.map { Paths.get(it) }.firstOrNull { Files.exists(it) }
|
.map { Paths.get(it) }.firstOrNull { Files.exists(it) }
|
||||||
|
|||||||
@@ -292,18 +292,7 @@ fun compileSwift(project: Project, target: KonanTarget, sources: List<String>, o
|
|||||||
val configs = platform.configurables as AppleConfigurables
|
val configs = platform.configurables as AppleConfigurables
|
||||||
val compiler = configs.absoluteTargetToolchain + "/usr/bin/swiftc"
|
val compiler = configs.absoluteTargetToolchain + "/usr/bin/swiftc"
|
||||||
|
|
||||||
val swiftTarget = when (target) {
|
val swiftTarget = configs.targetTriple.withOSVersion(configs.osVersionMin).toString()
|
||||||
KonanTarget.IOS_X64 -> "x86_64-apple-ios" + configs.osVersionMin
|
|
||||||
KonanTarget.IOS_ARM32 -> "armv7-apple-ios" + configs.osVersionMin
|
|
||||||
KonanTarget.IOS_ARM64 -> "arm64-apple-ios" + configs.osVersionMin
|
|
||||||
KonanTarget.TVOS_X64 -> "x86_64-apple-tvos" + configs.osVersionMin
|
|
||||||
KonanTarget.TVOS_ARM64 -> "arm64-apple-tvos" + configs.osVersionMin
|
|
||||||
KonanTarget.MACOS_X64 -> "x86_64-apple-macosx" + configs.osVersionMin
|
|
||||||
KonanTarget.MACOS_ARM64 -> "arm64-apple-macos" + configs.osVersionMin
|
|
||||||
KonanTarget.WATCHOS_X86 -> "i386-apple-watchos" + configs.osVersionMin
|
|
||||||
KonanTarget.WATCHOS_X64 -> "x86_64-apple-watchos" + configs.osVersionMin
|
|
||||||
else -> throw IllegalStateException("Test target $target is not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
val args = listOf("-sdk", configs.absoluteTargetSysRoot, "-target", swiftTarget) +
|
val args = listOf("-sdk", configs.absoluteTargetSysRoot, "-target", swiftTarget) +
|
||||||
options + "-o" + output.toString() + sources +
|
options + "-o" + output.toString() + sources +
|
||||||
@@ -411,3 +400,10 @@ internal fun StringBuilder.appendln(o: Any?) {
|
|||||||
append(o)
|
append(o)
|
||||||
append('\n')
|
append('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal val Project.testTargetConfigurables: Configurables
|
||||||
|
get() {
|
||||||
|
val platformManager = project.platformManager
|
||||||
|
val testTarget = project.testTarget
|
||||||
|
return platformManager.platform(testTarget).configurables
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package org.jetbrains.kotlin
|
package org.jetbrains.kotlin
|
||||||
|
|
||||||
import com.google.gson.annotations.Expose
|
import com.google.gson.annotations.Expose
|
||||||
import org.jetbrains.kotlin.konan.target.KonanTarget
|
import org.jetbrains.kotlin.konan.target.Family
|
||||||
import org.jetbrains.kotlin.konan.target.Xcode
|
import org.jetbrains.kotlin.konan.target.Xcode
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@@ -28,12 +28,12 @@ private fun Xcode.getSimulatorRuntimeDescriptors(): List<SimulatorRuntimeDescrip
|
|||||||
/**
|
/**
|
||||||
* Returns first available simulator runtime for [target] with at least [osMinVersion] OS version.
|
* Returns first available simulator runtime for [target] with at least [osMinVersion] OS version.
|
||||||
* */
|
* */
|
||||||
fun Xcode.getLatestSimulatorRuntimeFor(target: KonanTarget, osMinVersion: String): SimulatorRuntimeDescriptor? {
|
fun Xcode.getLatestSimulatorRuntimeFor(family: Family, osMinVersion: String): SimulatorRuntimeDescriptor? {
|
||||||
val osName = when (target) {
|
val osName = when (family) {
|
||||||
KonanTarget.IOS_X64 -> "iOS"
|
Family.IOS -> "iOS"
|
||||||
KonanTarget.WATCHOS_X64, KonanTarget.WATCHOS_X86 -> "watchOS"
|
Family.WATCHOS -> "watchOS"
|
||||||
KonanTarget.TVOS_X64 -> "tvOS"
|
Family.TVOS -> "tvOS"
|
||||||
else -> error("Unexpected simulator target: $target")
|
else -> error("Unexpected simulator OS: $family")
|
||||||
}
|
}
|
||||||
return getSimulatorRuntimeDescriptors().firstOrNull {
|
return getSimulatorRuntimeDescriptors().firstOrNull {
|
||||||
it.checkAvailability() && it.name.startsWith(osName) && compareStringsAsVersions(it.version, osMinVersion) >= 0
|
it.checkAvailability() && it.name.startsWith(osName) && compareStringsAsVersions(it.version, osMinVersion) >= 0
|
||||||
|
|||||||
@@ -32,16 +32,7 @@ class AppleConfigurablesImpl(
|
|||||||
private val xcodeAddonDependency = this.additionalToolsDir!!
|
private val xcodeAddonDependency = this.additionalToolsDir!!
|
||||||
|
|
||||||
override val absoluteTargetSysRoot: String get() = when (val provider = xcodePartsProvider) {
|
override val absoluteTargetSysRoot: String get() = when (val provider = xcodePartsProvider) {
|
||||||
is XcodePartsProvider.Local -> when (target) {
|
is XcodePartsProvider.Local -> provider.xcode.pathToPlatformSdk(platformName())
|
||||||
KonanTarget.MACOS_X64, KonanTarget.MACOS_ARM64 -> provider.xcode.macosxSdk
|
|
||||||
KonanTarget.IOS_ARM32, KonanTarget.IOS_ARM64 -> provider.xcode.iphoneosSdk
|
|
||||||
KonanTarget.IOS_X64 -> provider.xcode.iphonesimulatorSdk
|
|
||||||
KonanTarget.TVOS_ARM64 -> provider.xcode.appletvosSdk
|
|
||||||
KonanTarget.TVOS_X64 -> provider.xcode.appletvsimulatorSdk
|
|
||||||
KonanTarget.WATCHOS_ARM64, KonanTarget.WATCHOS_ARM32 -> provider.xcode.watchosSdk
|
|
||||||
KonanTarget.WATCHOS_X64, KonanTarget.WATCHOS_X86 -> provider.xcode.watchsimulatorSdk
|
|
||||||
else -> error(target)
|
|
||||||
}
|
|
||||||
XcodePartsProvider.InternalServer -> absolute(sdkDependency)
|
XcodePartsProvider.InternalServer -> absolute(sdkDependency)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,3 +92,26 @@ class AppleConfigurablesImpl(
|
|||||||
object InternalServer : XcodePartsProvider()
|
object InternalServer : XcodePartsProvider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of an Apple platform as in Xcode.app/Contents/Developer/Platforms.
|
||||||
|
*/
|
||||||
|
fun AppleConfigurables.platformName(): String = when (target.family) {
|
||||||
|
Family.OSX -> "MacOSX"
|
||||||
|
Family.IOS -> if (targetTriple.isSimulator) {
|
||||||
|
"iPhoneSimulator"
|
||||||
|
} else {
|
||||||
|
"iPhoneOS"
|
||||||
|
}
|
||||||
|
Family.TVOS -> if (targetTriple.isSimulator) {
|
||||||
|
"AppleTVSimulator"
|
||||||
|
} else {
|
||||||
|
"AppleTVOS"
|
||||||
|
}
|
||||||
|
Family.WATCHOS -> if (targetTriple.isSimulator) {
|
||||||
|
"WatchSimulator"
|
||||||
|
} else {
|
||||||
|
"WatchOS"
|
||||||
|
}
|
||||||
|
else -> error("Not an Apple target: $target")
|
||||||
|
}
|
||||||
|
|||||||
+15
-1
@@ -50,4 +50,18 @@ data class TargetTriple(
|
|||||||
?: ""
|
?: ""
|
||||||
return "$architecture-$vendor-$os$envSuffix"
|
return "$architecture-$vendor-$os$envSuffix"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that given target is Apple simulator.
|
||||||
|
*/
|
||||||
|
val TargetTriple.isSimulator: Boolean
|
||||||
|
get() = environment == "simulator"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends version to OS part of triple.
|
||||||
|
*
|
||||||
|
* Useful for precise target specification in Clang and Swift.
|
||||||
|
*/
|
||||||
|
fun TargetTriple.withOSVersion(osVersion: String): TargetTriple =
|
||||||
|
copy(os = "${os}${osVersion}")
|
||||||
@@ -35,6 +35,17 @@ interface Xcode {
|
|||||||
val additionalTools: String
|
val additionalTools: String
|
||||||
val simulatorRuntimes: String
|
val simulatorRuntimes: String
|
||||||
|
|
||||||
|
fun pathToPlatformSdk(platformName: String): String = when (platformName.toLowerCase()) {
|
||||||
|
"macosx" -> macosxSdk
|
||||||
|
"iphoneos" -> iphoneosSdk
|
||||||
|
"iphonesimulator" -> iphonesimulatorSdk
|
||||||
|
"appletvos" -> appletvosSdk
|
||||||
|
"appletvsimulator" -> appletvsimulatorSdk
|
||||||
|
"watchos" -> watchosSdk
|
||||||
|
"watchsimulator" -> watchsimulatorSdk
|
||||||
|
else -> error("Unknown Apple platform: $platformName")
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val current: Xcode by lazy {
|
val current: Xcode by lazy {
|
||||||
CurrentXcode
|
CurrentXcode
|
||||||
|
|||||||
Reference in New Issue
Block a user