Skip to content

Commit c982215

Browse files
authored
Split runner out of client assembly (#5025)
Fixes #5002 This decouples the Mill executable and the Mill server classpath. * Previously the Mill server took on the Mill executable as part of the classpath, with this change the Mill server classpath is resolved from Maven Central and the Mill client may not even be a jar anymore (e.g. if it is a graal native image) * Reduces the native executable download size from ~108mb to ~49mb, with the difference being the runner assembly jar we previously prepended to the executable that now gets resolved from maven central. * Removes the messy `MILL_CLASSPATH` environment variable handling that was necessary to reliably get the classpath of the client JVM across all different execution modes (local, packaged, native) * Introduces a `cachedComputedValue` helper in our client Java code to make it easy to wrap arbitrary expensive computations, and used to to add some caching to the `yamlHeader` processing
1 parent 32d6530 commit c982215

File tree

17 files changed

+499
-214
lines changed

17 files changed

+499
-214
lines changed

core/constants/src/mill/constants/Util.java

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package mill.constants;
22

33
import java.io.Console;
4+
import java.io.IOException;
45
import java.lang.reflect.InvocationTargetException;
56
import java.lang.reflect.Method;
67
import java.nio.charset.StandardCharsets;
@@ -76,14 +77,18 @@ public static boolean hasConsole() {
7677
return hasConsole0;
7778
}
7879

79-
public static String readYamlHeader(java.nio.file.Path buildFile) throws java.io.IOException {
80-
java.util.List<String> lines = java.nio.file.Files.readAllLines(buildFile);
81-
String yamlString = lines.stream()
82-
.takeWhile(line -> line.startsWith("//|"))
83-
.map(line -> line.substring(4)) // Remove the `//|` prefix
84-
.collect(java.util.stream.Collectors.joining("\n"));
85-
86-
return yamlString;
80+
public static String readYamlHeader(java.nio.file.Path buildFile) {
81+
try {
82+
java.util.List<String> lines = java.nio.file.Files.readAllLines(buildFile);
83+
String yamlString = lines.stream()
84+
.takeWhile(line -> line.startsWith("//|"))
85+
.map(line -> line.substring(4)) // Remove the `//|` prefix
86+
.collect(java.util.stream.Collectors.joining("\n"));
87+
88+
return yamlString;
89+
} catch (IOException e) {
90+
throw new RuntimeException(e);
91+
}
8792
}
8893

8994
private static String envInterpolatorPattern0 = "(\\$|[A-Z_][A-Z0-9_]*)";

core/util/src/mill/util/Jvm.scala

+6-1
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,12 @@ object Jvm {
679679

680680
val testOverridesClassloaders = System.getenv("MILL_LOCAL_TEST_OVERRIDE_CLASSPATH") match {
681681
case null => Nil
682-
case cp => Seq(new URLClassLoader(Array(os.Path(cp).toNIO.toUri.toURL)))
682+
case cp =>
683+
cp.split(';').map { s =>
684+
val url = os.Path(s).toNIO.toUri.toURL
685+
mill.constants.DebugLog.println("MILL_LOCAL_TEST_OVERRIDE_CLASSPATH " + url)
686+
new java.net.URLClassLoader(Array(url))
687+
}.toList
683688
}
684689

685690
try {

developer.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ You can reproduce these tests manually using `dist.installLocal`:
125125

126126
[source,console]
127127
----
128-
> ./mill dist.installLocal && (cd example/basic/1-simple && ../../../mill-assembly.jar run --text hello)
128+
> ./mill dist.installLocal && (cd example/javalib/basic/1-simple && ../../../../mill-assembly.jar run --text hello)
129129
----
130130

131131
You can also use `dist.native.installLocal` for a Graal Native Image executable,

dist/package.mill

+9-25
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ trait DistModule extends Module {
1515
lazy val allPublishModules = build.moduleInternal.modules.collect {
1616
case m: build.MillPublishJavaModule => m
1717
}
18-
def moduleDeps = Seq(build.runner)
1918

2019
def executableRaw: T[PathRef]
2120

@@ -73,8 +72,7 @@ trait DistModule extends Module {
7372

7473
object `package` extends MillJavaModule with DistModule {
7574

76-
// disable scalafix here because it crashes when a module has no sources
77-
def moduleDeps = Seq(build.runner)
75+
def moduleDeps = Seq(build.runner.client)
7876

7977
def transitiveLocalTestOverrides = Map(
8078
Task.traverse(allPublishModules)(_.localTestOverride)(): _*
@@ -91,10 +89,7 @@ object `package` extends MillJavaModule with DistModule {
9189
val isWin = scala.util.Properties.isWin
9290
val outputPath = Task.dest / (if (isWin) "run.bat" else "run")
9391

94-
val launcherForkArgs = testArgs() ++
95-
Seq(
96-
"-DMILL_CLASSPATH=" + runClasspath().map(_.path.toString).mkString(",")
97-
)
92+
val launcherForkArgs = testArgs()
9893
val (millArgs, otherArgs) =
9994
launcherForkArgs.partition(arg =>
10095
arg.startsWith("-DMILL") && !arg.startsWith("-DMILL_VERSION")
@@ -138,9 +133,6 @@ object `package` extends MillJavaModule with DistModule {
138133
"-Djna.nosys=true"
139134
)
140135

141-
def forkShellArgs = Seq("-DMILL_CLASSPATH=$0")
142-
def forkCmdArgs = Seq(""""-DMILL_CLASSPATH=%~dpnx0"""")
143-
144136
def mainClass = Some("mill.runner.client.MillClientMain")
145137

146138
def executableRaw = Task {
@@ -260,26 +252,18 @@ object `package` extends MillJavaModule with DistModule {
260252
s"${build.dist.artifactName()}-native-${artifactOsSuffix()}-${artifactCpuSuffix()}"
261253
}
262254

263-
def nativeImageClasspath = build.runner.client.runClasspath()
255+
// Use assembly jar as the upstream ivy classpath rather than using runClasspath
256+
// directly to try and avoid native image command length problems on windows
257+
def nativeImageClasspath =
258+
Seq(build.runner.client.resolvedIvyAssembly().pathRef) ++
259+
build.runner.client.upstreamLocalAssemblyClasspath() ++
260+
build.runner.client.localClasspath()
264261

265262
def localBinName = "mill-native"
266263

267264
def cacheBinarySuffix = "-native"
268265

269-
def executableRaw = Task {
270-
val previous = nativeImage().path
271-
val executable = Task.dest / previous.baseName
272-
273-
Using(os.write.outputStream(executable)) { out =>
274-
out.write(os.read.bytes(previous))
275-
out.write(System.lineSeparator.getBytes)
276-
out.write(os.read.bytes(assembly().path))
277-
}
278-
279-
if (!mill.constants.Util.isWindows) os.perms.set(executable, "rwxrwxrwx")
280-
281-
PathRef(executable)
282-
}
266+
def executableRaw = nativeImage()
283267

284268
def nativeImageOptions = Seq(
285269
"--no-fallback",

example/extending/plugins/7-writing-mill-plugins/build.mill

+10-7
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,20 @@ object myplugin extends ScalaModule with PublishModule {
1919
// Testing Config, with necessary setup for unit/integration/example tests
2020
object test extends ScalaTests with TestModule.Utest {
2121
def mvnDeps = Seq(mvn"com.lihaoyi::mill-testkit:$millVersion")
22-
def forkEnv = Map("MILL_EXECUTABLE_PATH" -> millExecutable.assembly().path.toString)
22+
def forkEnv = Task {
23+
val p = Task.dest / "mill/local-test-overrides" / s"com.lihaoyi-${myplugin.artifactId()}"
24+
os.write(p, myplugin.localClasspath().map(_.path).mkString("\n"), createFolders = true)
25+
Map(
26+
"MILL_EXECUTABLE_PATH" -> millExecutable.assembly().path.toString,
27+
"MILL_LOCAL_TEST_OVERRIDE_CLASSPATH" ->
28+
(sys.env.get("MILL_LOCAL_TEST_OVERRIDE_CLASSPATH") ++ Seq(Task.dest)).mkString(";")
29+
)
30+
}
2331

2432
// Create a Mill executable configured for testing our plugin
2533
object millExecutable extends JavaModule {
26-
def mvnDeps = Seq(mvn"com.lihaoyi:mill-runner_3:$millVersion")
34+
def mvnDeps = Seq(mvn"com.lihaoyi:mill-runner-client_3:$millVersion")
2735
def mainClass = Some("mill.runner.client.MillClientMain")
28-
def resources = Task {
29-
val p = Task.dest / "mill/local-test-overrides" / s"com.lihaoyi-${myplugin.artifactId()}"
30-
os.write(p, myplugin.localClasspath().map(_.path).mkString("\n"), createFolders = true)
31-
Seq(PathRef(Task.dest))
32-
}
3336
}
3437
}
3538

example/extending/plugins/7-writing-mill-plugins/myplugin/test/src/mill/testkit/ExampleTests.scala

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ object ExampleTests extends TestSuite {
66

77
def tests: Tests = Tests {
88
test("example") {
9+
// pprint.log(sys.env("MILL_EXECUTABLE_PATH"))
10+
// pprint.log(sys.env("MILL_LOCAL_TEST_OVERRIDE_CLASSPATH"))
911
val resourceFolder = os.Path(sys.env("MILL_TEST_RESOURCE_DIR"))
1012
ExampleTester.run(
1113
clientServerMode = true,

example/extending/plugins/7-writing-mill-plugins/myplugin/test/src/mill/testkit/IntegrationTests.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ object IntegrationTests extends TestSuite {
99
println("initializing myplugin.IntegrationTest.tests")
1010

1111
test("integration") {
12-
pprint.log(sys.env("MILL_EXECUTABLE_PATH"))
12+
// pprint.log(sys.env("MILL_EXECUTABLE_PATH"))
13+
// pprint.log(sys.env("MILL_LOCAL_TEST_OVERRIDE_CLASSPATH"))
1314
val resourceFolder = os.Path(sys.env("MILL_TEST_RESOURCE_DIR"))
1415
val tester = new IntegrationTester(
1516
clientServerMode = true,

integration/invalidation/process-file-deleted-exit/src/ProcessFileDeletedExit.scala

+10-3
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,17 @@ object ProcessFileDeletedExit extends UtestIntegrationTestSuite {
4141
if (tester.clientServerMode) workspacePath / "out/mill-server"
4242
else workspacePath / "out/mill-no-server"
4343

44-
os.list(processRoot).map(p => os.remove(p / "processId"))
44+
eventually {
45+
os.walk(processRoot).exists(_.last == "processId")
46+
}
47+
48+
os.list(processRoot).map { p =>
49+
os.remove(p / "processId")
50+
}
4551

46-
// Not sure why this is flaky on windows but disable it
47-
if (!mill.constants.Util.isWindows) eventually { watchTerminated == true }
52+
eventually {
53+
watchTerminated == true
54+
}
4855
}
4956
}
5057
}

runner/client/package.mill

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package build.runner.client
2+
3+
import mill._
4+
import mill.contrib.buildinfo.BuildInfo
5+
6+
object `package` extends build.MillPublishScalaModule with BuildInfo {
7+
// Disable scalafix because it seems to misbehave and cause
8+
// spurious errors when there are mixed Java/Scala sources
9+
def fix(args: String*): Command[Unit] = Task.Command {}
10+
11+
def buildInfoPackageName = "mill.runner.client"
12+
13+
def moduleDeps = Seq(build.runner.server.client)
14+
15+
def mvnDeps = Agg(
16+
build.Deps.nativeTerminal,
17+
build.Deps.coursier,
18+
build.Deps.coursierInterface,
19+
build.Deps.coursierJvm,
20+
build.Deps.logback,
21+
build.Deps.snakeyamlEngine,
22+
build.Deps.osLib
23+
)
24+
25+
def buildInfoObjectName = "Versions"
26+
27+
def buildInfoMembers = Task {
28+
val jlineNativeVersion = compileClasspath().map(_.path.last)
29+
.find(name => name.startsWith("jline-native-") && name.endsWith(".jar"))
30+
.map(_.stripPrefix("jline-native-").stripSuffix(".jar"))
31+
.getOrElse {
32+
sys.error("Cannot get jline-native version from compile class path")
33+
}
34+
Seq(
35+
BuildInfo.Value("coursierJvmIndexVersion", build.Deps.coursierJvmIndexVersion),
36+
BuildInfo.Value("jlineNativeVersion", jlineNativeVersion)
37+
)
38+
}
39+
}

0 commit comments

Comments
 (0)