Skip to content

Commit

Permalink
Add dependency update command (#1055)
Browse files Browse the repository at this point in the history
* Actionable diagnotics

* Add dependency update command
  • Loading branch information
lwronski authored Jul 14, 2022
1 parent de5282e commit 5d0da51
Show file tree
Hide file tree
Showing 19 changed files with 513 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class PersistentDiagnosticLogger(parent: Logger) extends Logger {
}

def log(ex: BuildException): Unit = parent.log(ex)
def debug(ex: BuildException): Unit = parent.debug(ex)
def exit(ex: BuildException): Nothing = parent.exit(ex)

def coursierLogger(printBefore: String): coursier.cache.CacheLogger =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,8 @@ case object ScalaPreprocessor extends Preprocessor {
val toFilePos = Position.Raw.filePos(path, content)
val deps = value {
dependencyTrees
.map { t =>
val pos = toFilePos(Position.Raw(t.start, t.end))
.map { t => /// skip ivy ($ivy.`) or dep syntax ($dep.`)
val pos = toFilePos(Position.Raw(t.start + "$ivy.`".length, t.end))
val strDep = t.prefix.drop(1).mkString(".")
val maybeDep = parseDependency(strDep, pos)
maybeDep.map(dep => Positioned(Seq(pos), dep))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package scala.build.tests

import com.eed3si9n.expecty.Expecty.expect
import scala.build.options.{BuildOptions, InternalOptions}
import scala.build.Ops._
import scala.build.{BuildThreads, Directories, LocalRepo}
import scala.build.actionable.ActionablePreprocessor
import scala.build.actionable.ActionableDiagnostic._
import coursier.core.Version

class ActionableDiagnosticTests extends munit.FunSuite {

val extraRepoTmpDir = os.temp.dir(prefix = "scala-cli-tests-actionable-diagnostic-")
val directories = Directories.under(extraRepoTmpDir)
val baseOptions = BuildOptions(
internal = InternalOptions(
localRepository = LocalRepo.localRepo(directories.localRepoDir)
)
)
val buildThreads = BuildThreads.create()

test("update os-lib") {
val dependencyOsLib = "com.lihaoyi::os-lib:0.7.8"
val testInputs = TestInputs(
os.rel / "Foo.scala" ->
s"""//> using lib "$dependencyOsLib"
|
|object Hello extends App {
| println("Hello")
|}
|""".stripMargin
)
testInputs.withBuild(baseOptions, buildThreads, None) {
(_, _, maybeBuild) =>
val build = maybeBuild.orThrow
val updateDiagnostics =
ActionablePreprocessor.generateActionableDiagnostics(build.options).orThrow

val osLibDiagnosticOpt = updateDiagnostics.collectFirst {
case diagnostic: ActionableDependencyUpdateDiagnostic => diagnostic
}

expect(osLibDiagnosticOpt.nonEmpty)
val osLibDiagnostic = osLibDiagnosticOpt.get

expect(Version(osLibDiagnostic.newVersion) > Version(osLibDiagnostic.oldDependency.version))
}
}

test("update ivy dependence upickle") {
val dependencyOsLib = "com.lihaoyi::upickle:1.4.0"
val testInputs = TestInputs(
os.rel / "Foo.scala" ->
s"""import $$ivy.`$dependencyOsLib`
|
|object Hello extends App {
| println("Hello")
|}
|""".stripMargin
)
testInputs.withBuild(baseOptions, buildThreads, None) {
(_, _, maybeBuild) =>
val build = maybeBuild.orThrow
val updateDiagnostics =
ActionablePreprocessor.generateActionableDiagnostics(build.options).orThrow

val osLibDiagnosticOpt = updateDiagnostics.collectFirst {
case diagnostic: ActionableDependencyUpdateDiagnostic => diagnostic
}

expect(osLibDiagnosticOpt.nonEmpty)
val osLibDiagnostic = osLibDiagnosticOpt.get

expect(osLibDiagnostic.oldDependency.render == dependencyOsLib)
expect(Version(osLibDiagnostic.newVersion) > Version(osLibDiagnostic.oldDependency.version))
}
}

test("update dep dependence upickle") {
val dependencyOsLib = "com.lihaoyi::upickle:1.4.0"
val testInputs = TestInputs(
os.rel / "Foo.scala" ->
s"""import $$dep.`$dependencyOsLib`
|
|object Hello extends App {
| println("Hello")
|}
|""".stripMargin
)
testInputs.withBuild(baseOptions, buildThreads, None) {
(_, _, maybeBuild) =>
val build = maybeBuild.orThrow
val updateDiagnostics =
ActionablePreprocessor.generateActionableDiagnostics(build.options).orThrow

val osLibDiagnosticOpt = updateDiagnostics.collectFirst {
case diagnostic: ActionableDependencyUpdateDiagnostic => diagnostic
}

expect(osLibDiagnosticOpt.nonEmpty)
val osLibDiagnostic = osLibDiagnosticOpt.get

expect(osLibDiagnostic.oldDependency.render == dependencyOsLib)
expect(Version(osLibDiagnostic.newVersion) > Version(osLibDiagnostic.oldDependency.version))
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ class BuildProjectTests extends munit.FunSuite {
this.diagnostics = this.diagnostics ++ diagnostics
}

override def log(ex: BuildException): Unit = {}
override def log(ex: BuildException): Unit = {}
override def debug(ex: BuildException): Unit = {}

override def exit(ex: BuildException): Nothing = ???

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ case class TestLogger(info: Boolean = true, debug: Boolean = false) extends Logg

def log(ex: BuildException): Unit =
System.err.println(ex.getMessage)
def debug(ex: BuildException): Unit =
debug(ex.getMessage)
def exit(ex: BuildException): Nothing =
throw new Exception(ex)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package scala.cli.commands

import caseapp._
import caseapp.core.help.Help

// format: off
@HelpMessage("Update dependencies in project")
final case class DependencyUpdateOptions(
@Recurse
shared: SharedOptions = SharedOptions(),
@Group("DependencyUpdate")
@HelpMessage("Update all dependency")
all: Boolean = false,
)
// format: on

object DependencyUpdateOptions {
implicit lazy val parser: Parser[DependencyUpdateOptions] = Parser.derive
implicit lazy val help: Help[DependencyUpdateOptions] = Help.derive
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class ScalaCliCommands(
Compile,
Config,
DefaultFile,
DependencyUpdate,
Directories,
Doc,
Doctor,
Expand Down
114 changes: 114 additions & 0 deletions modules/cli/src/main/scala/scala/cli/commands/DependencyUpdate.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package scala.cli.commands

import caseapp._
import os.Path

import scala.build.actionable.ActionableDependencyHandler
import scala.build.actionable.ActionableDiagnostic.ActionableDependencyUpdateDiagnostic
import scala.build.internal.CustomCodeWrapper
import scala.build.options.Scope
import scala.build.{CrossSources, Logger, Position, Sources}
import scala.cli.CurrentParams
import scala.cli.commands.util.SharedOptionsUtil._

object DependencyUpdate extends ScalaCommand[DependencyUpdateOptions] {
override def group = "Main"
override def sharedOptions(options: DependencyUpdateOptions) = Some(options.shared)

def run(options: DependencyUpdateOptions, args: RemainingArgs): Unit = {
val verbosity = options.shared.logging.verbosity
CurrentParams.verbosity = verbosity

val inputs = options.shared.inputsOrExit(args)
val logger = options.shared.logger
val buildOptions = options.shared.buildOptions()

val (crossSources, _) =
CrossSources.forInputs(
inputs,
Sources.defaultPreprocessors(
buildOptions.scriptOptions.codeWrapper.getOrElse(CustomCodeWrapper),
buildOptions.archiveCache,
buildOptions.internal.javaClassNameVersionOpt
),
logger
).orExit(logger)

val scopedSources = crossSources.scopedSources(buildOptions).orExit(logger)

def generateActionableUpdateDiagnostic(scope: Scope)
: Seq[ActionableDependencyUpdateDiagnostic] = {
val sources = scopedSources.sources(scope, crossSources.sharedOptions(buildOptions))

if (verbosity >= 3)
pprint.err.log(sources)

val options = buildOptions.orElse(sources.buildOptions)
ActionableDependencyHandler.createActionableDiagnostics(options).orExit(logger)
}

val actionableMainUpdateDiagnostics = generateActionableUpdateDiagnostic(Scope.Main)
val actionableTestUpdateDiagnostics = generateActionableUpdateDiagnostic(Scope.Test)
val actionableUpdateDiagnostics =
(actionableMainUpdateDiagnostics ++ actionableTestUpdateDiagnostics).distinct

if (options.all)
updateDependencies(actionableUpdateDiagnostics, logger)
else {
println("Updates")
actionableUpdateDiagnostics.foreach(update =>
println(s" * ${update.oldDependency.render} -> ${update.newVersion}")
)
println("""|To update all dependencies run:
| scala-cli dependency-update --all""".stripMargin)
}
}

private def updateDependencies(
actionableUpdateDiagnostics: Seq[ActionableDependencyUpdateDiagnostic],
logger: Logger
): Unit = {
val groupedByFileDiagnostics =
actionableUpdateDiagnostics.flatMap {
diagnostic =>
diagnostic.positions.collect {
case file: Position.File =>
file.path -> (file, diagnostic)
}
}.groupMap(_._1)(_._2)

groupedByFileDiagnostics.foreach {
case (Right(file), diagnostics) =>
val sortedByLine = diagnostics.sortBy(_._1.startPos._1).reverse
val appliedDiagnostics = updateDependencies(file, sortedByLine)
os.write.over(file, appliedDiagnostics)
diagnostics.foreach(diagnostic =>
logger.message(s"Updated dependency to: ${diagnostic._2.to}")
)
case (Left(file), diagnostics) =>
diagnostics.foreach {
diagnostic =>
logger.message(s"Warning: Scala CLI can't update ${diagnostic._2.to} in $file")
}
}
}

private def updateDependencies(
file: Path,
diagnostics: Seq[(Position.File, ActionableDependencyUpdateDiagnostic)]
): String = {
val fileContent = os.read(file)
val startIndicies = Position.Raw.lineStartIndices(fileContent)

diagnostics.foldLeft(fileContent) {
case (fileContent, (file, diagnostic)) =>
val (line, column) = (file.startPos._1, file.startPos._2)
val startIndex = startIndicies(line) + column
val endIndex = startIndex + diagnostic.oldDependency.render.length()

val newDependency = diagnostic.to
s"${fileContent.slice(0, startIndex)}$newDependency${fileContent.drop(endIndex)}"
}
}

}
3 changes: 3 additions & 0 deletions modules/cli/src/main/scala/scala/cli/internal/CliLogger.scala
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ class CliLogger(
if (verbosity >= 0)
printEx(ex, new mutable.HashMap)

def debug(ex: BuildException): Unit =
if (verbosity >= 2)
printEx(ex, new mutable.HashMap)
def exit(ex: BuildException): Nothing =
if (verbosity < 0)
sys.exit(1)
Expand Down
2 changes: 2 additions & 0 deletions modules/core/src/main/scala/scala/build/Logger.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ trait Logger {
): Unit = log(Seq(Diagnostic(message, severity, positions)))

def log(ex: BuildException): Unit
def debug(ex: BuildException): Unit
def exit(ex: BuildException): Nothing

def coursierLogger(printBefore: String): coursier.cache.CacheLogger
Expand All @@ -48,6 +49,7 @@ object Logger {

def log(diagnostics: Seq[Diagnostic]): Unit = ()
def log(ex: BuildException): Unit = ()
def debug(ex: BuildException): Unit = ()
def exit(ex: BuildException): Nothing =
throw new Exception(ex)

Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/main/scala/scala/build/Position.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ object Position {

// from https://github.com/com-lihaoyi/Ammonite/blob/76673f7f3eb9d9ae054482635f57a31527d248de/amm/interp/src/main/scala/ammonite/interp/script/PositionOffsetConversion.scala#L7-L69

private def lineStartIndices(content: String): Array[Int] = {
def lineStartIndices(content: String): Array[Int] = {

val content0 = content.toCharArray

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package scala.cli.integration

import com.eed3si9n.expecty.Expecty.expect

class DependencyUpdateTests extends munit.FunSuite {

test("dependency update test") {
val fileName = "Hello.scala"
val message = "Hello World"
val fileContent =
s"""|//> using lib "com.lihaoyi::os-lib:0.7.8"
|//> using lib "com.lihaoyi::utest:0.7.10"
|import $$ivy.`com.lihaoyi::geny:0.6.5`
|import $$dep.`com.lihaoyi::pprint:0.6.6`
|
|object Hello extends App {
| println("$message")
|}""".stripMargin
val inputs = TestInputs(
Seq(
os.rel / fileName -> fileContent
)
)
inputs.fromRoot { root =>
// update dependencies
val p = os.proc(TestUtil.cli, "dependency-update", "--all", fileName)
.call(
cwd = root,
stdin = os.Inherit,
mergeErrIntoOut = true
)
expect(p.out.text().trim.contains("Updated dependency to"))
expect( // check if dependency update command modify file
os.read(root / fileName) != fileContent)

// after updating dependencies app should run
val out = os.proc(TestUtil.cli, fileName).call(cwd = root).out.text().trim
expect(out == message)
}
}
}
Loading

0 comments on commit 5d0da51

Please sign in to comment.