2020-07-10 13:57:42 +03:00
|
|
|
import java.io.File
|
2020-08-10 13:14:39 +03:00
|
|
|
import java.nio.file.Path
|
2020-07-10 13:57:42 +03:00
|
|
|
|
2023-04-19 22:09:05 +03:00
|
|
|
import sbt._
|
2020-07-10 13:57:42 +03:00
|
|
|
import sbt.Keys._
|
2020-08-10 13:14:39 +03:00
|
|
|
import sbt.internal.util.ManagedLogger
|
2020-12-09 16:58:11 +03:00
|
|
|
import sbtassembly.AssemblyKeys.assembly
|
|
|
|
import sbtassembly.AssemblyPlugin.autoImport.assemblyOutputPath
|
2020-07-22 20:28:03 +03:00
|
|
|
|
2020-07-10 13:57:42 +03:00
|
|
|
import scala.sys.process._
|
|
|
|
|
|
|
|
object NativeImage {
|
2020-08-10 13:14:39 +03:00
|
|
|
|
2020-12-09 16:58:11 +03:00
|
|
|
/** Specifies whether the build executable should include debug symbols.
|
|
|
|
* Should be set to false for production builds. May work only on Linux.
|
2020-08-10 13:14:39 +03:00
|
|
|
*/
|
|
|
|
private val includeDebugInfo: Boolean = false
|
|
|
|
|
2023-05-31 10:38:59 +03:00
|
|
|
/** List of classes that should be initialized at build time by the native image.
|
|
|
|
* Note that we strive to initialize as much classes during the native image build
|
|
|
|
* time as possible, as this reduces the time needed to start the native image.
|
|
|
|
* One wildcard could theoretically be used instead of the list, but to make things
|
|
|
|
* more explicit, we use the list.
|
|
|
|
*/
|
|
|
|
private val defaultBuildTimeInitClasses = Seq(
|
|
|
|
"org",
|
|
|
|
"org.enso",
|
|
|
|
"scala",
|
|
|
|
"java",
|
|
|
|
"sun",
|
|
|
|
"cats",
|
|
|
|
"io",
|
|
|
|
"shapeless",
|
|
|
|
"com",
|
|
|
|
"izumi",
|
|
|
|
"zio",
|
|
|
|
"enumeratum",
|
|
|
|
"akka",
|
2023-09-04 12:40:16 +03:00
|
|
|
"nl",
|
|
|
|
"ch.qos.logback"
|
2023-05-31 10:38:59 +03:00
|
|
|
)
|
|
|
|
|
2020-10-22 17:12:28 +03:00
|
|
|
/** Creates a task that builds a native image for the current project.
|
2020-12-09 16:58:11 +03:00
|
|
|
*
|
|
|
|
* This task must be setup in such a way that the assembly JAR is built
|
|
|
|
* before it starts, as it uses this JAR for the build. Usually this can be
|
|
|
|
* done by appending `.dependsOn(LocalProject("project-name") / assembly)`.
|
|
|
|
*
|
|
|
|
* Additional Native Image configuration can be set for each project by
|
|
|
|
* editing configuration files in subdirectories of `META-INF/native-image`
|
|
|
|
* of its resources directory. More information can be found at
|
|
|
|
* [[https://github.com/oracle/graal/blob/master/substratevm/BuildConfiguration.md]].
|
2020-08-10 13:14:39 +03:00
|
|
|
*
|
|
|
|
* @param artifactName name of the artifact to create
|
|
|
|
* @param staticOnLinux specifies whether to link statically (applies only
|
|
|
|
* on Linux)
|
2020-12-09 16:58:11 +03:00
|
|
|
* @param additionalOptions additional options for the Native Image build
|
|
|
|
* tool
|
2023-04-19 22:09:05 +03:00
|
|
|
* @param buildMemoryLimitMegabytes a memory limit for the build tool, in
|
2021-05-14 15:08:39 +03:00
|
|
|
* megabytes; it is good to set this limit to
|
2020-12-09 16:58:11 +03:00
|
|
|
* make GC more aggressive thus allowing it to
|
|
|
|
* build successfully even with limited memory
|
2023-04-19 22:09:05 +03:00
|
|
|
* @param runtimeThreadStackMegabytes the runtime thread stack size; the
|
|
|
|
* minimum for ZIO to work is higher than the
|
|
|
|
* default value on some systems
|
2020-12-09 16:58:11 +03:00
|
|
|
* @param initializeAtRuntime a list of classes that should be initialized at
|
|
|
|
* run time - useful to set exceptions if build
|
|
|
|
* time initialization is set to default
|
2023-05-31 10:38:59 +03:00
|
|
|
* @param initializeAtBuildtime a list of classes that should be initialized at
|
|
|
|
* build time.
|
2020-08-10 13:14:39 +03:00
|
|
|
*/
|
|
|
|
def buildNativeImage(
|
|
|
|
artifactName: String,
|
|
|
|
staticOnLinux: Boolean,
|
2023-04-19 22:09:05 +03:00
|
|
|
additionalOptions: Seq[String] = Seq.empty,
|
|
|
|
buildMemoryLimitMegabytes: Option[Int] = Some(15608),
|
|
|
|
runtimeThreadStackMegabytes: Option[Int] = Some(2),
|
|
|
|
initializeAtRuntime: Seq[String] = Seq.empty,
|
2023-05-31 10:38:59 +03:00
|
|
|
initializeAtBuildtime: Seq[String] = defaultBuildTimeInitClasses,
|
2023-04-19 22:09:05 +03:00
|
|
|
mainClass: Option[String] = None,
|
|
|
|
cp: Option[String] = None
|
2020-12-09 16:58:11 +03:00
|
|
|
): Def.Initialize[Task[Unit]] = Def
|
|
|
|
.task {
|
|
|
|
val log = state.value.log
|
|
|
|
val javaHome = System.getProperty("java.home")
|
|
|
|
val subProjectRoot = baseDirectory.value
|
|
|
|
val nativeImagePath =
|
|
|
|
if (Platform.isWindows)
|
|
|
|
s"$javaHome\\bin\\native-image.cmd"
|
|
|
|
else s"$javaHome/bin/native-image"
|
|
|
|
val pathToJAR =
|
|
|
|
(assembly / assemblyOutputPath).value.toPath.toAbsolutePath.normalize
|
|
|
|
|
|
|
|
if (!file(nativeImagePath).exists()) {
|
|
|
|
log.error("Native Image component not found in the JVM distribution.")
|
|
|
|
log.error(
|
|
|
|
"You can install Native Image with `gu install native-image`."
|
|
|
|
)
|
|
|
|
throw new RuntimeException(
|
|
|
|
"Native Image build failed, " +
|
|
|
|
"because Native Image component was not found."
|
|
|
|
)
|
|
|
|
}
|
2023-09-19 16:10:12 +03:00
|
|
|
if (additionalOptions.contains("--language:java")) {
|
|
|
|
log.warn(
|
|
|
|
s"Building ${artifactName} image with experimental Espresso support!"
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
2020-12-09 16:58:11 +03:00
|
|
|
|
|
|
|
val debugParameters =
|
|
|
|
if (includeDebugInfo) Seq("-H:GenerateDebugInfo=1") else Seq()
|
|
|
|
|
|
|
|
val (staticParameters, pathExts) =
|
|
|
|
if (staticOnLinux && Platform.isLinux) {
|
|
|
|
// Note [Static Build On Linux]
|
|
|
|
val buildCache =
|
|
|
|
subProjectRoot / "build-cache"
|
|
|
|
val path = ensureMuslIsInstalled(buildCache, log)
|
|
|
|
(Seq("--static", "--libc=musl"), Seq(path.toString))
|
|
|
|
} else (Seq(), Seq())
|
|
|
|
|
|
|
|
val configLocation =
|
|
|
|
subProjectRoot / "native-image-config"
|
|
|
|
val configs =
|
|
|
|
if (configLocation.exists()) {
|
|
|
|
val path = configLocation.toPath.toAbsolutePath
|
|
|
|
log.debug(s"Picking up Native Image configuration from `$path`.")
|
|
|
|
Seq(s"-H:ConfigurationFileDirectories=$path")
|
|
|
|
} else {
|
|
|
|
log.debug(
|
|
|
|
"No Native Image configuration found, proceeding without it."
|
2020-07-10 13:57:42 +03:00
|
|
|
)
|
2020-12-09 16:58:11 +03:00
|
|
|
Seq()
|
2020-07-10 13:57:42 +03:00
|
|
|
}
|
|
|
|
|
2022-11-23 17:30:48 +03:00
|
|
|
val quickBuildOption =
|
|
|
|
if (BuildInfo.isReleaseMode) Seq() else Seq("-Ob")
|
|
|
|
|
2023-04-19 22:09:05 +03:00
|
|
|
val buildMemoryLimitOptions =
|
|
|
|
buildMemoryLimitMegabytes.map(megs => s"-J-Xmx${megs}M").toSeq
|
|
|
|
|
|
|
|
val runtimeMemoryOptions =
|
|
|
|
runtimeThreadStackMegabytes.map(megs => s"-R:StackSize=${megs}M").toSeq
|
2020-12-09 16:58:11 +03:00
|
|
|
|
2023-05-31 10:38:59 +03:00
|
|
|
val initializeAtBuildtimeOptions =
|
|
|
|
if (initializeAtBuildtime.isEmpty) Seq()
|
|
|
|
else {
|
|
|
|
val classes = initializeAtBuildtime.mkString(",")
|
|
|
|
Seq(s"--initialize-at-build-time=$classes")
|
|
|
|
}
|
|
|
|
|
2020-12-09 16:58:11 +03:00
|
|
|
val initializeAtRuntimeOptions =
|
|
|
|
if (initializeAtRuntime.isEmpty) Seq()
|
|
|
|
else {
|
|
|
|
val classes = initializeAtRuntime.mkString(",")
|
|
|
|
Seq(s"--initialize-at-run-time=$classes")
|
2020-07-10 13:57:42 +03:00
|
|
|
}
|
|
|
|
|
2022-09-22 17:45:10 +03:00
|
|
|
var cmd =
|
2020-12-09 16:58:11 +03:00
|
|
|
Seq(nativeImagePath) ++
|
2022-11-23 17:30:48 +03:00
|
|
|
quickBuildOption ++
|
2020-12-09 16:58:11 +03:00
|
|
|
debugParameters ++ staticParameters ++ configs ++
|
|
|
|
Seq("--no-fallback", "--no-server") ++
|
2023-05-31 10:38:59 +03:00
|
|
|
initializeAtBuildtimeOptions ++
|
2022-11-23 17:30:48 +03:00
|
|
|
initializeAtRuntimeOptions ++
|
2023-04-19 22:09:05 +03:00
|
|
|
buildMemoryLimitOptions ++
|
|
|
|
runtimeMemoryOptions ++
|
|
|
|
additionalOptions
|
2022-09-22 17:45:10 +03:00
|
|
|
|
|
|
|
if (mainClass.isEmpty) {
|
|
|
|
cmd = cmd ++
|
|
|
|
Seq("-jar", pathToJAR.toString) ++
|
|
|
|
Seq(artifactName)
|
|
|
|
} else {
|
|
|
|
val cpf = new File(cp.get).getAbsoluteFile()
|
|
|
|
if (!cpf.exists()) throw new IllegalStateException("Cannot find " + cpf)
|
|
|
|
val joinCp = pathToJAR.toString + File.pathSeparator + cpf
|
2023-03-11 03:15:58 +03:00
|
|
|
System.out.println("Class-path: " + joinCp);
|
2022-09-22 17:45:10 +03:00
|
|
|
|
|
|
|
cmd = cmd ++
|
|
|
|
Seq("-cp", joinCp) ++
|
|
|
|
Seq(mainClass.get) ++
|
|
|
|
Seq(artifactName)
|
|
|
|
}
|
2020-12-09 16:58:11 +03:00
|
|
|
|
|
|
|
val pathParts = pathExts ++ Option(System.getenv("PATH")).toSeq
|
|
|
|
val newPath = pathParts.mkString(File.pathSeparator)
|
|
|
|
|
|
|
|
log.debug(s"""PATH="$newPath" ${cmd.mkString(" ")}""")
|
|
|
|
|
|
|
|
val process =
|
|
|
|
Process(cmd, None, "PATH" -> newPath)
|
|
|
|
|
|
|
|
if (process.! != 0) {
|
|
|
|
log.error("Native Image build failed.")
|
|
|
|
throw new RuntimeException("Native Image build failed")
|
2020-07-10 13:57:42 +03:00
|
|
|
}
|
2020-12-09 16:58:11 +03:00
|
|
|
|
|
|
|
log.info("Native Image build successful.")
|
|
|
|
}
|
|
|
|
.dependsOn(Compile / compile)
|
2020-07-22 20:28:03 +03:00
|
|
|
|
2020-10-22 17:12:28 +03:00
|
|
|
/** Creates a task which watches for changes of any compiled files or
|
2020-08-10 13:14:39 +03:00
|
|
|
* dependencies and triggers a rebuild if and only if there are any changes.
|
|
|
|
*
|
|
|
|
* @param actualBuild reference to the task doing the actual Native Image
|
|
|
|
* build, usually one returned by [[buildNativeImage]]
|
|
|
|
* @param artifactName name of the artifact that is expected to be created
|
|
|
|
* by the native image build
|
|
|
|
* @return
|
|
|
|
*/
|
2020-07-22 20:28:03 +03:00
|
|
|
def incrementalNativeImageBuild(
|
|
|
|
actualBuild: TaskKey[Unit],
|
|
|
|
artifactName: String
|
2020-08-10 13:14:39 +03:00
|
|
|
): Def.Initialize[Task[Unit]] =
|
2020-07-22 20:28:03 +03:00
|
|
|
Def.taskDyn {
|
|
|
|
def rebuild(reason: String) = {
|
|
|
|
streams.value.log.info(
|
|
|
|
s"$reason, forcing a rebuild."
|
|
|
|
)
|
2021-09-08 23:59:04 +03:00
|
|
|
val artifact = artifactFile(artifactName)
|
|
|
|
if (artifact.exists()) {
|
|
|
|
artifact.delete()
|
|
|
|
}
|
2020-07-22 20:28:03 +03:00
|
|
|
Def.task {
|
|
|
|
actualBuild.value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
val classpath = (Compile / fullClasspath).value
|
|
|
|
val filesSet = classpath.flatMap(f => f.data.allPaths.get()).toSet
|
|
|
|
|
|
|
|
val store =
|
|
|
|
streams.value.cacheStoreFactory.make("incremental_native_image")
|
|
|
|
Tracked.diffInputs(store, FileInfo.hash)(filesSet) {
|
|
|
|
sourcesDiff: ChangeReport[File] =>
|
2020-12-09 16:58:11 +03:00
|
|
|
if (sourcesDiff.modified.nonEmpty)
|
2023-03-15 18:43:51 +03:00
|
|
|
rebuild(s"Native Image is not up to date")
|
2020-07-22 20:28:03 +03:00
|
|
|
else if (!artifactFile(artifactName).exists())
|
|
|
|
rebuild("Native Image does not exist")
|
|
|
|
else
|
|
|
|
Def.task {
|
2020-12-09 16:58:11 +03:00
|
|
|
streams.value.log.info(
|
|
|
|
s"No source changes, $artifactName Native Image is up to date."
|
2020-07-22 20:28:03 +03:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-22 17:12:28 +03:00
|
|
|
/** [[File]] representing the artifact called `name` built with the Native
|
2020-09-09 16:37:26 +03:00
|
|
|
* Image.
|
|
|
|
*/
|
|
|
|
def artifactFile(name: String): File =
|
2020-10-02 19:17:21 +03:00
|
|
|
if (Platform.isWindows) file(name + ".exe")
|
2020-07-22 20:28:03 +03:00
|
|
|
else file(name)
|
2020-08-10 13:14:39 +03:00
|
|
|
|
|
|
|
private val muslBundleUrl =
|
2020-09-09 16:37:26 +03:00
|
|
|
"https://github.com/gradinac/musl-bundle-example/releases/download/" +
|
|
|
|
"v1.0/musl.tar.gz"
|
2020-08-10 13:14:39 +03:00
|
|
|
|
2020-10-22 17:12:28 +03:00
|
|
|
/** Ensures that the `musl` bundle is installed.
|
2020-08-10 13:14:39 +03:00
|
|
|
*
|
|
|
|
* Checks for existence of its directory and if it does not exist, downloads
|
2020-08-28 14:03:09 +03:00
|
|
|
* and extracts the bundle. After extracting it does the required
|
|
|
|
* initialization (renaming paths to be absolute and creating a shell script
|
2022-02-11 19:05:13 +03:00
|
|
|
* called `x86_64-linux-musl-gcc`).
|
2020-08-28 14:03:09 +03:00
|
|
|
*
|
|
|
|
* `musl` is needed for static builds on Linux.
|
2020-08-10 13:14:39 +03:00
|
|
|
*
|
|
|
|
* @param buildCache build-cache directory for the current project
|
|
|
|
* @param log a logger instance
|
2020-08-28 14:03:09 +03:00
|
|
|
* @return path to the `musl` bundle binary directory which should be added
|
|
|
|
* to PATH of the launched native-image
|
2020-08-10 13:14:39 +03:00
|
|
|
*/
|
|
|
|
private def ensureMuslIsInstalled(
|
|
|
|
buildCache: File,
|
|
|
|
log: ManagedLogger
|
|
|
|
): Path = {
|
|
|
|
val muslRoot = buildCache / "musl-1.2.0"
|
|
|
|
val bundleLocation = muslRoot / "bundle"
|
2020-08-28 14:03:09 +03:00
|
|
|
val binaryLocation = bundleLocation / "bin"
|
2022-02-11 19:05:13 +03:00
|
|
|
val gccLocation = binaryLocation / "x86_64-linux-musl-gcc"
|
2020-08-28 14:03:09 +03:00
|
|
|
def isMuslInstalled =
|
|
|
|
gccLocation.exists() && gccLocation.isOwnerExecutable
|
|
|
|
if (!isMuslInstalled) {
|
2020-08-10 13:14:39 +03:00
|
|
|
log.info(
|
|
|
|
"`musl` is required for a static build, but it is not installed for " +
|
|
|
|
"this subproject."
|
|
|
|
)
|
|
|
|
try {
|
|
|
|
log.info("A `musl` bundle will be downloaded.")
|
|
|
|
buildCache.mkdirs()
|
|
|
|
val bundle = buildCache / "musl-bundle.tar.gz"
|
|
|
|
|
|
|
|
val downloadExitCode = (url(muslBundleUrl) #> bundle).!
|
|
|
|
if (downloadExitCode != 0) {
|
|
|
|
log.error("Cannot download `musl` bundle.")
|
|
|
|
throw new RuntimeException(s"Cannot download `$muslBundleUrl`.")
|
|
|
|
}
|
|
|
|
|
|
|
|
muslRoot.mkdirs()
|
|
|
|
val tarExitCode = Seq(
|
|
|
|
"tar",
|
|
|
|
"xf",
|
|
|
|
bundle.toPath.toAbsolutePath.toString,
|
|
|
|
"-C",
|
|
|
|
muslRoot.toPath.toAbsolutePath.toString
|
|
|
|
).!
|
|
|
|
if (tarExitCode != 0) {
|
|
|
|
log.error(
|
|
|
|
"An error occurred when extracting the `musl` library bundle."
|
|
|
|
)
|
|
|
|
throw new RuntimeException(s"Cannot extract $bundle.")
|
|
|
|
}
|
|
|
|
|
2020-08-28 14:03:09 +03:00
|
|
|
replacePathsInSpecs(
|
|
|
|
bundleLocation / "lib" / "musl-gcc.specs",
|
|
|
|
bundleLocation
|
|
|
|
)
|
|
|
|
createGCCWrapper(bundleLocation)
|
|
|
|
|
2020-08-10 13:14:39 +03:00
|
|
|
log.info("Installed `musl`.")
|
|
|
|
} catch {
|
|
|
|
case e: Exception =>
|
|
|
|
throw new RuntimeException(
|
|
|
|
"`musl` installation failed. Cannot proceed with a static " +
|
|
|
|
"Native Image build.",
|
|
|
|
e
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-08-28 14:03:09 +03:00
|
|
|
binaryLocation.toPath.toAbsolutePath.normalize
|
|
|
|
}
|
|
|
|
|
2020-10-22 17:12:28 +03:00
|
|
|
/** Replaces paths in `musl-gcc.specs` with absolute paths to the bundle.
|
2020-08-28 14:03:09 +03:00
|
|
|
*
|
|
|
|
* The paths in `musl-gcc.specs` start with `/build/bundle` which is not a
|
|
|
|
* valid path by default. Instead, these prefixes are replaced with an
|
|
|
|
* absolute path to the bundle.
|
|
|
|
*
|
|
|
|
* @param specs reference to `musl-gcc.specs` file
|
|
|
|
* @param bundleLocation location of the bundle root
|
|
|
|
*/
|
|
|
|
private def replacePathsInSpecs(specs: File, bundleLocation: File): Unit = {
|
|
|
|
val content = IO.read(specs)
|
|
|
|
val bundlePath = bundleLocation.toPath.toAbsolutePath.normalize.toString
|
|
|
|
val replaced = content.replace("/build/bundle", bundlePath)
|
|
|
|
IO.write(specs, replaced)
|
|
|
|
}
|
|
|
|
|
2020-10-22 17:12:28 +03:00
|
|
|
/** Creates a simple shell script called `musl-gcc` which calls the original
|
2020-08-28 14:03:09 +03:00
|
|
|
* `gcc` and ensures the bundle's configuration (`musl-gcc.specs`) is loaded.
|
|
|
|
*/
|
|
|
|
private def createGCCWrapper(bundleLocation: File): Unit = {
|
|
|
|
val bundlePath = bundleLocation.toPath.toAbsolutePath.normalize.toString
|
|
|
|
val content =
|
|
|
|
s"""#!/bin/sh
|
|
|
|
|exec "$${REALGCC:-gcc}" "$$@" -specs "$bundlePath/lib/musl-gcc.specs"
|
|
|
|
|""".stripMargin
|
2022-02-11 19:05:13 +03:00
|
|
|
val wrapper = bundleLocation / "bin" / "x86_64-linux-musl-gcc"
|
2020-08-28 14:03:09 +03:00
|
|
|
IO.write(wrapper, content)
|
|
|
|
wrapper.setExecutable(true)
|
2020-08-10 13:14:39 +03:00
|
|
|
}
|
|
|
|
|
2020-07-10 13:57:42 +03:00
|
|
|
}
|
2020-08-10 13:14:39 +03:00
|
|
|
|
|
|
|
/* Note [Static Build On Linux]
|
|
|
|
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
* The default `glibc` contains a bug that would cause crashes when downloading
|
|
|
|
* files form the internet, which is a crucial Launcher functionality. Instead,
|
|
|
|
* `musl` is suggested by Graal as an alternative libc. The sbt task
|
|
|
|
* automatically downloads a bundle containing all requirements for a static
|
|
|
|
* build with `musl`.
|
|
|
|
*
|
2020-08-28 14:03:09 +03:00
|
|
|
* The `musl` bundle that we use is not guaranteed to be maintained, so if in
|
|
|
|
* the future a new version of `musl` comes out and we need to upgrade, we may
|
|
|
|
* need to create our own infrastructure (a repository with CI jobs for creating
|
|
|
|
* such bundles). It is especially important to note that the libstdc++ that is
|
|
|
|
* included in this bundle should also be built using `musl` as otherwise linker
|
|
|
|
* errors may arise.
|
|
|
|
*
|
|
|
|
* Currently, to use `musl`, the `--libc=musl` option has to be added to the
|
2022-02-11 19:05:13 +03:00
|
|
|
* build and `x86_64-linux-musl-gcc` must be available in the system PATH for the
|
2020-08-28 14:03:09 +03:00
|
|
|
* native-image. In the future it is possible that a different option will be
|
|
|
|
* used or that the bundle will not be required anymore if it became
|
|
|
|
* prepackaged. This task may thus need an update when moving to a newer version
|
|
|
|
* of Graal.
|
|
|
|
*
|
|
|
|
* Currently to make the bundle work correctly with GraalVM 20.2, a shell script
|
2022-02-11 19:05:13 +03:00
|
|
|
* called `x86_64-linux-musl-gcc` which loads the bundle's configuration is created by the
|
2020-08-28 14:03:09 +03:00
|
|
|
* task and the paths starting with `/build/bundle` in `musl-gcc.specs` are
|
|
|
|
* replaced with absolute paths to the bundle location.
|
2020-08-10 13:14:39 +03:00
|
|
|
*/
|