Skip to content

Commit fea671b

Browse files
Read coursier env vars and Java properties via input tasks (#5793)
This is a new attempt at fixing #5134. Turns out setting the context class loader when evaluating user code addresses the issue while not running into #5398 again. --- Description from #5350 that still applies: This makes Mill read all environment variables and Java properties used by coursier via a `Task.Input`. In more detail, this adds a `CoursierConfigModule` external module in scalalib. It contains a single `Task.Input`, that reads all environment variables used by coursier via `Task.env` and all Java properties used by coursier via `sys.props`. Helper tasks compute default values for various parameters (repositories, cache location, etc.). `CoursierConfigModule.coursierConfig` returns most of these parameters in a case class defined in Mill, `CoursierConfig`. For convenience, down-the-line, we pass `CoursierConfig` instances around. Fixes #5134 (by managing coursier env vars / Java properties via `Task.Input`)
1 parent 10677ac commit fea671b

File tree

21 files changed

+488
-102
lines changed

21 files changed

+488
-102
lines changed

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

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,8 @@ private trait GroupExecution {
320320
counterMsg,
321321
destCreator,
322322
getEvaluator().asInstanceOf[Evaluator],
323-
terminal
323+
terminal,
324+
rootModule.getClass.getClassLoader
324325
) {
325326
try {
326327
task.evaluate(args) match {
@@ -533,7 +534,8 @@ private object GroupExecution {
533534
counterMsg: String,
534535
destCreator: DestCreator,
535536
evaluator: Evaluator,
536-
terminal: Task[?]
537+
terminal: Task[?],
538+
classLoader: ClassLoader
537539
)(t: => T): T = {
538540
val isCommand = terminal.isInstanceOf[Task.Command[?]]
539541
val isInput = terminal.isInstanceOf[Task.Input[?]]
@@ -578,11 +580,18 @@ private object GroupExecution {
578580
)
579581

580582
Evaluator.withCurrentEvaluator(exposedEvaluator) {
581-
if (!exclusive) t
582-
else {
583-
logger.prompt.reportKey(Seq(counterMsg))
584-
logger.prompt.withPromptPaused {
585-
t
583+
// Ensure the class loader used to load user code
584+
// is set as context class loader when running user code.
585+
// This is useful if users rely on libraries that look
586+
// for resources added by other libraries, by using
587+
// using java.util.ServiceLoader for example.
588+
mill.api.ClassLoader.withContextClassLoader(classLoader) {
589+
if (!exclusive) t
590+
else {
591+
logger.prompt.reportKey(Seq(counterMsg))
592+
logger.prompt.withPromptPaused {
593+
t
594+
}
586595
}
587596
}
588597
}

integration/bootstrap/no-java-bootstrap/src/NoJavaBootstrapTests.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ object NoJavaBootstrapTests extends UtestIntegrationTestSuite {
1919
val cache = FileCache()
2020
val index = JvmIndex.load(
2121
cache = cache,
22-
repositories = Resolve().repositories,
22+
repositories = Resolve.defaultRepositories,
2323
indexChannel = JvmChannel.module(
2424
JvmChannel.centralModule(),
2525
version = mill.api.BuildInfo.coursierJvmIndexVersion
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package build
2+
import mill._, scalalib._
3+
import java.io.File
4+
5+
object `package` extends ScalaModule {
6+
def scalaVersion = "2.13.16"
7+
def mvnDeps = Seq(
8+
mvn"org.slf4j:slf4j-simple:2.0.9"
9+
)
10+
def printRunClasspath() = Task.Command {
11+
println(runClasspath().map(_.path.toString).mkString(File.pathSeparator))
12+
}
13+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package mill.integration
2+
3+
import coursier.cache.FileCache
4+
import mill.testkit.UtestIntegrationTestSuite
5+
import utest._
6+
7+
import java.io.File
8+
9+
object CsEnvVarsTests extends UtestIntegrationTestSuite {
10+
val tests: Tests = Tests {
11+
test("cache") - integrationTest { tester =>
12+
import tester._
13+
14+
def check(cacheOpt: Option[os.Path]): Unit = {
15+
val env = cacheOpt.toSeq.map { cache =>
16+
"COURSIER_CACHE" -> cache.toString
17+
}
18+
val res = eval(
19+
("printRunClasspath"),
20+
env = env.toMap
21+
)
22+
assert(res.exitCode == 0)
23+
24+
val cp = res.out.split(File.pathSeparator).filter(_.nonEmpty).map(os.Path(_))
25+
26+
val actualCache = cacheOpt.getOrElse(os.Path(FileCache().location))
27+
assert(cp.exists(p => p.startsWith(actualCache) && p.last.startsWith("slf4j-api-")))
28+
}
29+
30+
check(None)
31+
check(Some(workspacePath / "cache-0"))
32+
check(Some(workspacePath / "cache-1"))
33+
}
34+
35+
test("mirrors") - integrationTest { tester =>
36+
import tester._
37+
38+
def check(mirrorFileOpt: Option[os.Path]): Unit = {
39+
val env = mirrorFileOpt.toSeq.map { file =>
40+
"COURSIER_MIRRORS" -> file.toString
41+
}
42+
val res = eval(
43+
("printRunClasspath"),
44+
env = env.toMap
45+
)
46+
assert(res.exitCode == 0)
47+
48+
val cacheRoot = os.Path(FileCache().location)
49+
val cp = res.out.split(File.pathSeparator)
50+
.filter(_.nonEmpty)
51+
.map(os.Path(_))
52+
.filter(_.startsWith(cacheRoot))
53+
.map(_.relativeTo(cacheRoot).asSubPath)
54+
55+
val centralReplaced = cp.forall { f =>
56+
f.startsWith(os.sub / "https/repo.maven.apache.org/maven2")
57+
}
58+
assert(cp.exists(f => f.last.startsWith("scala-library-") && f.last.endsWith(".jar")))
59+
assert(mirrorFileOpt.nonEmpty == centralReplaced)
60+
}
61+
62+
check(None)
63+
64+
val mirrorFile = workspacePath / "mirror.properties"
65+
os.write(
66+
mirrorFile,
67+
"""central.from=https://repo1.maven.org/maven2
68+
|central.to=https://repo.maven.apache.org/maven2/
69+
|""".stripMargin
70+
)
71+
check(Some(mirrorFile))
72+
}
73+
}
74+
}

integration/ide/bsp-server/resources/snapshots/logging

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
[1-buildInitialize] Entered buildInitialize
44
[1-buildInitialize] Got client semanticdbVersion: * Enabling SemanticDB support.
55
[1-buildInitialize] buildInitialize took * msec
6-
[bsp-init-mill-build/build.mill-58] [info] compiling * Scala sources to * ...
7-
[bsp-init-mill-build/build.mill-58] [info] done compiling
8-
[bsp-init-build.mill-58] [info] compiling * Scala sources to * ...
9-
[bsp-init-build.mill-58] [info] done compiling
6+
[bsp-init-mill-build/build.mill-59] [info] compiling * Scala sources to * ...
7+
[bsp-init-mill-build/build.mill-59] [info] done compiling
8+
[bsp-init-build.mill-59] [info] compiling * Scala sources to * ...
9+
[bsp-init-build.mill-59] [info] done compiling
1010
[bsp-init] SNAPSHOT
1111
[2-workspaceBuildTargets] Entered workspaceBuildTargets
1212
[2-workspaceBuildTargets] Evaluating * tasks
@@ -31,12 +31,12 @@
3131
[4-compile] buildTargetCompile took * msec
3232
[5-compile] Entered buildTargetCompile
3333
[5-compile] Evaluating 1 task
34-
[5-compile-54] [info] compiling * Scala source to * ...
35-
[5-compile-54] [error] *:2:3: not found: value nope
36-
[5-compile-54] [error] nope
37-
[5-compile-54] [error] ^
38-
[5-compile-54] [error] one error found
39-
[5-compile-54] errored.compilation-error.semanticDbDataDetailed failed
34+
[5-compile-55] [info] compiling * Scala source to * ...
35+
[5-compile-55] [error] *:2:3: not found: value nope
36+
[5-compile-55] [error] nope
37+
[5-compile-55] [error] ^
38+
[5-compile-55] [error] one error found
39+
[5-compile-55] errored.compilation-error.semanticDbDataDetailed failed
4040
[5-compile] Done
4141
[5-compile] buildTargetCompile took * msec
4242
[6-compile] Entered buildTargetCompile

libs/init/gradle/src/mill/main/gradle/GradleBuildGenMain.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import mill.api.daemon.internal.internal
55
import mill.main.buildgen.*
66
import mill.main.buildgen.BuildGenUtil.*
77
import mill.main.gradle.JavaModel.{Dep, ExternalDep}
8-
import mill.util.Jvm
8+
import mill.util.{CoursierConfig, Jvm}
99
import org.gradle.api.plugins.JavaPlugin
1010
import org.gradle.tooling.GradleConnector
1111
import os.Path
@@ -59,7 +59,10 @@ object GradleBuildGenMain extends BuildGenBase.MavenAndGradle[ProjectModel, Dep]
5959
val args =
6060
cfg.shared.basicConfig.jvmId.map { id =>
6161
println(s"resolving Java home for jvmId $id")
62-
val home = Jvm.resolveJavaHome(id).get
62+
val home = Jvm.resolveJavaHome(
63+
id,
64+
config = CoursierConfig.default()
65+
).get
6366
s"-Dorg.gradle.java.home=$home"
6467
} ++ Seq("--init-script", writeGradleInitScript.toString())
6568

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package mill.javalib
2+
3+
import coursier.CoursierEnv
4+
import coursier.cache.{CacheEnv, CachePolicy}
5+
import coursier.core.Repository
6+
import coursier.credentials.Credentials
7+
import coursier.params.Mirror
8+
import coursier.util.{EnvEntry, EnvValues}
9+
import mill.api.*
10+
import mill.{T, Task}
11+
12+
import scala.concurrent.duration.Duration
13+
14+
/**
15+
* Reads coursier environment variables and Java properties
16+
*
17+
* This module reads environment variables and Java properties, and uses
18+
* them to get default values for a number of coursier parameters, such as:
19+
*
20+
* * the coursier cache location, read from `COURSIER_CACHE` in the environment
21+
* and `coursier.cache` in Java properties. It is recommended to use an
22+
* absolute path rather than a relative one.
23+
*
24+
* * the coursier archive cache location, read from `COURSIER_ARCHIVE_CACHE` in
25+
* the environment and in `coursier.archive.cache` in Java properties.
26+
*
27+
* * credentials, read from `COURSIER_CREDENTIALS` in the environment, and
28+
* `coursier.credentials` in Java properties. Its format is documented
29+
* here: https://github.com/coursier/coursier/blob/701e93664855709db38e443cb7a19e36ddc49b78/doc/docs/other-credentials.md
30+
*
31+
* * TTL for snapshot artifacts / version listings / etc., read from
32+
* `COURSIER_TTL` in the environment and `coursier.ttl` in Java properties.
33+
* This value is parsed with `scala.concurrent.duration.Duration`, and accepts
34+
* formats like "5 hours", "1 day", etc. or just "0".
35+
*
36+
* * default repositories, read from `COURSIER_REPOSITORIES` in the environment
37+
* and `coursier.repositories` in Java properties. It can be set to values like
38+
* "ivy2Local|central|https://maven.google.com". Its format is documented here:
39+
* https://github.com/coursier/coursier/blob/701e93664855709db38e443cb7a19e36ddc49b78/doc/docs/other-repositories.md
40+
*
41+
* * mirror repository files, read from `COURSIER_MIRRORS` and `coursier.mirrors`,
42+
* whose file they point to should contains things like
43+
*
44+
* google.from=https://repo1.maven.org/maven2
45+
* google.to=https://maven.google.com
46+
*
47+
* Environment variables always have precedence over Java properties.
48+
*/
49+
object CoursierConfigModule extends ExternalModule with CoursierConfigModule {
50+
lazy val millDiscover = Discover[this.type]
51+
}
52+
53+
trait CoursierConfigModule extends Module {
54+
55+
// All environment / Java properties "entries" that we're going to read.
56+
// These are read all at once via coursierEnv.
57+
private val entries = Seq(
58+
CacheEnv.cache,
59+
CacheEnv.credentials,
60+
CacheEnv.ttl,
61+
CacheEnv.cachePolicy,
62+
CacheEnv.archiveCache,
63+
CoursierEnv.mirrors,
64+
CoursierEnv.mirrorsExtra,
65+
CoursierEnv.repositories,
66+
CoursierEnv.scalaCliConfig,
67+
CoursierEnv.configDir
68+
)
69+
70+
private val envVars = entries.map(_.envName).toSet
71+
private val propNames = entries.map(_.propName).toSet
72+
73+
extension (entry: EnvEntry)
74+
private def readFrom(env: Map[String, String], props: Map[String, String]): EnvValues =
75+
EnvValues(env.get(entry.envName), props.get(entry.propName))
76+
77+
/**
78+
* Environment variables and Java properties, and their values, used by coursier
79+
*/
80+
def coursierEnv: T[(env: Map[String, String], props: Map[String, String])] = Task.Input {
81+
val env = Task.env.filterKeys(envVars).toMap
82+
val props =
83+
propNames.iterator.flatMap(name => sys.props.get(name).iterator.map(name -> _)).toMap
84+
(env, props)
85+
}
86+
87+
/** Default repositories for dependency resolution */
88+
def defaultRepositories: Task[Seq[Repository]] = Task.Anon {
89+
val (env, props) = coursierEnv()
90+
CoursierEnv.defaultRepositories(
91+
CoursierEnv.repositories.readFrom(env, props),
92+
CoursierEnv.scalaCliConfig.readFrom(env, props)
93+
)
94+
}
95+
96+
/** Default JSON configuration files to be used by coursier */
97+
def defaultConfFiles: Task[Seq[PathRef]] = Task.Anon {
98+
val (env, props) = coursierEnv()
99+
CacheEnv.defaultConfFiles(CacheEnv.scalaCliConfig.readFrom(env, props))
100+
.map(os.Path(_, BuildCtx.workspaceRoot))
101+
.map(PathRef(_))
102+
}
103+
104+
/** Default mirrors to be used by coursier */
105+
def defaultMirrors: Task[Seq[Mirror]] = Task.Anon {
106+
val (env, props) = coursierEnv()
107+
CoursierEnv.defaultMirrors(
108+
CoursierEnv.mirrors.readFrom(env, props),
109+
CoursierEnv.mirrorsExtra.readFrom(env, props),
110+
CoursierEnv.scalaCliConfig.readFrom(env, props),
111+
CoursierEnv.configDir.readFrom(env, props)
112+
)
113+
}
114+
115+
/** Default cache location to be used by coursier */
116+
def defaultCacheLocation: Task[String] = Task.Anon {
117+
val (env, props) = coursierEnv()
118+
val path: java.nio.file.Path = CacheEnv.defaultCacheLocation(
119+
CacheEnv.cache.readFrom(env, props)
120+
)
121+
path.toString
122+
}
123+
124+
/**
125+
* Default archive cache location to be used by coursier
126+
*
127+
* This is the directory under which archives are unpacked
128+
* by `coursier.cache.ArchiveCache`. This includes JVMs
129+
* managed by coursier, which Mill relies on by default.
130+
*/
131+
def defaultArchiveCacheLocation: Task[String] = Task.Anon {
132+
val (env, props) = coursierEnv()
133+
val path: java.nio.file.Path = CacheEnv.defaultArchiveCacheLocation(
134+
CacheEnv.archiveCache.readFrom(env, props)
135+
)
136+
path.toString
137+
}
138+
139+
/** Default repository credentials to be used by coursier */
140+
def defaultCredentials: Task[Seq[Credentials]] = Task.Anon {
141+
val (env, props) = coursierEnv()
142+
CacheEnv.defaultCredentials(
143+
CacheEnv.credentials.readFrom(env, props),
144+
CacheEnv.scalaCliConfig.readFrom(env, props),
145+
CacheEnv.configDir.readFrom(env, props)
146+
)
147+
}
148+
149+
/** Default TTL used by coursier when downloading changing artifacts such as snapshots */
150+
def defaultTtl: Task[Option[Duration]] = Task.Anon {
151+
val (env, props) = coursierEnv()
152+
CacheEnv.defaultTtl(
153+
CacheEnv.ttl.readFrom(env, props)
154+
)
155+
}
156+
157+
/** Default cache policies used by coursier when downloading artifacts */
158+
def defaultCachePolicies: Task[Seq[CachePolicy]] = Task.Anon {
159+
val (env, props) = coursierEnv()
160+
CacheEnv.defaultCachePolicies(
161+
CacheEnv.cachePolicy.readFrom(env, props)
162+
)
163+
}
164+
165+
/**
166+
* Aggregates coursier parameters read from the environment
167+
*/
168+
def coursierConfig: Task[mill.util.CoursierConfig] = Task.Anon {
169+
mill.util.CoursierConfig(
170+
defaultRepositories(),
171+
defaultConfFiles().map(_.path),
172+
defaultMirrors(),
173+
defaultCacheLocation(),
174+
defaultArchiveCacheLocation(),
175+
defaultCredentials(),
176+
defaultTtl(),
177+
defaultCachePolicies()
178+
)
179+
}
180+
}

0 commit comments

Comments
 (0)