diff --git a/CHANGELOG.md b/CHANGELOG.md index b98a0e6..6d33aba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,20 @@ # Change Log -## [3.4.1] - 2024-11-12 +## [3.5.0] - 2024-11-13 + +### Changed +- `--arg, --arg-map, and --arg-map` are now `--target-argmap, and --target-argmap` +- Default commands directory from `monorail` to `monorail/cmd` + +### Added +- Optional per-target base argmaps +- Default argmaps directory of `monorail/argmap` ### Changed +## [3.4.1] - 2024-11-12 + +### Changed - `--arg, --arg-map, and --arg-map` may all be specified simultaneously, and are processed with a precendece order - Support for multiple `--arg-map` and `--arg-map-file` arguments and merging of them diff --git a/Cargo.toml b/Cargo.toml index 1d3e02c..3e2bfe2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ description = "A tool for effective polyglot, multi-project monorepo development license = "MIT" homepage = "https://github.com/pnordahl/monorail" repository = "https://github.com/pnordahl/monorail" -version = "3.4.1" +version = "3.5.0" authors = ["Patrick Nordahl "] edition = "2021" keywords = ["monorail", "monorepo", "build", "cli", "build-tool"] diff --git a/Monorail.reference.js b/Monorail.reference.js index a172aa0..e327887 100644 --- a/Monorail.reference.js +++ b/Monorail.reference.js @@ -59,6 +59,30 @@ const reference = { "path/within/repository" ], + /* + Configuration and overrides for argument mappings. + */ + "argmaps": { + /* + Default location within this target path containing argmap + definition files. This is used when locating argmaps for this + target automatically when it is changed. + + Optional, default: "monorail/argmap" + */ + "path": "path/within/this/target", + + /* + Path to the default argmap to load for this target. This argmap + is loaded prior to any arguments supplied by use of + --arg, --target-argmap, and --target-argmap-file switches on `monorail run`. + + Optional, default: "base.json" + */ + "base": "path/within/target/argmaps/path" + } + + /* Configuration and overrides for this targets commands. Optional. @@ -69,7 +93,7 @@ const reference = { executables. This is used when locating executables for a command when no definition path for that command is specified. - Optional, default: "monorail" + Optional, default: "monorail/cmd" */ "path": "path/within/this/target" diff --git a/README.md b/README.md index afe1714..c152907 100644 --- a/README.md +++ b/README.md @@ -299,37 +299,58 @@ This will run `build` first for `dep1`, and then `target1` and `target2` in para #### Arguments -Commands can be provided with runtime positional arguments by providing the `--arg (-a)`, `--arg-map (-m)`, and `--arg-map-file (-f)` switches to `monorail run`. For In your command executables, capture these positional arguments as you would in any program in that language. For example: +Commands can be provided with positional arguments at runtime. There are two mechanism for doing this; the base argmap, and as flags provided to `monorail run`. In your command executables, capture these positional arguments as you would in any program in that language. + +##### Base argmap +The base argmap is an optional file containing argument mappings, which, if provided, is automatically loaded before any other argument mappings provided to `monorail run`. This is useful for specifying static parameterization for commands, especially when a command executable is generic and reused among multiple targets. An example of this useful pattern is shown in https://github.com/pnordahl/monorail-example in the rust crate targets. + +For example, here `target1` has a base argmap in the default location: + +`target1/monorail/argmap/base.json` +```json +{ + "build": [ + "--all" + ] +} +``` + +You can change both the argmap search path and the base argmap file by specifying them in `Monorail.json`. Refer to the `argmaps` section of `Monorail.reference.js` for more details. + +Finally, while base argmaps are often required by commands that are built to expect them, you can disable base argmaps during a `monorail run` with the `--no-base-argmaps` switch. + +##### Runtime flags + +In addition to the base argmap, `monorail run` accepts `--arg (-a)`, `--target-argmap (-m)`, and `--target-argmap-file (-f)` flags for providing additional argument mappings. ```sh monorail run -c build -t target1 --arg '--release' --arg '-v' ``` -This will provide `--release` and `-v` as the first and second positional arguments to the `build` command for `target1`. +This will first provide `-all` from the base argmap as the first positional argument, and then `--release` and `-v` as the second and third positional arguments to the `build` command for `target1`. -Note that when using `--arg`, you must specify exactly one command and target. For more flexibility, use `--arg-map` and/or `--arg-map-file`, which allow for specifying argument arrays for specific command-target combinations. For example: +Note that when using `--arg`, you must specify exactly one command and target. For more flexibility, use `--target-argmap` and/or `--target-argmap-file`, which allow for specifying argument arrays for specific command-target combinations. For example: ```sh -monorail run -c build test --arg-map '{"target1":{"build":["--release"],"test":["--release"]}}' +monorail run -c build test --target-argmap '{"target1":{"build":["--release"],"test":["--release"]}}' ``` -This will provide the specified arguments to the appropriate command-target combinations. Multiple `--arg-map` flags may be provided, and if so keys that appear in both have their arrays appended to each other in the order specified. For example: +This will provide the specified arguments to the appropriate command-target combinations. Multiple `--target-argmap` flags may be provided, and if so keys that appear in both have their arrays appended to each other in the order specified. For example: ```sh -monorail run -c build test --arg-map '{"target1":{"build":["--release"],"test":["--release"]}}' --arg-map '{"target1":{"build":["-v"]}}' +monorail run -c build test --target-argmap '{"target1":{"build":["--release"],"test":["--release"]}}' --target-argmap '{"target1":{"build":["-v"]}}' ``` In this case, `build` appears twice for `target1` so the arrays are combined in order, and the final arguments array for `target1` is `[--release, -v]`. -Additionally, you can provide the `--arg-map-file` switch zero or more times with a filesystem path to a JSON file containing an argument mapping. The structure of this file is identical to the one described above for `--arg-map`. As with that switch, files provided are merged from left to right. For example +Additionally, you can provide the `--target-argmap-file` flag zero or more times with a filesystem path to a JSON file containing an argument mapping. The structure of this file is identical to the one described above for `--target-argmap`. As with that flag, files provided are merged from left to right. For example ```sh -monorail run -c build -f target1/argmap1.json -f target1/argmap2.json +monorail run -c build -f target1/monorail/argmap/foo.json -f target1/monorail/argmap/bar.json ``` Would merge the keys and values from each file in turn in order. - #### Sequences A sequence is an array of commands, and is specified in `Monorail.json`. It's useful for bundling together related commands into a convenient alias, and when used with `run` simply expands into the list of commands it references. For example: diff --git a/TUTORIAL.md b/TUTORIAL.md index ef750f8..d9d6bbc 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -103,7 +103,7 @@ monorail checkpoint update These commands will make a rust workspace with two member projects with tests: ```sh -mkdir -p rust/monorail +mkdir -p rust/monorail/cmd pushd rust cargo init --lib app1 cargo init --lib app2 @@ -121,7 +121,7 @@ popd ### Python These commands will make a simple python project with a virtualenv and a test: ```sh -mkdir -p python/app3/monorail +mkdir -p python/app3/monorail/cmd pushd python/app3 python3 -m venv "venv" @@ -153,7 +153,7 @@ popd ``` ### Protobuf ```sh -mkdir -p proto/monorail +mkdir -p proto/monorail/cmd pushd proto touch README.md popd @@ -164,13 +164,13 @@ popd Commands will be covered in more depth later in the tutorial (along with logging), but now that we have a valid `Monorail.json` we can execute a command and view logs right away. Run the following to create an executable (in this case, a `bash` script) for the `rust` target: ```sh -cat < rust/monorail/hello.sh +cat < rust/monorail/cmd/hello.sh #!/bin/bash echo 'Hello, world!' echo 'An error message' >&2 EOF -chmod +x rust/monorail/hello.sh +chmod +x rust/monorail/cmd/hello.sh ``` Now execute it: @@ -312,7 +312,7 @@ monorail analyze --changes | jq "path": "rust/app2/src/lib.rs" }, { - "path": "rust/monorail/hello.sh", + "path": "rust/monorail/cmd/hello.sh", } ], "targets": [ @@ -504,7 +504,7 @@ monorail log tail --stderr --stdout This has started a server that will receive logs. For the rest of this tutorial, leave that running in a separate window. Now, let's update our existing command to demonstrate tailing: ```sh -cat < rust/monorail/hello.sh +cat < rust/monorail/cmd/hello.sh #!/bin/bash for ((i=0; i<20; i++)); do @@ -604,30 +604,30 @@ cargo test -- --nocapture ### Defining a command -By default, `monorail` will use the `commands_path` (default: a `monorail` directory in the target path) field of a target as a search path for commands, and by default look for a file with a stem of `{{command}}`, e.g. `{{command}}.sh`. The command we defined earlier in the tutorial, `rust/monorail/hello.sh`, used these defaults; While customizing these defaults is possible via `Monorail.json`, it's not necessary for this tutorial. Let's define two new executables, this time in Python and Awk: +By default, `monorail` will use the `commands_path` (default: a `monorail` directory in the target path) field of a target as a search path for commands, and by default look for a file with a stem of `{{command}}`, e.g. `{{command}}.sh`. The command we defined earlier in the tutorial, `rust/monorail/cmd/hello.sh`, used these defaults; While customizing these defaults is possible via `Monorail.json`, it's not necessary for this tutorial. Let's define two new executables, this time in Python and Awk: ```sh -cat < python/app3/monorail/hello.py +cat < python/app3/monorail/cmd/hello.py #!venv/bin/python3 import sys print("Hello, from python/app3 and virtualenv python!") print("An error occurred", file=sys.stderr) EOF -chmod +x python/app3/monorail/hello.py +chmod +x python/app3/monorail/cmd/hello.py ``` NOTE: While we're able to use the venv python3 executable directly in this trivial way, we wouldn't be able to access things that are installed in the environment without first activating the venv in a shell. So, in practice Python projects generally require a command written in a shell script that activates the env and then calls the script. ```sh -cat < proto/monorail/hello.awk +cat < proto/monorail/cmd/hello.awk #!/usr/bin/awk -f BEGIN { print "Hello, from proto and awk!" } EOF -chmod +x proto/monorail/hello.awk +chmod +x proto/monorail/cmd/hello.awk ``` As mentioned earlier, commands can be written in any language, and need only be executable. We're using hashbangs to avoid cluttering the tutorial with compilation steps, but commands could be compiled to machine code, stored as something like `hello`, and executed just the same (though this approach wouldn't be portable across OS/architectures). Before we run this command, let's look at the output of analyze: @@ -774,7 +774,7 @@ monorail target show --commands | jq "commands": { "hello": { "name": "hello", - "path": "/private/tmp/monorail-tutorial/rust/monorail/hello.sh", + "path": "/private/tmp/monorail-tutorial/rust/monorail/cmd/hello.sh", "args": null, "is_executable": true } @@ -788,7 +788,7 @@ monorail target show --commands | jq "commands": { "hello": { "name": "hello", - "path": "/private/tmp/monorail-tutorial/python/app3/monorail/hello.py", + "path": "/private/tmp/monorail-tutorial/python/app3/monorail/cmd/hello.py", "args": null, "is_executable": true } @@ -802,7 +802,7 @@ monorail target show --commands | jq "commands": { "hello": { "name": "hello", - "path": "/private/tmp/monorail-tutorial/proto/monorail/hello.awk", + "path": "/private/tmp/monorail-tutorial/proto/monorail/cmd/hello.awk", "args": null, "is_executable": true } @@ -847,19 +847,19 @@ monorail checkpoint update --pending | jq "checkpoint": { "id": "4b5c5a4ce18a05b0175c1db6a14fb69bf1ca30d3", "pending": { - "python/app3/monorail/hello.py": "fad357d1f5adadb0e270dfcf1029c6ed76e2565e62c811613eb315de10143ceb", + "python/app3/monorail/cmd/hello.py": "fad357d1f5adadb0e270dfcf1029c6ed76e2565e62c811613eb315de10143ceb", "rust/app1/Cargo.toml": "044de847669ad2d9681ba25c4c71e584b5f12d836b9a49e71b4c8d68119e5592", "proto/LICENSE.md": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "Monorail.json": "b02cc0db02ef8c35ba8c285336748810c590950c263b5555f1781ac80f49a6da", "proto/README.md": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "rust/app2/Cargo.toml": "111f4cf0fd1b6ce6f690a5f938599be41963905db7d1169ec02684b00494e383", ".gitignore": "c1cf4f9ff4b1419420b8508426051e8925e2888b94b0d830e27b9071989e8e7d", - "rust/monorail/hello.sh": "c1b9355995507cd3e90727bc79a0d6716b3f921a29b003f9d7834882218e2020", + "rust/monorail/cmd/hello.sh": "c1b9355995507cd3e90727bc79a0d6716b3f921a29b003f9d7834882218e2020", "python/app3/hello.py": "3639634f2916441a55e4b9be3497673f110014d0ce3b241c93a9794ffcf2c910", "python/app3/tests/test_hello.py": "72b3668ed95f4f246150f5f618e71f6cdbd397af785cd6f1137ee87524566948", "rust/Cargo.toml": "a35f77bcdb163b0880db4c5efeb666f96496bcb409b4cd52ba6df517fb4d625b", "rust/app2/src/lib.rs": "536215b9277326854bd1c31401224ddf8f2d7758065c9076182b37621ad68bd9", - "proto/monorail/hello.awk": "5af404fedc153710aec00c8bf788d8f71b00c733c506d4c28fda1b7d618e4af6", + "proto/monorail/cmd/hello.awk": "5af404fedc153710aec00c8bf788d8f71b00c733c506d4c28fda1b7d618e4af6", "rust/app1/src/lib.rs": "536215b9277326854bd1c31401224ddf8f2d7758065c9076182b37621ad68bd9" } } diff --git a/src/api/cli.rs b/src/api/cli.rs index c662ab9..890e2e6 100644 --- a/src/api/cli.rs +++ b/src/api/cli.rs @@ -43,9 +43,10 @@ pub const ARG_STDOUT: &str = "stdout"; pub const ARG_ID: &str = "id"; pub const ARG_DEPS: &str = "deps"; pub const ARG_ARG: &str = "arg"; -pub const ARG_ARG_MAP: &str = "arg-map"; -pub const ARG_ARG_MAP_FILE: &str = "arg-map-file"; +pub const ARG_ARG_MAP: &str = "argmap"; +pub const ARG_ARG_MAP_FILE: &str = "argmap-file"; pub const ARG_FAIL_ON_UNDEFINED: &str = "fail-on-undefined"; +pub const ARG_NO_BASE_ARGMAPS: &str = "no-base-argmaps"; pub const VAL_JSON: &str = "json"; @@ -213,11 +214,10 @@ pub fn build() -> clap::Command { .subcommand(Command::new(CMD_RUN) .about("Run target-defined commands.") .after_help(r#" -When --arg-map-file, --arg-map, and/or --arg are provided, keys that appear multiple -times will have their respective arrays concatenated in the following order: +When --target-argmap-file, --target-argmap, and/or --arg are provided, keys that appear multiple times will have their respective arrays concatenated in the following order, after any base argmaps for targets involved in the run: - 1. Each --arg-map-file, in the order provided - 2. Each --arg-map literal, in the order provided + 1. Each --target-argmap-file, in the order provided + 2. Each --target-argmap literal, in the order provided 3. Each --arg, in the order provided Refer to --help for more information on these options. @@ -274,6 +274,12 @@ Refer to --help for more information on these options. .long_help("Fail commands that are undefined by targets.") .action(ArgAction::SetTrue), ) + .arg( + Arg::new(ARG_NO_BASE_ARGMAPS) + .long(ARG_NO_BASE_ARGMAPS) + .long_help("Disable loading base argmaps for run targets. By default, all base argmaps are loaded.") + .action(ArgAction::SetTrue), + ) .arg( Arg::new(ARG_ARG) .short('a') @@ -282,7 +288,7 @@ Refer to --help for more information on these options. .required(false) .action(ArgAction::Append) .help("One or more runtime argument(s) to be provided when executing a command for a single target.") - .long_help("This is a shorthand form of the more expressive '--arg-map' and '--arg-map-file', designed for single command + single target use. Providing this flag without specifying exactly one command and one target will result in an error.") + .long_help("This is a shorthand form of the more expressive '--target-argmap' and '--target-argmap-file', designed for single command + single target use. Providing this flag without specifying exactly one command and one target will result in an error.") ) .arg( Arg::new(ARG_ARG_MAP) @@ -771,6 +777,7 @@ impl<'a> TryFrom<&'a clap::ArgMatches> for app::run::HandleRunInput<'a> { .collect(), include_deps: cmd.get_flag(ARG_DEPS), fail_on_undefined: cmd.get_flag(ARG_FAIL_ON_UNDEFINED), + use_base_argmaps: !cmd.get_flag(ARG_NO_BASE_ARGMAPS), }) } } diff --git a/src/app/run.rs b/src/app/run.rs index 16b7709..cea7878 100644 --- a/src/app/run.rs +++ b/src/app/run.rs @@ -28,6 +28,7 @@ pub(crate) struct HandleRunInput<'a> { pub(crate) arg_map_file: Vec<&'a String>, pub(crate) include_deps: bool, pub(crate) fail_on_undefined: bool, + pub(crate) use_base_argmaps: bool, } #[derive(Debug, Serialize, Deserialize)] @@ -137,37 +138,54 @@ struct ArgMap { table: HashMap>>, } impl ArgMap { - fn merge( - src: HashMap>>, - dest: &mut HashMap>>, - ) { + fn new() -> Self { + Self { + table: HashMap::new(), + } + } + fn merge(&mut self, src: HashMap>>) { for (src_target, src_commands) in src { - let dest_commands = dest.entry(src_target).or_default(); + let self_commands = self.table.entry(src_target).or_default(); for (src_command, mut src_args) in src_commands { - dest_commands + self_commands .entry(src_command) .or_default() .append(&mut src_args); } } } - fn new(input: &HandleRunInput<'_>) -> Result { - let mut table = HashMap::new(); - + // Loads an argmap containing only commands. Specifically useful for a target's + // base argmap, which defines only command -> [arg]. + fn merge_target_commands(&mut self, target: &str, p: &path::Path) -> Result<(), MonorailError> { + let f = fs::File::open(p).map_err(MonorailError::from)?; + let br = io::BufReader::new(f); + let src = serde_json::from_reader(br).map_err(|e| { + MonorailError::Generic(format!( + "File arg map at {} contains invalid JSON; {}", + p.display(), + e + )) + })?; + self.merge(HashMap::from([(target.to_string(), src)])); + Ok(()) + } + fn merge_file(&mut self, p: &path::Path) -> Result<(), MonorailError> { + let f = fs::File::open(p).map_err(MonorailError::from)?; + let br = io::BufReader::new(f); + let src = serde_json::from_reader(br).map_err(|e| { + MonorailError::Generic(format!( + "File arg map at {} contains invalid JSON; {}", + p.display(), + e + )) + })?; + self.merge(src); + Ok(()) + } + fn merge_run_input(&mut self, input: &HandleRunInput) -> Result<(), MonorailError> { // first process files for f in &input.arg_map_file { - let p = path::Path::new(f); - let f = fs::File::open(p).map_err(MonorailError::from)?; - let br = io::BufReader::new(f); - let src: HashMap>> = serde_json::from_reader(br) - .map_err(|e| { - MonorailError::Generic(format!( - "File arg map at {} contains invalid JSON; {}", - p.display(), - e - )) - })?; - ArgMap::merge(src, &mut table); + self.merge_file(path::Path::new(f))?; } // next, argmap literals @@ -176,7 +194,7 @@ impl ArgMap { .map_err(|e| { MonorailError::Generic(format!("Inline arg map contains invalid JSON; {}", e)) })?; - ArgMap::merge(src, &mut table); + self.merge(src); } // finally, args @@ -203,9 +221,9 @@ impl ArgMap { input.args.iter().map(|s| s.to_string()).collect(), )]), )]); - ArgMap::merge(src, &mut table); + self.merge(src) } - Ok(Self { table }) + Ok(()) } fn get_args<'a>(&'a self, target: &'a str, command: &'a str) -> Option<&'a [String]> { if let Some(cmd_map) = &self.table.get(target) { @@ -231,7 +249,7 @@ pub(crate) async fn handle_run<'a>( let mut tracking_run = get_next_tracking_run(cfg, &tracking_table)?; let run_path = setup_run_path(cfg, tracking_run.id, work_path)?; let commands = get_all_commands(cfg, &input.commands, &input.sequences)?; - let arg_map = ArgMap::new(input)?; + let mut arg_map = ArgMap::new(); let (index, target_groups) = match input.targets.len() { 0 => { @@ -255,6 +273,15 @@ pub(crate) async fn handle_run<'a>( let target_groups = ao .target_groups .ok_or(MonorailError::from("No target groups found"))?; + if input.use_base_argmaps { + for t in ao.targets.iter() { + let base = + cfg.targets[*index.get_target_index(t)?].get_argmap_base_path(work_path); + if file::exists(&base).await { + arg_map.merge_target_commands(t, &base)?; + } + } + } (index, target_groups) } _ => { @@ -262,6 +289,16 @@ pub(crate) async fn handle_run<'a>( let target_groups = if input.include_deps { let ai = analyze::AnalyzeInput::new(false, false, true); let ao = analyze::analyze(&ai, &mut index, None)?; + if input.use_base_argmaps { + for t in ao.targets.iter() { + let base = cfg.targets[*index.get_target_index(t)?] + .get_argmap_base_path(work_path); + if file::exists(&base).await { + arg_map.merge_target_commands(t, &base)?; + } + } + } + ao.target_groups .ok_or(MonorailError::from("No target groups found"))? } else { @@ -269,6 +306,13 @@ pub(crate) async fn handle_run<'a>( // we will make synthetic serialized length 1 groups that ignore the graph let mut tg = vec![]; for t in input.targets.iter() { + if input.use_base_argmaps { + let base = cfg.targets[*index.get_target_index(t)?] + .get_argmap_base_path(work_path); + if file::exists(&base).await { + arg_map.merge_target_commands(t, &base)?; + } + } tg.push(vec![t.to_string()]); } debug!("Synthesized target groups"); @@ -278,6 +322,8 @@ pub(crate) async fn handle_run<'a>( } }; + arg_map.merge_run_input(input)?; + let plan = get_plan( &index, &commands, @@ -1036,6 +1082,7 @@ mod tests { arg_map_file, include_deps: false, fail_on_undefined: false, + use_base_argmaps: true, } } @@ -1076,7 +1123,10 @@ mod tests { vec![&path_str], ); - let arg_map = ArgMap::new(&input).expect("Expected valid ArgMap"); + let mut arg_map = ArgMap::new(); + arg_map + .merge_run_input(&input) + .expect("Expected argmap merge to succeed"); let args = arg_map .get_args("rust/crate1", "build") .expect("Args not found"); @@ -1102,7 +1152,10 @@ mod tests { vec![], ); - let arg_map = ArgMap::new(&input).expect("Expected valid ArgMap"); + let mut arg_map = ArgMap::new(); + arg_map + .merge_run_input(&input) + .expect("Expected argmap merge to succeed"); let args = arg_map .get_args("rust/crate1", "build") .expect("Args not found"); @@ -1125,7 +1178,8 @@ mod tests { vec![], ); - let result = ArgMap::new(&input); + let mut arg_map = ArgMap::new(); + let result = arg_map.merge_run_input(&input); assert!(result.is_err()); } @@ -1144,7 +1198,8 @@ mod tests { vec![], ); - let result = ArgMap::new(&input); + let mut arg_map = ArgMap::new(); + let result = arg_map.merge_run_input(&input); assert!(result.is_err()); } @@ -1155,7 +1210,10 @@ mod tests { let input = setup_handle_run_input(vec![], HashSet::new(), vec![], vec![&json, &json2], vec![]); - let arg_map = ArgMap::new(&input).expect("Expected valid ArgMap"); + let mut arg_map = ArgMap::new(); + arg_map + .merge_run_input(&input) + .expect("Expected argmap merge to succeed"); let args = arg_map .get_args("rust/crate1", "build") .expect("Args not found"); @@ -1169,7 +1227,8 @@ mod tests { let input = setup_handle_run_input(vec![], HashSet::new(), vec![], vec![&invalid_json], vec![]); - let result = ArgMap::new(&input); + let mut arg_map = ArgMap::new(); + let result = arg_map.merge_run_input(&input); assert!(result.is_err()); } @@ -1194,7 +1253,10 @@ mod tests { vec![], vec![&path_str, &path_str2], ); - let arg_map = ArgMap::new(&input).expect("Expected valid ArgMap"); + let mut arg_map = ArgMap::new(); + arg_map + .merge_run_input(&input) + .expect("Expected argmap merge to succeed"); let args = arg_map .get_args("rust/crate1", "build") @@ -1210,7 +1272,8 @@ mod tests { let path_str = path.display().to_string(); let input = setup_handle_run_input(vec![], HashSet::new(), vec![], vec![], vec![&path_str]); - let result = ArgMap::new(&input); + let mut arg_map = ArgMap::new(); + let result = arg_map.merge_run_input(&input); assert!(result.is_err()); } @@ -1220,7 +1283,10 @@ mod tests { let json = ARG_MAP_JSON.to_string(); let input = setup_handle_run_input(vec![], HashSet::new(), vec![], vec![&json], vec![]); - let arg_map = ArgMap::new(&input).expect("Expected valid ArgMap"); + let mut arg_map = ArgMap::new(); + arg_map + .merge_run_input(&input) + .expect("Expected argmap merge to succeed"); let args = arg_map.get_args("nonexistent_target", "nonexistent_command"); assert!(args.is_none()); diff --git a/src/core/file.rs b/src/core/file.rs index 1c09f53..d202a8b 100644 --- a/src/core/file.rs +++ b/src/core/file.rs @@ -9,6 +9,10 @@ use tracing::debug; use crate::core::error::MonorailError; +pub(crate) async fn exists(path: &path::Path) -> bool { + tokio::fs::metadata(path).await.is_ok() +} + pub(crate) fn contains_file(p: &path::Path) -> Result<(), MonorailError> { if p.is_file() { return Ok(()); diff --git a/src/core/mod.rs b/src/core/mod.rs index 8c6d1df..a8d5217 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -134,13 +134,13 @@ impl Config { } } -#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub(crate) struct CommandDefinition { #[serde(default)] pub(crate) path: String, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub(crate) struct Target { // The filesystem path, relative to the repository root. @@ -157,9 +157,56 @@ pub(crate) struct Target { // Configuration and optional overrides for commands. #[serde(default)] pub(crate) commands: TargetCommands, + + // Configuration and optional overrides for argmaps. + #[serde(default)] + pub(crate) argmaps: TargetArgMaps, +} +impl Target { + pub(crate) fn get_argmap_base_path(&self, work_path: &path::Path) -> path::PathBuf { + work_path + .join(&self.path) + .join(&self.argmaps.path) + .join(&self.argmaps.base) + } } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub(crate) struct TargetArgMaps { + // Relative path from this target's `path` to a directory containing + // base argmap files to be used when this target is involved in + // `monorail run`. These argmaps are the first loaded, so any runtime + // instances of --args, --target-argmap, and/or --target-argmap-files are merged + // into + #[serde(default = "TargetArgMaps::default_path")] + pub(crate) path: String, + + // A default argmap to load for this target. + #[serde(default = "TargetArgMaps::default_base")] + pub(crate) base: String, +} + +impl Default for TargetArgMaps { + fn default() -> Self { + Self { + path: Self::default_path(), + base: Self::default_base(), + } + } +} +impl TargetArgMaps { + fn default_path() -> String { + "monorail/argmap".into() + } +} +impl TargetArgMaps { + fn default_base() -> String { + "base.json".into() + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub(crate) struct TargetCommands { // Relative path from this target's `path` to a directory containing @@ -183,13 +230,14 @@ impl Default for TargetCommands { } impl TargetCommands { fn default_path() -> String { - "monorail".into() + "monorail/cmd".into() } } #[derive(Debug)] pub(crate) struct Index<'a> { pub(crate) targets: Vec, + pub(crate) target2index: HashMap<&'a str, usize>, pub(crate) targets_trie: Trie, pub(crate) ignores: Trie, pub(crate) uses: Trie, @@ -204,6 +252,7 @@ impl<'a> Index<'a> { work_path: &path::Path, ) -> Result { let mut targets = vec![]; + let mut target2index = HashMap::new(); let mut targets_builder = TrieBuilder::new(); let mut ignores_builder = TrieBuilder::new(); let mut uses_builder = TrieBuilder::new(); @@ -213,6 +262,7 @@ impl<'a> Index<'a> { let mut dag = graph::Dag::new(cfg.targets.len()); cfg.targets.iter().enumerate().try_for_each(|(i, target)| { + target2index.insert(target.path.as_str(), i); targets.push(target.path.to_owned()); let target_path_str = target.path.as_str(); file::contains_file(&work_path.join(target_path_str))?; @@ -293,6 +343,7 @@ impl<'a> Index<'a> { targets.sort(); Ok(Self { targets, + target2index, targets_trie, ignores: ignores_builder.build(), uses: uses_builder.build(), @@ -301,6 +352,11 @@ impl<'a> Index<'a> { dag, }) } + pub(crate) fn get_target_index(&self, target: &str) -> Result<&usize, MonorailError> { + self.target2index + .get(target) + .ok_or(MonorailError::from("Target not found")) + } } #[cfg(test)]