Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 11 additions & 5 deletions core/internal/cli/src/mill/internal/MillCliConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ case class MillCliConfig(
@arg(name = "version", short = 'v', doc = "Show mill version information and exit.")
showVersion: Flag = Flag(),
@arg(
hidden = true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that?

name = "bell",
short = 'b',
doc = "Ring the bell once if the run completes successfully, twice if it fails."
Expand All @@ -21,7 +22,7 @@ case class MillCliConfig(
tasks and where each log line came from"""
)
ticker: Option[Boolean] = None,
@arg(name = "debug", short = 'd', doc = "Show debug output on STDOUT")
@arg(hidden = true, name = "debug", short = 'd', doc = "Show debug output on STDOUT")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that hidden now? This option is useful. Why do we hide it from the user.

debugLog: Flag = Flag(),
@arg(
short = 'k',
Expand Down Expand Up @@ -64,6 +65,7 @@ case class MillCliConfig(
@arg(short = 'w', doc = "Watch and re-run the given tasks when when their inputs change.")
watch: Flag = Flag(),
@arg(
hidden = true,
name = "notify-watch",
doc = "Use filesystem based file watching instead of polling based one (defaults to true)."
)
Expand All @@ -72,7 +74,7 @@ case class MillCliConfig(
leftoverArgs: Leftover[String] = Leftover(),
@arg(doc =
"""Toggle colored output; by default enabled only if the console is interactive
or FORCE_COLOR environment variable is set, and NO_COLOR environment variable is not set"""
or FORCE_COLOR environment variable is set, and NO_COLOR is not set"""
)
color: Option[Boolean] = None,
@arg(
Expand All @@ -84,7 +86,7 @@ case class MillCliConfig(
metaLevel: Option[Int] = None,

// ==================== ADVANCED CLI FLAGS ====================
@arg(doc = "Allows command args to be passed positionally without `--arg` by default")
@arg(hidden = true, doc = "Allows command args to be passed positionally without `--arg` by default")
allowPositional: Flag = Flag(),
@arg(
hidden = true,
Expand Down Expand Up @@ -113,6 +115,7 @@ case class MillCliConfig(
)
noWaitForBuildLock: Flag = Flag(),
@arg(
hidden = true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make this visible again.

doc = """
Try to work offline.
This tells modules that support it to work offline and avoid any access to the internet.
Expand All @@ -122,6 +125,7 @@ case class MillCliConfig(
)
offline: Flag = Flag(),
@arg(
hidden = true,
doc = """
Globally disables the checks that prevent you from reading and writing to disallowed
files or folders during evaluation. Useful as an escape hatch in case you desperately
Expand All @@ -139,6 +143,7 @@ case class MillCliConfig(
)
useFileLocks: Flag = Flag(),
@arg(
hidden = true,
doc = """Runs Mill in tab-completion mode"""
)
tabComplete: Flag = Flag(),
Expand All @@ -163,8 +168,7 @@ case class MillCliConfig(
disableTicker: Flag,
@arg(
doc = """Open a JShell REPL with the classpath of the meta-level 1 build module (mill-build/).
This is useful for interactively testing and debugging your build logic.
Implies options `--meta-level 1` and `--no-server`."""
This is useful for interactively testing and debugging your build logic."""
)
jshell: Flag = Flag()
) {
Expand Down Expand Up @@ -203,6 +207,8 @@ Task cheat sheet:
mill foo.test + bar.test # run tests in the `foo` module and `bar` module
mill '{foo,bar,qux}.test' # run tests in the `foo` module, `bar` module, and `qux` module

mill foo.resolvedMvnSources # resolve `foo`'s third-party dependencies' source code for browsing

mill foo.assembly # generate an executable assembly of the module `foo`
mill show foo.assembly # print the output path of the assembly of module `foo`
mill inspect foo.assembly # show docs and metadata for the `assembly` task on module `foo`
Expand Down
50 changes: 31 additions & 19 deletions libs/javalib/src/mill/javalib/JavaModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,12 @@ import coursier.params.ResolutionParams
import coursier.parse.{JavaOrScalaModule, ModuleParser}
import coursier.util.{EitherT, ModuleMatcher, Monad}
import mainargs.Flag
import mill.api.{MillException, Result}
import mill.api.{BuildCtx, DefaultTaskModule, MillException, ModuleRef, PathRef, Result, Segment, Task, TaskCtx}
import mill.api.daemon.internal.{EvaluatorApi, JavaModuleApi, internal}
import mill.api.daemon.internal.bsp.{
BspBuildTarget,
BspJavaModuleApi,
BspModuleApi,
BspUri,
JvmBuildTarget
}
import mill.api.daemon.internal.bsp.{BspBuildTarget, BspJavaModuleApi, BspModuleApi, BspUri, JvmBuildTarget}
import mill.api.daemon.internal.eclipse.GenEclipseInternalApi
import mill.javalib.*
import mill.api.daemon.internal.idea.GenIdeaInternalApi
import mill.api.{DefaultTaskModule, ModuleRef, PathRef, Segment, Task, TaskCtx}
import mill.javalib.api.CompilationResult
import mill.javalib.api.internal.{JavaCompilerOptions, ZincOp}
import mill.javalib.bsp.{BspJavaModule, BspModule}
Expand Down Expand Up @@ -1058,15 +1051,7 @@ trait JavaModule
compileResources() ++ unmanagedClasspath()
}

/**
* Resolved dependencies
*/
def resolvedMvnDeps: T[Seq[PathRef]] = Task {
if (resolvedDepsWarnNonPlatform()) {
Dep.validatePlatformDeps(platformSuffix(), mvnDeps()).pipe(warn =>
if (warn.nonEmpty) Task.log.warn(warn.mkString("\n"))
)
}
private def resolvedMvnDeps0(sources: Boolean) = Task.Anon {
millResolver().classpath(
Seq(
BoundDep(
Expand All @@ -1075,7 +1060,8 @@ trait JavaModule
),
BoundDep(coursierDependencyTask(), force = false)
),
artifactTypes = Some(artifactTypes()),
sources = sources,
artifactTypes = if (sources) None else Some(artifactTypes()),
resolutionParamsMapOpt =
Some { params =>
params
Expand All @@ -1091,6 +1077,32 @@ trait JavaModule
)
}

/**
* Resolved dependencies
*/
def resolvedMvnDeps: T[Seq[PathRef]] = Task {
if (resolvedDepsWarnNonPlatform()) {
Dep.validatePlatformDeps(platformSuffix(), mvnDeps()).pipe(warn =>
if (warn.nonEmpty) Task.log.warn(warn.mkString("\n"))
)
}
resolvedMvnDeps0(sources = false)()
}

/**
* Resolved dependency sources, unpacked into a single directory. Useful to quickly
* look up the sources of the dependencies on your classpath so you can find the
* exact source code you are compiling and running against.
*/
def resolvedMvnSources: T[PathRef] = Task {
for (jar <- resolvedMvnDeps0(sources = true)()) {
val jarName = jar.path.last.stripSuffix(".jar")
os.unzip(jar.path, Task.dest / jarName)
}
println(s"Unpacked sources of transitive third-party dependencies into ${Task.dest.relativeTo(BuildCtx.workspaceRoot)} for browsing")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this a Task.log.debug?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to make it a println because this is probably going to be used manually, so having it print out the message is useful and shouldn't be hidden behind --debug

PathRef(Task.dest)
}

override def upstreamIvyAssemblyClasspath: T[Seq[PathRef]] = Task {
resolvedRunMvnDeps()
}
Expand Down
59 changes: 59 additions & 0 deletions libs/javalib/test/src/mill/javalib/ResolveDepsTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ object ResolveDepsTests extends TestSuite {
}
}

object sources extends JavaModule {
def mvnDeps = Seq(
mvn"com.lihaoyi:geny_2.13:1.0.0"
)
}

lazy val millDiscover = Discover[this.type]
}

Expand Down Expand Up @@ -306,5 +312,58 @@ object ResolveDepsTests extends TestSuite {
assert(dependsOnForcedAndNonForcedClassPathFileNames == expectedClassPathFileNames("4.6.1"))
}
}

test("resolvedMvnSources") {
UnitTester(TestCase, null).scoped { eval =>
// First check that regular resolvedMvnDeps works
val Right(depsResult) = eval(TestCase.sources.resolvedMvnDeps): @unchecked
val deps = depsResult.value
assert(deps.nonEmpty)
assert(deps.exists(_.path.last.contains("geny")))

// Now check resolvedMvnSources
val Right(result) = eval(TestCase.sources.resolvedMvnSources): @unchecked
val sourcesDir = result.value.path

// Check that the sources directory exists and contains unpacked source files
assert(os.exists(sourcesDir))
assert(os.isDir(sourcesDir))

// Find the jar directory names for geny and scala-library
val jarDirs = os.list(sourcesDir).filter(os.isDir).map(_.last)
val genyJar = jarDirs.find(_.contains("geny")).get
val scalaLibJar = jarDirs.find(_.contains("scala-library")).get

// Check that source files are unpacked
val allFiles = os
.walk(sourcesDir)
.filter(os.isFile)
.map(_.relativeTo(sourcesDir).toString)
.sorted

val expected = Set(
s"$genyJar/geny/ByteData.scala",
s"$genyJar/geny/Bytes.scala",
s"$genyJar/geny/Generator.scala",
s"$genyJar/geny/Internal.scala",
s"$genyJar/geny/Writable.scala",
s"$scalaLibJar/rootdoc.txt",
s"$scalaLibJar/scala/AnyVal.scala",
s"$scalaLibJar/scala/AnyValCompanion.scala",
s"$scalaLibJar/scala/App.scala",
s"$scalaLibJar/scala/Array.scala",
s"$scalaLibJar/scala/Boolean.scala",
s"$scalaLibJar/scala/Byte.scala",
s"$scalaLibJar/scala/Char.scala",
s"$scalaLibJar/scala/Console.scala",
s"$scalaLibJar/scala/DelayedInit.scala",
s"$scalaLibJar/scala/Double.scala",
s"$scalaLibJar/scala/DummyImplicit.scala",
s"$scalaLibJar/scala/Dynamic.scala"
)

assert(expected.subsetOf(allFiles.toSet))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -361,15 +361,15 @@ object TabCompleteTests extends TestSuite {
"--interactive Run Mill in interactive mode, suitable for opening REPLs and taking user input. Identical to --no-daemon. Must be the first argument.",
"--no-build-lock Evaluate tasks / commands without acquiring an exclusive lock on the Mill output directory",
"--tab-complete Runs Mill in tab-completion mode",
"--color <bool> Toggle colored output; by default enabled only if the console is interactive or FORCE_COLOR environment variable is set, and NO_COLOR environment variable is not set",
"--no-daemon Run without a long-lived background daemon. Must be the first argument.",
"--ticker <bool> Enable or disable the ticker log, which provides information on running tasks and where each log line came from",
"--allow-positional Allows command args to be passed positionally without `--arg` by default",
"--watch Watch and re-run the given tasks when when their inputs change.",
"--no-wait-for-build-lock Do not wait for an exclusive lock on the Mill output directory to evaluate tasks / commands.",
"--bsp Enable BSP server mode. Typically used by a BSP client when starting the Mill BSP server.",
"--help-advanced Print a internal or advanced command flags not intended for common usage",
"--import <str> Additional ivy dependencies to load into mill, e.g. plugins.",
"--meta-level <int> Select a meta-level to run the given tasks. Level 0 is the main project in `build.mill`, level 1 the first meta-build in `mill-build/build.mill`, etc. If negative, -1 means the deepest meta-build (boostrap build), -2 the second deepest meta-build, etc.",
"--jshell Open a JShell REPL with the classpath of the meta-level 1 build module (mill-build/). This is useful for interactively testing and debugging your build logic.",
"--offline Try to work offline. This tells modules that support it to work offline and avoid any access to the internet. This is on a best effort basis. There are currently no guarantees that modules don't attempt to fetch remote sources.",
"--keep-going Continue build, even after build failures.",
"--define <k=v> Define (or overwrite) a system property.",
Expand All @@ -380,10 +380,10 @@ object TabCompleteTests extends TestSuite {
"--bsp-install Create mill-bsp.json with Mill details under .bsp/",
"--help Print this help message and exit.",
"--jobs <str> The number of parallel threads. It can be an integer e.g. `5` meaning 5 threads, an expression e.g. `0.5C` meaning half as many threads as available cores, or `C-2` meaning 2 threads less than the number of cores. `1` disables parallelism and `0` (the default) uses 1 thread per core.",
"--ticker <bool> Enable or disable the ticker log, which provides information on running tasks and where each log line came from",
"--color <bool> Toggle colored output; by default enabled only if the console is interactive or FORCE_COLOR environment variable is set, and NO_COLOR is not set",
"--version Show mill version information and exit.",
"--task <str> The name or a query of the tasks(s) you want to build.",
"--help-advanced Print a internal or advanced command flags not intended for common usage",
"--jshell Open a JShell REPL with the classpath of the meta-level 1 build module (mill-build/). This is useful for interactively testing and debugging your build logic. Implies options `--meta-level 1` and `--no-server`."
"--task <str> The name or a query of the tasks(s) you want to build."
)
)
}
Expand Down
1 change: 0 additions & 1 deletion libs/util/java11/src/mill/util/Jvm.scala
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,6 @@ object Jvm {
)
.filter(f => f.exists())
.fold(toolName)(_.getAbsolutePath())

}

def jdkTool(toolName: String): String = jdkTool(toolName, None)
Expand Down
23 changes: 17 additions & 6 deletions libs/util/src/mill/util/JdkCommandsModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package mill.util

import mill.*
import mill.api.PathRef
import mill.util.Jvm.jdkTool

/**
* A trait providing convenient access to common JDK command-line tools.
Expand All @@ -17,38 +18,48 @@ trait JdkCommandsModule extends mill.api.Module {
*/
def jdkCommandsJavaHome: Task[Option[PathRef]] = Task.Anon { None }

private def callJdk(toolName: String, javaHome: Option[PathRef], args: Seq[String]): Int = {
os.call(
cmd = Seq(jdkTool(toolName, javaHome.map(_.path))) ++ args,
stdin = os.Inherit,
stdout = os.Inherit,
check = false
)
.exitCode
}

/**
* Runs the `java` command from this module's [[jdkCommandsJavaHome]].
* Renamed to `java` on the command line.
*/
@Task.rename("java")
@mainargs.main(name = "java")
def javaRun(args: String*): Command[Unit] = Task.Command(exclusive = true) {
Jvm.callJdkTool("java", args, jdkCommandsJavaHome().map(_.path))
Task.ctx().systemExit(callJdk("java", jdkCommandsJavaHome(), args))
Copy link
Member

@lefou lefou Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should stop the Mill process. It is enough to signal the status code via the task success value. Same for the other occurences.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, systemExit without a reason parameter is already deprecated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to propagate the existing JDK exit code, the task success/failure only lets you return 1 or 0 now and would stomp over any other code

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't see the value in those specific tasks, as they are not generic. I could very well live with some special entry point to wrap all kind of thirdparty dev tools, since we have all infrastructure to provide them, but having a dedicated task for each of them doesn't scale. Making it generic like mill tool java (as I suggested before) would scale better, since we could register more tools under the hood without inventing new tasks for each, and more importantly we could special handle such entry point and properly report exit values without stopping the Mill server.

Copy link
Member

@lefou lefou Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving with Task.ctx().systemExit makes subsequent calls to the same task significatly slower.

}

/** Runs the `javac` command from this module's [[jdkCommandsJavaHome]] */
def javac(args: String*): Command[Unit] = Task.Command(exclusive = true) {
Jvm.callJdkTool("javac", args, jdkCommandsJavaHome().map(_.path))
Task.ctx().systemExit(callJdk("javac", jdkCommandsJavaHome(), args))
}

/** Runs the `javap` command from this module's [[jdkCommandsJavaHome]] */
def javap(args: String*): Command[Unit] = Task.Command(exclusive = true) {
Jvm.callJdkTool("javap", args, jdkCommandsJavaHome().map(_.path))
Task.ctx().systemExit(callJdk("javap", jdkCommandsJavaHome(), args))
}

/** Runs the `jstack` command from this module's [[jdkCommandsJavaHome]] */
def jstack(args: String*): Command[Unit] = Task.Command(exclusive = true) {
Jvm.callJdkTool("jstack", args, jdkCommandsJavaHome().map(_.path))
Task.ctx().systemExit(callJdk("jstack", jdkCommandsJavaHome(), args))
}

/** Runs the `jps` command from this module's [[jdkCommandsJavaHome]] */
def jps(args: String*): Command[Unit] = Task.Command(exclusive = true) {
Jvm.callJdkTool("jps", args, jdkCommandsJavaHome().map(_.path))
Task.ctx().systemExit(callJdk("jps", jdkCommandsJavaHome(), args))
}

/** Runs the `jfr` command from this module's [[jdkCommandsJavaHome]] */
def jfr(args: String*): Command[Unit] = Task.Command(exclusive = true) {
Jvm.callJdkTool("jfr", args, jdkCommandsJavaHome().map(_.path))
Task.ctx().systemExit(callJdk("jfr", jdkCommandsJavaHome(), args))
}
}
Loading