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("")
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
I need find filename in folder (in build.sbt), and add in artifact list.
//in build.sbt
val myZipTask = taskKey[File]("return the bundle:dist-zip file")
myZipTask := {
val filesArray = new java.io.File("/target/bundle").listFiles()
//here need to find the file name by coincidence and convert to a string
file(fileName)
}; addArtifact( Artifact("bundle", "zip", "zip"), myZipTask)
I tried this option
//in build.sbt
val myZipTask = taskKey[File]("return the bundle:dist-zip file")
myZipTask := {
import java.io.File
def getListOfFiles(dir: String): List[String] = {
val file = new File(dir)
file.listFiles.filter(_.isFile)
.filter(_.getName.startsWith("startName"))
.map(_.getPath).toList
}
getListOfFiles("/target/bundle")
}; addArtifact( Artifact("bundle", "zip", "zip"), myZipTask)
And sbt return me error:
build.sbt: error: type mismatch;
found : List[String]
required: sbt.File
(which expands to) java.io.File
getListOfFiles("/target/bundle")
^
Check the documentation on Path Finders:
val finder: PathFinder = target.value / "bundle"
You can add * "startName*" if you want to filter by prefix. If you call finder.get, it will return you a Seq[File], so this is what you wanted from your getListOfFiles.
But the problem with your code is that you need to return one file, not a list. You could either output an error if the file doesn't exist:
finder.get.headOption.getOrElse {
sys.error("Couldn't find bundle dist-zip file")
}
or change you task type to Option[File], return finder.get.headOption and add the artifact only if the file is there:
myZipTask.value.foreach { zipFile =>
addArtifact(Artifact("bundle", "zip", "zip"), zipFile)
}
This foreach could work even for multiple files if that's an option in your usecase.
I would like to customize the .desktop file created by the javapackager as part of the JDKPackager plugin of sbt-native-packager. It obviously uses a template:
[info] Using default package resource [Menu shortcut descriptor]
(add package/linux/Foo.desktop to the class path to customize)
In particular, I want to add the StartupWMClass entry that will be used by Gnome to unify all the windows opened by my application.
The javapackager refers to the target directory of the plugin, i.e. target/jdkpackager. This is created for example when the javafx-ant build-file is written. So we can piggyback here:
// rewrite the task so that after the ant build is created,
// we add package/linux/MyApp.desktop
writeAntBuild in JDKPackager := {
val res = (writeAntBuild in JDKPackager).value
val main = (mainClass in JDKPackager).value
.getOrElse(sys.error("No main class specified"))
val tgt = (target in JDKPackager).value
val n = (name in JDKPackager).value
val wm = main.replace('.', '-')
val desktop =
s"""[Desktop Entry]
|Name=APPLICATION_NAME
|Comment=APPLICATION_SUMMARY
|Exec=/opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
|Icon=/opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.png
|Terminal=false
|Type=Application
|Categories=DEPLOY_BUNDLE_CATEGORY
|DESKTOP_MIMES
|StartupWMClass=$wm
|""".stripMargin
IO.write(tgt / "package" / "linux" / s"$n.desktop", desktop)
res
}
As long as there is a corresponding setting in the Ant task XML, there is an alternative solution: just rewriting the XML generated by the plugin, via antBuildDefn, will suffice.
Here's an example of specifying a menu category for the .desktop file, via adding the category attribute :
antBuildDefn in JDKPackager := {
val origTask = (antBuildDefn in JDKPackager).value
val InfoLabel = "info"
val KeyRegex = s"$InfoLabel\\.(.+)".r
import scala.xml._
import scala.xml.transform._
val infoRewrite = new RewriteRule {
override def transform(n: Node) = n match {
case e: Elem if e.prefix == "fx" && e.label == InfoLabel =>
e % Attribute("", "category", "Office", Null)
case other => other
}
}
new RuleTransformer(infoRewrite)(origTask)
}
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)
}