Skip to content
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

JavaModule's run command arguments should be a Task #1948

Closed
daddykotex opened this issue Jul 14, 2022 · 5 comments · Fixed by #2452
Closed

JavaModule's run command arguments should be a Task #1948

daddykotex opened this issue Jul 14, 2022 · 5 comments · Fixed by #2452
Labels
good-first-issue The fix/solution for the issue is considered easy. A good starting point for newbies. next-major This change is a candidate for a next major release
Milestone

Comments

@daddykotex
Copy link
Contributor

daddykotex commented Jul 14, 2022

Currently, we can't define a def a: Task[String] = ??? and use that to call the run command from the JavaModule. See the minimal repo:

import mill._
import mill.scalalib._

object proj extends JavaModule {

/*

/private/tmp/def/build.sc:10: Target#apply() call cannot use `value args` defined within the T{...} block
    run(args)()
        ^
/private/tmp/def/build.sc:8: `T.sources` definitions must have 0 parameter lists
  def aCommand() = T.command {
      ^
Compilation Failed
*/
  def myArgs: T[String] = T { "arg" }

  def aCommand() = T.command {
    val args = myArgs()
    run(args)()
  }
}

As per a conversation on gitter with @lefou , a straightforward fix for that is to turn def run(args: String*): Command[Unit] into def run(args: Task[Seq[String]]): Command[Unit]

@lefou lefou added the next-major This change is a candidate for a next major release label Jul 25, 2022
@lefou lefou added the good-first-issue The fix/solution for the issue is considered easy. A good starting point for newbies. label Dec 19, 2022
@lefou
Copy link
Member

lefou commented Apr 23, 2023

@lihaoyi
Copy link
Member

lihaoyi commented Apr 27, 2023

I tried a prototype in #2452. Thinking about this, it appears there are a few options here:

  1. def run(args: Task[String]*): That should work already I think, but it limits the number way arguments can be passed to run since the number of arguments is fixed up front. This is an awkward limitation, and even apart from that would probably result in boilerplate at the callsite to massage the incoming Task[Seq[String]] into the Seq[Task[String]] that we can pass in

  2. def run(args: Task[String*]) is not allowed by the Scala type system

  3. def run(args: Task[mainargs.Leftover[String]]) would require some patches to the mainargs library, because currently we hard-code support for mainargs.Leftover[T]. We'd need to un-hard-code it somehow to make the Task[Leftover[String]] param signature work, and then maybe provide an implicit constructor so people can pass in run(Seq("foo")) rather than run(mainargs.Leftover(Seq("foo"))) at the callsite

  4. Leave def run(args: String*) and just make it forward to an anonymous task def run(args: Task[Seq[String]]) = T.task{...}. This is probably the easiest thing to do to solve the immediate issue without major surgery, but doesn't generalize to other T.commands unless we repeat the boilerplate of T.command/T.task pairing for each one

It feels like either (3) or (4) are the best options here, depending on how much we want to generalize this v.s. how difficult it is to make the upstream mainargs changes necessary to support (3)

@lefou
Copy link
Member

lefou commented Apr 27, 2023

Beside what this means for the codebase, I think what we want is a args: Task[Something]. For different reasons.

  • Something can be a Seq[T] to support variable count
  • Something can be a case class or the like, which is easier to evolve in a binary compatible way

The JavaModule.run is a good example for a Seq-like parameter.

If we look at a more complex use case like JavaModule.ivyDepsTree, we see currently the following signature:

def ivyDepsTree(args: IvyDepsTreeArgs = IvyDepsTreeArgs()): Command[Unit]

with IvyDepsTreeArgs, which is annotated with various mainargs annotation to be parseable.

@lihaoyi
Copy link
Member

lihaoyi commented Apr 27, 2023

com-lihaoyi/mainargs#62 has the mainargs necessary to make (3) work. It would be a binary compatibility breakage for Mainargs, but IMO it's probably worth doing sooner or later, as apart from supporting this feature it would also clean up a lot of cruft within the MainArgs codebase, and if we're breaking bin-compat in Mill 0.11.0 then it could be worth doing now

@lihaoyi
Copy link
Member

lihaoyi commented Apr 27, 2023

I've got #2452 updated to use the mainargs PR above (publishLocaled), and everything works end to end. Still pretty janky, but I'm guessing it the syntax and use sites can be cleaned up without too much work.

There's also some concern about how to receive the output of the bar.run command, since it doesn't have the T.dest folder of the calling task for it to write its files to. Probably solvable but might need a bit of thought

