Skip to content

Commit

Permalink
Pre compile zinc compiler interface for common Scala versions (#2424)
Browse files Browse the repository at this point in the history
While we still the to keep the dynamic codepath necessary to compile
stuff at runtime, at least for the common case of existing Scala
versions we can pre-compile it, improving performance and removing some
of the confusing log messages about compiling stuff that the user didn't
write. This only affects the first load, but first impressions matter.
Doesn't benefit Scala 3, but there's half the community is still on
Scala 2, and Mill's own `build.sc` is still on Scala 2

Before, we can see that a clean `run` ends up compiling the compiler
interface twice (once to compile `build.sc`, once to compile the user's
`ScalaModule`) taking ~14s on my laptop

```
Compiling compiler interface...
[info] compiling 1 Scala source to /Users/lihaoyi/Github/mill/example/basic/1-hello-world/out/mill-build/compile.dest/classes ...
[info] done compiling
[34/48] compile
Compiling compiler interface...
8 warnings
[info] compiling 1 Scala source to /Users/lihaoyi/Github/mill/example/basic/1-hello-world/out/compile.dest/classes ...
[warn] 1 deprecation (since 2.13.0); re-run with -deprecation for details
[warn] one warning found
[info] done compiling
[48/48] run
<h1>hello</h1>
/Users/lihaoyi/Github/mill/target/mill-release -i run --text hello  47.58s user 1.70s system 343% cpu 14.345 total
```

After, we compile the compiler interface zero times, with a clean `run`
taking ~7s on my laptop

```
[build.sc] [40/47] compile
[info] compiling 1 Scala source to /Users/lihaoyi/Github/mill/example/basic/1-hello-world/out/mill-build/compile.dest/classes ...
[info] done compiling
[34/48] compile
[info] compiling 1 Scala source to /Users/lihaoyi/Github/mill/example/basic/1-hello-world/out/compile.dest/classes ...
[warn] 1 deprecation (since 2.13.0); re-run with -deprecation for details
[warn] one warning found
[info] done compiling
[48/48] run
<h1>hello</h1>
/Users/lihaoyi/Github/mill/target/mill-release -i run --text hello  19.27s user 0.85s system 297% cpu 6.768 total
```

## Implementation

I added cross modules `bridge[_]` for all supported Scala versions.
These more or less follow what `ZincWorkerModule#scalaCompilerBridgeJar`
already does on-the-fly: download the respective source jar from Maven
Central and compile it using the respective Scala version. Note that the
`META-INF` metadata is necessary for Zinc to properly pick these up, and
so I manually copy the folder from the unzipped source jar into the
`resources` so it can get propagated to the final jar.

To avoid the slowness of compiling every bridge version during local
development, we separate the `bridge[_]` publishing from the rest of the
build, only enabled via the `MILL_BUILD_COMPILER_BRIDGES=true`
environment variable, and published under a separate version. We expect
to bump the `bridge[_]` version rarely, and want to avoid redundantly
re-publishing them every Mill release.

## Testing

Most of the existing unit tests use the three scala versions that we
pre-compile in development mode, though some of them are left on other
versions for legacy reasons e.g. they depend on a specific version of
Scala to be compatible with a specific version of semanticDB or
scala-native, and those continue to use the old
download-compile-on-the-fly code path.

I also added a new test case
`mill.scalalib.HelloWorldTests.compile.nonPreCompiledBridge` that
together with `.fromScratch` specifically exercises the code paths for
compiled/not-compiled compiler bridges, asserting that the compiler
bridge is compiled in the versions it should be compiled for (i.e. those
that are not pre-compiled) and not compiled for versions it should not
be compiled for (i.e. those that *are* pre-compiled)
  • Loading branch information
lihaoyi authored Apr 11, 2023
1 parent 3d57f3b commit 0855b62
Show file tree
Hide file tree
Showing 48 changed files with 401 additions and 195 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/publish-bridges.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Publish Bridges

# Manually-triggered github action to publish mill-scala-compiler-bridges jars,
# since those do not change frequently enough to be worth including in the main
# publishing workflow that runs every Mill version
on:
pull_request:
workflow_dispatch:
jobs:
publish-bridges:
runs-on: ubuntu-latest

concurrency: publish-sonatype-${{ github.sha }}

env:
SONATYPE_PGP_SECRET: ${{ secrets.SONATYPE_PGP_SECRET }}
SONATYPE_USERNAME: ${{ secrets.SONATYPE_DEPLOY_USER }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_DEPLOY_PASSWORD }}
SONATYPE_PGP_PASSWORD: ${{ secrets.SONATYPE_PGP_PASSWORD }}
LANG: "en_US.UTF-8"
LC_MESSAGES: "en_US.UTF-8"
LC_ALL: "en_US.UTF-8"
MILL_BUILD_COMPILER_BRIDGES: "true"

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0

