Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7918f3d
Use path mapping when json-serializing PathRefs
lefou Oct 23, 2025
cb82912
Use path-roots mapping for path serialization
lefou Oct 23, 2025
00c0249
Hack: explicitly set outPath before executing anything
lefou Oct 24, 2025
8e80cbc
Set oupath when writing `mill-runner-state.json`
lefou Oct 24, 2025
2ac5708
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 24, 2025
d79a99b
Fix outpath propagation to test runner
lefou Oct 24, 2025
75377c2
fix expected show output
lefou Oct 24, 2025
6c6c405
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 24, 2025
ff26b24
Fix deprecation
lefou Oct 25, 2025
a420435
Name tuple items
lefou Oct 25, 2025
83e217a
Don't use current evaluator to get the current output path
lefou Oct 25, 2025
304cd99
Stabilize PathRef hashcode by using the encoded path
lefou Oct 25, 2025
c4e5221
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 25, 2025
8d8a2b0
Fix tests
lefou Oct 25, 2025
db55495
Refactor outDir init and propagation
lefou Oct 26, 2025
c735f56
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 26, 2025
c3ab1d6
Replace expected literals in itests
lefou Oct 26, 2025
b338d3a
Set outPath in UnitTester
lefou Oct 26, 2025
ddebbbd
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 26, 2025
0d4a916
Change design: root path mapping is now dynamic
lefou Oct 27, 2025
ba63983
Addd more checks
lefou Oct 27, 2025
082ae2e
Fix defaults
lefou Oct 27, 2025
cb672de
Cleanup
lefou Oct 27, 2025
965b5f2
Always use current mapping when serializing to json
lefou Oct 27, 2025
191bdb0
Ensure, we don't map any root paths in RPC communication
lefou Oct 27, 2025
2b08564
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 27, 2025
64ee917
Don't use placeholders in test runner testargs files
lefou Oct 27, 2025
98feb64
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 27, 2025
75656d1
Made test condition on Java version
lefou Oct 27, 2025
adb6cff
Readd pathref config in GroupExecution
lefou Oct 27, 2025
b944cbe
Fixed test expectation
lefou Oct 27, 2025
a56745e
Revert more eager expected test output changes
lefou Oct 27, 2025
356fa98
Ajust reading of json files
lefou Oct 27, 2025
4d65636
Fix tests
lefou Oct 27, 2025
82b8676
Fix test
lefou Oct 27, 2025
006615c
Revert accidental code paste
lefou Oct 28, 2025
ebc0e19
cleanup
lefou Oct 28, 2025
cc5d81d
cleanup
lefou Oct 28, 2025
3c81644
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 28, 2025
03f22c6
Moved new API to `MappedRoots` object
lefou Oct 28, 2025
9cd6301
Merge branch 'main' into tr-path-mapping-optional
lefou Oct 28, 2025
21ede11
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 28, 2025
9af1b09
Renamings
lefou Oct 28, 2025
289595e
Ensure `MappedRoots.withMillDefaults` is used with named parameters
lefou Oct 28, 2025
3d2501f
cleanup
lefou Oct 28, 2025
6914aca
Merge branch 'main' into tr-path-mapping-optional
lefou Oct 28, 2025
075c058
Merge branch 'main' into tr-path-mapping-optional
lefou Oct 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions core/api/src/mill/api/JsonFormatters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ trait JsonFormatters {

implicit val pathReadWrite: RW[os.Path] = upickle.readwriter[String]
.bimap[os.Path](
_.toString,
os.Path(_)
p => MappedRoots.encodeKnownRootsInPath(p),
s => os.Path(MappedRoots.decodeKnownRootsInPath(s))
)

implicit val relPathRW: RW[os.RelPath] = upickle.readwriter[String]
Expand Down
81 changes: 81 additions & 0 deletions core/api/src/mill/api/MappedRoots.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package mill.api

import mill.api.internal.NamedParameterOnlyDummy
import mill.constants.PathVars

import scala.annotation.unused
import scala.util.DynamicVariable

type MappedRoots = Seq[(key: String, path: os.Path)]

object MappedRoots extends MappedRootsImpl

trait MappedRootsImpl {

private val rootMapping: DynamicVariable[MappedRoots] = DynamicVariable(Seq())

def get: MappedRoots = rootMapping.value

def toMap: Map[String, os.Path] = get.map(m => (m.key, m.path)).toMap

def withMillDefaults[T](
@unused t: NamedParameterOnlyDummy = new NamedParameterOnlyDummy,
outPath: os.Path,
workspacePath: os.Path = BuildCtx.workspaceRoot,
homePath: os.Path = os.home
)(thunk: => T): T = withMapping(
Seq(
("MILL_OUT", outPath),
("WORKSPACE", workspacePath),
// TODO: add coursier here
("HOME", homePath)
)
)(thunk)

def withMapping[T](mapping: MappedRoots)(thunk: => T): T = withMapping(_ => mapping)(thunk)

def withMapping[T](mapping: MappedRoots => MappedRoots)(thunk: => T): T = {
val newMapping = mapping(rootMapping.value)
var seenKeys = Set[String]()
var seenPaths = Set[os.Path]()
newMapping.foreach { case m =>
require(!m.key.startsWith("$"), "Key must not start with a `$`.")
require(m.key != PathVars.ROOT, s"Invalid key, '${PathVars.ROOT}' is a reserved key name.")
require(
!seenKeys.contains(m.key),
s"Key must be unique, but '${m.key}' was given multiple times."
)
require(
!seenPaths.contains(m.path),
s"Paths must be unique, but '${m.path}' was given multiple times."
)
seenKeys += m.key
seenPaths += m.path
}
rootMapping.withValue(newMapping)(thunk)
}

def encodeKnownRootsInPath(p: os.Path): String = {
MappedRoots.get.collectFirst {
case rep if p.startsWith(rep.path) =>
s"$$${rep.key}${
if (p != rep.path) {
s"/${p.subRelativeTo(rep.path).toString()}"
} else ""
}"
}.getOrElse(p.toString)
}

def decodeKnownRootsInPath(encoded: String): String = {
if (encoded.startsWith("$")) {
val offset = 1 // "$".length
MappedRoots.get.collectFirst {
case mapping if encoded.startsWith(mapping.key, offset) =>
s"${mapping.path.toString}${encoded.substring(mapping.key.length + offset)}"
}.getOrElse(encoded)
} else {
encoded
}
}

}
48 changes: 39 additions & 9 deletions core/api/src/mill/api/PathRef.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import java.util.concurrent.ConcurrentHashMap
import scala.annotation.nowarn
import scala.language.implicitConversions
import scala.util.DynamicVariable
import scala.util.hashing.MurmurHash3

