Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ object lib extends CommonScalaModule {
mvn"co.fs2::fs2-core:3.11.0",
mvn"io.get-coursier:interface:1.0.28",
mvn"ch.epfl.scala::tasty-query:1.7.0",
mvn"org.scala-lang:scala3-tasty-inspector_3:3.8.1"
mvn"org.scala-lang:scala3-tasty-inspector_3:3.8.1",
mvn"com.github.pureconfig::pureconfig-core:0.17.10"
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
{"pattern": "\\Qcoursier/coursier.properties\\E"},
{"pattern": "\\Qcoursier/launcher/coursier.properties\\E"},
{"pattern": "\\Qlibrary.properties\\E"},
{"pattern": "\\Qjre.bin\\E"}
{"pattern": "\\Qjre.bin\\E"},
{"pattern": "\\Qreference.conf\\E"}
]
}
}
12 changes: 12 additions & 0 deletions cli/resources/reference.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
mill {
# When running mill, which binary to invoke
binary = "./mill"
}

sbt {
# When running sbt, which binary to invoke (sbt or sbtn)
binary = "sbt"
# Whether to use client mode (--client) or not (--batch) when running sbt.
# Client mode is much faster in case there is a running sbt server.
use-client-mode = false
Comment thread
rochala marked this conversation as resolved.
Outdated
}
17 changes: 10 additions & 7 deletions cli/src/cellar/cli/CellarApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,30 +48,33 @@ object CellarApp
private val noCacheOpt: Opts[Boolean] =
Opts.flag("no-cache", "Skip classpath cache (re-extract from build tool)").orFalse

private val configOpt: Opts[IO[Config]] =
Opts.option[Path]("config", "Path to config file", "c").orNone.map(Config.load)

private def parseAndResolve(raw: String, extraRepos: List[Repository]): IO[Either[String, MavenCoordinate]] =
MavenCoordinate.parse(raw) match
case Left(err) => IO.pure(Left(err))
case Right(coord) => coord.resolveLatest(extraRepos).map(Right(_))

private val getSubcmd: Opts[IO[ExitCode]] =
Opts.subcommand("get", "Fetch symbol info from the current project") {
(symbolArg, moduleOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, javaHome, noCache) =>
ProjectGetHandler.run(fqn, module, javaHome, noCache)
(symbolArg, moduleOpt, configOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, configIO, javaHome, noCache) =>
configIO.flatMap(ProjectGetHandler.run(fqn, module, _, javaHome, noCache))
}
}

private val listSubcmd: Opts[IO[ExitCode]] =
Opts.subcommand("list", "List symbols in a package or class from the current project") {
(symbolArg, moduleOpt, limitOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, limit, javaHome, noCache) =>
ProjectListHandler.run(fqn, module, limit, javaHome, noCache)
(symbolArg, moduleOpt, limitOpt, configOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, limit, configIO, javaHome, noCache) =>
configIO.flatMap(ProjectListHandler.run(fqn, module, limit, _, javaHome, noCache))
}
}

