From a86f8cad1a0c14d7cdd7652637c86be7d0637a20 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Thu, 15 May 2025 18:37:32 +0200 Subject: [PATCH 1/6] Support links in using file directive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Pawlik Co-authored-by: tgodzik --- .../main/scala/scala/build/CrossSources.scala | 46 ++++++++++++++++--- .../preprocessing/directives/Sources.scala | 16 ++++++- .../scala/build/options/InternalOptions.scala | 4 +- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/modules/build/src/main/scala/scala/build/CrossSources.scala b/modules/build/src/main/scala/scala/build/CrossSources.scala index c7e731752a..3d7584364c 100644 --- a/modules/build/src/main/scala/scala/build/CrossSources.scala +++ b/modules/build/src/main/scala/scala/build/CrossSources.scala @@ -1,7 +1,10 @@ package scala.build -import java.io.File +import coursier.cache.FileCache +import coursier.util.Artifact +import coursier.util.Task +import java.io.File import scala.build.CollectionOps.* import scala.build.EitherCps.{either, value} import scala.build.Ops.* @@ -209,7 +212,8 @@ object CrossSources { .flatMap(_.options) .flatMap(_.internal.extraSourceFiles) .distinct - val inputsElemFromDirectives: Seq[SingleFile] = + + val inputsElemFromDirectives: Seq[SingleElement] = value(resolveInputsFromSources(sourcesFromDirectives, inputs.enableMarkdown)) val preprocessedSourcesFromDirectives: Seq[PreprocessedSource] = value(preprocessSources(inputsElemFromDirectives.pipe(elements => @@ -403,7 +407,39 @@ object CrossSources { fromInputs ++ fromSources ++ fromSourcesWithRequirements } - private def resolveInputsFromSources(sources: Seq[Positioned[os.Path]], enableMarkdown: Boolean) = + // TODO: reuse existing one? e.g. scala.cli.commands.shared.SharedOptions.coursierCache + lazy val fileCache: FileCache[coursier.util.Task] = FileCache() + + private def downloadFile(pUri: Positioned[java.net.URI]) = + import scala.build.options.ScalaVersionUtil.fileWithTtl0 + val artifact = Artifact(pUri.value.toString).withChanging(true) + fileCache.fileWithTtl0(artifact) + .left + .map(err => + new MalformedDirectiveError(err.describe, pUri.positions) + ) // TODO: better erorr type + .map(f => os.read.bytes(os.Path(f, Os.pwd))).map(content => + Seq(Virtual(pUri.value.toString, content)) + ) + + type CodeFile = os.Path | java.net.URI + + private def resolveInputsFromSources( + sources: Seq[Positioned[CodeFile]], + enableMarkdown: Boolean + ) = + val links = sources.collect { + case Positioned(pos, value: java.net.URI) => Positioned(pos, value) + } + val paths = sources.collect { + case Positioned(pos, value: os.Path) => Positioned(pos, value) + } + + (resolveInputsFromPath(paths, enableMarkdown) ++ links.map(downloadFile)).sequence + .left.map(CompositeBuildException(_)) + .map(_.flatten) + + private def resolveInputsFromPath(sources: Seq[Positioned[os.Path]], enableMarkdown: Boolean) = sources.map { source => val sourcePath = source.value lazy val dir = sourcePath / os.up @@ -424,9 +460,7 @@ object CrossSources { else s"$sourcePath: not found path defined in using directive." Left(new MalformedDirectiveError(msg, source.positions)) } - }.sequence - .left.map(CompositeBuildException(_)) - .map(_.flatten) + } /** Filters out the sources from the input sequence based on the provided 'exclude' patterns. The * exclude patterns can be absolute paths, relative paths, or glob patterns. diff --git a/modules/directives/src/main/scala/scala/build/preprocessing/directives/Sources.scala b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Sources.scala index 82b1128826..169ff2e908 100644 --- a/modules/directives/src/main/scala/scala/build/preprocessing/directives/Sources.scala +++ b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Sources.scala @@ -27,6 +27,17 @@ final case class Sources( files: DirectiveValueParser.WithScopePath[List[Positioned[String]]] = DirectiveValueParser.WithScopePath.empty(Nil) ) extends HasBuildOptions { + + private def codeFile(codeFile: String, root: os.Path): Sources.CodeFile = + scala.util.Try { + val uri = java.net.URI.create(codeFile) + uri.getScheme match { + case "file" | "http" | "https" | "git+ssh" => uri + } + }.getOrElse { + os.Path(codeFile, root) + } + def buildOptions: Either[BuildException, BuildOptions] = either { val paths = files @@ -35,7 +46,7 @@ final case class Sources( for { root <- Directive.osRoot(files.scopePath, positioned.positions.headOption) path <- { - try Right(positioned.map(os.Path(_, root))) + try Right(positioned.map(codeFile(_, root))) catch { case e: IllegalArgumentException => Left(new WrongSourcePathError(positioned.value, e, positioned.positions)) @@ -55,5 +66,8 @@ final case class Sources( } object Sources { + + type CodeFile = os.Path | java.net.URI + val handler: DirectiveHandler[Sources] = DirectiveHandler.derive } 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 f9757edba7..20a8d917f8 100644 --- a/modules/options/src/main/scala/scala/build/options/InternalOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/InternalOptions.scala @@ -8,6 +8,8 @@ import scala.build.errors.BuildException import scala.build.interactive.Interactive import scala.build.interactive.Interactive.InteractiveNop +type CodeFile = os.Path | java.net.URI + final case class InternalOptions( keepDiagnostics: Boolean = false, cache: Option[FileCache[Task]] = None, @@ -24,7 +26,7 @@ final case class InternalOptions( * really needed. */ keepResolution: Boolean = false, - extraSourceFiles: Seq[Positioned[os.Path]] = Nil, + extraSourceFiles: Seq[Positioned[CodeFile]] = Nil, exclude: Seq[Positioned[String]] = Nil, offline: Option[Boolean] = None ) { From 6a90821d31ce4bb2e6ff929bfdfc59977189351e Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Fri, 16 May 2025 10:06:58 +0200 Subject: [PATCH 2/6] remove git+ssh, add remote example --- modules/build/src/main/scala/scala/build/CrossSources.scala | 6 ++---- .../scala/build/preprocessing/directives/Sources.scala | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/build/src/main/scala/scala/build/CrossSources.scala b/modules/build/src/main/scala/scala/build/CrossSources.scala index 3d7584364c..4806589064 100644 --- a/modules/build/src/main/scala/scala/build/CrossSources.scala +++ b/modules/build/src/main/scala/scala/build/CrossSources.scala @@ -1,10 +1,8 @@ package scala.build import coursier.cache.FileCache -import coursier.util.Artifact -import coursier.util.Task +import coursier.util.{Artifact, Task} -import java.io.File import scala.build.CollectionOps.* import scala.build.EitherCps.{either, value} import scala.build.Ops.* @@ -417,7 +415,7 @@ object CrossSources { .left .map(err => new MalformedDirectiveError(err.describe, pUri.positions) - ) // TODO: better erorr type + ) // TODO: better error type .map(f => os.read.bytes(os.Path(f, Os.pwd))).map(content => Seq(Virtual(pUri.value.toString, content)) ) diff --git a/modules/directives/src/main/scala/scala/build/preprocessing/directives/Sources.scala b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Sources.scala index 169ff2e908..861af92240 100644 --- a/modules/directives/src/main/scala/scala/build/preprocessing/directives/Sources.scala +++ b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Sources.scala @@ -11,6 +11,7 @@ import scala.util.Try @DirectiveGroupName("Custom sources") @DirectiveExamples("//> using file utils.scala") +@DirectiveExamples("//> using file https://raw.githubusercontent.com/softwaremill/sttp/refs/heads/master/examples/src/main/scala/sttp/client4/examples/json/GetAndParseJsonCatsEffectCirce.scala") @DirectiveUsage( "`//> using file `_path_ | `//> using files `_path1_ _path2_ …", """`//> using file` _path_ @@ -32,7 +33,7 @@ final case class Sources( scala.util.Try { val uri = java.net.URI.create(codeFile) uri.getScheme match { - case "file" | "http" | "https" | "git+ssh" => uri + case "file" | "http" | "https" => uri } }.getOrElse { os.Path(codeFile, root) From d29327e116980f51318e03fd89fedd8984d040b5 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Fri, 16 May 2025 19:15:31 +0200 Subject: [PATCH 3/6] scalafix & updated docs --- .../scala/scala/build/preprocessing/directives/Sources.scala | 4 +++- website/docs/reference/directives.md | 2 ++ website/docs/reference/scala-command/directives.md | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/directives/src/main/scala/scala/build/preprocessing/directives/Sources.scala b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Sources.scala index 861af92240..3a767f4b19 100644 --- a/modules/directives/src/main/scala/scala/build/preprocessing/directives/Sources.scala +++ b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Sources.scala @@ -11,7 +11,9 @@ import scala.util.Try @DirectiveGroupName("Custom sources") @DirectiveExamples("//> using file utils.scala") -@DirectiveExamples("//> using file https://raw.githubusercontent.com/softwaremill/sttp/refs/heads/master/examples/src/main/scala/sttp/client4/examples/json/GetAndParseJsonCatsEffectCirce.scala") +@DirectiveExamples( + "//> using file https://raw.githubusercontent.com/softwaremill/sttp/refs/heads/master/examples/src/main/scala/sttp/client4/examples/json/GetAndParseJsonCatsEffectCirce.scala" +) @DirectiveUsage( "`//> using file `_path_ | `//> using files `_path1_ _path2_ …", """`//> using file` _path_ diff --git a/website/docs/reference/directives.md b/website/docs/reference/directives.md index 5bf70d278b..f434c98f6e 100644 --- a/website/docs/reference/directives.md +++ b/website/docs/reference/directives.md @@ -126,6 +126,8 @@ Manually add sources to the project. Does not support chaining, sources are adde #### Examples `//> using file utils.scala` +`//> using file https://raw.githubusercontent.com/softwaremill/sttp/refs/heads/master/examples/src/main/scala/sttp/client4/examples/json/GetAndParseJsonCatsEffectCirce.scala` + ### Dependency Add dependencies diff --git a/website/docs/reference/scala-command/directives.md b/website/docs/reference/scala-command/directives.md index a4c35c9b94..c4416cdea5 100644 --- a/website/docs/reference/scala-command/directives.md +++ b/website/docs/reference/scala-command/directives.md @@ -197,6 +197,8 @@ Manually add sources to the project. Does not support chaining, sources are adde #### Examples `//> using file utils.scala` +`//> using file https://raw.githubusercontent.com/softwaremill/sttp/refs/heads/master/examples/src/main/scala/sttp/client4/examples/json/GetAndParseJsonCatsEffectCirce.scala` + ### Exclude sources Exclude sources from the project From 0691f6582a6f21ab0a99f951ed2219af953a12cd Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Wed, 21 May 2025 09:07:41 +0200 Subject: [PATCH 4/6] New error class for Uri file errors --- .../src/main/scala/scala/build/CrossSources.scala | 7 +++---- .../scala/build/errors/UsingFileFromUriError.scala | 12 ++++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 modules/directives/src/main/scala/scala/build/errors/UsingFileFromUriError.scala diff --git a/modules/build/src/main/scala/scala/build/CrossSources.scala b/modules/build/src/main/scala/scala/build/CrossSources.scala index 4806589064..04e3df7396 100644 --- a/modules/build/src/main/scala/scala/build/CrossSources.scala +++ b/modules/build/src/main/scala/scala/build/CrossSources.scala @@ -12,7 +12,8 @@ import scala.build.errors.{ CompositeBuildException, ExcludeDefinitionError, MalformedDirectiveError, - Severity + Severity, + UsingFileFromUriError } import scala.build.input.ElementsUtils.* import scala.build.input.* @@ -413,9 +414,7 @@ object CrossSources { val artifact = Artifact(pUri.value.toString).withChanging(true) fileCache.fileWithTtl0(artifact) .left - .map(err => - new MalformedDirectiveError(err.describe, pUri.positions) - ) // TODO: better error type + .map(cause => new UsingFileFromUriError(pUri.value, pUri.positions, cause)) .map(f => os.read.bytes(os.Path(f, Os.pwd))).map(content => Seq(Virtual(pUri.value.toString, content)) ) diff --git a/modules/directives/src/main/scala/scala/build/errors/UsingFileFromUriError.scala b/modules/directives/src/main/scala/scala/build/errors/UsingFileFromUriError.scala new file mode 100644 index 0000000000..67187679ed --- /dev/null +++ b/modules/directives/src/main/scala/scala/build/errors/UsingFileFromUriError.scala @@ -0,0 +1,12 @@ +package scala.build.errors + +import java.net.URI + +import scala.build.Position + +final class UsingFileFromUriError(uri: URI, positions: Seq[Position], cause: Throwable) + extends BuildException( + message = s"Error using file from $uri - ${cause.getLocalizedMessage}", + positions = positions, + cause = cause + ) From 20fe93aceb086e60d21ac6d73b1a9f19f9346bb7 Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Sat, 24 May 2025 13:12:35 +0200 Subject: [PATCH 5/6] Pass downloader directly --- .../src/main/scala/scala/build/Build.scala | 3 +- .../main/scala/scala/build/CrossSources.scala | 28 ++++++++----------- .../main/scala/scala/build/bsp/BspImpl.scala | 3 +- .../main/scala/scala/build/input/Inputs.scala | 6 ++-- .../scala/scala/cli/commands/bsp/Bsp.scala | 3 +- .../dependencyupdate/DependencyUpdate.scala | 3 +- .../scala/cli/commands/export0/Export.scala | 3 +- .../scala/cli/commands/fix/BuiltInRules.scala | 3 +- .../cli/commands/publish/PublishSetup.scala | 3 +- .../cli/commands/setupide/SetupIde.scala | 3 +- .../cli/commands/shared/SharedOptions.scala | 14 ++-------- .../build/errors/UsingFileFromUriError.scala | 7 ++--- .../scala/build/options/BuildOptions.scala | 19 +++++++++++++ 13 files changed, 55 insertions(+), 43 deletions(-) diff --git a/modules/build/src/main/scala/scala/build/Build.scala b/modules/build/src/main/scala/scala/build/Build.scala index 493ddf3333..404c05ac1e 100644 --- a/modules/build/src/main/scala/scala/build/Build.scala +++ b/modules/build/src/main/scala/scala/build/Build.scala @@ -267,7 +267,8 @@ object Build { ), logger, options.suppressWarningOptions, - options.internal.exclude + options.internal.exclude, + download = options.downloader ) private def build( diff --git a/modules/build/src/main/scala/scala/build/CrossSources.scala b/modules/build/src/main/scala/scala/build/CrossSources.scala index 04e3df7396..9e846c87a6 100644 --- a/modules/build/src/main/scala/scala/build/CrossSources.scala +++ b/modules/build/src/main/scala/scala/build/CrossSources.scala @@ -163,7 +163,8 @@ object CrossSources { logger: Logger, suppressWarningOptions: SuppressWarningOptions, exclude: Seq[Positioned[String]] = Nil, - maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e) + maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e), + download: BuildOptions.Download = BuildOptions.Download.notSupported )(using ScalaCliInvokeData): Either[BuildException, (CrossSources, Inputs)] = either { def preprocessSources(elems: Seq[SingleElement]) @@ -213,7 +214,7 @@ object CrossSources { .distinct val inputsElemFromDirectives: Seq[SingleElement] = - value(resolveInputsFromSources(sourcesFromDirectives, inputs.enableMarkdown)) + value(resolveInputsFromSources(sourcesFromDirectives, inputs.enableMarkdown, download)) val preprocessedSourcesFromDirectives: Seq[PreprocessedSource] = value(preprocessSources(inputsElemFromDirectives.pipe(elements => value(excludeSources(elements, inputs.workspace, allExclude)) @@ -406,24 +407,19 @@ object CrossSources { fromInputs ++ fromSources ++ fromSourcesWithRequirements } - // TODO: reuse existing one? e.g. scala.cli.commands.shared.SharedOptions.coursierCache - lazy val fileCache: FileCache[coursier.util.Task] = FileCache() - - private def downloadFile(pUri: Positioned[java.net.URI]) = - import scala.build.options.ScalaVersionUtil.fileWithTtl0 - val artifact = Artifact(pUri.value.toString).withChanging(true) - fileCache.fileWithTtl0(artifact) - .left - .map(cause => new UsingFileFromUriError(pUri.value, pUri.positions, cause)) - .map(f => os.read.bytes(os.Path(f, Os.pwd))).map(content => - Seq(Virtual(pUri.value.toString, content)) - ) + private def downloadFile(download: BuildOptions.Download)(pUri: Positioned[java.net.URI]) = + download(pUri.value.toString).left.map( + new UsingFileFromUriError(pUri.value, pUri.positions, _) + ).map(content => + Seq(Virtual(pUri.value.toString, content)) + ) type CodeFile = os.Path | java.net.URI private def resolveInputsFromSources( sources: Seq[Positioned[CodeFile]], - enableMarkdown: Boolean + enableMarkdown: Boolean, + download: BuildOptions.Download ) = val links = sources.collect { case Positioned(pos, value: java.net.URI) => Positioned(pos, value) @@ -432,7 +428,7 @@ object CrossSources { case Positioned(pos, value: os.Path) => Positioned(pos, value) } - (resolveInputsFromPath(paths, enableMarkdown) ++ links.map(downloadFile)).sequence + (resolveInputsFromPath(paths, enableMarkdown) ++ links.map(downloadFile(download))).sequence .left.map(CompositeBuildException(_)) .map(_.flatten) diff --git a/modules/build/src/main/scala/scala/build/bsp/BspImpl.scala b/modules/build/src/main/scala/scala/build/bsp/BspImpl.scala index 9313ea8990..991beb600c 100644 --- a/modules/build/src/main/scala/scala/build/bsp/BspImpl.scala +++ b/modules/build/src/main/scala/scala/build/bsp/BspImpl.scala @@ -113,7 +113,8 @@ final class BspImpl( logger = persistentLogger, suppressWarningOptions = buildOptions.suppressWarningOptions, exclude = buildOptions.internal.exclude, - maybeRecoverOnError = maybeRecoverOnError(Scope.Main) + maybeRecoverOnError = maybeRecoverOnError(Scope.Main), + download = buildOptions.downloader ).left.map((_, Scope.Main)) } diff --git a/modules/build/src/main/scala/scala/build/input/Inputs.scala b/modules/build/src/main/scala/scala/build/input/Inputs.scala index 8e93d14632..67895fa0b7 100644 --- a/modules/build/src/main/scala/scala/build/input/Inputs.scala +++ b/modules/build/src/main/scala/scala/build/input/Inputs.scala @@ -11,7 +11,7 @@ import scala.build.errors.{BuildException, InputsException, WorkspaceError} import scala.build.input.ElementsUtils.* import scala.build.internal.Constants import scala.build.internal.zip.WrappedZipInputStream -import scala.build.options.Scope +import scala.build.options.{BuildOptions, Scope} import scala.build.preprocessing.SheBang.isShebangScript import scala.util.matching.Regex import scala.util.{Properties, Try} @@ -225,7 +225,7 @@ object Inputs { def validateArgs( args: Seq[String], cwd: os.Path, - download: String => Either[String, Array[Byte]], + download: BuildOptions.Download, stdinOpt: => Option[Array[Byte]], acceptFds: Boolean, enableMarkdown: Boolean @@ -423,7 +423,7 @@ object Inputs { args: Seq[String], cwd: os.Path, defaultInputs: () => Option[Inputs] = () => None, - download: String => Either[String, Array[Byte]] = _ => Left("URL not supported"), + download: BuildOptions.Download = BuildOptions.Download.notSupported, stdinOpt: => Option[Array[Byte]] = None, scriptSnippetList: List[String] = List.empty, scalaSnippetList: List[String] = List.empty, diff --git a/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala b/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala index 7c2595ac94..71316ddabc 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala @@ -109,7 +109,8 @@ object Bsp extends ScalaCommand[BspOptions] { ), persistentLogger, baseOptions.suppressWarningOptions, - baseOptions.internal.exclude + baseOptions.internal.exclude, + download = baseOptions.downloader ) val (allInputs, finalBuildOptions) = { diff --git a/modules/cli/src/main/scala/scala/cli/commands/dependencyupdate/DependencyUpdate.scala b/modules/cli/src/main/scala/scala/cli/commands/dependencyupdate/DependencyUpdate.scala index d1a3fa898c..b4eca92209 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/dependencyupdate/DependencyUpdate.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/dependencyupdate/DependencyUpdate.scala @@ -45,7 +45,8 @@ object DependencyUpdate extends ScalaCommand[DependencyUpdateOptions] { ), logger, buildOptions.suppressWarningOptions, - buildOptions.internal.exclude + buildOptions.internal.exclude, + download = buildOptions.downloader ).orExit(logger) val sharedOptions = crossSources.sharedOptions(buildOptions) diff --git a/modules/cli/src/main/scala/scala/cli/commands/export0/Export.scala b/modules/cli/src/main/scala/scala/cli/commands/export0/Export.scala index 66641fe192..49691df86d 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/export0/Export.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/export0/Export.scala @@ -50,7 +50,8 @@ object Export extends ScalaCommand[ExportOptions] { ), logger, buildOptions.suppressWarningOptions, - buildOptions.internal.exclude + buildOptions.internal.exclude, + download = buildOptions.downloader ) } diff --git a/modules/cli/src/main/scala/scala/cli/commands/fix/BuiltInRules.scala b/modules/cli/src/main/scala/scala/cli/commands/fix/BuiltInRules.scala index ae88854602..57dd6c051a 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/fix/BuiltInRules.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/fix/BuiltInRules.scala @@ -176,7 +176,8 @@ object BuiltInRules extends CommandHelpers { ), logger = logger, suppressWarningOptions = SuppressWarningOptions.suppressAll, - exclude = buildOptions.internal.exclude + exclude = buildOptions.internal.exclude, + download = buildOptions.downloader ).orExit(logger) val sharedOptions = crossSources.sharedOptions(buildOptions) 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 ed301b5cc9..bc170369b3 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 @@ -89,7 +89,8 @@ object PublishSetup extends ScalaCommand[PublishSetupOptions] { ), logger, cliBuildOptions.suppressWarningOptions, - cliBuildOptions.internal.exclude + cliBuildOptions.internal.exclude, + download = cliBuildOptions.downloader ).orExit(logger) val crossSourcesSharedOptions = crossSources.sharedOptions(cliBuildOptions) diff --git a/modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIde.scala b/modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIde.scala index 1b3aa42ce3..2b8d005a0c 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIde.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIde.scala @@ -43,7 +43,8 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] { ), logger, options.suppressWarningOptions, - options.internal.exclude + options.internal.exclude, + download = options.downloader ) } 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 43c86092a1..aa90160c46 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 @@ -29,7 +29,6 @@ import scala.build.interactive.Interactive.{InteractiveAsk, InteractiveNop} import scala.build.internal.util.WarningMessages import scala.build.internal.{Constants, FetchExternalBinary, OsLibc, Util} import scala.build.internals.ConsoleUtils.ScalaCliConsole -import scala.build.options.ScalaVersionUtil.fileWithTtl0 import scala.build.options.{BuildOptions, ComputeVersion, Platform, ScalacOpt, ShadowingSeq} import scala.build.preprocessing.directives.ClasspathUtils.* import scala.build.preprocessing.directives.Toolkit.maxScalaNativeWarningMsg @@ -658,7 +657,7 @@ final case class SharedOptions( Inputs.validateArgs( args, Os.pwd, - SharedOptions.downloadInputs(coursierCache), + BuildOptions.Download.changing(coursierCache), SharedOptions.readStdin(logger = logger), !Properties.isWin, enableMarkdown = true @@ -675,15 +674,6 @@ object SharedOptions { implicit lazy val help: Help[SharedOptions] = Help.derive implicit lazy val jsonCodec: JsonValueCodec[SharedOptions] = JsonCodecMaker.make - private def downloadInputs(cache: FileCache[Task]): String => Either[String, Array[Byte]] = { - url => - val artifact = Artifact(url).withChanging(true) - cache.fileWithTtl0(artifact) - .left - .map(_.describe) - .map(f => os.read.bytes(os.Path(f, Os.pwd))) - } - /** [[Inputs]] builder, handy when you don't have a [[SharedOptions]] instance at hand */ def inputs( args: Seq[String], @@ -715,7 +705,7 @@ object SharedOptions { args, Os.pwd, defaultInputs = defaultInputs, - download = downloadInputs(cache), + download = BuildOptions.Download.changing(cache), stdinOpt = readStdin(logger = logger), scriptSnippetList = scriptSnippetList, scalaSnippetList = scalaSnippetList, diff --git a/modules/directives/src/main/scala/scala/build/errors/UsingFileFromUriError.scala b/modules/directives/src/main/scala/scala/build/errors/UsingFileFromUriError.scala index 67187679ed..5e6584bb1f 100644 --- a/modules/directives/src/main/scala/scala/build/errors/UsingFileFromUriError.scala +++ b/modules/directives/src/main/scala/scala/build/errors/UsingFileFromUriError.scala @@ -4,9 +4,8 @@ import java.net.URI import scala.build.Position -final class UsingFileFromUriError(uri: URI, positions: Seq[Position], cause: Throwable) +final class UsingFileFromUriError(uri: URI, positions: Seq[Position], description: String) extends BuildException( - message = s"Error using file from $uri - ${cause.getLocalizedMessage}", - positions = positions, - cause = cause + message = s"Error using file from $uri - $description", + positions = positions ) 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 66181732c1..c0fdb4085d 100644 --- a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala @@ -583,6 +583,8 @@ final case class BuildOptions( } } + lazy val downloader: BuildOptions.Download = BuildOptions.Download(finalCache) + lazy val interactive: Either[BuildException, Interactive] = internal.interactive.map(_()).getOrElse(Right(InteractiveNop)) } @@ -632,6 +634,23 @@ object BuildOptions { } } + type Download = String => Either[String, Array[Byte]] + object Download { + def apply( + cache: FileCache[Task], + toArtifact: String => Artifact = Artifact.fromUrl + ): Download = { + import scala.build.options.ScalaVersionUtil.fileWithTtl0 + url => + cache.fileWithTtl0(toArtifact(url)) + .left + .map(_.describe) + .map(f => os.read.bytes(os.Path(f, Os.pwd))) + } + def changing(cache: FileCache[Task]): Download = apply(cache, Artifact(_).withChanging(true)) + val notSupported: Download = _ => Left("URL not supported") + } + implicit val hasHashData: HasHashData[BuildOptions] = HasHashData.derive implicit val monoid: ConfigMonoid[BuildOptions] = ConfigMonoid.derive } From 2fc8042acec7ef956f531577aef69d3031ba3b2f Mon Sep 17 00:00:00 2001 From: Ivan Klass Date: Sat, 31 May 2025 11:14:57 +0200 Subject: [PATCH 6/6] add using file link test --- .../cli/integration/RunGistTestDefinitions.scala | 11 ++++++----- .../scala/scala/cli/integration/RunTestsDefault.scala | 11 +++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunGistTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunGistTestDefinitions.scala index 3e1818e8c7..df5f56aec7 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunGistTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunGistTestDefinitions.scala @@ -9,15 +9,16 @@ trait RunGistTestDefinitions { _: RunTestDefinitions => if (Properties.isWin) "\"" + url + "\"" else url + protected val scalaScriptUrl = + "https://gist.github.com/alexarchambault/f972d941bc4a502d70267cfbbc4d6343/raw/b0285fa0305f76856897517b06251970578565af/test.sc" + protected val scalaScriptMessage = "Hello from GitHub Gist" + test("Script URL") { - val url = - "https://gist.github.com/alexarchambault/f972d941bc4a502d70267cfbbc4d6343/raw/b0285fa0305f76856897517b06251970578565af/test.sc" - val message = "Hello from GitHub Gist" emptyInputs.fromRoot { root => - val output = os.proc(TestUtil.cli, extraOptions, escapedUrls(url)) + val output = os.proc(TestUtil.cli, extraOptions, escapedUrls(scalaScriptUrl)) .call(cwd = root) .out.trim() - expect(output == message) + expect(output == scalaScriptMessage) } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala index ce7c0fd7c5..09963d6c47 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestsDefault.scala @@ -111,6 +111,17 @@ class RunTestsDefault extends RunTestDefinitions } } + test("using file + http[s] link directive") { + val inputPath = os.rel / "usingFileLinkExample.scala" + TestInputs(inputPath -> s"//> using file $scalaScriptUrl\n").fromRoot { + root => + val res = os.proc(TestUtil.cli, "run", extraOptions, inputPath) + .call(cwd = root) + val out = res.out.trim() + expect(out == scalaScriptMessage) + } + } + for { suppressDeprecatedWarnings <- Seq(true, false) suppressByConfig <- if (suppressDeprecatedWarnings) Seq(true, false) else Seq(false)