/**
* A wrapper around `os.Path` that calculates it's hashcode based
* on the contents of the filesystem underneath it. Used to ensure filesystem
* changes can bust caches which are keyed off hashcodes.
* A wrapper around `os.Path` that calculates a `sig` (which ends up in the [[hashCode]])
* based on the contents of the filesystem underneath it.
* Used to ensure filesystem changes can bust caches which are keyed off hashcodes.
*/
case class PathRef private[mill] (
path: os.Path,
Expand All @@ -24,6 +25,18 @@ case class PathRef private[mill] (
) extends PathRefApi {
private[mill] def javaPath = path.toNIO

/**
* The path with common mapped path roots replaced, to make it relocatable.
* See [[MappedRoots]].
*/
private val mappedPath: String = MappedRoots.encodeKnownRootsInPath(path)

/**
* Apply the current contextual path mapping to this PathRef.
* Updates [[mappedPath]] but does not recalculate the [[sig]].
*/
def remap: PathRef = PathRef(path, quick, sig, revalidate)

def recomputeSig(): Int = PathRef.apply(path, quick).sig
def validate(): Boolean = recomputeSig() == sig

Expand All @@ -38,16 +51,33 @@ case class PathRef private[mill] (
def withRevalidate(revalidate: PathRef.Revalidate): PathRef = copy(revalidate = revalidate)
def withRevalidateOnce: PathRef = copy(revalidate = PathRef.Revalidate.Once)

override def toString: String = {
private def toStringPrefix: String = {
val quick = if (this.quick) "qref:" else "ref:"

val valid = revalidate match {
case PathRef.Revalidate.Never => "v0:"
case PathRef.Revalidate.Once => "v1:"
case PathRef.Revalidate.Always => "vn:"
}
val sig = String.format("%08x", this.sig: Integer)
quick + valid + sig + ":" + path.toString()
s"${quick}${valid}${sig}:"
}

override def toString: String = {
toStringPrefix + path.toString()
}

// Instead of using `path` we need to use `mappedPath` to make the hashcode stable as cache key
override def hashCode(): Int = {
var h = MurmurHash3.productSeed
h = MurmurHash3.mix(h, "PathRef".hashCode)
h = MurmurHash3.mix(h, mappedPath.hashCode)
h = MurmurHash3.mix(h, quick.##)
h = MurmurHash3.mix(h, sig.##)
h = MurmurHash3.mix(h, revalidate.##)
MurmurHash3.finalizeHash(h, 4)
}

}

object PathRef {
Expand Down Expand Up @@ -195,12 +225,12 @@ object PathRef {
implicit def jsonFormatter: RW[PathRef] = upickle.readwriter[String].bimap[PathRef](
p => {
storeSerializedPaths(p)
p.toString()
p.toStringPrefix + MappedRoots.encodeKnownRootsInPath(p.path)
},
{
case s"$prefix:$valid0:$hex:$pathString" if prefix == "ref" || prefix == "qref" =>
case s"$prefix:$valid0:$hex:$pathVal" if prefix == "ref" || prefix == "qref" =>

val path = os.Path(pathString)
val path = os.Path(MappedRoots.decodeKnownRootsInPath(pathVal))
val quick = prefix match {
case "qref" => true
case "ref" => false
Expand All @@ -219,7 +249,7 @@ object PathRef {
pr
case s =>
mill.api.BuildCtx.withFilesystemCheckerDisabled(
PathRef(os.Path(s, currentOverrideModulePath.value))
PathRef(os.Path(MappedRoots.decodeKnownRootsInPath(s), currentOverrideModulePath.value))
)
}
)
Expand Down
48 changes: 48 additions & 0 deletions core/api/test/src/mill/api/MappedRootsTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package mill.api

import utest.*

import java.nio.file.Files
import mill.api.{MappedRoots => MR}

object MappedRootsTests extends TestSuite {
val tests: Tests = Tests {
test("encode") {
withTmpDir { tmpDir =>
val workspaceDir = tmpDir / "workspace"
val outDir = workspaceDir / "out"
MR.withMillDefaults(outPath = outDir, workspacePath = workspaceDir) {

def check(path: os.Path, encContains: Seq[String], containsNot: Seq[String]) = {
val enc = MR.encodeKnownRootsInPath(path)
val dec = MR.decodeKnownRootsInPath(enc)
assert(path.toString == dec)
encContains.foreach(s => assert(enc.containsSlice(s)))
containsNot.foreach(s => assert(!enc.containsSlice(s)))

path -> enc
}

val file1 = tmpDir / "file1"
val file2 = workspaceDir / "file2"
val file3 = outDir / "file3"

Seq(
"mapping" -> MR.get,
check(file1, Seq(file1.toString), Seq("$WORKSPACE", "$MILL_OUT")),
check(file2, Seq("$WORKSPACE/file2"), Seq("$MILL_OUT")),
check(file3, Seq("$MILL_OUT/file3"), Seq("$WORKSPACE"))
)
}
}
}
}

private def withTmpDir[T](body: os.Path => T): T = {
val tmpDir = os.Path(Files.createTempDirectory(""))
val res = body(tmpDir)
os.remove.all(tmpDir)
res
}

}
12 changes: 10 additions & 2 deletions core/api/test/src/mill/api/PathRefTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ object PathRefTests extends TestSuite {
val sig2 = PathRef(file, quick).sig
assert(sig1 != sig2)
}

test("qref") - check(quick = true)
test("ref") - check(quick = false)
}
Expand All @@ -43,12 +44,18 @@ object PathRefTests extends TestSuite {
val file = tmpDir / "foo.txt"
val content = "hello"
os.write.over(file, content)
Files.setPosixFilePermissions(file.wrapped, PosixFilePermissions.fromString("rw-rw----"))
Files.setPosixFilePermissions(
file.wrapped,
PosixFilePermissions.fromString("rw-rw----")
)
val rwSig = PathRef(file, quick).sig
val rwSigb = PathRef(file, quick).sig
assert(rwSig == rwSigb)

Files.setPosixFilePermissions(file.wrapped, PosixFilePermissions.fromString("rwxrw----"))
Files.setPosixFilePermissions(
file.wrapped,
PosixFilePermissions.fromString("rwxrw----")
)
val rwxSig = PathRef(file, quick).sig

assert(rwSig != rwxSig)
Expand Down Expand Up @@ -76,6 +83,7 @@ object PathRefTests extends TestSuite {
val sig2 = PathRef(tmpDir, quick).sig
assert(sig1 == sig2)
}

test("qref") - check(quick = true)
test("ref") - check(quick = false)
}
Expand Down
22 changes: 22 additions & 0 deletions core/constants/src/mill/constants/PathVars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package mill.constants;

/**
* Central place containing all the path variables that Mill uses in <code>PathRef</code> or <code>os.Path</code>.
*/
public interface PathVars {

/**
* Output directory where Mill workers' state and Mill tasks output should be
* written to
*/
String MILL_OUT = "MILL_OUT";

/**
* The Mill project workspace root directory.
*/
String WORKSPACE = "WORKSPACE";

String HOME = "HOME";

String ROOT = "ROOT";
}
10 changes: 5 additions & 5 deletions core/exec/src/mill/exec/GroupExecution.scala
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ trait GroupExecution {
executionContext: mill.api.TaskCtx.Fork.Api,
exclusive: Boolean,
upstreamPathRefs: Seq[PathRef]
): GroupExecution.Results = {
): GroupExecution.Results = MappedRoots.withMillDefaults(outPath = outPath) {

val inputsHash = {
val externalInputsHash = MurmurHash3.orderedHash(
Expand Down Expand Up @@ -238,8 +238,8 @@ trait GroupExecution {
newEvaluated = newEvaluated.toSeq,
cached = if (labelled.isInstanceOf[Task.Input[?]]) null else false,
inputsHash = inputsHash,
previousInputsHash = cached.map(_._1).getOrElse(-1),
valueHashChanged = !cached.map(_._3).contains(valueHash),
previousInputsHash = cached.map(_.inputHash).getOrElse(-1),
valueHashChanged = !cached.map(_.valueHash).contains(valueHash),
serializedPaths = serializedPaths
)
}
Expand Down Expand Up @@ -448,7 +448,7 @@ trait GroupExecution {
inputsHash: Int,
labelled: Task.Named[?],
paths: ExecutionPaths
): Option[(Int, Option[(Val, Seq[PathRef])], Int)] = {
): Option[(inputHash: Int, valOpt: Option[(Val, Seq[PathRef])], valueHash: Int)] = {
for {
cached <-
try Some(upickle.read[Cached](paths.meta.toIO, trace = false))
Expand Down Expand Up @@ -613,7 +613,7 @@ object GroupExecution {
classLoader: ClassLoader
)(t: => T): T = {
// Tasks must be allowed to write to upstream worker's dest folders, because
// the point of workers is to manualy manage long-lived state which includes
// the point of workers is to manually manage long-lived state which includes
// state on disk.
val validWriteDests =
deps.collect { case n: Task.Worker[?] =>
Expand Down
2 changes: 1 addition & 1 deletion example/androidlib/java/1-hello-world/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ object app extends AndroidAppModule {
/** Usage

> ./mill show app.androidApk
".../out/app/androidApk.dest/app.apk"
"...$MILL_OUT/app/androidApk.dest/app.apk"

*/

Expand Down
2 changes: 1 addition & 1 deletion example/androidlib/java/2-app-bundle/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ object bundle extends AndroidAppBundle {
/** Usage

> ./mill show bundle.androidBundle
".../out/bundle/androidBundle.dest/signedBundle.aab"
"...$MILL_OUT/bundle/androidBundle.dest/signedBundle.aab"

*/

Expand Down
2 changes: 1 addition & 1 deletion example/androidlib/java/4-sum-lib-java/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ Publish ... to /home/.../.m2/repository/...
},
"payload": [
[
".../out/lib/androidAar.dest/library.aar",
"...$MILL_OUT/lib/androidAar.dest/library.aar",
"lib-0.0.1.aar"
],
...
Expand Down
2 changes: 1 addition & 1 deletion example/androidlib/java/6-native-libs/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ object app extends AndroidNativeAppModule { // <1>
/** Usage

> ./mill show app.androidApk
".../out/app/androidApk.dest/app.apk"
"...$MILL_OUT/app/androidApk.dest/app.apk"

> ./mill show app.createAndroidVirtualDevice
...Name: java-test, DeviceId: medium_phone...
Expand Down
2 changes: 1 addition & 1 deletion example/androidlib/kotlin/1-hello-kotlin/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ object app extends AndroidAppKotlinModule {
/** Usage

> ./mill show app.androidApk
".../out/app/androidApk.dest/app.apk"
"...$MILL_OUT/app/androidApk.dest/app.apk"

*/

Expand Down
2 changes: 1 addition & 1 deletion example/androidlib/kotlin/2-compose/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ object app extends AndroidAppKotlinModule {
/** Usage

> ./mill show app.androidApk
".../out/app/androidApk.dest/app.apk"
"...$MILL_OUT/app/androidApk.dest/app.apk"


> ./mill show app.createAndroidVirtualDevice
Expand Down
2 changes: 1 addition & 1 deletion example/androidlib/kotlin/4-sum-lib-kotlin/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Publish ... to /home/.../.m2/repository/...
},
"payload": [
[
".../out/lib/androidAar.dest/library.aar",
"...$MILL_OUT/lib/androidAar.dest/library.aar",
"lib-0.0.1.aar"
],
...
Expand Down
Loading
Loading