diff --git a/modules/cli/src/main/scala/scala/cli/commands/bloop/BloopExit.scala b/modules/cli/src/main/scala/scala/cli/commands/bloop/BloopExit.scala index a2f6612dac..8774ebd8ea 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/bloop/BloopExit.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/bloop/BloopExit.scala @@ -18,7 +18,7 @@ object BloopExit extends ScalaCommand[BloopExitOptions] { import opts.* compilationServer.bloopRifleConfig( global.logging.logger, - coursier.coursierCache(global.logging.logger.coursierLogger("Downloading Bloop")), + coursier.coursierCache(global.logging.logger.coursierLogger("Downloading Bloop"), global.logging.logger), global.logging.verbosity, "java", // shouldn't be used… Directories.directories diff --git a/modules/cli/src/main/scala/scala/cli/commands/bloop/BloopOutput.scala b/modules/cli/src/main/scala/scala/cli/commands/bloop/BloopOutput.scala index b53f155fb1..c7e93d409b 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/bloop/BloopOutput.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/bloop/BloopOutput.scala @@ -20,7 +20,7 @@ object BloopOutput extends ScalaCommand[BloopOutputOptions] { override def runCommand(options: BloopOutputOptions, args: RemainingArgs, logger: Logger): Unit = { val bloopRifleConfig = options.compilationServer.bloopRifleConfig( logger, - CoursierOptions().coursierCache(logger.coursierLogger("Downloading Bloop")), // unused here + CoursierOptions().coursierCache(logger.coursierLogger("Downloading Bloop"), logger), // unused here options.global.logging.verbosity, "unused-java", // unused here Directories.directories diff --git a/modules/cli/src/main/scala/scala/cli/commands/bloop/BloopStart.scala b/modules/cli/src/main/scala/scala/cli/commands/bloop/BloopStart.scala index 967b6fb74f..2eef7b0f6f 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/bloop/BloopStart.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/bloop/BloopStart.scala @@ -24,13 +24,13 @@ object BloopStart extends ScalaCommand[BloopStartOptions] { val buildOptions = BuildOptions( javaOptions = JvmUtils.javaOptions(jvm).orExit(global.logging.logger), internal = InternalOptions( - cache = Some(coursier.coursierCache(global.logging.logger.coursierLogger(""))) + cache = Some(coursier.coursierCache(global.logging.logger.coursierLogger(""), global.logging.logger)) ) ) compilationServer.bloopRifleConfig( global.logging.logger, - coursier.coursierCache(global.logging.logger.coursierLogger("Downloading Bloop")), + coursier.coursierCache(global.logging.logger.coursierLogger("Downloading Bloop"), logger), global.logging.verbosity, buildOptions.javaHome().value.javaCommand, Directories.directories diff --git a/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala b/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala index f6b3f0dad2..8659b16469 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala @@ -46,7 +46,7 @@ object Config extends ScalaCommand[ConfigOptions] { ) sys.exit(1) } - val coursierCache = options.coursier.coursierCache(logger.coursierLogger("")) + val coursierCache = options.coursier.coursierCache(logger.coursierLogger(""), logger) val secKeyEntry = Keys.pgpSecretKey val pubKeyEntry = Keys.pgpPublicKey diff --git a/modules/cli/src/main/scala/scala/cli/commands/github/SecretCreate.scala b/modules/cli/src/main/scala/scala/cli/commands/github/SecretCreate.scala index 7a308ad51a..71a74fa113 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/github/SecretCreate.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/github/SecretCreate.scala @@ -157,7 +157,7 @@ object SecretCreate extends ScalaCommand[SecretCreateOptions] { ).orExit(logger) } - val cache = options.coursier.coursierCache(logger.coursierLogger("")) + val cache = options.coursier.coursierCache(logger.coursierLogger(""), logger) val archiveCache = ArchiveCache().withCache(cache) LibSodiumJni.init(cache, archiveCache, logger) diff --git a/modules/cli/src/main/scala/scala/cli/commands/new/New.scala b/modules/cli/src/main/scala/scala/cli/commands/new/New.scala index ba07acfb25..0ee741c113 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/new/New.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/new/New.scala @@ -24,7 +24,7 @@ object New extends ScalaCommand[NewOptions] { Seq.empty, Some(scalaParameters), logger, - CoursierOptions().coursierCache(logger.coursierLogger("")), + CoursierOptions().coursierCache(logger.coursierLogger(""), logger), None ) match { case Right(value) => value diff --git a/modules/cli/src/main/scala/scala/cli/commands/pgp/PgpExternalCommand.scala b/modules/cli/src/main/scala/scala/cli/commands/pgp/PgpExternalCommand.scala index 90b5e5bc6c..50b889b810 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/pgp/PgpExternalCommand.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/pgp/PgpExternalCommand.scala @@ -106,7 +106,7 @@ abstract class PgpExternalCommand extends ExternalCommand { val logger = options.global.logging.logger - val cache = options.coursier.coursierCache(logger.coursierLogger("")) + val cache = options.coursier.coursierCache(logger.coursierLogger(""), logger) val retCode = tryRun( cache, remainingArgs, diff --git a/modules/cli/src/main/scala/scala/cli/commands/pgp/PgpPush.scala b/modules/cli/src/main/scala/scala/cli/commands/pgp/PgpPush.scala index 252fcb132e..718605aa04 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/pgp/PgpPush.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/pgp/PgpPush.scala @@ -31,7 +31,7 @@ object PgpPush extends ScalaCommand[PgpPushOptions] { sys.exit(1) } - lazy val coursierCache = options.coursier.coursierCache(logger.coursierLogger("")) + lazy val coursierCache = options.coursier.coursierCache(logger.coursierLogger(""), logger) for (key <- all) { val path = os.Path(key, os.pwd) diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishSetup.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishSetup.scala index a01a47196b..e23a9f67d9 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishSetup.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishSetup.scala @@ -42,7 +42,7 @@ object PublishSetup extends ScalaCommand[PublishSetupOptions] { ): Unit = { Publish.maybePrintLicensesAndExit(options.publishParams) - val coursierCache = options.coursier.coursierCache(logger.coursierLogger("")) + val coursierCache = options.coursier.coursierCache(logger.coursierLogger(""), logger) val directories = Directories.directories lazy val configDb = ConfigDbUtils.configDb.orExit(logger) 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 18c17ca1b5..0cfd9631f4 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 @@ -5,8 +5,11 @@ import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import coursier.cache.{CacheLogger, CachePolicy, FileCache} +import scala.build.Logger import scala.build.internals.EnvVar import scala.cli.commands.tags +import scala.cli.config.Keys +import scala.cli.util.ConfigDbUtils import scala.concurrent.duration.Duration // format: off @@ -39,8 +42,8 @@ final case class CoursierOptions( private def validateChecksums = coursierValidateChecksums.getOrElse(true) - def coursierCache(logger: CacheLogger) = { - var baseCache = FileCache().withLogger(logger) + def coursierCache(cacheLogger: CacheLogger, cliLogger: Logger) = { + var baseCache = FileCache().withLogger(cacheLogger) if (!validateChecksums) baseCache = baseCache.withChecksums(Nil) val ttlOpt = ttl.map(_.trim).filter(_.nonEmpty).map(Duration(_)) @@ -48,15 +51,16 @@ final case class CoursierOptions( baseCache = baseCache.withTtl(ttl0) for (loc <- cache.filter(_.trim.nonEmpty)) baseCache = baseCache.withLocation(loc) - for (isOffline <- getOffline() if isOffline) + for (isOffline <- getOffline(cliLogger) if isOffline) baseCache = baseCache.withCachePolicies(Seq(CachePolicy.LocalOnly)) baseCache } - def getOffline(): Option[Boolean] = offline + def getOffline(logger: Logger): Option[Boolean] = offline .orElse(EnvVar.Coursier.coursierMode.valueOpt.map(_ == "offline")) .orElse(Option(System.getProperty("coursier.mode")).map(_ == "offline")) + .orElse(ConfigDbUtils.getConfigDbOpt(logger).flatMap(_.get(Keys.offline).toOption.flatten)) } object CoursierOptions { 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 f3272d62ab..3115767a97 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 @@ -442,7 +442,7 @@ final case class SharedOptions( strictBloopJsonCheck = strictBloopJsonCheck, interactive = Some(() => interactive), exclude = exclude.map(Positioned.commandLine), - offline = coursier.getOffline() + offline = coursier.getOffline(logger) ), notForBloopOptions = bo.PostBuildOptions( scalaJsLinkerOptions = linkerOptions(js), @@ -595,11 +595,11 @@ final case class SharedOptions( options => bloopRifleConfig(Some(options)), threads.bloop, strictBloopJsonCheckOrDefault, - coursier.getOffline().getOrElse(false) + coursier.getOffline(logger).getOrElse(false) ) else SimpleScalaCompilerMaker("java", Nil) - lazy val coursierCache = coursier.coursierCache(logging.logger.coursierLogger("")) + lazy val coursierCache = coursier.coursierCache(logging.logger.coursierLogger(""), logger) def inputs( args: Seq[String], diff --git a/modules/cli/src/main/scala/scala/cli/launcher/LauncherCli.scala b/modules/cli/src/main/scala/scala/cli/launcher/LauncherCli.scala index bfbbf6a104..5525e72e56 100644 --- a/modules/cli/src/main/scala/scala/cli/launcher/LauncherCli.scala +++ b/modules/cli/src/main/scala/scala/cli/launcher/LauncherCli.scala @@ -21,7 +21,7 @@ object LauncherCli { def runAndExit(version: String, options: LauncherOptions, remainingArgs: Seq[String]): Nothing = { val logger = LoggingOptions().logger - val cache = CoursierOptions().coursierCache(logger.coursierLogger("")) + val cache = CoursierOptions().coursierCache(logger.coursierLogger(""), logger) val scalaVersion = options.cliScalaVersion.getOrElse(scalaCliScalaVersion(version)) val scalaParameters = ScalaParameters(scalaVersion) val snapshotsRepo = Seq(Repositories.central, Repositories.sonatype("snapshots")) diff --git a/modules/cli/src/test/scala/cli/tests/LauncherCliTest.scala b/modules/cli/src/test/scala/cli/tests/LauncherCliTest.scala index fa9c92a040..2ab45c025f 100644 --- a/modules/cli/src/test/scala/cli/tests/LauncherCliTest.scala +++ b/modules/cli/src/test/scala/cli/tests/LauncherCliTest.scala @@ -5,14 +5,16 @@ import dependency.ScalaParameters import scala.build.internal.Constants import scala.build.tests.TestLogger import scala.cli.commands.shared.CoursierOptions +import scala.cli.internal.CliLogger import scala.cli.launcher.LauncherCli class LauncherCliTest extends munit.FunSuite { override def munitFlakyOK: Boolean = TestUtil.isCI test("resolve nightly version".flaky) { - val logger = TestLogger() - val cache = CoursierOptions().coursierCache(logger.coursierLogger("")) + val cacheLogger = TestLogger() + val cliLogger = CliLogger.default + val cache = CoursierOptions().coursierCache(cacheLogger.coursierLogger(""), cliLogger) val scalaParameters = ScalaParameters(Constants.defaultScalaVersion) val nightlyCliVersion = LauncherCli.resolveNightlyScalaCliVersion(cache, scalaParameters) diff --git a/modules/config/src/main/scala/scala/cli/config/Keys.scala b/modules/config/src/main/scala/scala/cli/config/Keys.scala index 4b625d71f6..448a2ba5fa 100644 --- a/modules/config/src/main/scala/scala/cli/config/Keys.scala +++ b/modules/config/src/main/scala/scala/cli/config/Keys.scala @@ -70,6 +70,13 @@ object Keys { description = "Globally enables power mode (the '--power' launcher flag)." ) + val offline = new Key.BooleanEntry( + prefix = Seq.empty, + name = "offline", + specificationLevel = SpecificationLevel.EXPERIMENTAL, + description = "Globally enables offline mode (the '--offline' flag)." + ) + val suppressDirectivesInMultipleFilesWarning = new Key.BooleanEntry( prefix = Seq("suppress-warning"), @@ -176,6 +183,7 @@ object Keys { pgpSecretKey, pgpSecretKeyPassword, power, + offline, proxyAddress, proxyPassword, proxyUser, diff --git a/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala b/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala index 327f161350..588d242916 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala @@ -560,4 +560,45 @@ class ConfigTests extends ScalaCliSuite { } } + for { + offlineSetting <- Seq(true, false) + prefillCache <- if (offlineSetting) Seq(true, false) else Seq(false) + caption = s"offline mode: $offlineSetting, " + + (offlineSetting -> prefillCache match { + case (true, true) => "build should succeed when cache was pre-filled" + case (true, false) => "build should fail when cache is empty" + case _ => "dependencies should be downloaded as normal" + }) + } + test(caption) { + TestInputs( + os.rel / "simple.sc" -> "println(dotty.tools.dotc.config.Properties.versionNumberString)" + ) + .fromRoot { root => + val configFile = os.rel / "config" / "config.json" + val localRepoPath = root / "local-repo" + val envs = Map( + "COURSIER_CACHE" -> localRepoPath.toString, + "SCALA_CLI_CONFIG" -> configFile.toString + ) + os.proc(TestUtil.cli, "bloop", "exit", "--power").call(cwd = root) + os.proc(TestUtil.cli, "config", "--power", "offline", offlineSetting.toString) + .call(cwd = root, env = envs) + if (prefillCache) for { + artifactName <- Seq( + "scala3-compiler_3", + "scala3-staging_3", + "scala3-tasty-inspector_3", + "scala3-sbt-bridge" + ) + artifact = s"org.scala-lang:$artifactName:${Constants.scala3Next}" + } os.proc(TestUtil.cs, "fetch", "--cache", localRepoPath, artifact).call(cwd = root) + val buildExpectedToSucceed = !offlineSetting || prefillCache + val r = os.proc(TestUtil.cli, "run", "simple.sc", "--with-compiler") + .call(cwd = root, env = envs, check = buildExpectedToSucceed) + if (buildExpectedToSucceed) expect(r.out.trim() == Constants.scala3Next) + else expect(r.exitCode == 1) + } + } + } diff --git a/website/docs/guides/power/offline.md b/website/docs/guides/power/offline.md index 14ca81e925..4bac876ea5 100644 --- a/website/docs/guides/power/offline.md +++ b/website/docs/guides/power/offline.md @@ -39,6 +39,11 @@ or scala-cli -Dcoursier.mode=offline run Main.scala ``` +Finally, it's possible to enable offline mode via global config: +```bash ignore +scala-cli --power config offline true +``` + ## Changes in behaviour ### Scala artifacts diff --git a/website/docs/reference/commands.md b/website/docs/reference/commands.md index 62c4cad0e6..eb26c688a0 100644 --- a/website/docs/reference/commands.md +++ b/website/docs/reference/commands.md @@ -56,6 +56,7 @@ Available keys: - interactive Globally enables interactive mode (the '--interactive' flag). - interactive-was-suggested Setting indicating if the global interactive mode was already suggested. - java.properties Java properties for Scala CLI's execution. + - offline Globally enables offline mode (the '--offline' flag). - pgp.public-key The PGP public key, used for signing. - pgp.secret-key The PGP secret key, used for signing. - pgp.secret-key-password The PGP secret key password, used for signing. diff --git a/website/docs/reference/scala-command/commands.md b/website/docs/reference/scala-command/commands.md index e1afc27a37..504cca9a76 100644 --- a/website/docs/reference/scala-command/commands.md +++ b/website/docs/reference/scala-command/commands.md @@ -55,6 +55,7 @@ Available keys: - interactive Globally enables interactive mode (the '--interactive' flag). - interactive-was-suggested Setting indicating if the global interactive mode was already suggested. - java.properties Java properties for Scala CLI's execution. + - offline Globally enables offline mode (the '--offline' flag). - pgp.public-key The PGP public key, used for signing. - pgp.secret-key The PGP secret key, used for signing. - pgp.secret-key-password The PGP secret key password, used for signing. diff --git a/website/docs/reference/scala-command/runner-specification.md b/website/docs/reference/scala-command/runner-specification.md index 455ff0852a..0692f5459e 100644 --- a/website/docs/reference/scala-command/runner-specification.md +++ b/website/docs/reference/scala-command/runner-specification.md @@ -659,6 +659,7 @@ Available keys: - interactive Globally enables interactive mode (the '--interactive' flag). - interactive-was-suggested Setting indicating if the global interactive mode was already suggested. - java.properties Java properties for Scala CLI's execution. + - offline Globally enables offline mode (the '--offline' flag). - pgp.public-key The PGP public key, used for signing. - pgp.secret-key The PGP secret key, used for signing. - pgp.secret-key-password The PGP secret key password, used for signing.