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.
This commit is contained in:
Brendan Hansknecht 2020-09-14 16:34:50 -07:00
parent 099d9e35f6
commit 439c96b823
3 changed files with 229 additions and 233 deletions

225
cli/src/lib.rs Normal file
View file

@ -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<PathBuf, LoadingProblem> {
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)
}

View file

@ -1,69 +1,6 @@
#[macro_use] use roc_cli::{build, build_app, repl, DIRECTORY_OR_FILES};
extern crate clap; use std::io;
use std::path::Path;
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.")
)
)
}
fn main() -> io::Result<()> { fn main() -> io::Result<()> {
let matches = build_app().get_matches(); let matches = build_app().get_matches();
@ -92,161 +29,3 @@ fn main() -> io::Result<()> {
_ => unreachable!(), _ => 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<PathBuf, LoadingProblem> {
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)
}

View file

@ -3,8 +3,8 @@ extern crate inlinable_string;
extern crate roc_collections; extern crate roc_collections;
extern crate roc_load; extern crate roc_load;
extern crate roc_module; 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::env;
use std::io::Write; use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
@ -16,12 +16,6 @@ pub struct Out {
pub status: ExitStatus, 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 { 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 // 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") 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. // 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 expected_instructions = format!("{}{}{}", WELCOME_MESSAGE, INSTRUCTIONS, PROMPT);
let stdout = String::from_utf8(output.stdout).unwrap(); let stdout = String::from_utf8(output.stdout).unwrap();