Skip to content

Commit

Permalink
split dependencies from classpath
Browse files Browse the repository at this point in the history
  • Loading branch information
bishabosha committed Apr 14, 2023
1 parent 8ce459c commit 5727991
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ object ScalaCommand:
case Repl => "repl" :: rest
case Clean => "clean" :: Nil

def makeArgs(module: Module, subcommand: CommandReader, classpath: List[String], platform: PlatformKind, extraArgs: Shellable*)(using Settings): List[os.Shellable] =
def makeArgs(module: Module, subcommand: CommandReader, classpath: List[String], dependencies: List[String], platform: PlatformKind, extraArgs: Shellable*)(using Settings): List[os.Shellable] =
val workspace = os.pwd / os.RelPath(module.root)
extension (platform: PlatformKind) def asFlags: List[String] = platform match
case PlatformKind.jvm => Nil
Expand All @@ -44,6 +44,7 @@ object ScalaCommand:
subcommand.commandString(module)(
List(
(if classpath.nonEmpty then "--classpath" :: classpath.mkString(":") :: Nil else Nil),
(dependencies.map(dep => "--dependency" :: dep :: Nil).flatten),
platform.asFlags,
"--workspace", workspace,
extraArgs,
Expand Down
11 changes: 11 additions & 0 deletions modules/scala-builder/src/main/scala/builder/errors/optional.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package builder.errors

import scala.util.boundary, boundary.break, boundary.Label

object optional:

type CanAbortNone = Label[None.type]

inline def apply[T](inline body: CanAbortNone ?=> T): Option[T] =
boundary:
Some(body)
18 changes: 16 additions & 2 deletions modules/scala-builder/src/main/scala/builder/errors/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@ package builder.errors

import scala.util.boundary, boundary.{break, Label}

import optional.*

type CanError[E] = Label[Result.Failure[E]]
type CanAbort[T, E] = Label[Result[T, E]]

inline def failure[E](msg: E)(using inline canError: CanError[E]): Nothing =
break[Result.Failure[E]](Result.Failure(msg))


extension [A, B, E](inline f: A => Result[B, E])
inline def ?(using inline canError: CanError[E]): A => B = f(_).?
inline def ?(using inline canError: CanError[E]): A => B = f(_).?

extension [A, B](inline f: A => Option[B])
inline def ?(using inline canAbortNone: CanAbortNone): A => B = f(_).?

extension [T](opt: Option[T])
inline def ?(using inline canAbortNone: CanAbortNone): T =
opt match
case Some(value) => value
case None => break(None)
inline def asSuccess[E](onError: => E): Result[T, E] =
opt match
case Some(value) => Result.Success(value)
case None => Result.Failure(onError)
40 changes: 34 additions & 6 deletions modules/scala-builder/src/main/scala/builder/targets/Shared.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,37 @@ private[targets] object Shared:
.resolve:
case err: IOException => s"failed to create directory $path: ${err.getMessage}"

def readStructure(module: Module, platform: PlatformKind, classpath: List[String])(using Settings): Result[ujson.Value, String] =
def readStructure(module: Module, platform: PlatformKind, dependencies: List[String], classpath: List[String])(using Settings): Result[ujson.Value, String] =
Result:
val args = ScalaCommand.makeArgs(module, InternalCommand.ExportJson, classpath, platform)
// TODO: actually do not pass in dependencies to `scala` command, otherwise there is not a way to
// distinguish between declared dependencies and transitive dependencies.
// However that will mean tracking changes in dependencies in each step.
val args = ScalaCommand.makeArgs(module, InternalCommand.ExportJson, classpath, dependencies, platform)
val result = ScalaCommand.call(args).?
if result.exitCode != 0 then
failure(s"failed to read structure of module ${module.name}: ${result.err.lines().mkString("\n")}")
else
ujson.read(result.out.text())

def dependencies(project: ujson.Value, scope: String): Result[List[String], String] =
def dependency(value: ujson.Value): Option[String] = optional:
val groupId = value.obj.get("groupId").?.str
val fullName =
val artifactId = value.obj.get("artifactId").?
artifactId.obj.get("fullName").?.str
val version = value.obj.get("version").?.str
s"$groupId:$fullName:$version"

val dependencies =
optional:
val scopesObj = project.obj.get("scopes").?
val scopeObj = scopesObj.obj.get(scope).?
val dependencies = scopeObj.obj.get("dependencies").?
dependencies.arr.toList.map(dependency.?)

dependencies.asSuccess("failed to read dependencies")
end dependencies

def hash(module: Module, project: ujson.Value, scopes: Set[String]): Result[String, String] =
val md = MessageDigest.getInstance("SHA-1")

Expand All @@ -38,11 +60,17 @@ private[targets] object Shared:
.resolve:
case err: IOException => s"failed to hash file ${path}: ${err.getMessage}"

def parseScopedSources = optional:
val scopesObj = project.obj.get("scopes").?
val scopeObjs = scopes.toList.map(scopesObj.obj.get.?)
val sourceArrs = scopeObjs.map(_.obj.get("sources").?.arr)
sourceArrs

Result:
val scopedSources = parseScopedSources.asSuccess("failed to read sources").?
for
scopesObj <- project.obj.get("scopes")
scope <- scopes.toList.flatMap(scopesObj.obj.get)
source <- scope("sources").arr
scope <- scopedSources
source <- scope
do
val bytes = readFile(source.str).?
md.update(bytes)
Expand All @@ -67,7 +95,7 @@ private[targets] object Shared:
reporter.debug(s"dependency of ${module.name} updated, cleaning module ${module.name}...")
else
reporter.info(s"cleaning module ${module.name}...")
val args = ScalaCommand.makeArgs(module, SubCommand.Clean, Nil, PlatformKind.jvm)
val args = ScalaCommand.makeArgs(module, SubCommand.Clean, Nil, Nil, PlatformKind.jvm)
val result = ScalaCommand.call(args).?
if result.exitCode != 0 then
failure(s"failed to clean module ${module.name}: ${result.err.lines().mkString("\n")}")
Expand Down
37 changes: 23 additions & 14 deletions modules/scala-builder/src/main/scala/builder/targets/Step.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,23 @@ final case class CompileScalaStep(module: Module, platform: PlatformKind) extend
val initialDeps = module.dependsOn.flatMap(initial.optLibrary(_, platform)) // might not exist yet
val currentDeps = module.dependsOn.map(project.library(_, platform)) // must exist

val dclasspath = currentDeps.map(_.outClasspath).flatten.toList.distinct.sorted
val dDependencies = currentDeps.flatMap(_.outDependencies).distinct.sorted
val dclasspath = currentDeps.flatMap(_.outClasspath).distinct.sorted

val oldTarget = project.graph.get(module.name)

val structure = Shared.readStructure(module, platform, dclasspath).?
val structure = Shared.readStructure(module, platform, dDependencies, dclasspath).?
val inputHash = Shared.hash(module, structure, Set("main")).?

val outDependencies = Shared.dependencies(structure, "main").?

def structureIsSame = oldTarget.flatMap(_.optProject(platform)).exists(_ == structure)
def inputHashIsSame = oldTarget.flatMap(_.optLibrary(platform)).exists(_.inputHash == inputHash)
def depsChanged = initialDeps.map(_.token) != currentDeps.map(_.token)

def resourceDir = os.pwd / ".scala-builder" / module.name / "managed_resources"
val buildDir = os.pwd / os.RelPath(module.root) / ".scala-build"
val buildDirStr = buildDir.toString

def computeClasspath() =
Result:
Expand All @@ -44,33 +49,34 @@ final case class CompileScalaStep(module: Module, platform: PlatformKind) extend
"--resource-dir" :: resourceDir.toString :: Nil
else
Nil
val args = ScalaCommand.makeArgs(module, InternalCommand.Compile, dclasspath, platform, "--print-class-path", resourceArgs)
val args = ScalaCommand.makeArgs(module, InternalCommand.Compile, dclasspath, dDependencies, platform, "--print-class-path", resourceArgs)
reporter.debug(s"exporting classpath of module ${module.name} with args: ${args.map(_.value.mkString(" ")).mkString(" ")}")
val res = ScalaCommand.call(args).?
if res.exitCode != 0 then
failure(s"failed to compile module ${module.name}: ${res.err.lines().mkString("\n")}")
val classpath = res.out.lines().head.split(":").toList.distinct.sorted
val rawClasspath = res.out.lines().head.split(":").toList.distinct.sorted
val classpath = rawClasspath.filter(_.startsWith(buildDirStr))
reporter.debug(s"classpath of module ${module.name}:main:${platform} is ${classpath.mkString(":")}")
classpath

if !structureIsSame then
if depsChanged then
Shared.doCleanModule(module, dependency = true).?
val mclasspath = computeClasspath().?
Some(TargetUpdate(Some(platform -> structure), TargetState.Library(inputHash, platform, dclasspath, mclasspath)))
Some(TargetUpdate(Some(platform -> structure), TargetState.Library(inputHash, platform, dDependencies, dclasspath, outDependencies, mclasspath)))
else
val previous = oldTarget.get.optLibrary(platform)
if depsChanged then
Shared.doCleanModule(module, dependency = true).?
val mclasspath = computeClasspath().? // execute compile for side-effects
for old <- previous do
assert(old.outClasspath == mclasspath) // output classpath should be the same
Some(TargetUpdate(Some(platform -> structure), TargetState.Library(inputHash, platform, dclasspath, mclasspath)))
Some(TargetUpdate(Some(platform -> structure), TargetState.Library(inputHash, platform, dDependencies, dclasspath, outDependencies, mclasspath)))
else if !inputHashIsSame then
val mclasspath = computeClasspath().? // execute compile for side-effects
for old <- previous do
assert(old.outClasspath == mclasspath) // output classpath should be the same
Some(TargetUpdate(Some(platform -> structure), TargetState.Library(inputHash, platform, dclasspath, mclasspath)))
Some(TargetUpdate(Some(platform -> structure), TargetState.Library(inputHash, platform, dDependencies, dclasspath, outDependencies, mclasspath)))
else
None
end exec
Expand All @@ -82,11 +88,12 @@ final case class PackageScalaStep(module: Module, info: ModuleKind.Application,
val initialDeps = module.dependsOn.flatMap(initial.optLibrary(_, platform)) // might not exist yet
val currentDeps = module.dependsOn.map(project.library(_, platform)) // must exist

val dclasspath = currentDeps.map(_.outClasspath).flatten.toList.distinct.sorted
val dDependencies = currentDeps.flatMap(_.outDependencies).distinct.sorted
val dclasspath = currentDeps.flatMap(_.outClasspath).distinct.sorted

val oldTarget = project.graph.get(module.name)

val structure = Shared.readStructure(module, platform, dclasspath).?
val structure = Shared.readStructure(module, platform, dDependencies, dclasspath).?
val inputHash = Shared.hash(module, structure, Set("main")).?

def structureIsSame = oldTarget.flatMap(_.optProject(platform)).exists(_ == structure)
Expand All @@ -111,7 +118,7 @@ final case class PackageScalaStep(module: Module, info: ModuleKind.Application,
val outputPath = os.pwd / ".scala-builder" / module.name / "packaged"
Shared.makeDir(outputPath).?
val artifactPath = outputPath / artifact
val args = ScalaCommand.makeArgs(module, InternalCommand.Package(artifactPath), dclasspath, platform, mainArgs, resourceArgs)
val args = ScalaCommand.makeArgs(module, InternalCommand.Package(artifactPath), dclasspath, dDependencies, platform, mainArgs, resourceArgs)
reporter.debug(s"packaging application ${module.name} with args: ${args.map(_.value.mkString(" ")).mkString(" ")}")
val res = ScalaCommand.call(args).?
if res.exitCode != 0 then
Expand Down Expand Up @@ -173,7 +180,8 @@ final case class CopyResourceStep(module: Module, fromTarget: Target) extends St
val dest = destDir / copyTo.last
os.copy(os.Path(currentDep.outPath), dest, replaceExisting = true)
reporter.debug(s"copied resource from ${currentDep.outPath} to $dest")
None // No caching for now
reporter.info(s"updated resource target ${module.name}:copy[${fromTarget.show}]") // TODO: delete when caching is implemented
None // No caching for now, we can hash the `currentDep.outPath` and compare it to the previous one
end exec
end CopyResourceStep

Expand All @@ -182,11 +190,12 @@ final case class RunScalaStep(module: Module, info: ModuleKind.Application) exte
val initialDeps = module.dependsOn.flatMap(initial.optLibrary(_, PlatformKind.jvm)) // might not exist yet
val currentDeps = module.dependsOn.map(project.library(_, PlatformKind.jvm)) // must exist

val dclasspath = currentDeps.map(_.outClasspath).flatten.toList.distinct.sorted
val dDependencies = currentDeps.flatMap(_.outDependencies).distinct.sorted
val dclasspath = currentDeps.flatMap(_.outClasspath).distinct.sorted

val oldTarget = project.graph.get(module.name)

val structure = Shared.readStructure(module, PlatformKind.jvm, dclasspath).?
val structure = Shared.readStructure(module, PlatformKind.jvm, dDependencies, dclasspath).?
val inputHash = Shared.hash(module, structure, Set("main")).?

def structureIsSame = oldTarget.flatMap(_.optProject(PlatformKind.jvm)).exists(_ == structure)
Expand All @@ -204,7 +213,7 @@ final case class RunScalaStep(module: Module, info: ModuleKind.Application) exte
"--resource-dir" :: resourceDir.toString :: Nil
else
Nil
val args = ScalaCommand.makeArgs(module, SubCommand.Run, dclasspath, PlatformKind.jvm, mainArgs, resourceArgs)
val args = ScalaCommand.makeArgs(module, SubCommand.Run, dclasspath, dDependencies, PlatformKind.jvm, mainArgs, resourceArgs)
reporter.debug(s"compiling application ${module.name} with args: ${args.map(_.value.mkString(" ")).mkString(" ")}")
val res = ScalaCommand.call(args).?
if res.exitCode != 0 then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ case class Targets(graph: Map[String, TargetContext]) derives ReadWriter:
update
)
extension (st: TargetState) def targetKind: TargetKind = st match
case TargetState.Library(_, platform, _, _) => TargetKind.Library(platform)
case TargetState.Library(_, platform, _, _, _, _) => TargetKind.Library(platform)
case TargetState.Application(_, _) => TargetKind.Application
case TargetState.Package(_, _) => TargetKind.Package

Expand Down Expand Up @@ -211,11 +211,11 @@ case class TargetContext(projects: Map[PlatformKind, ujson.Value], targets: Set[

/** A target is a cacheable entity, associated with a module */
enum TargetState(val token: TargetId) derives ReadWriter:
case Library(inputHash: String, platform: PlatformKind, depsClasspath: List[String], outClasspath: List[String]) extends TargetState(TargetId())
case Library(inputHash: String, platform: PlatformKind, depsDependencies: List[String], depsClasspath: List[String], outDependencies: List[String], outClasspath: List[String]) extends TargetState(TargetId())
case Application(inputHash: String, outCommand: List[String]) extends TargetState(TargetId())
case Package(inputHash: String, outPath: String) extends TargetState(TargetId())

def describe(name: String): String = this match
case TargetState.Library(_, platform, _, _) => s"scala library target $name:main:$platform"
case TargetState.Library(_, platform, _, _, _, _) => s"scala library target $name:main:$platform"
case TargetState.Application(_, _) => s"scala application target $name:runner"
case TargetState.Package(_, _) => s"package target $name:package"
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,13 @@ object Tasks:
Result:
val target = project.library(module.name, PlatformKind.jvm)
val classpath = target.depsClasspath
val dependencies = target.depsDependencies
val resourceArgs =
if module.resourceGenerators.sizeIs > 0 then
"--resource-dir" :: resourceDir.toString :: Nil
else
Nil
val args = ScalaCommand.makeArgs(module, SubCommand.Repl, classpath, PlatformKind.jvm, resourceArgs)
val args = ScalaCommand.makeArgs(module, SubCommand.Repl, classpath, dependencies, PlatformKind.jvm, resourceArgs)
reporter.debug(s"running command: ${args.map(_.value.mkString(" ")).mkString(" ")}")
val result = ScalaCommand.spawn(args).?

Expand All @@ -76,14 +77,15 @@ object Tasks:
Shared.doCleanModule(module, dependency = true).?

val classpath = targetDeps.flatMap(_.outClasspath).distinct.sorted
val dependencies = targetDeps.flatMap(_.outDependencies).distinct.sorted

val resourceArgs =
if module.resourceGenerators.sizeIs > 0 then
"--resource-dir" :: resourceDir.toString :: Nil
else
Nil

val args = ScalaCommand.makeArgs(module, SubCommand.Test, classpath, PlatformKind.jvm, resourceArgs)
val args = ScalaCommand.makeArgs(module, SubCommand.Test, classpath, dependencies, PlatformKind.jvm, resourceArgs)
reporter.debug(s"running command: ${args.map(_.value.mkString(" ")).mkString(" ")}")
val result = ScalaCommand.spawn(args).?

Expand Down

0 comments on commit 5727991

Please sign in to comment.