Add roc run to run even if there are build errors.

This commit is contained in:
Richard Feldman 2022-04-20 13:50:00 -04:00
parent a47b3be9c0
commit 62484d3890
No known key found for this signature in database
GPG key ID: 7E4127D1E4241798
4 changed files with 141 additions and 91 deletions

View file

@ -1,7 +1,7 @@
use bumpalo::Bump;
use roc_build::{
link::{link, rebuild_host, LinkType},
program,
program::{self, Problems},
};
use roc_builtins::bitcode;
use roc_load::LoadingProblem;
@ -21,25 +21,9 @@ fn report_timing(buf: &mut String, label: &str, duration: Duration) {
));
}
pub enum BuildOutcome {
NoProblems,
OnlyWarnings,
Errors,
}
impl BuildOutcome {
pub fn status_code(&self) -> i32 {
match self {
Self::NoProblems => 0,
Self::OnlyWarnings => 1,
Self::Errors => 2,
}
}
}
pub struct BuiltFile {
pub binary_path: PathBuf,
pub outcome: BuildOutcome,
pub problems: Problems,
pub total_time: Duration,
}
@ -184,7 +168,7 @@ pub fn build_file<'a>(
// This only needs to be mutable for report_problems. This can't be done
// inside a nested scope without causing a borrow error!
let mut loaded = loaded;
program::report_problems_monomorphized(&mut loaded);
let problems = program::report_problems_monomorphized(&mut loaded);
let loaded = loaded;
let code_gen_timing = program::gen_from_mono_module(
@ -243,7 +227,7 @@ pub fn build_file<'a>(
// Step 2: link the precompiled host and compiled app
let link_start = SystemTime::now();
let outcome = if surgically_link {
let problems = if surgically_link {
roc_linker::link_preprocessed_host(target, &host_input_path, app_o_file, &binary_path)
.map_err(|err| {
todo!(
@ -251,12 +235,12 @@ pub fn build_file<'a>(
err
);
})?;
BuildOutcome::NoProblems
problems
} else if matches!(link_type, LinkType::None) {
// Just copy the object file to the output folder.
binary_path.set_extension(app_extension);
std::fs::copy(app_o_file, &binary_path).unwrap();
BuildOutcome::NoProblems
problems
} else {
let mut inputs = vec![
host_input_path.as_path().to_str().unwrap(),
@ -281,11 +265,15 @@ pub fn build_file<'a>(
todo!("gracefully handle error after `ld` spawned");
})?;
// TODO change this to report whether there were errors or warnings!
if exit_status.success() {
BuildOutcome::NoProblems
problems
} else {
BuildOutcome::Errors
let mut problems = problems;
// Add an error for `ld` failing
problems.errors += 1;
problems
}
};
let linking_time = link_start.elapsed().unwrap();
@ -298,7 +286,7 @@ pub fn build_file<'a>(
Ok(BuiltFile {
binary_path,
outcome,
problems,
total_time,
})
}
@ -350,10 +338,6 @@ fn spawn_rebuild_thread(
}
let rebuild_host_end = rebuild_host_start.elapsed().unwrap();
if !precompiled {
println!("Done!");
}
rebuild_host_end.as_millis()
})
}
@ -364,7 +348,7 @@ pub fn check_file(
src_dir: PathBuf,
roc_file_path: PathBuf,
emit_timings: bool,
) -> Result<usize, LoadingProblem> {
) -> Result<program::Problems, LoadingProblem> {
let compilation_start = SystemTime::now();
// only used for generating errors. We don't do code generation, so hardcoding should be fine

View file

@ -1,7 +1,7 @@
#[macro_use]
extern crate const_format;
use build::{BuildOutcome, BuiltFile};
use build::BuiltFile;
use bumpalo::Bump;
use clap::{App, AppSettings, Arg, ArgMatches};
use roc_build::link::LinkType;
@ -24,6 +24,7 @@ mod format;
pub use format::format;
pub const CMD_BUILD: &str = "build";
pub const CMD_RUN: &str = "run";
pub const CMD_REPL: &str = "repl";
pub const CMD_EDIT: &str = "edit";
pub const CMD_DOCS: &str = "docs";
@ -142,6 +143,14 @@ pub fn build_app<'a>() -> App<'a> {
.subcommand(App::new(CMD_REPL)
.about("Launch the interactive Read Eval Print Loop (REPL)")
)
.subcommand(App::new(CMD_RUN)
.about("Run a .roc file even if it has build errors")
.arg(
Arg::new(ROC_FILE)
.about("The .roc file of an app to run")
.required(true),
)
)
.subcommand(App::new(CMD_FORMAT)
.about("Format a .roc file using standard Roc formatting")
.arg(
@ -168,7 +177,7 @@ pub fn build_app<'a>() -> App<'a> {
)
.arg(
Arg::new(ROC_FILE)
.about("The .roc file of an app to run")
.about("The .roc file of an app to check")
.required(true),
)
)
@ -273,6 +282,7 @@ pub fn docs(files: Vec<PathBuf>) {
pub enum BuildConfig {
BuildOnly,
BuildAndRun { roc_file_arg_index: usize },
BuildAndRunIfNoErrors { roc_file_arg_index: usize },
}
pub enum FormatMode {
@ -380,7 +390,7 @@ pub fn build(matches: &ArgMatches, config: BuildConfig) -> io::Result<i32> {
match res_binary_path {
Ok(BuiltFile {
binary_path,
outcome,
problems,
total_time,
}) => {
match config {
@ -401,50 +411,26 @@ pub fn build(matches: &ArgMatches, config: BuildConfig) -> io::Result<i32> {
);
// Return a nonzero exit code if there were problems
Ok(outcome.status_code())
Ok(problems.exit_code())
}
BuildAndRun { roc_file_arg_index } => {
let mut cmd = match triple.architecture {
Architecture::Wasm32 => {
// 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);
// No need to waste time freeing this memory,
// since the process is about to exit anyway.
std::mem::forget(arena);
let args = std::env::args()
.skip(roc_file_arg_index)
.collect::<Vec<_>>();
run_with_wasmer(generated_filename, &args);
return Ok(0);
}
_ => Command::new(&binary_path),
};
if let Architecture::Wasm32 = triple.architecture {
cmd.arg(binary_path);
}
// Forward all the arguments after the .roc file argument
// to the new process. This way, you can do things like:
//
// roc 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);
}
}
match outcome {
BuildOutcome::Errors => Ok(outcome.status_code()),
_ => roc_run(cmd.current_dir(original_cwd)),
BuildAndRun { roc_file_arg_index } => roc_run(
&arena,
&original_cwd,
triple,
roc_file_arg_index,
&binary_path,
),
BuildAndRunIfNoErrors { roc_file_arg_index } => {
if problems.errors == 0 {
roc_run(
&arena,
&original_cwd,
triple,
roc_file_arg_index,
&binary_path,
)
} else {
Ok(problems.exit_code())
}
}
}
@ -461,11 +447,55 @@ pub fn build(matches: &ArgMatches, config: BuildConfig) -> io::Result<i32> {
}
#[cfg(target_family = "unix")]
fn roc_run(cmd: &mut Command) -> io::Result<i32> {
fn roc_run(
arena: &Bump,
cwd: &Path,
triple: Triple,
roc_file_arg_index: usize,
binary_path: &Path,
) -> io::Result<i32> {
use std::os::unix::process::CommandExt;
let mut cmd = match triple.architecture {
Architecture::Wasm32 => {
// 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);
// No need to waste time freeing this memory,
// since the process is about to exit anyway.
std::mem::forget(arena);
let args = std::env::args()
.skip(roc_file_arg_index)
.collect::<Vec<_>>();
run_with_wasmer(generated_filename, &args);
return Ok(0);
}
_ => Command::new(&binary_path),
};
if let Architecture::Wasm32 = triple.architecture {
cmd.arg(binary_path);
}
// Forward all the arguments after the .roc file argument
// to the new process. This way, you can do things like:
//
// roc 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);
}
}
// This is much faster than spawning a subprocess if we're on a UNIX system!
let err = cmd.exec();
let err = cmd.current_dir(cwd).exec();
// If exec actually returned, it was definitely an error! (Otherwise,
// this process would have been replaced by the other one, and we'd

View file

@ -1,7 +1,8 @@
use roc_cli::build::check_file;
use roc_cli::{
build_app, docs, format, BuildConfig, FormatMode, CMD_BUILD, CMD_CHECK, CMD_DOCS, CMD_EDIT,
CMD_FORMAT, CMD_REPL, CMD_VERSION, DIRECTORY_OR_FILES, FLAG_CHECK, FLAG_TIME, ROC_FILE,
CMD_FORMAT, CMD_REPL, CMD_RUN, CMD_VERSION, DIRECTORY_OR_FILES, FLAG_CHECK, FLAG_TIME,
ROC_FILE,
};
use roc_load::LoadingProblem;
use std::fs::{self, FileType};
@ -27,7 +28,10 @@ fn main() -> io::Result<()> {
Some(arg_index) => {
let roc_file_arg_index = arg_index + 1; // Not sure why this +1 is necessary, but it is!
build(&matches, BuildConfig::BuildAndRun { roc_file_arg_index })
build(
&matches,
BuildConfig::BuildAndRunIfNoErrors { roc_file_arg_index },
)
}
None => {
@ -37,6 +41,21 @@ fn main() -> io::Result<()> {
}
}
}
Some((CMD_RUN, matches)) => {
match matches.index_of(ROC_FILE) {
Some(arg_index) => {
let roc_file_arg_index = arg_index + 1; // Not sure why this +1 is necessary, but it is!
build(&matches, BuildConfig::BuildAndRun { roc_file_arg_index })
}
None => {
eprintln!("What .roc file do you want to run? Specify it at the end of the `roc run` command.");
Ok(1)
}
}
}
Some((CMD_BUILD, matches)) => Ok(build(matches, BuildConfig::BuildOnly)?),
Some((CMD_CHECK, matches)) => {
let arena = bumpalo::Bump::new();
@ -47,10 +66,7 @@ fn main() -> io::Result<()> {
let src_dir = roc_file_path.parent().unwrap().to_owned();
match check_file(&arena, src_dir, roc_file_path, emit_timings) {
Ok(number_of_errors) => {
let exit_code = if number_of_errors != 0 { 1 } else { 0 };
Ok(exit_code)
}
Ok(problems) => Ok(problems.exit_code()),
Err(LoadingProblem::FormattedReport(report)) => {
print!("{}", report);

View file

@ -28,7 +28,7 @@ const LLVM_VERSION: &str = "12";
// them after type checking (like Elm does) so we can complete the entire
// `roc check` process without needing to monomorphize.
/// Returns the number of problems reported.
pub fn report_problems_monomorphized(loaded: &mut MonomorphizedModule) -> usize {
pub fn report_problems_monomorphized(loaded: &mut MonomorphizedModule) -> Problems {
report_problems_help(
loaded.total_problems(),
&loaded.sources,
@ -39,7 +39,7 @@ pub fn report_problems_monomorphized(loaded: &mut MonomorphizedModule) -> usize
)
}
pub fn report_problems_typechecked(loaded: &mut LoadedModule) -> usize {
pub fn report_problems_typechecked(loaded: &mut LoadedModule) -> Problems {
report_problems_help(
loaded.total_problems(),
&loaded.sources,
@ -50,6 +50,23 @@ pub fn report_problems_typechecked(loaded: &mut LoadedModule) -> usize {
)
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Problems {
pub errors: usize,
pub warnings: usize,
}
impl Problems {
pub fn exit_code(&self) -> i32 {
// 0 means no problems, 1 means errors, 2 means warnings
if self.errors > 0 {
1
} else {
self.warnings.min(1) as i32
}
}
}
fn report_problems_help(
total_problems: usize,
sources: &MutMap<ModuleId, (PathBuf, Box<str>)>,
@ -57,7 +74,7 @@ fn report_problems_help(
can_problems: &mut MutMap<ModuleId, Vec<roc_problem::can::Problem>>,
type_problems: &mut MutMap<ModuleId, Vec<roc_solve::solve::TypeError>>,
mono_problems: &mut MutMap<ModuleId, Vec<roc_mono::ir::MonoProblem>>,
) -> usize {
) -> Problems {
use roc_reporting::report::{
can_problem, mono_problem, type_problem, Report, RocDocAllocator, Severity::*,
DEFAULT_PALETTE,
@ -144,13 +161,13 @@ fn report_problems_help(
if errors.is_empty() {
problems_reported = warnings.len();
for warning in warnings {
for warning in warnings.iter() {
println!("\n{}\n", warning);
}
} else {
problems_reported = errors.len();
for error in errors {
for error in errors.iter() {
println!("\n{}\n", error);
}
}
@ -165,7 +182,10 @@ fn report_problems_help(
println!("{}\u{001B}[0m\n", Report::horizontal_rule(&palette));
}
problems_reported
Problems {
errors: errors.len(),
warnings: warnings.len(),
}
}
#[cfg(not(feature = "llvm"))]