- uses: actions/setup-java@v3
with:
java-version: 8
distribution: temurin

- run: ci/release-bridge-maven.sh
124 changes: 95 additions & 29 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,62 @@ def millBinPlatform: T[String] = T {
}
def baseDir = build.millSourcePath

// We limit the number of compiler bridges to compile and publish for local
// development and testing, because otherwise it takes forever to compile all
// of them. Compiler bridges not in this set will get downloaded and compiled
// on the fly anyway. For publishing, we publish everything.
val buildAllCompilerBridges = interp.watchValue(sys.env.contains("MILL_BUILD_COMPILER_BRIDGES"))
val bridgeVersion = "0.0.1"
val bridgeScalaVersions = Seq(
// Our version of Zinc doesn't work with Scala 2.12.0 and 2.12.4 compiler
// bridges. We skip 2.12.1 because it's so old not to matter, and we need a
// non-supported scala versionm for testing purposes. We skip 2.13.0-2 because
// scaladoc fails on windows
/*"2.12.0",*/ /*2.12.1",*/ "2.12.2", "2.12.3", /*"2.12.4",*/ "2.12.5", "2.12.6", "2.12.7", "2.12.8",
"2.12.9", "2.12.10", "2.12.11", "2.12.12", "2.12.13", "2.12.14", "2.12.15", "2.12.16", "2.12.17",
/*"2.13.0", "2.13.1", "2.13.2",*/ "2.13.3", "2.13.4", "2.13.5", "2.13.6", "2.13.7", "2.13.8", "2.13.9", "2.13.10"
)
val buildBridgeScalaVersions =
if (!buildAllCompilerBridges) Seq()
else bridgeScalaVersions

object bridge extends Cross[BridgeModule](buildBridgeScalaVersions: _*)
class BridgeModule(val crossScalaVersion: String) extends PublishModule with CrossScalaModule {
def scalaVersion = crossScalaVersion
def publishVersion = bridgeVersion
def artifactName = T{ "mill-scala-compiler-bridge" }
def pomSettings = commonPomSettings(artifactName())
def crossFullScalaVersion = true
def ivyDeps = Agg(
ivy"org.scala-sbt:compiler-interface:${Versions.zinc}",
ivy"org.scala-lang:scala-compiler:${crossScalaVersion}",
)

def resources = T.sources {
os.copy(generatedSources().head.path / "META-INF", T.dest / "META-INF")
Seq(PathRef(T.dest))
}

def generatedSources = T {
import mill.scalalib.api.ZincWorkerUtil.{grepJar, scalaBinaryVersion}
val resolvedJars = resolveDeps(
T.task { Agg(ivy"org.scala-sbt::compiler-bridge:${Deps.zinc.dep.version}") },
sources = true
)()

val bridgeJar = grepJar(
resolvedJars.map(_.path),
s"compiler-bridge_${scalaBinaryVersion(scalaVersion())}",
Deps.zinc.dep.version,
true
)

mill.api.IO.unpackZip(bridgeJar, os.rel)

Seq(PathRef(T.dest))
}
}