lihaoyi added a commit to com-lihaoyi/mainargs that referenced this issue Apr 29, 2023
…port alternate implementations (#62)

This PR moves the handling of `mainargs.Leftover`/`Flag`/`SubParser`
from a hard-coded `ArgSig` that only works with `mainargs.Leftover` or
`mainargs.Flag`, to properties of the `TokensReader` that can be
configured to work with any custom type.

Should probably be reviewed concurrently with
com-lihaoyi/mill#1948, which is the motivation
for this PR: we want to be able to define a CLI entrypoing taking
`mill.define.Task[mainargs.Leftover[T]]` or equivalent, which is
currently impossible due to the hard-coded nature of `mainargs.Leftover`
(and `mainargs.Flag` etc.)

# Major Changes

1. `ArgReader` is eliminated and `ArgSig` is greatly simplified to a
single type with no subtypes or type parameters

2. `TokensReader` is split into 5primary sub-types - `.Simple`,
`Constant`, `.Flag`, `.Leftover`, and `.Class`. These roughly mirror the
original `{ArgSig,ArgReader}.{Simple,Flag,Leftover,Class}` case classes.
The 5 sub-classes control behavior through
`Renderer.scala`/`Invoker.scala`/`TokensGrouping.scala` in the same way.

The major effect of moving the logic from `{ArgSig,ArgReader}` to
`TokensReader` is that they now are no longer hard-coded to work with
`mainargs.{Flag,Leftover,Subparser}` types. Now, anyone who has a custom
type `Foo` can choose whether they want to define a
`TokensReader.Simple` for it, or whether they want to define a
`TokensReader.Leftover` or `TokensReader.Flag`. Similarly, people can
define their own `TokensReader.Class` rather than relying on the default
implementation in `mainargs.ParserForClass`.

# Testing

Tested with two new flavors of `VarargsTests` (now renamed
`VarargsBasedTests`:

1. `VarargsWrappedTests` that exercises using a custom wrapper type to
define a main entrypoints that takes `Wrapper[mainargs.Leftover[T]]`,

2. `VarargsCustomTests` that replaces `mainargs.Leftover[T]` entirely
and defines main entrypoints that take `Wrapper[T]`

3. Added a `ConstantTests.scala` to exercise the code path, which was
previously the `noTokens` codepath and un-tested in this repo

4. All existing tests pass

# Notes

1. I chose to remove the type params from `ArgSig` because they weren't
really paying for their complexity; most of the time we were passing
around `ArgSig[_, _]`s anyway, so we weren't getting type safety, but
they nevertheless caused tons of headaches trying to get types to line
up. The un-typed ` default: Option[Any => Any], reader: TokensReader[_]`
isn't great, but it's a lot easier to work with and TBH not really much
less type-safe than the status quo

2. Because `ArgSig` and `TokensReader` now have a circular dependency on
each other (via `TokensReader.Class` -> `MainData` -> `ArgSig` ->
`TokensReader`), I moved them into the same file. This both makes the
acyclic linter happy, and also kind of makes sense since they're now
part of the same recursive data structure (v.s. previously `ArgSig` was
the recursive data structure with `TokensReader`s hanging off of each
node)

3. The new structure with `TokensReader` as a `sealed trait` with 5
distinct sub-types is different from what it was before, with
`TokensReader` as a single `class` with a grab-bag of all possible
fields and callbacks. I thought the `sealed trait` approach is much
cleaner here, since they reflect exactly the data necessary 4 different
scenarios we care about, whereas otherwise we'd find some fields
meaningless in some cases e.g. `Flag` has no meaningful fields,
`Leftover` doesn't care about `noTokens` or `alwaysRepeatable` or
`allowEmpty`, etc.
lihaoyi added a commit that referenced this issue May 1, 2023
Fixes #1948. Should probably
be reviewed concurrently with
com-lihaoyi/mainargs#62 which it depends on

# Major Changes

1. Pull in com-lihaoyi/mainargs#62, which allows
MainArg's "aggregate leftover arguments" logic to apply to custom types
and not be hardcoded to `mainargs.Leftover`

2. Update MainArgs integration code in this repo to accomadate the
changes

3. Replace the `def run(args: String*)` with `def run(args: Task[Args] =
T.task(Args()))`.

# Minor Changes

1. I moved the `import mill.main.TokenReaders._` into the `Discover`
macro, so we can stop manually importing it everywhere.

2. I moved all the aliases from `import mill.define._` to `import
mill._`. `Task[_]` in particular is a type I believe we should start
encouraging people to use more often - e.g. annotating all command
parameter types as `Task[_]`s - and so it should be in the default
`import mill._` without needing a separate import.

3. Added a direct dependency to `com.lihaoyi::pprint`, since the
implicit dependency in `mainargs` was removed in 0.5.0 due to being
unnecessary

# Testing

1. All existing tests pass, at least those I've run locally so far
(`./mill -i -j 3 example.__.local.test`, can't use CI until the mainargs
PR is published). This includes unit tests that call `run`
programmatically and integration/example tests that call `run` via the
CLI.

4. Added a new example test `5-module-run-task`, that demonstrates and
explains how to use a `ScalaModule`'s `run` method to implement a
`task`, passing in input parameters from upstream tasks and using the
output (in this case generated files) in downstream tasks

# Notes

1. There is still some boilerplate around `run`, both defining it `args:
Task[Args] = T.task(Args())` and calling it programmatically,
`run(T.task(Args(...)))`. For now I haven't figured out how to reduce
this, as we already "used up" our implicit conversion budget with the
`T.*{...}` macros. Probably can be done, although it won't be quite as
low-boilerplate as the original `args: String*` syntax. Maybe in a
follow-up PR

2. The example test `5-module-run-task` is something I've wanted to be
able to write for years now. Nice to finally be able to do it without
having to piece together a `Jvm.runSubprocess()` call myself!

3. `5-module-run-task` is still a bit clunkier than I was hoping. Having
to `def barWorkingDir` beforehand to pass it to both the `bar.run` and
the `def sources` override is weird and unintuitive. Maybe we can change
the `def run` return type to make it easier to return the `T.dest`
folder or other metadata out of the body of `def run` so it can be used
by downstream tasks? e.g. making `def run` return `Command[PathRef]`
rather than `Command[Unit]`


5. IMO we should recommend that all CLI arguments to `T.command`s come
in the form of `Task[T]`s, unless there's concrete reasons otherwise
(e.g. we want to dynamically change `T.command` implementations based on
the input). That would maximise the ability to re-use commands in the
body of of tasks, which is something people probably want

---------

Co-authored-by: Tobias Roeser <[email protected]>
@lefou lefou added this to the 0.11.0-M9 milestone May 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
good-first-issue The fix/solution for the issue is considered easy. A good starting point for newbies. next-major This change is a candidate for a next major release
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants