Skip to content

Commit 3d9ec06

Browse files
authored
Merge pull request #317 from lwronski/scala-update
Add command to updating scala-cli
2 parents 978b055 + 0590d0f commit 3d9ec06

File tree

12 files changed

+339
-10
lines changed

12 files changed

+339
-10
lines changed

modules/cli/src/main/scala/scala/cli/ScalaCli.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ object ScalaCli extends CommandsEntryPoint {
3232
Run,
3333
SetupIde,
3434
Test,
35+
Update,
3536
Version
3637
)
3738

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package scala.cli.commands
2+
3+
object CommandUtils {
4+
5+
def isOutOfDateVersion(newVersion: String, oldVersion: String): Boolean = {
6+
import coursier.core.Version
7+
8+
Version(newVersion) > Version(oldVersion)
9+
}
10+
11+
lazy val shouldCheckUpdate: Boolean = scala.util.Random.nextInt % 10 == 1
12+
}

modules/cli/src/main/scala/scala/cli/commands/Compile.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ object Compile extends ScalaCommand[CompileOptions] {
1818
logger,
1919
Some(name)
2020
)
21+
if (CommandUtils.shouldCheckUpdate)
22+
Update.checkUpdateSafe(logger)
2123

2224
val cross = options.cross.cross.getOrElse(false)
2325
if (options.classPath && cross) {

modules/cli/src/main/scala/scala/cli/commands/InstallHome.scala

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,6 @@ import scala.util.{Properties, Try}
99
object InstallHome extends ScalaCommand[InstallHomeOptions] {
1010
override def hidden: Boolean = true
1111

12-
private def isOutOfDate(newVersion: String, oldVersion: String): Boolean = {
13-
import coursier.core.Version
14-
15-
Version(newVersion) > Version(oldVersion)
16-
}
17-
1812
private def logEqual(version: String) = {
1913
System.err.println(
2014
s"Scala-cli $version is already installed and up-to-date."
@@ -63,7 +57,7 @@ object InstallHome extends ScalaCommand[InstallHomeOptions] {
6357
if (os.exists(binDirPath))
6458
if (options.force) () // skip logging
6559
else if (newVersion == oldVersion) logEqual(newVersion)
66-
else if (isOutOfDate(newVersion, oldVersion))
60+
else if (CommandUtils.isOutOfDateVersion(newVersion, oldVersion))
6761
logUpdate(options.env, newVersion, oldVersion)
6862
else logDowngrade(options.env, newVersion, oldVersion)
6963

@@ -93,9 +87,18 @@ object InstallHome extends ScalaCommand[InstallHomeOptions] {
9387
updater.applyUpdate(update)
9488
}
9589

96-
if (didUpdate) "Profile was updated"
97-
9890
println(s"Successfully installed scala-cli $newVersion")
91+
92+
if (didUpdate) {
93+
if (Properties.isLinux)
94+
println(
95+
s"""|Profile file(s) updated.
96+
|To run scala-cli, log out and log back in, or run 'source ~/.profile'""".stripMargin
97+
)
98+
if (Properties.isMac)
99+
println("To run scala-cli, open new terminal or run 'source ~/.profile'")
100+
}
101+
99102
}
100103
}
101104
}

modules/cli/src/main/scala/scala/cli/commands/Run.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ object Run extends ScalaCommand[RunOptions] {
4545
logger,
4646
Some(name)
4747
)
48+
if (CommandUtils.shouldCheckUpdate)
49+
Update.checkUpdateSafe(logger)
4850

4951
if (options.watch.watch) {
5052
val watcher = Build.watch(

modules/cli/src/main/scala/scala/cli/commands/Test.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ object Test extends ScalaCommand[TestOptions] {
2525
logger,
2626
Some(name)
2727
)
28+
if (CommandUtils.shouldCheckUpdate)
29+
Update.checkUpdateSafe(logger)
2830

2931
val initialBuildOptions = options.buildOptions
3032
val bloopRifleConfig = options.shared.bloopRifleConfig()
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package scala.cli.commands
2+
3+
import caseapp._
4+
5+
import scala.build.Logger
6+
import scala.io.StdIn.readLine
7+
import scala.util.{Failure, Properties, Success, Try}
8+
9+
object Update extends ScalaCommand[UpdateOptions] {
10+
11+
private def updateScalaCli(options: UpdateOptions, newVersion: String) = {
12+
if (coursier.paths.Util.useAnsiOutput()) {
13+
println(s"Do you want to update scala-cli to version $newVersion [Y/n]")
14+
val response = readLine()
15+
if (response != "Y") {
16+
System.err.println("Abort")
17+
sys.exit(1)
18+
}
19+
}
20+
else if (!options.force) {
21+
System.err.println(s"To update scala-cli to $newVersion pass -f or --force")
22+
sys.exit(1)
23+
}
24+
25+
val installScript =
26+
os.proc("curl", "-sSLf", "https://virtuslab.github.io/scala-cli-packages/scala-setup.sh")
27+
.spawn(stderr = os.Inherit)
28+
29+
// format: off
30+
val res = os.proc(
31+
"sh", "-s", "--",
32+
"--version", newVersion,
33+
"--force",
34+
"--binary-name", options.binaryName,
35+
"--bin-dir", options.installDirPath,
36+
).call(
37+
cwd = os.pwd,
38+
stdin = installScript.stdout,
39+
stdout = os.Inherit,
40+
check = false,
41+
mergeErrIntoOut = true
42+
)
43+
// format: on
44+
val output = res.out.text().trim
45+
if (res.exitCode != 0) {
46+
System.err.println(s"Error during updating scala-cli: $output")
47+
sys.exit(1)
48+
}
49+
}
50+
51+
def update(options: UpdateOptions, scalaCliBinPath: os.Path) = {
52+
val currentVersion = Try {
53+
os.proc(scalaCliBinPath, "version").call(cwd = os.pwd).out.text().trim
54+
}.toOption.getOrElse("0.0.0")
55+
56+
lazy val newestScalaCliVersion = {
57+
val resp =
58+
os.proc("curl", "--silent", "https://github.com/VirtusLab/scala-cli/releases/latest")
59+
.call(cwd = os.pwd, mergeErrIntoOut = true, check = false)
60+
.out.text().trim
61+
62+
val scalaCliVersionRegex = "tag/v(.*)\"".r
63+
scalaCliVersionRegex.findFirstMatchIn(resp).map(_.group(1))
64+
}.getOrElse(
65+
sys.error("Can not resolve ScalaCLI version to update")
66+
)
67+
68+
val isOutdated = CommandUtils.isOutOfDateVersion(newestScalaCliVersion, currentVersion)
69+
70+
if (!options.isInternalRun)
71+
if (isOutdated)
72+
updateScalaCli(options, newestScalaCliVersion)
73+
else println("ScalaCLI is up-to-date")
74+
else if (isOutdated)
75+
println(
76+
s"""Your ScalaCLI $currentVersion is outdated, please update ScalaCLI to $newestScalaCliVersion
77+
|Run 'curl -sSLf https://virtuslab.github.io/scala-cli-packages/scala-setup.sh | sh' to update ScalaCLI.""".stripMargin
78+
)
79+
}
80+
81+
def checkUpdate(options: UpdateOptions) = {
82+
83+
val scalaCliBinPath = options.installDirPath / options.binaryName
84+
85+
lazy val execScalaCliPath = os.proc("which", "scala-cli").call(
86+
cwd = os.pwd,
87+
mergeErrIntoOut = true,
88+
check = false
89+
).out.text().trim
90+
lazy val isScalaCliInPath = // if binDir is non empty, we not except scala-cli in PATH, it is useful in tests
91+
execScalaCliPath.contains(options.installDirPath.toString()) || options.binDir.isDefined
92+
93+
if (!os.exists(scalaCliBinPath) || !isScalaCliInPath) {
94+
if (!options.isInternalRun) {
95+
System.err.println(
96+
"Scala CLI was not installed by the installation script, please use your package manager to update scala-cli."
97+
)
98+
sys.exit(1)
99+
}
100+
}
101+
else if (Properties.isWin) {
102+
if (!options.isInternalRun) {
103+
System.err.println("ScalaCLI update is not supported on Windows.")
104+
sys.exit(1)
105+
}
106+
}
107+
else update(options, scalaCliBinPath)
108+
}
109+
110+
def run(options: UpdateOptions, args: RemainingArgs): Unit =
111+
checkUpdate(options)
112+
113+
def checkUpdateSafe(logger: Logger): Unit = {
114+
Try {
115+
val classesDir =
116+
this.getClass.getProtectionDomain.getCodeSource.getLocation.toURI.toString
117+
val binRepoDir = build.Directories.default().binRepoDir.toString()
118+
// log about update only if scala-cli was installed from installation script
119+
if (classesDir.contains(binRepoDir))
120+
checkUpdate(UpdateOptions(isInternalRun = true))
121+
} match {
122+
case Failure(ex) =>
123+
logger.debug(s"Ignoring error during checking update: $ex")
124+
case Success(_) => ()
125+
}
126+
}
127+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package scala.cli.commands
2+
3+
import caseapp._
4+
5+
// format: off
6+
@HelpMessage("Update scala-cli - it works only for installation script")
7+
final case class UpdateOptions(
8+
@Group("Update")
9+
@HelpMessage("Binary name")
10+
binaryName: String = "scala-cli",
11+
@Group("Update")
12+
@HelpMessage("Binary directory")
13+
binDir: Option[String] = None,
14+
@Name("f")
15+
@HelpMessage("Update scala-cli if is outdated")
16+
force: Boolean = false,
17+
@Hidden
18+
isInternalRun: Boolean = false
19+
) {
20+
// format: on
21+
lazy val binDirPath = binDir.map(os.Path(_, os.pwd))
22+
lazy val installDirPath =
23+
binDirPath.getOrElse(scala.build.Directories.default().binRepoDir / binaryName)
24+
}
25+
26+
object UpdateOptions {
27+
implicit lazy val parser: Parser[UpdateOptions] = Parser.derive
28+
implicit lazy val help: Help[UpdateOptions] = Help.derive
29+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package scala.cli.integration
2+
3+
import com.eed3si9n.expecty.Expecty.expect
4+
5+
import scala.util.Properties
6+
7+
class UpdateTests extends munit.FunSuite {
8+
9+
val firstVersion = "0.0.1"
10+
val dummyScalaCliFirstName = "DummyScalaCli-1.scala"
11+
val dummyScalaCliBinName = "scala-cli-dummy-test"
12+
val testInputs = TestInputs(
13+
Seq(
14+
os.rel / dummyScalaCliFirstName ->
15+
s"""
16+
|object DummyScalaCli extends App {
17+
| println(\"$firstVersion\")
18+
|}""".stripMargin
19+
)
20+
)
21+
22+
private def packageDummyScalaCli(root: os.Path, dummyScalaCliFileName: String, output: String) = {
23+
// format: off
24+
val cmd = Seq[os.Shellable](
25+
TestUtil.cli, "package", dummyScalaCliFileName, "-o", output
26+
)
27+
// format: on
28+
os.proc(cmd).call(
29+
cwd = root,
30+
stdin = os.Inherit,
31+
stdout = os.Inherit
32+
)
33+
}
34+
35+
private def installScalaCli(
36+
root: os.Path,
37+
binVersion: String,
38+
binDirPath: os.Path
39+
) = {
40+
// format: off
41+
val cmdInstallVersion = Seq[os.Shellable](
42+
TestUtil.cli, "install-home",
43+
"--env",
44+
"--scala-cli-binary-path", binVersion,
45+
"--binary-name", dummyScalaCliBinName,
46+
"--bin-dir", binDirPath,
47+
"--force"
48+
)
49+
// format: on
50+
os.proc(cmdInstallVersion).call(
51+
cwd = root,
52+
stdin = os.Inherit,
53+
stdout = os.Inherit
54+
)
55+
}
56+
57+
def runUpdate(): Unit = {
58+
59+
testInputs.fromRoot { root =>
60+
val binDirPath = root / ".scala" / "scala-cli"
61+
62+
val binDummyScalaCliFirst = dummyScalaCliFirstName.stripSuffix(".scala").toLowerCase
63+
64+
packageDummyScalaCli(root, dummyScalaCliFirstName, binDummyScalaCliFirst)
65+
66+
// install 1 version
67+
installScalaCli(root, binDummyScalaCliFirst, binDirPath)
68+
69+
val v1Install = os.proc(binDirPath / dummyScalaCliBinName).call(
70+
cwd = root,
71+
stdin = os.Inherit
72+
).out.text().trim
73+
expect(v1Install == firstVersion)
74+
75+
// update to newest version
76+
// format: off
77+
val cmdUpdate = Seq[os.Shellable](
78+
TestUtil.cli,
79+
"update",
80+
"--binary-name", dummyScalaCliBinName,
81+
"--bin-dir", binDirPath,
82+
"--force"
83+
)
84+
// format: on
85+
os.proc(cmdUpdate).call(
86+
cwd = root,
87+
stdin = os.Inherit,
88+
stdout = os.Inherit
89+
)
90+
91+
val nextVersion = os.proc(binDirPath / dummyScalaCliBinName, "version").call(
92+
cwd = root,
93+
stdin = os.Inherit
94+
).out.text().trim
95+
96+
expect(firstVersion != nextVersion)
97+
}
98+
}
99+
100+
if (!Properties.isWin)
101+
test("updating dummy scala-cli using update command") {
102+
runUpdate()
103+
}
104+
105+
test("run update before run/test/compile should not return exit code") {
106+
val res = os.proc(TestUtil.cli, "update", "--is-internal-run").call(cwd = os.pwd)
107+
expect(res.exitCode == 0)
108+
}
109+
110+
}

website/docs/reference/cli-options.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ Available in commands:
309309
- [`run`](./commands.md#run)
310310
- [`setup-ide`](./commands.md#setup-ide)
311311
- [`test`](./commands.md#test)
312+
- [`update`](./commands.md#update)
312313
- [`version`](./commands.md#version)
313314

314315

@@ -959,6 +960,30 @@ Name of test framework's runner class to use while running tests
959960

960961
Fail if no test suites were run
961962

963+
## Update options
964+
965+
Available in commands:
966+
- [`update`](./commands.md#update)
967+
968+
969+
<!-- Automatically generated, DO NOT EDIT MANUALLY -->
970+
971+
#### `--binary-name`
972+
973+
Binary name
974+
975+
#### `--bin-dir`
976+
977+
Binary directory
978+
979+
#### `--force`
980+
981+
Aliases: `-f`
982+
983+
Update scala-cli if is outdated
984+
985+
#### `--is-internal-run`
986+
962987
## Watch options
963988

964989
Available in commands:

0 commit comments

Comments
 (0)