trait BuildInfo extends JavaModule {
/**
Expand Down Expand Up @@ -397,15 +453,9 @@ object BuildInfo{
}
}


trait MillPublishModule extends PublishModule {
override def artifactName = "mill-" + super.artifactName()
def publishVersion = millVersion()
override def publishProperties: Target[Map[String, String]] = super.publishProperties() ++ Map(
"info.releaseNotesURL" -> Settings.changelogUrl
)
def pomSettings = PomSettings(
description = artifactName(),
def commonPomSettings(artifactName: String) = {
PomSettings(
description = artifactName,
organization = Settings.pomOrg,
url = Settings.projectUrl,
licenses = Seq(License.MIT),
Expand All @@ -415,6 +465,15 @@ trait MillPublishModule extends PublishModule {
Developer("lefou", "Tobias Roeser", "https://github.com/lefou")
)
)
}

trait MillPublishModule extends PublishModule {
override def artifactName = "mill-" + super.artifactName()
def publishVersion = millVersion()
override def publishProperties: Target[Map[String, String]] = super.publishProperties() ++ Map(
"info.releaseNotesURL" -> Settings.changelogUrl
)
def pomSettings = commonPomSettings(artifactName())
override def javacOptions = Seq("-source", "1.8", "-target", "1.8", "-encoding", "UTF-8")
}

Expand Down Expand Up @@ -652,25 +711,17 @@ object main extends MillModule {
object testrunner extends MillModule {
override def moduleDeps = Seq(scalalib.api, main.util)
}
object scalalib extends MillModule with BuildInfo{


object scalalib extends MillModule {
override def moduleDeps = Seq(main, scalalib.api, testrunner)

override def ivyDeps = Agg(
Deps.scalafmtDynamic
)

def buildInfoPackageName = "mill.scalalib"
def buildInfoObjectName = "Versions"

def buildInfoMembers = Seq(
BuildInfo.Value("ammonite", Deps.ammoniteVersion, "Version of Ammonite."),
BuildInfo.Value("zinc", Deps.zinc.dep.version, "Version of Zinc"),
BuildInfo.Value("semanticDBVersion", Deps.semanticDB.dep.version, "SemanticDB version."),
BuildInfo.Value("semanticDbJavaVersion", Deps.semanticDbJava.dep.version, "Java SemanticDB plugin version."),
BuildInfo.Value("millModuledefsVersion", Deps.millModuledefsVersion, "Mill ModuleDefs plugins version.")
)

override def testIvyDeps = super.testIvyDeps() ++ Agg(Deps.scalaCheck)

def testArgs = T {

val artifactsString =
Expand All @@ -684,7 +735,7 @@ object scalalib extends MillModule with BuildInfo{
"-Djna.nosys=true",
"-DMILL_SCALA_LIB=" + runClasspath().map(_.path).mkString(","),
s"-DTEST_SCALAFMT_VERSION=${Deps.scalafmtDynamic.dep.version}",
s"-DMILL_EMBEDDED_DEPS=\"$artifactsString\""
s"-DMILL_EMBEDDED_DEPS=$artifactsString"
)
}
object backgroundwrapper extends MillPublishModule {
Expand All @@ -697,9 +748,25 @@ object scalalib extends MillModule with BuildInfo{
)
}
}
object api extends MillApiModule {
object api extends MillApiModule with BuildInfo {
override def moduleDeps = Seq(main.api)

def buildInfoPackageName = "mill.scalalib.api"

def buildInfoObjectName = "Versions"

def buildInfoMembers = Seq(
BuildInfo.Value("ammonite", Deps.ammoniteVersion, "Version of Ammonite."),
BuildInfo.Value("zinc", Deps.zinc.dep.version, "Version of Zinc"),
BuildInfo.Value("semanticDBVersion", Deps.semanticDB.dep.version, "SemanticDB version."),
BuildInfo.Value("semanticDbJavaVersion", Deps.semanticDbJava.dep.version, "Java SemanticDB plugin version."),
BuildInfo.Value("millModuledefsVersion", Deps.millModuledefsVersion, "Mill ModuleDefs plugins version."),
BuildInfo.Value("millCompilerBridgeScalaVersions", bridgeScalaVersions.mkString(",")),
BuildInfo.Value("millCompilerBridgeVersion", bridgeVersion),
BuildInfo.Value("millVersion", millVersion(), "Mill version.")
)
}

object worker extends MillInternalModule with BuildInfo{

override def moduleDeps = Seq(scalalib.api)
Expand All @@ -708,11 +775,7 @@ object scalalib extends MillModule with BuildInfo{
Deps.zinc,
Deps.log4j2Core
)
def testArgs = T {
Seq(
"-DMILL_SCALA_WORKER=" + runClasspath().map(_.path).mkString(",")
)
}
def testArgs = Seq("-DMILL_SCALA_WORKER=" + runClasspath().map(_.path).mkString(","))

def buildInfoPackageName = "mill.scalalib.worker"
def buildInfoObjectName = "Versions"
Expand Down Expand Up @@ -1092,7 +1155,10 @@ def installLocalCache() = T.command {
}

def installLocalTask(binFile: Task[String], ivyRepo: String = null): Task[os.Path] = {
val modules = build.millInternal.modules.collect { case m: PublishModule => m }
val modules = build.millInternal.modules.collect{
case m: PublishModule => m
}

T.task {
T.traverse(modules)(m => m.publishLocal(ivyRepo))()
val millBin = assembly()
Expand Down
56 changes: 41 additions & 15 deletions ci/mill-bootstrap.patch
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
diff --git a/build.sc b/build.sc
index cbc7b7aaa..351a37fbc 100644
index 8fcdeb538..c8155558d 100644
--- a/build.sc
+++ b/build.sc
@@ -2,22 +2,12 @@
@@ -2,27 +2,18 @@
import $file.ci.shared
import $file.ci.upload
import $ivy.`org.scalaj::scalaj-http:2.4.2`
Expand All @@ -27,7 +27,13 @@ index cbc7b7aaa..351a37fbc 100644
import mill._
import mill.define.{Command, Source, Sources, Target, Task}
import mill.eval.Evaluator
@@ -185,12 +175,8 @@ object Deps {
import mill.main.MainModule
import mill.scalalib._
+import mill.scalalib.api.Versions
import mill.scalalib.publish._
import mill.modules.Jvm
import mill.define.SelectMode
@@ -185,12 +176,8 @@ object Deps {
val requests = ivy"com.lihaoyi::requests:0.8.0"
}

Expand All @@ -42,10 +48,31 @@ index cbc7b7aaa..351a37fbc 100644
def millBinPlatform: T[String] = T {
val tag = millLastTag()
if (tag.contains("-M")) tag
@@ -202,202 +188,6 @@ def millBinPlatform: T[String] = T {
def baseDir = build.millSourcePath
@@ -239,219 +226,23 @@ class BridgeModule(val crossScalaVersion: String) extends PublishModule with Cro
def generatedSources = T {
import mill.scalalib.api.ZincWorkerUtil.{grepJar, scalaBinaryVersion}
val resolvedJars = resolveDeps(
- T.task { Agg(ivy"org.scala-sbt::compiler-bridge:${Deps.zinc.dep.version}") },
+ T.task { Agg(ivy"org.scala-sbt::compiler-bridge:${Deps.zinc.dep.version}").map(bindDependency()) },
sources = true
)()

val bridgeJar = grepJar(
- resolvedJars.map(_.path),
+ resolvedJars,
s"compiler-bridge_${scalaBinaryVersion(scalaVersion())}",
Deps.zinc.dep.version,
true
)

- mill.api.IO.unpackZip(bridgeJar, os.rel)
+ mill.api.IO.unpackZip(bridgeJar.path, os.rel)

Seq(PathRef(T.dest))
}
}

-
-trait BuildInfo extends JavaModule {
- /**
- * The package name under which the BuildInfo data object will be stored.
Expand Down Expand Up @@ -241,11 +268,10 @@ index cbc7b7aaa..351a37fbc 100644
- }
-}
-
-
trait MillPublishModule extends PublishModule {
override def artifactName = "mill-" + super.artifactName()
def publishVersion = millVersion()
@@ -447,27 +237,8 @@ trait MillCoursierModule extends CoursierModule {
def commonPomSettings(artifactName: String) = {
PomSettings(
description = artifactName,
@@ -505,27 +296,8 @@ trait MillCoursierModule extends CoursierModule {
)
}

Expand Down Expand Up @@ -274,39 +300,39 @@ index cbc7b7aaa..351a37fbc 100644
}

/** A Module compiled with applied Mill-specific compiler plugins: mill-moduledefs. */
@@ -758,6 +529,7 @@ object scalajslib extends MillModule with BuildInfo{
@@ -821,6 +593,7 @@ object scalajslib extends MillModule with BuildInfo{
}
object worker extends Cross[WorkerModule]("1")
class WorkerModule(scalajsWorkerVersion: String) extends MillInternalModule {
+ override def millSourcePath: os.Path = super.millSourcePath / scalajsWorkerVersion
override def moduleDeps = Seq(scalajslib.`worker-api`)
override def ivyDeps = Agg(
Deps.Scalajs_1.scalajsLinker,
@@ -820,6 +592,7 @@ object contrib extends MillModule {
@@ -883,6 +656,7 @@ object contrib extends MillModule {

object worker extends Cross[WorkerModule](Deps.play.keys.toSeq: _*)
class WorkerModule(playBinary: String) extends MillInternalModule {
+ override def millSourcePath: os.Path = super.millSourcePath / playBinary
override def sources = T.sources {
// We want to avoid duplicating code as long as the Play APIs allow.
// But if newer Play versions introduce incompatibilities,
@@ -1011,6 +784,7 @@ object scalanativelib extends MillModule {
@@ -1074,6 +848,7 @@ object scalanativelib extends MillModule {
object worker extends Cross[WorkerModule]("0.4")
class WorkerModule(scalaNativeWorkerVersion: String)
extends MillInternalModule {
+ override def millSourcePath: os.Path = super.millSourcePath / scalaNativeWorkerVersion
override def moduleDeps = Seq(scalanativelib.`worker-api`)
override def ivyDeps = scalaNativeWorkerVersion match {
case "0.4" =>
@@ -1171,6 +945,7 @@ trait IntegrationTestModule extends MillScalaModule {
@@ -1237,6 +1012,7 @@ trait IntegrationTestModule extends MillScalaModule {
}

trait IntegrationTestCrossModule extends IntegrationTestModule {
+ override def millSourcePath = super.millSourcePath / repoSlug
object local extends ModeModule
object fork extends ModeModule
object server extends ModeModule
@@ -1725,53 +1500,7 @@ def launcher = T {
@@ -1791,53 +1567,7 @@ def launcher = T {


def uploadToGithub(authKey: String) = T.command {
Expand Down
25 changes: 25 additions & 0 deletions ci/release-bridge-maven.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash

set -eu

echo $SONATYPE_PGP_SECRET | base64 --decode > gpg_key

gpg --import --no-tty --batch --yes gpg_key

rm gpg_key

# Build all artifacts
./mill -i __.publishArtifacts

export MILL_BUILD_COMPILER_BRIDGES=true

# Publish all artifacts
./mill -i \
mill.scalalib.PublishModule/publishAll \
--sonatypeCreds $SONATYPE_USERNAME:$SONATYPE_PASSWORD \
--gpgArgs --passphrase=$SONATYPE_PGP_PASSWORD,--no-tty,--pinentry-mode,loopback,--batch,--yes,-a,-b \
--publishArtifacts bridges.__.publishArtifacts \
--readTimeout 3600000 \
--awaitTimeout 3600000 \
--release true \
--signed true
2 changes: 1 addition & 1 deletion example/basic/1-hello-world/build.sc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import mill._, scalalib._

object foo extends RootModule with ScalaModule {
def scalaVersion = "2.13.2"
def scalaVersion = "2.13.8"
def ivyDeps = Agg(
ivy"com.lihaoyi::scalatags:0.8.2",
ivy"com.lihaoyi::mainargs:0.4.0"
Expand Down
Loading

0 comments on commit 0855b62

Please sign in to comment.