diff --git a/.gitattributes b/.gitattributes index 873ddfb587..666eddc88d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ +* text=auto eol=lf + *.ml linguist-language=OCaml *.mli linguist-language=OCaml *.res linguist-language=ReScript diff --git a/rewatch/src/build.rs b/rewatch/src/build.rs index c76120064e..ab00db8aad 100644 --- a/rewatch/src/build.rs +++ b/rewatch/src/build.rs @@ -18,10 +18,12 @@ use console::style; use indicatif::{ProgressBar, ProgressStyle}; use log::log_enabled; use serde::Serialize; +use std::ffi::OsString; use std::fmt; use std::fs::File; use std::io::{stdout, Write}; use std::path::{Path, PathBuf}; +use std::process::Stdio; use std::time::{Duration, Instant}; use self::compile::compiler_args; @@ -551,3 +553,18 @@ pub fn build( } } } + +pub fn pass_through_legacy(args: Vec) -> i32 { + let project_root = helpers::get_abs_path(Path::new(".")); + let workspace_root = helpers::get_workspace_root(&project_root); + + let bsb_path = helpers::get_rescript_legacy(&project_root, workspace_root); + + let status = std::process::Command::new(bsb_path) + .args(args) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status(); + + status.map(|s| s.code().unwrap_or(1)).unwrap_or(1) +} diff --git a/rewatch/src/build/clean.rs b/rewatch/src/build/clean.rs index 5624e6ac33..0057437997 100644 --- a/rewatch/src/build/clean.rs +++ b/rewatch/src/build/clean.rs @@ -335,7 +335,6 @@ pub fn clean( path: &Path, show_progress: bool, bsc_path: &Option, - build_dev_deps: bool, snapshot_output: bool, ) -> Result<()> { let project_root = helpers::get_abs_path(path); @@ -345,8 +344,9 @@ pub fn clean( &project_root, &workspace_root, show_progress, - // Always clean dev dependencies - build_dev_deps, + // Build the package tree with dev dependencies. + // They should always be cleaned if they are there. + true, )?; let root_config_name = packages::read_package_name(&project_root)?; let bsc_path = match bsc_path { diff --git a/rewatch/src/cli.rs b/rewatch/src/cli.rs new file mode 100644 index 0000000000..87ec1efb09 --- /dev/null +++ b/rewatch/src/cli.rs @@ -0,0 +1,178 @@ +use std::ffi::OsString; + +use clap::{Args, Parser, Subcommand}; +use clap_verbosity_flag::InfoLevel; + +/// Rewatch is an alternative build system for the Rescript Compiler bsb (which uses Ninja internally). It strives +/// to deliver consistent and faster builds in monorepo setups with multiple packages, where the +/// default build system fails to pick up changed interfaces across multiple packages. +#[derive(Parser, Debug)] +#[command(version)] +#[command(args_conflicts_with_subcommands = true)] +pub struct Cli { + /// Verbosity: + /// -v -> Debug + /// -vv -> Trace + /// -q -> Warn + /// -qq -> Error + /// -qqq -> Off. + /// Default (/ no argument given): 'info' + #[command(flatten)] + pub verbose: clap_verbosity_flag::Verbosity, + + /// The command to run. If not provided it will default to build. + #[command(subcommand)] + pub command: Option, + + /// The relative path to where the main rescript.json resides. IE - the root of your project. + #[arg(default_value = ".")] + pub folder: String, + + #[command(flatten)] + pub build_args: BuildArgs, +} + +#[derive(Args, Debug, Clone)] +pub struct BuildArgs { + /// Filter files by regex + /// + /// Filter allows for a regex to be supplied which will filter the files to be compiled. For + /// instance, to filter out test files for compilation while doing feature work. + #[arg(short, long)] + pub filter: Option, + + /// Action after build + /// + /// This allows one to pass an additional command to the watcher, which allows it to run when + /// finished. For instance, to play a sound when done compiling, or to run a test suite. + /// NOTE - You may need to add '--color=always' to your subcommand in case you want to output + /// color as well + #[arg(short, long)] + pub after_build: Option, + + /// Create source_dirs.json + /// + /// This creates a source_dirs.json file at the root of the monorepo, which is needed when you + /// want to use Reanalyze + #[arg(short, long, default_value_t = false, num_args = 0..=1)] + pub create_sourcedirs: bool, + + /// Build development dependencies + /// + /// This is the flag to also compile development dependencies + /// It's important to know that we currently do not discern between project src, and + /// dependencies. So enabling this flag will enable building _all_ development dependencies of + /// _all_ packages + #[arg(long, default_value_t = false, num_args = 0..=1)] + pub dev: bool, + + /// Disable timing on the output + #[arg(short, long, default_value_t = false, num_args = 0..=1)] + pub no_timing: bool, + + /// simple output for snapshot testing + #[arg(short, long, default_value = "false", num_args = 0..=1)] + pub snapshot_output: bool, + + /// Path to bsc + #[arg(long)] + pub bsc_path: Option, +} + +#[derive(Args, Clone, Debug)] +pub struct WatchArgs { + /// Filter files by regex + /// + /// Filter allows for a regex to be supplied which will filter the files to be compiled. For + /// instance, to filter out test files for compilation while doing feature work. + #[arg(short, long)] + pub filter: Option, + + /// Action after build + /// + /// This allows one to pass an additional command to the watcher, which allows it to run when + /// finished. For instance, to play a sound when done compiling, or to run a test suite. + /// NOTE - You may need to add '--color=always' to your subcommand in case you want to output + /// color as well + #[arg(short, long)] + pub after_build: Option, + + /// Create source_dirs.json + /// + /// This creates a source_dirs.json file at the root of the monorepo, which is needed when you + /// want to use Reanalyze + #[arg(short, long, default_value_t = false, num_args = 0..=1)] + pub create_sourcedirs: bool, + + /// Build development dependencies + /// + /// This is the flag to also compile development dependencies + /// It's important to know that we currently do not discern between project src, and + /// dependencies. So enabling this flag will enable building _all_ development dependencies of + /// _all_ packages + #[arg(long, default_value_t = false, num_args = 0..=1)] + pub dev: bool, + + /// simple output for snapshot testing + #[arg(short, long, default_value = "false", num_args = 0..=1)] + pub snapshot_output: bool, + + /// Path to bsc + #[arg(long)] + pub bsc_path: Option, +} + +#[derive(Subcommand, Clone, Debug)] +pub enum Command { + /// Build using Rewatch + Build(BuildArgs), + /// Build, then start a watcher + Watch(WatchArgs), + /// Clean the build artifacts + Clean { + /// Path to bsc + #[arg(long)] + bsc_path: Option, + + /// simple output for snapshot testing + #[arg(short, long, default_value = "false", num_args = 0..=1)] + snapshot_output: bool, + }, + /// Alias to `legacy format`. + #[command(disable_help_flag = true)] + Format { + #[arg(allow_hyphen_values = true, num_args = 0..)] + format_args: Vec, + }, + /// Alias to `legacy dump`. + #[command(disable_help_flag = true)] + Dump { + #[arg(allow_hyphen_values = true, num_args = 0..)] + dump_args: Vec, + }, + /// This prints the compiler arguments. It expects the path to a rescript.json file. + CompilerArgs { + /// Path to a rescript.json file + #[command()] + path: String, + + #[arg(long, default_value_t = false, num_args = 0..=1)] + dev: bool, + + /// To be used in conjunction with compiler_args + #[arg(long)] + rescript_version: Option, + + /// A custom path to bsc + #[arg(long)] + bsc_path: Option, + }, + /// Use the legacy build system. + /// + /// After this command is encountered, the rest of the arguments are passed to the legacy build system. + #[command(disable_help_flag = true)] + Legacy { + #[arg(allow_hyphen_values = true, num_args = 0..)] + legacy_args: Vec, + }, +} diff --git a/rewatch/src/helpers.rs b/rewatch/src/helpers.rs index a91dee77f5..b05dc9b2ad 100644 --- a/rewatch/src/helpers.rs +++ b/rewatch/src/helpers.rs @@ -218,6 +218,41 @@ pub fn get_bsc(root_path: &Path, workspace_root: &Option) -> PathBuf { } } +pub fn get_rescript_legacy(root_path: &Path, workspace_root: Option) -> PathBuf { + let subfolder = match (std::env::consts::OS, std::env::consts::ARCH) { + ("macos", "aarch64") => "darwin-arm64", + ("macos", _) => "darwin-x64", + ("linux", "aarch64") => "linux-arm64", + ("linux", _) => "linux-x64", + ("windows", "aarch64") => "win-arm64", + ("windows", _) => "win-x64", + _ => panic!("Unsupported architecture"), + }; + + let legacy_path_fragment = Path::new("node_modules") + .join("@rescript") + .join(subfolder) + .join("bin") + .join("rescript-legacy"); + + match ( + root_path + .join(&legacy_path_fragment) + .canonicalize() + .map(StrippedVerbatimPath::to_stripped_verbatim_path), + workspace_root.map(|workspace_root| { + workspace_root + .join(&legacy_path_fragment) + .canonicalize() + .map(StrippedVerbatimPath::to_stripped_verbatim_path) + }), + ) { + (Ok(path), _) => path, + (_, Some(Ok(path))) => path, + _ => panic!("Could not find rescript-legacy"), + } +} + pub fn string_ends_with_any(s: &Path, suffixes: &[&str]) -> bool { suffixes .iter() diff --git a/rewatch/src/lib.rs b/rewatch/src/lib.rs index 9dc6f5591c..2df92a48f3 100644 --- a/rewatch/src/lib.rs +++ b/rewatch/src/lib.rs @@ -1,4 +1,5 @@ pub mod build; +pub mod cli; pub mod cmd; pub mod config; pub mod helpers; diff --git a/rewatch/src/main.rs b/rewatch/src/main.rs index 5c27a4d02b..dcebec5945 100644 --- a/rewatch/src/main.rs +++ b/rewatch/src/main.rs @@ -1,93 +1,17 @@ use anyhow::Result; -use clap::{Parser, ValueEnum}; -use clap_verbosity_flag::InfoLevel; +use clap::Parser; use log::LevelFilter; use regex::Regex; -use std::io::Write; -use std::path::{Path, PathBuf}; +use std::{ + io::Write, + path::{Path, PathBuf}, +}; -use rewatch::{build, cmd, lock, watcher}; - -#[derive(Debug, Clone, ValueEnum)] -enum Command { - /// Build using Rewatch - Build, - /// Build, then start a watcher - Watch, - /// Clean the build artifacts - Clean, -} - -/// Rewatch is an alternative build system for the Rescript Compiler bsb (which uses Ninja internally). It strives -/// to deliver consistent and faster builds in monorepo setups with multiple packages, where the -/// default build system fails to pick up changed interfaces across multiple packages. -#[derive(Parser, Debug)] -#[command(version)] -struct Args { - #[arg(value_enum)] - command: Option, - - /// The relative path to where the main rescript.json resides. IE - the root of your project. - folder: Option, - - /// Filter allows for a regex to be supplied which will filter the files to be compiled. For - /// instance, to filter out test files for compilation while doing feature work. - #[arg(short, long)] - filter: Option, - - /// This allows one to pass an additional command to the watcher, which allows it to run when - /// finished. For instance, to play a sound when done compiling, or to run a test suite. - /// NOTE - You may need to add '--color=always' to your subcommand in case you want to output - /// colour as well - #[arg(short, long)] - after_build: Option, - - // Disable timing on the output - #[arg(short, long, default_value = "false", num_args = 0..=1)] - no_timing: bool, - - // simple output for snapshot testing - #[arg(short, long, default_value = "false", num_args = 0..=1)] - snapshot_output: bool, - - /// Verbosity: - /// -v -> Debug - /// -vv -> Trace - /// -q -> Warn - /// -qq -> Error - /// -qqq -> Off. - /// Default (/ no argument given): 'info' - #[command(flatten)] - verbose: clap_verbosity_flag::Verbosity, - - /// This creates a source_dirs.json file at the root of the monorepo, which is needed when you - /// want to use Reanalyze - #[arg(short, long, default_value_t = false, num_args = 0..=1)] - create_sourcedirs: bool, - - /// This prints the compiler arguments. It expects the path to a rescript.json file. - /// This also requires --bsc-path and --rescript-version to be present - #[arg(long)] - compiler_args: Option, - - /// This is the flag to also compile development dependencies - /// It's important to know that we currently do not discern between project src, and - /// dependencies. So enabling this flag will enable building _all_ development dependencies of - /// _all_ packages - #[arg(long, default_value_t = false, num_args = 0..=1)] - dev: bool, - - /// To be used in conjunction with compiler_args - #[arg(long)] - rescript_version: Option, - - /// A custom path to bsc - #[arg(long)] - bsc_path: Option, -} +use rewatch::{build, cli, cmd, lock, watcher}; fn main() -> Result<()> { - let args = Args::parse(); + let args = cli::Cli::parse(); + let log_level_filter = args.verbose.log_level_filter(); env_logger::Builder::new() @@ -96,82 +20,113 @@ fn main() -> Result<()> { .target(env_logger::fmt::Target::Stdout) .init(); - let command = args.command.unwrap_or(Command::Build); - let folder = args.folder.unwrap_or(".".to_string()); - let filter = args - .filter - .map(|filter| Regex::new(filter.as_ref()).expect("Could not parse regex")); + let command = args.command.unwrap_or(cli::Command::Build(args.build_args)); - match args.compiler_args { - None => (), - Some(path) => { + // handle those commands early, because we don't need a lock for them + match command.clone() { + cli::Command::Legacy { legacy_args } => { + let code = build::pass_through_legacy(legacy_args); + std::process::exit(code); + } + cli::Command::Format { mut format_args } => { + format_args.insert(0, "format".into()); + let code = build::pass_through_legacy(format_args); + std::process::exit(code); + } + cli::Command::Dump { mut dump_args } => { + dump_args.insert(0, "dump".into()); + let code = build::pass_through_legacy(dump_args); + std::process::exit(code); + } + cli::Command::CompilerArgs { + path, + dev, + rescript_version, + bsc_path, + } => { println!( "{}", build::get_compiler_args( Path::new(&path), - args.rescript_version, - &args.bsc_path.map(PathBuf::from), - args.dev + rescript_version, + &bsc_path.map(PathBuf::from), + dev )? ); std::process::exit(0); } + _ => (), } // The 'normal run' mode will show the 'pretty' formatted progress. But if we turn off the log // level, we should never show that. let show_progress = log_level_filter == LevelFilter::Info; - match lock::get(&folder) { + match lock::get(&args.folder) { lock::Lock::Error(ref e) => { println!("Could not start Rewatch: {e}"); std::process::exit(1) } lock::Lock::Aquired(_) => match command { - Command::Clean => build::clean::clean( - Path::new(&folder), + cli::Command::Clean { + bsc_path, + snapshot_output, + } => build::clean::clean( + Path::new(&args.folder), show_progress, - &args.bsc_path.map(PathBuf::from), - args.dev, - args.snapshot_output, + &bsc_path.map(PathBuf::from), + snapshot_output, ), - Command::Build => { + cli::Command::Build(build_args) => { + let filter = build_args + .filter + .map(|filter| Regex::new(filter.as_ref()).expect("Could not parse regex")); + match build::build( &filter, - Path::new(&folder), + Path::new(&args.folder), show_progress, - args.no_timing, - args.create_sourcedirs, - &args.bsc_path.map(PathBuf::from), - args.dev, - args.snapshot_output, + build_args.no_timing, + build_args.create_sourcedirs, + &build_args.bsc_path.map(PathBuf::from), + build_args.dev, + build_args.snapshot_output, ) { Err(e) => { println!("{e}"); std::process::exit(1) } Ok(_) => { - if let Some(args_after_build) = args.after_build { + if let Some(args_after_build) = build_args.after_build { cmd::run(args_after_build) } std::process::exit(0) } }; } - Command::Watch => { + cli::Command::Watch(watch_args) => { + let filter = watch_args + .filter + .map(|filter| Regex::new(filter.as_ref()).expect("Could not parse regex")); watcher::start( &filter, show_progress, - &folder, - args.after_build, - args.create_sourcedirs, - args.dev, - args.bsc_path, - args.snapshot_output, + &args.folder, + watch_args.after_build, + watch_args.create_sourcedirs, + watch_args.dev, + watch_args.bsc_path, + watch_args.snapshot_output, ); Ok(()) } + cli::Command::CompilerArgs { .. } + | cli::Command::Legacy { .. } + | cli::Command::Format { .. } + | cli::Command::Dump { .. } => { + unreachable!("command already handled") + } }, } } diff --git a/rewatch/tests/compile.sh b/rewatch/tests/compile.sh index fd035bf902..f694b3b0bb 100755 --- a/rewatch/tests/compile.sh +++ b/rewatch/tests/compile.sh @@ -33,32 +33,32 @@ fi node ./packages/main/src/Main.mjs > ./packages/main/src/output.txt mv ./packages/main/src/Main.res ./packages/main/src/Main2.res -rewatch build &> ../tests/snapshots/rename-file.txt +rewatch build --snapshot-output &> ../tests/snapshots/rename-file.txt mv ./packages/main/src/Main2.res ./packages/main/src/Main.res # Rename a file with a dependent - this should trigger an error mv ./packages/main/src/InternalDep.res ./packages/main/src/InternalDep2.res -rewatch build &> ../tests/snapshots/rename-file-internal-dep.txt +rewatch build --snapshot-output &> ../tests/snapshots/rename-file-internal-dep.txt # normalize paths so the snapshot is the same on all machines normalize_paths ../tests/snapshots/rename-file-internal-dep.txt mv ./packages/main/src/InternalDep2.res ./packages/main/src/InternalDep.res # Rename a file with a dependent in a namespaced package - this should trigger an error (regression) mv ./packages/new-namespace/src/Other_module.res ./packages/new-namespace/src/Other_module2.res -rewatch build &> ../tests/snapshots/rename-file-internal-dep-namespace.txt +rewatch build --snapshot-output &> ../tests/snapshots/rename-file-internal-dep-namespace.txt # normalize paths so the snapshot is the same on all machines normalize_paths ../tests/snapshots/rename-file-internal-dep-namespace.txt mv ./packages/new-namespace/src/Other_module2.res ./packages/new-namespace/src/Other_module.res rewatch build &> /dev/null mv ./packages/main/src/ModuleWithInterface.resi ./packages/main/src/ModuleWithInterface2.resi -rewatch build &> ../tests/snapshots/rename-interface-file.txt +rewatch build --snapshot-output &> ../tests/snapshots/rename-interface-file.txt # normalize paths so the snapshot is the same on all machines normalize_paths ../tests/snapshots/rename-interface-file.txt mv ./packages/main/src/ModuleWithInterface2.resi ./packages/main/src/ModuleWithInterface.resi rewatch build &> /dev/null mv ./packages/main/src/ModuleWithInterface.res ./packages/main/src/ModuleWithInterface2.res -rewatch build &> ../tests/snapshots/rename-file-with-interface.txt +rewatch build --snapshot-output &> ../tests/snapshots/rename-file-with-interface.txt # normalize paths so the snapshot is the same on all machines normalize_paths ../tests/snapshots/rename-file-with-interface.txt mv ./packages/main/src/ModuleWithInterface2.res ./packages/main/src/ModuleWithInterface.res @@ -66,7 +66,7 @@ rewatch build &> /dev/null # when deleting a file that other files depend on, the compile should fail rm packages/dep02/src/Dep02.res -rewatch build &> ../tests/snapshots/remove-file.txt +rewatch build --snapshot-output &> ../tests/snapshots/remove-file.txt # normalize paths so the snapshot is the same on all machines normalize_paths ../tests/snapshots/remove-file.txt git checkout -- packages/dep02/src/Dep02.res @@ -74,7 +74,7 @@ rewatch build &> /dev/null # it should show an error when we have a dependency cycle echo 'Dep01.log()' >> packages/new-namespace/src/NS_alias.res -rewatch build &> ../tests/snapshots/dependency-cycle.txt +rewatch build --snapshot-output &> ../tests/snapshots/dependency-cycle.txt git checkout -- packages/new-namespace/src/NS_alias.res # it should compile dev dependencies with the --dev flag @@ -95,7 +95,7 @@ else exit 1 fi -rewatch clean --dev &> /dev/null +rewatch clean &> /dev/null file_count=$(find ./packages/with-dev-deps -name *.mjs | wc -l) if [ "$file_count" -eq 0 ]; then diff --git a/rewatch/tests/suite-ci.sh b/rewatch/tests/suite-ci.sh index d36319b988..9e5eefe1a8 100755 --- a/rewatch/tests/suite-ci.sh +++ b/rewatch/tests/suite-ci.sh @@ -8,10 +8,13 @@ cd $(dirname $0) # Get rewatch executable location from the first argument or use default if [ -n "$1" ]; then REWATCH_EXECUTABLE="$1" + BSC_PATH="" else - REWATCH_EXECUTABLE="../target/release/rewatch --bsc-path ../../_build/install/default/bin/bsc" + REWATCH_EXECUTABLE="../target/release/rewatch" + BSC_PATH="--bsc-path ../../_build/install/default/bin/bsc" fi export REWATCH_EXECUTABLE +export BSC_PATH source ./utils.sh diff --git a/rewatch/tests/utils.sh b/rewatch/tests/utils.sh index 1dffbe8a8d..44a22ba0e3 100644 --- a/rewatch/tests/utils.sh +++ b/rewatch/tests/utils.sh @@ -3,8 +3,8 @@ overwrite() { echo -e "\r\033[1A\033[0K$@"; } success() { echo -e "- ✅ \033[32m$1\033[0m"; } error() { echo -e "- 🛑 \033[31m$1\033[0m"; } bold() { echo -e "\033[1m$1\033[0m"; } -rewatch() { RUST_BACKTRACE=1 $REWATCH_EXECUTABLE --no-timing=true --snapshot-output=true $@; } -rewatch_bg() { RUST_BACKTRACE=1 nohup $REWATCH_EXECUTABLE --no-timing=true --snapshot-output=true $@; } +rewatch() { RUST_BACKTRACE=1 $REWATCH_EXECUTABLE $@ $BSC_PATH; } +rewatch_bg() { RUST_BACKTRACE=1 nohup $REWATCH_EXECUTABLE $@ $BSC_PATH; } # Detect if running on Windows is_windows() {