private val searchSubcmd: Opts[IO[ExitCode]] =
Opts.subcommand("search", "Substring search for symbol names in the current project") {
(Opts.argument[String]("query"), moduleOpt, limitOpt, javaHomeOpt, noCacheOpt).mapN {
(query, module, limit, javaHome, noCache) =>
ProjectSearchHandler.run(query, module, limit, javaHome, noCache)
(Opts.argument[String]("query"), moduleOpt, limitOpt, configOpt, javaHomeOpt, noCacheOpt).mapN {
(query, module, limit, configIO, javaHome, noCache) =>
configIO.flatMap(ProjectSearchHandler.run(query, module, limit, _, javaHome, noCache))
}
}

Expand Down
32 changes: 32 additions & 0 deletions lib/src/cellar/Config.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package cellar

import cats.effect.IO
import pureconfig.*

import java.nio.file.{Files, Path}

case class MillConfig(binary: String) derives ConfigReader

case class SbtConfig(useClientMode: Boolean, binary: String) derives ConfigReader

case class Config(mill: MillConfig, sbt: SbtConfig) derives ConfigReader

object Config {
val default: IO[Config] = load(None)

val defaultPath: Path = Path.of(".cellar").resolve("cellar.conf")
Comment thread
rochala marked this conversation as resolved.
Outdated

def load(path: Option[Path]): IO[Config] = {
def load(path: Option[Path]) =
Comment thread
rochala marked this conversation as resolved.
Outdated
IO.blocking {
path.foldLeft(ConfigSource.default)((cs, p) => ConfigSource.file(p).withFallback(cs)).loadOrThrow[Config]
}

path match
case sp: Some[_] => load(sp)
case None => IO.blocking(Files.exists(defaultPath)).flatMap {
Comment thread
rochala marked this conversation as resolved.
Outdated
case true => load(Some(defaultPath))
case false => load(None)
}
}
}
11 changes: 6 additions & 5 deletions lib/src/cellar/build/MillBuildTool.scala
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package cellar.build

import cats.effect.IO
import cellar.CellarError
import cellar.{CellarError, MillConfig}
import cellar.process.ProcessRunner

import java.nio.file.{Files, Path}

class MillBuildTool(cwd: Path, binary: String = "./mill") extends BuildTool:
class MillBuildTool(cwd: Path, config: MillConfig) extends BuildTool:
def kind: BuildToolKind = BuildToolKind.Mill

def compile(module: Option[String]): IO[Unit] =
requireModule(module).flatMap { mod =>
ProcessRunner.run(List(binary, s"$mod.compile"), Some(cwd)).flatMap { result =>
ProcessRunner.run(List(config.binary, s"$mod.compile"), Some(cwd)).flatMap { result =>
if result.exitCode == 0 then IO.unit
else IO.raiseError(CellarError.CompilationFailed(BuildToolKind.Mill, result.stderr))
}
Expand All @@ -19,10 +20,10 @@ class MillBuildTool(cwd: Path, binary: String = "./mill") extends BuildTool:
def extractClasspath(module: Option[String]): IO[List[Path]] =
requireModule(module).flatMap { mod =>
for
compileResult <- ProcessRunner.run(List(binary, "show", s"$mod.compile"), Some(cwd))
compileResult <- ProcessRunner.run(List(config.binary, "show", s"$mod.compile"), Some(cwd))
_ <- checkCompileResult(compileResult, mod)
classesDir <- IO.blocking(parseClassesDir(compileResult.stdout))
cpResult <- ProcessRunner.run(List(binary, "show", s"$mod.compileClasspath"), Some(cwd))
cpResult <- ProcessRunner.run(List(config.binary, "show", s"$mod.compileClasspath"), Some(cwd))
paths <- parseClasspathResult(cpResult, classesDir)
yield paths
}
Expand Down
17 changes: 9 additions & 8 deletions lib/src/cellar/build/ProjectClasspathProvider.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package cellar.build

import cats.effect.{IO, Resource}
import cellar.ContextResource
import cellar.{Config, ContextResource}

import java.nio.file.Path
import tastyquery.Classpaths.Classpath
import tastyquery.Contexts.Context
Expand All @@ -12,15 +13,15 @@ object ProjectClasspathProvider:
module: Option[String],
jreClasspath: Classpath,
noCache: Boolean,
millBinary: String = "./mill"
config: Config
): Resource[IO, (Context, Classpath)] =
Resource.eval(resolveClasspath(cwd, module, noCache, millBinary)).flatMap { paths =>
Resource.eval(resolveClasspath(cwd, module, noCache, config)).flatMap { paths =>
ContextResource.make(paths, jreClasspath)
}

private def resolveClasspath(cwd: Path, module: Option[String], noCache: Boolean, millBinary: String): IO[List[Path]] =
private def resolveClasspath(cwd: Path, module: Option[String], noCache: Boolean, config: Config): IO[List[Path]] =
BuildToolDetector.detectKind(cwd).flatMap { kind =>
val buildTool = instantiate(kind, cwd, millBinary)
val buildTool = instantiate(kind, cwd, config)
val useCache = kind != BuildToolKind.ScalaCli && !noCache

if useCache then cachedFlow(buildTool, module, cwd)
Expand All @@ -40,7 +41,7 @@ object ProjectClasspathProvider:
case None => buildTool.extractClasspath(module).flatTap(paths => cache.put(hash, paths))
yield paths

private def instantiate(kind: BuildToolKind, cwd: Path, millBinary: String): BuildTool = kind match
case BuildToolKind.Mill => MillBuildTool(cwd, millBinary)
case BuildToolKind.Sbt => SbtBuildTool(cwd)
private def instantiate(kind: BuildToolKind, cwd: Path, config: Config): BuildTool = kind match
case BuildToolKind.Mill => MillBuildTool(cwd, config.mill)
case BuildToolKind.Sbt => SbtBuildTool(cwd, config.sbt)
case BuildToolKind.ScalaCli => ScalaCliBuildTool(cwd)
11 changes: 7 additions & 4 deletions lib/src/cellar/build/SbtBuildTool.scala
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
package cellar.build

import cats.effect.IO
import cellar.CellarError
import cellar.{CellarError, SbtConfig}
import cellar.process.ProcessRunner

import java.nio.file.{Files, Path}

class SbtBuildTool(cwd: Path) extends BuildTool:
class SbtBuildTool(cwd: Path, config: SbtConfig) extends BuildTool:
private val mode: String = if config.useClientMode then "--client" else "--batch"

def kind: BuildToolKind = BuildToolKind.Sbt

def compile(module: Option[String]): IO[Unit] =
requireModule(module).flatMap { mod =>
ProcessRunner.run(List("sbt", "--batch", s"$mod/compile"), Some(cwd)).flatMap { result =>
ProcessRunner.run(List(config.binary, mode, s"$mod/compile"), Some(cwd)).flatMap { result =>
if result.exitCode == 0 then IO.unit
else IO.raiseError(CellarError.CompilationFailed(BuildToolKind.Sbt, extractErrors(result.stdout, result.stderr)))
}
}

def extractClasspath(module: Option[String]): IO[List[Path]] =
requireModule(module).flatMap { mod =>
ProcessRunner.run(List("sbt", "--batch", s"export $mod/Compile/fullClasspath"), Some(cwd)).flatMap { result =>
ProcessRunner.run(List(config.binary, mode, s"export $mod/Compile/fullClasspath"), Some(cwd)).flatMap { result =>
if result.exitCode != 0 then
IO.raiseError(CellarError.CompilationFailed(BuildToolKind.Sbt, extractErrors(result.stdout, result.stderr)))
else
Expand Down
6 changes: 3 additions & 3 deletions lib/src/cellar/handlers/ProjectGetHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ object ProjectGetHandler:
def run(
fqn: String,
module: Option[String],
config: Config,
javaHome: Option[Path] = None,
noCache: Boolean = false,
cwd: Option[Path] = None,
millBinary: String = "./mill"
cwd: Option[Path] = None
)(using Console[IO]): IO[ExitCode] =
ProjectHandler.run(javaHome, cwd, module, noCache, millBinary) { (ctx, classpath, _) =>
ProjectHandler.run(javaHome, cwd, module, noCache, config) { (ctx, classpath, _) =>
given Context = ctx
GetHandler.runCore(fqn, classpath, coord = None)
}
4 changes: 2 additions & 2 deletions lib/src/cellar/handlers/ProjectHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ object ProjectHandler:
cwd: Option[Path],
module: Option[String],
noCache: Boolean,
millBinary: String
config: Config
)(body: (Context, Classpath, Classpath) => IO[ExitCode])(using Console[IO]): IO[ExitCode] =
val program =
for
jreClasspath <- javaHome.fold(JreClasspath.jrtPath())(JreClasspath.jrtPath)
workingDir <- cwd.fold(IO.blocking(Path.of(System.getProperty("user.dir"))))(IO.pure)
result <- build.ProjectClasspathProvider.provide(workingDir, module, jreClasspath, noCache, millBinary).use { (ctx, classpath) =>
result <- build.ProjectClasspathProvider.provide(workingDir, module, jreClasspath, noCache, config).use { (ctx, classpath) =>
body(ctx, classpath, jreClasspath)
}
yield result
Expand Down
6 changes: 3 additions & 3 deletions lib/src/cellar/handlers/ProjectListHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ object ProjectListHandler:
fqn: String,
module: Option[String],
limit: Int,
config: Config,
javaHome: Option[Path] = None,
noCache: Boolean = false,
cwd: Option[Path] = None,
millBinary: String = "./mill"
cwd: Option[Path] = None
)(using Console[IO]): IO[ExitCode] =
ProjectHandler.run(javaHome, cwd, module, noCache, millBinary) { (ctx, _, _) =>
ProjectHandler.run(javaHome, cwd, module, noCache, config) { (ctx, _, _) =>
given Context = ctx
ListHandler.runCore(fqn, limit, coord = None)
}
6 changes: 3 additions & 3 deletions lib/src/cellar/handlers/ProjectSearchHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ object ProjectSearchHandler:
query: String,
module: Option[String],
limit: Int,
config: Config,
javaHome: Option[Path] = None,
noCache: Boolean = false,
cwd: Option[Path] = None,
millBinary: String = "./mill"
cwd: Option[Path] = None
)(using Console[IO]): IO[ExitCode] =
ProjectHandler.run(javaHome, cwd, module, noCache, millBinary) { (ctx, classpath, jreClasspath) =>
ProjectHandler.run(javaHome, cwd, module, noCache, config) { (ctx, classpath, jreClasspath) =>
given Context = ctx
SearchHandler.runCore(query, limit, classpath, jreClasspath)
}
34 changes: 18 additions & 16 deletions lib/test/src/cellar/ProjectAwareIntegrationTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -180,17 +180,19 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite:
// --- MillBuildTool tests ---

test("MillBuildTool: rejects missing --module"):
build.MillBuildTool(Path.of(".")).extractClasspath(None).attempt.map { result =>
assert(result.isLeft)
assert(result.left.exists(_.getMessage.contains("--module is required for Mill")))
Config.default.map(config => build.MillBuildTool(Path.of("."), config.mill))
.flatMap(_.extractClasspath(None)).attempt.map { result =>
assert(result.isLeft)
assert(result.left.exists(_.getMessage.contains("--module is required for Mill")))
}

// --- SbtBuildTool tests ---

test("SbtBuildTool: rejects missing --module"):
build.SbtBuildTool(Path.of(".")).extractClasspath(None).attempt.map { result =>
assert(result.isLeft)
assert(result.left.exists(_.getMessage.contains("--module is required for sbt")))
Config.default.map(config => build.SbtBuildTool(Path.of("."), config.sbt)).flatMap(_.extractClasspath(None))
.attempt.map { result =>
assert(result.isLeft)
assert(result.left.exists(_.getMessage.contains("--module is required for sbt")))
}

// --- BuildFingerprint tests ---
Expand Down Expand Up @@ -300,7 +302,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite:
| def hello: String = "world"
|""".stripMargin
)) >>
handlers.ProjectGetHandler.run("example.MyClass", module = None, cwd = Some(dir)).map { code =>
Config.default.flatMap(handlers.ProjectGetHandler.run("example.MyClass", module = None, _, cwd = Some(dir))).map { code =>
assertEquals(code, ExitCode.Success, s"Stderr: ${console.errBuf}")
assert(console.outBuf.toString.contains("MyClass"), s"Output: ${console.outBuf}")
assert(console.outBuf.toString.contains("hello"), s"Output: ${console.outBuf}")
Expand All @@ -321,7 +323,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite:
| def run: Unit = ()
|""".stripMargin
)) >>
handlers.ProjectGetHandler.run("cats.Monad", module = None, cwd = Some(dir)).map { code =>
Config.default.flatMap(handlers.ProjectGetHandler.run("cats.Monad", module = None, _, cwd = Some(dir))).map { code =>
assertEquals(code, ExitCode.Success)
assert(console.outBuf.toString.contains("Monad"), s"Output: ${console.outBuf}")
}
Expand All @@ -340,7 +342,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite:
| def goodbye: Int = 42
|""".stripMargin
)) >>
handlers.ProjectListHandler.run("example.MyClass", module = None, limit = 50, cwd = Some(dir)).map { code =>
Config.default.flatMap(handlers.ProjectListHandler.run("example.MyClass", module = None, limit = 50, _, cwd = Some(dir))).map { code =>
assertEquals(code, ExitCode.Success)
val out = console.outBuf.toString
assert(out.contains("hello"), s"Output: $out")
Expand All @@ -360,7 +362,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite:
| def run: Unit = ()
|""".stripMargin
)) >>
handlers.ProjectSearchHandler.run("UniqueTestClassName123", module = None, limit = 50, cwd = Some(dir)).map { code =>
Config.default.flatMap(handlers.ProjectSearchHandler.run("UniqueTestClassName123", module = None, limit = 50, _, cwd = Some(dir))).map { code =>
assertEquals(code, ExitCode.Success)
assert(console.outBuf.toString.contains("UniqueTestClassName123"), s"Output: ${console.outBuf}")
}
Expand All @@ -376,7 +378,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite:
|class Foo
|""".stripMargin
)) >>
handlers.ProjectGetHandler.run("example.Foo", module = Some("bar"), cwd = Some(dir)).map { code =>
Config.default.flatMap(handlers.ProjectGetHandler.run("example.Foo", module = Some("bar"), _, cwd = Some(dir))).map { code =>
assertEquals(code, ExitCode.Error)
assert(console.errBuf.toString.contains("--module is not supported"), s"Stderr: ${console.errBuf}")
}
Expand All @@ -394,7 +396,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite:
|}
|""".stripMargin
)) >>
handlers.ProjectGetHandler.run("example.Bad", module = None, cwd = Some(dir)).map { code =>
Config.default.flatMap(handlers.ProjectGetHandler.run("example.Bad", module = None, _, cwd = Some(dir))).map { code =>
assertEquals(code, ExitCode.Error)
assert(console.errBuf.toString.contains("Compilation failed"), s"Stderr: ${console.errBuf}")
}
Expand Down Expand Up @@ -426,7 +428,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite:
|""".stripMargin
)
} >>
handlers.ProjectGetHandler.run("example.MillClass", module = Some("app"), cwd = Some(dir), millBinary = millBinary).map { code =>
Config.default.flatMap(config => handlers.ProjectGetHandler.run("example.MillClass", module = Some("app"), cwd = Some(dir), config = config.copy(mill = MillConfig(millBinary)))).map { code =>
assertEquals(code, ExitCode.Success, s"Stderr: ${console.errBuf}\nStdout: ${console.outBuf}")
assert(console.outBuf.toString.contains("MillClass"), s"Output: ${console.outBuf}")
assert(console.outBuf.toString.contains("greet"), s"Output: ${console.outBuf}")
Expand All @@ -439,7 +441,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite:
val console = CapturingConsole()
given Console[IO] = console
IO.blocking(Files.writeString(dir.resolve("build.mill"), "")) >>
handlers.ProjectGetHandler.run("example.Foo", module = None, cwd = Some(dir), millBinary = millBinary).map { code =>
Config.default.flatMap(config => handlers.ProjectGetHandler.run("example.Foo", module = None, cwd = Some(dir), config = config.copy(mill = MillConfig(millBinary)))).map { code =>
assertEquals(code, ExitCode.Error)
assert(console.errBuf.toString.contains("--module is required for Mill"), s"Stderr: ${console.errBuf}")
}
Expand Down Expand Up @@ -469,7 +471,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite:
|""".stripMargin
)
} >>
handlers.ProjectGetHandler.run("example.SbtClass", module = Some("cellar-test"), cwd = Some(dir)).map { code =>
Config.default.flatMap(handlers.ProjectGetHandler.run("example.SbtClass", module = Some("cellar-test"), _, cwd = Some(dir))).map { code =>
assertEquals(code, ExitCode.Success, s"Stderr: ${console.errBuf}")
assert(console.outBuf.toString.contains("SbtClass"), s"Output: ${console.outBuf}")
}
Expand All @@ -481,7 +483,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite:
val console = CapturingConsole()
given Console[IO] = console
IO.blocking(Files.writeString(dir.resolve("build.sbt"), "")) >>
handlers.ProjectGetHandler.run("example.Foo", module = None, cwd = Some(dir)).map { code =>
Config.default.flatMap(handlers.ProjectGetHandler.run("example.Foo", module = None, _, cwd = Some(dir))).map { code =>
assertEquals(code, ExitCode.Error)
assert(console.errBuf.toString.contains("--module is required for sbt"), s"Stderr: ${console.errBuf}")
}
Expand Down