diff --git a/cli/src/build.rs b/cli/src/build.rs index 78a149888c..1ff3272ba1 100644 --- a/cli/src/build.rs +++ b/cli/src/build.rs @@ -7,7 +7,6 @@ use roc_can::builtins::builtin_defs_map; use roc_collections::all::MutMap; use roc_gen::llvm::build::OptLevel; use roc_load::file::LoadingProblem; -use std::env; use std::path::PathBuf; use std::time::{Duration, SystemTime}; use target_lexicon::Triple; @@ -21,6 +20,18 @@ fn report_timing(buf: &mut String, label: &str, duration: Duration) { )); } +pub enum BuildOutcome { + NoProblems, + OnlyWarnings, + Errors, +} + +pub struct BuiltFile { + pub binary_path: PathBuf, + pub outcome: BuildOutcome, + pub total_time: Duration, +} + pub fn build_file<'a>( arena: &'a Bump, target: &Triple, @@ -29,7 +40,7 @@ pub fn build_file<'a>( opt_level: OptLevel, emit_debug_info: bool, link_type: LinkType, -) -> Result> { +) -> Result> { let compilation_start = SystemTime::now(); let ptr_bytes = target.pointer_width().unwrap().bytes() as u32; @@ -184,26 +195,23 @@ pub fn build_file<'a>( todo!("gracefully handle error after `rustc` spawned"); }); - let link_end = link_start.elapsed().unwrap(); + let linking_time = link_start.elapsed().unwrap(); + if emit_debug_info { - println!("Finished linking in {} ms\n", link_end.as_millis()); + println!("Finished linking in {} ms\n", linking_time.as_millis()); } + let total_time = compilation_start.elapsed().unwrap(); + // If the cmd errored out, return the Err. cmd_result?; - // If possible, report the generated executable name relative to the current dir. - let generated_filename = binary_path - .strip_prefix(env::current_dir().unwrap()) - .unwrap_or(&binary_path); + // TODO change this to report whether there were errors or warnings! + let outcome = BuildOutcome::NoProblems; - let total_end = compilation_start.elapsed().unwrap(); - - println!( - "🎉 Built {} in {} ms", - generated_filename.to_str().unwrap(), - total_end.as_millis() - ); - - Ok(binary_path) + Ok(BuiltFile { + binary_path, + total_time, + outcome, + }) } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index c15257487b..f5dfe3f999 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -1,12 +1,13 @@ #[macro_use] extern crate clap; +use build::{build_file, BuildOutcome, BuiltFile}; use bumpalo::Bump; -use clap::ArgMatches; -use clap::{App, Arg}; +use clap::{App, AppSettings, Arg, ArgMatches}; use roc_build::link::LinkType; use roc_gen::llvm::build::OptLevel; use roc_load::file::LoadingProblem; +use std::env; use std::io; use std::path::{Path, PathBuf}; use std::process; @@ -16,18 +17,25 @@ use target_lexicon::Triple; pub mod build; pub mod repl; -pub static FLAG_DEBUG: &str = "debug"; -pub static FLAG_OPTIMIZE: &str = "optimize"; -pub static FLAG_ROC_FILE: &str = "ROC_FILE"; -pub static DIRECTORY_OR_FILES: &str = "DIRECTORY_OR_FILES"; +pub const CMD_RUN: &str = "run"; +pub const CMD_BUILD: &str = "build"; +pub const CMD_REPL: &str = "repl"; +pub const CMD_EDIT: &str = "edit"; +pub const CMD_DOCS: &str = "docs"; + +pub const FLAG_DEBUG: &str = "debug"; +pub const FLAG_OPTIMIZE: &str = "optimize"; +pub const ROC_FILE: &str = "ROC_FILE"; +pub const DIRECTORY_OR_FILES: &str = "DIRECTORY_OR_FILES"; +pub const ARGS_FOR_APP: &str = "ARGS_FOR_APP"; pub fn build_app<'a>() -> App<'a> { App::new("roc") .version(crate_version!()) - .subcommand(App::new("build") + .subcommand(App::new(CMD_BUILD) .about("Build a program") .arg( - Arg::with_name(FLAG_ROC_FILE) + Arg::with_name(ROC_FILE) .help("The .roc file to build") .required(true), ) @@ -44,13 +52,9 @@ pub fn build_app<'a>() -> App<'a> { .required(false), ) ) - .subcommand(App::new("run") + .subcommand(App::new(CMD_RUN) .about("Build and run a program") - .arg( - Arg::with_name(FLAG_ROC_FILE) - .help("The .roc file to build and run") - .required(true), - ) + .setting(AppSettings::TrailingVarArg) .arg( Arg::with_name(FLAG_OPTIMIZE) .long(FLAG_OPTIMIZE) @@ -63,11 +67,21 @@ pub fn build_app<'a>() -> App<'a> { .help("Store LLVM debug information in the generated program") .required(false), ) + .arg( + Arg::with_name(ROC_FILE) + .help("The .roc file of an app to build and run") + .required(true), + ) + .arg( + Arg::with_name(ARGS_FOR_APP) + .help("Arguments to pass into the app being run") + .multiple(true), + ) ) - .subcommand(App::new("repl") + .subcommand(App::new(CMD_REPL) .about("Launch the interactive Read Eval Print Loop (REPL)") ) - .subcommand(App::new("edit") + .subcommand(App::new(CMD_EDIT) .about("Launch the Roc editor") .arg(Arg::with_name(DIRECTORY_OR_FILES) .index(1) @@ -77,7 +91,7 @@ pub fn build_app<'a>() -> App<'a> { ) ) .subcommand( - App::new("docs") + App::new(CMD_DOCS) .about("Generate documentation for Roc modules") .arg(Arg::with_name(DIRECTORY_OR_FILES) .index(1) @@ -97,10 +111,18 @@ pub fn docs(files: Vec) { ) } -pub fn build(target: &Triple, matches: &ArgMatches, run_after_build: bool) -> io::Result<()> { - let arena = Bump::new(); - let filename = matches.value_of(FLAG_ROC_FILE).unwrap(); +pub enum BuildConfig { + BuildOnly, + BuildAndRun { roc_file_arg_index: usize }, +} +pub fn build(target: &Triple, matches: &ArgMatches, config: BuildConfig) -> io::Result { + use BuildConfig::*; + + let arena = Bump::new(); + let filename = matches.value_of(ROC_FILE).unwrap(); + + let original_cwd = std::env::current_dir()?; let opt_level = if matches.is_present(FLAG_OPTIMIZE) { OptLevel::Optimize } else { @@ -130,7 +152,7 @@ pub fn build(target: &Triple, matches: &ArgMatches, run_after_build: bool) -> io } }); - let res_binary_path = build::build_file( + let res_binary_path = build_file( &arena, target, src_dir, @@ -141,23 +163,77 @@ pub fn build(target: &Triple, matches: &ArgMatches, run_after_build: bool) -> io ); match res_binary_path { - Ok(binary_path) => { - if run_after_build { - // Run the compiled app - Command::new(binary_path) - .spawn() - .unwrap_or_else(|err| panic!("Failed to run app after building it: {:?}", err)) - .wait() - .expect("TODO gracefully handle block_on failing"); + Ok(BuiltFile { + binary_path, + outcome, + total_time, + }) => { + match config { + BuildOnly => { + // If possible, report the generated executable name relative to the current dir. + let generated_filename = binary_path + .strip_prefix(env::current_dir().unwrap()) + .unwrap_or(&binary_path); + + println!( + "🎉 Built {} in {} ms", + generated_filename.to_str().unwrap(), + total_time.as_millis() + ); + + // Return a nonzero exit code if there were problems + let status_code = match outcome { + BuildOutcome::NoProblems => 0, + BuildOutcome::OnlyWarnings => 1, + BuildOutcome::Errors => 2, + }; + + Ok(status_code) + } + BuildAndRun { roc_file_arg_index } => { + let mut cmd = Command::new(binary_path); + + // Forward all the arguments after the .roc file argument + // to the new process. This way, you can do things like: + // + // roc run app.roc foo bar baz + // + // ...and have it so that app.roc will receive only `foo`, + // `bar`, and `baz` as its arguments. + for (index, arg) in std::env::args().enumerate() { + if index > roc_file_arg_index { + cmd.arg(arg); + } + } + + // Run the compiled app + let exit_status = cmd + .current_dir(original_cwd) + .spawn() + .unwrap_or_else(|err| panic!("Failed to run app after building it: {:?}", err)) + .wait() + .expect("TODO gracefully handle block_on failing when roc run spawns a subprocess for the compiled app"); + + // `roc run` 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 run subprocess terminating with a signal."); + } + } + } } } Err(LoadingProblem::FormattedReport(report)) => { print!("{}", report); + + Ok(1) } Err(other) => { panic!("build_file failed with error:\n{:?}", other); } } - - Ok(()) } diff --git a/cli/src/main.rs b/cli/src/main.rs index fca8e3924b..191e8409b0 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,4 +1,7 @@ -use roc_cli::{build, build_app, docs, repl, DIRECTORY_OR_FILES}; +use roc_cli::{ + build, build_app, docs, repl, BuildConfig, CMD_BUILD, CMD_DOCS, CMD_EDIT, CMD_REPL, CMD_RUN, + DIRECTORY_OR_FILES, ROC_FILE, +}; use std::io; use std::path::{Path, PathBuf}; use target_lexicon::Triple; @@ -6,38 +9,58 @@ use target_lexicon::Triple; fn main() -> io::Result<()> { let matches = build_app().get_matches(); - match matches.subcommand_name() { - None => roc_editor::launch(&[]), - Some("build") => build( + let exit_code = match matches.subcommand_name() { + None => { + roc_editor::launch(&[])?; + + // rustc couldn't infer the error type here + Result::::Ok(0) + } + Some(CMD_BUILD) => Ok(build( &Triple::host(), - matches.subcommand_matches("build").unwrap(), - false, - ), - Some("run") => build( - &Triple::host(), - matches.subcommand_matches("run").unwrap(), - true, - ), - Some("repl") => repl::main(), - Some("edit") => { + matches.subcommand_matches(CMD_BUILD).unwrap(), + BuildConfig::BuildOnly, + )?), + Some(CMD_RUN) => { + let subcmd_matches = matches.subcommand_matches(CMD_RUN).unwrap(); + let roc_file_arg_index = subcmd_matches.index_of(ROC_FILE).unwrap() + 1; // Not sure why this +1 is necessary, but it is! + + Ok(build( + &Triple::host(), + subcmd_matches, + BuildConfig::BuildAndRun { roc_file_arg_index }, + )?) + } + Some(CMD_REPL) => { + repl::main()?; + + // Exit 0 if the repl exited normally + Ok(0) + } + Some(CMD_EDIT) => { match matches - .subcommand_matches("edit") + .subcommand_matches(CMD_EDIT) .unwrap() .values_of_os(DIRECTORY_OR_FILES) { - None => roc_editor::launch(&[]), + None => { + roc_editor::launch(&[])?; + } Some(values) => { let paths = values .map(|os_str| Path::new(os_str)) .collect::>(); - roc_editor::launch(&paths) + roc_editor::launch(&paths)?; } } + + // Exit 0 if the editor exited normally + Ok(0) } - Some("docs") => { + Some(CMD_DOCS) => { let values = matches - .subcommand_matches("docs") + .subcommand_matches(CMD_DOCS) .unwrap() .values_of_os(DIRECTORY_OR_FILES) .unwrap(); @@ -48,8 +71,10 @@ fn main() -> io::Result<()> { docs(paths); - Ok(()) + Ok(0) } _ => unreachable!(), - } + }?; + + std::process::exit(exit_code); } diff --git a/examples/cli/platform/src/lib.rs b/examples/cli/platform/src/lib.rs index 778563395a..de86867df1 100644 --- a/examples/cli/platform/src/lib.rs +++ b/examples/cli/platform/src/lib.rs @@ -2,7 +2,6 @@ use roc_std::{alloca, RocCallResult, RocResult, RocStr}; use std::alloc::Layout; -use std::time::SystemTime; extern "C" { #[link_name = "roc__mainForHost_1_exposed"]