From 439c96b823b763d0ec12a07f74c2b422c070b132 Mon Sep 17 00:00:00 2001 From: Brendan Hansknecht Date: Mon, 14 Sep 2020 16:34:50 -0700 Subject: [PATCH] Split roc_cli into binary and lib Splitting into binary and lib enables using the lib in tests. main.rs is now a thin wrapper around the lib. In the future, the exact code split should potentialy be changed so that main.rs does all arg parsing and then calls into the lib with fully unwrapped parameters. --- cli/src/lib.rs | 225 ++++++++++++++++++++++++++++++++++++++++++ cli/src/main.rs | 227 +------------------------------------------ cli/tests/helpers.rs | 10 +- 3 files changed, 229 insertions(+), 233 deletions(-) create mode 100644 cli/src/lib.rs diff --git a/cli/src/lib.rs b/cli/src/lib.rs new file mode 100644 index 0000000000..ce97bc90f3 --- /dev/null +++ b/cli/src/lib.rs @@ -0,0 +1,225 @@ +#[macro_use] +extern crate clap; + +use bumpalo::Bump; +use clap::ArgMatches; +use clap::{App, Arg}; +use roc_build::program::gen; +use roc_collections::all::MutMap; +use roc_gen::llvm::build::OptLevel; +use roc_load::file::LoadingProblem; +use std::io::{self, ErrorKind}; +use std::path::{Path, PathBuf}; +use std::process; +use std::process::Command; +use std::time::{Duration, SystemTime}; +use target_lexicon::Triple; + +pub mod repl; + +pub static FLAG_OPTIMIZE: &str = "optimize"; +pub static FLAG_ROC_FILE: &str = "ROC_FILE"; +pub static DIRECTORY_OR_FILES: &str = "DIRECTORY_OR_FILES"; + +pub fn build_app<'a>() -> App<'a> { + App::new("roc") + .version(crate_version!()) + .subcommand(App::new("build") + .about("Build a program") + .arg( + Arg::with_name(FLAG_ROC_FILE) + .help("The .roc file to build") + .required(true), + ) + .arg( + Arg::with_name(FLAG_OPTIMIZE) + .long(FLAG_OPTIMIZE) + .help("Optimize the compiled program to run faster. (Optimization takes time to complete.)") + .required(false), + ) + ) + .subcommand(App::new("run") + .about("Build and run a program") + .arg( + Arg::with_name(FLAG_ROC_FILE) + .help("The .roc file to build and run") + .required(true), + ) + .arg( + Arg::with_name(FLAG_OPTIMIZE) + .long(FLAG_OPTIMIZE) + .help("Optimize the compiled program to run faster. (Optimization takes time to complete.)") + .required(false), + ) + ) + .subcommand(App::new("repl") + .about("Launch the interactive Read Eval Print Loop (REPL)") + ) + .subcommand(App::new("edit") + .about("Launch the Roc editor") + .arg(Arg::with_name(DIRECTORY_OR_FILES) + .index(1) + .multiple(true) + .required(false) + .help("(optional) The directory or files to open on launch.") + ) + ) +} + +pub fn build(matches: &ArgMatches, run_after_build: bool) -> io::Result<()> { + let filename = matches.value_of(FLAG_ROC_FILE).unwrap(); + let opt_level = if matches.is_present(FLAG_OPTIMIZE) { + OptLevel::Optimize + } else { + OptLevel::Normal + }; + let path = Path::new(filename).canonicalize().unwrap(); + let src_dir = path.parent().unwrap().canonicalize().unwrap(); + + // Spawn the root task + let path = path.canonicalize().unwrap_or_else(|err| { + use ErrorKind::*; + + match err.kind() { + NotFound => { + match path.to_str() { + Some(path_str) => println!("File not found: {}", path_str), + None => println!("Malformed file path : {:?}", path), + } + + process::exit(1); + } + _ => { + todo!("TODO Gracefully handle opening {:?} - {:?}", path, err); + } + } + }); + + let binary_path = + build_file(src_dir, path, opt_level).expect("TODO gracefully handle build_file failing"); + + 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(()) +} + +fn report_timing(buf: &mut String, label: &str, duration: Duration) { + buf.push_str(&format!( + " {:.3} ms {}\n", + duration.as_secs_f64() * 1000.0, + label, + )); +} + +fn build_file( + src_dir: PathBuf, + filename: PathBuf, + opt_level: OptLevel, +) -> Result { + let compilation_start = SystemTime::now(); + let arena = Bump::new(); + + // Step 1: compile the app and generate the .o file + let subs_by_module = MutMap::default(); + + // Release builds use uniqueness optimizations + let stdlib = match opt_level { + OptLevel::Normal => roc_builtins::std::standard_stdlib(), + OptLevel::Optimize => roc_builtins::unique::uniq_stdlib(), + }; + let loaded = + roc_load::file::load(filename.clone(), &stdlib, src_dir.as_path(), subs_by_module)?; + let dest_filename = filename.with_extension("o"); + + let buf = &mut String::with_capacity(1024); + + for (module_id, module_timing) in loaded.timings.iter() { + let module_name = loaded.interns.module_name(*module_id); + + buf.push_str(" "); + buf.push_str(module_name); + buf.push_str("\n"); + + report_timing(buf, "Read .roc file from disk", module_timing.read_roc_file); + report_timing(buf, "Parse header", module_timing.parse_header); + report_timing(buf, "Parse body", module_timing.parse_body); + report_timing(buf, "Canonicalize", module_timing.canonicalize); + report_timing(buf, "Constrain", module_timing.constrain); + report_timing(buf, "Solve", module_timing.solve); + report_timing(buf, "Other", module_timing.other()); + buf.push('\n'); + report_timing(buf, "Total", module_timing.total()); + } + + println!( + "\n\nCompilation finished! Here's how long each module took to compile:\n\n{}", + buf + ); + + gen( + &arena, + loaded, + filename, + Triple::host(), + &dest_filename, + opt_level, + ); + + let compilation_end = compilation_start.elapsed().unwrap(); + + println!( + "Finished compilation and code gen in {} ms\n", + compilation_end.as_millis() + ); + + let cwd = dest_filename.parent().unwrap(); + let lib_path = dest_filename.with_file_name("libroc_app.a"); + + // Step 2: turn the .o file into a .a static library + Command::new("ar") // TODO on Windows, use `link` + .args(&[ + "rcs", + lib_path.to_str().unwrap(), + dest_filename.to_str().unwrap(), + ]) + .spawn() + .map_err(|_| { + todo!("gracefully handle `ar` failing to spawn."); + })? + .wait() + .map_err(|_| { + todo!("gracefully handle error after `ar` spawned"); + })?; + + // Step 3: have rustc compile the host and link in the .a file + let binary_path = cwd.join("app"); + + Command::new("rustc") + .args(&[ + "-L", + ".", + "--crate-type", + "bin", + "host.rs", + "-o", + binary_path.as_path().to_str().unwrap(), + ]) + .current_dir(cwd) + .spawn() + .map_err(|_| { + todo!("gracefully handle `rustc` failing to spawn."); + })? + .wait() + .map_err(|_| { + todo!("gracefully handle error after `rustc` spawned"); + })?; + + Ok(binary_path) +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 73ee5903a1..dd92325b72 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,69 +1,6 @@ -#[macro_use] -extern crate clap; - -use bumpalo::Bump; -use clap::{App, Arg, ArgMatches}; -use roc_build::program::gen; -use roc_collections::all::MutMap; -use roc_gen::llvm::build::OptLevel; -use roc_load::file::LoadingProblem; -use std::io::{self, ErrorKind}; -use std::path::{Path, PathBuf}; -use std::process; -use std::process::Command; -use std::time::{Duration, SystemTime}; -use target_lexicon::Triple; - -pub mod repl; - -pub static FLAG_OPTIMIZE: &str = "optimize"; -pub static FLAG_ROC_FILE: &str = "ROC_FILE"; -pub static DIRECTORY_OR_FILES: &str = "DIRECTORY_OR_FILES"; - -pub fn build_app<'a>() -> App<'a> { - App::new("roc") - .version(crate_version!()) - .subcommand(App::new("build") - .about("Build a program") - .arg( - Arg::with_name(FLAG_ROC_FILE) - .help("The .roc file to build") - .required(true), - ) - .arg( - Arg::with_name(FLAG_OPTIMIZE) - .long(FLAG_OPTIMIZE) - .help("Optimize the compiled program to run faster. (Optimization takes time to complete.)") - .required(false), - ) - ) - .subcommand(App::new("run") - .about("Build and run a program") - .arg( - Arg::with_name(FLAG_ROC_FILE) - .help("The .roc file to build and run") - .required(true), - ) - .arg( - Arg::with_name(FLAG_OPTIMIZE) - .long(FLAG_OPTIMIZE) - .help("Optimize the compiled program to run faster. (Optimization takes time to complete.)") - .required(false), - ) - ) - .subcommand(App::new("repl") - .about("Launch the interactive Read Eval Print Loop (REPL)") - ) - .subcommand(App::new("edit") - .about("Launch the Roc editor") - .arg(Arg::with_name(DIRECTORY_OR_FILES) - .index(1) - .multiple(true) - .required(false) - .help("(optional) The directory or files to open on launch.") - ) - ) -} +use roc_cli::{build, build_app, repl, DIRECTORY_OR_FILES}; +use std::io; +use std::path::Path; fn main() -> io::Result<()> { let matches = build_app().get_matches(); @@ -92,161 +29,3 @@ fn main() -> io::Result<()> { _ => unreachable!(), } } - -pub fn build(matches: &ArgMatches, run_after_build: bool) -> io::Result<()> { - let filename = matches.value_of(FLAG_ROC_FILE).unwrap(); - let opt_level = if matches.is_present(FLAG_OPTIMIZE) { - OptLevel::Optimize - } else { - OptLevel::Normal - }; - let path = Path::new(filename).canonicalize().unwrap(); - let src_dir = path.parent().unwrap().canonicalize().unwrap(); - - // Spawn the root task - let path = path.canonicalize().unwrap_or_else(|err| { - use ErrorKind::*; - - match err.kind() { - NotFound => { - match path.to_str() { - Some(path_str) => println!("File not found: {}", path_str), - None => println!("Malformed file path : {:?}", path), - } - - process::exit(1); - } - _ => { - todo!("TODO Gracefully handle opening {:?} - {:?}", path, err); - } - } - }); - - let binary_path = - build_file(src_dir, path, opt_level).expect("TODO gracefully handle build_file failing"); - - 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(()) -} - -fn report_timing(buf: &mut String, label: &str, duration: Duration) { - buf.push_str(&format!( - " {:.3} ms {}\n", - duration.as_secs_f64() * 1000.0, - label, - )); -} - -fn build_file( - src_dir: PathBuf, - filename: PathBuf, - opt_level: OptLevel, -) -> Result { - let compilation_start = SystemTime::now(); - let arena = Bump::new(); - - // Step 1: compile the app and generate the .o file - let subs_by_module = MutMap::default(); - - // Release builds use uniqueness optimizations - let stdlib = match opt_level { - OptLevel::Normal => roc_builtins::std::standard_stdlib(), - OptLevel::Optimize => roc_builtins::unique::uniq_stdlib(), - }; - let loaded = - roc_load::file::load(filename.clone(), &stdlib, src_dir.as_path(), subs_by_module)?; - let dest_filename = filename.with_extension("o"); - - let buf = &mut String::with_capacity(1024); - - for (module_id, module_timing) in loaded.timings.iter() { - let module_name = loaded.interns.module_name(*module_id); - - buf.push_str(" "); - buf.push_str(module_name); - buf.push_str("\n"); - - report_timing(buf, "Read .roc file from disk", module_timing.read_roc_file); - report_timing(buf, "Parse header", module_timing.parse_header); - report_timing(buf, "Parse body", module_timing.parse_body); - report_timing(buf, "Canonicalize", module_timing.canonicalize); - report_timing(buf, "Constrain", module_timing.constrain); - report_timing(buf, "Solve", module_timing.solve); - report_timing(buf, "Other", module_timing.other()); - buf.push('\n'); - report_timing(buf, "Total", module_timing.total()); - } - - println!( - "\n\nCompilation finished! Here's how long each module took to compile:\n\n{}", - buf - ); - - gen( - &arena, - loaded, - filename, - Triple::host(), - &dest_filename, - opt_level, - ); - - let compilation_end = compilation_start.elapsed().unwrap(); - - println!( - "Finished compilation and code gen in {} ms\n", - compilation_end.as_millis() - ); - - let cwd = dest_filename.parent().unwrap(); - let lib_path = dest_filename.with_file_name("libroc_app.a"); - - // Step 2: turn the .o file into a .a static library - Command::new("ar") // TODO on Windows, use `link` - .args(&[ - "rcs", - lib_path.to_str().unwrap(), - dest_filename.to_str().unwrap(), - ]) - .spawn() - .map_err(|_| { - todo!("gracefully handle `ar` failing to spawn."); - })? - .wait() - .map_err(|_| { - todo!("gracefully handle error after `ar` spawned"); - })?; - - // Step 3: have rustc compile the host and link in the .a file - let binary_path = cwd.join("app"); - - Command::new("rustc") - .args(&[ - "-L", - ".", - "--crate-type", - "bin", - "host.rs", - "-o", - binary_path.as_path().to_str().unwrap(), - ]) - .current_dir(cwd) - .spawn() - .map_err(|_| { - todo!("gracefully handle `rustc` failing to spawn."); - })? - .wait() - .map_err(|_| { - todo!("gracefully handle error after `rustc` spawned"); - })?; - - Ok(binary_path) -} diff --git a/cli/tests/helpers.rs b/cli/tests/helpers.rs index a6dbca4082..f0b2bf6a2a 100644 --- a/cli/tests/helpers.rs +++ b/cli/tests/helpers.rs @@ -3,8 +3,8 @@ extern crate inlinable_string; extern crate roc_collections; extern crate roc_load; extern crate roc_module; -// extern crate roc_cli; // TODO FIXME why doesn't this resolve? +use roc_cli::repl::{INSTRUCTIONS, PROMPT, WELCOME_MESSAGE}; use std::env; use std::io::Write; use std::path::PathBuf; @@ -16,12 +16,6 @@ pub struct Out { pub status: ExitStatus, } -// TODO get these from roc_cli::repl instead, after figuring out why -// `extern crate roc_cli;` doesn't work. -const WELCOME_MESSAGE: &str = "\n The rockin’ \u{001b}[36mroc repl\u{001b}[0m\n\u{001b}[35m────────────────────────\u{001b}[0m\n\n"; -const INSTRUCTIONS: &str = "Enter an expression, or :help, or :exit.\n"; -const PROMPT: &str = "\n\u{001b}[36m»\u{001b}[0m "; - pub fn path_to_roc_binary() -> PathBuf { // Adapted from https://github.com/volta-cli/volta/blob/cefdf7436a15af3ce3a38b8fe53bb0cfdb37d3dd/tests/acceptance/support/sandbox.rs#L680 - BSD-2-Clause licensed let mut path = env::var_os("CARGO_BIN_PATH") @@ -129,8 +123,6 @@ pub fn repl_eval(input: &str) -> Out { // Remove the initial instructions from the output. - // TODO get these from roc_cli::repl instead, after figuring out why - // `extern crate roc_cli;` doesn't work. let expected_instructions = format!("{}{}{}", WELCOME_MESSAGE, INSTRUCTIONS, PROMPT); let stdout = String::from_utf8(output.stdout).unwrap();