-
-
Notifications
You must be signed in to change notification settings - Fork 418
Simple YAML-backed modules and single-file script modules #5836
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…ll (com-lihaoyi#5820)"" This reverts commit 72db1f3.
ecae588
to
32888ba
Compare
7e884cf
to
7bf6fae
Compare
7bf6fae
to
b3ab4f0
Compare
f7cf97c
to
fb36045
Compare
912d4d5
to
167d73c
Compare
I think the generated code should only use well tested code blocks, so the chance of a compile error is rather low, since if we managed to fully generate the code, we already know how to map each found YAML section to such a tested code block. Recompilation is probably only needed, if the (We might even have pre-compiled backing impls for most polular traits like |
We should move the discussion of the topic into a dedicated issue. |
CC @alexarchambault @arturaz if you have a moment to review this PR and feature proposal it would be great to get your thoughts as well |
Due to time constraints, I couldn't make a thorough review. OTOH, I fear an accidental/premature merge of this feature before we had a proper discussion, which I think is necessary. I want to remind us that once, we merge, we need to stick to that feature, since it is rather tightly coupled (located inside the existing modules, special CLI handling, ...). In addition to my previous comments and ideas, which are not directly related to this PR but to the general concept, here are some quick notes regarding this PR. File namesThe
Should we use Option handlingAlso, I think we should not skip the need to pass a Specifying an alternative build file with Simple modulesWe should not make the simple modules like To establish some naming pattern, we could place all the simple support files/classes in a |
01ff4a2
to
1c2f830
Compare
Closing in favor of #5951 |
…ration (#5951) This PR implements a way to define "Simple Modules" based on `mill.yaml` files or single `.java`/`.scala`/`.kt` files with `//|` build headers. This should make Mill more appealing for small projects, where the use of a `build.mill` file adds significant boilerplate and complexity. Most small projects need minimal customization of the build, and so the full Scala `.mill` syntax provides no value over a more lightweight config-only approach. This PR also allows interop between simple YAML modules and custom module classes written in `mill-build/src/`, to allow a gradual transition to the more flexible programmatic configuration ## Simple Module with `build.mill.yaml` `mill.yaml` ```yaml extends: ["mill.javalib.JavaModule"] mvnDeps: - "net.sourceforge.argparse4j:argparse4j:0.9.0" - "org.thymeleaf:thymeleaf:3.1.1.RELEASE" ``` `test/mill.yaml` ```yamlextends: "mill.javalib.JavaModule.Junit4" moduleDeps: ["build"] mvnDeps: - "com.google.guava:guava:33.3.0-jre" ``` ```console > ./mill run --text hello <h1>hello</h1> > ./mill .:run --text hello # `.` for explicit root module, `:` as new external module separator > ./mill test ... + foo.FooTests...simple ... "<h1>hello</h1>" + foo.FooTests...escaping ... "<h1><hello></h1>" > ./mill test:testForked # explicit task ``` ## Single-File Module with `.java`, `.scala`, or `.kt` `Foo.scala` ```scala //| mvnDeps: //| - "com.lihaoyi::scalatags:0.13.1" //| - "com.lihaoyi::mainargs:0.7.6" import scalatags.Text.all.* import mainargs.{main, ParserForMethods} object Foo { def generateHtml(text: String) = { h1(text).toString } @main def main(text: String) = { println(generateHtml(text)) } def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args) } ``` ```console > ./mill Foo.scala --text hello compiling 1 Scala source to... <h1>hello</h1> > ./mill Foo.scala:run --text hello <h1>hello</h1> > ./mill show Foo.scala:assembly # show the output of the assembly task ".../out/Foo.scala/assembly.dest/out.jar" > java -jar ./out/Foo.scala/assembly.dest/out.jar --text hello <h1>hello</h1> ``` ## Implementation * Mill considers `build.mill.yaml` and `package.mill.yaml` files similarly to `build.mill` and `package.mill` * Mill code-generates Scala code for a `package` object for each `build.mill.yaml`/`package.mill.yaml` file: `extends` becomes the inheritance list, `moduleDeps` becomes `moduleDeps`, and everything else becomes `def key = Task.Literal("""value""")` * A bunch of the `upickle.default.ReadWriter`s have been made more lenient to allow convenient YAML/JSON input of their values: `PathRef`s, the various `mill.javalib.publish.*` types, * `.mill.yaml` and `.mill` can depend on each other and inter-operate, since in the end they all expand into Scala code * Single-file scripts continue to be modelled as `ExternalModule`s instantiated during the `Resolve` phase. These cannot be depended upon by `.mill.yaml`/`.mill` modules Supersedes #5836 ### Advantages 1. This allows use of normal `trait` modules without needing to package them up into special `class`es, so it can be applied much more generally to dozens of `trait`s available in the Mill libraries 2. This allows multiple inheritance, e.g. `extends: [mill.javalib.JavaModule, mill.javalib.PublishModule]` 3. Modules defined in `build/package.mill.yaml` can interop much more cleanly with `build/package.mill` files, including type-safe references in either direction 4. We can handle things like requiring abstract methods to be defined in the YAML, since the YAML keys get code-gened into method implementations ### Disadvantages 1. Generated code can slow down compiles, whereas previous reflective instantiation was basically instant 2. Compile errors in generated code may be confusing to the user, since they didn't write the generated code themselves 3. Script files will still need to be instantiated reflectively from `class`es, as the ad-hoc nature of scripts means they may live in arbitrary files that are only discovered pretty late (e.g in the middle of execution of a `show` command that resolves things) that cannot be easily discovered during Mill's `walkBuildFiles` step 4. Script files cannot use test frameworks for now, though they can be tested via normal downstream scripts with `main` methods. This could be added in future ## Docs and Testing `.mill.yaml` files are largely covered by example tests: these also form the documentation, which takes over the first two examples in `{javalib,scalalib,kotlinlib}/intro.html`, and on an additional page `{javalib,scalalib,kotlinlib}/simple.html`. Since the main logic around these `.mill.yaml` files runs as part of `MillBuildBootstrap`/`MillBuildRootModule/`CodeGen`, they cannot be easily exercised with unit tests, and so these example tests will have to do for now --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This PR implements a way to define "Simple Modules" based on
mill.yaml
files or single.java
/.scala
/.kt
files with//|
build headers. This should make Mill more appealing for small projects, where the use of abuild.mill
file adds significant boilerplate and complexity. Most small projects need minimal customization of the build, and so the full Scala.mill
syntax provides no value over a more lightweight config-only approach. This PR also allows interop between simple YAML modules and custom module classes written inmill-build/src/
, to allow a gradual transition to the more flexible programmatic configurationSimple Module with
mill.yaml
mill.yaml
test/mill.yaml
Single-File Module with
.java
,.scala
, or.kt
Foo.scala
Implementation
Each
mill.yaml
simple module or*.{java,scala,kt}
script file on disk is instantiated into anExternalModule
in Mill, and can be configured via YAML data and depend on each other and on traditional "Programmatic Modules" (and vice versa). Each one instantiates amill.simple.SimpleModule
specified viaextends:
, and users can define their own concrete subclass ofSimpleModule
to use in their simple modules if the defaults do not do everything they want.mill.Module
s, you can run multiple simple modules or script files together with+
, or run them together with programmatic modules, or use them withshow
orinspect
. They also can depend on each other arbitrarily, since in the end they are allmill.Module
s in the Mill daemon classloaderParsing the query selectors is done by first attempting a parse-and-resolution for normal programmatic modules and tasks, and only if that fails do we attempt a parse-and-resolution for simple yaml-backed modules. This preserves backwards compatibility by ensuring queries that resolved to programmatic modules in the past continue to do so unchanged, while allowing a concise syntax for referencing simple yaml-backed modules
We re-use most of the infrastructure from Mill's existing YAML build headers, though refactored so the YAML metadata can apply to arbitrary modules rather than only the root meta-build module.
As we need to instantiate these modules reflectively, we cannot use
trait
s orabstract class
es, and so the modules thatmill.yaml
files or single-file scripts use inextends
need to be concrete classes. These are configured to take aSimpleModule.Config
case class
injected by Mill containing non-task metadata like the script file path andmoduleDeps
, though we can extend it to contain additional fields in future if necessaryclass
modules need to be stubbed out with= Task { ??? }
TODO
MavenModule
/MavenKotlinModule
/SbtModule
mill.yaml
files and build headersSupersedes #5826