enso/project/DistributionPackage.scala
Pavel Marek 9182f91e35
engine-runner and language-server are separate JPMS modules (#10823)
* Use moduleDependencies instead of modulePath

* Fix compilation of editions

* Fix compilation of distribution-manager

* polyglot-api needs to explicitly compile module-info

* Fix compilationOrder in library-manager and edition-updater

* engine-runner-common is module

* JPMSPlugin provides default implementation of compileModuleInfo

* Remove unused setting key from JPMSUtils.compileModuleInfo

* JPMSPlugin has internalModuleDependencies and exportedModule tasks.

* Use BuildVersion instead of buildInfo

* Manual compilation of module-info.java is reported as warning

* Define org.enso.scalalibs.wrapper meta project.

* Fix module check in JPMSPlugin.

This is a fix for projects that declare `Compile /exportJars := true`

* version-output is a module

* ydoc-server uses internalModuleDependencies

* persistance is module

* engine-common uses internalModuleDependencies

* polyglot-api does not override compileModuleInfo task

* runtime-parser uses internalModuleDependencies

* edition-updater is module

* Update moduleDependencies for distribution-manager

* editions is module

* Fix some dependencies of modules

* scala-yaml is a module

* Add scala-compiler to scala-libs-wrapper

* cli depends on scala-library module

* Add dependencies for distribution-manager module

* Add some scala-library dependencies in some modules

* engine-runner uses internalModuleDependencies

* Fix module dependencies of library-manager

* Rename org.enso.scalalibs.wrapper to org.enso.scala.wrapper

* Add jsoniter-scala-macros to org.enso.scala.wrapper fat module

* Fix dependencies of some projects

* polyglot-api does not depend on truffle-api

* Fix dependencies of some projects

* runtime does not use com.google.common

* runtime is a module

* text-buffer is a module

* refactoring-utils is a module

* runtime-compiler is a module

* runtime-instrument-common is a module

* connected-lock-manager is a module

* JPMSUtils reports project name in some error messages

* Modularize some instruments

* module-info compilation is cached

* runtime-instrument-runtime-server is module

* runtime-language-epb is module

* Remove runtime-fat-jar

* engine-runner is not a fat jar

* JPMSPlugin defines exportedModuleBin task

* Redefine componentModulesPaths task

* interpreter-dsl is module

* Redefine componentModulesPaths task

* fmt sbt

* scala-libs-wrapper is a modular fat jar

* Add some module deps to org.enso.runtime

* engine-runner is not a fat jar

* Rename package in logging-config

* Rename package in logging-service

* Rename package in logging-service-logback

* Fix dependencies of exportedModuleBin task

* Mixed projects have own compileJava task

this task does not compile only module-info.java but all the java sources. So that we can see errors more easily.

When only module-info.java is compiled, the only errors that we can see are that we did not include some modules on module-path.

* Fix definition of exportedModule task.

* Remove usages of non-existing buildInfo and replace it with BuildVersion

* Fix some dependencies of org.enso.runtime module

* module-info compilation is handled directly by FrgaalCompiler

* module-info compilation is forced for projects that has only Scala sources with single module-info.java

* Fix compilation of org.enso.runtime

* manual module-info compilation is not a warning

* Rename packages in logging-utils-akka

* Create org.enso.language.server.deps.wrapper module

* language-server is module

* Creat akka-wrapper modular fat jar

* fmt

* Define common settings for modularFatJarWrapper

* Fix compilation of json-rpc-server

* Use akka and zio wrappers

* language-server depends on org.eclipse.jgit

* Fix some dependencies - update library manifests works now!

* update library manifests invokes runner directly

* buildEngineDistribution does not copy runner.jar

* Remove EngineRunnerBootLoader

* Fix compilation of std libs

* --patch-module and --add-exports are also passed to javac

* Rename package in runtime-integration-tests.

The package name org.enso.compiler clashes with the package from the module

* Remove usage of buildInfo

* FrgaalJavaCompiler can deal with non-existing module-info.java when shouldCompileModuleInfo is true.

It just generates a warning in such case as it suggests that there is something wrong with the project configuration.

* Revert AliasAnalysisTest.scala

* Fix dependencies and java cmdline options for runtime-integration-tests

* Rename test package

* runtime-integration-test depends on logging-service-logback/Test/compile

* Rename package in logging-service-logback/Test

* Fix FrgaalJavaCompiler creation for projects

* Sanitize Test/javaOptions arguments

* Sanitize Test/javaOptions arguments

* All the JPMSPlugin settings are scoped

* Remove unused sbt tasks

* modularFatJarWrapperSettings do not override javacOptions

* Resolve issue "Cannot find TestLoggerProvider" in runtime-integration-tests

* org.enso.runtime module is open

* Test that test classes are unconditionally opened and exported

* polyglot-api-macros is a module

* JPMSPlugin handles --add-opens cmdline option

* RuntimeServerTest ensures instruments are initialized

* Add some exports to org.enso.runtime.compiler

* Add instruments on module-path to runtime-integration-tests

* Replace TestLogProviderOnClassPath with TestLogProviderOnModulePath

* Replace buildInfo with BuildVersion

* Add jpms-wrapper-scalatest

* ReportLogsOnFailure is in non-modular testkit project

* Add necessary dependencies to testkit project

* Revert "Add jpms-wrapper-scalatest"

This reverts commit 732b3427a2.

* modularize filewatcher and wrap its dependencies

* Initial fix for language-server/test

* frgaal compiler setting are scoped for Compile and Test

* Rename package in language-server/test

* Exclude com.sun.jna from wrapper jars

* Rename package in library-manager-test

* testkit is an automatic module

* process-utils is module

* akka-wrapper contains akka-http

* Some fixes for library-manager-test

* Fix dependencies for akka-wrapper

* scala-libs-wrapper exports shapeless

* lang server deps wrapper exports pureconfig

* json-rpc-server requires org.slf4j

* Add some dependencies

* lang server deps wrapper exports pureconfig.generic

* language server test requires bouncycastle provider

* language server depends on cli

* directory-watcher wrapper requires org.slf4j

* WatcherAdapter logs unsuccessful initialization errors

* Fix error reporting in WatcherAdapter

* Fix rest of the language-server tests

* language-server-deps-wrapper depends on scala-libs-wrapper

* Fix rest of the language-server tests

* Missing module-info.class in an internal project is a warning, not an error

* Rename jpms-methvin-directory-watcher-wrapper to a simpler name

* compileOrder has to be specified before libraryDependencies

* exclude module-info.java from polyglot-api-macros

* Remove temporary logging in customFrgaalCompilerSettings

* Fix compilation of logging-service-logback

* Fix compilation of runtime-benchmarks

* Fix runtime-benchmarks/run

* HostClassLoader delegates to org.graalvm.polyglot class loader if org.enso.runtime is not on boot layer

* org.enso.runtime.lnaguage.epb module must be opened to allow it to be used by annnotation processor

* fmt

* Fix afetr merge

* Add module deps after merge

* Print stack trace of the uncaught exception from the annotation processor

* Remove akka-actor-typed from akka-wrapper

* runtime-instrument-common depends on slf4j

* Fix module-path for runtime-instrument-repl-debugger

* runtime-benchmarks depends on runtime-language-arrow

* --module-path is passed directly to frgaal

* Fix some module-related cmd line options for std-benchmarks

* Revert "--module-path is passed directly to frgaal"

This reverts commit da63f66a0e.

* Avoid closing of System.err when closing Context

* Avoid processing altogether when requested annotations are empty

* Pass shouldNotLimitModules opt to frgaal

* Pass module-path and add-modules options with -J prefix to frgaal

* BenchProcessor annotation processor creates its own truffle module layer

* bench-processor and benchmarks-common are modules

* fmt

* Fix after mege

* Enable JMH annotation processor

* Fix compileOrder in some projects

* Insert TruffleBoundary to QualifiedName.

This is a revert

* Fix building of engine-runner native image

* Add more deps to the native image

* Force module-info compilation in instruments.

This fixes some weird sbt bug

* Don't run engine-runner/assembly from Rust build script

* Update docs of JPMSPlugin

* fmt

* runtime-benchmarks depends on benchmarks-common module

* Fix benchmark report writing

* std-benchmarks annot processing does not take settings from runtime-benchmarks

* Suppress interpreter only warning in annotation processor

* Runtime version manager does not expect runtime.jar fat jar

* fmt

* Fix module entry point

* Move some polyglot tests to runtime-integration-tests.

Also make their output silent

* pkg has no dependency on org.graalvm.truffle

* Fix compiler dependencies test

* Rename all runtime.jar in fake releases

* Add language-server with dependencies to component dir

* No module-info.class in target dir is warning not error

* language-server does not depend on netbeans lookup uitl

* Declare LanguageServerApi service provider in module-info

* connected-lock-manager-server is JPMS module

* task-progress-notifications is module

* Add fansi-wrapper module

* Fix compilation of connected-lock-manager-server

* Define correct Test/internalModuleDependencies for project-manager

* fmt

* Fix LauncherRunnerSpec - no runtime.jar

* Add fansi-wrapper to runtime-integration-tests and runtime-benchmarks

* Fix engine-runner native image build

* Use newer JNA version - fixes running of hyperd

* DRY

* scala-compiler DRY

* fmt

* More build.sbt refactoring

* Include runtime-instrument-id-execution in engine-runner native image

* TruffleBoundary for QualifiedName.toString

* Finding a needle in a haystack

🤦

* More scala-library DRY

* more mixed-java/scala goodies

* Fix compilation of syntax-rust-definition

* Test that engine-runner does not depend on language-server

* Append rather than assign `moduleDependencies`

`++=` is less error prone than `:=`. Also discovered some unnecessary
dependencies.

* Replace : with File.pathSeparator

* [WIP] Make logging in ProjectService more verbose

* language-server/test didn't start because of missing lookup and fansi modules

* Formatting

* org.enso.cli.task.notifications needs Akka and Circe to link

* project-manager/test depends on buildEngineDistribution

* [WIP] Even more verbose logging for creating projects

* [WIP] Even more verbose logging for creating projects

* Revert "[WIP] Even more verbose logging for creating projects"

This reverts commit a7067c8472.

* Revert "[WIP] Even more verbose logging for creating projects"

This reverts commit fc6f53d4f1.

* Revert "[WIP] Make logging in ProjectService more verbose"

This reverts commit 427428e142.

* All the project with JPMSPlugin has stripped artifact names

* Revert all placeholder fake release components to runtime.jar without version

* Eliminate a cross version hack

We shouldn't be specifying Scala dependencies with a Scala cross version
in the suffix.

* Address SBT lint warnings

* Revert "Eliminate a cross version hack"

This reverts commit 8861dab288.

* logging-service-logback  is mixedJavaScalaProject

* fmt

* Stripped artifact name contains classifier.

This fixes tests as those were named like `artifact-tests.jar`.

* Don't use LocalProject unless really needed

* Add more logging when BenchProcessor fails

* logging-service-logback is not mixed project

* Work with java.io.File.getPath to avoid mixing slash and backslash on Windows

* Reapply "Eliminate a cross version hack"

This reverts commit edaa436ee8.

* Pass scalaBinaryVersion correctly

* Remove scala-compiler from the distribution

* Fix IllegalAccessErrors from serde

* typos

* License review

* fmt

* Move testLogProviderOnModulePath to TestJPMSConfiguration

* logging-service-logback is not a mixed project

---------

Co-authored-by: Jaroslav Tulach <jaroslav.tulach@enso.org>
Co-authored-by: Hubert Plociniczak <hubert.plociniczak@gmail.com>
2024-09-25 21:33:13 +02:00

1058 lines
32 KiB
Scala

import io.circe.yaml
import io.circe.syntax._
import org.apache.commons.io.IOUtils
import sbt.internal.util.ManagedLogger
import sbt._
import sbt.io.syntax.fileToRichFile
import sbt.util.{CacheStore, CacheStoreFactory, FileInfo, Tracked}
import scala.sys.process._
import org.enso.build.WithDebugCommand
import java.nio.file.Paths
import scala.util.Try
object DistributionPackage {
/** File extensions. */
implicit class FileExtensions(file: File) {
/** Get the outermost directory of this file. For absolute paths this
* function always returns root.
*
* == Example ==
* Get top directory of the relative path.
* {{{
* file("foo/bar/baz").getTopDirectory == file("foo")
* }}}
*
* Get top directory of the absolute path.
* {{{
* file(/foo/bar/baz").getTopDirectory == file("/")
* }}}
*
* @return the outermost directory of this file.
*/
def getTopDirectory: File = {
@scala.annotation.tailrec
def go(path: File): File = {
val parent = path.getParentFile
if (parent == null) path else go(parent)
}
go(file)
}
}
/** Conditional copying, based on the contents of cache and timestamps of files.
*
* @param source source directory
* @param destination target directory
* @param cache cache used for persisting the cached information
* @return true, if copying was necessary, false if no change was detected between the directories
*/
def copyDirectoryIncremental(
source: File,
destination: File,
cache: CacheStore
): Boolean = {
val allFiles = source.allPaths.get().toSet
Tracked.diffInputs(cache, FileInfo.lastModified)(allFiles) { diff =>
val missing = diff.unmodified.exists { f =>
val relativePath = f.relativeTo(source).get
val destinationFile =
destination.toPath.resolve(relativePath.toPath).toFile
!destinationFile.exists()
}
if (diff.modified.nonEmpty || diff.removed.nonEmpty || missing) {
IO.delete(destination)
IO.copyDirectory(source, destination)
true
} else false
}
}
def copyFilesIncremental(
sources: Seq[File],
destinationDirectory: File,
cache: CacheStore
): Unit = {
val allFiles = sources.toSet
IO.createDirectory(destinationDirectory)
Tracked.diffInputs(cache, FileInfo.lastModified)(allFiles) { diff =>
for (f <- diff.removed) {
IO.delete(destinationDirectory / f.getName)
}
for (f <- diff.modified -- diff.removed) {
IO.copyFile(f, destinationDirectory / f.getName)
}
for (f <- diff.unmodified) {
val destinationFile = destinationDirectory / f.getName
if (!destinationFile.exists()) {
IO.copyFile(f, destinationDirectory / f.getName)
}
}
}
}
def executableName(baseName: String): String =
if (Platform.isWindows) baseName + ".exe" else baseName
private def batName(baseName: String): String =
if (Platform.isWindows) baseName + ".bat" else baseName
def createProjectManagerPackage(
distributionRoot: File,
cacheFactory: CacheStoreFactory
): Unit = {
copyDirectoryIncremental(
file("distribution/project-manager/THIRD-PARTY"),
distributionRoot / "THIRD-PARTY",
cacheFactory.make("project-manager-third-party")
)
copyFilesIncremental(
Seq(file(executableName("project-manager"))),
distributionRoot / "bin",
cacheFactory.make("project-manager-exe")
)
}
def createEnginePackage(
distributionRoot: File,
cacheFactory: CacheStoreFactory,
log: Logger,
jarModulesToCopy: Seq[File],
graalVersion: String,
javaVersion: String,
ensoVersion: String,
editionName: String,
sourceStdlibVersion: String,
targetStdlibVersion: String,
targetDir: File,
generateIndex: Boolean
): Unit = {
copyDirectoryIncremental(
file("distribution/engine/THIRD-PARTY"),
distributionRoot / "THIRD-PARTY",
cacheFactory.make("engine-third-party")
)
copyFilesIncremental(
jarModulesToCopy,
distributionRoot / "component",
cacheFactory.make("module jars")
)
val parser = targetDir / Platform.dynamicLibraryFileName("enso_parser")
copyFilesIncremental(
Seq(parser),
distributionRoot / "component",
cacheFactory.make("engine-parser-library")
)
(distributionRoot / "editions").mkdirs()
Editions.writeEditionConfig(
editionsRoot = distributionRoot / "editions",
ensoVersion = ensoVersion,
editionName = editionName,
libraryVersion = targetStdlibVersion,
log = log
)
copyLibraryCacheIncremental(
sourceRoot = file("distribution/lib"),
destinationRoot = distributionRoot / "lib",
sourceVersion = sourceStdlibVersion,
targetVersion = targetStdlibVersion,
cacheFactory = cacheFactory.sub("engine-libraries"),
log = log
)
copyDirectoryIncremental(
file("distribution/bin"),
distributionRoot / "bin",
cacheFactory.make("engine-bin")
)
buildEngineManifest(
template = file("distribution/manifest.template.yaml"),
destination = distributionRoot / "manifest.yaml",
graalVersion = graalVersion,
javaVersion = javaVersion
)
if (generateIndex) {
indexStdLibs(
stdLibVersion = targetStdlibVersion,
ensoVersion = ensoVersion,
stdLibRoot = distributionRoot / "lib",
ensoExecutable = distributionRoot / "bin" / "enso",
cacheFactory = cacheFactory.sub("stdlib"),
log = log
)
}
}
def indexStdLibs(
stdLibVersion: String,
ensoVersion: String,
stdLibRoot: File,
ensoExecutable: File,
cacheFactory: CacheStoreFactory,
log: Logger
): Unit = {
for {
libMajor <- stdLibRoot.listFiles()
libName <- (stdLibRoot / libMajor.getName).listFiles()
} yield {
indexStdLib(
libName,
stdLibVersion,
ensoVersion,
ensoExecutable,
cacheFactory,
log
)
}
}
def indexStdLib(
libName: File,
stdLibVersion: String,
ensoVersion: String,
ensoExecutable: File,
cacheFactory: CacheStoreFactory,
log: Logger
): Unit = {
object FileOnlyFilter extends sbt.io.FileFilter {
def accept(arg: File): Boolean = arg.isFile
}
val cache = cacheFactory.make(s"$libName.$ensoVersion")
val path = libName / ensoVersion
Tracked.diffInputs(cache, FileInfo.lastModified)(
path.globRecursive("*.enso" && FileOnlyFilter).get().toSet
) { diff =>
if (diff.modified.nonEmpty) {
log.info(s"Generating index for $libName ")
val command = Seq(
Platform.executableFile(ensoExecutable.getAbsoluteFile),
"--no-compile-dependencies",
"--no-global-cache",
"--compile",
path.getAbsolutePath
)
log.debug(command.mkString(" "))
val runningProcess = Process(
command,
Some(path.getAbsoluteFile.getParentFile),
"JAVA_OPTS" -> "-Dorg.jline.terminal.dumb=true"
).run
// Poor man's solution to stuck index generation
val GENERATING_INDEX_TIMEOUT = 60 * 2 // 2 minutes
var current = 0
var timeout = false
while (runningProcess.isAlive() && !timeout) {
if (current > GENERATING_INDEX_TIMEOUT) {
java.lang.System.err
.println("Reached timeout when generating index. Terminating...")
try {
val pidOfProcess = pid(runningProcess)
val javaHome = System.getProperty("java.home")
val jstack =
if (javaHome == null) "jstack"
else
Paths.get(javaHome, "bin", "jstack").toAbsolutePath.toString
val in = java.lang.Runtime.getRuntime
.exec(Array(jstack, pidOfProcess.toString))
.getInputStream
System.err.println(IOUtils.toString(in, "UTF-8"))
} catch {
case e: Throwable =>
java.lang.System.err
.println("Failed to get threaddump of a stuck process", e);
} finally {
timeout = true
runningProcess.destroy()
}
} else {
Thread.sleep(1000)
current += 1
}
}
if (timeout) {
throw new RuntimeException(
s"TIMEOUT: Failed to compile $libName in $GENERATING_INDEX_TIMEOUT seconds"
)
}
if (runningProcess.exitValue() != 0) {
throw new RuntimeException(s"Cannot compile $libName.")
}
} else {
log.debug(s"No modified files. Not generating index for $libName.")
}
}
}
def runEnginePackage(
distributionRoot: File,
args: Seq[String],
log: Logger
): Boolean = {
import scala.collection.JavaConverters._
val enso = distributionRoot / "bin" / batName("enso")
val pb = new java.lang.ProcessBuilder()
val all = new java.util.ArrayList[String]()
val runArgumentIndex = locateRunArgument(args)
val runArgument = runArgumentIndex.map(args)
val disablePrivateCheck = runArgument match {
case Some(whatToRun) =>
if (whatToRun.startsWith("test/") && whatToRun.endsWith("_Tests")) {
whatToRun.contains("_Internal_")
} else {
false
}
case None => false
}
val runArgumentAsFile = runArgument.flatMap(createFileIfValidPath)
val projectDirectory = runArgumentAsFile.flatMap(findProjectRoot)
val cwdOverride: Option[File] =
projectDirectory.flatMap(findParentFile).map(_.getAbsoluteFile)
all.add(enso.getAbsolutePath)
all.addAll(args.asJava)
// Override the working directory of new process to be the parent of the project directory.
cwdOverride.foreach { c =>
pb.directory(c)
}
if (cwdOverride.isDefined) {
// If the working directory is changed, we need to translate the path - make it absolute.
all.set(runArgumentIndex.get + 1, runArgumentAsFile.get.getAbsolutePath)
}
if (args.contains("--debug")) {
all.remove("--debug")
pb.environment().put("JAVA_OPTS", "-ea " + WithDebugCommand.DEBUG_OPTION)
} else {
pb.environment().put("JAVA_OPTS", "-ea")
}
if (disablePrivateCheck) {
all.add("--disable-private-check")
}
pb.command(all)
pb.inheritIO()
log.info(s"Executing ${all.asScala.mkString(" ")}")
val p = pb.start()
val exitCode = p.waitFor()
if (exitCode != 0) {
log.warn(enso + " finished with exit code " + exitCode)
}
exitCode == 0
}
// https://stackoverflow.com/questions/23279898/get-process-id-of-scala-sys-process-process
def pid(p: Process): Long = {
val procField = p.getClass.getDeclaredField("p")
procField.synchronized {
procField.setAccessible(true)
val proc = procField.get(p)
try {
proc match {
case unixProc
if unixProc.getClass.getName == "java.lang.UNIXProcess" =>
val pidField = unixProc.getClass.getDeclaredField("pid")
pidField.synchronized {
pidField.setAccessible(true)
try {
pidField.getLong(unixProc)
} finally {
pidField.setAccessible(false)
}
}
case javaProc: java.lang.Process =>
javaProc.pid()
case other =>
throw new RuntimeException(
"Cannot get PID of a " + proc.getClass.getName
)
}
} finally {
procField.setAccessible(false)
}
}
}
/** Returns the index of the next argument after `--run`, if it exists. */
private def locateRunArgument(args: Seq[String]): Option[Int] = {
val findRun = args.indexOf("--run")
if (findRun >= 0 && findRun + 1 < args.size) {
Some(findRun + 1)
} else {
None
}
}
/** Returns a file, only if the provided string represented a valid path. */
private def createFileIfValidPath(path: String): Option[File] =
Try(new File(path)).toOption
/** Looks for a parent directory that contains `package.yaml`. */
private def findProjectRoot(file: File): Option[File] =
if (file.isDirectory && (file / "package.yaml").exists()) {
Some(file)
} else {
findParentFile(file).flatMap(findProjectRoot)
}
private def findParentFile(file: File): Option[File] =
Option(file.getParentFile)
def runProjectManagerPackage(
engineRoot: File,
distributionRoot: File,
args: Seq[String],
log: Logger
): Boolean = {
import scala.collection.JavaConverters._
val enso = distributionRoot / "bin" / "project-manager"
log.info(s"Executing $enso ${args.mkString(" ")}")
val pb = new java.lang.ProcessBuilder()
val all = new java.util.ArrayList[String]()
all.add(enso.getAbsolutePath())
all.addAll(args.asJava)
pb.command(all)
pb.environment().put("ENSO_ENGINE_PATH", engineRoot.toString())
pb.environment().put("ENSO_JVM_PATH", System.getProperty("java.home"))
if (args.contains("--debug")) {
all.remove("--debug")
pb.environment().put("ENSO_JVM_OPTS", WithDebugCommand.DEBUG_OPTION)
}
pb.inheritIO()
val p = pb.start()
val exitCode = p.waitFor()
if (exitCode != 0) {
log.warn(enso + " finished with exit code " + exitCode)
}
exitCode == 0
}
def fixLibraryManifest(
packageRoot: File,
targetVersion: String,
log: Logger
): Unit = {
val packageConfig = packageRoot / "package.yaml"
val originalContent = IO.read(packageConfig)
yaml.parser.parse(originalContent) match {
case Left(error) =>
log.error(s"Failed to parse $packageConfig: $error")
throw error
case Right(parsed) =>
val obj = parsed.asObject.getOrElse {
throw new IllegalStateException(s"Incorrect format of $packageConfig")
}
val key = "version"
val updated = obj.remove(key).add(key, targetVersion.asJson)
val serialized = yaml.printer.print(updated.asJson)
if (serialized == originalContent) {
log.info(
s"No need to update $packageConfig, already in correct version."
)
} else {
IO.write(packageConfig, serialized)
log.debug(s"Updated $packageConfig to $targetVersion")
}
}
}
def copyLibraryCacheIncremental(
sourceRoot: File,
destinationRoot: File,
sourceVersion: String,
targetVersion: String,
cacheFactory: CacheStoreFactory,
log: Logger
): Unit = {
val existingLibraries =
collection.mutable.ArrayBuffer.empty[(String, String)]
for (prefix <- sourceRoot.list()) {
for (libName <- (sourceRoot / prefix).list()) {
val targetPackageRoot =
destinationRoot / prefix / libName / targetVersion
copyDirectoryIncremental(
source = sourceRoot / prefix / libName / sourceVersion,
destination = targetPackageRoot,
cache = cacheFactory.make(s"$prefix.$libName")
)
fixLibraryManifest(targetPackageRoot, targetVersion, log)
existingLibraries.append((prefix, libName))
}
}
val existingLibrariesSet = existingLibraries.toSet
for (prefix <- destinationRoot.list()) {
for (libName <- (destinationRoot / prefix).list()) {
if (!existingLibrariesSet.contains((prefix, libName))) {
log.info(
s"Removing a library $prefix.$libName from the distribution, " +
s"because it does not exist in the sources anymore."
)
}
}
}
}
private def buildEngineManifest(
template: File,
destination: File,
graalVersion: String,
javaVersion: String
): Unit = {
val base = IO.read(template)
val extensions =
s"""graal-vm-version: $graalVersion
|graal-java-version: $javaVersion
|""".stripMargin
IO.write(destination, base + extensions)
}
def createLauncherPackage(
distributionRoot: File,
cacheFactory: CacheStoreFactory
): Unit = {
copyDirectoryIncremental(
file("distribution/launcher/THIRD-PARTY"),
distributionRoot / "THIRD-PARTY",
cacheFactory.make("launcher-third-party")
)
copyFilesIncremental(
Seq(file(executableName("ensoup"))),
distributionRoot / "bin",
cacheFactory.make("launcher-exe")
)
IO.createDirectory(distributionRoot / "dist")
IO.createDirectory(distributionRoot / "runtime")
copyFilesIncremental(
Seq(
file("distribution/launcher/.enso.portable"),
file("distribution/launcher/README.md")
),
distributionRoot,
cacheFactory.make("launcher-rootfiles")
)
}
sealed trait OS {
def name: String
def hasSupportForSulong: Boolean
def executableName(base: String): String = base
def archiveExt: String = ".tar.gz"
def isUNIX: Boolean = true
def archs: Seq[Architecture]
}
object OS {
case object Linux extends OS {
override val name: String = "linux"
override val hasSupportForSulong: Boolean = true
override val archs = Seq(Architecture.X64)
}
trait MacOS extends OS {
override val name: String = "macos"
}
case object MacOSAmd extends MacOS {
override val hasSupportForSulong: Boolean = true
override val archs = Seq(Architecture.X64)
}
case object MacOSArm extends MacOS {
override val hasSupportForSulong: Boolean = true
override val archs = Seq(Architecture.AarchX64)
}
case object Windows extends OS {
override val name: String = "windows"
override val hasSupportForSulong: Boolean = false
override def executableName(base: String): String = base + ".exe"
override def archiveExt: String = ".zip"
override def isUNIX: Boolean = false
override val archs = Seq(Architecture.X64)
}
val platforms = Seq(Linux, MacOSArm, MacOSAmd, Windows)
def apply(name: String, arch: Option[String]): Option[OS] =
name.toLowerCase match {
case Linux.`name` => Some(Linux)
case MacOSAmd.`name` =>
arch match {
case Some(Architecture.X64.`name`) =>
Some(MacOSAmd)
case Some(Architecture.AarchX64.`name`) =>
Some(MacOSArm)
case _ =>
None
}
case MacOSArm.`name` => Some(MacOSArm)
case Windows.`name` => Some(Windows)
case _ => None
}
}
sealed trait Architecture {
def name: String
/** Name of the architecture for GraalVM releases
*/
def graalName: String
}
object Architecture {
case object X64 extends Architecture {
override val name: String = "amd64"
override def graalName: String = "x64"
}
case object AarchX64 extends Architecture {
override val name: String = "aarch64"
override def graalName: String = "x64"
}
}
/** A helper class that manages building distribution artifacts. */
class Builder(
ensoVersion: String,
graalVersion: String,
graalJavaVersion: String,
artifactRoot: File
) {
def artifactName(
component: String,
os: OS,
architecture: Architecture
): String =
s"enso-$component-$ensoVersion-${os.name}-${architecture.name}"
def graalInPackageName: String =
s"graalvm-ce-java$graalJavaVersion-$graalVersion"
private def extractZip(archive: File, root: File): Unit = {
IO.createDirectory(root)
val exitCode = Process(
Seq("unzip", "-q", archive.toPath.toAbsolutePath.normalize.toString),
cwd = Some(root)
).!
if (exitCode != 0) {
throw new RuntimeException(s"Cannot extract $archive.")
}
}
private def listZip(archive: File): Seq[File] = {
val suppressStdErr = ProcessLogger(_ => ())
val zipList = Process(
Seq("zip", "-l", archive.toPath.toAbsolutePath.normalize.toString)
)
zipList.lineStream(suppressStdErr).map(file)
}
private def extractTarGz(archive: File, root: File): Unit = {
IO.createDirectory(root)
val exitCode = Process(
Seq(
"tar",
"xf",
archive.toPath.toAbsolutePath.toString
),
cwd = Some(root)
).!
if (exitCode != 0) {
throw new RuntimeException(s"Cannot extract $archive.")
}
}
private def listTarGz(archive: File): Seq[File] = {
val suppressStdErr = ProcessLogger(_ => ())
val tarList =
Process(Seq("tar", "tf", archive.toPath.toAbsolutePath.toString))
tarList.lineStream(suppressStdErr).map(file)
}
private def extract(archive: File, root: File): Unit = {
if (archive.getName.endsWith("zip")) {
extractZip(archive, root)
} else {
extractTarGz(archive, root)
}
}
private def list(archive: File): Seq[File] = {
if (archive.getName.endsWith("zip")) {
listZip(archive)
} else {
listTarGz(archive)
}
}
private def graalArchive(os: OS, architecture: Architecture): File = {
val packageDir =
artifactRoot / s"graalvm-$graalVersion-${os.name}-${architecture.name}"
if (!packageDir.exists()) {
IO.createDirectory(packageDir)
}
val archiveName =
s"graalvm-${os.name}-${architecture.name}-$graalVersion-$graalJavaVersion"
packageDir / (archiveName + os.archiveExt)
}
private def downloadGraal(
log: ManagedLogger,
os: OS,
architecture: Architecture
): File = {
val archive = graalArchive(os, architecture)
if (!archive.exists()) {
log.info(
s"Downloading GraalVM $graalVersion Java $graalJavaVersion " +
s"for $os $architecture"
)
val graalUrl =
s"https://github.com/graalvm/graalvm-ce-builds/releases/download/" +
s"jdk-$graalJavaVersion/" +
s"graalvm-community-jdk-${graalJavaVersion}_${os.name}-" +
s"${architecture.graalName}_bin${os.archiveExt}"
val exitCode = (url(graalUrl) #> archive).!
if (exitCode != 0) {
throw new RuntimeException(s"Graal download from $graalUrl failed.")
}
}
archive
}
private def copyGraal(
os: OS,
architecture: Architecture,
runtimeDir: File
): Unit = {
val archive = graalArchive(os, architecture)
extract(archive, runtimeDir)
}
/** Prepare the GraalVM package.
*
* @param log the logger
* @param os the system type
* @param architecture the architecture type
* @return the path to the created GraalVM package
*/
def createGraalPackage(
log: ManagedLogger,
os: OS,
architecture: Architecture
): File = {
log.info("Building GraalVM distribution")
val archive = downloadGraal(log, os, architecture)
if (os.hasSupportForSulong) {
log.info("Building GraalVM distribution2")
val packageDir = archive.getParentFile
val archiveRootDir = list(archive).head.getTopDirectory.getName
val extractedGraalDir0 = packageDir / archiveRootDir
val graalRuntimeDir =
s"graalvm-ce-java${graalJavaVersion}-${graalVersion}"
val extractedGraalDir = packageDir / graalRuntimeDir
if (extractedGraalDir0.exists()) {
IO.delete(extractedGraalDir0)
}
if (extractedGraalDir.exists()) {
IO.delete(extractedGraalDir)
}
log.info(s"Extracting $archive to $packageDir")
extract(archive, packageDir)
if (extractedGraalDir0 != extractedGraalDir) {
log.info(s"Standardizing GraalVM directory name")
IO.move(extractedGraalDir0, extractedGraalDir)
}
log.info("Installing components")
gu(log, os, extractedGraalDir, "install", "python")
log.info(s"Re-creating $archive")
IO.delete(archive)
makeArchive(packageDir, graalRuntimeDir, archive)
log.info(s"Cleaning up $extractedGraalDir")
IO.delete(extractedGraalDir)
}
archive
}
/** Run the `gu` executable from the GraalVM distribution.
*
* @param log the logger
* @param os the system type
* @param graalDir the directory with a GraalVM distribution
* @param arguments the command arguments
* @return Stdout from the `gu` command.
*/
def gu(
log: ManagedLogger,
os: OS,
graalDir: File,
arguments: String*
): String = {
val shallowFile = graalDir / "bin" / "gu"
val deepFile = graalDir / "Contents" / "Home" / "bin" / "gu"
val executableFile = os match {
case OS.Linux =>
shallowFile
case _: OS.MacOS =>
if (deepFile.exists) {
deepFile
} else {
shallowFile
}
case OS.Windows =>
graalDir / "bin" / "gu.cmd"
}
val javaHomeFile = executableFile.getParentFile.getParentFile
val javaHome = javaHomeFile.toPath.toAbsolutePath
val command =
executableFile.toPath.toAbsolutePath.toString +: arguments
log.debug(
s"Running $command in $graalDir with JAVA_HOME=${javaHome.toString}"
)
try {
Process(
command,
Some(graalDir),
("JAVA_HOME", javaHome.toString),
("GRAALVM_HOME", javaHome.toString)
).!!
} catch {
case _: RuntimeException =>
throw new RuntimeException(
s"Failed to run '${command.mkString(" ")}'"
)
}
}
def copyEngine(os: OS, architecture: Architecture, distDir: File): Unit = {
val engine = builtArtifact("engine", os, architecture)
if (!engine.exists()) {
throw new IllegalStateException(
s"Cannot create bundle for $os / $architecture because corresponding " +
s"engine has not been built."
)
}
IO.copyDirectory(engine / s"enso-$ensoVersion", distDir / ensoVersion)
}
def makeExecutable(file: File): Unit = {
val ownerOnly = false
file.setExecutable(true, ownerOnly)
}
def fixLauncher(root: File, os: OS): Unit = {
makeExecutable(root / "enso" / "bin" / os.executableName("enso"))
IO.createDirectories(
Seq("dist", "config", "runtime").map(root / "enso" / _)
)
}
def makeArchive(root: File, rootDir: String, target: File): Unit = {
val exitCode = if (target.getName.endsWith("zip")) {
Process(
Seq(
"zip",
"-9",
"-q",
"-r",
target.toPath.toAbsolutePath.normalize.toString,
rootDir
),
cwd = Some(root)
).!
} else {
Process(
Seq(
"tar",
"--use-compress-program=gzip -9",
"-cf",
target.toPath.toAbsolutePath.normalize.toString,
rootDir
),
cwd = Some(root)
).!
}
if (exitCode != 0) {
throw new RuntimeException(s"Failed to create archive $target")
}
}
/** Path to an arbitrary built artifact. */
def builtArtifact(
component: String,
os: OS,
architecture: Architecture
): File = artifactRoot / artifactName(component, os, architecture)
/** Path to the artifact that is built on this local machine. */
def localArtifact(component: String): File = {
val os =
if (Platform.isWindows) OS.Windows
else if (Platform.isLinux) OS.Linux
else if (Platform.isMacOS) {
if (Platform.isAmd64) OS.MacOSAmd
else if (Platform.isArm64) OS.MacOSArm
else
throw new IllegalStateException(
"Unknown Arch: " + sys.props("os.arch")
)
} else
throw new IllegalStateException("Unknown OS: " + sys.props("os.name"))
artifactRoot / artifactName(component, os, os.archs.head)
}
/** Path to a built archive.
*
* These archives are built by [[makePackages]] and [[makeBundles]].
*/
def builtArchive(
component: String,
os: OS,
architecture: Architecture
): File =
artifactRoot / (artifactName(
component,
os,
architecture
) + os.archiveExt)
private def cleanDirectory(dir: File): Unit = {
for (f <- IO.listFiles(dir)) {
IO.delete(f)
}
}
/** Creates compressed and ready for release packages for the launcher and
* engine.
*
* A project manager package is not created, as we release only its bundle.
* See [[makeBundles]].
*
* It does not trigger any builds. Instead, it uses available artifacts
* placed in `artifactRoot`. These artifacts may be created using the
* `enso/build*Distribution` tasks or they may come from other workers (as
* is the case in the release CI where the artifacts are downloaded from
* other jobs).
*/
def makePackages = Command.command("makePackages") { state =>
val log = state.log
for {
os <- OS.platforms
arch <- os.archs
} {
val launcher = builtArtifact("launcher", os, arch)
if (launcher.exists()) {
fixLauncher(launcher, os)
val archive = builtArchive("launcher", os, arch)
makeArchive(launcher, "enso", archive)
log.info(s"Created $archive")
}
val engine = builtArtifact("engine", os, arch)
if (engine.exists()) {
if (os.isUNIX) {
makeExecutable(engine / s"enso-$ensoVersion" / "bin" / "enso")
}
val archive = builtArchive("engine", os, arch)
makeArchive(engine, s"enso-$ensoVersion", archive)
log.info(s"Created $archive")
}
}
state
}
/** Creates launcher and project-manager bundles that include the component
* itself, the engine and a Graal runtime.
*
* It will download the GraalVM runtime and cache it in `artifactRoot` so
* further invocations for the same version will not need to download it.
*
* It does not trigger any builds. Instead, it uses available artifacts
* placed in `artifactRoot`. These artifacts may be created using the
* `enso/build*Distribution` tasks or they may come from other workers (as
* is the case in the release CI where the artifacts are downloaded from
* other jobs).
*/
def makeBundles = Command.command("makeBundles") { state =>
val log = state.log
for {
os <- OS.platforms
arch <- os.archs
} {
val launcher = builtArtifact("launcher", os, arch)
if (launcher.exists()) {
fixLauncher(launcher, os)
copyEngine(os, arch, launcher / "enso" / "dist")
copyGraal(
os,
arch,
launcher / "enso" / "runtime" / s"graalvm-ce-java$graalJavaVersion-$graalVersion/"
)
val archive = builtArchive("bundle", os, arch)
makeArchive(launcher, "enso", archive)
cleanDirectory(launcher / "enso" / "dist")
cleanDirectory(launcher / "enso" / "runtime")
log.info(s"Created $archive")
}
val pm = builtArtifact("project-manager", os, arch)
if (pm.exists()) {
if (os.isUNIX) {
makeExecutable(pm / "enso" / "bin" / "project-manager")
}
copyEngine(os, arch, pm / "enso" / "dist")
copyGraal(
os,
arch,
pm / "enso" / "runtime" / s"graalvm-ce-java$graalJavaVersion-$graalVersion/"
)
IO.copyFile(
file("distribution/enso.bundle.template"),
pm / "enso" / ".enso.bundle"
)
val archive = builtArchive("project-manager", os, arch)
makeArchive(pm, "enso", archive)
cleanDirectory(pm / "enso" / "dist")
cleanDirectory(pm / "enso" / "runtime")
log.info(s"Created $archive")
}
}
state
}
}
}