Skip to content

Commit 290fdcf

Browse files
lihaoyilefouautofix-ci[bot]
authored
Add a YAML header comment syntax for configuration of .mill- files, import $ivy, mill-version (#4969)
This PR adds a YAML 1.2 header comment syntax that subsumes many of the ad-hoc configuration mechanisms we have today: `.mill-version`, `.mill-opts`, `.mill-jvm-opts`, `.mill-jvm-version`, `import $ivy`, and possibly more meta-build overrides or other configuration options in future. ```scala //| mill-version: 0.13.0 //| mill-jvm-version: 17 //| repositories: [$PWD_URI/custom-repo] //| mvnDeps: [org.thymeleaf:thymeleaf:3.1.1.RELEASE] package build import mill._, javalib._ import org.thymeleaf.TemplateEngine import org.thymeleaf.context.Context object foo extends JavaModule { def htmlSnippet = Task { val context = new Context context.setVariable("heading", "hello") context.setVariable("paragraph", "world") new TemplateEngine().process( """<div><h1 th:text="${heading}"></h1><p th:text="${paragraph}"></p></div>""", context ) } def resources = Task { os.write(Task.dest / "snippet.txt", htmlSnippet()) super.resources() ++ Seq(PathRef(Task.dest)) } } ``` The YAML header for any `.mill` file must be a line-comment block starting from the first line of the file and prefixed by `//|`. The prefix is chosen to avoid conflicts with normal comments (`//`), Scala-CLI directives (`//>`), and Java markdown comments (`///`), and to be reminiscent of Scala `|`s commonly used in `.stripMargin` We use YAML 1.2 (via `snakeyaml-engine`) rather than ScalaCLI directives because: - It can map more directly to Mill concepts: `mill-version`, `mill-jvm-version`, `mvnDeps`, and other config keys can be directly in YAML without any mapping. - YAML's hierarchical list/dict/primitive data model maps clearly to Mill's JSON data model for tasks, whereas Scala-CLI directives do not have such a clean correspondence and will require that we manually maintain an explicit mapping - YAML will also be more familiar to non-Scala developers than Scala-CLI's directive syntax, including all the edge cases that are moderately well defined (quoting? escaping? comments? type coercions? multi-line strings?) - Processing by third-party tools will be easier: anyone can write a Python script to strip the prefixes and parse out the YAML using pyyaml or yamlcore, or their equivalents in Node.js/Ruby/Rust/Go/whatever, whereas with Scala-CLI directives you are restricted to a single JVM implementation without spec or documentation All of these concerns were raised in the original Scala-CLI directives discussion but were ignored ([link](https://contributors.scala-lang.org/t/pre-sip-using-directives/5700/5?u=lihaoyi), [link](https://contributors.scala-lang.org/t/pre-sip-scala-cli-as-new-scala-command/5628/92?u=lihaoyi)), so that brings us to today where we have to diverge from their syntax. The choice of YAML over TOML was largely arbitrary. Python chose TOML for their script-header metadata in [PEP-723](https://peps.python.org/pep-0723/), whereas all Markdown implementations ([Github](https://docs.github.com/en/contributing/writing-for-github-docs/using-yaml-frontmatter), [RMarkdown](https://zsmith27.github.io/rmarkdown_crash-course/lesson-4-yaml-headers.html), [Jekyll](https://jekyllrb.com/docs/front-matter/)) chose YAML for their header-metadata syntax. YAML 1.2 seems to fix/mitigate most of the problems with YAML 1.1 (e.g. the `no == false` thing). Kotlin has their own syntax for file-level metadata [`@file:JvmName("Foo")`](https://kotlinlang.org/docs/annotations.html#annotation-use-site-targets), as does Swift [`@Metadata { @DocumentationExtension(mergeBehavior: override) }`](https://www.swift.org/documentation/docc/metadata) Builds on top of #4624 --------- Co-authored-by: Tobias Roeser <le.petit.fou@web.de> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 252ccef commit 290fdcf

File tree

135 files changed

+797
-833
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

135 files changed

+797
-833
lines changed

.config/mill-version

Lines changed: 0 additions & 1 deletion
This file was deleted.

build.mill

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//| mill-version: 0.13.0-M1-43-b217bc
2+
13
package build
24
// imports
35
//import com.github.lolgab.mill.mima.Mima
@@ -230,6 +232,7 @@ object Deps {
230232
val hiltGradlePlugin = mvn"com.google.dagger:hilt-android-gradle-plugin:2.56"
231233

232234
val sbt = mvn"org.scala-sbt:sbt:1.10.10"
235+
val snakeyamlEngine = mvn"org.snakeyaml:snakeyaml-engine:2.9"
233236

234237
object RuntimeDeps {
235238
val dokkaVersion = "2.0.0"

ci/mill-bootstrap.patch

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
diff --git a/build.mill b/build.mill
2+
index d815508132a..ba62e147383 100644
3+
--- a/build.mill
4+
+++ b/build.mill
5+
@@ -19,9 +19,8 @@ import mill.define.Cross
6+
import scala.util.Properties
7+
8+
// plugins and dependencies
9+
-import $meta._
10+
+//| meta: true
11+
12+
-import $packages._
13+
14+
object Settings {
15+
val pomOrg = "com.lihaoyi"
116
diff --git a/mill-build/build.mill b/mill-build/build.mill
217
index f32b3cbcf65..3fc9b0097c8 100644
318
--- a/mill-build/build.mill

contrib/package.mill

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@ import mill.T
1414
import mill.define.Cross
1515
import build.Deps
1616

17-
// plugins and dependencies
18-
import $meta._
19-
2017
/**
2118
* [[build.contrib]] contains user-contributed Mill plugins that satisfy
2219
* a range of use cases not covered by the core Mill builtins. `contrib`

core/constants/package.mill

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import mill.scalalib._
99
* server, and the rest of the Mill codebase.
1010
*/
1111
object `package` extends RootModule with build.MillPublishJavaModule with BuildInfo {
12-
def buildInfoPackageName = "mill.cosntants"
12+
def buildInfoPackageName = "mill.constants"
1313
def buildInfoMembers = Seq(BuildInfo.Value("millVersion", build.millVersion(), "Mill version."))
1414

1515
object test extends JavaTests with TestModule.Junit4 {

core/constants/src/mill/constants/DebugLog.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
* home folder so you can find them
1010
*/
1111
public class DebugLog {
12+
public static synchronized void apply(String s) {
13+
println(s);
14+
}
15+
1216
public static synchronized void println(String s) {
1317
Path path = Paths.get(System.getProperty("user.home"), "mill-debug-log.txt");
1418
try {

core/constants/src/mill/constants/EnvVars.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ public class EnvVars {
2121
*/
2222
public static final String MILL_SERVER_TIMEOUT_MILLIS = "MILL_SERVER_TIMEOUT_MILLIS";
2323

24-
public static final String MILL_JVM_VERSION_PATH = "MILL_JVM_VERSION_PATH";
25-
public static final String MILL_JVM_OPTS_PATH = "MILL_JVM_OPTS_PATH";
26-
public static final String MILL_OPTS_PATH = "MILL_OPTS_PATH";
27-
2824
/**
2925
* Output directory where Mill workers' state and Mill tasks output should be
3026
* written to

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import java.security.MessageDigest;
88
import java.security.NoSuchAlgorithmException;
99
import java.util.Locale;
10+
import java.util.Map;
11+
import java.util.regex.Matcher;
12+
import java.util.regex.Pattern;
1013

1114
public class Util {
1215

@@ -71,4 +74,48 @@ public static String hexArray(byte[] bytes) {
7174
public static boolean hasConsole() {
7275
return hasConsole0;
7376
}
77+
78+
public static String readYamlHeader(java.nio.file.Path buildFile) throws java.io.IOException {
79+
java.util.List<String> lines = java.nio.file.Files.readAllLines(buildFile);
80+
String yamlString = lines.stream()
81+
.filter(line -> line.startsWith("//|"))
82+
.map(line -> line.substring(4)) // Remove the `//|` prefix
83+
.collect(java.util.stream.Collectors.joining("\n"));
84+
85+
return yamlString;
86+
}
87+
88+
private static String envInterpolatorPattern0 = "(\\$|[A-Z_][A-Z0-9_]*)";
89+
private static Pattern envInterpolatorPattern =
90+
Pattern.compile("\\$\\{" + envInterpolatorPattern0 + "\\}|\\$" + envInterpolatorPattern0);
91+
92+
/**
93+
* Interpolate variables in the form of <code>${VARIABLE}</code> based on the given Map <code>env</code>.
94+
* Missing vars will be replaced by the empty string.
95+
*/
96+
public static String interpolateEnvVars(String input, Map<String, String> env0) {
97+
Matcher matcher = envInterpolatorPattern.matcher(input);
98+
// StringBuilder to store the result after replacing
99+
StringBuffer result = new StringBuffer();
100+
101+
Map<String, String> env = new java.util.HashMap<>();
102+
env.putAll(env0);
103+
104+
env.put("MILL_VERSION", mill.constants.BuildInfo.millVersion);
105+
while (matcher.find()) {
106+
String match = matcher.group(1);
107+
if (match == null) match = matcher.group(2);
108+
if (match.equals("$")) {
109+
matcher.appendReplacement(result, "\\$");
110+
} else {
111+
String envVarValue;
112+
mill.constants.DebugLog.println("MATCH " + match);
113+
envVarValue = env.containsKey(match) ? env.get(match) : "";
114+
matcher.appendReplacement(result, envVarValue);
115+
}
116+
}
117+
118+
matcher.appendTail(result); // Append the remaining part of the string
119+
return result.toString();
120+
}
74121
}

core/exec/package.mill

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ import mill._
88
*/
99
object `package` extends RootModule with build.MillStableScalaModule {
1010
def moduleDeps = Seq(build.core.define, build.core.internal)
11+
def mvnDeps = Seq(build.Deps.snakeyamlEngine)
1112
}

core/exec/src/mill/exec/Execution.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ private[mill] case class Execution(
3737
systemExit: Int => Nothing,
3838
exclusiveSystemStreams: SystemStreams,
3939
getEvaluator: () => EvaluatorApi,
40-
offline: Boolean
40+
offline: Boolean,
41+
headerData: String
4142
) extends GroupExecution with AutoCloseable {
4243

4344
// this (shorter) constructor is used from [[MillBuildBootstrap]] via reflection
@@ -57,7 +58,8 @@ private[mill] case class Execution(
5758
systemExit: Int => Nothing,
5859
exclusiveSystemStreams: SystemStreams,
5960
getEvaluator: () => EvaluatorApi,
60-
offline: Boolean
61+
offline: Boolean,
62+
headerData: String
6163
) = this(
6264
baseLogger,
6365
new JsonArrayLogger.ChromeProfile(os.Path(outPath) / millChromeProfile),
@@ -76,7 +78,8 @@ private[mill] case class Execution(
7678
systemExit,
7779
exclusiveSystemStreams,
7880
getEvaluator,
79-
offline
81+
offline,
82+
headerData
8083
)
8184

8285
def withBaseLogger(newBaseLogger: Logger) = this.copy(baseLogger = newBaseLogger)

core/exec/src/mill/exec/GroupExecution.scala

Lines changed: 114 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,40 @@ private trait GroupExecution {
3333
def systemExit: Int => Nothing
3434
def exclusiveSystemStreams: SystemStreams
3535
def getEvaluator: () => EvaluatorApi
36+
def headerData: String
37+
lazy val parsedHeaderData: Map[String, ujson.Value] = {
38+
import org.snakeyaml.engine.v2.api.{Load, LoadSettings}
39+
val loaded = new Load(LoadSettings.builder().build()).loadFromString(headerData)
40+
// recursively convert java data structure to ujson.Value
41+
val envWithPwd = env ++ Seq(
42+
"PWD" -> workspace.toString,
43+
"PWD_URI" -> workspace.toNIO.toUri.toString
44+
)
45+
def rec(x: Any): ujson.Value = {
46+
import collection.JavaConverters._
47+
x match {
48+
case d: java.util.Date => ujson.Str(d.toString)
49+
case s: String => ujson.Str(mill.constants.Util.interpolateEnvVars(s, envWithPwd.asJava))
50+
case d: Double => ujson.Num(d)
51+
case d: Int => ujson.Num(d)
52+
case d: Long => ujson.Num(d)
53+
case true => ujson.True
54+
case false => ujson.False
55+
case null => ujson.Null
56+
case m: java.util.Map[Object, Object] =>
57+
import collection.JavaConverters._
58+
val scalaMap = m.asScala
59+
ujson.Obj.from(scalaMap.map { case (k, v) => (k.toString, rec(v)) })
60+
case l: java.util.List[Object] =>
61+
import collection.JavaConverters._
62+
val scalaList: collection.Seq[Object] = l.asScala
63+
ujson.Arr.from(scalaList.map(rec))
64+
}
65+
}
66+
67+
rec(loaded).objOpt.getOrElse(Map.empty[String, ujson.Value]).toMap
68+
}
69+
3670
lazy val constructorHashSignatures: Map[String, Seq[(String, Int)]] =
3771
CodeSigUtils.constructorHashSignatures(codeSignatures)
3872

@@ -85,79 +119,95 @@ private trait GroupExecution {
85119
terminal match {
86120

87121
case labelled: NamedTask[_] =>
88-
val out = if (!labelled.ctx.external) outPath else externalOutPath
89-
val paths = ExecutionPaths.resolve(out, labelled.ctx.segments)
90-
val cached = loadCachedJson(logger, inputsHash, labelled, paths)
91-
92-
// `cached.isEmpty` means worker metadata file removed by user so recompute the worker
93-
val upToDateWorker = loadUpToDateWorker(logger, inputsHash, labelled, cached.isEmpty)
94-
95-
val cachedValueAndHash =
96-
upToDateWorker.map((_, inputsHash))
97-
.orElse(cached.flatMap { case (inputHash, valOpt, valueHash) =>
98-
valOpt.map((_, valueHash))
99-
})
100-
101-
cachedValueAndHash match {
102-
case Some((v, hashCode)) =>
103-
val res = ExecResult.Success((v, hashCode))
104-
val newResults: Map[Task[?], ExecResult[(Val, Int)]] =
105-
Map(labelled -> res)
106-
122+
labelled.ctx.segments.value match {
123+
case Seq(Segment.Label(single)) if parsedHeaderData.contains(single) =>
124+
val jsonData = parsedHeaderData(single)
125+
val resultData = upickle.default.read[Any](jsonData)(
126+
using labelled.readWriterOpt.get.asInstanceOf[upickle.default.Reader[Any]]
127+
)
107128
GroupExecution.Results(
108-
newResults,
129+
Map(labelled -> ExecResult.Success(Val(resultData), resultData.##)),
109130
Nil,
110131
cached = true,
111132
inputsHash,
112133
-1,
113-
valueHashChanged = false
134+
false
114135
)
115-
116136
case _ =>
117-
// uncached
118-
if (!labelled.persistent) os.remove.all(paths.dest)
119-
120-
val (newResults, newEvaluated) =
121-
executeGroup(
122-
group = group,
123-
results = results,
124-
inputsHash = inputsHash,
125-
paths = Some(paths),
126-
maybeTargetLabel = Some(terminal.toString),
127-
counterMsg = countMsg,
128-
reporter = zincProblemReporter,
129-
testReporter = testReporter,
130-
logger = logger,
131-
executionContext = executionContext,
132-
exclusive = exclusive,
133-
isCommand = labelled.isInstanceOf[Command[?]],
134-
deps = deps,
135-
offline = offline
136-
)
137-
138-
val valueHash = newResults(labelled) match {
139-
case ExecResult.Success((v, _)) =>
140-
val valueHash = getValueHash(v, terminal, inputsHash)
141-
handleTaskResult(v, valueHash, paths.meta, inputsHash, labelled)
142-
valueHash
137+
val out = if (!labelled.ctx.external) outPath else externalOutPath
138+
val paths = ExecutionPaths.resolve(out, labelled.ctx.segments)
139+
val cached = loadCachedJson(logger, inputsHash, labelled, paths)
140+
141+
// `cached.isEmpty` means worker metadata file removed by user so recompute the worker
142+
val upToDateWorker = loadUpToDateWorker(logger, inputsHash, labelled, cached.isEmpty)
143+
144+
val cachedValueAndHash =
145+
upToDateWorker.map((_, inputsHash))
146+
.orElse(cached.flatMap { case (inputHash, valOpt, valueHash) =>
147+
valOpt.map((_, valueHash))
148+
})
149+
150+
cachedValueAndHash match {
151+
case Some((v, hashCode)) =>
152+
val res = ExecResult.Success((v, hashCode))
153+
val newResults: Map[Task[?], ExecResult[(Val, Int)]] =
154+
Map(labelled -> res)
155+
156+
GroupExecution.Results(
157+
newResults,
158+
Nil,
159+
cached = true,
160+
inputsHash,
161+
-1,
162+
valueHashChanged = false
163+
)
143164

144165
case _ =>
145-
// Wipe out any cached meta.json file that exists, so
146-
// a following run won't look at the cached metadata file and
147-
// assume it's associated with the possibly-borked state of the
148-
// destPath after an evaluation failure.
149-
os.remove.all(paths.meta)
150-
0
151-
}
166+
// uncached
167+
if (!labelled.persistent) os.remove.all(paths.dest)
168+
169+
val (newResults, newEvaluated) =
170+
executeGroup(
171+
group = group,
172+
results = results,
173+
inputsHash = inputsHash,
174+
paths = Some(paths),
175+
maybeTargetLabel = Some(terminal.toString),
176+
counterMsg = countMsg,
177+
reporter = zincProblemReporter,
178+
testReporter = testReporter,
179+
logger = logger,
180+
executionContext = executionContext,
181+
exclusive = exclusive,
182+
isCommand = labelled.isInstanceOf[Command[?]],
183+
deps = deps,
184+
offline = offline
185+
)
186+
187+
val valueHash = newResults(labelled) match {
188+
case ExecResult.Success((v, _)) =>
189+
val valueHash = getValueHash(v, terminal, inputsHash)
190+
handleTaskResult(v, valueHash, paths.meta, inputsHash, labelled)
191+
valueHash
192+
193+
case _ =>
194+
// Wipe out any cached meta.json file that exists, so
195+
// a following run won't look at the cached metadata file and
196+
// assume it's associated with the possibly-borked state of the
197+
// destPath after an evaluation failure.
198+
os.remove.all(paths.meta)
199+
0
200+
}
152201

153-
GroupExecution.Results(
154-
newResults,
155-
newEvaluated.toSeq,
156-
cached = if (labelled.isInstanceOf[InputImpl[?]]) null else false,
157-
inputsHash,
158-
cached.map(_._1).getOrElse(-1),
159-
!cached.map(_._3).contains(valueHash)
160-
)
202+
GroupExecution.Results(
203+
newResults,
204+
newEvaluated.toSeq,
205+
cached = if (labelled.isInstanceOf[InputImpl[?]]) null else false,
206+
inputsHash,
207+
cached.map(_._1).getOrElse(-1),
208+
!cached.map(_._3).contains(valueHash)
209+
)
210+
}
161211
}
162212
case task =>
163213
val (newResults, newEvaluated) = executeGroup(
@@ -328,7 +378,7 @@ private trait GroupExecution {
328378
// invalidate workers in the scenario where a worker classloader is
329379
// re-created - so the worker *class* changes - but the *value* inputs to the
330380
// worker does not change. This typically happens when the worker class is
331-
// brought in via `import $ivy`, since the class then comes from the
381+
// brought in via `//| mvnDeps:`, since the class then comes from the
332382
// non-bootstrap classloader which can be re-created when the `build.mill` file
333383
// changes.
334384
//

core/internal/package.mill

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ object `package` extends RootModule with build.MillPublishScalaModule {
1818
build.Deps.mainargs,
1919
build.Deps.upickle,
2020
build.Deps.pprint,
21-
build.Deps.fansi
21+
build.Deps.fansi,
22+
build.Deps.snakeyamlEngine
2223
)
2324
}

0 commit comments

Comments
 (0)