Pass args from clap to roc/roc run child processes

This works much better than the previous "arg index"
approach, especially with subcommands like `roc run`
This commit is contained in:
Richard Feldman 2022-05-06 17:06:47 -04:00
parent e3865a3679
commit 81bdc507f5
No known key found for this signature in database
GPG key ID: 7E4127D1E4241798
2 changed files with 100 additions and 85 deletions

View file

@ -97,8 +97,7 @@ pub fn build_app<'a>() -> Command<'a> {
let roc_file_to_run = Arg::new(ROC_FILE) let roc_file_to_run = Arg::new(ROC_FILE)
.help("The .roc file of an app to run") .help("The .roc file of an app to run")
.allow_invalid_utf8(true) .allow_invalid_utf8(true);
.required(true);
let args_for_app = Arg::new(ARGS_FOR_APP) let args_for_app = Arg::new(ARGS_FOR_APP)
.help("Arguments to pass into the app being run") .help("Arguments to pass into the app being run")
@ -159,7 +158,7 @@ pub fn build_app<'a>() -> Command<'a> {
.arg(flag_linker.clone()) .arg(flag_linker.clone())
.arg(flag_precompiled.clone()) .arg(flag_precompiled.clone())
.arg(flag_valgrind.clone()) .arg(flag_valgrind.clone())
.arg(roc_file_to_run.clone()) .arg(roc_file_to_run.clone().required(true))
.arg(args_for_app.clone()) .arg(args_for_app.clone())
) )
.subcommand(Command::new(CMD_FORMAT) .subcommand(Command::new(CMD_FORMAT)
@ -209,7 +208,7 @@ pub fn build_app<'a>() -> Command<'a> {
.arg(flag_linker) .arg(flag_linker)
.arg(flag_precompiled) .arg(flag_precompiled)
.arg(flag_valgrind) .arg(flag_valgrind)
.arg(roc_file_to_run) .arg(roc_file_to_run.required(false))
.arg(args_for_app); .arg(args_for_app);
if cfg!(feature = "editor") { if cfg!(feature = "editor") {
@ -236,8 +235,8 @@ pub fn docs(files: Vec<PathBuf>) {
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum BuildConfig { pub enum BuildConfig {
BuildOnly, BuildOnly,
BuildAndRun { roc_file_arg_index: usize }, BuildAndRun,
BuildAndRunIfNoErrors { roc_file_arg_index: usize }, BuildAndRunIfNoErrors,
} }
pub enum FormatMode { pub enum FormatMode {
@ -372,7 +371,7 @@ pub fn build(
// Return a nonzero exit code if there were problems // Return a nonzero exit code if there were problems
Ok(problems.exit_code()) Ok(problems.exit_code())
} }
BuildAndRun { roc_file_arg_index } => { BuildAndRun => {
if problems.errors > 0 || problems.warnings > 0 { if problems.errors > 0 || problems.warnings > 0 {
println!( println!(
"\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.\n\nRunning program anyway…\n\n\x1B[36m{}\x1B[39m", "\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.\n\nRunning program anyway…\n\n\x1B[36m{}\x1B[39m",
@ -403,15 +402,11 @@ pub fn build(
); );
} }
roc_run( let args = matches.values_of_os(ARGS_FOR_APP).unwrap_or_default();
arena,
&original_cwd, roc_run(arena, &original_cwd, triple, args, &binary_path)
triple,
roc_file_arg_index,
&binary_path,
)
} }
BuildAndRunIfNoErrors { roc_file_arg_index } => { BuildAndRunIfNoErrors => {
if problems.errors == 0 { if problems.errors == 0 {
if problems.warnings > 0 { if problems.warnings > 0 {
println!( println!(
@ -427,13 +422,9 @@ pub fn build(
); );
} }
roc_run( let args = matches.values_of_os(ARGS_FOR_APP).unwrap_or_default();
arena,
&original_cwd, roc_run(arena, &original_cwd, triple, args, &binary_path)
triple,
roc_file_arg_index,
&binary_path,
)
} else { } else {
println!( println!(
"\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.\n\nYou can run the program anyway with: \x1B[32mroc run {}\x1B[39m", "\x1B[{}m{}\x1B[39m {} and \x1B[{}m{}\x1B[39m {} found in {} ms.\n\nYou can run the program anyway with: \x1B[32mroc run {}\x1B[39m",
@ -479,16 +470,13 @@ pub fn build(
} }
} }
#[cfg(target_family = "unix")] fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
fn roc_run(
arena: Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it! arena: Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it!
cwd: &Path, cwd: &Path,
triple: Triple, triple: Triple,
roc_file_arg_index: usize, args: I,
binary_path: &Path, binary_path: &Path,
) -> io::Result<i32> { ) -> io::Result<i32> {
use std::os::unix::process::CommandExt;
match triple.architecture { match triple.architecture {
Architecture::Wasm32 => { Architecture::Wasm32 => {
// If possible, report the generated executable name relative to the current dir. // If possible, report the generated executable name relative to the current dir.
@ -500,19 +488,44 @@ fn roc_run(
// since the process is about to exit anyway. // since the process is about to exit anyway.
std::mem::forget(arena); std::mem::forget(arena);
let args = std::env::args() if cfg!(target_family = "unix") {
.skip(roc_file_arg_index) use std::os::unix::ffi::OsStrExt;
.collect::<Vec<_>>();
run_with_wasmer(generated_filename, &args); run_with_wasmer(
return Ok(0); generated_filename,
args.into_iter().map(|os_str| os_str.as_bytes()),
);
} else {
run_with_wasmer(
generated_filename,
args.into_iter().map(|os_str| {
os_str.to_str().expect(
"Roc does not currently support passing non-UTF8 arguments to Wasmer.",
)
}),
);
}
Ok(0)
}
_ => {
if cfg!(target_family = "unix") {
roc_run_unix(cwd, args, binary_path)
} else {
roc_run_non_unix(arena, cwd, args, binary_path)
}
} }
_ => std::process::Command::new(&binary_path),
};
if let Architecture::Wasm32 = triple.architecture {
cmd.arg(binary_path);
} }
}
fn roc_run_unix<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
cwd: &Path,
args: I,
binary_path: &Path,
) -> io::Result<i32> {
use std::os::unix::process::CommandExt;
let mut cmd = std::process::Command::new(&binary_path);
// Forward all the arguments after the .roc file argument // Forward all the arguments after the .roc file argument
// to the new process. This way, you can do things like: // to the new process. This way, you can do things like:
@ -521,10 +534,8 @@ fn roc_run(
// //
// ...and have it so that app.roc will receive only `foo`, // ...and have it so that app.roc will receive only `foo`,
// `bar`, and `baz` as its arguments. // `bar`, and `baz` as its arguments.
for (index, arg) in std::env::args().enumerate() { for arg in args {
if index > roc_file_arg_index { cmd.arg(arg);
cmd.arg(arg);
}
} }
// This is much faster than spawning a subprocess if we're on a UNIX system! // This is much faster than spawning a subprocess if we're on a UNIX system!
@ -536,29 +547,36 @@ fn roc_run(
Err(err) Err(err)
} }
#[cfg(not(target_family = "unix"))] fn roc_run_non_unix<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
fn roc_run(cmd: &mut Command) -> io::Result<i32> { _arena: Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it!
// Run the compiled app _cwd: &Path,
let exit_status = cmd _args: I,
.spawn() _binary_path: &Path,
.unwrap_or_else(|err| panic!("Failed to run app after building it: {:?}", err)) ) -> io::Result<i32> {
.wait() todo!("TODO support running roc programs on non-UNIX targets");
.expect("TODO gracefully handle block_on failing when `roc` spawns a subprocess for the compiled app"); // let mut cmd = std::process::Command::new(&binary_path);
// `roc [FILE]` exits with the same status code as the app it ran. // // Run the compiled app
// // let exit_status = cmd
// If you want to know whether there were compilation problems // .spawn()
// via status code, use either `roc build` or `roc check` instead! // .unwrap_or_else(|err| panic!("Failed to run app after building it: {:?}", err))
match exit_status.code() { // .wait()
Some(code) => Ok(code), // .expect("TODO gracefully handle block_on failing when `roc` spawns a subprocess for the compiled app");
None => {
todo!("TODO gracefully handle the `roc [FILE]` subprocess terminating with a signal."); // // `roc [FILE]` exits with the same status code as the app it ran.
} // //
} // // If you want to know whether there were compilation problems
// // via status code, use either `roc build` or `roc check` instead!
// match exit_status.code() {
// Some(code) => Ok(code),
// None => {
// todo!("TODO gracefully handle the `roc [FILE]` subprocess terminating with a signal.");
// }
// }
} }
#[cfg(feature = "run-wasm32")] #[cfg(feature = "run-wasm32")]
fn run_with_wasmer(wasm_path: &std::path::Path, args: &[String]) { fn run_with_wasmer<I: Iterator<Item = S>, S: AsRef<[u8]>>(wasm_path: &std::path::Path, args: I) {
use wasmer::{Instance, Module, Store}; use wasmer::{Instance, Module, Store};
let store = Store::default(); let store = Store::default();
@ -589,8 +607,8 @@ fn run_with_wasmer(wasm_path: &std::path::Path, args: &[String]) {
} }
#[cfg(not(feature = "run-wasm32"))] #[cfg(not(feature = "run-wasm32"))]
fn run_with_wasmer(_wasm_path: &std::path::Path, _args: &[String]) { fn run_with_wasmer<I: Iterator<Item = S>, S: AsRef<[u8]>>(_wasm_path: &std::path::Path, _args: I) {
println!("Running wasm files not support"); println!("Running wasm files is not supported on this target.");
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]

View file

@ -26,37 +26,34 @@ fn main() -> io::Result<()> {
let matches = build_app().get_matches(); let matches = build_app().get_matches();
let exit_code = match matches.subcommand() { let exit_code = match matches.subcommand() {
None => match matches.index_of(ROC_FILE) { None => {
Some(arg_index) => build( if matches.is_present(ROC_FILE) {
&matches, build(
BuildConfig::BuildAndRunIfNoErrors { &matches,
roc_file_arg_index: arg_index + 1, BuildConfig::BuildAndRunIfNoErrors,
}, Triple::host(),
Triple::host(), LinkType::Executable,
LinkType::Executable, )
), } else {
None => {
launch_editor(None)?; launch_editor(None)?;
Ok(0) Ok(0)
} }
}, }
Some((CMD_RUN, matches)) => match matches.index_of(ROC_FILE) { Some((CMD_RUN, matches)) => {
Some(arg_index) => build( if matches.is_present(ROC_FILE) {
matches, build(
BuildConfig::BuildAndRun { matches,
roc_file_arg_index: arg_index + 1, BuildConfig::BuildAndRun,
}, Triple::host(),
Triple::host(), LinkType::Executable,
LinkType::Executable, )
), } else {
None => {
eprintln!("What .roc file do you want to run? Specify it at the end of the `roc run` command."); eprintln!("What .roc file do you want to run? Specify it at the end of the `roc run` command.");
Ok(1) Ok(1)
} }
}, }
Some((CMD_BUILD, matches)) => { Some((CMD_BUILD, matches)) => {
let target: Target = matches.value_of_t(FLAG_TARGET).unwrap_or_default(); let target: Target = matches.value_of_t(FLAG_TARGET).unwrap_or_default();