Unverified Commit de761f6b authored by Alexey Tsvetkov's avatar Alexey Tsvetkov Committed by GitHub

Replace Gradle native compilation tasks with custom tasks (#237)

* Replace Gradle native compilation tasks with custom tasks

* Code review fixes

* Log args when info logging is enabled
parent cf6413c8
...@@ -6,7 +6,6 @@ import org.jetbrains.kotlin.utils.keysToMap ...@@ -6,7 +6,6 @@ import org.jetbrains.kotlin.utils.keysToMap
plugins { plugins {
kotlin("multiplatform") version "1.5.31" kotlin("multiplatform") version "1.5.31"
`cpp-library`
`maven-publish` `maven-publish`
id("org.gradle.crypto.checksum") version "1.1.0" id("org.gradle.crypto.checksum") version "1.1.0"
id("de.undercouch.download") version "4.1.1" id("de.undercouch.download") version "4.1.1"
...@@ -53,6 +52,10 @@ val skiaZip = run { ...@@ -53,6 +52,10 @@ val skiaZip = run {
}.map { zipFile } }.map { zipFile }
} }
val windowsSdkPaths: WindowsSdkPaths by lazy {
findWindowsSdkPathsForCurrentOS(gradle)
}
val crossTargets = listOf( val crossTargets = listOf(
OS.Wasm to Arch.Wasm, OS.Wasm to Arch.Wasm,
OS.IOS to Arch.X64, OS.IOS to Arch.X64,
...@@ -105,42 +108,44 @@ val skiaDirProviderForCrossTargets: Map<Pair<OS, Arch>, Provider<File>> = crossT ...@@ -105,42 +108,44 @@ val skiaDirProviderForCrossTargets: Map<Pair<OS, Arch>, Provider<File>> = crossT
} }
} }
val wasmCrossCompile = tasks.register<CrossCompileTask>("wasmCrossCompile") { val compileWasm = tasks.register<CompileSkikoCppTask>("compileWasm") {
val osArch = OS.Wasm to Arch.Wasm val osArch = OS.Wasm to Arch.Wasm
val unzipper = skiaDirProviderForCrossTargets[osArch]!! val unzipper = skiaDirProviderForCrossTargets[osArch]!!
dependsOn(unzipper) dependsOn(unzipper)
val unpackedSkia = unzipper.get() val unpackedSkia = unzipper.get()
compiler.set("emcc") compiler.set(compilerForTarget(OS.Wasm, Arch.Wasm))
crossCompileTargetArch.set(osArch.second) buildTargetOS.set(osArch.first)
buildTargetArch.set(osArch.second)
buildVariant.set(buildType) buildVariant.set(buildType)
sourceFiles = val srcDirs = projectDirs("src/jsMain/cpp", "src/commonMain/cpp") +
project.fileTree("src/jsMain/cpp") { include("**/*.cc") } + if (skiko.includeTestHelpers) projectDirs("src/commonTest/cpp") else emptyList()
project.fileTree("src/commonMain/cpp") { include("**/*.cc") } sourceRoots.set(srcDirs)
if (skiko.includeTestHelpers) {
sourceFiles += project.fileTree("src/commonTest/cpp") { include("**/*.cc") }
}
outDir.set(project.layout.buildDirectory.dir("out/compile/${buildType.id}-${osArch.first.id}-${osArch.second.id}"))
includeHeadersNonRecursive(projectDir.resolve("src/commonMain/cpp")) includeHeadersNonRecursive(projectDir.resolve("src/commonMain/cpp"))
includeHeadersNonRecursive(skiaHeadersDirs(unpackedSkia)) includeHeadersNonRecursive(skiaHeadersDirs(unpackedSkia))
flags.set(listOf( flags.set(listOf(
*skiaPreprocessorFlags() *skiaPreprocessorFlags(),
"-DSKIKO_WASM"
)) ))
} }
fun registerNativeBridgesTask(os: OS, arch: Arch): TaskProvider<CrossCompileTask> { fun registerNativeBridgesTask(os: OS, arch: Arch): TaskProvider<CompileSkikoCppTask> {
return tasks.register<CrossCompileTask>("${os.id}_${arch.id}_CrossCompile") { return tasks.register<CompileSkikoCppTask>("${os.id}_${arch.id}_CrossCompile") {
val osArch = os to arch val osArch = os to arch
val unzipper = skiaDirProviderForCrossTargets[osArch]!! val unzipper = skiaDirProviderForCrossTargets[osArch]!!
dependsOn(unzipper) dependsOn(unzipper)
val unpackedSkia = unzipper.get() val unpackedSkia = unzipper.get()
compiler.set("clang++") compiler.set(compilerForTarget(os, arch))
buildTargetOS.set(osArch.first)
buildTargetArch.set(osArch.second)
buildVariant.set(buildType)
when (os) { when (os) {
OS.IOS -> { OS.IOS -> {
val sdkRoot = "/Applications/Xcode.app/Contents/Developer/Platforms" val sdkRoot = "/Applications/Xcode.app/Contents/Developer/Platforms"
...@@ -195,46 +200,44 @@ fun registerNativeBridgesTask(os: OS, arch: Arch): TaskProvider<CrossCompileTask ...@@ -195,46 +200,44 @@ fun registerNativeBridgesTask(os: OS, arch: Arch): TaskProvider<CrossCompileTask
else -> throw GradleException("$os not yet supported") else -> throw GradleException("$os not yet supported")
} }
crossCompileTargetOS.set(osArch.first) val srcDirs = projectDirs("src/nativeMain/cpp", "src/commonMain/cpp") +
crossCompileTargetArch.set(osArch.second) if (skiko.includeTestHelpers) projectDirs("src/commonTest/cpp") else emptyList()
buildVariant.set(buildType) sourceRoots.set(srcDirs)
sourceFiles =
project.fileTree("src/nativeMain/cpp") { include("**/*.cc") } +
project.fileTree("src/commonMain/cpp") { include("**/*.cc") }
if (skiko.includeTestHelpers) {
sourceFiles += project.fileTree("src/commonTest/cpp") { include("**/*.cc") }
}
outDir.set(project.layout.buildDirectory.dir("out/compile/${buildType.id}-${osArch.first.id}-${osArch.second.id}"))
includeHeadersNonRecursive(projectDir.resolve("src/commonMain/cpp")) includeHeadersNonRecursive(projectDir.resolve("src/commonMain/cpp"))
includeHeadersNonRecursive(skiaHeadersDirs(unpackedSkia)) includeHeadersNonRecursive(skiaHeadersDirs(unpackedSkia))
} }
} }
val linkWasm = tasks.register<LinkWasmTask>("linkWasm") { val linkWasm = tasks.register<LinkSkikoWasmTask>("linkWasm") {
val osArch = OS.Wasm to Arch.Wasm val osArch = OS.Wasm to Arch.Wasm
dependsOn(wasmCrossCompile) dependsOn(compileWasm)
val unzipper = skiaDirProviderForCrossTargets[osArch]!! val unzipper = skiaDirProviderForCrossTargets[osArch]!!
dependsOn(unzipper) dependsOn(unzipper)
val unpackedSkia = unzipper.get() val unpackedSkia = unzipper.get()
linker.set(linkerForTarget(OS.Wasm, Arch.Wasm))
buildTargetOS.set(osArch.first)
buildTargetArch.set(osArch.second)
buildVariant.set(buildType)
libFiles = project.fileTree(unpackedSkia) { include("**/*.a") } libFiles = project.fileTree(unpackedSkia) { include("**/*.a") }
objectFiles = project.fileTree(wasmCrossCompile.map { it.outDir.get() }) { include("**/*.o") } objectFiles = project.fileTree(compileWasm.map { it.outDir.get() }) {
include("**/*.o")
}
wasmFileName.set("skiko.wasm") libOutputFileName.set("skiko.wasm")
jsFileName.set("skiko.js") jsOutputFileName.set("skiko.js")
skikoJsPrefix.set(project.layout.projectDirectory.file("src/jsMain/resources/setup.js")) skikoJsPrefix.set(project.layout.projectDirectory.file("src/jsMain/resources/setup.js"))
outDir.set(project.layout.buildDirectory.dir("out/link/${buildType.id}-${osArch.first.id}-${osArch.second.id}"))
flags.set(listOf( flags.set(listOf(
"-l", "GL", "-l", "GL",
"-s", "USE_WEBGL2=1", "-s", "USE_WEBGL2=1",
"-s", "OFFSCREEN_FRAMEBUFFER=1", "-s", "OFFSCREEN_FRAMEBUFFER=1",
"--bind",
)) ))
doLast { doLast {
...@@ -277,7 +280,6 @@ val Project.supportNative: Boolean ...@@ -277,7 +280,6 @@ val Project.supportNative: Boolean
val Project.supportWasm: Boolean val Project.supportWasm: Boolean
get() = properties.get("skiko.wasm.enabled") == "true" get() = properties.get("skiko.wasm.enabled") == "true"
kotlin { kotlin {
jvm { jvm {
compilations.all { compilations.all {
...@@ -573,55 +575,68 @@ fun skiaStaticLibraries(skiaDir: String, targetString: String): List<String> { ...@@ -573,55 +575,68 @@ fun skiaStaticLibraries(skiaDir: String, targetString: String): List<String> {
} }
} }
// See https://docs.gradle.org/current/userguide/cpp_library_plugin.html. val compileJvmBindings = tasks.register<CompileSkikoCppTask>("compileJvmBindings") {
tasks.withType(CppCompile::class.java).configureEach {
// Prefer 'java.home' system property to simplify overriding from Intellij. // Prefer 'java.home' system property to simplify overriding from Intellij.
// When used from command-line, it is effectively equal to JAVA_HOME. // When used from command-line, it is effectively equal to JAVA_HOME.
if (JavaVersion.current() < JavaVersion.VERSION_11) { if (JavaVersion.current() < JavaVersion.VERSION_11) {
error("JDK 11+ is required, but Gradle JVM is ${JavaVersion.current()}. " + error("JDK 11+ is required, but Gradle JVM is ${JavaVersion.current()}. " +
"Check JAVA_HOME (CLI) or Gradle settings (Intellij).") "Check JAVA_HOME (CLI) or Gradle settings (Intellij).")
} }
val jdkHome = System.getProperty("java.home") ?: error("'java.home' is null") val jdkHome = File(System.getProperty("java.home") ?: error("'java.home' is null"))
dependsOn(skiaDir) dependsOn(skiaDir)
compilerArgs.addAll( buildTargetOS.set(targetOs)
listOf("-I$jdkHome/include") buildTargetArch.set(targetArch)
+ includeHeadersFlags(skiaHeadersDirs(skiaDir.get())) buildVariant.set(buildType)
+ skiaPreprocessorFlags()
val srcDirs = projectDirs(
"src/jvmMain/cpp/common",
"src/jvmMain/cpp/${targetOs.id}",
"src/jvmTest/cpp"
) )
compilerArgs.add("-I$projectDir/src/jvmMain/cpp/include") sourceRoots.set(srcDirs)
includeHeadersNonRecursive(jdkHome.resolve("include"))
includeHeadersNonRecursive(skiaHeadersDirs(skiaDir.get()))
includeHeadersNonRecursive(projectDir.resolve("src/jvmMain/cpp/include"))
compiler.set(compilerForTarget(targetOs, targetArch))
val osFlags: Array<String>
when (targetOs) { when (targetOs) {
OS.MacOS -> { OS.MacOS -> {
compilerArgs.addAll( includeHeadersNonRecursive(jdkHome.resolve("include/darwin"))
listOf( osFlags = arrayOf(
*targetOs.clangFlags,
*buildType.clangFlags,
"-fPIC",
"-stdlib=libc++",
"-fvisibility=hidden", "-fvisibility=hidden",
"-fvisibility-inlines-hidden", "-fvisibility-inlines-hidden",
"-I$jdkHome/include/darwin",
"-DSK_SHAPER_CORETEXT_AVAILABLE", "-DSK_SHAPER_CORETEXT_AVAILABLE",
"-DSK_BUILD_FOR_MAC", "-DSK_BUILD_FOR_MAC",
"-DSK_METAL", "-DSK_METAL",
*targetArch.clangFlags,
*buildType.clangFlags
)
) )
} }
OS.Linux -> { OS.Linux -> {
compilerArgs.addAll( includeHeadersNonRecursive(jdkHome.resolve("include/linux"))
listOf( osFlags = arrayOf(
*buildType.clangFlags,
"-fPIC",
"-fno-rtti", "-fno-rtti",
"-fno-exceptions", "-fno-exceptions",
"-fvisibility=hidden", "-fvisibility=hidden",
"-fvisibility-inlines-hidden", "-fvisibility-inlines-hidden",
"-I$jdkHome/include/linux",
"-DSK_BUILD_FOR_LINUX", "-DSK_BUILD_FOR_LINUX",
"-D_GLIBCXX_USE_CXX11_ABI=0", "-D_GLIBCXX_USE_CXX11_ABI=0",
*buildType.clangFlags
)
) )
} }
OS.Windows -> { OS.Windows -> {
compilerArgs.addAll( compiler.set(windowsSdkPaths.compiler.absolutePath)
listOf( includeHeadersNonRecursive(windowsSdkPaths.includeDirs)
"-I$jdkHome/include/win32", includeHeadersNonRecursive(jdkHome.resolve("include/win32"))
osFlags = arrayOf(
"/nologo",
*buildType.msvcCompilerFlags,
"-DSK_BUILD_FOR_WIN", "-DSK_BUILD_FOR_WIN",
"-D_CRT_SECURE_NO_WARNINGS", "-D_CRT_SECURE_NO_WARNINGS",
"-D_HAS_EXCEPTIONS=0", "-D_HAS_EXCEPTIONS=0",
...@@ -635,12 +650,104 @@ tasks.withType(CppCompile::class.java).configureEach { ...@@ -635,12 +650,104 @@ tasks.withType(CppCompile::class.java).configureEach {
// "-I$skiaDir/third_party/externals/angle2/include", // "-I$skiaDir/third_party/externals/angle2/include",
// "-I$skiaDir/src/gpu", // "-I$skiaDir/src/gpu",
// "-DSK_ANGLE", // "-DSK_ANGLE",
*buildType.msvcFlags
) )
}
OS.Wasm, OS.IOS -> error("Should not reach here")
}
flags.set(
listOf(
*skiaPreprocessorFlags(),
*osFlags
)
)
}
val linkJvmBindings = tasks.register<LinkSkikoTask>("linkJvmBindings") {
val skiaBinDir = skiaDir.get().absolutePath + "/" + skiaBinSubdir
val osFlags: Array<String>
libFiles = fileTree(skiaDir.map { it.resolve(skiaBinSubdir)}) {
include(if (targetOs.isWindows) "*.lib" else "*.a")
}
dependsOn(compileJvmBindings)
objectFiles = fileTree(compileJvmBindings.map { it.outDir.get() }) {
include("**/*.o")
}
val libNamePrefix = if (targetOs.isWindows) "skiko" else "libskiko"
libOutputFileName.set("$libNamePrefix-${targetOs.id}-${targetArch.id}${targetOs.dynamicLibExt}")
buildTargetOS.set(targetOs)
buildTargetArch.set(targetArch)
buildVariant.set(buildType)
linker.set(linkerForTarget(targetOs, targetArch))
when (targetOs) {
OS.MacOS -> {
dependsOn(project.tasks.named("objcCompile"))
objectFiles += fileTree("$buildDir/objc/$target") {
include("**/*.o")
}
osFlags = arrayOf(
*targetOs.clangFlags,
"-shared",
"-dead_strip",
"-lobjc",
"-framework", "AppKit",
"-framework", "CoreFoundation",
"-framework", "CoreGraphics",
"-framework", "CoreServices",
"-framework", "CoreText",
"-framework", "Foundation",
"-framework", "IOKit",
"-framework", "Metal",
"-framework", "OpenGL",
"-framework", "QuartzCore" // for CoreAnimation
)
}
OS.Linux -> {
osFlags = arrayOf(
"-shared",
"-static-libstdc++",
"-static-libgcc",
"-lGL",
"-lfontconfig",
// A fix for https://github.com/JetBrains/compose-jb/issues/413.
// Dynamic position independent linking uses PLT thunks relying on jump targets in GOT (Global Offsets Table).
// GOT entries marked as (for example) R_X86_64_JUMP_SLOT in the relocation table. So, if there's code loading
// platform libstdc++.so, lazy resolve code will resolve GOT entries to platform libstdc++.so on first invocation,
// and so further execution will break, as those two libstdc++ are not compatible.
// To fix it we enforce resolve of all GOT entries at library load time, and make it read-only afterwards.
"-Wl,-z,relro,-z,now",
// Hack to fix problem with linker not always finding certain declarations.
"$skiaBinDir/libsksg.a",
"$skiaBinDir/libskia.a",
"$skiaBinDir/libskunicode.a"
)
}
OS.Windows -> {
linker.set(windowsSdkPaths.linker.absolutePath)
libDirs.set(windowsSdkPaths.libDirs)
osFlags = arrayOf(
*buildType.msvcLinkerFlags,
"/NOLOGO",
"/DLL",
"Advapi32.lib",
"gdi32.lib",
"Dwmapi.lib",
"opengl32.lib",
"shcore.lib",
"user32.lib",
) )
} }
OS.Wasm, OS.IOS -> throw GradleException("Should not reach here") OS.Wasm, OS.IOS -> {
throw GradleException("This task shalln't be used with WASM")
}
} }
flags.set(listOf(*osFlags))
} }
// Very hacky way to compile Objective-C sources and add the // Very hacky way to compile Objective-C sources and add the
...@@ -655,7 +762,6 @@ project.tasks.register<Exec>("objcCompile") { ...@@ -655,7 +762,6 @@ project.tasks.register<Exec>("objcCompile") {
val skiaDir = skiaDir.get().absolutePath val skiaDir = skiaDir.get().absolutePath
commandLine = listOf( commandLine = listOf(
"clang", "clang",
*targetArch.clangFlags,
*targetOs.clangFlags, *targetOs.clangFlags,
"-I$jdkHome/include", "-I$jdkHome/include",
"-I$jdkHome/include/darwin", "-I$jdkHome/include/darwin",
...@@ -672,12 +778,6 @@ project.tasks.register<Exec>("objcCompile") { ...@@ -672,12 +778,6 @@ project.tasks.register<Exec>("objcCompile") {
outputs.files(outs) outputs.files(outs)
} }
fun List<String>.findAllFiles(suffix: String): List<String> = this
.map { File(it) }
.flatMap { it.walk().toList() }
.map { it.absolutePath }
.filter { it.endsWith(suffix) }
val generateVersion = project.tasks.register("generateVersion") { val generateVersion = project.tasks.register("generateVersion") {
val outDir = generatedKotlin val outDir = generatedKotlin
file(outDir).mkdirs() file(outDir).mkdirs()
...@@ -789,146 +889,47 @@ fun remoteSignCodesign(signHost: String, lib: File, out: File) { ...@@ -789,146 +889,47 @@ fun remoteSignCodesign(signHost: String, lib: File, out: File) {
} }
} }
tasks.withType(LinkSharedLibrary::class.java).configureEach {
when (targetOs) {
OS.MacOS -> {
dependsOn(project.tasks.named("objcCompile"))
linkerArgs.addAll(
listOf(
*targetArch.clangFlags,
"-dead_strip",
"-lobjc",
"-framework", "AppKit",
"-framework", "CoreFoundation",
"-framework", "CoreGraphics",
"-framework", "CoreServices",
"-framework", "CoreText",
"-framework", "Foundation",
"-framework", "IOKit",
"-framework", "Metal",
"-framework", "OpenGL",
"-framework", "QuartzCore" // for CoreAnimation
)
)
}
OS.Linux -> {
val skia = skiaDir.get().absolutePath + "/" + skiaBinSubdir
linkerArgs.addAll(
listOf(
"-static-libstdc++",
"-static-libgcc",
"-lGL",
"-lfontconfig",
// A fix for https://github.com/JetBrains/compose-jb/issues/413.
// Dynamic position independent linking uses PLT thunks relying on jump targets in GOT (Global Offsets Table).
// GOT entries marked as (for example) R_X86_64_JUMP_SLOT in the relocation table. So, if there's code loading
// platform libstdc++.so, lazy resolve code will resolve GOT entries to platform libstdc++.so on first invocation,
// and so further execution will break, as those two libstdc++ are not compatible.
// To fix it we enforce resolve of all GOT entries at library load time, and make it read-only afterwards.
"-Wl,-z,relro,-z,now",
// Hack to fix problem with linker not always finding certain declarations.
"$skia/libsksg.a",
"$skia/libskia.a",
"$skia/libskunicode.a"
)
)
}
OS.Windows -> {
linkerArgs.addAll(
listOf(
"Advapi32.lib",
"gdi32.lib",
"Dwmapi.lib",
"opengl32.lib",
"shcore.lib",
"user32.lib"
)
)
}
OS.Wasm, OS.IOS -> {
throw GradleException("This task shalln't be used with $targetOs")
}
}
}
extensions.configure<CppLibrary> {
val paths = mutableListOf(
fileTree("$projectDir/src/jvmMain/cpp/common"),
fileTree("$projectDir/src/jvmMain/cpp/${targetOs.id}")
).apply {
if (skiko.includeTestHelpers) {
add(fileTree("$projectDir/src/jvmTest/cpp/TestHelpers.cc"))
}
}
source.setFrom(paths)
}
library {
linkage.addAll(listOf(Linkage.SHARED))
targetMachines.addAll(listOf(machines.macOS.x86_64, machines.linux.x86_64, machines.windows.x86_64))
baseName.set("skiko-$target")
dependencies {
implementation(
skiaDir.map {
fileTree(it.resolve(skiaBinSubdir))
.matching { include(if (targetOs.isWindows) "**.lib" else "**.a") }
}
)
implementation(fileTree("$buildDir/objc/$target").matching {
include("**.o")
})
}
toolChains {
withType(VisualCpp::class.java) {
// In some cases Gradle is unable to find VC++ toolchain
// https://github.com/gradle/gradle-native/issues/617
skiko.visualStudioBuildToolsDir?.let {
setInstallDir(it)
}
}
}
}
val skikoJvmJar: Provider<Jar> by tasks.registering(Jar::class) { val skikoJvmJar: Provider<Jar> by tasks.registering(Jar::class) {
archiveBaseName.set("skiko-jvm") archiveBaseName.set("skiko-jvm")
from(kotlin.jvm().compilations["main"].output.allOutputs) from(kotlin.jvm().compilations["main"].output.allOutputs)
} }
val skikoNativeLib: File
get() {
val linkTask = project.tasks.withType(LinkSharedLibrary::class.java).single { it.name.contains(buildType.id) }
val lib =
linkTask.outputs.files.single { it.name.endsWith(".dll") || it.name.endsWith(".dylib") || it.name.endsWith(".so") }
return lib
}
val maybeSign by project.tasks.registering { val maybeSign by project.tasks.registering {
val linkTask = project.tasks.withType(LinkSharedLibrary::class.java).single { it.name.contains(buildType.id) } dependsOn(linkJvmBindings)
dependsOn(linkTask)
val lib = linkTask.outputs.files.single { it.name.endsWith(".dll") || it.name.endsWith(".dylib") || it.name.endsWith(".so") } val lib = linkJvmBindings.map { task ->
task.outDir.get().asFile.walk().single { file -> file.name.endsWith(targetOs.dynamicLibExt) }
}
inputs.files(lib) inputs.files(lib)
val output = file(lib.absolutePath + ".maybesigned")
val outputDir = project.layout.buildDirectory.dir("maybe-signed")
val output = outputDir.map { it.asFile.resolve(lib.get().name + ".maybesigned") }
outputs.files(output) outputs.files(output)
doLast { doLast {
outputDir.get().asFile.apply {
deleteRecursively()
mkdirs()
}
val libFile = lib.get()
val outputFile = output.get()
if (targetOs == OS.Linux) { if (targetOs == OS.Linux) {
// Linux requires additional sealing to run on wider set of platforms. // Linux requires additional sealing to run on wider set of platforms.
val sealer = "$projectDir/tools/sealer-${hostArch.id}" val sealer = "$projectDir/tools/sealer-${hostArch.id}"
sealBinary(sealer, lib) sealBinary(sealer, libFile)
} }
if (skiko.signHost != null) { if (skiko.signHost != null) {
remoteSignCodesign(skiko.signHost!!, lib, output) remoteSignCodesign(skiko.signHost!!, libFile, outputFile)
} else { } else {
lib.copyTo(output, overwrite = true) libFile.copyTo(outputFile, overwrite = true)
} }
} }
} }
val createChecksums by project.tasks.registering(org.gradle.crypto.checksum.Checksum::class) { val createChecksums by project.tasks.registering(org.gradle.crypto.checksum.Checksum::class) {
dependsOn(maybeSign) dependsOn(maybeSign)
files = maybeSign.get().outputs.files + files = project.files(maybeSign.map { it.outputs.files }) +
if (targetOs.isWindows) files(skiaDir.map { it.resolve("${skiaBinSubdir}/icudtl.dat") }) else files() if (targetOs.isWindows) files(skiaDir.map { it.resolve("${skiaBinSubdir}/icudtl.dat") }) else files()
algorithm = Checksum.Algorithm.SHA256 algorithm = Checksum.Algorithm.SHA256
outputDir = file("$buildDir/checksums") outputDir = file("$buildDir/checksums")
...@@ -938,7 +939,7 @@ val skikoJvmRuntimeJar by project.tasks.registering(Jar::class) { ...@@ -938,7 +939,7 @@ val skikoJvmRuntimeJar by project.tasks.registering(Jar::class) {
dependsOn(createChecksums) dependsOn(createChecksums)
archiveBaseName.set("skiko-$target") archiveBaseName.set("skiko-$target")
from(skikoJvmJar.map { zipTree(it.archiveFile) }) from(skikoJvmJar.map { zipTree(it.archiveFile) })
from(maybeSign.get().outputs.files) from(maybeSign.map { it.outputs.files })
rename { rename {
// Not just suffix, as could be in middle of SHA256. // Not just suffix, as could be in middle of SHA256.
it.replace(".maybesigned", "") it.replace(".maybesigned", "")
...@@ -946,7 +947,7 @@ val skikoJvmRuntimeJar by project.tasks.registering(Jar::class) { ...@@ -946,7 +947,7 @@ val skikoJvmRuntimeJar by project.tasks.registering(Jar::class) {
if (targetOs.isWindows) { if (targetOs.isWindows) {
from(files(skiaDir.map { it.resolve("${skiaBinSubdir}/icudtl.dat") })) from(files(skiaDir.map { it.resolve("${skiaBinSubdir}/icudtl.dat") }))
} }
from(createChecksums.get().outputs.files) from(createChecksums.map { it.outputs.files })
} }
val skikoWasmJar by project.tasks.registering(Jar::class) { val skikoWasmJar by project.tasks.registering(Jar::class) {
...@@ -965,15 +966,6 @@ val skikoWasmJar by project.tasks.registering(Jar::class) { ...@@ -965,15 +966,6 @@ val skikoWasmJar by project.tasks.registering(Jar::class) {
} }
} }
// disable unexpected native publications (default C++ publications are failing)
tasks.withType<AbstractPublishToMaven>().configureEach {
doFirst {
if (!publication.isSkikoPublication()) {
throw StopExecutionException("Publication '${publication.name}' is disabled")
}
}
}
val skikoRuntimeDirForTests by project.tasks.registering(Copy::class) { val skikoRuntimeDirForTests by project.tasks.registering(Copy::class) {
dependsOn(skikoJvmRuntimeJar) dependsOn(skikoJvmRuntimeJar)
from(zipTree(skikoJvmRuntimeJar.flatMap { it.archiveFile })) { from(zipTree(skikoJvmRuntimeJar.flatMap { it.archiveFile })) {
...@@ -1005,17 +997,6 @@ tasks.withType<Test>().configureEach { ...@@ -1005,17 +997,6 @@ tasks.withType<Test>().configureEach {
} }
} }
fun Publication.isSkikoPublication(): Boolean {
val component = (this as? org.gradle.api.publish.maven.internal.publication.DefaultMavenPublication)?.component
// Don't publish libraries included into jvm-runtime-*.
// Or whatever `cpp-library` considers their public api.
if (component is CppBinary ||
component?.javaClass?.simpleName == "MainLibraryVariant") return false
return true
}
fun Task.disable() { fun Task.disable() {
enabled = false enabled = false
group = "Disabled tasks" group = "Disabled tasks"
...@@ -1036,20 +1017,6 @@ afterEvaluate { ...@@ -1036,20 +1017,6 @@ afterEvaluate {
} }
} }
// Gradle metadata for default C++ publications fails.
tasks.withType(GenerateModuleMetadata::class).configureEach {
if (!this.publication.get().isSkikoPublication()) {
disable()
}
}
// Publishing for default C++ publications fails.
tasks.withType<AbstractPublishToMaven>().configureEach {
if (!publication.isSkikoPublication()) {
disable()
}
}
tasks.named("clean").configure { tasks.named("clean").configure {
doLast { doLast {
delete(skiko.dependenciesDir) delete(skiko.dependenciesDir)
...@@ -1142,3 +1109,8 @@ if (hostOs == OS.Linux && hostArch != Arch.X64) { ...@@ -1142,3 +1109,8 @@ if (hostOs == OS.Linux && hostArch != Arch.X64) {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().download = false rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().download = false
} }
} }
fun Task.projectDirs(vararg relativePaths: String): List<Directory> {
val projectDir = project.layout.projectDirectory
return relativePaths.map { path -> projectDir.dir(path) }
}
\ No newline at end of file
import internal.utils.*
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.internal.file.FileOperations
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.process.ExecOperations
import org.gradle.work.InputChanges
import org.gradle.workers.WorkerExecutor
import java.io.File
import javax.inject.Inject
abstract class AbstractSkikoNativeToolTask : DefaultTask() {
@get:Inject
abstract val execOperations: ExecOperations
@get:Inject
abstract val fileOperations: FileOperations
@get:Inject
abstract val workerExecutor: WorkerExecutor
@get:Input
abstract val buildTargetOS: Property<OS>
@get:Input
abstract val buildTargetArch: Property<Arch>
@get:Input
abstract val buildVariant: Property<SkiaBuildType>
@get:Internal
protected abstract val outDirNameForTool: String
internal open fun configureArgs(): ArgBuilder = createArgBuilder()
internal open fun createArgBuilder(): ArgBuilder =
DefaultArgBuilder()
@get:OutputDirectory
val outDir: DirectoryProperty =
project.objects.directoryProperty().apply {
set(
project.layout.buildDirectory.map {
val suffix = "${buildVariant.get().id}-${buildTargetOS.get().id}-${buildTargetArch.get().id}"
it.dir("out/$outDirNameForTool/$suffix")
}
)
}
@get:LocalState
internal val taskStateDir: DirectoryProperty =
project.objects.directoryProperty().apply {
set(project.layout.buildDirectory.dir("tmp/$name"))
}
@TaskAction
fun run(inputChanges: InputChanges) {
beforeRun()
val mode = determineToolMode(inputChanges)
when (mode) {
is ToolMode.NonIncremental -> cleanStaleOutput(mode)
is ToolMode.Incremental -> cleanStaleOutput(mode)
}
val args = configureArgs()
execute(mode, args)
afterRun()
}
internal open fun determineToolMode(
inputChanges: InputChanges
): ToolMode {
return ToolMode.NonIncremental("$this is not incremental")
}
protected abstract fun execute(
mode: ToolMode,
args: ArgBuilder
)
protected open fun cleanStaleOutput(mode: ToolMode.Incremental) {
error("Incremental execution mode is not implemented by ${this.javaClass.canonicalName}")
}
protected open fun cleanStaleOutput(mode: ToolMode.NonIncremental) {
cleanDirs(outDir, taskStateDir)
}
protected fun cleanDirs(vararg dirs: Any) {
for (dir in dirs) {
fileOperations.delete(dir)
fileOperations.mkdir(dir)
}
}
protected open fun beforeRun() {}
protected open fun afterRun() {}
protected fun logArgs(prefix: String, args: ArgBuilder, argFile: File) {
if (logger.isInfoEnabled) {
val argsString = args.toArray().joinToString(", ", prefix = "[", postfix = "]")
logger.info("$prefix: $argsString")
} else logger.warn("$prefix: '$argFile'")
}
}
import internal.utils.*
import org.gradle.api.file.Directory
import org.gradle.api.file.FileCollection
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.work.ChangeType
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import org.gradle.workers.WorkerExecutionException
import java.io.File
import java.util.*
import java.util.concurrent.Callable
import kotlin.collections.HashSet
abstract class CompileSkikoCppTask : AbstractSkikoNativeToolTask() {
@get:Input
abstract val flags: ListProperty<String>
@get:Internal
abstract val sourceRoots: ListProperty<Directory>
@get:Input
abstract val compiler: Property<String>
override val outDirNameForTool: String
get() = "compile"
@get:InputFiles
@get:Incremental
val sourceFiles: FileCollection =
project.files(Callable {
val sources = project.objects.fileCollection()
for (sourceRoot in sourceRoots.get()) {
sources.from(project.fileTree(sourceRoot) { include("**/*.cc") })
}
sources
})
/**
* Used only for up-to-date checks of headers' content
*
* @see [headersDirs]
*/
@Suppress("UNUSED")
@get:InputFiles
@get:Incremental
val headerFiles: FileCollection =
project.files(Callable {
val headers = project.objects.fileCollection()
for (dir in headersDirs) {
headers.from(project.fileTree(dir) {
// headers from include dirs should be included non-recursively
include("*.h")
include("*.hh")
})
}
for (sourceRoot in sourceRoots.get()) {
headers.from(project.fileTree(sourceRoot) {
// headers from source roots should be included recursively
include("**/*.h")
include("**/*.hh")
})
}
headers
})
@get:Internal
internal val headersDirs = LinkedHashSet<File>()
fun includeHeadersNonRecursive(dirs: Collection<File>) {
headersDirs.addAll(dirs)
}
fun includeHeadersNonRecursive(dir: File) {
headersDirs.add(dir)
}
private val sourceToOutputMapping = SourceToOutputMapping()
private val sourceToOutputMappingFile: File
get() = taskStateDir.get().asFile.resolve("source-to-output.txt")
override fun beforeRun() {
if (sourceToOutputMappingFile.exists()) {
sourceToOutputMapping.load(sourceToOutputMappingFile)
}
}
override fun afterRun() {
sourceToOutputMapping.save(sourceToOutputMappingFile)
}
private val compilerArgsRootDir = taskStateDir.map { it.dir("args") }
override fun createArgBuilder(): ArgBuilder =
if (buildTargetOS.get().isWindows) VisualCppCompilerArgBuilder()
else super.createArgBuilder()
override fun execute(mode: ToolMode, args: ArgBuilder) {
val sourcesToCompile: Collection<File> = when (mode) {
is ToolMode.Incremental -> mode.newOrModifiedFiles()
is ToolMode.NonIncremental -> sourceFiles.files.also {
logger.warn("Performing non-incremental compilation: ${mode.reason}")
}
}
updateSourcesToOutputsMapping(sourcesToCompile)
val outDir = outDir.get().asFile
val compilerExecutablePath = findCompilerExecutable().absolutePath
val workQueue = workerExecutor.noIsolation()
val submittedWorks = HashSet<String>()
val sourceOutputPairs = sourcesToCompile.map { sourceFile ->
// check all output files and their parent dirs before compiling anything
val outputFile = sourceToOutputMapping[sourceFile]
?: error("Could not find output file for source file: $sourceFile")
outputFile.parentFile.mkdirs()
check(!outputFile.exists()) {
"Output file should not exist before compilation: '$outputFile'"
}
sourceFile to outputFile
}
val argFilesDir = compilerArgsRootDir.get().asFile
cleanDirs(argFilesDir)
val commonArgsFile = argFilesDir.parentFile.resolve("common-args.txt")
args.createArgFile(commonArgsFile)
logArgs("Compiler args", args, commonArgsFile)
for ((sourceFile, outputFile) in sourceOutputPairs) {
val workId = "Compiling '${sourceFile.absolutePath}'"
submittedWorks.add(workId)
val workArgs = args.copy {
arg("-o", outputFile)
arg(value = sourceFile)
}
val argFile = run {
val relative = outputFile.parentFile.relativeTo(outDir).path
val argFileDir = argFilesDir.resolve(relative)
argFileDir.resolve("${outputFile.nameWithoutExtension}-args.txt")
}
workQueue.submit(RunExternalProcessWork::class.java) {
this.workId = workId
executable = compilerExecutablePath
workingDir = outDir
this.args = listOf(workArgs.createArgFile(argFile))
}
}
try {
workQueue.await()
} catch (e: WorkerExecutionException) {
for (work in submittedWorks) {
val result = RunExternalProcessWork.workResults[work]
if (result == null) {
logger.error("Error: no results for work '$work'")
} else if (result.failure != null) {
logger.warn("\n$work:")
result.log.flushTo(logger)
}
}
error("Some files were not compiled. Check the log for more details")
} finally {
for (work in submittedWorks) {
RunExternalProcessWork.workResults.remove(work)
}
}
}
private fun findCompilerExecutable(): File {
val compilerNameOrFile = compiler.get()
val compilerFile = File(compilerNameOrFile)
if (compilerFile.isFile) return compilerFile
val paths = System.getenv("PATH").split(File.pathSeparator)
for (path in paths) {
val file = File(path).resolve(compilerNameOrFile)
if (file.isFile) return file
}
error("Could not find compiler '$compilerNameOrFile' in PATH")
}
override fun cleanStaleOutput(mode: ToolMode.NonIncremental) {
super.cleanStaleOutput(mode)
// file is deleted by the base class
check(!sourceToOutputMappingFile.exists())
sourceToOutputMapping.clear()
}
override fun cleanStaleOutput(mode: ToolMode.Incremental) {
for (sourceFile in mode.outdatedFiles()) {
val outdatedOutputFile = sourceToOutputMapping.remove(sourceFile)
check(outdatedOutputFile != null) {
"Could not find output file for source file: $sourceFile"
}
check(outdatedOutputFile.exists()) {
"Expected outdated output file does not exist: $outdatedOutputFile"
}
outdatedOutputFile.delete()
}
}
private fun updateSourcesToOutputsMapping(sourcesToCompile: Collection<File>) {
val mappingsForNewOrModifiedFiles = mapSourceFilesToOutputFiles(
sourceRoots = sourceRoots.get().map { it.asFile },
sourceFiles = sourcesToCompile,
outDir = outDir.get().asFile,
sourceFileExt = ".cc",
outputFileExt = ".o"
)
sourceToOutputMapping.putAll(mappingsForNewOrModifiedFiles)
}
override fun determineToolMode(inputChanges: InputChanges): ToolMode {
if (!sourceToOutputMappingFile.exists()) {
return ToolMode.NonIncremental("first build or clean build")
}
if (!inputChanges.isIncremental) {
return ToolMode.NonIncremental("inputs' changes are not incremental")
}
if (inputChanges.getFileChanges(headerFiles).any()) {
return ToolMode.NonIncremental("header files are modified or removed")
}
val removedFiles = arrayListOf<File>()
val newFiles = arrayListOf<File>()
val modifiedFiles = arrayListOf<File>()
val sourceFilesChanges = inputChanges.getFileChanges(sourceFiles)
for (change in sourceFilesChanges) {
when (change.changeType) {
ChangeType.ADDED -> newFiles.add(change.file)
ChangeType.MODIFIED -> modifiedFiles.add(change.file)
ChangeType.REMOVED -> removedFiles.add(change.file)
}
}
return ToolMode.Incremental(
removedFiles = removedFiles,
newFiles = newFiles,
modifiedFiles = modifiedFiles
)
}
override fun configureArgs() =
super.configureArgs().apply {
arg("-c")
repeatedArg("-I", headersDirs)
// todo: ensure that flags do not start with '-I' (all headers should be added via [headersDirs])
rawArgs(flags.get())
}
}
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.process.ExecOperations
import org.gradle.work.ChangeType
import org.gradle.work.FileChange
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import java.io.File
import java.util.LinkedHashSet
import java.util.concurrent.Callable
import javax.inject.Inject
abstract class CrossCompileTask : DefaultTask() {
@get:Inject
abstract val execOperations: ExecOperations
@get:Input
@get:Optional
abstract val crossCompileTargetOS: Property<OS>
@get:Input
abstract val crossCompileTargetArch: Property<Arch>
@get:Input
abstract val buildVariant: Property<SkiaBuildType>
@get:Input
abstract val flags: ListProperty<String>
@get:InputFiles
@get:Incremental
lateinit var sourceFiles: FileCollection
@get:OutputDirectory
abstract val outDir: DirectoryProperty
@get:Input
abstract val compiler: Property<String>
/**
* Used only for up-to-date checks of headers' content
*
* @see [headersDirs]
*/
@Suppress("UNUSED")
@get:InputFiles
@get:Incremental
val headerFiles: FileCollection =
project.files(Callable {
fun File.isHeaderFile(): Boolean =
isFile && name.endsWith(".h", ignoreCase = true)
val headers = hashSetOf<File>()
for (dir in headersDirs) {
val canonicalDir = dir.canonicalFile
dir.listFiles()?.forEach { file ->
if (file.isHeaderFile()) {
headers.add(canonicalDir.resolve(file.name))
}
}
}
headers
})
@get:Internal
internal val headersDirs = LinkedHashSet<File>()
fun includeHeadersNonRecursive(dirs: Collection<File>) {
headersDirs.addAll(dirs)
}
fun includeHeadersNonRecursive(dir: File) {
headersDirs.add(dir)
}
@TaskAction
open fun run(inputChanges: InputChanges) {
execOperations.exec {
val sourcesToCompile = determineSourcesToCompile(inputChanges)
executable = compiler.get()
args = arrayListOf<String>().also { args ->
configureCompilerArgs(args)
args.addAll(sourcesToCompile.map { it.absolutePath })
}
workingDir = outDir.get().asFile
// todo: log args to file system
}
}
private sealed class Mode {
class Incremental(val changes: Iterable<FileChange>) : Mode()
class NonIncremental(val reason: String) : Mode()
}
private fun determineSourcesToCompile(inputChanges: InputChanges): Collection<File> {
val compilationMode = analyzeIncrementalChanges(inputChanges)
val outDir = outDir.get().asFile
return when (compilationMode) {
is Mode.Incremental -> {
val sourcesToCompile = arrayListOf<File>()
for (change in compilationMode.changes) {
val outdatedOutputFile = outDir.resolve(change.file.nameWithoutExtension + ".o")
if (outdatedOutputFile.exists()) {
// `outdatedOutputFile` might not exist,
// when `change.file` is a new file, which was not compiled
// todo: log in verbose mode
outdatedOutputFile.delete()
}
if (change.changeType != ChangeType.REMOVED) {
sourcesToCompile.add(change.file)
}
}
logger.warn("Compiling ${sourcesToCompile.size} files incrementally")
sourcesToCompile
}
is Mode.NonIncremental -> {
outDir.deleteRecursively()
outDir.mkdirs()
logger.warn("Recompiling all files: ${compilationMode.reason}")
sourceFiles.files
}
}
}
private fun analyzeIncrementalChanges(inputChanges: InputChanges): Mode {
if (!inputChanges.isIncremental) {
return Mode.NonIncremental("input changes are not incremental")
}
if (inputChanges.getFileChanges(headerFiles).any()) {
return Mode.NonIncremental("header files are modified or removed")
}
return Mode.Incremental(inputChanges.getFileChanges(sourceFiles))
}
open fun configureCompilerArgs(args: MutableList<String>) {
args.add("-c")
args.addAll(headersDirs.map { "-I${it.absolutePath}" })
// todo: ensure that flags do not start with '-I' (all headers should be added via [headersDirs])
args.addAll(flags.get())
args.addAll(crossCompileTargetOS.orNull?.clangFlags ?: arrayOf())
args.addAll(crossCompileTargetArch.get().clangFlags)
val bt = buildVariant.get()
args.addAll(bt.flags)
args.addAll(bt.clangFlags)
}
}
\ No newline at end of file
import internal.utils.*
import org.gradle.api.file.FileCollection
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import java.io.File
abstract class LinkSkikoTask : AbstractSkikoNativeToolTask() {
@get:InputFiles
lateinit var libFiles: FileCollection
@get:Input
abstract val libDirs: ListProperty<File>
@get:InputFiles
lateinit var objectFiles: FileCollection
@get:Input
abstract val libOutputFileName: Property<String>
@get:Input
abstract val flags: ListProperty<String>
@get:Input
abstract val linker: Property<String>
override val outDirNameForTool: String
get() = "link"
private val argsFile = taskStateDir.file("args.txt")
override fun createArgBuilder(): ArgBuilder =
if (buildTargetOS.get().isWindows) VisualCppLinkerArgBuilder()
else super.createArgBuilder()
override fun execute(mode: ToolMode, args: ArgBuilder) {
check(mode is ToolMode.NonIncremental) {
"Linking is not incremental, but $mode is received"
}
val argFile = argsFile.get().asFile
val argFileArg = args.createArgFile(argFile)
logArgs("Linker args", args, argFile)
execOperations.exec {
executable = linker.get()
workingDir = outDir.get().asFile
this.args = listOf(argFileArg)
}
}
override fun configureArgs() =
super.configureArgs().apply {
arg("-o", outDir.resolveToIoFile(libOutputFileName))
repeatedArg("-L", values = libDirs.get())
repeatedArg(values = objectFiles.files)
repeatedArg(values = libFiles.files)
rawArgs(flags.get())
}
}
\ No newline at end of file
import internal.utils.*
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
abstract class LinkSkikoWasmTask : LinkSkikoTask() {
@get:InputFile
@get:Optional
abstract val skikoJsPrefix: RegularFileProperty
@get:Input
abstract val jsOutputFileName: Property<String>
override fun configureArgs() =
super.configureArgs().apply {
arg("-o", outDir.resolveToIoFile(jsOutputFileName))
arg("--extern-post-js", skikoJsPrefix)
}
}
\ No newline at end of file
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.process.ExecOperations
import javax.inject.Inject
abstract class LinkWasmTask : DefaultTask() {
@get:Inject
abstract val execOperations: ExecOperations
@get:InputFiles
lateinit var libFiles: FileCollection
@get:InputFiles
lateinit var objectFiles: FileCollection
@get:Input
abstract val wasmFileName: Property<String>
@get:Input
abstract val jsFileName: Property<String>
@get:InputFile
@get:Optional
abstract val skikoJsPrefix: RegularFileProperty
@get:OutputDirectory
abstract val outDir: DirectoryProperty
@get:Input
abstract val flags: ListProperty<String>
@TaskAction
fun run() {
outDir.get().asFile.apply {
deleteRecursively()
mkdirs()
}
execOperations.exec {
executable = "emcc"
args = arrayListOf<String>().also { configureCompilerArgs(it) }
workingDir = outDir.get().asFile
// todo: log args to file system
}
}
fun configureCompilerArgs(args: MutableList<String>) {
args.addAll(flags.get())
args.addAll(objectFiles.files.map { it.absolutePath })
args.addAll(libFiles.files.map { it.absolutePath })
args.add("--extern-post-js")
args.add(skikoJsPrefix.get().asFile.absolutePath)
args.add("-o")
args.add(outDir.resolveToAbsolutePath(wasmFileName))
args.add("-o")
args.add(outDir.resolveToAbsolutePath(jsFileName))
}
}
\ No newline at end of file
import java.io.File
sealed class ToolMode {
class Incremental(
val removedFiles: Iterable<File>,
val modifiedFiles: Iterable<File>,
val newFiles: Iterable<File>
) : ToolMode() {
fun newOrModifiedFiles() = modifiedFiles + newFiles
fun outdatedFiles() = modifiedFiles + removedFiles
}
class NonIncremental(val reason: String) : ToolMode()
}
\ No newline at end of file
val OS.dynamicLibExt: String
get() = when (this) {
OS.Linux -> ".so"
OS.Windows -> ".dll"
OS.MacOS, OS.IOS -> ".dylib"
OS.Wasm -> ".wasm"
}
package internal.utils
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.provider.Provider
import java.io.File
interface ArgBuilder {
/**
* Writes all args to [file]
*
* @return an argument to a compiler/linker, pointing to the arg file (aka response file)
*/
fun createArgFile(file: File): String
fun copy(fn: ArgBuilder.() -> Unit): ArgBuilder
fun arg(argName: String? = null, value: Any? = null)
fun repeatedArg(argName: String? = null, values: Collection<Any>) {
values.forEach { value -> arg(argName, value) }
}
fun rawArg(arg: String)
fun rawArgs(args: Collection<String>) {
args.forEach { rawArg(it) }
}
fun toArray(): Array<String>
}
internal abstract class AbstractArgBuilder : ArgBuilder {
protected val args = arrayListOf<String>()
protected abstract fun newSelfInstance(): ArgBuilder
override fun copy(fn: ArgBuilder.() -> Unit): ArgBuilder {
val newArgs = newSelfInstance()
newArgs.rawArgs(args)
newArgs.fn()
return newArgs
}
override fun createArgFile(file: File): String {
file.writeLines(args)
return "@${escapePathIfNeeded(file)}"
}
override fun arg(argName: String?, value: Any?) {
addTransformedArgs(
argName = transformName(argName),
value = transformValue(value)
)
}
override fun rawArg(arg: String) {
args.add(arg)
}
protected open fun addTransformedArgs(argName: String?, value: String?) {
argName?.let { args.add(it) }
value?.let { args.add(it) }
}
protected open fun transformName(argName: String?): String? =
argName
private fun transformValue(value: Any?): String? =
when (value) {
is Provider<*> -> transformValue(value.get())
is FileSystemLocation -> transformValue(value.asFile)
is File -> escapePathIfNeeded(value)
is Any -> value.toString()
else -> null
}
protected open fun escapePathIfNeeded(file: File): String =
file.absolutePath
override fun toArray(): Array<String> =
args.toTypedArray()
}
internal abstract class BaseVisualStudioBuildToolsArgBuilder : AbstractArgBuilder() {
override fun escapePathIfNeeded(file: File): String {
val path = file.absolutePath
.replace("/", "\\")
.replace("\\", "\\\\")
return if (" " in path) "\"$path\"" else path
}
}
internal class DefaultArgBuilder() : AbstractArgBuilder() {
override fun newSelfInstance(): ArgBuilder = DefaultArgBuilder()
}
internal class VisualCppCompilerArgBuilder : BaseVisualStudioBuildToolsArgBuilder() {
private val objectOutputArg = "/Fo"
private val includeDirArg = "/I"
private val argsToJoinWithValues = listOf(objectOutputArg, includeDirArg)
override fun newSelfInstance(): ArgBuilder = VisualCppCompilerArgBuilder()
override fun transformName(argName: String?): String? =
when (argName) {
"-o", "--output" -> objectOutputArg
"-c", "--compile" -> "/c"
"-I", "--include-directory" -> includeDirArg
else -> super.transformName(argName)
}
override fun addTransformedArgs(argName: String?, value: String?) {
if (argName in argsToJoinWithValues) {
args.add("$argName$value")
} else {
super.addTransformedArgs(argName, value)
}
}
}
internal class VisualCppLinkerArgBuilder : BaseVisualStudioBuildToolsArgBuilder() {
private val outArg = "/OUT"
private val libPathArg = "/LIBPATH"
private val argsToJoinWithValues = listOf(outArg, libPathArg)
override fun newSelfInstance(): ArgBuilder = VisualCppLinkerArgBuilder()
override fun transformName(argName: String?): String? =
when (argName) {
"-o", "--output" -> outArg
"-L", "--library-directory" -> libPathArg
else -> super.transformName(argName)
}
override fun addTransformedArgs(argName: String?, value: String?) {
if (argName in argsToJoinWithValues) {
args.add("$argName:$value")
} else {
super.addTransformedArgs(argName, value)
}
}
}
\ No newline at end of file
package internal.utils
import org.gradle.api.logging.Logger
import org.gradle.process.ExecOperations
import org.gradle.workers.WorkAction
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
internal abstract class RunExternalProcessWork: WorkAction<RunExternalProcessWorkParameters> {
@get:Inject
abstract val execOperations: ExecOperations
override fun execute() {
val log = BufferedLog()
var failure: Exception? = null
try {
LineBufferingOutputStream { log.log(it) }.use { out ->
LineBufferingOutputStream { log.error(it) }.use { err ->
execOperations.exec {
executable = parameters.executable
args = parameters.args
workingDir = parameters.workingDir
errorOutput = err
standardOutput = out
}.assertNormalExitValue()
}
}
} catch (e: Exception) {
failure = e
}
workResults[parameters.workId] = WorkResult(log = log, failure = failure)
if (failure != null)
throw failure
}
companion object {
val workResults = ConcurrentHashMap<String, WorkResult>()
}
}
internal class WorkResult(val log: BufferedLog, val failure: Exception?)
internal class BufferedLog() {
private val logLines = arrayListOf<LogLine>()
private data class LogLine(val line: String, val isError: Boolean)
@Synchronized
fun log(message: String) {
logLines.add(LogLine(message, isError = false))
}
@Synchronized
fun error(message: String) {
logLines.add(LogLine(message, isError = false))
}
@Synchronized
fun flushTo(logger: Logger) {
for ((line, isError) in logLines) {
val lineWithIndent = " > $line"
if (isError) logger.error(lineWithIndent) else logger.warn(lineWithIndent)
}
}
}
internal class LineBufferingOutputStream(private val processLine: (String) -> Unit) : OutputStream() {
private val newLine = '\n'.toInt()
private val buffer = ByteArrayOutputStream()
private var closed = false
override fun write(b: Int) {
if (b == newLine) {
flushBuffer()
} else {
buffer.write(b)
}
}
private fun flushBuffer() {
if (buffer.size() > 0) {
val s = buffer.toString(Charsets.UTF_8)
buffer.reset()
processLine(s)
}
}
override fun close() {
flushBuffer()
closed = true
}
override fun flush() {
flushBuffer()
}
}
\ No newline at end of file
package internal.utils
import org.gradle.workers.WorkParameters
import java.io.File
internal interface RunExternalProcessWorkParameters : WorkParameters {
var workId: String
var executable: String
var workingDir: File
var args: List<String>
}
\ No newline at end of file
package internal.utils
import java.io.File
import java.util.*
/**
* Maps [sourceFiles] to output files, using relative paths.
* `<SOURCE_ROOT>/relative/path.[sourceFileExt]` is mapped to `[outDir]/relative/path.[outputFileExt]`
*
* The algorithm works in O(N * M), where:
* * N is a number of source roots;
* * M is a number of unique subdirectories, containing source files;
* usually both numbers are relatively small, so we should be OK
*
* @throws IllegalStateException when a source file does not have a matching source root
*/
internal fun mapSourceFilesToOutputFiles(
sourceRoots: Collection<File>,
sourceFiles: Collection<File>,
outDir: File,
sourceFileExt: String,
outputFileExt: String
): SourceToOutputMapping {
val sourceRoots = sourceRoots.map { it.absoluteFile }
val sourceRootCache = HashMap<File, File>()
fun findSourceRootFor(sourceFile: File): File? {
val parentDir = sourceFile.parentFile ?: return null
return sourceRootCache.getOrPut(parentDir) {
sourceRoots.firstOrNull { root -> parentDir.path.startsWith(root.path) }
?: throw UnknownSourceRootException(sourceFile, sourceRoots)
}
}
val sourceToOutput = SourceToOutputMapping()
for (sourceFile in sourceFiles.map { it.absoluteFile }) {
val sourceRoot = findSourceRootFor(sourceFile)
val relativeSourcePath =
if (sourceRoot == null) sourceFile.name
else sourceFile.relativeTo(sourceRoot).path
val relativeOutputPath = relativeSourcePath.removeSuffix(sourceFileExt) + outputFileExt
sourceToOutput[sourceFile] = outDir.resolve(relativeOutputPath)
}
return sourceToOutput
}
internal class SourceToOutputMapping {
private val sourceToOutput = TreeMap<File, File>(object : Comparator<File> {
override fun compare(f1: File, f2: File): Int =
f1.absolutePath.compareTo(f2.absolutePath)
})
operator fun get(file: File): File? =
sourceToOutput[file]
fun remove(file: File): File? =
sourceToOutput.remove(file)
operator fun set(sourceFile: File, outputFile: File) {
sourceToOutput[sourceFile] = outputFile
}
fun putAll(other: SourceToOutputMapping) {
sourceToOutput.putAll(other.sourceToOutput)
}
fun clear() {
sourceToOutput.clear()
}
fun save(file: File) {
file.bufferedWriter().use { writer ->
for ((source, output) in sourceToOutput.entries) {
writer.write(source.absolutePath)
writer.write(File.pathSeparator)
writer.write(output.absolutePath)
writer.write("\n")
}
}
}
fun load(file: File) {
file.bufferedReader().useLines { lines ->
for (line in lines) {
val parts = line.split(File.pathSeparator)
check(parts.size == 2) {
"""
Line does not match the expected format!
Expected format: '<PATH_1>${File.pathSeparator}<PATH_2>'
Actual value: '$line'
""".trimIndent()
}
val (sourcePath, outputPath) = parts
this[File(sourcePath)] = File(outputPath)
}
}
}
}
\ No newline at end of file
package internal.utils
import java.io.File
internal class UnknownSourceRootException(
sourceFile: File,
sourceRoots: Collection<File>
) : IllegalStateException(
buildString {
appendLine("Could not find source root for: $sourceFile")
appendLine("Known source roots:")
for (root in sourceRoots) {
appendLine("* $root")
}
}
)
\ No newline at end of file
package internal.utils
import org.gradle.api.Task
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import java.io.File
import java.io.Writer
internal fun Provider<out FileSystemLocation>.resolveToIoFile(relative: Provider<String>): File =
get().asFile.resolve(relative.get())
internal inline fun <reified T> Task.provider(noinline fn: () -> T): Provider<T> =
project.provider(fn)
internal fun File.writeLines(lines: Collection<String>) {
if (exists()) {
delete()
} else {
parentFile?.mkdirs()
}
bufferedWriter().use { writer ->
lines.forEach { writer.writeLine(it) }
}
}
internal fun Writer.writeLine(line: String) {
write(line)
write("\n")
}
...@@ -16,24 +16,48 @@ enum class OS( ...@@ -16,24 +16,48 @@ enum class OS(
get() = this == Windows get() = this == Windows
} }
enum class Arch( fun compilerForTarget(os: OS, arch: Arch): String =
val id: String, when (os) {
val clangFlags: Array<String> OS.Linux -> when (arch) {
) { Arch.X64 -> "g++"
X64("x64", arrayOf()), Arch.Arm64 -> "clang++"
Arm64("arm64", arrayOf()), Arch.Wasm -> "Unexpected combination: $os & $arch"
Wasm("wasm", arrayOf("--bind", "-DSKIKO_WASM")) }
OS.Windows -> "cl.exe"
OS.MacOS, OS.IOS -> "clang++"
OS.Wasm -> "emcc"
}
fun linkerForTarget(os: OS, arch: Arch): String =
if (os.isWindows) "link.exe" else compilerForTarget(os, arch)
enum class Arch(val id: String) {
X64("x64"),
Arm64("arm64"),
Wasm("wasm")
} }
enum class SkiaBuildType( enum class SkiaBuildType(
val id: String, val id: String,
val flags: Array<String>, val flags: Array<String>,
val clangFlags: Array<String>, val clangFlags: Array<String>,
val msvcFlags: Array<String> val msvcCompilerFlags: Array<String>,
val msvcLinkerFlags: Array<String>
) { ) {
DEBUG("Debug", arrayOf("-DSK_DEBUG"), arrayOf("-std=c++17", "-g"), emptyArray()), DEBUG(
RELEASE("Release", arrayOf("-DNDEBUG"), arrayOf("-std=c++17", "-O3"), arrayOf("/O2")) "Debug",
; flags = arrayOf("-DSK_DEBUG"),
clangFlags = arrayOf("-std=c++17", "-g"),
msvcCompilerFlags = arrayOf("/Zi"),
msvcLinkerFlags = arrayOf("/DEBUG"),
),
RELEASE(
id = "Release",
flags = arrayOf("-DNDEBUG"),
clangFlags = arrayOf("-std=c++17", "-O3"),
msvcCompilerFlags = arrayOf("/O2"),
msvcLinkerFlags = arrayOf("/DEBUG"),
);
override fun toString() = id override fun toString() = id
} }
......
import org.gradle.api.Task
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
internal val Provider<out FileSystemLocation>.absolutePath: String
get() = get().asFile.absolutePath
internal fun Provider<out FileSystemLocation>.resolveToAbsolutePath(path: Provider<String>): String =
get().asFile.absoluteFile.resolve(path.get()).absolutePath
inline fun <reified T : Any> ObjectFactory.nullableProperty(): Property<T?> =
property(T::class.java)
inline fun <reified T : Any> ObjectFactory.notNullProperty(): Property<T> =
property(T::class.java)
inline fun <reified T : Any> ObjectFactory.notNullProperty(defaultValue: T): Property<T> =
property(T::class.java).value(defaultValue)
inline fun <reified T> Task.provider(noinline fn: () -> T): Provider<T> =
project.provider(fn)
import org.gradle.api.invocation.Gradle
import org.gradle.kotlin.dsl.support.serviceOf
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.*
import org.gradle.nativeplatform.platform.internal.NativePlatformInternal
import org.gradle.nativeplatform.toolchain.internal.SystemLibraries
import org.gradle.nativeplatform.toolchain.internal.msvcpp.*
import java.io.File
data class WindowsSdkPaths(
val compiler: File,
val linker: File,
val includeDirs: Collection<File>,
val libDirs: Collection<File>,
)
private const val ENV_SKIKO_VSBT_PATH = "SKIKO_VSBT_PATH"
private const val ENV_SKIKO_VSBT_VERSION = "SKIKO_VSBT_VERSION"
private const val ENV_SKIKO_WINDOWS_SDK_VERSION = "SKIKO_WINDOWS_SDK_VERSION"
fun findWindowsSdkPathsForCurrentOS(gradle: Gradle): WindowsSdkPaths {
check(hostOs.isWindows) { "Unexpected host os: $hostOs, expected: ${OS.Windows}" }
val hostPlatform = host()
val finder = GradleWindowsComponentFinderWrapper(gradle, hostPlatform)
val visualCpp = finder.findVisualCpp()
val windowsSdk = finder.findWindowsSdk()
val ucrt = finder.findUcrt()
val systemLibraries = listOf(visualCpp, windowsSdk, ucrt)
return WindowsSdkPaths(
compiler = visualCpp.compilerExecutable,
linker = visualCpp.linkerExecutable,
includeDirs = systemLibraries.flatMap { it.includeDirs },
libDirs = systemLibraries.flatMap { it.libDirs }
)
}
private class GradleWindowsComponentFinderWrapper(
private val gradle: Gradle,
private val hostPlatform: NativePlatformInternal
) {
fun findVisualCpp(): VisualCpp {
val skikoVsbtPath = System.getenv(ENV_SKIKO_VSBT_PATH)
val vsLocator = gradle.serviceOf<VisualStudioLocator>()
val vsComponent = if (skikoVsbtPath != null) {
val vsbtDir = File(skikoVsbtPath)
check(vsbtDir.isDirectory) {
"Environment variable '$ENV_SKIKO_VSBT_PATH' points to non-existing directory: '$skikoVsbtPath'\n" +
"Please set it to existing Visual Studio Build Tools installation"
}
val searchResult = vsLocator.locateComponent(vsbtDir)
if (!searchResult.isAvailable)
error("Could not find valid Visual Studio Build Tools installation " +
"at the location specified by '$ENV_SKIKO_VSBT_PATH': $skikoVsbtPath"
)
else searchResult.component
} else {
vsLocator.locateAllComponents().chooseComponentByPreferredVersion(
componentType = "VS Build Tools",
preferredVersionEnvVar = ENV_SKIKO_VSBT_VERSION
)
}
return vsComponent.visualCpp.forPlatform(hostPlatform)
?: error("Visual Studio location component for host platform '$hostPlatform' is null")
}
fun findWindowsSdk(): SystemLibraries {
val windowsSdkLocator = gradle.serviceOf<WindowsSdkLocator>()
val windowsSdkComponent = windowsSdkLocator.locateAllComponents()
.chooseComponentByPreferredVersion(
componentType = "Windows SDK",
preferredVersionEnvVar = ENV_SKIKO_WINDOWS_SDK_VERSION
)
return windowsSdkComponent.forPlatform(hostPlatform)
?: error("Windows SDK component for host platform '$hostPlatform' is null")
}
fun findUcrt(): SystemLibraries {
val ucrtLocator = gradle.serviceOf<UcrtLocator>()
val ucrtComponent = ucrtLocator.locateAllComponents()
.chooseComponentByPreferredVersion(
componentType = "UCRT",
preferredVersionEnvVar = ENV_SKIKO_WINDOWS_SDK_VERSION
)
return ucrtComponent.getCRuntime(hostPlatform)
?: error("UCRT component for host platform '$hostPlatform' is null")
}
private fun <T : Any> List<T>.chooseComponentByPreferredVersion(
componentType: String,
preferredVersionEnvVar: String,
): T = chooseComponentByPreferredVersion(componentType, preferredVersionEnvVar, this)
private fun <T : Any> chooseComponentByPreferredVersion(
componentType: String,
preferredVersionEnvVar: String,
components: List<T>
): T {
return when (components.size) {
0 -> error("Could not find any $componentType locations")
1 -> components.single()
else -> {
val versions = components.associateBy { component ->
when (component) {
is WindowsKitInstall -> component.version
is WindowsSdkInstall -> component.version
is VisualStudioInstall -> component.version
else -> error("Unknown class of $componentType: ${component.javaClass.canonicalName}")
}
}
val preferredVersion = System.getenv(preferredVersionEnvVar)
if (preferredVersion != null) {
for ((version, component) in versions.entries) {
if (preferredVersion == version.toString()) return component
}
}
val latestVersion = versions.keys.maxOf { it }
val warningMessage = buildString {
appendLine("w: Multiple $componentType versions are found: ${versions.keys.joinToString(", ") { "'$it'"}}")
appendLine("Using the latest version '$latestVersion'")
appendLine("Use '$preferredVersionEnvVar' environment variable to specify the preferred version")
}
gradle.rootProject.logger.warn(warningMessage)
versions[latestVersion]!!
}
}
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment