diff --git a/modules/build/src/main/scala/scala/build/compiler/BloopCompilerMaker.scala b/modules/build/src/main/scala/scala/build/compiler/BloopCompilerMaker.scala index e7534ea271..dcb673c586 100644 --- a/modules/build/src/main/scala/scala/build/compiler/BloopCompilerMaker.scala +++ b/modules/build/src/main/scala/scala/build/compiler/BloopCompilerMaker.scala @@ -4,20 +4,23 @@ import bloop.rifle.{BloopRifleConfig, BloopServer, BloopThreads} import ch.epfl.scala.bsp4j.BuildClient import scala.build.Logger +import scala.build.errors.FetchingDependenciesError import scala.build.internal.Constants import scala.concurrent.duration.DurationInt +import scala.util.Try final class BloopCompilerMaker( config: BloopRifleConfig, threads: BloopThreads, - strictBloopJsonCheck: Boolean + strictBloopJsonCheck: Boolean, + offline: Boolean ) extends ScalaCompilerMaker { def create( workspace: os.Path, classesDir: os.Path, buildClient: BuildClient, logger: Logger - ): BloopCompiler = { + ): ScalaCompiler = { val createBuildServer = () => BloopServer.buildServer( @@ -30,6 +33,20 @@ final class BloopCompilerMaker( threads, logger.bloopRifleLogger ) - new BloopCompiler(createBuildServer, 20.seconds, strictBloopJsonCheck) + + Try(new BloopCompiler(createBuildServer, 20.seconds, strictBloopJsonCheck)) + .toEither + .left.flatMap { + case e if offline => + e.getCause match + case _: FetchingDependenciesError => + logger.debug("Couldn't fetch Bloop from cache, fallback to using scalac") + Right( + SimpleScalaCompilerMaker("java", Nil) + .create(workspace, classesDir, buildClient, logger) + ) + case _ => Left(e) + case e => Left(e) + }.fold(t => throw t, identity) } } diff --git a/modules/build/src/test/scala/scala/build/tests/TestInputs.scala b/modules/build/src/test/scala/scala/build/tests/TestInputs.scala index eac2d853f9..2678c36b40 100644 --- a/modules/build/src/test/scala/scala/build/tests/TestInputs.scala +++ b/modules/build/src/test/scala/scala/build/tests/TestInputs.scala @@ -86,7 +86,12 @@ final case class TestInputs( withCustomInputs(fromDirectory, None, skipCreatingSources) { (root, inputs) => val compilerMaker = bloopConfigOpt match { case Some(bloopConfig) => - new BloopCompilerMaker(bloopConfig, buildThreads.bloop, strictBloopJsonCheck = true) + new BloopCompilerMaker( + bloopConfig, + buildThreads.bloop, + strictBloopJsonCheck = true, + offline = false + ) case None => SimpleScalaCompilerMaker("java", Nil) } diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/CoursierOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/CoursierOptions.scala index 1ecfb5d9f7..8471421e91 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/CoursierOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/CoursierOptions.scala @@ -3,7 +3,7 @@ package scala.cli.commands.shared import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* -import coursier.cache.{CacheLogger, FileCache} +import coursier.cache.{CacheLogger, CachePolicy, FileCache} import scala.cli.commands.tags import scala.concurrent.duration.Duration @@ -26,7 +26,12 @@ final case class CoursierOptions( @HelpMessage("Enable checksum validation of artifacts downloaded by coursier") @Tag(tags.implementation) @Hidden - coursierValidateChecksums: Option[Boolean] = None + coursierValidateChecksums: Option[Boolean] = None, + + @Group(HelpGroup.Dependency.toString) + @HelpMessage("Disable using the network to download dependencies, use only the cache") + @Tag(tags.experimental) + offline: Option[Boolean] = None ) { // format: on @@ -42,6 +47,12 @@ final case class CoursierOptions( baseCache = baseCache.withTtl(ttl0) for (loc <- cache.filter(_.trim.nonEmpty)) baseCache = baseCache.withLocation(loc) + + offline.foreach { + case true => baseCache = baseCache.withCachePolicies(Seq(CachePolicy.LocalOnly)) + case false => baseCache + } + baseCache } } diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala index 7ed8169786..f0a6453472 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala @@ -401,7 +401,8 @@ final case class SharedOptions( verbosity = Some(logging.verbosity), strictBloopJsonCheck = strictBloopJsonCheck, interactive = Some(() => interactive), - exclude = exclude.map(Positioned.commandLine) + exclude = exclude.map(Positioned.commandLine), + offline = coursier.offline ), notForBloopOptions = bo.PostBuildOptions( scalaJsLinkerOptions = linkerOptions(js), @@ -543,7 +544,8 @@ final case class SharedOptions( new BloopCompilerMaker( value(bloopRifleConfig()), threads.bloop, - strictBloopJsonCheckOrDefault + strictBloopJsonCheckOrDefault, + coursier.offline.getOrElse(false) ) else SimpleScalaCompilerMaker("java", Nil) diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala index 29da426132..e9d540ff5f 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala @@ -1818,4 +1818,134 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String]) } } } + + test("offline mode should fail on missing artifacts") { + // Kill bloop deamon to test scalac fallback + os.proc(TestUtil.cli, "--power", "bloop", "exit") + .call(cwd = os.pwd) + + val depScalaVersion = actualScalaVersion match { + case sv if sv.startsWith("2.12") => "2.12" + case sv if sv.startsWith("2.13") => "2.13" + case sv if sv.startsWith("3") => "3" + } + + val dep = s"com.lihaoyi:os-lib_$depScalaVersion:0.9.1" + val inputs = TestInputs( + os.rel / "NoDeps.scala" -> + """//> using jvm zulu:11 + |object NoDeps extends App { + | println("Hello from NoDeps") + |} + |""".stripMargin, + os.rel / "WithDeps.scala" -> + s"""//> using jvm zulu:11 + |//> using dep $dep + | + |object WithDeps extends App { + | println("Hello from WithDeps") + |} + |""".stripMargin + ) + inputs.fromRoot { root => + val cachePath = root / ".cache" + os.makeDir(cachePath) + + val extraEnv = Map("COURSIER_CACHE" -> cachePath.toString) + + val emptyCacheWalkSize = os.walk(cachePath).size + + val noArtifactsRes = os.proc( + TestUtil.cli, + "--power", + "NoDeps.scala", + extraOptions, + "--offline", + "--cache", + cachePath.toString + ) + .call(cwd = root, check = false, mergeErrIntoOut = true) + expect(noArtifactsRes.exitCode == 1) + + // Cache unchanged + expect(emptyCacheWalkSize == os.walk(cachePath).size) + + // Download the artifacts for scala + os.proc(TestUtil.cs, "install", s"scala:$actualScalaVersion") + .call(cwd = root, env = extraEnv) + os.proc(TestUtil.cs, "install", s"scalac:$actualScalaVersion") + .call(cwd = root, env = extraEnv) + + // Download JVM that won't suit Bloop, also no Bloop artifacts are present + os.proc(TestUtil.cs, "java-home", "--jvm", "zulu:11") + .call(cwd = root, env = extraEnv) + + val scalaJvmCacheWalkSize = os.walk(cachePath).size + + val scalaAndJvmRes = os.proc( + TestUtil.cli, + "--power", + "NoDeps.scala", + extraOptions, + "--offline", + "--cache", + cachePath.toString, + "-v", + "-v" + ) + .call(cwd = root, mergeErrIntoOut = true) + expect(scalaAndJvmRes.exitCode == 0) + expect(scalaAndJvmRes.out.trim().contains( + "Couldn't fetch Bloop from cache, fallback to using scalac" + )) + expect(scalaAndJvmRes.out.trim().contains("Hello from NoDeps")) + + // Cache unchanged + expect(scalaJvmCacheWalkSize == os.walk(cachePath).size) + + // Missing dependencies + val missingDepsRes = os.proc( + TestUtil.cli, + "--power", + "WithDeps.scala", + extraOptions, + "--offline", + "--cache", + cachePath.toString + ) + .call(cwd = root, check = false, mergeErrIntoOut = true) + expect(missingDepsRes.exitCode == 1) + expect(missingDepsRes.out.trim().contains("Error downloading com.lihaoyi:os-lib")) + + // Cache unchanged + expect(scalaJvmCacheWalkSize == os.walk(cachePath).size) + + // Download dependencies + os.proc(TestUtil.cs, "fetch", dep) + .call(cwd = root, env = extraEnv) + + val withDependencyCacheWalkSize = os.walk(cachePath).size + + val depsRes = os.proc( + TestUtil.cli, + "--power", + "WithDeps.scala", + extraOptions, + "--offline", + "--cache", + cachePath.toString, + "-v", + "-v" + ) + .call(cwd = root, mergeErrIntoOut = true) + expect(depsRes.exitCode == 0) + expect( + depsRes.out.trim().contains("Couldn't fetch Bloop from cache, fallback to using scalac") + ) + expect(depsRes.out.trim().contains("Hello from WithDeps")) + + // Cache changed + expect(withDependencyCacheWalkSize == os.walk(cachePath).size) + } + } } diff --git a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala index 7ec5c92f9c..2b84e49750 100644 --- a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala @@ -1,7 +1,7 @@ package scala.build.options import com.github.plokhotnyuk.jsoniter_scala.core.* -import coursier.cache.{ArchiveCache, FileCache} +import coursier.cache.{ArchiveCache, FileCache, UnArchiver} import coursier.core.{Repository, Version} import coursier.parse.RepositoryParser import coursier.util.{Artifact, Task} @@ -305,6 +305,8 @@ final case class BuildOptions( val svOpt: Option[String] = scalaOptions.scalaVersion match { case Some(MaybeScalaVersion(None)) => None + case Some(MaybeScalaVersion(Some(svInput))) if internal.offline.getOrElse(false) => + Some(svInput) case Some(MaybeScalaVersion(Some(svInput))) => val sv = value { svInput match { @@ -345,16 +347,19 @@ final case class BuildOptions( ) } } +// match { +// // A maven-metadata containing available scala versions may not be available in the cache, +// // so we can skip the version validation hoping that it is correct +// // The maven metadata is absent even after cs install scala(c):3.3.0 +// case Left(e) if internal.offline.getOrElse(false) => +// println(s"!!! Caught exception: $e") +// e.printStackTrace(System.out) +// svInput +// case leftOrRight => value(leftOrRight) +// } Some(sv) - case None => - val allStableVersions = - ScalaVersionUtil.allMatchingVersions(None, finalCache, value(finalRepositories)) - .filter(ScalaVersionUtil.isStable) - val sv = value { - ScalaVersionUtil.default(allStableVersions) - } - Some(sv) + case None => Some(Constants.defaultScalaVersion) } svOpt match { diff --git a/modules/options/src/main/scala/scala/build/options/InternalOptions.scala b/modules/options/src/main/scala/scala/build/options/InternalOptions.scala index f8efed0482..f9757edba7 100644 --- a/modules/options/src/main/scala/scala/build/options/InternalOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/InternalOptions.scala @@ -25,7 +25,8 @@ final case class InternalOptions( */ keepResolution: Boolean = false, extraSourceFiles: Seq[Positioned[os.Path]] = Nil, - exclude: Seq[Positioned[String]] = Nil + exclude: Seq[Positioned[String]] = Nil, + offline: Option[Boolean] = None ) { def verbosityOrDefault: Int = verbosity.getOrElse(0) def strictBloopJsonCheckOrDefault: Boolean = diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index 8f3bb7af8e..01bbf00277 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -213,6 +213,33 @@ Aliases: `-f` Force overwriting values for key +## Coursier options + +Available in commands: + +[`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`github secret create` , `gh secret create`](./commands.md#github-secret-create), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall) + + + +### `--ttl` + +[Internal] +Specify a TTL for changing dependencies, such as snapshots + +### `--cache` + +[Internal] +Set the coursier cache location + +### `--coursier-validate-checksums` + +[Internal] +Enable checksum validation of artifacts downloaded by coursier + +### `--offline` + +Disable using the network to download dependencies, use only the cache + ## Cross options Available in commands: @@ -1848,29 +1875,6 @@ Aliases: `--name` [Internal] Name of BSP -### Coursier options - -Available in commands: - -[`bloop`](./commands.md#bloop), [`bloop exit`](./commands.md#bloop-exit), [`bloop start`](./commands.md#bloop-start), [`bsp`](./commands.md#bsp), [`compile`](./commands.md#compile), [`config`](./commands.md#config), [`dependency-update`](./commands.md#dependency-update), [`doc`](./commands.md#doc), [`export`](./commands.md#export), [`fmt` , `format` , `scalafmt`](./commands.md#fmt), [`package`](./commands.md#package), [`pgp push`](./commands.md#pgp-push), [`publish`](./commands.md#publish), [`publish local`](./commands.md#publish-local), [`publish setup`](./commands.md#publish-setup), [`repl` , `console`](./commands.md#repl), [`run`](./commands.md#run), [`github secret create` , `gh secret create`](./commands.md#github-secret-create), [`setup-ide`](./commands.md#setup-ide), [`shebang`](./commands.md#shebang), [`test`](./commands.md#test), [`uninstall`](./commands.md#uninstall) - - - -### `--ttl` - -[Internal] -Specify a TTL for changing dependencies, such as snapshots - -### `--cache` - -[Internal] -Set the coursier cache location - -### `--coursier-validate-checksums` - -[Internal] -Enable checksum validation of artifacts downloaded by coursier - ### Default file options Available in commands: