From 5a242149a6d1c4c4d95fd53204738de8f5457879 Mon Sep 17 00:00:00 2001 From: Alex Archambault Date: Wed, 3 Jun 2026 16:39:33 +0200 Subject: [PATCH 1/4] Send launcher messages to stderr So that this doesn't polute the output of a scala-cli command, if users pipe it to a file or another command for example --- scala-cli.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scala-cli.sh b/scala-cli.sh index 0c436b7ae2..534eeb2384 100755 --- a/scala-cli.sh +++ b/scala-cli.sh @@ -39,7 +39,7 @@ elif [ "$(uname)" == "Darwin" ]; then exit 2 fi else - echo "This standalone scala-cli launcher is supported only in Linux and macOS. If you are using Windows, please use the dedicated launcher scala-cli.bat" + echo "This standalone scala-cli launcher is supported only in Linux and macOS. If you are using Windows, please use the dedicated launcher scala-cli.bat" 1>&2 exit 1 fi @@ -49,7 +49,7 @@ SCALA_CLI_BIN_PATH=${CACHE_DEST%.gz} if [ ! -f "$CACHE_DEST" ]; then mkdir -p "$(dirname "$CACHE_DEST")" TMP_DEST="$CACHE_DEST.tmp-setup" - echo "Downloading $SCALA_CLI_URL" + echo "Downloading $SCALA_CLI_URL" 1>&2 curl -fLo "$TMP_DEST" "$SCALA_CLI_URL" mv "$TMP_DEST" "$CACHE_DEST" fi From 73ddd3ed376b511f015ebfd000a1f6a078b0abc4 Mon Sep 17 00:00:00 2001 From: Alex Archambault Date: Wed, 3 Jun 2026 17:03:50 +0200 Subject: [PATCH 2/4] Fix use of undefined command echoerr in scala-cli.sh --- scala-cli.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scala-cli.sh b/scala-cli.sh index 534eeb2384..fe93554cce 100755 --- a/scala-cli.sh +++ b/scala-cli.sh @@ -23,7 +23,7 @@ if [ "$(expr substr $(uname -s) 1 5 2>/dev/null)" == "Linux" ]; then if [[ "$arch" == "aarch64" ]] || [[ "$arch" == "x86_64" ]]; then SCALA_CLI_URL="https://github.com/$GH_ORG/$GH_NAME/releases/download/$TAG/scala-cli-${arch}-pc-linux.gz" else - echoerr "scala-cli is not supported on $arch" + echo "scala-cli is not supported on $arch" 1>&2 exit 2 fi CACHE_BASE="$HOME/.cache/coursier/v1" @@ -35,7 +35,7 @@ elif [ "$(uname)" == "Darwin" ]; then elif [[ "$arch" == "arm64" ]]; then SCALA_CLI_URL="https://github.com/$GH_ORG/$GH_NAME/releases/download/$TAG/scala-cli-aarch64-apple-darwin.gz" else - echoerr "scala-cli is not supported on $arch" + echo "scala-cli is not supported on $arch" 1>&2 exit 2 fi else From eebf7a174240aaa862a358729ea801330f49d954 Mon Sep 17 00:00:00 2001 From: Alex Archambault Date: Wed, 3 Jun 2026 17:06:13 +0200 Subject: [PATCH 3/4] Respect COURSIER_CACHE in scala-cli.sh --- scala-cli.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scala-cli.sh b/scala-cli.sh index fe93554cce..fd14dd3725 100755 --- a/scala-cli.sh +++ b/scala-cli.sh @@ -26,10 +26,10 @@ if [ "$(expr substr $(uname -s) 1 5 2>/dev/null)" == "Linux" ]; then echo "scala-cli is not supported on $arch" 1>&2 exit 2 fi - CACHE_BASE="$HOME/.cache/coursier/v1" + CACHE_BASE="${COURSIER_CACHE:-"$HOME/.cache/coursier/v1"}" elif [ "$(uname)" == "Darwin" ]; then arch=$(uname -m) - CACHE_BASE="$HOME/Library/Caches/Coursier/v1" + CACHE_BASE="${COURSIER_CACHE:-"$HOME/Library/Caches/Coursier/v1"}" if [[ "$arch" == "x86_64" ]]; then SCALA_CLI_URL="https://github.com/$GH_ORG/$GH_NAME/releases/download/$TAG/scala-cli-x86_64-apple-darwin.gz" elif [[ "$arch" == "arm64" ]]; then From cb417c5e6021048ec9a6feccfb481bbfdd7bcce5 Mon Sep 17 00:00:00 2001 From: Alex Archambault Date: Wed, 3 Jun 2026 15:54:21 +0000 Subject: [PATCH 4/4] Add integration tests for scala-cli.sh launcher script Verify the launcher's own messages (e.g. "Downloading ...") go to stderr rather than stdout, so that only the dummy app's output ends up on stdout - both on a cold cache (when the launcher gets downloaded) and on a warm one. The script path is passed in from the build via forkEnv, and COURSIER_CACHE is pointed at a fresh per-test directory. Co-Authored-By: Claude Opus 4.8 --- build.mill | 5 +- .../cli/integration/ShellLauncherTests.scala | 63 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 modules/integration/src/test/scala/scala/cli/integration/ShellLauncherTests.scala diff --git a/build.mill b/build.mill index 49b206e44a..de9a2b911e 100644 --- a/build.mill +++ b/build.mill @@ -169,6 +169,8 @@ object integration extends CliIntegration { object `docs-tests` extends Cross[DocsTests](Scala.scala3MainVersions) with CrossScalaDefaultToInternal +def scalaCliSh = Task.Source("scala-cli.sh") + trait DocsTests extends CrossSbtModule with ScalaCliScalafixModule with LocatedInModules with HasTests { main => override def mvnDeps: T[Seq[Dep]] = Seq( @@ -1037,7 +1039,8 @@ trait CliIntegration extends SbtModule override def forkEnv: T[Map[String, String]] = super.forkEnv() ++ Seq( "SCALA_CLI_TMP" -> tmpDirBase().path.toString, "SCALA_CLI_PRINT_STACK_TRACES" -> "1", - "SCALA_CLI_CONFIG" -> (tmpDirBase().path / "config" / "config.json").toString + "SCALA_CLI_CONFIG" -> (tmpDirBase().path / "config" / "config.json").toString, + "SCALA_CLI_SHELL_LAUNCHER" -> scalaCliSh().path.toString ) def constantsFile: T[PathRef] = Task(persistent = true) { diff --git a/modules/integration/src/test/scala/scala/cli/integration/ShellLauncherTests.scala b/modules/integration/src/test/scala/scala/cli/integration/ShellLauncherTests.scala new file mode 100644 index 0000000000..77799fb35d --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/ShellLauncherTests.scala @@ -0,0 +1,63 @@ +package scala.cli.integration + +import com.eed3si9n.expecty.Expecty.expect + +import scala.concurrent.duration.{Duration, DurationInt} +import scala.util.Properties + +class ShellLauncherTests extends ScalaCliSuite { + + // downloading the launcher + a JVM + the compiler on a cold cache can take a while + override def munitTimeout: Duration = + 10.minutes + + private lazy val launcherScript: os.Path = { + val path = Option(System.getenv("SCALA_CLI_SHELL_LAUNCHER")).getOrElse { + sys.error("SCALA_CLI_SHELL_LAUNCHER not set") + } + os.Path(path) + } + + private def hasCachedLaunchers(cache: os.Path): Boolean = + os.walk(cache).exists { p => + val subPath = p.subRelativeTo(cache) + p.last.startsWith("scala-cli-") && subPath.segments.contains("github.com") && os.isFile(p) + } + + if (!Properties.isWin) + test("only the app output goes to stdout") { + stdoutTest() + } + + def stdoutTest(): Unit = { + val appMessage = "Hello from the dummy app" + val appRelPath = os.rel / "app.sc" + + TestInputs( + appRelPath -> s"""println("$appMessage")""" + ).fromRoot { root => + val cache = root / "cs-cache" + os.makeDir.all(cache) + + // sanity check: the launcher really isn't in the cache to begin with + expect(!hasCachedLaunchers(cache)) + + def runLauncher(): os.CommandResult = + os.proc(launcherScript, "run", "--server=false", appRelPath).call( + cwd = root, + env = Map("COURSIER_CACHE" -> cache.toString) + ) + + val res = runLauncher() + + // the launcher's own messages (e.g. "Downloading ...") must not leak to stdout + expect(res.out.trim() == appMessage) + // the launcher got downloaded under COURSIER_CACHE + expect(hasCachedLaunchers(cache)) + + // second run, with the launcher already present in the cache + val res1 = runLauncher() + expect(res1.out.trim() == appMessage) + } + } +}