Skip to content

Commit

Permalink
Ability to relocate/shade in assembly
Browse files Browse the repository at this point in the history
  • Loading branch information
joan38 committed Aug 25, 2020
1 parent befdf8e commit 0db909c
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 12 deletions.
4 changes: 3 additions & 1 deletion build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ object Deps {
val utest = ivy"com.lihaoyi::utest:0.7.4"
val zinc = ivy"org.scala-sbt::zinc:1.4.0-M1"
val bsp = ivy"ch.epfl.scala:bsp4j:2.0.0-M4"
val jarjarabrams = ivy"com.eed3si9n.jarjarabrams::jarjar-abrams-core:0.3.0"
}

trait MillPublishModule extends PublishModule{
Expand Down Expand Up @@ -167,7 +168,8 @@ object main extends MillModule {
// Necessary so we can share the JNA classes throughout the build process
Deps.jna,
Deps.jnaPlatform,
Deps.coursier
Deps.coursier,
Deps.jarjarabrams
)

def generatedSources = T {
Expand Down
5 changes: 3 additions & 2 deletions docs/pages/2 - Configuring Mill.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ compilation output, but if there is more than one or the main class comes from
some library you can explicitly specify which one to use. This also adds the
main class to your `foo.jar` and `foo.assembly` jars.

## Merge/exclude files from assembly
## Merge/exclude/relocate files from assembly

When you make a runnable jar of your project with `assembly` command,
you may want to exclude some files from a final jar (like signature files, and manifest files from library jars),
Expand All @@ -532,7 +532,8 @@ object foo extends ScalaModule {
def assemblyRules = Seq(
Rule.Append("application.conf"), // all application.conf files will be concatenated into single file
Rule.AppendPattern(".*\\.conf"), // all *.conf files will be concatenated into single file
Rule.ExcludePattern("*.temp") // all *.temp files will be excluded from a final jar
Rule.ExcludePattern("*.temp"), // all *.temp files will be excluded from a final jar
Rule.Relocate("shapeless.**", "shade.shapless.@1") // the `shapeless` package will be shaded under the `shade` package
)
}
```
Expand Down
29 changes: 24 additions & 5 deletions main/src/modules/Assembly.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package mill.modules

import java.io.InputStream
import com.eed3si9n.jarjarabrams.{ShadePattern, Shader}
import java.io.{ByteArrayInputStream, InputStream}
import java.util.jar.JarFile
import java.util.regex.Pattern
import mill.Agg
import os.Generator
import scala.collection.JavaConverters._
import scala.tools.nsc.io.Streamable

object Assembly {

Expand All @@ -30,6 +32,8 @@ object Assembly {

case class Exclude(path: String) extends Rule

case class Relocate(from: String, to: String) extends Rule

object ExcludePattern {
def apply(pattern: String): ExcludePattern = ExcludePattern(Pattern.compile(pattern))
}
Expand Down Expand Up @@ -74,20 +78,35 @@ object Assembly {
}
}

def loadClasspath(
inputPaths: Agg[os.Path]
def loadShadedClasspath(
inputPaths: Agg[os.Path],
assemblyRules: Seq[Assembly.Rule]
): Generator[(String, UnopenedInputStream)] = {
val shadeRules = assemblyRules.collect {
case Rule.Relocate(from, to) => ShadePattern.Rename(List(from -> to)).inAll
}
val shader =
if (shadeRules.isEmpty) (name: String, inputStream: UnopenedInputStream) => Some(name -> inputStream)
else {
val shader = Shader.bytecodeShader(shadeRules, verbose = false)
(name: String, inputStream: UnopenedInputStream) =>
shader(Streamable.bytes(inputStream()), name).map {
case (bytes, name) =>
name -> (() => new ByteArrayInputStream(bytes) { override def close(): Unit = inputStream().close() })
}
}

Generator.from(inputPaths).filter(os.exists).flatMap { path =>
if (os.isFile(path)) {
val jarFile = new JarFile(path.toIO)
Generator.from(jarFile.entries().asScala.filterNot(_.isDirectory))
.map(entry => entry.getName -> (() => jarFile.getInputStream(entry)))
.flatMap(entry => shader(entry.getName, () => jarFile.getInputStream(entry)))
}
else {
os.walk
.stream(path)
.filter(os.isFile)
.map(subPath => subPath.relativeTo(path).toString -> (() => os.read.inputStream(subPath)))
.flatMap(subPath => shader(subPath.relativeTo(path).toString, () => os.read.inputStream(subPath)))
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion main/src/modules/Jvm.scala
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ object Jvm {
manifest.build.write(manifestOut)
manifestOut.close()

val mappings = Assembly.loadClasspath(inputPaths)
val mappings = Assembly.loadShadedClasspath(inputPaths, assemblyRules)
Assembly.groupAssemblyEntries(mappings, assemblyRules).foreach {
case (mapping, entry) =>
val path = zipFs.getPath(mapping).toAbsolutePath
Expand Down
13 changes: 13 additions & 0 deletions scalalib/test/resources/hello-world-deps/core/src/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
import akka.http.scaladsl.server.Directives._

object Main extends App {
val route =
path("hello") {
get {
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to akka-http</h1>"))
}
}

println(route)
}
40 changes: 37 additions & 3 deletions scalalib/test/src/HelloWorldTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ object HelloWorldTests extends TestSuite {
}
}

object HelloWorldAkkaHttpRelocate extends HelloBase {
object core extends HelloWorldModuleWithMain {
def ivyDeps = akkaHttpDeps

def assemblyRules = Seq(Assembly.Rule.Relocate("akka.**", "shaded.akka.@1"))
}
}

object HelloWorldAkkaHttpNoRules extends HelloBase {
object core extends HelloWorldModuleWithMain {
def ivyDeps = akkaHttpDeps
Expand Down Expand Up @@ -684,9 +692,7 @@ object HelloWorldTests extends TestSuite {
referenceContent.contains("Akka Stream Reference Config File"),
// our application config is present too
referenceContent.contains("My application Reference Config File"),
referenceContent.contains(
"""akka.http.client.user-agent-header="hello-world-client""""
)
referenceContent.contains("""akka.http.client.user-agent-header="hello-world-client"""")
)
}

Expand Down Expand Up @@ -767,6 +773,34 @@ object HelloWorldTests extends TestSuite {
resourcePath = helloWorldMultiResourcePath
)

def checkRelocate[M <: TestUtil.BaseModule](module: M,
target: Target[PathRef],
resourcePath: os.Path = resourcePath
) =
workspaceTest(module, resourcePath) { eval =>
val Right((result, _)) = eval.apply(target)

val jarFile = new JarFile(result.path.toIO)

assert(!jarEntries(jarFile).contains("akka/http/scaladsl/model/HttpEntity.class"))
assert(jarEntries(jarFile).contains("shaded/akka/http/scaladsl/model/HttpEntity.class"))
}

'relocate - {
'withDeps - checkRelocate(
HelloWorldAkkaHttpRelocate,
HelloWorldAkkaHttpRelocate.core.assembly
)

'run - workspaceTest(
HelloWorldAkkaHttpRelocate,
resourcePath = os.pwd / 'scalalib / 'test / 'resources / "hello-world-deps"
) { eval =>
val Right((_, evalCount)) = eval.apply(HelloWorldAkkaHttpRelocate.core.runMain("Main"))
assert(evalCount > 0)
}
}

'writeDownstreamWhenNoRule - {
'withDeps - workspaceTest(HelloWorldAkkaHttpNoRules) { eval =>
val Right((result, _)) = eval.apply(HelloWorldAkkaHttpNoRules.core.assembly)
Expand Down

0 comments on commit 0db909c

Please sign in to comment.