Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
scala.cli.commands.package0.Package.scala Maven / Gradle / Ivy
package scala.cli.commands.package0
import ai.kien.python.Python
import caseapp.*
import caseapp.core.help.HelpFormat
import coursier.launcher.*
import dependency.*
import packager.config.*
import packager.deb.DebianPackage
import packager.docker.DockerPackage
import packager.mac.dmg.DmgPackage
import packager.mac.pkg.PkgPackage
import packager.rpm.RedHatPackage
import packager.windows.WindowsPackage
import java.io.{ByteArrayOutputStream, OutputStream}
import java.nio.charset.StandardCharsets
import java.nio.file.attribute.FileTime
import java.util.zip.{ZipEntry, ZipOutputStream}
import scala.build.EitherCps.{either, value}
import scala.build.Ops.*
import scala.build.*
import scala.build.errors.*
import scala.build.interactive.InteractiveFileOps
import scala.build.internal.Util.*
import scala.build.internal.resource.NativeResourceMapper
import scala.build.internal.{Runner, ScalaJsLinkerConfig}
import scala.build.options.PackageType.Native
import scala.build.options.{BuildOptions, JavaOpt, PackageType, Platform, ScalaNativeTarget}
import scala.cli.CurrentParams
import scala.cli.commands.OptionsHelper.*
import scala.cli.commands.doc.Doc
import scala.cli.commands.packaging.Spark
import scala.cli.commands.publish.ConfigUtil.*
import scala.cli.commands.run.Run.orPythonDetectionError
import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, MainClassOptions, SharedOptions}
import scala.cli.commands.util.BuildCommandHelpers
import scala.cli.commands.{CommandUtils, ScalaCommand, WatchUtil}
import scala.cli.config.{ConfigDb, Keys}
import scala.cli.errors.ScalaJsLinkingError
import scala.cli.internal.{CachedBinary, Constants, ProcUtil, ScalaJsLinker}
import scala.cli.packaging.{Library, NativeImage}
import scala.cli.util.ArgHelpers.*
import scala.cli.util.ConfigDbUtils
import scala.util.Properties
object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
override def name = "package"
override def group: String = HelpCommandGroup.Main.toString
val primaryHelpGroups: Seq[HelpGroup] = Seq(
HelpGroup.Package,
HelpGroup.Scala,
HelpGroup.Java,
HelpGroup.Debian,
HelpGroup.MacOS,
HelpGroup.RedHat,
HelpGroup.Windows,
HelpGroup.Docker,
HelpGroup.NativeImage
)
val hiddenHelpGroups: Seq[HelpGroup] = Seq(HelpGroup.Entrypoint, HelpGroup.Watch)
override def helpFormat: HelpFormat = super.helpFormat
.withHiddenGroups(hiddenHelpGroups)
.withPrimaryGroups(primaryHelpGroups)
override def sharedOptions(options: PackageOptions): Option[SharedOptions] = Some(options.shared)
override def scalaSpecificationLevel = SpecificationLevel.RESTRICTED
override def buildOptions(options: PackageOptions): Option[BuildOptions] =
Some(options.baseBuildOptions.orExit(options.shared.logger))
override def runCommand(options: PackageOptions, args: RemainingArgs, logger: Logger): Unit = {
val inputs = options.shared.inputs(args.remaining).orExit(logger)
CurrentParams.workspaceOpt = Some(inputs.workspace)
// FIXME mainClass encoding has issues with special chars, such as '-'
val initialBuildOptions = finalBuildOptions(options)
val threads = BuildThreads.create()
val compilerMaker = options.compilerMaker(threads).orExit(logger)
val docCompilerMakerOpt = options.docCompilerMakerOpt
val cross = options.compileCross.cross.getOrElse(false)
val configDb = ConfigDbUtils.configDb.orExit(logger)
val actionableDiagnostics =
options.shared.logging.verbosityOptions.actions.orElse(
configDb.get(Keys.actions).getOrElse(None)
)
if (options.watch.watchMode) {
var expectedModifyEpochSecondOpt = Option.empty[Long]
val watcher = Build.watch(
inputs,
initialBuildOptions,
compilerMaker,
docCompilerMakerOpt,
logger,
crossBuilds = cross,
buildTests = false,
partial = None,
actionableDiagnostics = actionableDiagnostics,
postAction = () => WatchUtil.printWatchMessage()
) { res =>
res.orReport(logger).map(_.main).foreach {
case s: Build.Successful =>
s.copyOutput(options.shared)
val mtimeDestPath = doPackage(
logger = logger,
outputOpt = options.output.filter(_.nonEmpty),
force = options.force,
forcedPackageTypeOpt = options.forcedPackageTypeOpt,
build = s,
extraArgs = args.unparsed,
expectedModifyEpochSecondOpt = expectedModifyEpochSecondOpt,
allowTerminate = !options.watch.watchMode,
mainClassOptions = options.mainClass
)
.orReport(logger)
for (valueOpt <- mtimeDestPath)
expectedModifyEpochSecondOpt = valueOpt
case _: Build.Failed =>
System.err.println("Compilation failed")
case _: Build.Cancelled =>
System.err.println("Build cancelled")
}
}
try WatchUtil.waitForCtrlC(() => watcher.schedule())
finally watcher.dispose()
}
else {
val builds =
Build.build(
inputs,
initialBuildOptions,
compilerMaker,
docCompilerMakerOpt,
logger,
crossBuilds = cross,
buildTests = false,
partial = None,
actionableDiagnostics = actionableDiagnostics
)
.orExit(logger)
builds.main match {
case s: Build.Successful =>
s.copyOutput(options.shared)
val res0 = doPackage(
logger = logger,
outputOpt = options.output.filter(_.nonEmpty),
force = options.force,
forcedPackageTypeOpt = options.forcedPackageTypeOpt,
build = s,
extraArgs = args.unparsed,
expectedModifyEpochSecondOpt = None,
allowTerminate = !options.watch.watchMode,
mainClassOptions = options.mainClass
)
res0.orExit(logger)
case _: Build.Failed =>
System.err.println("Compilation failed")
sys.exit(1)
case _: Build.Cancelled =>
System.err.println("Build cancelled")
sys.exit(1)
}
}
}
def finalBuildOptions(options: PackageOptions): BuildOptions = {
val initialOptions = options.finalBuildOptions.orExit(options.shared.logger)
val finalBuildOptions = initialOptions.copy(scalaOptions =
initialOptions.scalaOptions.copy(defaultScalaVersion = Some(defaultScalaVersion))
)
val buildOptions = finalBuildOptions.copy(
javaOptions = finalBuildOptions.javaOptions.copy(
javaOpts =
finalBuildOptions.javaOptions.javaOpts ++
options.java.allJavaOpts.map(JavaOpt(_)).map(Positioned.commandLine)
)
)
buildOptions
}
private def doPackage(
logger: Logger,
outputOpt: Option[String],
force: Boolean,
forcedPackageTypeOpt: Option[PackageType],
build: Build.Successful,
extraArgs: Seq[String],
expectedModifyEpochSecondOpt: Option[Long],
allowTerminate: Boolean,
mainClassOptions: MainClassOptions
): Either[BuildException, Option[Long]] = either {
if (mainClassOptions.mainClassLs.contains(true))
value {
mainClassOptions
.maybePrintMainClasses(build.foundMainClasses(), shouldExit = allowTerminate)
.map(_ => None)
}
else {
val packageType: PackageType = value(resolvePackageType(build, forcedPackageTypeOpt))
// TODO When possible, call alreadyExistsCheck() before compiling stuff
def extension = packageType match {
case PackageType.LibraryJar => ".jar"
case PackageType.SourceJar => ".jar"
case PackageType.DocJar => ".jar"
case _: PackageType.Assembly => ".jar"
case PackageType.Spark => ".jar"
case PackageType.Js => ".js"
case PackageType.Debian => ".deb"
case PackageType.Dmg => ".dmg"
case PackageType.Pkg => ".pkg"
case PackageType.Rpm => ".rpm"
case PackageType.Msi => ".msi"
case PackageType.Native.Application =>
if Properties.isWin then ".exe" else ""
case PackageType.Native.LibraryDynamic =>
if Properties.isWin then ".dll" else if Properties.isMac then ".dylib" else ".so"
case PackageType.Native.LibraryStatic =>
if Properties.isWin then ".lib" else ".a"
case PackageType.GraalVMNativeImage if Properties.isWin => ".exe"
case _ if Properties.isWin => ".bat"
case _ => ""
}
def defaultName = packageType match {
case PackageType.LibraryJar => "library.jar"
case PackageType.SourceJar => "source.jar"
case PackageType.DocJar => "scaladoc.jar"
case _: PackageType.Assembly => "app.jar"
case PackageType.Spark => "job.jar"
case PackageType.Js => "app.js"
case PackageType.Debian => "app.deb"
case PackageType.Dmg => "app.dmg"
case PackageType.Pkg => "app.pkg"
case PackageType.Rpm => "app.rpm"
case PackageType.Msi => "app.msi"
case PackageType.Native.Application =>
if Properties.isWin then "app.exe" else "app"
case PackageType.Native.LibraryDynamic =>
if Properties.isWin then "library.dll"
else if Properties.isMac then "library.dylib"
else "library.so"
case PackageType.Native.LibraryStatic =>
if Properties.isWin then "library.lib" else "library.a"
case PackageType.GraalVMNativeImage if Properties.isWin => "app.exe"
case _ if Properties.isWin => "app.bat"
case _ => "app"
}
val output = outputOpt.map {
case path
if packageType == PackageType.GraalVMNativeImage
&& Properties.isWin && !path.endsWith(".exe") =>
s"$path.exe" // graalvm-native-image requires .exe extension on Windows
case path => path
}
val packageOutput = build.options.notForBloopOptions.packageOptions.output
val dest = output.orElse(packageOutput)
.orElse {
build.sources.defaultMainClass
.map(n => n.drop(n.lastIndexOf('.') + 1))
.map(_.stripSuffix("_sc"))
.map(_ + extension)
}
.orElse(build.retainedMainClass(logger).map(
_.stripSuffix("_sc") + extension
).toOption)
.orElse(build.sources.paths.collectFirst(_._1.baseName + extension))
.getOrElse(defaultName)
val destPath = os.Path(dest, Os.pwd)
val printableDest = CommandUtils.printablePath(destPath)
def alreadyExistsCheck(): Either[BuildException, Unit] = {
val alreadyExists = !force &&
os.exists(destPath) &&
!expectedModifyEpochSecondOpt.contains(os.mtime(destPath))
if (alreadyExists)
build.options.interactive.map { interactive =>
InteractiveFileOps.erasingPath(interactive, printableDest, destPath) { () =>
val errorMsg =
if (expectedModifyEpochSecondOpt.isEmpty) s"$printableDest already exists"
else s"$printableDest was overwritten by another process"
System.err.println(s"Error: $errorMsg. Pass -f or --force to force erasing it.")
sys.exit(1)
}
}
else
Right(())
}
value(alreadyExistsCheck())
def mainClass: Either[BuildException, String] =
build.options.mainClass match {
case Some(cls) => Right(cls)
case None => build.retainedMainClass(logger)
}
def mainClassOpt: Option[String] =
build.options.mainClass.orElse {
build.retainedMainClassOpt(build.foundMainClasses(), logger)
}
val packageOptions = build.options.notForBloopOptions.packageOptions
val outputPath = packageType match {
case PackageType.Bootstrap =>
value(bootstrap(build, destPath, value(mainClass), () => alreadyExistsCheck(), logger))
destPath
case PackageType.LibraryJar =>
val libraryJar = Library.libraryJar(build)
value(alreadyExistsCheck())
if (force) os.copy.over(libraryJar, destPath, createFolders = true)
else os.copy(libraryJar, destPath, createFolders = true)
destPath
case PackageType.SourceJar =>
val now = System.currentTimeMillis()
val content = sourceJar(build, now)
value(alreadyExistsCheck())
if (force) os.write.over(destPath, content, createFolders = true)
else os.write(destPath, content, createFolders = true)
destPath
case PackageType.DocJar =>
val docJarPath = value(docJar(build, logger, extraArgs))
value(alreadyExistsCheck())
if (force) os.copy.over(docJarPath, destPath, createFolders = true)
else os.copy(docJarPath, destPath, createFolders = true)
destPath
case a: PackageType.Assembly =>
value {
assembly(
build,
destPath,
a.mainClassInManifest match {
case None =>
if (a.addPreamble) {
val clsName = value {
mainClass.left.map {
case e: NoMainClassFoundError =>
// This one has a slightly better error message, suggesting --preamble=false
new NoMainClassFoundForAssemblyError(e)
case e => e
}
}
Some(clsName)
}
else
mainClassOpt
case Some(false) => None
case Some(true) => Some(value(mainClass))
},
Nil,
withPreamble = a.addPreamble,
() => alreadyExistsCheck(),
logger
)
}
destPath
case PackageType.Spark =>
value {
assembly(
build,
destPath,
mainClassOpt,
// The Spark modules are assumed to be already on the class path,
// along with all their transitive dependencies (originating from
// the Spark distribution), so we don't include any of them in the
// assembly.
Spark.sparkModules,
withPreamble = false,
() => alreadyExistsCheck(),
logger
)
}
destPath
case PackageType.Js =>
value(buildJs(build, destPath, mainClassOpt, logger))
case tpe: PackageType.Native =>
import PackageType.Native.*
val mainClassO =
tpe match
case Application => Some(value(mainClass))
case _ => None
val cachedDest = value(buildNative(
build = build,
mainClass = mainClassO,
targetType = tpe,
destPath = Some(destPath),
logger = logger
))
if (force) os.copy.over(cachedDest, destPath, createFolders = true)
else os.copy(cachedDest, destPath, createFolders = true)
destPath
case PackageType.GraalVMNativeImage =>
NativeImage.buildNativeImage(
build,
value(mainClass),
destPath,
build.inputs.nativeImageWorkDir,
extraArgs,
logger
)
destPath
case nativePackagerType: PackageType.NativePackagerType =>
val bootstrapPath = os.temp.dir(prefix = "scala-packager") / "app"
value {
bootstrap(
build,
bootstrapPath,
value(mainClass),
() => alreadyExistsCheck(),
logger
)
}
val sharedSettings = SharedSettings(
sourceAppPath = bootstrapPath,
version = packageOptions.packageVersion,
force = force,
outputPath = destPath,
logoPath = packageOptions.logoPath,
launcherApp = packageOptions.launcherApp
)
lazy val debianSettings = DebianSettings(
shared = sharedSettings,
maintainer = packageOptions.maintainer.mandatory("--maintainer", "debian"),
description = packageOptions.description.mandatory("--description", "debian"),
debianConflicts = packageOptions.debianOptions.conflicts,
debianDependencies = packageOptions.debianOptions.dependencies,
architecture = packageOptions.debianOptions.architecture.mandatory(
"--deb-architecture",
"debian"
),
priority = packageOptions.debianOptions.priority,
section = packageOptions.debianOptions.section
)
lazy val macOSSettings = MacOSSettings(
shared = sharedSettings,
identifier =
packageOptions.macOSidentifier.mandatory("--identifier-parameter", "macOs")
)
lazy val redHatSettings = RedHatSettings(
shared = sharedSettings,
description = packageOptions.description.mandatory("--description", "redHat"),
license =
packageOptions.redHatOptions.license.mandatory("--license", "redHat"),
release =
packageOptions.redHatOptions.release.mandatory("--release", "redHat"),
rpmArchitecture = packageOptions.redHatOptions.architecture.mandatory(
"--rpm-architecture",
"redHat"
)
)
lazy val windowsSettings = WindowsSettings(
shared = sharedSettings,
maintainer = packageOptions.maintainer.mandatory("--maintainer", "windows"),
licencePath = packageOptions.windowsOptions.licensePath.mandatory(
"--licence-path",
"windows"
),
productName = packageOptions.windowsOptions.productName.mandatory(
"--product-name",
"windows"
),
exitDialog = packageOptions.windowsOptions.exitDialog,
suppressValidation =
packageOptions.windowsOptions.suppressValidation.getOrElse(false),
extraConfigs = packageOptions.windowsOptions.extraConfig,
is64Bits = packageOptions.windowsOptions.is64Bits.getOrElse(true),
installerVersion = packageOptions.windowsOptions.installerVersion,
wixUpgradeCodeGuid = packageOptions.windowsOptions.wixUpgradeCodeGuid
)
nativePackagerType match {
case PackageType.Debian =>
DebianPackage(debianSettings).build()
case PackageType.Dmg =>
DmgPackage(macOSSettings).build()
case PackageType.Pkg =>
PkgPackage(macOSSettings).build()
case PackageType.Rpm =>
RedHatPackage(redHatSettings).build()
case PackageType.Msi =>
WindowsPackage(windowsSettings).build()
}
destPath
case PackageType.Docker =>
value(docker(build, value(mainClass), logger))
destPath
}
val printableOutput = CommandUtils.printablePath(outputPath)
if (packageType.runnable.nonEmpty)
logger.message {
if (packageType.runnable.contains(true))
s"Wrote $outputPath, run it with" + System.lineSeparator() +
" " + printableOutput
else if (packageType == PackageType.Js)
s"Wrote $outputPath, run it with" + System.lineSeparator() +
" node " + printableOutput
else
s"Wrote $outputPath"
}
val mTimeDestPathOpt = if (packageType.runnable.isEmpty) None else Some(os.mtime(destPath))
mTimeDestPathOpt
}
// end of doPackage
}
def docJar(
build: Build.Successful,
logger: Logger,
extraArgs: Seq[String]
): Either[BuildException, os.Path] = either {
val workDir = build.inputs.docJarWorkDir
val dest = workDir / "doc.jar"
val cacheData =
CachedBinary.getCacheData(
build,
extraArgs.toList,
dest,
workDir
)
if (cacheData.changed) {
val contentDir = value(Doc.generateScaladocDirPath(build, logger, extraArgs))
var outputStream: OutputStream = null
try {
outputStream = os.write.outputStream(dest, createFolders = true)
Library.writeLibraryJarTo(
outputStream,
build,
hasActualManifest = false,
contentDirOverride = Some(contentDir)
)
}
finally
if (outputStream != null)
outputStream.close()
CachedBinary.updateProjectAndOutputSha(dest, workDir, cacheData.projectSha)
}
dest
}
private val generatedSourcesPrefix = os.rel / "META-INF" / "generated"
def sourceJar(build: Build.Successful, defaultLastModified: Long): Array[Byte] = {
val baos = new ByteArrayOutputStream
var zos: ZipOutputStream = null
def fromSimpleSources = build.sources.paths.iterator.map {
case (path, relPath) =>
val lastModified = os.mtime(path)
val content = os.read.bytes(path)
(relPath, content, lastModified)
}
def fromGeneratedSources = build.sources.inMemory.iterator.flatMap { inMemSource =>
val lastModified = inMemSource.originalPath match {
case Right((_, origPath)) => os.mtime(origPath)
case Left(_) => defaultLastModified
}
val originalOpt = inMemSource.originalPath.toOption.collect {
case (subPath, origPath) if subPath != inMemSource.generatedRelPath =>
val origContent = os.read.bytes(origPath)
(subPath, origContent, lastModified)
}
val prefix = if (originalOpt.isEmpty) os.rel else generatedSourcesPrefix
val generated = (
prefix / inMemSource.generatedRelPath,
inMemSource.content,
lastModified
)
Iterator(generated) ++ originalOpt.iterator
}
def paths = fromSimpleSources ++ fromGeneratedSources
try {
zos = new ZipOutputStream(baos)
for ((relPath, content, lastModified) <- paths) {
val name = relPath.toString
val ent = new ZipEntry(name)
ent.setLastModifiedTime(FileTime.fromMillis(lastModified))
ent.setSize(content.length)
zos.putNextEntry(ent)
zos.write(content)
zos.closeEntry()
}
}
finally if (zos != null) zos.close()
baos.toByteArray
}
private def docker(
build: Build.Successful,
mainClass: String,
logger: Logger
): Either[BuildException, Unit] = either {
val packageOptions = build.options.notForBloopOptions.packageOptions
if (build.options.platform.value == Platform.Native && (Properties.isMac || Properties.isWin)) {
System.err.println(
"Package scala native application to docker image is not supported on MacOs and Windows"
)
sys.exit(1)
}
val exec = packageOptions.dockerOptions.cmd.orElse {
build.options.platform.value match {
case Platform.JVM => Some("sh")
case Platform.JS => Some("node")
case Platform.Native => None
}
}
val from = packageOptions.dockerOptions.from.getOrElse {
build.options.platform.value match {
case Platform.JVM => "openjdk:17-slim"
case Platform.JS => "node"
case Platform.Native => "debian:stable-slim"
}
}
val repository = packageOptions.dockerOptions.imageRepository.mandatory(
"--docker-image-repository",
"docker"
)
val tag = packageOptions.dockerOptions.imageTag.getOrElse("latest")
val dockerSettings = DockerSettings(
from = from,
registry = packageOptions.dockerOptions.imageRegistry,
repository = repository,
tag = Some(tag),
exec = exec,
dockerExecutable = None
)
val appPath = os.temp.dir(prefix = "scala-cli-docker") / "app"
build.options.platform.value match {
case Platform.JVM => value(bootstrap(build, appPath, mainClass, () => Right(()), logger))
case Platform.JS => buildJs(build, appPath, Some(mainClass), logger)
case Platform.Native =>
val dest =
value(buildNative(
build = build,
mainClass = Some(mainClass),
targetType = PackageType.Native.Application,
destPath = None,
logger = logger
))
os.copy(dest, appPath)
}
logger.message(
"Started building docker image with your application, it might take some time"
)
DockerPackage(appPath, dockerSettings).build()
logger.message(
"Built docker image, run it with" + System.lineSeparator() +
s" docker run $repository:$tag"
)
}
private def buildJs(
build: Build.Successful,
destPath: os.Path,
mainClass: Option[String],
logger: Logger
): Either[BuildException, os.Path] = for {
isFullOpt <- build.options.scalaJsOptions.fullOpt
linkerConfig = build.options.scalaJsOptions.linkerConfig(logger)
linkResult <- linkJs(
build,
destPath,
mainClass,
addTestInitializer = false,
linkerConfig,
isFullOpt,
build.options.scalaJsOptions.noOpt.getOrElse(false),
logger
)
} yield linkResult
private def bootstrap(
build: Build.Successful,
destPath: os.Path,
mainClass: String,
alreadyExistsCheck: () => Either[BuildException, Unit],
logger: Logger
): Either[BuildException, Unit] = either {
val byteCodeZipEntries = os.walk(build.output)
.filter(os.isFile(_))
.map { path =>
val name = path.relativeTo(build.output).toString
val content = os.read.bytes(path)
val lastModified = os.mtime(path)
val ent = new ZipEntry(name)
ent.setLastModifiedTime(FileTime.fromMillis(lastModified))
ent.setSize(content.length)
(ent, content)
}
// TODO Generate that in memory
val tmpJar = os.temp(prefix = destPath.last.stripSuffix(".jar"), suffix = ".jar")
val tmpJarParams = Parameters.Assembly()
.withExtraZipEntries(byteCodeZipEntries)
.withMainClass(mainClass)
AssemblyGenerator.generate(tmpJarParams, tmpJar.toNIO)
val tmpJarContent = os.read.bytes(tmpJar)
os.remove(tmpJar)
def dependencyEntries =
build.artifacts.artifacts.map {
case (url, path) =>
if (build.options.notForBloopOptions.packageOptions.isStandalone)
ClassPathEntry.Resource(path.last, os.mtime(path), os.read.bytes(path))
else
ClassPathEntry.Url(url)
}
val byteCodeEntry = ClassPathEntry.Resource(s"${destPath.last}-content.jar", 0L, tmpJarContent)
val extraClassPath = build.options.classPathOptions.extraClassPath.map { classPath =>
ClassPathEntry.Resource(classPath.last, os.mtime(classPath), os.read.bytes(classPath))
}
val allEntries = Seq(byteCodeEntry) ++ dependencyEntries ++ extraClassPath
val loaderContent = coursier.launcher.ClassLoaderContent(allEntries)
val preamble = Preamble()
.withOsKind(Properties.isWin)
.callsItself(Properties.isWin)
.withJavaOpts(build.options.javaOptions.javaOpts.toSeq.map(_.value.value))
val baseParams = Parameters.Bootstrap(Seq(loaderContent), mainClass)
.withDeterministic(true)
.withPreamble(preamble)
val params =
if (build.options.notForBloopOptions.doSetupPython.getOrElse(false)) {
val res = value {
Artifacts.fetchAnyDependencies(
Seq(Positioned.none(
dep"${Constants.pythonInterfaceOrg}:${Constants.pythonInterfaceName}:${Constants.pythonInterfaceVersion}"
)),
Nil,
None,
logger,
build.options.finalCache,
None,
Some(_)
)
}
val entries = res.artifacts.map {
case (a, f) =>
val path = os.Path(f)
if (build.options.notForBloopOptions.packageOptions.isStandalone)
ClassPathEntry.Resource(path.last, os.mtime(path), os.read.bytes(path))
else
ClassPathEntry.Url(a.url)
}
val pythonContent = Seq(
ClassLoaderContent(entries)
)
baseParams.addExtraContent("python", pythonContent).withPython(true)
}
else
baseParams
value(alreadyExistsCheck())
BootstrapGenerator.generate(params, destPath.toNIO)
ProcUtil.maybeUpdatePreamble(destPath)
}
/** Returns the dependency sub-graph of the provided modules, that is, all their JARs and their
* transitive dependencies' JARs.
*
* Note that this is not exactly the same as resolving those modules on their own (with their
* versions): other dependencies in the whole dependency sub-graph may bump versions in the
* provided dependencies sub-graph here.
*
* Here, among the JARs of the whole dependency graph, we pick the ones that were pulled by the
* provided modules, and might have been bumped by other modules. This is strictly a subset of
* the whole dependency graph.
*/
def providedFiles(
build: Build.Successful,
provided: Seq[dependency.AnyModule],
logger: Logger
): Either[BuildException, Seq[os.Path]] = either {
logger.debug(s"${provided.length} provided dependencies")
val res = build.artifacts.resolution.getOrElse {
sys.error("Internal error: expected resolution to have been kept")
}
val modules = value {
provided
.map(_.toCs(build.scalaParams))
.sequence
.left.map(CompositeBuildException(_))
}
val modulesSet = modules.toSet
val providedDeps = res
.dependencyArtifacts
.map(_._1)
.filter(dep => modulesSet.contains(dep.module))
val providedRes = res.subset(providedDeps)
val fileMap = build.artifacts.detailedRuntimeArtifacts
.map {
case (_, _, artifact, path) =>
artifact -> path
}
.toMap
val providedFiles = coursier.Artifacts.artifacts(providedRes, Set.empty, None, None, true)
.map(_._3)
.map { a =>
fileMap.getOrElse(a, sys.error(s"should not happen (missing: $a)"))
}
logger.debug {
val it = Iterator(s"${providedFiles.size} provided JAR(s)") ++
providedFiles.toVector.map(_.toString).sorted.iterator.map(f => s" $f")
it.mkString(System.lineSeparator())
}
providedFiles
}
def assembly(
build: Build.Successful,
destPath: os.Path,
mainClassOpt: Option[String],
extraProvided: Seq[dependency.AnyModule],
withPreamble: Boolean,
alreadyExistsCheck: () => Either[BuildException, Unit],
logger: Logger
): Either[BuildException, Unit] = either {
val byteCodeZipEntries = os.walk(build.output)
.filter(os.isFile(_))
.map { path =>
val name = path.relativeTo(build.output).toString
val content = os.read.bytes(path)
val lastModified = os.mtime(path)
val ent = new ZipEntry(name)
ent.setLastModifiedTime(FileTime.fromMillis(lastModified))
ent.setSize(content.length)
(ent, content)
}
val provided = build.options.notForBloopOptions.packageOptions.provided ++ extraProvided
val allFiles =
build.artifacts.runtimeArtifacts.map(_._2) ++ build.options.classPathOptions.extraClassPath
val files =
if (provided.isEmpty) allFiles
else {
val providedFilesSet = value(providedFiles(build, provided, logger)).toSet
allFiles.filterNot(providedFilesSet.contains)
}
val preambleOpt =
if (withPreamble)
Some {
Preamble()
.withOsKind(Properties.isWin)
.callsItself(Properties.isWin)
}
else
None
val params = Parameters.Assembly()
.withExtraZipEntries(byteCodeZipEntries)
.withFiles(files.map(_.toIO))
.withMainClass(mainClassOpt)
.withPreambleOpt(preambleOpt)
value(alreadyExistsCheck())
AssemblyGenerator.generate(params, destPath.toNIO)
ProcUtil.maybeUpdatePreamble(destPath)
}
final class NoMainClassFoundForAssemblyError(cause: NoMainClassFoundError) extends BuildException(
"No main class found for assembly. Either pass one with --main-class, or make the assembly non-runnable with --preamble=false",
cause = cause
)
def withSourceJar[T](
build: Build.Successful,
defaultLastModified: Long,
fileName: String = "library"
)(f: os.Path => T): T = {
val jarContent = sourceJar(build, defaultLastModified)
val jar = os.temp(jarContent, prefix = fileName.stripSuffix(".jar"), suffix = "-sources.jar")
try f(jar)
finally os.remove(jar)
}
private object LinkingDir {
case class Input(linkJsInput: ScalaJsLinker.LinkJSInput, scratchDirOpt: Option[os.Path])
private var currentInput: Option[Input] = None
private var currentLinkingDir: Option[os.Path] = None
def getOrCreate(
linkJsInput: ScalaJsLinker.LinkJSInput,
scratchDirOpt: Option[os.Path]
): os.Path =
val input = Input(linkJsInput, scratchDirOpt)
currentLinkingDir match {
case Some(linkingDir) if currentInput.contains(input) =>
linkingDir
case _ =>
scratchDirOpt.foreach(os.makeDir.all(_))
currentLinkingDir.foreach(dir => os.remove.all(dir))
currentLinkingDir = None
val linkingDirectory = os.temp.dir(
dir = scratchDirOpt.orNull,
prefix = "scala-cli-js-linking",
deleteOnExit = scratchDirOpt.isEmpty
)
currentInput = Some(input)
currentLinkingDir = Some(linkingDirectory)
linkingDirectory
}
}
def linkJs(
build: Build.Successful,
dest: os.Path,
mainClassOpt: Option[String],
addTestInitializer: Boolean,
config: ScalaJsLinkerConfig,
fullOpt: Boolean,
noOpt: Boolean,
logger: Logger,
scratchDirOpt: Option[os.Path] = None
): Either[BuildException, os.Path] = {
val mainJar = Library.libraryJar(build)
val classPath = mainJar +: build.artifacts.classPath
val input = ScalaJsLinker.LinkJSInput(
options = build.options.notForBloopOptions.scalaJsLinkerOptions,
javaCommand =
build.options.javaHome().value.javaCommand, // FIXME Allow users to use another JVM here?
classPath = classPath,
mainClassOrNull = mainClassOpt.orNull,
addTestInitializer = addTestInitializer,
config = config,
fullOpt = fullOpt,
noOpt = noOpt,
scalaJsVersion = build.options.scalaJsOptions.finalVersion
)
val linkingDir = LinkingDir.getOrCreate(input, scratchDirOpt)
either {
value {
ScalaJsLinker.link(
input,
linkingDir,
logger,
build.options.finalCache,
build.options.archiveCache
)
}
val relMainJs = os.rel / "main.js"
val relSourceMapJs = os.rel / "main.js.map"
val mainJs = linkingDir / relMainJs
val sourceMapJs = linkingDir / relSourceMapJs
if (os.exists(mainJs))
if (
os.walk.stream(linkingDir)
.filter(_ != mainJs)
.filter(_ != sourceMapJs)
.headOption
.nonEmpty
) {
// copy linking dir to dest
os.copy(
linkingDir,
dest,
createFolders = true,
replaceExisting = true,
mergeFolders = true
)
logger.debug(
s"Scala.js linker generate multiple files for js multi-modules. Copy files to $dest directory."
)
dest / "main.js"
}
else {
os.copy(mainJs, dest, replaceExisting = true)
if (build.options.scalaJsOptions.emitSourceMaps && os.exists(sourceMapJs)) {
val sourceMapDest =
build.options.scalaJsOptions.sourceMapsDest.getOrElse(os.Path(s"$dest.map"))
val updatedMainJs = ScalaJsLinker.updateSourceMappingURL(dest)
os.write.over(dest, updatedMainJs)
os.copy(sourceMapJs, sourceMapDest, replaceExisting = true)
logger.message(s"Emitted js source maps to: $sourceMapDest")
}
dest
}
else {
val found = os.walk(linkingDir).map(_.relativeTo(linkingDir))
value(Left(new ScalaJsLinkingError(relMainJs, found)))
}
}
}
def buildNative(
build: Build.Successful,
mainClass: Option[String], // when building a static/dynamic library, we don't need a main class
targetType: PackageType.Native,
destPath: Option[os.Path],
logger: Logger
): Either[BuildException, os.Path] = either {
val dest = build.inputs.nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}"
val cliOptions =
build.options.scalaNativeOptions.configCliOptions(build.sources.resourceDirs.nonEmpty)
val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false)
val pythonLdFlags =
if (setupPython)
value {
val python = Python()
val flagsOrError = python.ldflags
logger.debug(s"Python ldflags: $flagsOrError")
flagsOrError.orPythonDetectionError
}
else
Nil
val pythonCliOptions = pythonLdFlags.flatMap(f => Seq("--linking-option", f)).toList
val libraryLinkingOptions: Seq[String] =
Option.when(targetType != PackageType.Native.Application) {
/* If we are building a library, we make sure to change the name
that the linker will put into the loading path - otherwise
the built library will depend on some internal path within .scala-build
*/
destPath.flatMap(_.lastOpt).toSeq.flatMap { filename =>
val linkerOption =
if Properties.isLinux then s"-Wl,-soname,$filename" else s"-Wl,-install_name,$filename"
Seq("--linking-option", linkerOption)
}
}.toSeq.flatten
import PackageType.Native.*
val allCliOptions = pythonCliOptions ++
cliOptions ++
libraryLinkingOptions ++
mainClass.toSeq.flatMap(m => Seq("--main", m))
val nativeWorkDir = build.inputs.nativeWorkDir
os.makeDir.all(nativeWorkDir)
val cacheData =
CachedBinary.getCacheData(
build,
allCliOptions,
dest,
nativeWorkDir
)
if (cacheData.changed) {
NativeResourceMapper.copyCFilesToScalaNativeDir(build, nativeWorkDir)
val mainJar = Library.libraryJar(build)
val classpath = mainJar.toString +: build.artifacts.classPath.map(_.toString)
val args =
allCliOptions ++
logger.scalaNativeCliInternalLoggerOptions ++
List[String](
"--outpath",
dest.toString(),
"--workdir",
nativeWorkDir.toString()
) ++ classpath
val scalaNativeCli = build.artifacts.scalaOpt
.getOrElse {
sys.error("Expected Scala artifacts to be fetched")
}
.scalaNativeCli
val exitCode =
Runner.runJvm(
build.options.javaHome().value.javaCommand,
build.options.javaOptions.javaOpts.toSeq.map(_.value.value),
scalaNativeCli,
"scala.scalanative.cli.ScalaNativeLd",
args,
logger
).waitFor()
if (exitCode == 0)
CachedBinary.updateProjectAndOutputSha(dest, nativeWorkDir, cacheData.projectSha)
else
throw new ScalaNativeBuildError
}
dest
}
def resolvePackageType(
build: Build.Successful,
forcedPackageTypeOpt: Option[PackageType]
): Either[BuildException, PackageType] = {
val basePackageTypeOpt = build.options.notForBloopOptions.packageOptions.packageTypeOpt
lazy val validPackageScalaJS =
Seq(PackageType.Js, PackageType.LibraryJar, PackageType.SourceJar, PackageType.DocJar)
lazy val validPackageScalaNative =
Seq(
PackageType.LibraryJar,
PackageType.SourceJar,
PackageType.DocJar,
PackageType.Native.Application,
PackageType.Native.LibraryDynamic,
PackageType.Native.LibraryStatic
)
forcedPackageTypeOpt -> build.options.platform.value match {
case (Some(forcedPackageType), _) => Right(forcedPackageType)
case (_, _) if build.options.notForBloopOptions.packageOptions.isDockerEnabled =>
basePackageTypeOpt match {
case Some(PackageType.Docker) | None => Right(PackageType.Docker)
case Some(packageType) => Left(new MalformedCliInputError(
s"Unsupported package type: $packageType for Docker."
))
}
case (_, Platform.JS) =>
val validatedPackageType =
for (basePackageType <- basePackageTypeOpt)
yield
if (validPackageScalaJS.contains(basePackageType)) Right(basePackageType)
else Left(new MalformedCliInputError(
s"Unsupported package type: $basePackageType for Scala.js."
))
validatedPackageType.getOrElse(Right(PackageType.Js))
case (_, Platform.Native) =>
val specificNativePackageType =
import ScalaNativeTarget.*
build.options.scalaNativeOptions.buildTargetStr.flatMap(fromString).map {
case Application => PackageType.Native.Application
case LibraryDynamic => PackageType.Native.LibraryDynamic
case LibraryStatic => PackageType.Native.LibraryStatic
}
val validatedPackageType =
for
basePackageType <- specificNativePackageType orElse basePackageTypeOpt
yield
if (validPackageScalaNative.contains(basePackageType)) Right(basePackageType)
else Left(new MalformedCliInputError(
s"Unsupported package type: $basePackageType for Scala Native."
))
validatedPackageType.getOrElse(Right(PackageType.Native.Application))
case _ => Right(basePackageTypeOpt.getOrElse(PackageType.Bootstrap))
}
}
}