Can anyone show me how to add multiple Artifacts within a SBT Command? I want to do the following. Given the following tree:
- target
|- scala-2.10
|- classes
|- my
|- package
|- foo
- bar
- john
- doe
I want to create a command that publishes a individual Artifact (jar) for each folder under my/package. There should not be any default Artifact. Projects that depend on this library should use classifiers, i.e.
libraryDependencies += "whatever" % "whatever" % "1.0" Artifacts(Artifact("foo", "john") )
Here is what I have so far.
def myCommand = Command.single("myCommand") {
case (currentState, arg) =>
val extracted = Project.extract(currentState)
val compileDirectory = extracted.get[File](classDirectory in Compile)
val packageDir = new sbt.File(compileDirectory, s"my${File.separator}package")
val artifactDefinitions = IO.listFiles(packageDir).toList.map {
serviceDir =>
val serviceName = serviceDir.name.split(File.separatorChar).reverse.head
println(s"Adding artifact for $serviceName")
val serviceTask = taskKey[File](s"Adds a $serviceName artifact")
serviceTask := {
serviceDir
}
val definition = addArtifact(Artifact("service", serviceName), serviceTask)
extracted.append(definition.settings, currentState) <-- fails on this line
definition
}
currentState
}
Error I am getting:
Reference to undefined setting:
*:serviceTask from *:packagedArtifacts ((sbt.BuildExtra) Defaults.scala:1767)
def myCommand = Command.single("myCommand") {
case (currentState, arg) =>
val extracted = Project.extract(currentState)
val targetDirectory = extracted.get[File](target in Compile)
val compileDirectory = extracted.get[File](classDirectory in Compile)
val packageDir = new sbt.File(compileDirectory, s"my${java.io.File.separator}package")
val additionalArtifacts = IO.listFiles(packageDir).toList.map { serviceDir =>
val serviceName = serviceDir.name.split(java.io.File.separatorChar).reverse.head
println(s"Adding artifact for $serviceName")
val files = Path.allSubpaths(serviceDir)
val outputZip = new sbt.File(targetDirectory, s"$serviceName.jar")
sbt.IO.zip(files, outputZip)
(Artifact(serviceName, serviceName), outputZip)
}.toMap
val settings = Seq[Setting[_]](
packagedArtifacts ++= additionalArtifacts,
artifacts ++= additionalArtifacts.keys.toSeq
)
extracted.append(settings, s)
}
Related
My project stopped working from one day to another, without any changes in the project. I'm suspecting one of the dependencies updated, but it's unclear from the error message.
[warn] Merging 'META-INF/aop.xml' with strategy 'aopMerge'
[error] org.xml.sax.SAXParseExceptionpublicId: -//AspectJ//DTD//EN; systemId: http://www.eclipse.org/aspectj/dtd/aspectj.dtd; lineNumber: 1; columnNumber: 2; The markup declarations contained or pointed to by the document type declaration must be well-formed.
The aopMerge strategy in my build.sbt is
val aopMerge: MergeStrategy = new MergeStrategy {
val name = "aopMerge"
import scala.xml._
import scala.xml.dtd._
def apply(tempDir: File, path: String, files: Seq[File]): Either[String, Seq[(File, String)]] = {
val dt = DocType("aspectj", PublicID("-//AspectJ//DTD//EN", "https://www.eclipse.org/aspectj/dtd/aspectj.dtd"), Nil)
val file = MergeStrategy.createMergeTarget(tempDir, path)
val xmls: Seq[Elem] = files.map(XML.loadFile)
val aspectsChildren: Seq[Node] = xmls.flatMap(_ \\ "aspectj" \ "aspects" \ "_")
val weaverChildren: Seq[Node] = xmls.flatMap(_ \\ "aspectj" \ "weaver" \ "_")
val options: String = xmls.map(x => (x \\ "aspectj" \ "weaver" \ "#options").text).mkString(" ").trim
val weaverAttr = if (options.isEmpty) Null else new UnprefixedAttribute("options", options, Null)
val aspects = new Elem(null, "aspects", Null, TopScope, false, aspectsChildren: _*)
val weaver = new Elem(null, "weaver", weaverAttr, TopScope, false, weaverChildren: _*)
val aspectj = new Elem(null, "aspectj", Null, TopScope, false, aspects, weaver)
XML.save(file.toString, aspectj, "UTF-8", xmlDecl = false, dt)
IO.append(file, IO.Newline.getBytes(IO.defaultCharset))
Right(Seq(file -> path))
}
}
I tried changing http://www.eclipse.org/aspectj/dtd/aspectj.dtd into https://www.eclipse.org/aspectj/dtd/aspectj.dtd based on a similar question on StackOverflow. Unfortunately, I'm getting the same error, even with http:// instead of https://. Note that I did run sbt reload after the change.
How can I find out why this is happening? And what can I do to solve this?
The workaround I used was generating a new file with the HTTPS url based on the files passed in files: Seq[File]) parameter. Then changing val xmls: Seq[Elem] = files.map(XML.loadFile) to use this new files.
val xmls: Seq[Elem] = files.map(generateFileWithSecureDtdUrl).map(XML.loadFile)
with the following declared on build.sbt
def generateFileWithSecureDtdUrl(originalFile: File): File = {
val fixedFileName = originalFile.getPath + ".fixed"
val ps: PrintStream = new PrintStream(fixedFileName) {
val originalFileSource: BufferedSource = Source.fromFile(originalFile)
originalFileSource
.getLines()
.map { line =>
if (line.contains("DOCTYPE") && line.contains("http://www.eclipse.org/aspectj/dtd/aspectj.dtd"))
line.replace("http://www.eclipse.org/aspectj/dtd/aspectj.dtd", "https://www.eclipse.org/aspectj/dtd/aspectj.dtd")
else
line
}
.foreach(line => write(line.getBytes("UTF8")))
originalFileSource.close()
}
ps.close()
new File(fixedFileName) }
I have a directory structure like this:
a
/one
hey.class
hey.tasty
you.class
you.tasty
/two
foo.class
foo.tasty
/three
bar.class
bar.tasty
b
/one
/two
/three
I need a way to copy all the .tasty files from their respective places in /a to their corresponding places in /b in a sbt task.
You can create the sbt task and pass the original and destination folders in the following manner:
val copyTasties = inputKey[Unit]("Copy .tasty files")
copyTasties := {
val userInput = Def.spaceDelimited().parsed
if (userInput.size != 2) {
throw new IllegalArgumentException("Original and target directories should be define!")
}
val from = Paths.get(userInput.head)
val to = Paths.get(userInput.last)
Files
.walk(from)
.filter(Files.isRegularFile(_))
.filter(path => path.toString.endsWith("tasty"))
.forEach { original =>
val relative = from.relativize(original)
val destination = to.resolve(relative)
IO.copyFile(original.toFile, destination.toFile)
}
}
Then you can invoke it like this:
copyTasties C:\\Dev\\sandbox\\a C:\\Dev\\sandbox\\b
If the original and destination are stable(for example they are directories inside the project) you can rewrite the task:
import java.nio.file.{Files, Paths}
import sbt._
val copyTastiesHardcoded = taskKey[Unit]("Copy .tasty files")
copyTastiesHardcoded := {
val baseDir = baseDirectory.value.toPath
val from = baseDir.resolve("a")
val to = baseDir.resolve("b")
Files
.walk(from)
.filter(Files.isRegularFile(_))
.filter(path => path.toString.endsWith("tasty"))
.forEach { original =>
val relative = from.relativize(original)
val destination = to.resolve(relative)
IO.copyFile(original.toFile, destination.toFile)
}
}
and invoke it without arguments
copyTastiesHardcoded
There are some useful utility methods in sbt that will help you find files and copy them around. Unfortunately the use a wild mixture of the traditional java.io.File and the more modern java.nio.file.Path, so you need to do conversions between them:
val copyFiles = taskKey[Unit]("copy files")
copyFiles := {
val inputDir = baseDirectory.value.toPath / "a"
val files: Seq[(Path, FileAttributes)] =
FileTreeView.default.list(inputDir.toGlob / RecursiveGlob / "*.tasty")
val outputDir = baseDirectory.value.toPath / "b"
IO.copy {
files.map { case (p, _) =>
p.toFile -> outputDir.resolve(inputDir.relativize(p)).toFile
}
}
}
Note that I'm using a glob here to find the files:
https://www.scala-sbt.org/1.x/docs/Globs.html
Let it be the following hierarchy:
object X extends Y{
...
}
trait Y extends Z {
...
}
trait Z {
def run(): Unit
}
I parse the scala file containing the X and
I want to know if its parent or grandparent is Z.
I can check for parent as follows:
Given that x: Defn.Object is the X class I parsed,
x
.children.collect { case c: Template => c }
.flatMap(p => p.children.collectFirst { case c: Init => c }
will give Y.
Question: Any idea how I can get the parent of the parent of X (which is Z in the above example) ?
Loading Y (the same way I loaded X) and finding it's parent doesn't seem like a good idea, since the above is part of a scan procedure where among all files under src/main/scala I'm trying to find all classes which extend Z and implement run, so I don't see an easy and performant way to create a graph with all intermediate classes so as to load them in the right order and check for their parents.
It seems you want Scalameta to process your sources not syntactically but semantically. Then you need SemanticDB. Probably the most convenient way to work with SemanticDB is Scalafix
rules/src/main/scala/MyRule.scala
import scalafix.v1._
import scala.meta._
class MyRule extends SemanticRule("MyRule") {
override def isRewrite: Boolean = true
override def description: String = "My Rule"
override def fix(implicit doc: SemanticDocument): Patch = {
doc.tree.traverse {
case q"""..$mods object $ename extends ${template"""
{ ..$stats } with ..$inits { $self => ..$stats1 }"""}""" =>
val initsParents = inits.collect(_.symbol.info.map(_.signature) match {
case Some(ClassSignature(_, parents, _, _)) => parents
}).flatten
println(s"object: $ename, parents: $inits, grand-parents: $initsParents")
}
Patch.empty
}
}
in/src/main/scala/App.scala
object X extends Y{
override def run(): Unit = ???
}
trait Y extends Z {
}
trait Z {
def run(): Unit
}
Output of sbt out/compile
object: X, parents: List(Y), grand-parents: List(AnyRef, Z)
build.sbt
name := "scalafix-codegen"
inThisBuild(
List(
//scalaVersion := "2.13.2",
scalaVersion := "2.11.12",
addCompilerPlugin(scalafixSemanticdb),
scalacOptions ++= List(
"-Yrangepos"
)
)
)
lazy val rules = project
.settings(
libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % "0.9.16",
organization := "com.example",
version := "0.1",
)
lazy val in = project
lazy val out = project
.settings(
sourceGenerators.in(Compile) += Def.taskDyn {
val root = baseDirectory.in(ThisBuild).value.toURI.toString
val from = sourceDirectory.in(in, Compile).value
val to = sourceManaged.in(Compile).value
val outFrom = from.toURI.toString.stripSuffix("/").stripPrefix(root)
val outTo = to.toURI.toString.stripSuffix("/").stripPrefix(root)
Def.task {
scalafix
.in(in, Compile)
.toTask(s" --rules=file:rules/src/main/scala/MyRule.scala --out-from=$outFrom --out-to=$outTo")
.value
(to ** "*.scala").get
}
}.taskValue
)
project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.16")
Other examples:
https://github.com/olafurpg/scalafix-codegen (semantic)
https://github.com/DmytroMitin/scalafix-codegen (semantic)
https://github.com/DmytroMitin/scalameta-demo (syntactic)
Is it possible to using macro to modify the generated code of structural-typing instance invocation? (semantic)
Scala conditional compilation (syntactic)
Macro annotation to override toString of Scala function (syntactic)
How to merge multiple imports in scala? (syntactic)
You can avoid Scalafix but then you'll have to work with internals of SemanticDB manually
import scala.meta._
import scala.meta.interactive.InteractiveSemanticdb
import scala.meta.internal.semanticdb.{ClassSignature, Range, SymbolInformation, SymbolOccurrence, TypeRef}
val source: String =
"""object X extends Y{
| override def run(): Unit = ???
|}
|
|trait Y extends Z
|
|trait Z {
| def run(): Unit
|}""".stripMargin
val textDocument = InteractiveSemanticdb.toTextDocument(
InteractiveSemanticdb.newCompiler(List(
"-Yrangepos"
)),
source
)
implicit class TreeOps(tree: Tree) {
val occurence: Option[SymbolOccurrence] = {
val treeRange = Range(tree.pos.startLine, tree.pos.startColumn, tree.pos.endLine, tree.pos.endColumn)
textDocument.occurrences
.find(_.range.exists(occurrenceRange => treeRange == occurrenceRange))
}
val info: Option[SymbolInformation] = occurence.flatMap(_.symbol.info)
}
implicit class StringOps(symbol: String) {
val info: Option[SymbolInformation] = textDocument.symbols.find(_.symbol == symbol)
}
source.parse[Source].get.traverse {
case tree#q"""..$mods object $ename extends ${template"""
{ ..$stats } with ..$inits { $self => ..$stats1 }"""}""" =>
val initsParents = inits.collect(_.info.map(_.signature) match {
case Some(ClassSignature(_, parents, _, _)) =>
parents.collect {
case TypeRef(_, symbol, _) => symbol
}
}).flatten
println(s"object = $ename = ${ename.info.map(_.symbol)}, parents = $inits = ${inits.map(_.info.map(_.symbol))}, grand-parents = $initsParents")
}
Output:
object = X = Some(_empty_/X.), parents = List(Y) = List(Some(_empty_/Y#)), grand-parents = List(scala/AnyRef#, _empty_/Z#)
build.sbt
//scalaVersion := "2.13.3"
scalaVersion := "2.11.12"
lazy val scalametaV = "4.3.18"
libraryDependencies ++= Seq(
"org.scalameta" %% "scalameta" % scalametaV,
"org.scalameta" % "semanticdb-scalac" % scalametaV cross CrossVersion.full
)
Semanticdb code seems to be working in Scala 3
https://scastie.scala-lang.org/DmytroMitin/3QQwsDG2Rqm71qa6mMMkTw/36 [copy] (at Scastie -Dscala.usejavacp=true didn't help with object scala.runtime in compiler mirror not found, so I used Coursier to guarantee that scala-library is on path, locally it works without Coursier)
I am trying to write an SBT task that may be used like this:
> deploy key1=value1 key2=value2 ...
and does the following:
reads a default properties file
adds the parsed keys and values to the properties object
writes the new properties object to resources/config.properties
packages a fat .jar withsbt-assembly
writes the default properties to resources/config.properties
I have been trying to achieve it with Def.sequential, but I cant seem to find a way to use it withinputKey. Mybuild.sbtlooks like this:
val config = inputKey[Unit] (
"Set configuration options before deployment.")
val deploy = inputKey[Unit](
"assemble fat .jar with configuration options")
val defaultProperties = settingKey[Properties](
"default application properties.")
val propertiesPath = settingKey[File]("path to config.properties")
val writeDefaultProperties = taskKey[Unit]("write default properties file.")
val parser = (((' ' ~> StringBasic) <~ '=') ~ StringBasic).+
lazy val root = (project in file("."))
.settings(
propertiesPath := {
val base = (resourceDirectory in Compile).value
base / "config.properties"
},
defaultProperties := {
val path = propertiesPath.value
val defaultConfig = new Properties
IO.load(defaultConfig, path)
defaultConfig
},
config := {
val path = propertiesPath.value
val defaultConfig = defaultProperties.value
val options = parser.parsed
val deployConfig = new Properties
deployConfig.putAll(defaultConfig)
options.foreach(option =>
deployConfig
.setProperty(option._1, option._2))
IO.write(deployConfig, "", path)
},
writeDefaultProperties := {
val default = defaultProperties.value
val path = propertiesPath.value
IO.write(default, "", path)
},
deploy := Def.sequential(
config.parsed, // does not compile
assembly,
writeDefaultProperties),
...)
Can I makeDef.sequential work with input keys, or do I need to do something more involved?
See Defining a sequential task with Def.sequential. It uses scalastyle as an example:
(scalastyle in Compile).toTask("")
I want to have a command publish-snapshot that would run the publish task with modified version setting (that setting is to be computed at the time of execution of the command).
I figured out how to get the current value of the version inside command, and Project.runTask("task", "scope", ...) seems to be a right call for invoking the publish task.
The only thing that I'm confused with is how to modify the State instance with a new version value. All my attempts seem to do nothing to the original version setting.
My last attempt:
val printVers = TaskKey[Unit]("printvers")
val printVersTask = TaskKey[Unit]("printvers") <<= {version map println}
def publishSnapshot = Command.command("publish-snapshot") { state =>
val newState = SessionSettings.reapply(state.get(sessionSettings).get.appendRaw(version := "???"), state)
Project.runTask(printVers in Compile, newState, true)
state
}
lazy val root = Project("main", file("."),
settings =
Defaults.defaultSettings ++
Seq(printVersTask)).settings(commands += publishSnapshot)
Is there some way to fix that behavior?
With the help from sbt mailing list, I was able to create a solution as follows:
def publishSnapshot = Command.command("publish-snapshot") { state =>
val extracted = Project extract state
import extracted._
val eVersion = getOpt(version).get // getting current version
runTask(publish in Compile,
append(Seq(version := "newVersion"), state),
true
)
state
}
This actually did not work for me. I'm using SBT 0.13.7
Adapting what I had to do to the above example, I had to do something like:
def publishSnapshot = Command.command("publish-snapshot") { state =>
val extracted = Project extract state
val newState = extracted.append(Seq(version := "newVersion"), state)
val (s, _) = Project.extract(newState).runTask(publish in Compile, newState)
s
}
Or alternatively do:
def publishSnapshot = Command.command("publish-snapshot") { state =>
val newState =
Command.process("""set version := "newVersion" """, state)
val (s, _) = Project.extract(newState).runTask(publish in Compile, newState)
s
}
To update an arbitrary setting from a command, do something like the following:
def updateFoo = Command.command("updateFoo") { state =>
val extracted = Project extract state
import extracted._
println("'foo' set to true")
//append returns state with updated Foo
append(Seq(foo := true), state)
}