roc/crates/cli/src/lib.rs

1458 lines
54 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Provides the core CLI functionality for the Roc binary.
#[macro_use]
extern crate const_format;
use bumpalo::Bump;
use clap::{
builder::PossibleValuesParser, parser::ValueSource, value_parser, Arg, ArgAction, ArgMatches,
Command,
};
use roc_build::link::{LinkType, LinkingStrategy};
use roc_build::program::{
handle_error_module, handle_loading_problem, standard_load_config, BuildFileError,
BuildOrdering, BuiltFile, CodeGenBackend, CodeGenOptions, DEFAULT_ROC_FILENAME,
};
#[cfg(not(windows))]
use roc_collections::MutMap;
use roc_error_macros::{internal_error, user_error};
use roc_gen_dev::AssemblyBackendMode;
use roc_gen_llvm::llvm::build::LlvmBackendMode;
use roc_load::{ExpectMetadata, Threading};
#[cfg(not(windows))]
use roc_module::symbol::ModuleId;
use roc_mono::ir::OptLevel;
use roc_packaging::cache::RocCacheDir;
use roc_packaging::tarball::Compression;
#[cfg(not(windows))]
use roc_reporting::report::ANSI_STYLE_CODES;
use roc_target::{Architecture, Target};
use std::env;
use std::ffi::{CString, OsStr, OsString};
use std::io;
use std::mem::ManuallyDrop;
use std::os::raw::{c_char, c_int};
use std::path::{Path, PathBuf};
use std::process;
#[cfg(not(windows))]
use std::time::Duration;
use std::time::Instant;
use strum::IntoEnumIterator;
#[cfg(not(target_os = "linux"))]
use tempfile::TempDir;
mod format;
pub use format::{format_files, format_src, FormatMode};
pub const CMD_BUILD: &str = "build";
pub const CMD_RUN: &str = "run";
pub const CMD_DEV: &str = "dev";
pub const CMD_REPL: &str = "repl";
pub const CMD_DOCS: &str = "docs";
pub const CMD_CHECK: &str = "check";
pub const CMD_VERSION: &str = "version";
pub const CMD_FORMAT: &str = "format";
pub const CMD_TEST: &str = "test";
pub const CMD_GLUE: &str = "glue";
pub const CMD_PREPROCESS_HOST: &str = "preprocess-host";
pub const FLAG_EMIT_LLVM_IR: &str = "emit-llvm-ir";
pub const FLAG_PROFILING: &str = "profiling";
pub const FLAG_BUNDLE: &str = "bundle";
pub const FLAG_DEV: &str = "dev";
pub const FLAG_OPTIMIZE: &str = "optimize";
pub const FLAG_MAX_THREADS: &str = "max-threads";
pub const FLAG_OPT_SIZE: &str = "opt-size";
pub const FLAG_LIB: &str = "lib";
pub const FLAG_NO_LINK: &str = "no-link";
pub const FLAG_TARGET: &str = "target";
pub const FLAG_TIME: &str = "time";
pub const FLAG_VERBOSE: &str = "verbose";
pub const FLAG_NO_COLOR: &str = "no-color";
pub const FLAG_NO_HEADER: &str = "no-header";
pub const FLAG_LINKER: &str = "linker";
pub const FLAG_BUILD_HOST: &str = "build-host";
pub const FLAG_SUPPRESS_BUILD_HOST_WARNING: &str = "suppress-build-host-warning";
pub const FLAG_CHECK: &str = "check";
pub const FLAG_STDIN: &str = "stdin";
pub const FLAG_STDOUT: &str = "stdout";
pub const FLAG_WASM_STACK_SIZE_KB: &str = "wasm-stack-size-kb";
pub const FLAG_OUTPUT: &str = "output";
pub const FLAG_FUZZ: &str = "fuzz";
pub const FLAG_MAIN: &str = "main";
pub const ROC_FILE: &str = "ROC_FILE";
pub const ROC_DIR: &str = "ROC_DIR";
pub const GLUE_DIR: &str = "GLUE_DIR";
pub const GLUE_SPEC: &str = "GLUE_SPEC";
pub const DIRECTORY_OR_FILES: &str = "DIRECTORY_OR_FILES";
pub const ARGS_FOR_APP: &str = "ARGS_FOR_APP";
pub const FLAG_PP_HOST: &str = "host";
pub const FLAG_PP_PLATFORM: &str = "platform";
pub const FLAG_PP_DYLIB: &str = "lib";
pub const VERSION: &str = env!("ROC_VERSION");
const DEFAULT_GENERATED_DOCS_DIR: &str = "generated-docs";
pub fn build_app() -> Command {
let flag_optimize = Arg::new(FLAG_OPTIMIZE)
.long(FLAG_OPTIMIZE)
.help("Optimize the compiled program to run faster\n(Optimization takes time to complete.)")
.action(ArgAction::SetTrue)
.required(false);
let flag_max_threads = Arg::new(FLAG_MAX_THREADS)
.long(FLAG_MAX_THREADS)
.help("Limit the number of threads (and hence cores) used during compilation")
.value_parser(value_parser!(usize))
.required(false);
let flag_opt_size = Arg::new(FLAG_OPT_SIZE)
.long(FLAG_OPT_SIZE)
.help("Optimize the compiled program to have a small binary size\n(Optimization takes time to complete.)")
.action(ArgAction::SetTrue)
.required(false);
let flag_dev = Arg::new(FLAG_DEV)
.long(FLAG_DEV)
.help("Make compilation finish as soon as possible, at the expense of runtime performance")
.action(ArgAction::SetTrue)
.required(false);
let flag_emit_llvm_ir = Arg::new(FLAG_EMIT_LLVM_IR)
.long(FLAG_EMIT_LLVM_IR)
.help("Emit a `.ll` file containing the LLVM IR of the program")
.action(ArgAction::SetTrue)
.required(false);
let flag_profiling = Arg::new(FLAG_PROFILING)
.long(FLAG_PROFILING)
.help("Keep debug info in the final generated program even in optimized builds")
.action(ArgAction::SetTrue)
.required(false);
let flag_time = Arg::new(FLAG_TIME)
.long(FLAG_TIME)
.help("Print detailed compilation time information")
.action(ArgAction::SetTrue)
.required(false);
let flag_linker = Arg::new(FLAG_LINKER)
.long(FLAG_LINKER)
.help("Set which linker to use\n(The surgical linker is enabled by default only when building for wasm32 or x86_64 Linux, because those are the only targets it currently supports. Otherwise the legacy linker is used by default.)")
.value_parser(["surgical", "legacy"])
.required(false);
let flag_build_host = Arg::new(FLAG_BUILD_HOST)
.long(FLAG_BUILD_HOST)
.help("WARNING: platforms are responsible for building hosts, this flag will be removed when internal test platforms have a build script")
.action(ArgAction::SetTrue)
.required(false);
let flag_suppress_build_host_warning = Arg::new(FLAG_SUPPRESS_BUILD_HOST_WARNING)
.long(FLAG_SUPPRESS_BUILD_HOST_WARNING)
.help("WARNING: platforms are responsible for building hosts, this flag will be removed when internal test platforms have a build script")
.action(ArgAction::SetTrue)
.required(false);
let flag_wasm_stack_size_kb = Arg::new(FLAG_WASM_STACK_SIZE_KB)
.long(FLAG_WASM_STACK_SIZE_KB)
.help("Stack size in kilobytes for wasm32 target\n(This only applies when --dev also provided.)")
.value_parser(value_parser!(u32))
.required(false);
let flag_fuzz = Arg::new(FLAG_FUZZ)
.long(FLAG_FUZZ)
.help("Instrument the roc binary for fuzzing with roc-fuzz")
.action(ArgAction::SetTrue)
.required(false);
let flag_main = Arg::new(FLAG_MAIN)
.long(FLAG_MAIN)
.help("The .roc file of the main app/package module to resolve dependencies from")
.value_parser(value_parser!(PathBuf))
.required(false);
let roc_file_to_run = Arg::new(ROC_FILE)
.help("The .roc file of an app to run")
.value_parser(value_parser!(PathBuf))
.required(false)
.default_value(DEFAULT_ROC_FILENAME);
let args_for_app = Arg::new(ARGS_FOR_APP)
.help("Arguments to pass into the app being run\ne.g. `roc run -- arg1 arg2`")
.value_parser(value_parser!(OsString))
.num_args(0..)
.allow_hyphen_values(true);
let build_target_values_parser =
PossibleValuesParser::new(Target::iter().map(Into::<&'static str>::into));
Command::new("roc")
.version(VERSION)
.about("Run the given .roc file, if there are no compilation errors.\nYou can use one of the SUBCOMMANDS below to do something else!")
.args_conflicts_with_subcommands(true)
.subcommand(Command::new(CMD_BUILD)
.about("Build a binary from the given .roc file, but don't run it")
.arg(Arg::new(FLAG_OUTPUT)
.long(FLAG_OUTPUT)
.help("The full path to the output binary, including filename. To specify directory only, specify a path that ends in a directory separator (e.g. a slash).")
.value_parser(value_parser!(OsString))
.required(false)
)
.arg(flag_optimize.clone())
.arg(flag_max_threads.clone())
.arg(flag_opt_size.clone())
.arg(flag_dev.clone())
.arg(flag_emit_llvm_ir.clone())
.arg(flag_profiling.clone())
.arg(flag_time.clone())
.arg(flag_linker.clone())
.arg(flag_build_host.clone())
.arg(flag_suppress_build_host_warning.clone())
.arg(flag_fuzz.clone())
.arg(flag_wasm_stack_size_kb)
.arg(
Arg::new(FLAG_TARGET)
.long(FLAG_TARGET)
.help("Choose a different target")
.default_value(Into::<&'static str>::into(Target::default()))
.value_parser(build_target_values_parser.clone())
.required(false),
)
.arg(
Arg::new(FLAG_LIB)
.long(FLAG_LIB)
.help("Build a C library instead of an executable")
.action(ArgAction::SetTrue)
.required(false),
)
.arg(
Arg::new(FLAG_BUNDLE)
.long(FLAG_BUNDLE)
.help("Create an archive of a package (for example, a .tar, .tar.gz, or .tar.br file), so others can add it as a HTTPS dependency.")
.conflicts_with(FLAG_TARGET)
.value_parser([".tar", ".tar.gz", ".tar.br"])
.required(false),
)
.arg(
Arg::new(FLAG_NO_LINK)
.long(FLAG_NO_LINK)
.help("Do not link\n(Instead, just output the `.o` file.)")
.action(ArgAction::SetTrue)
.required(false),
)
.arg(
Arg::new(ROC_FILE)
.help("The .roc file to build")
.value_parser(value_parser!(PathBuf))
.required(false)
.default_value(DEFAULT_ROC_FILENAME),
)
)
.subcommand(Command::new(CMD_TEST)
.about("Run all top-level `expect`s in a main module and any modules it imports")
.arg(flag_main.clone())
.arg(flag_optimize.clone())
.arg(flag_max_threads.clone())
.arg(flag_opt_size.clone())
.arg(flag_dev.clone())
.arg(flag_emit_llvm_ir.clone())
.arg(flag_profiling.clone())
.arg(flag_time.clone())
.arg(flag_linker.clone())
.arg(flag_build_host.clone())
.arg(flag_suppress_build_host_warning.clone())
.arg(flag_fuzz.clone())
.arg(
Arg::new(FLAG_VERBOSE)
.long(FLAG_VERBOSE)
.help("Print detailed test statistics by module")
.action(ArgAction::SetTrue)
.required(false)
)
.arg(
Arg::new(ROC_FILE)
.help("The .roc file to test")
.value_parser(value_parser!(PathBuf))
.required(false)
.default_value(DEFAULT_ROC_FILENAME)
)
.arg(args_for_app.clone().last(true))
)
.subcommand(Command::new(CMD_REPL)
.about("Launch the interactive Read Eval Print Loop (REPL)")
.arg(
Arg::new(FLAG_NO_COLOR)
.long(FLAG_NO_COLOR)
.help("Do not use any ANSI color codes in the repl output")
.action(ArgAction::SetTrue)
.required(false)
)
.arg(
Arg::new(FLAG_NO_HEADER)
.long(FLAG_NO_HEADER)
.help("Do not print the repl header")
.action(ArgAction::SetTrue)
.required(false)
)
)
.subcommand(Command::new(CMD_RUN)
.about("Run a .roc file even if it has build errors")
.arg(flag_optimize.clone())
.arg(flag_max_threads.clone())
.arg(flag_opt_size.clone())
.arg(flag_dev.clone())
.arg(flag_emit_llvm_ir.clone())
.arg(flag_profiling.clone())
.arg(flag_time.clone())
.arg(flag_linker.clone())
.arg(flag_build_host.clone())
.arg(flag_suppress_build_host_warning.clone())
.arg(flag_fuzz.clone())
.arg(roc_file_to_run.clone())
.arg(args_for_app.clone().last(true))
)
.subcommand(Command::new(CMD_DEV)
.about("`check` a .roc file, and then run it if there were no errors")
.arg(flag_optimize.clone())
.arg(flag_max_threads.clone())
.arg(flag_opt_size.clone())
.arg(flag_dev.clone())
.arg(flag_emit_llvm_ir.clone())
.arg(flag_profiling.clone())
.arg(flag_time.clone())
.arg(flag_linker.clone())
.arg(flag_build_host.clone())
.arg(flag_suppress_build_host_warning.clone())
.arg(flag_fuzz.clone())
.arg(roc_file_to_run.clone())
.arg(args_for_app.clone().last(true))
)
.subcommand(Command::new(CMD_FORMAT)
.about("Format a .roc file or the .roc files contained in a directory using standard\nRoc formatting")
.arg(
Arg::new(DIRECTORY_OR_FILES)
.index(1)
.num_args(0..)
.required(false)
.value_parser(value_parser!(OsString)))
.arg(
Arg::new(FLAG_CHECK)
.long(FLAG_CHECK)
.help("Checks that specified files are formatted\n(If formatting is needed, return a non-zero exit code.)")
.action(ArgAction::SetTrue)
.required(false),
)
.arg(
Arg::new(FLAG_STDIN)
.long(FLAG_STDIN)
.help("Read file to format from stdin")
.action(ArgAction::SetTrue)
.required(false),
)
.arg(
Arg::new(FLAG_STDOUT)
.long(FLAG_STDOUT)
.help("Print formatted file to stdout")
.action(ArgAction::SetTrue)
.required(false),
)
.after_help("If DIRECTORY_OR_FILES is omitted, the .roc files in the current working\ndirectory are formatted.")
)
.subcommand(Command::new(CMD_VERSION)
.about(concatcp!("Print the Roc compilers version, which is currently ", VERSION)))
.subcommand(Command::new(CMD_CHECK)
.about("Check the code for problems, but dont build or run it")
.arg(flag_main.clone())
.arg(flag_time.clone())
.arg(flag_max_threads.clone())
.arg(
Arg::new(ROC_FILE)
.help("The .roc file to check")
.value_parser(value_parser!(PathBuf))
.required(false)
.default_value(DEFAULT_ROC_FILENAME),
)
)
.subcommand(
Command::new(CMD_DOCS)
.about("Generate documentation for a Roc package")
.arg(Arg::new(FLAG_OUTPUT)
.long(FLAG_OUTPUT)
.help("Output directory for the generated documentation files.")
.value_parser(value_parser!(OsString))
.required(false)
.default_value(DEFAULT_GENERATED_DOCS_DIR),
)
.arg(Arg::new(ROC_FILE)
.help("The package's main .roc file")
.value_parser(value_parser!(PathBuf))
.required(false)
.default_value(DEFAULT_ROC_FILENAME),
)
)
.subcommand(Command::new(CMD_GLUE)
.about("Generate glue code between a platform's Roc API and its host language")
.arg(&flag_dev)
.arg(
Arg::new(GLUE_SPEC)
.help("The specification for how to translate Roc types into output files.")
.value_parser(value_parser!(PathBuf))
.required(true)
)
.arg(
Arg::new(GLUE_DIR)
.help("The directory for the generated glue code.\nNote: The implementation can write to any file in this directory.")
.value_parser(value_parser!(PathBuf))
.required(true)
)
.arg(
Arg::new(ROC_FILE)
.help("The .roc file whose exposed types should be translated.")
.value_parser(value_parser!(PathBuf))
.required(false)
.default_value(DEFAULT_ROC_FILENAME)
)
)
.subcommand(Command::new(CMD_PREPROCESS_HOST)
.about("Runs the surgical linker preprocessor to generate `.rh` and `.rm` files.")
.arg(
Arg::new(FLAG_PP_HOST)
.help("Path to the host executable where the app was linked dynamically")
.value_parser(value_parser!(PathBuf))
.required(true)
)
.arg(
Arg::new(FLAG_PP_PLATFORM)
.help("Path to the platform/main.roc file")
.value_parser(value_parser!(PathBuf))
.required(true)
)
.arg(
Arg::new(FLAG_PP_DYLIB)
.help("Path to a stubbed app dynamic library (e.g. roc build --lib app.roc)")
.value_parser(value_parser!(PathBuf))
.required(true)
)
.arg(
Arg::new(FLAG_VERBOSE)
.long(FLAG_VERBOSE)
.help("Print detailed information while pre-processing host")
.action(ArgAction::SetTrue)
.required(false)
)
.arg(
Arg::new(FLAG_TARGET)
.long(FLAG_TARGET)
.help("Choose a different target")
.default_value(Into::<&'static str>::into(Target::default()))
.value_parser(build_target_values_parser.clone())
.required(false),
)
)
.arg(flag_optimize)
.arg(flag_max_threads)
.arg(flag_opt_size)
.arg(flag_dev)
.arg(flag_emit_llvm_ir)
.arg(flag_profiling)
.arg(flag_time)
.arg(flag_linker)
.arg(flag_build_host)
.arg(flag_suppress_build_host_warning)
.arg(flag_fuzz)
.arg(roc_file_to_run)
.arg(args_for_app.trailing_var_arg(true))
}
#[derive(Debug, PartialEq, Eq)]
pub enum BuildConfig {
BuildOnly,
BuildAndRun,
BuildAndRunIfNoErrors,
}
fn opt_level_from_flags(matches: &ArgMatches) -> OptLevel {
match (
matches.get_flag(FLAG_OPTIMIZE),
matches.get_flag(FLAG_OPT_SIZE),
matches.get_flag(FLAG_DEV),
) {
(true, false, false) => OptLevel::Optimize,
(false, true, false) => OptLevel::Size,
(false, false, true) => OptLevel::Development,
(false, false, false) => OptLevel::Normal,
_ => user_error!("build can be only one of `--dev`, `--optimize`, or `--opt-size`"),
}
}
#[cfg(windows)]
pub fn test(_matches: &ArgMatches, _target: Target) -> io::Result<i32> {
todo!("running tests does not work on windows right now")
}
#[cfg(not(windows))]
struct ModuleTestResults {
module_id: ModuleId,
failed_count: usize,
passed_count: usize,
tests_duration: Duration,
}
#[cfg(not(windows))]
pub fn test(matches: &ArgMatches, target: Target) -> io::Result<i32> {
use roc_build::program::report_problems_monomorphized;
use roc_load::{ExecutionMode, FunctionKind, LoadConfig, LoadMonomorphizedError};
use roc_packaging::cache;
let start_time = Instant::now();
let arena = Bump::new();
let opt_level = opt_level_from_flags(matches);
let threading = match matches.get_one::<usize>(FLAG_MAX_THREADS) {
None => Threading::AllAvailable,
Some(0) => user_error!("cannot build with at most 0 threads"),
Some(1) => Threading::Single,
Some(n) => Threading::AtMost(*n),
};
let path = matches.get_one::<PathBuf>(ROC_FILE).unwrap();
// Spawn the root task
if !path.exists() {
let current_dir = env::current_dir().unwrap();
let expected_file_path = current_dir.join(path);
let current_dir_string = current_dir.display();
let expected_file_path_string = expected_file_path.display();
// TODO these should use roc_reporting to display nicer error messages.
match matches.value_source(ROC_FILE) {
Some(ValueSource::DefaultValue) => {
eprintln!(
"\nThe current directory ({current_dir_string}) does not contain a {DEFAULT_ROC_FILENAME} file to use as a default.\n\nYou can run `roc help` for more information on how to provide a .roc file.\n"
)
}
_ => eprintln!("\nThis file was not found: {expected_file_path_string}\n\nYou can run `roc help` for more information on how to provide a .roc file.\n"),
}
process::exit(1);
}
let arena = &arena;
let function_kind = FunctionKind::from_env();
let opt_main_path = matches.get_one::<PathBuf>(FLAG_MAIN);
// Step 1: compile the app and generate the .o file
let load_config = LoadConfig {
target,
function_kind,
// TODO: expose this from CLI?
render: roc_reporting::report::RenderTarget::ColorTerminal,
palette: roc_reporting::report::DEFAULT_PALETTE,
threading,
exec_mode: ExecutionMode::Test,
};
let load_result = roc_load::load_and_monomorphize(
arena,
path.to_path_buf(),
opt_main_path.cloned(),
RocCacheDir::Persistent(cache::roc_cache_packages_dir().as_path()),
load_config,
);
let mut loaded = match load_result {
Ok(loaded) => loaded,
Err(LoadMonomorphizedError::LoadingProblem(problem)) => {
return handle_loading_problem(problem);
}
Err(LoadMonomorphizedError::ErrorModule(module)) => {
return handle_error_module(module, start_time.elapsed(), path.as_os_str(), false);
}
};
let problems = report_problems_monomorphized(&mut loaded);
let mut expectations = std::mem::take(&mut loaded.expectations);
let interns = loaded.interns.clone();
let sources = loaded.sources.clone();
let (dyn_lib, expects_by_module, layout_interner) =
roc_repl_expect::run::expect_mono_module_to_dylib(
arena,
target,
loaded,
opt_level,
LlvmBackendMode::CliTest,
)
.unwrap();
// Print warnings before running tests.
{
debug_assert_eq!(
problems.errors, 0,
"if there were errors, we would have already exited."
);
if problems.warnings > 0 {
problems.print_error_warning_count(start_time.elapsed());
println!(".\n\nRunning tests…\n\n\x1B[36m{}\x1B[39m", "".repeat(80));
}
}
// Run the tests.
let arena = &bumpalo::Bump::new();
let interns = arena.alloc(interns);
let mut writer = std::io::stdout();
let mut total_failed_count = 0;
let mut total_passed_count = 0;
let mut results_by_module = Vec::new();
let global_layout_interner = layout_interner.into_global();
let compilation_duration = start_time.elapsed();
for (module_id, expects) in expects_by_module.into_iter() {
let test_start_time = Instant::now();
let (failed_count, passed_count) = roc_repl_expect::run::run_toplevel_expects(
&mut writer,
roc_reporting::report::RenderTarget::ColorTerminal,
arena,
interns,
&global_layout_interner,
&dyn_lib,
&mut expectations,
expects,
)
.unwrap();
let tests_duration = test_start_time.elapsed();
results_by_module.push(ModuleTestResults {
module_id,
failed_count,
passed_count,
tests_duration,
});
total_failed_count += failed_count;
total_passed_count += passed_count;
}
let total_duration = start_time.elapsed();
if total_failed_count == 0 && total_passed_count == 0 {
// TODO print this in a more nicely formatted way!
println!("No expectations were found.");
// If no tests ran, treat that as an error. This is perhaps
// briefly annoying at the very beginning of a project when
// you actually have zero tests, but it can save you from
// having a change to your CI script accidentally stop
// running tests altogether!
Ok(2)
} else {
if matches.get_flag(FLAG_VERBOSE) {
println!("Compiled in {} ms.", compilation_duration.as_millis());
for module_test_results in results_by_module {
print_test_results(module_test_results, &sources);
}
} else {
let test_summary_str =
test_summary(total_failed_count, total_passed_count, total_duration);
println!("{test_summary_str}");
}
Ok((total_failed_count > 0) as i32)
}
}
#[cfg(not(windows))]
fn print_test_results(
module_test_results: ModuleTestResults,
sources: &MutMap<ModuleId, (PathBuf, Box<str>)>,
) {
let ModuleTestResults {
module_id,
failed_count,
passed_count,
tests_duration,
} = module_test_results;
let test_summary_str = test_summary(failed_count, passed_count, tests_duration);
let (module_path, _) = sources.get(&module_id).unwrap();
let module_name = module_path.file_name().unwrap().to_str().unwrap();
println!("\n{module_name}:\n {test_summary_str}",);
}
#[cfg(not(windows))]
fn test_summary(failed_count: usize, passed_count: usize, tests_duration: Duration) -> String {
let failed_color = if failed_count == 0 {
ANSI_STYLE_CODES.green
} else {
ANSI_STYLE_CODES.red
};
let passed_color = ANSI_STYLE_CODES.green;
let reset = ANSI_STYLE_CODES.reset;
format!(
"{failed_color}{failed_count}{reset} failed and {passed_color}{passed_count}{reset} passed in {} ms.",
tests_duration.as_millis()
)
}
/// Find the element of `options` with the smallest edit distance to
/// `reference`. Returns a tuple containing the element and the distance, or
/// `None` if the `options` `Vec` is empty.
fn nearest_match<'a>(reference: &str, options: &'a [String]) -> Option<(&'a String, usize)> {
options
.iter()
.map(|s| (s, distance::damerau_levenshtein(reference, s)))
.min_by(|(_, a), (_, b)| a.cmp(b))
}
pub fn build(
matches: &ArgMatches,
subcommands: &[String],
config: BuildConfig,
target: Target,
out_path: Option<&Path>,
roc_cache_dir: RocCacheDir<'_>,
link_type: LinkType,
) -> io::Result<i32> {
use BuildConfig::*;
let path = matches.get_one::<PathBuf>(ROC_FILE).unwrap();
{
// Spawn the root task
if !path.exists() {
let current_dir = env::current_dir().unwrap();
let expected_file_path = current_dir.join(path);
let current_dir_string = current_dir.display();
let expected_file_path_string = expected_file_path.display();
// TODO these should use roc_reporting to display nicer error messages.
match matches.value_source(ROC_FILE) {
Some(ValueSource::DefaultValue) => {
eprintln!(
"\nThe current directory ({current_dir_string}) does not contain a {DEFAULT_ROC_FILENAME} file to use as a default.\n\nYou can run `roc help` for more information on how to provide a .roc file.\n"
)
}
_ => {
let mut error_lines = Vec::new();
error_lines.push(format!(
"This file was not found: {expected_file_path_string}"
));
// Add some additional hints if run as `roc [FILENAME]`.
if matches.subcommand().is_none() {
match path.to_str() {
Some(possible_typo) if !possible_typo.ends_with(".roc") => {
if let Some((nearest_command, _)) =
nearest_match(possible_typo, subcommands)
{
error_lines.push(format!(
"Did you mean to use the {nearest_command} subcommand?"
));
}
}
_ => (),
}
}
error_lines.push("You can run `roc help` to see the list of available subcommands and for more information on how to provide a .roc file.".to_string());
eprintln!("\n{}\n", error_lines.join("\n\n"));
}
}
process::exit(1);
}
if config == BuildConfig::BuildOnly && matches.contains_id(FLAG_BUNDLE) {
let start_time = Instant::now();
let compression =
Compression::try_from(matches.get_one::<String>(FLAG_BUNDLE).unwrap().as_str())
.unwrap();
// Print a note of advice. This is mainly here because brotli takes so long but produces
// such smaller output files; the idea is to encourage people to wait for brotli,
// so that downloads go faster. The compression only happens once, but the network
// transfer and decompression will happen many more times!
match compression {
Compression::Brotli => {
println!("Compressing with Brotli at maximum quality level…\n\n(Note: Brotli compression can take awhile! Using --{FLAG_BUNDLE} .tar.gz takes less time, but usually produces a significantly larger output file. Brotli is generally worth the up-front wait if this is a file people will be downloading!)\n");
}
Compression::Gzip => {
println!("Compressing with gzip at minimum quality…\n\n(Note: Gzip usually runs faster than Brotli but typically produces significantly larger output files. Consider using --{FLAG_BUNDLE} .tar.br if this is a file people will be downloading!)\n");
}
Compression::Uncompressed => {
println!("Building .tar archive without compression…\n\n(Note: Compression takes more time to run but typically produces much smaller output files. Consider using --{FLAG_BUNDLE} .tar.br if this is a file people will be downloading!)\n");
}
}
// Rather than building an executable or library, we're building
// a tarball so this code can be distributed via HTTPS
let filename = roc_packaging::tarball::build(path, compression)?;
let total_time_ms = start_time.elapsed().as_millis();
let total_time = if total_time_ms > 1000 {
format!("{}s {}ms", total_time_ms / 1000, total_time_ms % 1000)
} else {
format!("{total_time_ms} ms")
};
let created_path = path.with_file_name(&filename);
println!(
"\nBundled \x1B[33m{}\x1B[39m and its dependent files into the following archive in {total_time}:\n\n\t\x1B[33m{}\x1B[39m\n\nTo distribute this archive as a package, upload this to some URL and then add it as a dependency with:\n\n\t\x1B[32m\"https://your-url-goes-here/{filename}\"\x1B[39m\n",
path.to_string_lossy(),
created_path.to_string_lossy()
);
return Ok(0);
}
}
// the process will end after this function,
// so we don't want to spend time freeing these values
let arena = ManuallyDrop::new(Bump::new());
let opt_level = if let BuildConfig::BuildAndRunIfNoErrors = config {
OptLevel::Development
} else {
opt_level_from_flags(matches)
};
// Note: This allows using `--dev` with `--optimize`.
// This means frontend optimizations and dev backend.
let code_gen_backend = if matches.get_flag(FLAG_DEV) {
if matches!(target.architecture(), Architecture::Wasm32) {
CodeGenBackend::Wasm
} else {
CodeGenBackend::Assembly(AssemblyBackendMode::Binary)
}
} else {
let backend_mode = match opt_level {
OptLevel::Development => LlvmBackendMode::BinaryDev,
OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => LlvmBackendMode::Binary,
};
CodeGenBackend::Llvm(backend_mode)
};
let emit_llvm_ir = matches.get_flag(FLAG_EMIT_LLVM_IR);
if emit_llvm_ir && !matches!(code_gen_backend, CodeGenBackend::Llvm(_)) {
user_error!("Cannot emit llvm ir while using a dev backend.");
}
let emit_debug_info = matches.get_flag(FLAG_PROFILING)
|| matches!(opt_level, OptLevel::Development | OptLevel::Normal);
let emit_timings = matches.get_flag(FLAG_TIME);
let threading = match matches.get_one::<usize>(FLAG_MAX_THREADS) {
None => Threading::AllAvailable,
Some(0) => user_error!("cannot build with at most 0 threads"),
Some(1) => Threading::Single,
Some(n) => Threading::AtMost(*n),
};
let wasm_dev_backend = matches!(code_gen_backend, CodeGenBackend::Wasm);
let linking_strategy = if wasm_dev_backend {
LinkingStrategy::Additive
} else if !roc_linker::supported(link_type, target)
|| matches.get_one::<String>(FLAG_LINKER).map(|s| s.as_str()) == Some("legacy")
{
LinkingStrategy::Legacy
} else {
LinkingStrategy::Surgical
};
// All hosts should be prebuilt, this flag keeps the rebuilding behvaiour
// as required for internal tests
let build_host = matches.get_flag(FLAG_BUILD_HOST);
let suppress_build_host_warning = matches.get_flag(FLAG_SUPPRESS_BUILD_HOST_WARNING);
let fuzz = matches.get_flag(FLAG_FUZZ);
if fuzz && !matches!(code_gen_backend, CodeGenBackend::Llvm(_)) {
user_error!("Cannot instrument binary for fuzzing while using a dev backend.");
}
let wasm_dev_stack_bytes: Option<u32> = matches
.try_get_one::<u32>(FLAG_WASM_STACK_SIZE_KB)
.ok()
.flatten()
.map(|x| x * 1024);
let build_ordering = match config {
BuildAndRunIfNoErrors => BuildOrdering::BuildIfChecks,
_ => BuildOrdering::AlwaysBuild,
};
let code_gen_options = CodeGenOptions {
backend: code_gen_backend,
opt_level,
emit_debug_info,
emit_llvm_ir,
fuzz,
};
let load_config = standard_load_config(target, build_ordering, threading);
let res_binary_path = roc_build::program::build_file(
&arena,
target,
path.to_owned(),
code_gen_options,
emit_timings,
link_type,
linking_strategy,
build_host,
suppress_build_host_warning,
wasm_dev_stack_bytes,
roc_cache_dir,
load_config,
out_path,
);
match res_binary_path {
Ok(BuiltFile {
binary_path,
problems,
total_time,
expect_metadata,
}) => {
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)
.to_str()
.unwrap();
// No need to waste time freeing this memory,
// since the process is about to exit anyway.
// std::mem::forget(arena);
problems.print_error_warning_count(total_time);
println!(" while successfully building:\n\n {generated_filename}");
// Return a nonzero exit code if there were problems
Ok(problems.exit_code())
}
BuildAndRun => {
if problems.fatally_errored {
problems.print_error_warning_count(total_time);
println!(
".\n\nCannot run program due to fatal error…\n\n\x1B[36m{}\x1B[39m",
"".repeat(80)
);
// Return a nonzero exit code due to fatal problem
return Ok(problems.exit_code());
}
if problems.errors > 0 || problems.warnings > 0 {
problems.print_error_warning_count(total_time);
println!(
".\n\nRunning program anyway…\n\n\x1B[36m{}\x1B[39m",
"".repeat(80)
);
}
let args = matches
.get_many::<OsString>(ARGS_FOR_APP)
.unwrap_or_default()
.map(|s| s.as_os_str());
// don't waste time deallocating; the process ends anyway
// ManuallyDrop will leak the bytes because we don't drop manually
let bytes = &ManuallyDrop::new(std::fs::read(&binary_path).unwrap());
roc_run(&arena, opt_level, target, args, bytes, expect_metadata)
}
BuildAndRunIfNoErrors => {
if problems.fatally_errored {
problems.print_error_warning_count(total_time);
println!(
".\n\nCannot run program due to fatal error…\n\n\x1B[36m{}\x1B[39m",
"".repeat(80)
);
// Return a nonzero exit code due to fatal problem
return Ok(problems.exit_code());
}
debug_assert_eq!(
problems.errors, 0,
"if there are non-fatal errors, they should have been returned as an error variant"
);
if problems.warnings > 0 {
problems.print_error_warning_count(total_time);
println!(
".\n\nRunning program…\n\n\x1B[36m{}\x1B[39m",
"".repeat(80)
);
}
let args = matches
.get_many::<OsString>(ARGS_FOR_APP)
.unwrap_or_default()
.map(|s| s.as_os_str());
// don't waste time deallocating; the process ends anyway
// ManuallyDrop will leak the bytes because we don't drop manually
let bytes = &ManuallyDrop::new(std::fs::read(&binary_path).unwrap());
roc_run(&arena, opt_level, target, args, bytes, expect_metadata)
}
}
}
Err(BuildFileError::ErrorModule { module, total_time }) => {
handle_error_module(module, total_time, path.as_os_str(), true)
}
Err(BuildFileError::LoadingProblem(problem)) => handle_loading_problem(problem),
}
}
fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
arena: &Bump,
opt_level: OptLevel,
target: Target,
args: I,
binary_bytes: &[u8],
expect_metadata: ExpectMetadata,
) -> io::Result<i32> {
match target.architecture() {
Architecture::Wasm32 => {
let executable = roc_run_executable_file_path(binary_bytes)?;
let path = executable.as_path();
// If possible, report the generated executable name relative to the current dir.
let generated_filename = path
.strip_prefix(env::current_dir().unwrap())
.unwrap_or(path);
#[cfg(target_family = "unix")]
{
use std::os::unix::ffi::OsStrExt;
run_wasm(
generated_filename,
args.into_iter().map(|os_str| os_str.as_bytes()),
);
}
#[cfg(not(target_family = "unix"))]
{
run_wasm(
generated_filename,
args.into_iter().map(|os_str| {
os_str.to_str().expect(
"Roc does not currently support passing non-UTF8 arguments to Wasm.",
)
}),
);
}
Ok(0)
}
_ => roc_run_native(arena, opt_level, args, binary_bytes, expect_metadata),
}
}
#[cfg(target_family = "unix")]
fn os_str_as_utf8_bytes(os_str: &OsStr) -> &[u8] {
use std::os::unix::ffi::OsStrExt;
os_str.as_bytes()
}
#[cfg(not(target_family = "unix"))]
fn os_str_as_utf8_bytes(os_str: &OsStr) -> &[u8] {
os_str.to_str().unwrap().as_bytes()
}
fn make_argv_envp<'a, I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
arena: &'a Bump,
executable: &ExecutableFile,
args: I,
) -> (
bumpalo::collections::Vec<'a, CString>,
bumpalo::collections::Vec<'a, CString>,
) {
use bumpalo::collections::CollectIn;
let path = executable.as_path();
let path_cstring = CString::new(os_str_as_utf8_bytes(path.as_os_str())).unwrap();
// argv is an array of pointers to strings passed to the new program
// as its command-line arguments. By convention, the first of these
// strings (i.e., argv[0]) should contain the filename associated
// with the file being executed. The argv array must be terminated
// by a NULL pointer. (Thus, in the new program, argv[argc] will be NULL.)
let it = args
.into_iter()
.map(|x| CString::new(os_str_as_utf8_bytes(x.as_ref())).unwrap());
let argv_cstrings: bumpalo::collections::Vec<CString> =
std::iter::once(path_cstring).chain(it).collect_in(arena);
// envp is an array of pointers to strings, conventionally of the
// form key=value, which are passed as the environment of the new
// program. The envp array must be terminated by a NULL pointer.
let mut buffer = Vec::with_capacity(100);
let envp_cstrings: bumpalo::collections::Vec<CString> = std::env::vars_os()
.map(|(k, v)| {
buffer.clear();
use std::io::Write;
buffer.write_all(os_str_as_utf8_bytes(&k)).unwrap();
buffer.write_all(b"=").unwrap();
buffer.write_all(os_str_as_utf8_bytes(&v)).unwrap();
CString::new(buffer.as_slice()).unwrap()
})
.collect_in(arena);
(argv_cstrings, envp_cstrings)
}
/// Run on the native OS (not on wasm)
#[cfg(target_family = "unix")]
fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
arena: &Bump,
opt_level: OptLevel,
args: I,
binary_bytes: &[u8],
expect_metadata: ExpectMetadata,
) -> std::io::Result<i32> {
use bumpalo::collections::CollectIn;
let executable = roc_run_executable_file_path(binary_bytes)?;
let (argv_cstrings, envp_cstrings) = make_argv_envp(arena, &executable, args);
let argv: bumpalo::collections::Vec<*const c_char> = argv_cstrings
.iter()
.map(|s| s.as_ptr())
.chain([std::ptr::null()])
.collect_in(arena);
let envp: bumpalo::collections::Vec<*const c_char> = envp_cstrings
.iter()
.map(|s| s.as_ptr())
.chain([std::ptr::null()])
.collect_in(arena);
match opt_level {
OptLevel::Development => roc_dev_native(arena, executable, argv, envp, expect_metadata),
OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => unsafe {
roc_run_native_fast(executable, &argv, &envp);
},
}
Ok(1)
}
unsafe fn roc_run_native_fast(
executable: ExecutableFile,
argv: &[*const c_char],
envp: &[*const c_char],
) {
if executable.execve(argv, envp) != 0 {
internal_error!(
"libc::{}({:?}, ..., ...) failed: {:?}",
ExecutableFile::SYSCALL,
executable.as_path(),
errno::errno()
);
}
}
#[derive(Debug)]
enum ExecutableFile {
#[cfg(target_os = "linux")]
MemFd(libc::c_int, PathBuf),
// We store the TempDir in the onDisk variant alongside the path to the executable,
// so that the TempDir doesn't get dropped until after we're done with the path.
// If we didn't do that, then the tempdir would potentially get deleted by the
// TempDir's Drop impl before the file had been executed.
#[allow(dead_code)]
#[cfg(not(target_os = "linux"))]
OnDisk(TempDir, PathBuf),
}
impl ExecutableFile {
#[cfg(target_os = "linux")]
const SYSCALL: &'static str = "fexecve";
#[cfg(not(target_os = "linux"))]
const SYSCALL: &'static str = "execve";
fn as_path(&self) -> &Path {
match self {
#[cfg(target_os = "linux")]
ExecutableFile::MemFd(_, path_buf) => path_buf.as_ref(),
#[cfg(not(target_os = "linux"))]
ExecutableFile::OnDisk(_, path_buf) => path_buf.as_ref(),
}
}
unsafe fn execve(&self, argv: &[*const c_char], envp: &[*const c_char]) -> c_int {
match self {
#[cfg(target_os = "linux")]
ExecutableFile::MemFd(fd, _path) => libc::fexecve(*fd, argv.as_ptr(), envp.as_ptr()),
#[cfg(all(target_family = "unix", not(target_os = "linux")))]
ExecutableFile::OnDisk(_, path) => {
use std::os::unix::ffi::OsStrExt;
let path_cstring = CString::new(path.as_os_str().as_bytes()).unwrap();
libc::execve(path_cstring.as_ptr().cast(), argv.as_ptr(), envp.as_ptr())
}
#[cfg(target_family = "windows")]
ExecutableFile::OnDisk(_, path) => {
let path_cstring = CString::new(path.to_str().unwrap()).unwrap();
libc::execve(path_cstring.as_ptr().cast(), argv.as_ptr(), envp.as_ptr())
}
}
}
}
// with Expect
#[cfg(target_family = "unix")]
fn roc_dev_native(
arena: &Bump,
executable: ExecutableFile,
argv: bumpalo::collections::Vec<*const c_char>,
envp: bumpalo::collections::Vec<*const c_char>,
expect_metadata: ExpectMetadata,
) -> ! {
use std::sync::{atomic::AtomicBool, Arc};
use roc_repl_expect::run::{ChildProcessMsg, ExpectMemory};
let ExpectMetadata {
mut expectations,
interns,
layout_interner,
} = expect_metadata;
let shm_name = format!("/roc_expect_buffer_{}", std::process::id());
let mut memory = ExpectMemory::create_or_reuse_mmap(&shm_name);
let layout_interner = layout_interner.into_global();
match unsafe { libc::fork() } {
0 => unsafe {
// we are the child
executable.execve(&argv, &envp);
// Display a human-friendly error message
println!("Error {:?}", std::io::Error::last_os_error());
std::process::exit(1);
},
-1 => {
// something failed
// Display a human-friendly error message
println!("Error {:?}", std::io::Error::last_os_error());
std::process::exit(1)
}
pid @ 1.. => {
let sigchld = Arc::new(AtomicBool::new(false));
signal_hook::flag::register(signal_hook::consts::SIGCHLD, Arc::clone(&sigchld))
.unwrap();
let exit_code = loop {
match memory.wait_for_child(sigchld.clone()) {
ChildProcessMsg::Terminate => {
let mut status = 0;
let options = 0;
unsafe { libc::waitpid(pid, &mut status, options) };
// if `WIFEXITED` returns false, `WEXITSTATUS` will just return junk
break if libc::WIFEXITED(status) {
libc::WEXITSTATUS(status)
} else {
// we don't have an exit code, but something went wrong if we're in this else
1
};
}
ChildProcessMsg::Expect => {
let mut writer = std::io::stdout();
roc_repl_expect::run::render_expects_in_memory(
&mut writer,
arena,
&mut expectations,
&interns,
&layout_interner,
&memory,
)
.unwrap();
memory.reset();
}
}
};
std::process::exit(exit_code)
}
_ => unreachable!(),
}
}
#[cfg(target_os = "linux")]
fn roc_run_executable_file_path(binary_bytes: &[u8]) -> std::io::Result<ExecutableFile> {
// on linux, we use the `memfd_create` function to create an in-memory anonymous file.
let flags = 0;
let anonymous_file_name = "roc_file_descriptor\0";
let fd = unsafe { libc::memfd_create(anonymous_file_name.as_ptr().cast(), flags) };
if fd == 0 {
internal_error!(
"libc::memfd_create({:?}, {}) failed: file descriptor is 0",
anonymous_file_name,
flags
);
}
let path = PathBuf::from(format!("/proc/self/fd/{fd}"));
std::fs::write(&path, binary_bytes)?;
Ok(ExecutableFile::MemFd(fd, path))
}
#[cfg(all(target_family = "unix", not(target_os = "linux")))]
fn roc_run_executable_file_path(binary_bytes: &[u8]) -> std::io::Result<ExecutableFile> {
use std::fs::OpenOptions;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let temp_dir = tempfile::tempdir()?;
// We have not found a way to use a virtual file on non-Linux OSes.
// Hence we fall back to just writing the file to the file system, and using that file.
let app_path_buf = temp_dir.path().join("roc_app_binary");
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.mode(0o777) // create the file as executable
.open(&app_path_buf)?;
file.write_all(binary_bytes)?;
// We store the TempDir in this variant alongside the path to the executable,
// so that the TempDir doesn't get dropped until after we're done with the path.
// If we didn't do that, then the tempdir would potentially get deleted by the
// TempDir's Drop impl before the file had been executed.
Ok(ExecutableFile::OnDisk(temp_dir, app_path_buf))
}
#[cfg(target_family = "windows")]
fn roc_run_executable_file_path(binary_bytes: &[u8]) -> std::io::Result<ExecutableFile> {
use std::fs::OpenOptions;
use std::io::Write;
let temp_dir = tempfile::tempdir()?;
// We have not found a way to use a virtual file on non-Linux OSes.
// Hence we fall back to just writing the file to the file system, and using that file.
let app_path_buf = temp_dir.path().join("roc_app_binary.exe");
let mut file = OpenOptions::new()
.create(true)
.write(true)
//.mode(0o777) // create the file as executable
.open(&app_path_buf)?;
file.write_all(binary_bytes)?;
// We store the TempDir in this variant alongside the path to the executable,
// so that the TempDir doesn't get dropped until after we're done with the path.
// If we didn't do that, then the tempdir would potentially get deleted by the
// TempDir's Drop impl before the file had been executed.
Ok(ExecutableFile::OnDisk(temp_dir, app_path_buf))
}
/// Run on the native OS (not on wasm)
#[cfg(not(target_family = "unix"))]
fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
arena: &Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it!
opt_level: OptLevel,
args: I,
binary_bytes: &[u8],
_expect_metadata: ExpectMetadata,
) -> io::Result<i32> {
use bumpalo::collections::CollectIn;
unsafe {
let executable = roc_run_executable_file_path(binary_bytes)?;
// TODO forward the arguments
let (argv_cstrings, envp_cstrings) = make_argv_envp(&arena, &executable, args);
let argv: bumpalo::collections::Vec<*const c_char> = argv_cstrings
.iter()
.map(|s| s.as_ptr())
.chain([std::ptr::null()])
.collect_in(arena);
let envp: bumpalo::collections::Vec<*const c_char> = envp_cstrings
.iter()
.map(|s| s.as_ptr())
.chain([std::ptr::null()])
.collect_in(arena);
match opt_level {
OptLevel::Development => {
// roc_run_native_debug(executable, &argv, &envp, expectations, interns)
internal_error!("running `expect`s does not currently work on windows")
}
OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => {
roc_run_native_fast(executable, &argv, &envp);
}
}
}
Ok(1)
}
#[cfg(feature = "run-wasm32")]
fn run_wasm<I: Iterator<Item = S>, S: AsRef<[u8]>>(wasm_path: &std::path::Path, args: I) {
use bumpalo::collections::Vec;
use roc_wasm_interp::{DefaultImportDispatcher, Instance};
let bytes = std::fs::read(wasm_path).unwrap();
let arena = Bump::new();
let mut argv = Vec::<&[u8]>::new_in(&arena);
for arg in args {
let mut arg_copy = Vec::<u8>::new_in(&arena);
arg_copy.extend_from_slice(arg.as_ref());
argv.push(arg_copy.into_bump_slice());
}
let import_dispatcher = DefaultImportDispatcher::new(&argv);
let mut instance = Instance::from_bytes(&arena, &bytes, import_dispatcher, false).unwrap();
instance
.call_export("_start", [])
.unwrap()
.unwrap()
.expect_i32()
.unwrap();
}
#[cfg(not(feature = "run-wasm32"))]
fn run_wasm<I: Iterator<Item = S>, S: AsRef<[u8]>>(_wasm_path: &std::path::Path, _args: I) {
println!("Running wasm files is not supported on this target.");
}