diff --git a/build.mill b/build.mill index aa388888b0..f98bd81673 100644 --- a/build.mill +++ b/build.mill @@ -420,6 +420,7 @@ trait Core extends ScalaCliCrossSbtModule .exclude(("com.github.plokhotnyuk.jsoniter-scala", "jsoniter-scala-macros")) // Let's favor our config module rather than the one coursier pulls .exclude((organization, "config_2.13")) + .exclude((organization, "config_3")) .exclude(("org.scala-lang.modules", "scala-collection-compat_2.13")), Deps.dependency, Deps.guava, // for coursierJvm / scalaJsEnvNodeJs, see above @@ -927,7 +928,7 @@ trait Cli extends CrossSbtModule with ProtoBuildModule with CliLaunchers Deps.caseApp, Deps.coursierLauncher, Deps.coursierProxySetup, - Deps.coursierPublish.exclude((organization, "config_2.13")), + Deps.coursierPublish.exclude((organization, "config_2.13")).exclude((organization, "config_3")), Deps.jimfs, // scalaJsEnvNodeJs pulls jimfs:1.1, whose class path seems borked (bin compat issue with the guava version it depends on) Deps.jniUtils, Deps.jsoniterCore, @@ -935,7 +936,7 @@ trait Cli extends CrossSbtModule with ProtoBuildModule with CliLaunchers Deps.metaconfigTypesafe, Deps.pythonNativeLibs, Deps.scalaPackager.exclude("com.lihaoyi" -> "os-lib_2.13"), - Deps.signingCli.exclude((organization, "config_2.13")), + Deps.signingCli.exclude((organization, "config_2.13")).exclude((organization, "config_3")), Deps.slf4jNop, // to silence jgit Deps.sttp, Deps.scalafixInterfaces, 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 01d9f7b2d1..84d1c0080b 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 @@ -17,7 +17,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, cacheLoggerPrefix = "Downloading Bloop"), 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 6a751ea7c5..c4a392309c 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 @@ -19,7 +19,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, cacheLoggerPrefix = "Downloading Bloop"), // 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 27ba8a8bbc..aa90126380 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 @@ -22,14 +22,12 @@ object BloopStart extends ScalaCommand[BloopStartOptions] { import opts.* val buildOptions = BuildOptions( javaOptions = JvmUtils.javaOptions(jvm).orExit(global.logging.logger), - internal = InternalOptions( - cache = Some(coursier.coursierCache(global.logging.logger.coursierLogger(""))) - ) + internal = InternalOptions(cache = Some(coursier.coursierCache(global.logging.logger))) ) compilationServer.bloopRifleConfig( global.logging.logger, - coursier.coursierCache(global.logging.logger.coursierLogger("Downloading Bloop")), + coursier.coursierCache(global.logging.logger, cacheLoggerPrefix = "Downloading Bloop"), 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 5954c3f79f..1e95199966 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 @@ -45,7 +45,7 @@ object Config extends ScalaCommand[ConfigOptions] { ) sys.exit(1) } - val coursierCache = options.coursier.coursierCache(logger.coursierLogger("")) + val coursierCache = options.coursier.coursierCache(logger) val secKeyEntry = Keys.pgpSecretKey val pubKeyEntry = Keys.pgpPublicKey @@ -132,7 +132,7 @@ object Config extends ScalaCommand[ConfigOptions] { System.err.println(err) sys.exit(1) case Right(passwordOption) => - val password = passwordOption.getBytes() + val password = passwordOption.getBytes System.out.write(password.value) } else diff --git a/modules/cli/src/main/scala/scala/cli/commands/default/Default.scala b/modules/cli/src/main/scala/scala/cli/commands/default/Default.scala index d944eb0f43..8a76021d96 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/default/Default.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/default/Default.scala @@ -43,7 +43,7 @@ class Default(actualHelp: => RuntimeCommandsHelp) Version.runCommand( options = VersionOptions( global = options.shared.global, - offline = options.shared.coursier.getOffline().getOrElse(false) + offline = options.shared.coursier.getOffline(logger).getOrElse(false) ), args = args, logger = logger 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 9221ed2a21..60b26cde78 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) 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 4499fc9ec5..afbe4169e5 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 @@ -27,7 +27,7 @@ object New extends ScalaCommand[NewOptions] { Seq.empty, Some(scalaParameters), logger, - CoursierOptions().coursierCache(logger.coursierLogger("")), + CoursierOptions().coursierCache(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 0825ea6d36..3af2cd7f03 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 @@ -100,7 +100,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) 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 b890d02e9c..25587baed9 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 @@ -28,7 +28,7 @@ object PgpPush extends ScalaCommand[PgpPushOptions] { sys.exit(1) } - lazy val coursierCache = options.coursier.coursierCache(logger.coursierLogger("")) + lazy val coursierCache = options.coursier.coursierCache(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 c6f9496c9e..a586a23594 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) 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..9b91dd5709 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 @@ -4,9 +4,13 @@ import caseapp.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import coursier.cache.{CacheLogger, CachePolicy, FileCache} +import coursier.util.Task +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 +43,8 @@ final case class CoursierOptions( private def validateChecksums = coursierValidateChecksums.getOrElse(true) - def coursierCache(logger: CacheLogger) = { - var baseCache = FileCache().withLogger(logger) + def coursierCache(logger: Logger, cacheLogger: CacheLogger): FileCache[Task] = { + var baseCache = FileCache().withLogger(cacheLogger) if (!validateChecksums) baseCache = baseCache.withChecksums(Nil) val ttlOpt = ttl.map(_.trim).filter(_.nonEmpty).map(Duration(_)) @@ -48,15 +52,19 @@ 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(logger) if isOffline) baseCache = baseCache.withCachePolicies(Seq(CachePolicy.LocalOnly)) baseCache } - def getOffline(): Option[Boolean] = offline + def coursierCache(logger: Logger, cacheLoggerPrefix: String = ""): FileCache[Task] = + coursierCache(logger, logger.coursierLogger(cacheLoggerPrefix)) + + 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 8371b1cd3a..fe4b8903d9 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 @@ -431,7 +431,7 @@ final case class SharedOptions( strictBloopJsonCheck = strictBloopJsonCheck, interactive = Some(() => interactive), exclude = exclude.map(Positioned.commandLine), - offline = coursier.getOffline() + offline = coursier.getOffline(logger) ), notForBloopOptions = scala.build.options.PostBuildOptions( scalaJsLinkerOptions = linkerOptions(js), @@ -607,11 +607,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: FileCache[Task] = coursier.coursierCache(logging.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 a8cfc9133a..fd077c2051 100644 --- a/modules/cli/src/main/scala/scala/cli/launcher/LauncherCli.scala +++ b/modules/cli/src/main/scala/scala/cli/launcher/LauncherCli.scala @@ -26,7 +26,7 @@ object LauncherCli { ): Either[BuildException, Nothing] = either { val logger = LoggingOptions().logger - val cache = CoursierOptions().coursierCache(logger.coursierLogger("")) + val cache = CoursierOptions().coursierCache(logger) val scalaVersion = options.cliScalaVersion.getOrElse(scalaCliScalaVersion(version)) val scalaParameters = ScalaParameters(scalaVersion) val snapshotsRepo = Seq( diff --git a/modules/cli/src/test/scala/cli/tests/LauncherCliTest.scala b/modules/cli/src/test/scala/cli/tests/LauncherCliTest.scala index abb0d41435..ef81f7c3ba 100644 --- a/modules/cli/src/test/scala/cli/tests/LauncherCliTest.scala +++ b/modules/cli/src/test/scala/cli/tests/LauncherCliTest.scala @@ -10,7 +10,7 @@ import scala.cli.launcher.LauncherCli class LauncherCliTest extends TestUtil.ScalaCliSuite { test("resolve nightly version".flaky) { val logger = TestLogger() - val cache = CoursierOptions().coursierCache(logger.coursierLogger("")) + val cache = CoursierOptions().coursierCache(logger) val scalaParameters = ScalaParameters(Constants.defaultScalaVersion) val nightlyCliVersion = 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 3c0f2bbc03..3359f883cd 100644 --- a/modules/config/src/main/scala/scala/cli/config/Keys.scala +++ b/modules/config/src/main/scala/scala/cli/config/Keys.scala @@ -67,6 +67,13 @@ object Keys { description = "Globally enables power mode (the '--power' launcher flag)." ) + val offline = new Key.BooleanEntry( + prefix = Seq.empty, + name = "offline", + specificationLevel = SpecificationLevel.IMPLEMENTATION, + description = "Globally enables offline mode (the '--offline' flag)." + ) + val suppressDirectivesInMultipleFilesWarning = new Key.BooleanEntry( prefix = Seq("suppress-warning"), @@ -177,6 +184,7 @@ object Keys { suppressOutdatedDependenciessWarning, suppressExperimentalFeatureWarning, suppressDeprecatedFeatureWarning, + offline, pgpPublicKey, pgpSecretKey, pgpSecretKeyPassword, 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 5b2dd93ad5..4647fb31ab 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala @@ -562,4 +562,48 @@ 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", "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) + os.proc(TestUtil.cli, "config", "offline", "--unset") + .call(cwd = root, env = envs) + os.proc(TestUtil.cli, "bloop", "exit", "--power").call(cwd = root) + } + } + } diff --git a/project/deps/package.mill b/project/deps/package.mill index b7e599e170..f12ef82f94 100644 --- a/project/deps/package.mill +++ b/project/deps/package.mill @@ -257,6 +257,7 @@ object Deps { .exclude(("com.github.plokhotnyuk.jsoniter-scala", "jsoniter-scala-macros_3")) .exclude(("com.lihaoyi", "os-lib_3")) .exclude(("com.lihaoyi", "os-lib_2.13")) + .exclude(("org.virtuslab.scala-cli", "config_3")) def signingCli = mvn"org.virtuslab.scala-cli-signing::cli:${Versions.signingCli}" // to prevent collisions with scala-cli's case-app version 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 173c89f8dd..8b5b788835 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 ab81f865fd..9d190bca31 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 d1598abddc..65024b3e0a 100644 --- a/website/docs/reference/scala-command/runner-specification.md +++ b/website/docs/reference/scala-command/runner-specification.md @@ -677,6 +677,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.