
scala.build.Build.scala Maven / Gradle / Ivy
package scala.build
import ch.epfl.scala.bsp4j
import com.swoval.files.FileTreeViews.Observer
import com.swoval.files.{PathWatcher, PathWatchers}
import dependency.ScalaParameters
import java.io.File
import java.nio.file.FileSystemException
import java.util.concurrent.{ScheduledExecutorService, ScheduledFuture}
import scala.annotation.tailrec
import scala.build.EitherCps.{either, value}
import scala.build.Ops.*
import scala.build.compiler.{ScalaCompiler, ScalaCompilerMaker}
import scala.build.errors.*
import scala.build.input.VirtualScript.VirtualScriptNameRegex
import scala.build.input.*
import scala.build.internal.resource.ResourceMapper
import scala.build.internal.{Constants, MainClass, Name, Util}
import scala.build.options.ScalaVersionUtil.asVersion
import scala.build.options.*
import scala.build.options.validation.ValidationException
import scala.build.postprocessing.LineConversion.scalaLineToScLineShift
import scala.build.postprocessing.*
import scala.collection.mutable.ListBuffer
import scala.concurrent.duration.DurationInt
import scala.util.control.NonFatal
import scala.util.{Properties, Try}
trait Build {
def inputs: Inputs
def options: BuildOptions
def scope: Scope
def outputOpt: Option[os.Path]
def success: Boolean
def diagnostics: Option[Seq[(Either[String, os.Path], bsp4j.Diagnostic)]]
def successfulOpt: Option[Build.Successful]
}
object Build {
final case class Successful(
inputs: Inputs,
options: BuildOptions,
scalaParams: Option[ScalaParameters],
scope: Scope,
sources: Sources,
artifacts: Artifacts,
project: Project,
output: os.Path,
diagnostics: Option[Seq[(Either[String, os.Path], bsp4j.Diagnostic)]],
generatedSources: Seq[GeneratedSource],
isPartial: Boolean
) extends Build {
def success: Boolean = true
def successfulOpt: Some[this.type] = Some(this)
def outputOpt: Some[os.Path] = Some(output)
def dependencyClassPath: Seq[os.Path] = sources.resourceDirs ++ artifacts.classPath
def fullClassPath: Seq[os.Path] = Seq(output) ++ dependencyClassPath
def foundMainClasses(): Seq[String] =
MainClass.find(output).sorted ++
options.classPathOptions.extraClassPath.flatMap(MainClass.find).sorted
def retainedMainClass(
mainClasses: Seq[String],
commandString: String,
logger: Logger
): Either[BuildException, String] = {
val defaultMainClassOpt = sources.defaultMainClass
.filter(name => mainClasses.contains(name))
def foundMainClass: Either[BuildException, String] =
mainClasses match {
case Seq() => Left(new NoMainClassFoundError)
case Seq(mainClass) => Right(mainClass)
case _ =>
inferredMainClass(mainClasses, logger)
.left.flatMap { mainClasses =>
// decode the names to present them to the user,
// but keep the link to each original name to account for package prefixes:
// "pack.Main$minus1" decodes to "pack.Main-1", which encodes back to "pack$u002EMain$minus1"
// ^^^^^^^^^^^^^^^^----------------NOT THE SAME-----------------------^^^^^^^^^^^^^^^^^^^^^
val decodedToEncoded = mainClasses.map(mc => Name.decoded(mc) -> mc).toMap
options.interactive.flatMap { interactive =>
interactive
.chooseOne(
"Found several main classes. Which would you like to run?",
decodedToEncoded.keys.toList
)
.map(decodedToEncoded(_)) // encode back the name of the chosen class
.toRight {
SeveralMainClassesFoundError(
::(mainClasses.head, mainClasses.tail.toList),
commandString,
Nil
)
}
}
}
}
defaultMainClassOpt match {
case Some(cls) => Right(cls)
case None => foundMainClass
}
}
private def inferredMainClass(
mainClasses: Seq[String],
logger: Logger
): Either[Seq[String], String] = {
val scriptInferredMainClasses =
sources.inMemory.collect {
case Sources.InMemory(_, _, _, Some(wrapperParams)) =>
wrapperParams.mainClass
}
val filteredMainClasses =
mainClasses.filter(!scriptInferredMainClasses.contains(_))
if (filteredMainClasses.length == 1) {
val pickedMainClass = filteredMainClasses.head
if (scriptInferredMainClasses.nonEmpty) {
val firstScript = scriptInferredMainClasses.head
val scriptsString = scriptInferredMainClasses.mkString(", ")
logger.message(
s"Running $pickedMainClass. Also detected script main classes: $scriptsString"
)
logger.message(
s"You can run any one of them by passing option --main-class, i.e. --main-class $firstScript"
)
logger.message(
"All available main classes can always be listed by passing option --list-main-classes"
)
}
Right(pickedMainClass)
}
else
Left(mainClasses)
}
def retainedMainClassOpt(
mainClasses: Seq[String],
logger: Logger
): Option[String] = {
val defaultMainClassOpt = sources.defaultMainClass
.filter(name => mainClasses.contains(name))
def foundMainClass =
mainClasses match {
case Seq() => None
case Seq(mainClass) => Some(mainClass)
case _ => inferredMainClass(mainClasses, logger).toOption
}
defaultMainClassOpt.orElse(foundMainClass)
}
def crossKey: CrossKey = {
val optKey = scalaParams.map { params =>
BuildOptions.CrossKey(
params.scalaVersion,
options.platform.value
)
}
CrossKey(optKey, scope)
}
}
final case class Failed(
inputs: Inputs,
options: BuildOptions,
scope: Scope,
sources: Sources,
artifacts: Artifacts,
project: Project,
diagnostics: Option[Seq[(Either[String, os.Path], bsp4j.Diagnostic)]]
) extends Build {
def success: Boolean = false
def successfulOpt: None.type = None
def outputOpt: None.type = None
}
final case class Cancelled(
inputs: Inputs,
options: BuildOptions,
scope: Scope,
reason: String
) extends Build {
def success: Boolean = false
def successfulOpt: None.type = None
def outputOpt: None.type = None
def diagnostics: None.type = None
}
/** If some options are manually overridden, append a hash of the options to the project name
* Using only the command-line options not the ones from the sources.
*/
def updateInputs(
inputs: Inputs,
options: BuildOptions,
testOptions: Option[BuildOptions] = None
): Inputs = {
// If some options are manually overridden, append a hash of the options to the project name
// Using options, not options0 - only the command-line options are taken into account. No hash is
// appended for options from the sources.
val optionsHash = options.hash
val testOptionsHash = testOptions.flatMap(_.hash)
inputs.copy(
baseProjectName =
inputs.baseProjectName
+ optionsHash.map("_" + _).getOrElse("")
+ testOptionsHash.map("_" + _).getOrElse("")
)
}
private def build(
inputs: Inputs,
options: BuildOptions,
logger: Logger,
buildClient: BloopBuildClient,
compiler: ScalaCompiler,
docCompilerOpt: Option[ScalaCompiler],
crossBuilds: Boolean,
buildTests: Boolean,
partial: Option[Boolean],
actionableDiagnostics: Option[Boolean]
)(using ScalaCliInvokeData): Either[BuildException, Builds] = either {
// allInputs contains elements from using directives
val (crossSources, allInputs) = value {
CrossSources.forInputs(
inputs,
Sources.defaultPreprocessors(
options.archiveCache,
options.internal.javaClassNameVersionOpt,
() => options.javaHome().value.javaCommand
),
logger,
options.suppressWarningOptions,
options.internal.exclude
)
}
val sharedOptions = crossSources.sharedOptions(options)
val crossOptions = sharedOptions.crossOptions
def doPostProcess(build: Build, inputs: Inputs, scope: Scope): Unit = build match {
case build: Build.Successful =>
for (sv <- build.project.scalaCompiler.map(_.scalaVersion))
postProcess(
build.generatedSources,
inputs.generatedSrcRoot(scope),
build.output,
logger,
inputs.workspace,
updateSemanticDbs = true,
scalaVersion = sv
).left.foreach(_.foreach(logger.message(_)))
case _ =>
}
final case class NonCrossBuilds(
main: Build,
testOpt: Option[Build],
docOpt: Option[Build],
testDocOpt: Option[Build]
)
def doBuild(
overrideOptions: BuildOptions
): Either[BuildException, NonCrossBuilds] = either {
val baseOptions = overrideOptions.orElse(sharedOptions)
val scopedSources = value(crossSources.scopedSources(baseOptions))
val mainSources =
value(scopedSources.sources(Scope.Main, baseOptions, allInputs.workspace, logger))
val mainOptions = mainSources.buildOptions
val testSources =
value(scopedSources.sources(Scope.Test, baseOptions, allInputs.workspace, logger))
val testOptions = testSources.buildOptions
val inputs0 = updateInputs(
allInputs,
mainOptions, // update hash in inputs with options coming from the CLI or cross-building, not from the sources
Some(testOptions).filter(_ != mainOptions)
)
def doBuildScope(
options: BuildOptions,
sources: Sources,
scope: Scope,
actualCompiler: ScalaCompiler = compiler
): Either[BuildException, Build] =
either {
val sources0 = sources.withVirtualDir(inputs0, scope, options)
val generatedSources = sources0.generateSources(inputs0.generatedSrcRoot(scope))
val res = build(
inputs0,
sources0,
generatedSources,
options,
scope,
logger,
buildClient,
actualCompiler,
buildTests,
partial,
actionableDiagnostics
)
value(res)
}
val mainBuild = value(doBuildScope(mainOptions, mainSources, Scope.Main))
val mainDocBuildOpt = docCompilerOpt match {
case None => None
case Some(docCompiler) =>
Some(value(doBuildScope(
mainOptions,
mainSources,
Scope.Main,
actualCompiler = docCompiler
)))
}
def testBuildOpt(doc: Boolean = false): Either[BuildException, Option[Build]] = either {
if (buildTests) {
val actualCompilerOpt =
if (doc) docCompilerOpt
else Some(compiler)
actualCompilerOpt match {
case None => None
case Some(actualCompiler) =>
val testBuild = value {
mainBuild match {
case s: Build.Successful =>
val extraTestOptions = BuildOptions(
classPathOptions = ClassPathOptions(
extraClassPath = Seq(s.output)
)
)
val testOptions0 = extraTestOptions.orElse(testOptions)
doBuildScope(
testOptions0,
testSources,
Scope.Test,
actualCompiler = actualCompiler
)
case _ =>
Right(Build.Cancelled(
inputs,
sharedOptions,
Scope.Test,
"Parent build failed or cancelled"
))
}
}
Some(testBuild)
}
}
else None
}
val testBuildOpt0 = value(testBuildOpt())
doPostProcess(mainBuild, inputs0, Scope.Main)
for (testBuild <- testBuildOpt0)
doPostProcess(testBuild, inputs0, Scope.Test)
val docTestBuildOpt0 = value(testBuildOpt(doc = true))
NonCrossBuilds(mainBuild, testBuildOpt0, mainDocBuildOpt, docTestBuildOpt0)
}
def buildScopes(): Either[BuildException, Builds] =
either {
val nonCrossBuilds = value(doBuild(BuildOptions()))
val (extraMainBuilds, extraTestBuilds, extraDocBuilds, extraDocTestBuilds) =
if (crossBuilds) {
val extraBuilds = value {
val maybeBuilds = crossOptions.map(doBuild)
maybeBuilds
.sequence
.left.map(CompositeBuildException(_))
}
(
extraBuilds.map(_.main),
extraBuilds.flatMap(_.testOpt),
extraBuilds.flatMap(_.docOpt),
extraBuilds.flatMap(_.testDocOpt)
)
}
else
(Nil, Nil, Nil, Nil)
Builds(
Seq(nonCrossBuilds.main) ++ nonCrossBuilds.testOpt.toSeq,
Seq(extraMainBuilds, extraTestBuilds),
nonCrossBuilds.docOpt.toSeq ++ nonCrossBuilds.testDocOpt.toSeq,
Seq(extraDocBuilds, extraDocTestBuilds)
)
}
val builds = value(buildScopes())
ResourceMapper.copyResourceToClassesDir(builds.main)
for (testBuild <- builds.get(Scope.Test))
ResourceMapper.copyResourceToClassesDir(testBuild)
if (actionableDiagnostics.getOrElse(true)) {
val projectOptions = builds.get(Scope.Test).getOrElse(builds.main).options
projectOptions.logActionableDiagnostics(logger)
}
builds
}
private def build(
inputs: Inputs,
sources: Sources,
generatedSources: Seq[GeneratedSource],
options: BuildOptions,
scope: Scope,
logger: Logger,
buildClient: BloopBuildClient,
compiler: ScalaCompiler,
buildTests: Boolean,
partial: Option[Boolean],
actionableDiagnostics: Option[Boolean]
)(using ScalaCliInvokeData): Either[BuildException, Build] = either {
val build0 = value {
buildOnce(
inputs,
sources,
generatedSources,
options,
scope,
logger,
buildClient,
compiler,
partial
)
}
build0 match {
case successful: Successful =>
if (options.jmhOptions.runJmh.getOrElse(false) && scope == Scope.Main)
value {
val res = jmhBuild(
inputs,
successful,
logger,
successful.options.javaHome().value.javaCommand,
buildClient,
compiler,
buildTests,
actionableDiagnostics = actionableDiagnostics
)
res.flatMap {
case Some(b) => Right(b)
case None => Left(new JmhBuildFailedError)
}
}
else
build0
case _ => build0
}
}
def projectRootDir(root: os.Path, projectName: String): os.Path =
root / Constants.workspaceDirName / projectName
def classesRootDir(root: os.Path, projectName: String): os.Path =
projectRootDir(root, projectName) / "classes"
def classesDir(root: os.Path, projectName: String, scope: Scope, suffix: String = ""): os.Path =
classesRootDir(root, projectName) / s"${scope.name}$suffix"
def resourcesRegistry(
root: os.Path,
projectName: String,
scope: Scope
): os.Path =
root / Constants.workspaceDirName / projectName / s"resources-${scope.name}"
def scalaNativeSupported(
options: BuildOptions,
inputs: Inputs,
logger: Logger
): Either[BuildException, Option[ScalaNativeCompatibilityError]] =
either {
val scalaParamsOpt = value(options.scalaParams)
scalaParamsOpt.flatMap { scalaParams =>
val scalaVersion = scalaParams.scalaVersion
val nativeVersionMaybe = options.scalaNativeOptions.numeralVersion
def snCompatError =
Left(
new ScalaNativeCompatibilityError(
scalaVersion,
options.scalaNativeOptions.finalVersion
)
)
def warnIncompatibleNativeOptions(numeralVersion: SNNumeralVersion) =
if (
numeralVersion < SNNumeralVersion(0, 4, 4)
&& options.scalaNativeOptions.embedResources.isDefined
)
logger.diagnostic(
"This Scala Version cannot embed resources, regardless of the options used."
)
val numeralOrError: Either[ScalaNativeCompatibilityError, SNNumeralVersion] =
nativeVersionMaybe match {
case Some(snNumeralVer) =>
if (snNumeralVer < SNNumeralVersion(0, 4, 1) && Properties.isWin)
snCompatError
else if (scalaVersion.startsWith("3.0"))
snCompatError
else if (scalaVersion.startsWith("3"))
if (snNumeralVer >= SNNumeralVersion(0, 4, 3)) Right(snNumeralVer)
else snCompatError
else if (scalaVersion.startsWith("2.13"))
Right(snNumeralVer)
else if (scalaVersion.startsWith("2.12"))
if (
inputs.sourceFiles().forall {
case _: AnyScript => snNumeralVer >= SNNumeralVersion(0, 4, 3)
case _ => true
}
) Right(snNumeralVer)
else snCompatError
else snCompatError
case None => snCompatError
}
numeralOrError match {
case Left(compatError) => Some(compatError)
case Right(snNumeralVersion) =>
warnIncompatibleNativeOptions(snNumeralVersion)
None
}
}
}
def build(
inputs: Inputs,
options: BuildOptions,
compilerMaker: ScalaCompilerMaker,
docCompilerMakerOpt: Option[ScalaCompilerMaker],
logger: Logger,
crossBuilds: Boolean,
buildTests: Boolean,
partial: Option[Boolean],
actionableDiagnostics: Option[Boolean]
)(using ScalaCliInvokeData): Either[BuildException, Builds] = {
val buildClient = BloopBuildClient.create(
logger,
keepDiagnostics = options.internal.keepDiagnostics
)
val classesDir0 = classesRootDir(inputs.workspace, inputs.projectName)
compilerMaker.withCompiler(
inputs.workspace / Constants.workspaceDirName,
classesDir0,
buildClient,
logger
) { compiler =>
docCompilerMakerOpt match {
case None =>
build(
inputs = inputs,
options = options,
logger = logger,
buildClient = buildClient,
compiler = compiler,
docCompilerOpt = None,
crossBuilds = crossBuilds,
buildTests = buildTests,
partial = partial,
actionableDiagnostics = actionableDiagnostics
)
case Some(docCompilerMaker) =>
docCompilerMaker.withCompiler(
inputs.workspace / Constants.workspaceDirName,
classesDir0, // ???
buildClient,
logger
) { docCompiler =>
build(
inputs = inputs,
options = options,
logger = logger,
buildClient = buildClient,
compiler = compiler,
docCompilerOpt = Some(docCompiler),
crossBuilds = crossBuilds,
buildTests = buildTests,
partial = partial,
actionableDiagnostics = actionableDiagnostics
)
}
}
}
}
def validate(
logger: Logger,
options: BuildOptions
): Either[BuildException, Unit] = {
val (errors, otherDiagnostics) = options.validate.partition(_.severity == Severity.Error)
logger.log(otherDiagnostics)
if (errors.nonEmpty)
Left(CompositeBuildException(errors.map(new ValidationException(_))))
else
Right(())
}
def watch(
inputs: Inputs,
options: BuildOptions,
compilerMaker: ScalaCompilerMaker,
docCompilerMakerOpt: Option[ScalaCompilerMaker],
logger: Logger,
crossBuilds: Boolean,
buildTests: Boolean,
partial: Option[Boolean],
actionableDiagnostics: Option[Boolean],
postAction: () => Unit = () => ()
)(action: Either[BuildException, Builds] => Unit)(using ScalaCliInvokeData): Watcher = {
val buildClient = BloopBuildClient.create(
logger,
keepDiagnostics = options.internal.keepDiagnostics
)
val threads = BuildThreads.create()
val classesDir0 = classesRootDir(inputs.workspace, inputs.projectName)
val compiler = compilerMaker.create(
inputs.workspace / Constants.workspaceDirName,
classesDir0,
buildClient,
logger
)
val docCompilerOpt = docCompilerMakerOpt.map(_.create(
inputs.workspace / Constants.workspaceDirName,
classesDir0,
buildClient,
logger
))
var res: Either[BuildException, Builds] = null
def run(): Unit = {
try {
res = build(
inputs,
options,
logger,
buildClient,
compiler,
docCompilerOpt,
crossBuilds = crossBuilds,
buildTests = buildTests,
partial = partial,
actionableDiagnostics = actionableDiagnostics
)
action(res)
}
catch {
case NonFatal(e) =>
Util.printException(e)
}
postAction()
}
run()
val watcher = new Watcher(ListBuffer(), threads.fileWatcher, run(), compiler.shutdown())
def doWatch(): Unit = {
val elements: Seq[Element] =
if (res == null) inputs.elements
else
res
.map { builds =>
val mainElems = builds.main.inputs.elements
val testElems = builds.get(Scope.Test).map(_.inputs.elements).getOrElse(Nil)
(mainElems ++ testElems).distinct
}
.getOrElse(inputs.elements)
for (elem <- elements) {
val depth = elem match {
case _: SingleFile => -1
case _ => Int.MaxValue
}
val eventFilter: PathWatchers.Event => Boolean = elem match {
case d: Directory =>
// Filtering event for directories, to ignore those related to the .bloop directory in particular
event =>
val p = os.Path(event.getTypedPath.getPath.toAbsolutePath)
val relPath = p.relativeTo(d.path)
val isHidden = relPath.segments.exists(_.startsWith("."))
val pathLast = relPath.lastOpt.orElse(p.lastOpt).getOrElse("")
def isScalaFile = pathLast.endsWith(".sc") || pathLast.endsWith(".scala")
def isJavaFile = pathLast.endsWith(".java")
!isHidden && (isScalaFile || isJavaFile)
case _ => _ => true
}
val watcher0 = watcher.newWatcher()
elem match {
case d: OnDisk =>
watcher0.register(d.path.toNIO, depth)
case _: Virtual =>
}
watcher0.addObserver {
onChangeBufferedObserver { event =>
if (eventFilter(event))
watcher.schedule()
}
}
}
val artifacts = res
.map { builds =>
def artifacts(build: Build): Seq[os.Path] =
build.successfulOpt.toSeq.flatMap(_.artifacts.classPath)
val main = artifacts(builds.main)
val test = builds.get(Scope.Test).map(artifacts).getOrElse(Nil)
val allScopesArtifacts = (main ++ test).distinct
allScopesArtifacts
.filterNot(_.segments.contains(Constants.workspaceDirName))
}
.getOrElse(Nil)
for (artifact <- artifacts) {
val depth = if (os.isFile(artifact)) -1 else Int.MaxValue
val watcher0 = watcher.newWatcher()
watcher0.register(artifact.toNIO, depth)
watcher0.addObserver {
onChangeBufferedObserver { _ =>
watcher.schedule()
}
}
}
}
try doWatch()
catch {
case NonFatal(e) =>
watcher.dispose()
throw e
}
watcher
}
def releaseFlag(
options: BuildOptions,
compilerJvmVersionOpt: Option[Positioned[Int]],
logger: Logger
): Option[Int] = {
lazy val javaHome = options.javaHome()
if (compilerJvmVersionOpt.exists(javaHome.value.version > _.value)) {
logger.log(List(Diagnostic(
Diagnostic.Messages.bloopTooOld,
Severity.Warning,
javaHome.positions ++ compilerJvmVersionOpt.map(_.positions).getOrElse(Nil)
)))
None
}
else if (compilerJvmVersionOpt.exists(_.value == 8))
None
else if (
options.scalaOptions.scalacOptions.values.exists(opt =>
opt.headOption.exists(_.value.value.startsWith("-release")) ||
opt.headOption.exists(_.value.value.startsWith("-java-output-version"))
)
)
None
else if (compilerJvmVersionOpt.isEmpty && javaHome.value.version == 8)
None
else
Some(javaHome.value.version)
}
/** Builds a Bloop project.
*
* @param inputs
* inputs to be included in the project
* @param sources
* sources to be included in the project
* @param generatedSources
* sources generated by Scala CLI as part of the build
* @param options
* build options
* @param compilerJvmVersionOpt
* compiler JVM version (optional)
* @param scope
* build scope for which the project is to be created
* @param logger
* logger
* @param maybeRecoverOnError
* a function handling [[BuildException]] instances, possibly recovering them; returns None on
* recovery, Some(e: BuildException) otherwise
* @return
* a bloop [[Project]]
*/
def buildProject(
inputs: Inputs,
sources: Sources,
generatedSources: Seq[GeneratedSource],
options: BuildOptions,
compilerJvmVersionOpt: Option[Positioned[Int]],
scope: Scope,
logger: Logger,
artifacts: Artifacts,
maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e)
): Either[BuildException, Project] = either {
val allSources = sources.paths.map(_._1) ++ generatedSources.map(_.generated)
val classesDir0 = classesDir(inputs.workspace, inputs.projectName, scope)
val scaladocDir = classesDir(inputs.workspace, inputs.projectName, scope, suffix = "-doc")
val generateSemanticDbs = options.scalaOptions.generateSemanticDbs.getOrElse(false)
val releaseFlagVersion = releaseFlag(options, compilerJvmVersionOpt, logger).map(_.toString)
val scalaCompilerParamsOpt = artifacts.scalaOpt match {
case Some(scalaArtifacts) =>
val params = value(options.scalaParams).getOrElse {
sys.error(
"Should not happen (inconsistency between Scala parameters in BuildOptions and ScalaArtifacts)"
)
}
val pluginScalacOptions = scalaArtifacts.compilerPlugins.distinct.map {
case (_, _, path) =>
ScalacOpt(s"-Xplugin:$path")
}
val semanticDbScalacOptions =
if (generateSemanticDbs)
if (params.scalaVersion.startsWith("2."))
Seq(
"-Yrangepos",
"-P:semanticdb:failures:warning",
"-P:semanticdb:synthetics:on",
s"-P:semanticdb:sourceroot:${inputs.workspace}"
).map(ScalacOpt(_))
else
Seq(
"-Xsemanticdb",
"-sourceroot",
inputs.workspace.toString
).map(ScalacOpt(_))
else Nil
val sourceRootScalacOptions =
if (params.scalaVersion.startsWith("2.")) Nil
else Seq("-sourceroot", inputs.workspace.toString).map(ScalacOpt(_))
val scalaJsScalacOptions =
if (options.platform.value == Platform.JS && !params.scalaVersion.startsWith("2."))
Seq(ScalacOpt("-scalajs"))
else Nil
val scalacReleaseV =
// the -release flag is not supported for Scala 2.12.x < 2.12.5
if params.scalaVersion.asVersion < "2.12.5".asVersion then Nil
else
releaseFlagVersion
.map(v => List("-release", v).map(ScalacOpt(_)))
.getOrElse(Nil)
val scalapyOptions =
if (
params.scalaVersion.startsWith("2.13.") &&
options.notForBloopOptions.python.getOrElse(false)
)
Seq(ScalacOpt("-Yimports:java.lang,scala,scala.Predef,me.shadaj.scalapy"))
else Nil
val scalacOptions =
options.scalaOptions.scalacOptions.map(_.value) ++
pluginScalacOptions ++
semanticDbScalacOptions ++
sourceRootScalacOptions ++
scalaJsScalacOptions ++
scalacReleaseV ++
scalapyOptions
val compilerParams = ScalaCompilerParams(
scalaVersion = params.scalaVersion,
scalaBinaryVersion = params.scalaBinaryVersion,
scalacOptions = scalacOptions.toSeq.map(_.value),
compilerClassPath = scalaArtifacts.compilerClassPath
)
Some(compilerParams)
case None =>
None
}
val javacOptions = {
val semanticDbJavacOptions =
// FIXME Should this be in scalaOptions, now that we use it for javac stuff too?
if (generateSemanticDbs) {
// from https://github.com/scalameta/metals/blob/04405c0401121b372ea1971c361e05108fb36193/metals/src/main/scala/scala/meta/internal/metals/JavaInteractiveSemanticdb.scala#L137-L146
val compilerPackages = Seq(
"com.sun.tools.javac.api",
"com.sun.tools.javac.code",
"com.sun.tools.javac.model",
"com.sun.tools.javac.tree",
"com.sun.tools.javac.util"
)
val exports = compilerPackages.flatMap { pkg =>
Seq("-J--add-exports", s"-Jjdk.compiler/$pkg=ALL-UNNAMED")
}
Seq(
// does the path need to be escaped somehow?
s"-Xplugin:semanticdb -sourceroot:${inputs.workspace} -targetroot:javac-classes-directory"
) ++ exports
}
else
Nil
val javacReleaseV = releaseFlagVersion.map(v => List("--release", v)).getOrElse(Nil)
javacReleaseV ++ semanticDbJavacOptions ++ options.javaOptions.javacOptions.map(_.value)
}
// `test` scope should contains class path to main scope
val mainClassesPath =
if (scope == Scope.Test)
List(classesDir(inputs.workspace, inputs.projectName, Scope.Main))
else Nil
value(validate(logger, options))
val fullClassPath = artifacts.compileClassPath ++
mainClassesPath ++
artifacts.javacPluginDependencies.map(_._3) ++
artifacts.extraJavacPlugins
val project = Project(
directory = inputs.workspace / Constants.workspaceDirName,
argsFilePath =
projectRootDir(inputs.workspace, inputs.projectName) / Constants.scalacArgumentsFileName,
workspace = inputs.workspace,
classesDir = classesDir0,
scaladocDir = scaladocDir,
scalaCompiler = scalaCompilerParamsOpt,
scalaJsOptions =
if (options.platform.value == Platform.JS)
Some(value(options.scalaJsOptions.config(logger)))
else None,
scalaNativeOptions =
if (options.platform.value == Platform.Native)
Some(options.scalaNativeOptions.bloopConfig())
else None,
projectName = inputs.scopeProjectName(scope),
classPath = fullClassPath,
resolution = Some(Project.resolution(artifacts.detailedArtifacts)),
sources = allSources,
resourceDirs = sources.resourceDirs,
scope = scope,
javaHomeOpt = Option(options.javaHomeLocation().value),
javacOptions = javacOptions
)
project
}
def prepareBuild(
inputs: Inputs,
sources: Sources,
generatedSources: Seq[GeneratedSource],
options: BuildOptions,
compilerJvmVersionOpt: Option[Positioned[Int]],
scope: Scope,
compiler: ScalaCompiler,
logger: Logger,
buildClient: BloopBuildClient,
maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e)
): Either[BuildException, (os.Path, Option[ScalaParameters], Artifacts, Project, Boolean)] =
either {
val options0 =
if (sources.hasJava && !sources.hasScala)
options.copy(
scalaOptions = options.scalaOptions.copy(
scalaVersion = options.scalaOptions.scalaVersion.orElse {
Some(MaybeScalaVersion.none)
}
)
)
else
options
val params = value(options0.scalaParams)
val scopeParams =
if (scope == Scope.Main) Nil
else Seq(scope.name)
buildClient.setProjectParams(scopeParams ++ value(options0.projectParams))
val classesDir0 = classesDir(inputs.workspace, inputs.projectName, scope)
val artifacts = value(options0.artifacts(logger, scope, maybeRecoverOnError))
value(validate(logger, options0))
val project = value {
buildProject(
inputs,
sources,
generatedSources,
options0,
compilerJvmVersionOpt,
scope,
logger,
artifacts,
maybeRecoverOnError
)
}
val projectChanged = compiler.prepareProject(project, logger)
if (projectChanged) {
if (compiler.usesClassDir && os.isDir(classesDir0)) {
logger.debug(s"Clearing $classesDir0")
os.list(classesDir0).foreach { p =>
logger.debug(s"Removing $p")
try os.remove.all(p)
catch {
case ex: FileSystemException =>
logger.debug(s"Ignoring $ex while cleaning up $p")
}
}
}
if (os.exists(project.argsFilePath)) {
logger.debug(s"Removing ${project.argsFilePath}")
try os.remove(project.argsFilePath)
catch {
case ex: FileSystemException =>
logger.debug(s"Ignoring $ex while cleaning up ${project.argsFilePath}")
}
}
}
(classesDir0, params, artifacts, project, projectChanged)
}
def buildOnce(
inputs: Inputs,
sources: Sources,
generatedSources: Seq[GeneratedSource],
options: BuildOptions,
scope: Scope,
logger: Logger,
buildClient: BloopBuildClient,
compiler: ScalaCompiler,
partialOpt: Option[Boolean]
): Either[BuildException, Build] = either {
if (options.platform.value == Platform.Native)
value(scalaNativeSupported(options, inputs, logger)) match {
case None =>
case Some(error) => value(Left(error))
}
val (classesDir0, scalaParams, artifacts, project, projectChanged) = value {
prepareBuild(
inputs,
sources,
generatedSources,
options,
compiler.jvmVersion,
scope,
compiler,
logger,
buildClient
)
}
buildClient.clear()
buildClient.setGeneratedSources(scope, generatedSources)
val partial = partialOpt.getOrElse {
options.notForBloopOptions.packageOptions.packageTypeOpt.exists(_.sourceBased)
}
val success = partial || compiler.compile(project, logger)
if (success)
Successful(
inputs,
options,
scalaParams,
scope,
sources,
artifacts,
project,
classesDir0,
buildClient.diagnostics,
generatedSources,
partial
)
else
Failed(
inputs,
options,
scope,
sources,
artifacts,
project,
buildClient.diagnostics
)
}
def postProcess(
generatedSources: Seq[GeneratedSource],
generatedSrcRoot: os.Path,
classesDir: os.Path,
logger: Logger,
workspace: os.Path,
updateSemanticDbs: Boolean,
scalaVersion: String
): Either[Seq[String], Unit] =
if (os.exists(classesDir)) {
// TODO Write classes to a separate directory during post-processing
logger.debug("Post-processing class files of pre-processed sources")
val mappings = generatedSources
.map { source =>
val relPath = source.generated.relativeTo(generatedSrcRoot).toString
val reportingPath = source.reportingPath.fold(s => s, _.last)
(relPath, (reportingPath, scalaLineToScLineShift(source.wrapperParamsOpt)))
}
.toMap
val postProcessors =
Seq(ByteCodePostProcessor) ++
(if (updateSemanticDbs) Seq(SemanticDbPostProcessor) else Nil) ++
Seq(TastyPostProcessor)
val failures = postProcessors.flatMap(
_.postProcess(generatedSources, mappings, workspace, classesDir, logger, scalaVersion)
.fold(e => Seq(e), _ => Nil)
)
if (failures.isEmpty) Right(()) else Left(failures)
}
else
Right(())
def onChangeBufferedObserver(onEvent: PathWatchers.Event => Unit): Observer[PathWatchers.Event] =
new Observer[PathWatchers.Event] {
def onError(t: Throwable): Unit = {
// TODO Log that properly
System.err.println("got error:")
@tailrec
def printEx(t: Throwable): Unit =
if (t != null) {
System.err.println(t)
System.err.println(
t.getStackTrace.iterator.map(" " + _ + System.lineSeparator()).mkString
)
printEx(t.getCause)
}
printEx(t)
}
def onNext(event: PathWatchers.Event): Unit =
onEvent(event)
}
final class Watcher(
val watchers: ListBuffer[PathWatcher[PathWatchers.Event]],
val scheduler: ScheduledExecutorService,
onChange: => Unit,
onDispose: => Unit
) {
def newWatcher(): PathWatcher[PathWatchers.Event] = {
val w = PathWatchers.get(true)
watchers += w
w
}
def dispose(): Unit = {
onDispose
watchers.foreach(_.close())
scheduler.shutdown()
}
private val lock = new Object
private var f: ScheduledFuture[?] = _
private val waitFor = 50.millis
private val runnable: Runnable = { () =>
lock.synchronized {
f = null
}
onChange // FIXME Log exceptions
}
def schedule(): Unit =
if (f == null)
lock.synchronized {
if (f == null)
f = scheduler.schedule(runnable, waitFor.length, waitFor.unit)
}
}
private def printable(path: os.Path): String =
if (path.startsWith(os.pwd)) path.relativeTo(os.pwd).toString
else path.toString
private def jmhBuild(
inputs: Inputs,
build: Build.Successful,
logger: Logger,
javaCommand: String,
buildClient: BloopBuildClient,
compiler: ScalaCompiler,
buildTests: Boolean,
actionableDiagnostics: Option[Boolean]
)(using ScalaCliInvokeData): Either[BuildException, Option[Build]] = either {
val jmhProjectName = inputs.projectName + "_jmh"
val jmhOutputDir = inputs.workspace / Constants.workspaceDirName / jmhProjectName
os.remove.all(jmhOutputDir)
val jmhSourceDir = jmhOutputDir / "sources"
val jmhResourceDir = jmhOutputDir / "resources"
val retCode = run(
javaCommand,
build.fullClassPath.map(_.toIO),
"org.openjdk.jmh.generators.bytecode.JmhBytecodeGenerator",
Seq(printable(build.output), printable(jmhSourceDir), printable(jmhResourceDir), "default"),
logger
)
if (retCode != 0) {
val red = Console.RED
val lightRed = "\u001b[91m"
val reset = Console.RESET
System.err.println(
s"${red}jmh bytecode generator exited with return code $lightRed$retCode$red.$reset"
)
}
if (retCode == 0) {
val jmhInputs = inputs.copy(
baseProjectName = jmhProjectName,
// hash of the underlying project if needed is already in jmhProjectName
mayAppendHash = false,
elements = inputs.elements ++ Seq(
Directory(jmhSourceDir),
ResourceDirectory(jmhResourceDir)
)
)
val updatedOptions = build.options.copy(
jmhOptions = build.options.jmhOptions.copy(
runJmh = build.options.jmhOptions.runJmh.map(_ => false)
)
)
val jmhBuilds = value {
Build.build(
jmhInputs,
updatedOptions,
logger,
buildClient,
compiler,
None,
crossBuilds = false,
buildTests = buildTests,
partial = None,
actionableDiagnostics = actionableDiagnostics
)
}
Some(jmhBuilds.main)
}
else None
}
private def run(
javaCommand: String,
classPath: Seq[File],
mainClass: String,
args: Seq[String],
logger: Logger
): Int = {
val command =
Seq(javaCommand) ++
Seq(
"-cp",
classPath.iterator.map(_.getAbsolutePath).mkString(File.pathSeparator),
mainClass
) ++
args
logger.log(
s"Running ${command.mkString(" ")}",
" Running" + System.lineSeparator() +
command.iterator.map(_ + System.lineSeparator()).mkString
)
new ProcessBuilder(command*)
.inheritIO()
.start()
.waitFor()
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy