diff --git a/modules/build/src/test/scala/scala/build/options/publish/ComputeVersionTests.scala b/modules/build/src/test/scala/scala/build/options/publish/ComputeVersionTests.scala index 05add9ce8c..48df9c2cae 100644 --- a/modules/build/src/test/scala/scala/build/options/publish/ComputeVersionTests.scala +++ b/modules/build/src/test/scala/scala/build/options/publish/ComputeVersionTests.scala @@ -8,20 +8,6 @@ import scala.build.options.ComputeVersion import scala.build.tests.{TestInputs, TestUtil} class ComputeVersionTests extends munit.FunSuite { - - test("command") { - val ver = "1.2.3" - val inputs = TestInputs( - os.rel / "version" -> ver - ) - inputs.fromRoot { root => - val cv = ComputeVersion.Command(Seq("cat", "version")) - val readVersion = cv.get(root) - .fold(ex => throw new Exception(ex), identity) - expect(readVersion == ver) - } - } - test("git tag") { TestInputs().fromRoot { root => val ghRepo = "scala-cli/compute-version-test" @@ -31,7 +17,7 @@ class ComputeVersionTests extends munit.FunSuite { os.proc("git", "clone", repo) .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit) val dir = root / "compute-version-test" - val cv = ComputeVersion.GitTag(os.rel, true, "0.0.1-SNAPSHOT") + val cv = ComputeVersion.GitTag(os.rel, true, Nil, "0.0.1-SNAPSHOT") val commitExpectedVersions = Seq( "8ea4e87f202fbcc369bec9615e7ddf2c14b39e9d" -> "0.2.0-1-g8ea4e87-SNAPSHOT", @@ -56,7 +42,7 @@ class ComputeVersionTests extends munit.FunSuite { expect(!hasHead) val defaultVersion = "0.0.2-SNAPSHOT" - val cv = ComputeVersion.GitTag(os.rel, true, defaultVersion) + val cv = ComputeVersion.GitTag(os.rel, true, Nil, defaultVersion) val version = cv.get(root) .fold(ex => throw new Exception(ex), identity) diff --git a/modules/build/src/test/scala/scala/build/tests/SourceGeneratorTests.scala b/modules/build/src/test/scala/scala/build/tests/SourceGeneratorTests.scala index 9f9d30e91d..76e2da1ff9 100644 --- a/modules/build/src/test/scala/scala/build/tests/SourceGeneratorTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/SourceGeneratorTests.scala @@ -74,7 +74,7 @@ class SourceGeneratorTests extends munit.FunSuite { println(s"Git initialized at $cwd") } - test(s"BuildInfo source generated") { + test("BuildInfo source generated") { val inputs = TestInputs( os.rel / "main.scala" -> """//> using dep com.lihaoyi::os-lib:0.9.1 @@ -154,7 +154,7 @@ class SourceGeneratorTests extends munit.FunSuite { } - test(s"BuildInfo for native") { + test("BuildInfo for native") { val inputs = TestInputs( os.rel / "main.scala" -> s"""//> using dep "com.lihaoyi::os-lib:0.9.1" @@ -230,7 +230,7 @@ class SourceGeneratorTests extends munit.FunSuite { } } - test(s"BuildInfo for js") { + test("BuildInfo for js") { val inputs = TestInputs( os.rel / "main.scala" -> s"""//> using dep "com.lihaoyi::os-lib:0.9.1" @@ -244,7 +244,6 @@ class SourceGeneratorTests extends munit.FunSuite { |//> using platform scala-js |//> using jsVersion 1.13.1 |//> using jsEsVersionStr es2015 - |//> using computeVersion "command:echo TestVersion" | |//> using buildInfo | @@ -281,7 +280,7 @@ class SourceGeneratorTests extends munit.FunSuite { | val jsEsVersion = Some("es2015") | val scalaNativeVersion = None | val mainClass = Some("Main") - | val projectVersion = Some("TestVersion") + | val projectVersion = None | | object Main { | val sources = Seq("${root / "main.scala"}") @@ -308,7 +307,7 @@ class SourceGeneratorTests extends munit.FunSuite { } } - test(s"BuildInfo for Scala 2") { + test("BuildInfo for Scala 2") { val inputs = TestInputs( os.rel / "main.scala" -> s"""//> using dep "com.lihaoyi::os-lib:0.9.1" @@ -319,7 +318,6 @@ class SourceGeneratorTests extends munit.FunSuite { |//> using mainClass "Main" |//> using resourceDir ./resources |//> using jar TEST1.jar TEST2.jar - |//> using computeVersion "command:echo TestVersion" | |//> using buildInfo | @@ -356,7 +354,7 @@ class SourceGeneratorTests extends munit.FunSuite { | val jsEsVersion = None | val scalaNativeVersion = None | val mainClass = Some("Main") - | val projectVersion = Some("TestVersion") + | val projectVersion = None | | object Main { | val sources = Seq("${root / "main.scala"}") @@ -382,4 +380,47 @@ class SourceGeneratorTests extends munit.FunSuite { ) } } + + test("BuildInfo no git repository error") { + val usingPrefix = "//> using computeVersion \"" + val usingValue = "git:tag" + val usingSuffix = "\"" + + val inputs = TestInputs( + os.rel / "main.scala" -> + s""" + |$usingPrefix$usingValue$usingSuffix + |//> using buildInfo + | + |import scala.cli.build.BuildInfo + | + |object Main extends App { + | println(s"Scala version: $${BuildInfo.projectVersion}") + |} + |""".stripMargin + ) + + inputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { + (root, _, maybeBuild) => + maybeBuild match { + case Left(buildException) => + expect(buildException.positions.size == 1) + val position = buildException.positions.head + + assertEquals( + position, + scala.build.Position.File( + Right(root / "main.scala"), + (1, usingPrefix.length), + (1, (usingPrefix + usingValue).length) + ) + ) + assertNoDiff( + buildException.message, + s"BuildInfo generation error: $root doesn't look like a Git repository" + ) + case _ => fail("Build should fail") + } + } + } } diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala index 88c858ef4c..d0811f1a65 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala @@ -370,7 +370,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { name } def defaultComputeVersion(mayDefaultToGitTag: Boolean): Option[ComputeVersion] = - if (mayDefaultToGitTag) Some(ComputeVersion.GitTag(os.rel, dynVer = false)) + if (mayDefaultToGitTag) Some(ComputeVersion.GitTag(os.rel, dynVer = false, positions = Nil)) else None def defaultVersionError = new MissingPublishOptionError("version", "--version", "publish.version") diff --git a/modules/core/src/main/scala/scala/build/errors/BuildInfoGenerationError.scala b/modules/core/src/main/scala/scala/build/errors/BuildInfoGenerationError.scala index c8d884d9f9..8ed06792b1 100644 --- a/modules/core/src/main/scala/scala/build/errors/BuildInfoGenerationError.scala +++ b/modules/core/src/main/scala/scala/build/errors/BuildInfoGenerationError.scala @@ -1,4 +1,10 @@ package scala.build.errors -final class BuildInfoGenerationError(cause: Exception) - extends BuildException(s"BuildInfo generation error: ${cause.getMessage}", cause = cause) +import scala.build.Position + +final class BuildInfoGenerationError(msg: String, positions: Seq[Position], cause: Exception) + extends BuildException( + s"BuildInfo generation error: $msg", + positions = positions, + cause = cause + ) diff --git a/modules/generate-reference-doc/src/main/scala/scala/cli/doc/GenerateReferenceDoc.scala b/modules/generate-reference-doc/src/main/scala/scala/cli/doc/GenerateReferenceDoc.scala index 9db3c067e4..ab2bb21df5 100644 --- a/modules/generate-reference-doc/src/main/scala/scala/cli/doc/GenerateReferenceDoc.scala +++ b/modules/generate-reference-doc/src/main/scala/scala/cli/doc/GenerateReferenceDoc.scala @@ -574,6 +574,28 @@ object GenerateReferenceDoc extends CaseApp[InternalDocOptions] { |```""".stripMargin ) + b.section( + """## Project version + | + |A part of the BuildInfo object is the project version. By default, an attempt is made to deduce it using git tags + |of the workspace repository. If this fails (e.g. no git repository is present), the version is set to `0.1.0-SNAPSHOT`. + |You can override this behaviour by passing the `--project-version` option to Scala CLI or by using a + |`//> using projectVersion` directive. + | + |Please note that only tags that follow the semantic versioning are taken into consideration. + | + |Values available for project version configuration are: + |- `git:tag` or `git`: use the latest stable git tag, if it is older than HEAD then try to increment it + | and add a suffix `-SNAPSHOT`, if no tag is available then use `0.1.0-SNAPSHOT` + |- `git:dynver`: use the latest (stable or unstable) git tag, if it is older than HEAD then use the output of + | `-{distance from last tag}-g{shortened version of HEAD commit hash}-SNAPSHOT`, if no tag is available then use `0.1.0-SNAPSHOT` + | + |The difference between stable and unstable tags are, that the latter can contain letters, e.g. `v0.1.0-RC1`. + |It is also possible to specify the path to the repository, e.g. `git:tag:../my-repo`, `git:dynver:../my-repo`. + | + |""".stripMargin + ) + b.mkString } diff --git a/modules/options/src/main/scala/scala/build/info/BuildInfo.scala b/modules/options/src/main/scala/scala/build/info/BuildInfo.scala index 6db83766bf..8bf625b12c 100644 --- a/modules/options/src/main/scala/scala/build/info/BuildInfo.scala +++ b/modules/options/src/main/scala/scala/build/info/BuildInfo.scala @@ -100,19 +100,25 @@ object BuildInfo { def apply( options: BuildOptions, workspace: os.Path - ): Either[BuildException, BuildInfo] = either { + ): Either[BuildException, BuildInfo] = either[Exception] { Seq( BuildInfo( mainClass = options.mainClass, projectVersion = options.sourceGeneratorOptions.computeVersion .map(cv => value(cv.get(workspace))) - .orElse(ComputeVersion.GitTag(os.rel, dynVer = false).get(workspace).toOption) + .orElse( + ComputeVersion.GitTag(os.rel, dynVer = false, positions = Nil).get(workspace).toOption + ) ), scalaVersionSettings(options), platformSettings(options) ) .reduceLeft(_ + _) - }.left.map(BuildInfoGenerationError(_)) + }.left.map { + case e: BuildException => + BuildInfoGenerationError(e.message, positions = e.positions, cause = e) + case e => BuildInfoGenerationError(e.getMessage, Nil, e) + } def escapeBackslashes(s: String): String = s.replace("\\", "\\\\") diff --git a/modules/options/src/main/scala/scala/build/options/ComputeVersion.scala b/modules/options/src/main/scala/scala/build/options/ComputeVersion.scala index 4c1400c9a9..41f96b3eb3 100644 --- a/modules/options/src/main/scala/scala/build/options/ComputeVersion.scala +++ b/modules/options/src/main/scala/scala/build/options/ComputeVersion.scala @@ -5,43 +5,23 @@ import com.github.plokhotnyuk.jsoniter_scala.macros.* import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.{Constants, Ref} -import scala.build.Positioned import scala.build.errors.{BuildException, MalformedInputError} +import scala.build.{Position, Positioned} import scala.io.Codec import scala.jdk.CollectionConverters.* import scala.util.{Success, Try, Using} sealed abstract class ComputeVersion extends Product with Serializable { def get(workspace: os.Path): Either[BuildException, String] + + val positions: Seq[Position] } object ComputeVersion { - - final case class Command(command: Seq[String]) extends ComputeVersion { - def get(workspace: os.Path): Either[BuildException, String] = { - val maybeRes = Try(os.proc(command).call(stdin = os.Inherit, cwd = workspace, check = false)) - maybeRes match { - case Success(res) if res.exitCode == 0 => - Right(res.out.trim(Codec.default)) - case _ => - Left(new Command.ComputeVersionCommandError( - command, - maybeRes.map(_.exitCode).getOrElse(1) - )) - } - } - } - - object Command { - final class ComputeVersionCommandError(command: Seq[String], exitCode: Int) - extends BuildException( - s"Error running command ${command.mkString(" ")} (exit code: $exitCode)" - ) - } - final case class GitTag( repo: os.FilePath, dynVer: Boolean, + positions: Seq[Position], defaultFirstVersion: String = "0.1.0-SNAPSHOT" ) extends ComputeVersion { import GitTag.GitTagError @@ -115,17 +95,19 @@ object ComputeVersion { Option(tagOrNull) match { case None => Left(new GitTagError( + positions, s"Unexpected error when running git describe from Git repository $repo0 (git describe doesn't find back tag $tag)" )) case Some(tag) => versionOf(tag).map(_ + "-SNAPSHOT").toRight( new GitTagError( + positions, s"Unexpected error when running git describe from Git repository $repo0 (git describe-provided tag $tag doesn't have the expected shape)" ) ) } case (Some(_), None) => - Left(new GitTagError(s"No stable tag found in Git repository $repo0")) + Left(new GitTagError(positions, s"No stable tag found in Git repository $repo0")) case (_, Some((tag, name))) => val idx = name.lastIndexOf('.') if ( @@ -134,6 +116,7 @@ object ComputeVersion { Right(name.take(idx + 1) + (name.drop(idx + 1).toInt + 1).toString + "-SNAPSHOT") else Left(new GitTagError( + positions, s"Don't know how to bump version in tag $tag in Git repository $repo0" )) } @@ -142,11 +125,12 @@ object ComputeVersion { Right(defaultFirstVersion) } else - Left(new GitTagError(s"$repo0 doesn't look like a Git repository")) + Left(new GitTagError(positions, s"$repo0 doesn't look like a Git repository")) } } object GitTag { - final class GitTagError(message: String) extends BuildException(message) + final class GitTagError(positions: Seq[Position], message: String) + extends BuildException(message, positions) } private lazy val commandCodec: JsonValueCodec[List[String]] = @@ -154,43 +138,27 @@ object ComputeVersion { def parse(input: Positioned[String]): Either[BuildException, ComputeVersion] = if (input.value == "git" || input.value == "git:tag") - Right(ComputeVersion.GitTag(os.rel, dynVer = false)) + Right(ComputeVersion.GitTag(os.rel, dynVer = false, positions = input.positions)) else if (input.value.startsWith("git:tag:")) - Right(ComputeVersion.GitTag(os.FilePath(input.value.stripPrefix("git:tag:")), dynVer = false)) + Right(ComputeVersion.GitTag( + os.FilePath(input.value.stripPrefix("git:tag:")), + dynVer = false, + positions = input.positions + )) else if (input.value == "git:dynver") - Right(ComputeVersion.GitTag(os.rel, dynVer = true)) + Right(ComputeVersion.GitTag(os.rel, dynVer = true, positions = input.positions)) else if (input.value.startsWith("git:dynver:")) Right(ComputeVersion.GitTag( os.FilePath(input.value.stripPrefix("git:dynver:")), - dynVer = true + dynVer = true, + positions = input.positions )) - else if (input.value.startsWith("command:[")) - try { - val command = readFromString(input.value.stripPrefix("command:"))(commandCodec) - Right(ComputeVersion.Command(command)) - } - catch { - case e: JsonReaderException => - Left( - new MalformedInputError( - "compute-version", - input.value, - "git|git:tag|command:…", - input.positions, - cause = Some(e) - ) - ) - } - else if (input.value.startsWith("command:")) { - val command = input.value.stripPrefix("command:").split("\\s+").toSeq - Right(ComputeVersion.Command(command)) - } else Left( new MalformedInputError( "compute-version", input.value, - "git|git:tag|command:…", + "git|git:tag|git:dynver", input.positions ) ) diff --git a/website/docs/commands/publishing/publish.md b/website/docs/commands/publishing/publish.md index d866af7e73..cbb0f2519c 100644 --- a/website/docs/commands/publishing/publish.md +++ b/website/docs/commands/publishing/publish.md @@ -81,6 +81,17 @@ To override this default value, set the `publish.computeVersion` directive, like //> using publish.computeVersion git:tag ``` +Please note that only tags that follow the semantic versioning are taken into consideration. + +Values available for project version configuration are: +- `git:tag` or `git`: use the latest stable git tag, if it is older than HEAD then try to increment it + and add a suffix `-SNAPSHOT`, if no tag is available then use `0.1.0-SNAPSHOT` +- `git:dynver`: use the latest (stable or unstable) git tag, if it is older than HEAD then use the output of + `-{distance from last tag}-g{shortened version of HEAD commit hash}-SNAPSHOT`, if no tag is available then use `0.1.0-SNAPSHOT` + +The difference between stable and unstable tags are, that the latter can contain letters, e.g. `v0.1.0-RC1`. +It is also possible to specify the path to the repository, e.g. `git:tag:../my-repo`, `git:dynver:../my-repo`. + ## Repository settings A repository is required for the `publish` command, and might need other settings to work fine diff --git a/website/docs/reference/build-info.md b/website/docs/reference/build-info.md index c6afc59442..6772aaa762 100644 --- a/website/docs/reference/build-info.md +++ b/website/docs/reference/build-info.md @@ -90,3 +90,23 @@ object BuildInfo { } ``` +## Project version + +A part of the BuildInfo object is the project version. By default, an attempt is made to deduce it using git tags +of the workspace repository. If this fails (e.g. no git repository is present), the version is set to `0.1.0-SNAPSHOT`. +You can override this behaviour by passing the `--project-version` option to Scala CLI or by using a +`//> using projectVersion` directive. + +Please note that only tags that follow the semantic versioning are taken into consideration. + +Values available for project version configuration are: +- `git:tag` or `git`: use the latest stable git tag, if it is older than HEAD then try to increment it + and add a suffix `-SNAPSHOT`, if no tag is available then use `0.1.0-SNAPSHOT` +- `git:dynver`: use the latest (stable or unstable) git tag, if it is older than HEAD then use the output of + `-{distance from last tag}-g{shortened version of HEAD commit hash}-SNAPSHOT`, if no tag is available then use `0.1.0-SNAPSHOT` + +The difference between stable and unstable tags are, that the latter can contain letters, e.g. `v0.1.0-RC1`. +It is also possible to specify the path to the repository, e.g. `git:tag:../my-repo`, `git:dynver:../my-repo`. + + +