Files
skiko/samples/SkiaMultiplatformSample/build.gradle.kts
Ivan Matkov cb5f486534 AGP 9 migration: upgrade wrappers to Gradle 9.5 and fix sample runtime flows (#1196)
- Upgrade Gradle wrappers to `9.5.0` across root and sample projects.
- Migrate Android build logic to AGP 9-compatible APIs (including KMP
Android integration updates).
- Fix `SkiaMultiplatformSample` iOS simulator run flow:
  - remove `applePlugin` usage
- replace invalid simctl spawn `--standalone <kexe>` with app bundle
install + launch
  - add `-Pskiko.iosSimulatorDevice=<device>` for simulator targeting
  - fix `IosClocks` renderApi reference typo
- Disable Kotlin JS Yarn lock generation in JS-enabled projects
2026-06-10 10:59:02 +02:00

418 lines
14 KiB
Kotlin

@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
import org.jetbrains.kotlin.gradle.plugin.mpp.*
buildscript {
repositories {
google()
mavenCentral {
url = uri("https://cache-redirector.jetbrains.com/maven-central")
}
maven("https://redirector.kotlinlang.org/maven/compose-dev")
}
dependencies {
val kotlinVersion = project.property("kotlin.version") as String
classpath(kotlin("gradle-plugin", version = kotlinVersion))
}
}
plugins {
kotlin("multiplatform")
}
repositories {
google()
mavenCentral {
url = uri("https://cache-redirector.jetbrains.com/maven-central")
}
mavenLocal()
maven("https://redirector.kotlinlang.org/maven/compose-dev")
}
val osName = System.getProperty("os.name")
val hostOs = when {
osName == "Mac OS X" -> "macos"
osName.startsWith("Win") -> "windows"
osName.startsWith("Linux") -> "linux"
else -> error("Unsupported OS: $osName")
}
val osArch = System.getProperty("os.arch")
var hostArch = when (osArch) {
"x86_64", "amd64" -> "x64"
"aarch64" -> "arm64"
else -> error("Unsupported arch: $osArch")
}
val host = "${hostOs}-${hostArch}"
val isCompositeBuild = extra.properties.getOrDefault("skiko.composite.build", "") == "1"
if (project.hasProperty("skiko.version") && isCompositeBuild) {
project.logger.warn("skiko.version property has no effect when skiko.composite.build is set")
}
val skikoWasm by configurations.creating
dependencies {
skikoWasm(if (isCompositeBuild) {
// When we build skiko locally, we have no say in setting skiko.version in the included build.
// That said, it is always built as "0.0.0-SNAPSHOT" and setting any other version is misleading
// and can create conflict due to incompatibility of skiko runtime and skiko libs
files(gradle.includedBuild("skiko").projectDir.resolve("./build/libs/skiko-wasm-0.0.0-SNAPSHOT.jar"))
} else {
libs.skiko.wasm.runtime
})
}
val unpackWasmRuntime = tasks.register("unpackWasmRuntime", Copy::class) {
destinationDir = file("$buildDir/resources/")
from(skikoWasm.map { zipTree(it) })
if (isCompositeBuild) {
dependsOn(gradle.includedBuild("skiko").task(":skikoWasmJar"))
}
}
tasks.withType<org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile>().configureEach {
dependsOn(unpackWasmRuntime)
}
kotlin {
if (hostOs == "macos") {
macosX64() {
configureToLaunchFromXcode()
}
macosArm64() {
configureToLaunchFromXcode()
}
iosSimulatorArm64() {
configureToLaunchFromAppCode()
configureToLaunchFromXcode()
}
tvosX64() {
configureToLaunchFromAppCode()
configureToLaunchFromXcode()
}
tvosArm64() {
configureToLaunchFromAppCode()
configureToLaunchFromXcode()
}
tvosSimulatorArm64() {
configureToLaunchFromAppCode()
configureToLaunchFromXcode()
}
}
jvm("awt") {
compilations.all {
compileTaskProvider.configure {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}
}
}
js(IR) {
browser {
commonWebpackConfig {
outputFileName = "webApp.js"
}
}
binaries.executable()
}
wasmJs {
browser {
commonWebpackConfig {
outputFileName = "webApp.js"
}
}
binaries.executable()
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.skiko)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}
}
val nativeMain by creating {
dependsOn(commonMain)
}
val awtMain by getting {
dependsOn(commonMain)
dependencies {
implementation(libs.skiko.awt.runtime)
}
}
val webMain by creating {
dependsOn(commonMain)
resources.setSrcDirs(resources.srcDirs)
resources.srcDirs(unpackWasmRuntime.map { it.destinationDir })
}
val jsMain by getting {
dependsOn(webMain)
}
val wasmJsMain by getting {
dependsOn(webMain)
}
val darwinMain by creating {
dependsOn(nativeMain)
}
val macosMain by creating {
dependsOn(darwinMain)
}
if (hostOs == "macos") {
val macosX64Main by getting {
dependsOn(macosMain)
}
val macosArm64Main by getting {
dependsOn(macosMain)
}
val uikitMain by creating {
dependsOn(darwinMain)
}
val iosMain by creating {
dependsOn(uikitMain)
}
val iosSimulatorArm64Main by getting {
dependsOn(iosMain)
}
val tvosMain by creating {
dependsOn(uikitMain)
}
val tvosX64Main by getting {
dependsOn(tvosMain)
}
val tvosArm64Main by getting {
dependsOn(tvosMain)
}
val tvosSimulatorArm64Main by getting {
dependsOn(tvosMain)
}
}
}
}
if (hostOs == "macos") {
val iosSimDevice = providers.gradleProperty("skiko.iosSimulatorDevice").orElse("booted")
val iosSimAppName = "SkiaMultiplatformSample"
val iosSimBundleId = "org.jetbrains.skiko.sample"
val iosSimLinkExecutableTaskName = when (host) {
"macos-x64" -> throw GradleException("runIosSim is supported only on Apple Silicon hosts (iosSimulatorArm64 target)")
"macos-arm64" -> "linkReleaseExecutableIosSimulatorArm64"
else -> throw GradleException("Host OS is not supported")
}
val packageIosSimApp = project.tasks.register("packageIosSimApp") {
val binTask = project.tasks.named(iosSimLinkExecutableTaskName)
dependsOn(binTask)
doLast {
val executable = fileTree(binTask.get().outputs.files.files.single()) { include("*.kexe") }
.single { it.name.endsWith(".kexe") }
val appDir = project.layout.buildDirectory.dir("iosSimulator/${iosSimAppName}.app").get().asFile
appDir.mkdirs()
val targetExecutable = appDir.resolve(iosSimAppName)
executable.copyTo(targetExecutable, overwrite = true)
targetExecutable.setExecutable(true)
appDir.resolve("PkgInfo").writeText("APPL????")
val plistTemplate = project.file("plists/Ios/Info.plist").readText()
appDir.resolve("Info.plist").writeText(
plistTemplate
.replace("$(DEVELOPMENT_LANGUAGE)", "en")
.replace("$(EXECUTABLE_NAME)", iosSimAppName)
.replace("$(PRODUCT_BUNDLE_IDENTIFIER)", iosSimBundleId)
.replace("$(PRODUCT_NAME)", iosSimAppName)
)
}
}
project.tasks.register("runIosSim") {
dependsOn(packageIosSimApp)
doLast {
fun runCommand(command: List<String>, ignoreFailure: Boolean = false) {
val process = ProcessBuilder(command)
.directory(project.projectDir)
.inheritIO()
.start()
val exitCode = process.waitFor()
if (exitCode != 0 && !ignoreFailure) {
throw GradleException("Command failed ($exitCode): ${command.joinToString(" ")}")
}
}
val appDir = project.layout.buildDirectory.dir("iosSimulator/${iosSimAppName}.app").get().asFile.absolutePath
val device = iosSimDevice.get()
val launchTarget = if (device == "booted") "booted" else device
if (device != "booted") {
runCommand(listOf("xcrun", "simctl", "boot", device), ignoreFailure = true)
runCommand(listOf("xcrun", "simctl", "bootstatus", device, "-b"))
}
runCommand(listOf("xcrun", "simctl", "install", launchTarget, appDir))
runCommand(listOf("xcrun", "simctl", "launch", "--terminate-running-process", launchTarget, iosSimBundleId))
}
}
project.tasks.register<Exec>("runNative") {
workingDir = project.buildDir
val hostOsCap = hostOs.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
val hostArchCap = hostArch.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
val binTask = project.tasks.named("linkDebugExecutable${hostOsCap}${hostArchCap}")
dependsOn(binTask)
// Hacky approach.
commandLine = listOf("bash", "-c")
argumentProviders.add {
val out = fileTree(binTask.get().outputs.files.files.single()) { include("*.kexe") }
println("Run $out")
listOf(out.single { it.name.endsWith(".kexe") }.absolutePath)
}
}
}
project.tasks.register<JavaExec>("runAwt") {
val kotlinTask = project.tasks.named("compileKotlinAwt")
dependsOn(kotlinTask)
systemProperty("skiko.fps.enabled", "true")
systemProperty("skiko.linux.autodpi", "true")
systemProperty("skiko.hardwareInfo.enabled", "true")
systemProperty("skiko.win.exception.logger.enabled", "true")
systemProperty("skiko.win.exception.handler.enabled", "true")
jvmArgs("-ea")
System.getProperties().entries
.associate {
(it.key as String) to (it.value as String)
}
.filterKeys { it.startsWith("skiko.") }
.forEach { systemProperty(it.key, it.value) }
mainClass.set("org.jetbrains.skiko.sample.App_awtKt")
classpath(kotlinTask.get().outputs)
classpath(kotlin.jvm("awt").compilations["main"].runtimeDependencyFiles)
}
enum class Target(val simulator: Boolean, val key: String) {
WATCHOS_X86(true, "watchos"),
WATCHOS_ARM64(false, "watchos"),
IOS_X64(true, "iosX64"),
IOS_ARM64(false, "iosArm64"),
IOS_SIMULATOR_ARM64(true, "iosSimulatorArm64"),
TVOS_X64(true, "tvosX64"),
TVOS_ARM64(true, "tvosArm64"),
TVOS_SIMULATOR_ARM64(true, "tvosSimulatorArm64"),
}
if (hostOs == "macos") {
// Create Xcode integration tasks.
val sdkName: String? = System.getenv("SDK_NAME")
println("Configuring XCode for $sdkName")
val target = sdkName.orEmpty().let {
when {
it.startsWith("iphoneos") -> Target.IOS_ARM64
it.startsWith("appletvsimulator") -> when (host) {
"macos-x64" -> Target.TVOS_X64
"macos-arm64" -> Target.TVOS_SIMULATOR_ARM64
else -> throw GradleException("Host OS is not supported")
}
it.startsWith("appletvos") -> Target.TVOS_ARM64
it.startsWith("watchos") -> Target.WATCHOS_ARM64
it.startsWith("watchsimulator") -> Target.WATCHOS_X86
else -> when (host) {
"macos-x64" -> Target.IOS_X64
"macos-arm64" -> Target.IOS_SIMULATOR_ARM64
else -> throw GradleException("Host OS is not supported")
}
}
}
val targetBuildDir: String? = System.getenv("TARGET_BUILD_DIR")
val executablePath: String? = System.getenv("EXECUTABLE_PATH")
val buildType = System.getenv("CONFIGURATION")?.let {
org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.valueOf(it.uppercase())
} ?: org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.DEBUG
val currentTarget = kotlin.targets[target.key] as org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
val kotlinBinary = currentTarget.binaries.getExecutable(buildType)
val xcodeIntegrationGroup = "Xcode integration"
val packForXCode = if (sdkName == null || targetBuildDir == null || executablePath == null) {
// The build is launched not by Xcode ->
// We cannot create a copy task and just show a meaningful error message.
tasks.create("packForXCode").doLast {
throw IllegalStateException("Please run the task from Xcode")
}
} else {
// Otherwise copy the executable into the Xcode output directory.
tasks.create("packForXCode", Copy::class.java) {
dependsOn(kotlinBinary.linkTaskProvider)
println("Packing for XCode: ${kotlinBinary.target}")
destinationDir = file(targetBuildDir)
val dsymSource = kotlinBinary.outputFile.absolutePath + ".dSYM"
val dsymDestination = File(executablePath).parentFile.name + ".dSYM"
val oldExecName = kotlinBinary.outputFile.name
val newExecName = File(executablePath).name
from(dsymSource) {
into(dsymDestination)
rename(oldExecName, newExecName)
}
from(kotlinBinary.outputFile) {
rename { executablePath }
}
}
}
}
fun KotlinNativeTarget.configureToLaunchFromAppCode() {
binaries {
framework {
baseName = "shared"
freeCompilerArgs += listOf(
"-linker-option", "-framework", "-linker-option", "Metal",
"-linker-option", "-framework", "-linker-option", "CoreText",
"-linker-option", "-framework", "-linker-option", "CoreGraphics"
)
}
}
}
fun KotlinNativeTarget.configureToLaunchFromXcode() {
binaries {
executable {
entryPoint = "org.jetbrains.skiko.sample.main"
freeCompilerArgs += listOf(
"-linker-option", "-framework", "-linker-option", "Metal",
"-linker-option", "-framework", "-linker-option", "CoreText",
"-linker-option", "-framework", "-linker-option", "CoreGraphics"
)
}
}
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile>().configureEach {
compilerOptions.freeCompilerArgs.add("-opt-in=kotlinx.cinterop.ExperimentalForeignApi")
}