diff --git a/Cargo.lock b/Cargo.lock index f28bb3cfcde..1c620936ee3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1283,6 +1283,34 @@ dependencies = [ "similar", ] +[[package]] +name = "moon_action" +version = "0.1.0" + +[[package]] +name = "moon_action_runner" +version = "0.1.0" +dependencies = [ + "insta", + "moon_action", + "moon_cache", + "moon_config", + "moon_error", + "moon_hasher", + "moon_lang", + "moon_lang_node", + "moon_logger", + "moon_project", + "moon_terminal", + "moon_toolchain", + "moon_utils", + "moon_vcs", + "moon_workspace", + "petgraph", + "thiserror", + "tokio", +] + [[package]] name = "moon_cache" version = "0.1.0" @@ -1309,6 +1337,8 @@ dependencies = [ "indicatif", "insta", "itertools", + "moon_action", + "moon_action_runner", "moon_cache", "moon_config", "moon_lang", @@ -1497,19 +1527,14 @@ dependencies = [ name = "moon_workspace" version = "0.1.0" dependencies = [ - "insta", "moon_cache", "moon_config", "moon_error", - "moon_hasher", - "moon_lang_node", "moon_logger", "moon_project", - "moon_terminal", "moon_toolchain", "moon_utils", "moon_vcs", - "petgraph", "thiserror", "tokio", ] diff --git a/crates/action-runner/Cargo.toml b/crates/action-runner/Cargo.toml new file mode 100644 index 00000000000..8b341074b9d --- /dev/null +++ b/crates/action-runner/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "moon_action_runner" +version = "0.1.0" +edition = "2021" + +[dependencies] +moon_action = { path = "../action" } +moon_cache = { path = "../cache" } +moon_config = { path = "../config" } +moon_error = { path = "../error" } +moon_hasher = { path = "../hasher" } +moon_lang = { path = "../lang" } +moon_lang_node = { path = "../lang-node" } +moon_logger = { path = "../logger" } +moon_project = { path = "../project" } +moon_terminal = { path = "../terminal" } +moon_toolchain = { path = "../toolchain" } +moon_utils = { path = "../utils" } +moon_vcs = { path = "../vcs" } +moon_workspace = { path = "../workspace" } +petgraph = "0.6.0" +thiserror = "1.0.31" +tokio = { version = "1.18.2", features = ["full"] } + +[dev-dependencies] +insta = "1.14.0" diff --git a/crates/workspace/src/actions/install_node_deps.rs b/crates/action-runner/src/actions/install_node_deps.rs similarity index 95% rename from crates/workspace/src/actions/install_node_deps.rs rename to crates/action-runner/src/actions/install_node_deps.rs index 60755016e12..3d07e85dd31 100644 --- a/crates/workspace/src/actions/install_node_deps.rs +++ b/crates/action-runner/src/actions/install_node_deps.rs @@ -1,11 +1,12 @@ -use crate::action::ActionStatus; -use crate::errors::WorkspaceError; -use crate::workspace::Workspace; +use crate::context::ActionRunnerContext; +use crate::errors::ActionRunnerError; +use moon_action::{Action, ActionStatus}; use moon_config::PackageManager; use moon_error::map_io_to_fs_error; use moon_logger::{color, debug, warn}; use moon_terminal::output::{label_checkpoint, Checkpoint}; use moon_utils::{fs, is_offline}; +use moon_workspace::Workspace; use std::sync::Arc; use tokio::sync::RwLock; @@ -62,8 +63,10 @@ fn add_engines_constraint(workspace: &mut Workspace) -> bool { } pub async fn install_node_deps( + _action: &mut Action, + _context: &ActionRunnerContext, workspace: Arc>, -) -> Result { +) -> Result { // Writes root `package.json` { let mut workspace = workspace.write().await; diff --git a/crates/action-runner/src/actions/mod.rs b/crates/action-runner/src/actions/mod.rs new file mode 100644 index 00000000000..1349a801ccc --- /dev/null +++ b/crates/action-runner/src/actions/mod.rs @@ -0,0 +1,10 @@ +mod install_node_deps; +mod run_target; +mod setup_toolchain; +mod sync_node_project; +mod target; + +pub use install_node_deps::*; +pub use run_target::*; +pub use setup_toolchain::*; +pub use sync_node_project::*; diff --git a/crates/workspace/src/actions/run_target.rs b/crates/action-runner/src/actions/run_target.rs similarity index 68% rename from crates/workspace/src/actions/run_target.rs rename to crates/action-runner/src/actions/run_target.rs index 6fca4e69985..0779db60c0b 100644 --- a/crates/workspace/src/actions/run_target.rs +++ b/crates/action-runner/src/actions/run_target.rs @@ -1,17 +1,18 @@ -use crate::action::{Action, ActionStatus, Attempt}; -use crate::actions::hashing::create_target_hasher; -use crate::errors::WorkspaceError; -use crate::workspace::Workspace; +use crate::actions::target::{ + create_node_target_command, create_system_target_command, create_target_hasher, +}; +use crate::context::ActionRunnerContext; +use crate::errors::ActionRunnerError; +use moon_action::{Action, ActionStatus, Attempt}; use moon_cache::RunTargetState; use moon_config::TaskType; use moon_logger::{color, debug, warn}; use moon_project::{Project, Target, Task}; use moon_terminal::output::{label_checkpoint, Checkpoint}; -use moon_toolchain::{get_path_env_var, Executable}; use moon_utils::process::{join_args, output_to_string, Command, Output}; -use moon_utils::{is_ci, is_test_env, path, string_vec, time}; +use moon_utils::{is_ci, is_test_env, path, time}; +use moon_workspace::Workspace; use std::collections::HashMap; -use std::path::Path; use std::sync::Arc; use tokio::sync::RwLock; @@ -21,7 +22,7 @@ async fn create_env_vars( workspace: &Workspace, project: &Project, task: &Task, -) -> Result, WorkspaceError> { +) -> Result, ActionRunnerError> { let mut env_vars = HashMap::new(); env_vars.insert( @@ -59,140 +60,11 @@ async fn create_env_vars( Ok(env_vars) } -fn create_node_options(task: &Task) -> Vec { - string_vec![ - // "--inspect", // Enable node inspector - "--preserve-symlinks", - "--title", - &task.target, - "--unhandled-rejections", - "throw", - ] -} - -/// Runs a task command through our toolchain's installed Node.js instance. -/// We accomplish this by executing the Node.js binary as a child process, -/// while passing a file path to a package's node module binary (this is the file -/// being executed). We then also pass arguments defined in the task. -/// This would look something like the following: -/// -/// ~/.moon/tools/node/1.2.3/bin/node --inspect /path/to/node_modules/.bin/eslint -/// --cache --color --fix --ext .ts,.tsx,.js,.jsx -#[cfg(not(windows))] -#[track_caller] -fn create_node_target_command( - workspace: &Workspace, - project: &Project, - task: &Task, -) -> Result { - let node = workspace.toolchain.get_node(); - let mut cmd = node.get_bin_path(); - let mut args = vec![]; - - match task.command.as_str() { - "node" => { - args.extend(create_node_options(task)); - } - "npm" => { - cmd = node.get_npm().get_bin_path(); - } - "pnpm" => { - cmd = node.get_pnpm().unwrap().get_bin_path(); - } - "yarn" => { - cmd = node.get_yarn().unwrap().get_bin_path(); - } - bin => { - let bin_path = node.find_package_bin(bin, &project.root)?; - - args.extend(create_node_options(task)); - args.push(path::path_to_string(&bin_path)?); - } - }; - - // Create the command - let mut command = Command::new(cmd); - - command.args(&args).args(&task.args).envs(&task.env).env( - "PATH", - get_path_env_var(node.get_bin_path().parent().unwrap()), - ); - - Ok(command) -} - -/// Windows works quite differently than other systems, so we cannot do the above. -/// On Windows, the package binary is a ".cmd" file, which means it needs to run -/// through "cmd.exe" and not "node.exe". Because of this, the order of operations -/// is switched, and "node.exe" is detected through the `PATH` env var. -#[cfg(windows)] -#[track_caller] -fn create_node_target_command( - workspace: &Workspace, - project: &Project, - task: &Task, -) -> Result { - use moon_lang_node::node; - - let node = workspace.toolchain.get_node(); - - let cmd = match task.command.as_str() { - "node" => node.get_bin_path().clone(), - "npm" => node.get_npm().get_bin_path().clone(), - "pnpm" => node.get_pnpm().unwrap().get_bin_path().clone(), - "yarn" => node.get_yarn().unwrap().get_bin_path().clone(), - bin => node.find_package_bin(bin, &project.root)?, - }; - - // Create the command - let mut command = Command::new(cmd); - - command - .args(&task.args) - .envs(&task.env) - .env( - "PATH", - get_path_env_var(node.get_bin_path().parent().unwrap()), - ) - .env( - "NODE_OPTIONS", - node::extend_node_options_env_var(&create_node_options(task).join(" ")), - ); - - Ok(command) -} - -#[cfg(not(windows))] -fn create_system_target_command(task: &Task, _cwd: &Path) -> Command { - let mut cmd = Command::new(&task.command); - cmd.args(&task.args).envs(&task.env); - cmd -} - -#[cfg(windows)] -fn create_system_target_command(task: &Task, cwd: &Path) -> Command { - use moon_utils::process::is_windows_script; - - let mut cmd = Command::new(&task.command); - - for arg in &task.args { - // cmd.exe requires an absolute path to batch files - if is_windows_script(arg) { - cmd.arg(cwd.join(arg)); - } else { - cmd.arg(arg); - } - } - - cmd.envs(&task.env); - cmd -} - async fn create_target_command( workspace: &Workspace, project: &Project, task: &Task, -) -> Result { +) -> Result { let working_dir = if task.options.run_from_workspace_root { &workspace.root } else { @@ -223,12 +95,11 @@ async fn create_target_command( } pub async fn run_target( - workspace: Arc>, action: &mut Action, + context: &ActionRunnerContext, + workspace: Arc>, target_id: &str, - primary_target: &str, - passthrough_args: &[String], -) -> Result { +) -> Result { debug!( target: LOG_TARGET, "Running target {}", @@ -239,13 +110,14 @@ pub async fn run_target( let mut cache = workspace.cache.cache_run_target_state(target_id).await?; // Gather the project and task - let is_primary = primary_target == target_id; + let is_primary = context.primary_targets.contains(target_id); let (project_id, task_id) = Target::parse(target_id)?.ids()?; let project = workspace.projects.load(&project_id)?; let task = project.get_task(&task_id)?; // Abort early if this build has already been cached/hashed - let hasher = create_target_hasher(&workspace, &project, task, passthrough_args).await?; + let hasher = + create_target_hasher(&workspace, &project, task, &context.passthrough_args).await?; let hash = hasher.to_hash(); debug!( @@ -275,7 +147,10 @@ pub async fn run_target( // Build the command to run based on the task let mut command = create_target_command(&workspace, &project, task).await?; - command.args(passthrough_args); + + if !context.passthrough_args.is_empty() { + command.args(&context.passthrough_args); + } if workspace .config @@ -302,7 +177,7 @@ pub async fn run_target( // Print label *before* output is streamed since it may stay open forever, // or it may use ANSI escape codes to alter the terminal. print_target_label(target_id, &attempt, attempt_total, Checkpoint::Pass); - print_target_command(&workspace, &project, task, passthrough_args); + print_target_command(&workspace, &project, task, &context.passthrough_args); // If this target matches the primary target (the last task to run), // then we want to stream the output directly to the parent (inherit mode). @@ -311,7 +186,7 @@ pub async fn run_target( .await } else { print_target_label(target_id, &attempt, attempt_total, Checkpoint::Start); - print_target_command(&workspace, &project, task, passthrough_args); + print_target_command(&workspace, &project, task, &context.passthrough_args); // Otherwise we run the process in the background and write the output // once it has completed. @@ -335,7 +210,9 @@ pub async fn run_target( output = out; break; } else if attempt_index >= attempt_total { - return Err(WorkspaceError::Moon(command.output_to_error(&out, false))); + return Err(ActionRunnerError::Moon( + command.output_to_error(&out, false), + )); } else { attempt_index += 1; @@ -349,7 +226,7 @@ pub async fn run_target( } // process itself failed Err(error) => { - return Err(WorkspaceError::Moon(error)); + return Err(ActionRunnerError::Moon(error)); } } } diff --git a/crates/workspace/src/actions/setup_toolchain.rs b/crates/action-runner/src/actions/setup_toolchain.rs similarity index 81% rename from crates/workspace/src/actions/setup_toolchain.rs rename to crates/action-runner/src/actions/setup_toolchain.rs index 39def09913d..4328e5d5a5c 100644 --- a/crates/workspace/src/actions/setup_toolchain.rs +++ b/crates/action-runner/src/actions/setup_toolchain.rs @@ -1,7 +1,8 @@ -use crate::action::ActionStatus; -use crate::errors::WorkspaceError; -use crate::workspace::Workspace; +use crate::context::ActionRunnerContext; +use crate::errors::ActionRunnerError; +use moon_action::{Action, ActionStatus}; use moon_logger::debug; +use moon_workspace::Workspace; use std::sync::Arc; use tokio::sync::RwLock; @@ -10,8 +11,10 @@ const MINUTE: u128 = SECOND * 60; const HOUR: u128 = MINUTE * 60; pub async fn setup_toolchain( + _action: &mut Action, + _context: &ActionRunnerContext, workspace: Arc>, -) -> Result { +) -> Result { debug!( target: "moon:action:setup-toolchain", "Setting up toolchain", diff --git a/crates/workspace/src/actions/sync_project.rs b/crates/action-runner/src/actions/sync_node_project.rs similarity index 93% rename from crates/workspace/src/actions/sync_project.rs rename to crates/action-runner/src/actions/sync_node_project.rs index a2999d2fae9..e5e0454fd40 100644 --- a/crates/workspace/src/actions/sync_project.rs +++ b/crates/action-runner/src/actions/sync_node_project.rs @@ -1,17 +1,19 @@ -use crate::action::ActionStatus; -use crate::errors::WorkspaceError; -use crate::workspace::Workspace; +use crate::context::ActionRunnerContext; +use crate::errors::ActionRunnerError; +use moon_action::{Action, ActionStatus}; use moon_config::{tsconfig::TsConfigJson, TypeScriptConfig}; use moon_logger::{color, debug}; use moon_project::Project; use moon_utils::is_ci; use moon_utils::path::relative_from; +use moon_workspace::Workspace; use std::path::PathBuf; use std::sync::Arc; use tokio::sync::RwLock; -const LOG_TARGET: &str = "moon:action:sync-project"; +const LOG_TARGET: &str = "moon:action:sync-node-project"; +// Sync projects references to the root `tsconfig.json`. fn sync_root_tsconfig( tsconfig: &mut TsConfigJson, typescript_config: &TypeScriptConfig, @@ -36,10 +38,12 @@ fn sync_root_tsconfig( false } -pub async fn sync_project( +pub async fn sync_node_project( + _action: &mut Action, + _context: &ActionRunnerContext, workspace: Arc>, project_id: &str, -) -> Result { +) -> Result { let mut mutated_files = false; let mut typescript_config; diff --git a/crates/workspace/src/actions/hashing/target.rs b/crates/action-runner/src/actions/target/hasher.rs similarity index 95% rename from crates/workspace/src/actions/hashing/target.rs rename to crates/action-runner/src/actions/target/hasher.rs index b8c7ea3def1..d6734574c3e 100644 --- a/crates/workspace/src/actions/hashing/target.rs +++ b/crates/action-runner/src/actions/target/hasher.rs @@ -1,13 +1,14 @@ -use crate::{Workspace, WorkspaceError}; +use crate::errors::ActionRunnerError; use moon_hasher::TargetHasher; use moon_project::{ExpandedFiles, Project, Task}; use moon_utils::path::path_to_string; +use moon_workspace::Workspace; use std::path::Path; fn convert_paths_to_strings( paths: &ExpandedFiles, workspace_root: &Path, -) -> Result, WorkspaceError> { +) -> Result, ActionRunnerError> { let mut files: Vec = vec![]; for path in paths { @@ -33,7 +34,7 @@ pub async fn create_target_hasher( project: &Project, task: &Task, passthrough_args: &[String], -) -> Result { +) -> Result { let vcs = &workspace.vcs; let globset = task.create_globset()?; let mut hasher = TargetHasher::new(workspace.config.node.version.clone()); diff --git a/crates/action-runner/src/actions/target/mod.rs b/crates/action-runner/src/actions/target/mod.rs new file mode 100644 index 00000000000..8dda305e10d --- /dev/null +++ b/crates/action-runner/src/actions/target/mod.rs @@ -0,0 +1,7 @@ +mod hasher; +mod node; +mod system; + +pub use hasher::create_target_hasher; +pub use node::*; +pub use system::*; diff --git a/crates/action-runner/src/actions/target/node.rs b/crates/action-runner/src/actions/target/node.rs new file mode 100644 index 00000000000..d82c964041d --- /dev/null +++ b/crates/action-runner/src/actions/target/node.rs @@ -0,0 +1,111 @@ +use crate::errors::ActionRunnerError; +use moon_project::{Project, Task}; +use moon_toolchain::{get_path_env_var, Executable}; +use moon_utils::process::Command; +use moon_utils::string_vec; +use moon_workspace::Workspace; + +fn create_node_options(task: &Task) -> Vec { + string_vec![ + // "--inspect", // Enable node inspector + "--preserve-symlinks", + "--title", + &task.target, + "--unhandled-rejections", + "throw", + ] +} + +/// Runs a task command through our toolchain's installed Node.js instance. +/// We accomplish this by executing the Node.js binary as a child process, +/// while passing a file path to a package's node module binary (this is the file +/// being executed). We then also pass arguments defined in the task. +/// This would look something like the following: +/// +/// ~/.moon/tools/node/1.2.3/bin/node --inspect /path/to/node_modules/.bin/eslint +/// --cache --color --fix --ext .ts,.tsx,.js,.jsx +#[cfg(not(windows))] +#[track_caller] +pub fn create_node_target_command( + workspace: &Workspace, + project: &Project, + task: &Task, +) -> Result { + use moon_utils::path; + + let node = workspace.toolchain.get_node(); + let mut cmd = node.get_bin_path(); + let mut args = vec![]; + + match task.command.as_str() { + "node" => { + args.extend(create_node_options(task)); + } + "npm" => { + cmd = node.get_npm().get_bin_path(); + } + "pnpm" => { + cmd = node.get_pnpm().unwrap().get_bin_path(); + } + "yarn" => { + cmd = node.get_yarn().unwrap().get_bin_path(); + } + bin => { + let bin_path = node.find_package_bin(bin, &project.root)?; + + args.extend(create_node_options(task)); + args.push(path::path_to_string(&bin_path)?); + } + }; + + // Create the command + let mut command = Command::new(cmd); + + command.args(&args).args(&task.args).envs(&task.env).env( + "PATH", + get_path_env_var(node.get_bin_path().parent().unwrap()), + ); + + Ok(command) +} + +/// Windows works quite differently than other systems, so we cannot do the above. +/// On Windows, the package binary is a ".cmd" file, which means it needs to run +/// through "cmd.exe" and not "node.exe". Because of this, the order of operations +/// is switched, and "node.exe" is detected through the `PATH` env var. +#[cfg(windows)] +#[track_caller] +pub fn create_node_target_command( + workspace: &Workspace, + project: &Project, + task: &Task, +) -> Result { + use moon_lang_node::node; + + let node = workspace.toolchain.get_node(); + + let cmd = match task.command.as_str() { + "node" => node.get_bin_path().clone(), + "npm" => node.get_npm().get_bin_path().clone(), + "pnpm" => node.get_pnpm().unwrap().get_bin_path().clone(), + "yarn" => node.get_yarn().unwrap().get_bin_path().clone(), + bin => node.find_package_bin(bin, &project.root)?, + }; + + // Create the command + let mut command = Command::new(cmd); + + command + .args(&task.args) + .envs(&task.env) + .env( + "PATH", + get_path_env_var(node.get_bin_path().parent().unwrap()), + ) + .env( + "NODE_OPTIONS", + node::extend_node_options_env_var(&create_node_options(task).join(" ")), + ); + + Ok(command) +} diff --git a/crates/action-runner/src/actions/target/system.rs b/crates/action-runner/src/actions/target/system.rs new file mode 100644 index 00000000000..79288c91497 --- /dev/null +++ b/crates/action-runner/src/actions/target/system.rs @@ -0,0 +1,29 @@ +use moon_project::Task; +use moon_utils::process::Command; +use std::path::Path; + +#[cfg(not(windows))] +pub fn create_system_target_command(task: &Task, _cwd: &Path) -> Command { + let mut cmd = Command::new(&task.command); + cmd.args(&task.args).envs(&task.env); + cmd +} + +#[cfg(windows)] +pub fn create_system_target_command(task: &Task, cwd: &Path) -> Command { + use moon_utils::process::is_windows_script; + + let mut cmd = Command::new(&task.command); + + for arg in &task.args { + // cmd.exe requires an absolute path to batch files + if is_windows_script(arg) { + cmd.arg(cwd.join(arg)); + } else { + cmd.arg(arg); + } + } + + cmd.envs(&task.env); + cmd +} diff --git a/crates/action-runner/src/context.rs b/crates/action-runner/src/context.rs new file mode 100644 index 00000000000..9443769e492 --- /dev/null +++ b/crates/action-runner/src/context.rs @@ -0,0 +1,8 @@ +use std::collections::HashSet; + +#[derive(Default)] +pub struct ActionRunnerContext { + pub passthrough_args: Vec, + + pub primary_targets: HashSet, +} diff --git a/crates/action-runner/src/dep_graph.rs b/crates/action-runner/src/dep_graph.rs new file mode 100644 index 00000000000..6538d90e7f4 --- /dev/null +++ b/crates/action-runner/src/dep_graph.rs @@ -0,0 +1,424 @@ +use crate::errors::DepGraphError; +use moon_config::ProjectLanguage; +use moon_lang::SupportedLanguage; +use moon_logger::{color, debug, map_list, trace, warn}; +use moon_project::{ + Project, ProjectGraph, ProjectID, Target, TargetError, TargetID, TargetProject, + TouchedFilePaths, +}; +use petgraph::algo::toposort; +use petgraph::dot::{Config, Dot}; +use petgraph::graph::DiGraph; +use petgraph::Graph; +use std::collections::{HashMap, HashSet}; +use std::hash::{Hash, Hasher}; + +pub use petgraph::graph::NodeIndex; + +const TARGET: &str = "moon:dep-graph"; + +#[derive(Clone, Eq)] +pub enum Node { + InstallDeps(SupportedLanguage), + RunTarget(TargetID), + SetupToolchain, + SyncProject(SupportedLanguage, ProjectID), +} + +impl Node { + pub fn label(&self) -> String { + match self { + Node::InstallDeps(lang) => format!("InstallDeps({})", lang), + Node::RunTarget(id) => format!("RunTarget({})", id), + Node::SetupToolchain => "SetupToolchain".into(), + Node::SyncProject(lang, id) => { + format!("SyncProject({}, {})", lang, id) + } + } + } +} + +impl PartialEq for Node { + fn eq(&self, other: &Self) -> bool { + self.label() == other.label() + } +} + +impl Hash for Node { + fn hash(&self, state: &mut H) { + self.label().hash(state); + } +} + +fn get_lang_from_project(project: &Project) -> SupportedLanguage { + if let Some(cfg) = &project.config { + return match cfg.language { + ProjectLanguage::JavaScript | ProjectLanguage::TypeScript => SupportedLanguage::Node, + _ => SupportedLanguage::System, + }; + } + + SupportedLanguage::System +} + +pub type DepGraphType = DiGraph; +pub type BatchedTopoSort = Vec>; + +/// A directed acyclic graph (DAG) for the work that needs to be processed, based on a +/// project or task's dependency chain. This is also known as a "task graph" (not to +/// be confused with ours) or a "dependency graph". +pub struct DepGraph { + pub graph: DepGraphType, + + indices: HashMap, +} + +impl DepGraph { + pub fn default() -> Self { + debug!(target: TARGET, "Creating dependency graph",); + + let mut graph: DepGraphType = Graph::new(); + let setup_toolchain_index = graph.add_node(Node::SetupToolchain); + + DepGraph { + graph, + indices: HashMap::from([(Node::SetupToolchain, setup_toolchain_index)]), + } + } + + pub fn get_index_from_node(&self, node: &Node) -> Option<&NodeIndex> { + self.indices.get(node) + } + + pub fn get_node_from_index(&self, index: &NodeIndex) -> Option<&Node> { + self.graph.node_weight(*index) + } + + pub fn get_or_insert_node(&mut self, node: Node) -> NodeIndex { + if let Some(index) = self.get_index_from_node(&node) { + return *index; + } + + let index = self.graph.add_node(node.clone()); + + self.indices.insert(node, index); + + index + } + + pub fn install_deps(&mut self, lang: SupportedLanguage) -> NodeIndex { + let node = Node::InstallDeps(lang.clone()); + + if let Some(index) = self.get_index_from_node(&node) { + return *index; + } + + trace!(target: TARGET, "Installing {} dependencies", lang.label()); + + let setup_toolchain_index = self.get_or_insert_node(Node::SetupToolchain); + let install_deps_index = self.get_or_insert_node(node); + + self.graph + .add_edge(install_deps_index, setup_toolchain_index, ()); + + install_deps_index + } + + pub fn install_project_deps( + &mut self, + project_id: &str, + projects: &ProjectGraph, + ) -> Result { + let project = projects.load(project_id)?; + let lang = get_lang_from_project(&project); + + Ok(self.install_deps(lang)) + } + + pub fn run_target( + &mut self, + target: &Target, + projects: &ProjectGraph, + touched_files: Option<&TouchedFilePaths>, + ) -> Result { + let task_id = &target.task_id; + let mut inserted_count = 0; + + match &target.project { + // :task + TargetProject::All => { + for project_id in projects.ids() { + let project = projects.load(&project_id)?; + + if project.tasks.contains_key(task_id) + && self + .insert_target(&project_id, task_id, projects, touched_files)? + .is_some() + { + inserted_count += 1; + } + } + } + // ^:task + TargetProject::Deps => { + target.fail_with(TargetError::NoProjectDepsInRunContext)?; + } + // project:task + TargetProject::Id(project_id) => { + if self + .insert_target(project_id, task_id, projects, touched_files)? + .is_some() + { + inserted_count += 1; + } + } + // ~:task + TargetProject::Own => { + target.fail_with(TargetError::NoProjectSelfInRunContext)?; + } + }; + + Ok(inserted_count) + } + + pub fn run_target_dependents( + &mut self, + target: &Target, + projects: &ProjectGraph, + ) -> Result<(), DepGraphError> { + trace!( + target: TARGET, + "Adding dependents to run for target {}", + color::target(&target.id), + ); + + let (project_id, task_id) = target.ids()?; + let project = projects.load(&project_id)?; + let dependents = projects.get_dependents_of(&project)?; + + for dependent_id in dependents { + let dependent = projects.load(&dependent_id)?; + + if dependent.tasks.contains_key(&task_id) { + self.run_target(&Target::new(&dependent_id, &task_id)?, projects, None)?; + } + } + + Ok(()) + } + + pub fn sort_topological(&self) -> Result, DepGraphError> { + let list = match toposort(&self.graph, None) { + Ok(nodes) => nodes, + Err(error) => { + return Err(DepGraphError::CycleDetected( + self.get_node_from_index(&error.node_id()).unwrap().label(), + )); + } + }; + + Ok(list.into_iter().rev().collect()) + } + + pub fn sort_batched_topological(&self) -> Result { + let mut batches: BatchedTopoSort = vec![]; + + // Count how many times an index is referenced across nodes and edges + let mut node_counts = HashMap::::new(); + + for ix in self.graph.node_indices() { + node_counts.entry(ix).and_modify(|e| *e += 1).or_insert(0); + + for dep_ix in self.graph.neighbors(ix) { + node_counts + .entry(dep_ix) + .and_modify(|e| *e += 1) + .or_insert(0); + } + } + + // Gather root nodes (count of 0) + let mut root_nodes = HashSet::::new(); + + for (ix, count) in &node_counts { + if *count == 0 { + root_nodes.insert(*ix); + } + } + + // If no root nodes are found, but nodes exist, then we have a cycle + if root_nodes.is_empty() && !node_counts.is_empty() { + self.detect_cycle()?; + } + + while !root_nodes.is_empty() { + // Push this batch onto the list + batches.push(root_nodes.clone().into_iter().collect()); + + // Reset the root nodes and find new ones after decrementing + let mut next_root_nodes = HashSet::::new(); + + for ix in &root_nodes { + for dep_ix in self.graph.neighbors(*ix) { + let count = node_counts + .entry(dep_ix) + .and_modify(|e| *e -= 1) + .or_insert(0); + + if *count == 0 { + next_root_nodes.insert(dep_ix); + } + } + } + + root_nodes = next_root_nodes; + } + + Ok(batches.into_iter().rev().collect()) + } + + pub fn sync_project( + &mut self, + project_id: &str, + projects: &ProjectGraph, + ) -> Result { + let project = projects.load(project_id)?; + let lang = get_lang_from_project(&project); + let node = Node::SyncProject(lang, project_id.to_owned()); + + if let Some(index) = self.get_index_from_node(&node) { + return Ok(*index); + } + + trace!( + target: TARGET, + "Syncing project {} configs and dependencies", + color::id(project_id), + ); + + // Sync can be run in parallel while deps are installing + let setup_toolchain_index = self.get_or_insert_node(Node::SetupToolchain); + let sync_project_index = self.get_or_insert_node(node); + + self.graph + .add_edge(sync_project_index, setup_toolchain_index, ()); + + // But we need to wait on all dependent nodes + for dep_id in projects.get_dependencies_of(&project)? { + let sync_dep_project_index = self.sync_project(&dep_id, projects)?; + + self.graph + .add_edge(sync_project_index, sync_dep_project_index, ()); + } + + Ok(sync_project_index) + } + + pub fn to_dot(&self) -> String { + let graph = self.graph.map(|_, n| n.label(), |_, e| e); + let dot = Dot::with_config(&graph, &[Config::EdgeNoLabel]); + + format!("{:?}", dot) + } + + #[track_caller] + fn detect_cycle(&self) -> Result<(), DepGraphError> { + use petgraph::algo::kosaraju_scc; + + let scc = kosaraju_scc(&self.graph); + let cycle = scc + .last() + .unwrap() + .iter() + .map(|i| self.get_node_from_index(i).unwrap().label()) + .collect::>() + .join(" → "); + + Err(DepGraphError::CycleDetected(cycle)) + } + + fn insert_target( + &mut self, + project_id: &str, + task_id: &str, + projects: &ProjectGraph, + touched_files: Option<&TouchedFilePaths>, + ) -> Result, DepGraphError> { + let target_id = Target::format(project_id, task_id)?; + let node = Node::RunTarget(target_id.clone()); + + if let Some(index) = self.get_index_from_node(&node) { + return Ok(Some(*index)); + } + + let project = projects.load(project_id)?; + + // Compare against touched files if provided + if let Some(touched) = touched_files { + let globally_affected = projects.is_globally_affected(touched); + + if globally_affected { + warn!( + target: TARGET, + "Moon files touched, marking all targets as affected", + ); + } + + // Validate task exists for project + if !globally_affected && !project.get_task(task_id)?.is_affected(touched)? { + trace!( + target: TARGET, + "Project {} task {} not affected based on touched files, skipping", + color::id(project_id), + color::id(task_id), + ); + + return Ok(None); + } + } + + trace!( + target: TARGET, + "Target {} does not exist in the dependency graph, inserting", + color::target(&target_id), + ); + + // We should install deps & sync projects *before* running targets + let install_deps_index = self.install_project_deps(&project.id, projects)?; + let sync_project_index = self.sync_project(&project.id, projects)?; + let run_target_index = self.get_or_insert_node(node); + + self.graph + .add_edge(run_target_index, install_deps_index, ()); + self.graph + .add_edge(run_target_index, sync_project_index, ()); + + // And we also need to wait on all dependent nodes + let task = project.get_task(task_id)?; + + if !task.deps.is_empty() { + trace!( + target: TARGET, + "Adding dependencies {} from target {}", + map_list(&task.deps, |f| color::symbol(f)), + color::target(&target_id), + ); + + for dep_target_id in &task.deps { + let dep_target = Target::parse(dep_target_id)?; + + if let Some(run_dep_target_index) = self.insert_target( + &dep_target.project_id.unwrap(), + &dep_target.task_id, + projects, + touched_files, + )? { + self.graph + .add_edge(run_target_index, run_dep_target_index, ()); + } + } + } + + Ok(Some(run_target_index)) + } +} diff --git a/crates/action-runner/src/errors.rs b/crates/action-runner/src/errors.rs new file mode 100644 index 00000000000..bb37574b9b2 --- /dev/null +++ b/crates/action-runner/src/errors.rs @@ -0,0 +1,45 @@ +use moon_error::MoonError; +use moon_project::ProjectError; +use moon_toolchain::ToolchainError; +use moon_vcs::VcsError; +use moon_workspace::WorkspaceError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ActionRunnerError { + #[error("{0}")] + Failure(String), + + #[error(transparent)] + DepGraph(#[from] DepGraphError), + + #[error(transparent)] + Moon(#[from] MoonError), + + #[error(transparent)] + Project(#[from] ProjectError), + + #[error(transparent)] + Toolchain(#[from] ToolchainError), + + #[error(transparent)] + Vcs(#[from] VcsError), + + #[error(transparent)] + Workspace(#[from] WorkspaceError), +} + +#[derive(Error, Debug)] +pub enum DepGraphError { + #[error("A dependency cycle has been detected for {0}.")] + CycleDetected(String), + + #[error("Unknown node {0} found in dependency graph. How did this get here?")] + UnknownNode(usize), + + #[error(transparent)] + Moon(#[from] MoonError), + + #[error(transparent)] + Project(#[from] ProjectError), +} diff --git a/crates/action-runner/src/lib.rs b/crates/action-runner/src/lib.rs new file mode 100644 index 00000000000..8380b06d109 --- /dev/null +++ b/crates/action-runner/src/lib.rs @@ -0,0 +1,10 @@ +mod actions; +mod context; +mod dep_graph; +mod errors; +mod runner; + +pub use context::ActionRunnerContext; +pub use dep_graph::*; +pub use errors::*; +pub use runner::*; diff --git a/crates/workspace/src/action_runner.rs b/crates/action-runner/src/runner.rs similarity index 68% rename from crates/workspace/src/action_runner.rs rename to crates/action-runner/src/runner.rs index 890498d1dec..abb975ba135 100644 --- a/crates/workspace/src/action_runner.rs +++ b/crates/action-runner/src/runner.rs @@ -1,9 +1,11 @@ -use crate::action::{Action, ActionStatus}; -use crate::actions::{install_node_deps, run_target, setup_toolchain, sync_project}; +use crate::actions::{install_node_deps, run_target, setup_toolchain, sync_node_project}; +use crate::context::ActionRunnerContext; use crate::dep_graph::{DepGraph, Node}; -use crate::errors::WorkspaceError; -use crate::workspace::Workspace; +use crate::errors::{ActionRunnerError, DepGraphError}; +use moon_action::{Action, ActionStatus}; +use moon_lang::SupportedLanguage; use moon_logger::{color, debug, error, trace}; +use moon_workspace::Workspace; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::RwLock; @@ -12,26 +14,24 @@ use tokio::task; const LOG_TARGET: &str = "moon:action-runner"; async fn run_action( - workspace: Arc>, + node: &Node, action: &mut Action, - action_node: &Node, - primary_target: &str, - passthrough_args: &[String], -) -> Result<(), WorkspaceError> { - let result = match action_node { - Node::InstallNodeDeps => install_node_deps(workspace).await, - Node::RunTarget(target_id) => { - run_target( - workspace, - action, - target_id, - primary_target, - passthrough_args, - ) - .await - } - Node::SetupToolchain => setup_toolchain(workspace).await, - Node::SyncProject(project_id) => sync_project(workspace, project_id).await, + context: &ActionRunnerContext, + workspace: Arc>, +) -> Result<(), ActionRunnerError> { + let result = match node { + Node::InstallDeps(lang) => match lang { + SupportedLanguage::Node => install_node_deps(action, context, workspace).await, + _ => Ok(ActionStatus::Passed), + }, + Node::RunTarget(target_id) => run_target(action, context, workspace, target_id).await, + Node::SetupToolchain => setup_toolchain(action, context, workspace).await, + Node::SyncProject(lang, project_id) => match lang { + SupportedLanguage::Node => { + sync_node_project(action, context, workspace, project_id).await + } + _ => Ok(ActionStatus::Passed), + }, }; match result { @@ -42,9 +42,7 @@ async fn run_action( action.fail(error.to_string()); // If these fail, we should abort instead of trying to continue - if matches!(action_node, Node::SetupToolchain) - || matches!(action_node, Node::InstallNodeDeps) - { + if matches!(node, Node::SetupToolchain) || matches!(node, Node::InstallDeps(_)) { action.abort(); } } @@ -58,22 +56,16 @@ pub struct ActionRunner { pub duration: Option, - passthrough_args: Vec, - - primary_target: String, - workspace: Arc>, } impl ActionRunner { pub fn new(workspace: Workspace) -> Self { - debug!(target: LOG_TARGET, "Creating action runner",); + debug!(target: LOG_TARGET, "Creating action runner"); ActionRunner { bail: false, duration: None, - passthrough_args: Vec::new(), - primary_target: String::new(), workspace: Arc::new(RwLock::new(workspace)), } } @@ -83,7 +75,7 @@ impl ActionRunner { self } - pub async fn cleanup(&self) -> Result<(), WorkspaceError> { + pub async fn cleanup(&self) -> Result<(), ActionRunnerError> { let workspace = self.workspace.read().await; // Delete all previously created runfiles @@ -94,14 +86,17 @@ impl ActionRunner { Ok(()) } - pub async fn run(&mut self, graph: DepGraph) -> Result, WorkspaceError> { + pub async fn run( + &mut self, + graph: DepGraph, + context: ActionRunnerContext, + ) -> Result, ActionRunnerError> { let start = Instant::now(); let node_count = graph.graph.node_count(); let batches = graph.sort_batched_topological()?; let batches_count = batches.len(); let graph = Arc::new(RwLock::new(graph)); - let passthrough_args = Arc::new(self.passthrough_args.clone()); - let primary_target = Arc::new(self.primary_target.clone()); + let context = Arc::new(context); // Clean the runner state *before* running actions instead of after, // so that failing or broken builds can dig into and debug the state! @@ -129,16 +124,15 @@ impl ActionRunner { for (i, node_index) in batch.into_iter().enumerate() { let action_count = i + 1; - let workspace_clone = Arc::clone(&self.workspace); let graph_clone = Arc::clone(&graph); - let passthrough_args_clone = Arc::clone(&passthrough_args); - let primary_target_clone = Arc::clone(&primary_target); + let context_clone = Arc::clone(&context); + let workspace_clone = Arc::clone(&self.workspace); action_handles.push(task::spawn(async move { - let mut action = Action::new(node_index); + let mut action = Action::new(node_index.index(), None); let own_graph = graph_clone.read().await; - if let Some(node) = own_graph.get_node_from_index(node_index) { + if let Some(node) = own_graph.get_node_from_index(&node_index) { action.label = Some(node.label()); let log_target_name = @@ -151,14 +145,7 @@ impl ActionRunner { log_action_label ); - run_action( - workspace_clone, - &mut action, - node, - &primary_target_clone, - &passthrough_args_clone, - ) - .await?; + run_action(node, &mut action, &context_clone, workspace_clone).await?; if action.has_failed() { trace!( @@ -178,7 +165,9 @@ impl ActionRunner { } else { action.status = ActionStatus::Invalid; - return Err(WorkspaceError::DepGraphUnknownNode(node_index.index())); + return Err(ActionRunnerError::DepGraph(DepGraphError::UnknownNode( + node_index.index(), + ))); } Ok(action) @@ -198,7 +187,7 @@ impl ActionRunner { } if self.bail && result.error.is_some() || result.should_abort() { - return Err(WorkspaceError::ActionRunnerFailure(result.error.unwrap())); + return Err(ActionRunnerError::Failure(result.error.unwrap())); } results.push(result); @@ -207,7 +196,7 @@ impl ActionRunner { return Err(e); } Err(e) => { - return Err(WorkspaceError::ActionRunnerFailure(e.to_string())); + return Err(ActionRunnerError::Failure(e.to_string())); } } } @@ -224,14 +213,4 @@ impl ActionRunner { Ok(results) } - - pub fn set_passthrough_args(&mut self, args: Vec) -> &mut Self { - self.passthrough_args = args; - self - } - - pub fn set_primary_target(&mut self, target: &str) -> &mut Self { - self.primary_target = target.to_owned(); - self - } } diff --git a/crates/action-runner/tests/dep_graph_test.rs b/crates/action-runner/tests/dep_graph_test.rs new file mode 100644 index 00000000000..85d2090b730 --- /dev/null +++ b/crates/action-runner/tests/dep_graph_test.rs @@ -0,0 +1,506 @@ +use insta::assert_snapshot; +use moon_action_runner::{BatchedTopoSort, DepGraph, NodeIndex}; +use moon_cache::CacheEngine; +use moon_config::GlobalProjectConfig; +use moon_project::{ProjectGraph, Target}; +use moon_utils::test::get_fixtures_dir; +use std::collections::{HashMap, HashSet}; + +async fn create_project_graph() -> ProjectGraph { + let workspace_root = get_fixtures_dir("projects"); + + ProjectGraph::create( + &workspace_root, + GlobalProjectConfig::default(), + &HashMap::from([ + ("advanced".to_owned(), "advanced".to_owned()), + ("basic".to_owned(), "basic".to_owned()), + ("emptyConfig".to_owned(), "empty-config".to_owned()), + ("noConfig".to_owned(), "no-config".to_owned()), + ("foo".to_owned(), "deps/foo".to_owned()), + ("bar".to_owned(), "deps/bar".to_owned()), + ("baz".to_owned(), "deps/baz".to_owned()), + ("tasks".to_owned(), "tasks".to_owned()), + ]), + &CacheEngine::create(&workspace_root).await.unwrap(), + ) + .await + .unwrap() +} + +async fn create_tasks_project_graph() -> ProjectGraph { + let workspace_root = get_fixtures_dir("tasks"); + let global_config = GlobalProjectConfig { + file_groups: HashMap::from([("sources".to_owned(), vec!["src/**/*".to_owned()])]), + ..GlobalProjectConfig::default() + }; + + ProjectGraph::create( + &workspace_root, + global_config, + &HashMap::from([ + ("basic".to_owned(), "basic".to_owned()), + ("build-a".to_owned(), "build-a".to_owned()), + ("build-b".to_owned(), "build-b".to_owned()), + ("build-c".to_owned(), "build-c".to_owned()), + ("chain".to_owned(), "chain".to_owned()), + ("cycle".to_owned(), "cycle".to_owned()), + ("inputA".to_owned(), "input-a".to_owned()), + ("inputB".to_owned(), "input-b".to_owned()), + ("inputC".to_owned(), "input-c".to_owned()), + ("mergeAppend".to_owned(), "merge-append".to_owned()), + ("mergePrepend".to_owned(), "merge-prepend".to_owned()), + ("mergeReplace".to_owned(), "merge-replace".to_owned()), + ("no-tasks".to_owned(), "no-tasks".to_owned()), + ]), + &CacheEngine::create(&workspace_root).await.unwrap(), + ) + .await + .unwrap() +} + +fn sort_batches(batches: BatchedTopoSort) -> BatchedTopoSort { + let mut list: BatchedTopoSort = vec![]; + + for batch in batches { + let mut new_batch = batch.clone(); + new_batch.sort(); + list.push(new_batch); + } + + list +} + +#[test] +fn default_graph() { + let graph = DepGraph::default(); + + assert_snapshot!(graph.to_dot()); + + assert_eq!(graph.sort_topological().unwrap(), vec![NodeIndex::new(0)]); + assert_eq!( + sort_batches(graph.sort_batched_topological().unwrap()), + vec![vec![NodeIndex::new(0)]] + ); +} + +#[tokio::test] +#[should_panic( + expected = "CycleDetected(\"RunTarget(cycle:a) → RunTarget(cycle:b) → RunTarget(cycle:c)\")" +)] +async fn detects_cycles() { + let projects = create_tasks_project_graph().await; + + let mut graph = DepGraph::default(); + graph + .run_target(&Target::new("cycle", "a").unwrap(), &projects, None) + .unwrap(); + graph + .run_target(&Target::new("cycle", "b").unwrap(), &projects, None) + .unwrap(); + graph + .run_target(&Target::new("cycle", "c").unwrap(), &projects, None) + .unwrap(); + + assert_eq!( + sort_batches(graph.sort_batched_topological().unwrap()), + vec![vec![NodeIndex::new(0)], vec![NodeIndex::new(1)]] + ); +} + +mod run_target { + use super::*; + + #[tokio::test] + async fn single_targets() { + let projects = create_project_graph().await; + + let mut graph = DepGraph::default(); + graph + .run_target(&Target::new("tasks", "test").unwrap(), &projects, None) + .unwrap(); + graph + .run_target(&Target::new("tasks", "lint").unwrap(), &projects, None) + .unwrap(); + assert_snapshot!(graph.to_dot()); + + assert_eq!( + graph.sort_topological().unwrap(), + vec![ + NodeIndex::new(0), + NodeIndex::new(1), + NodeIndex::new(2), // sync project + NodeIndex::new(3), // test + NodeIndex::new(4), // lint + ] + ); + assert_eq!( + sort_batches(graph.sort_batched_topological().unwrap()), + vec![ + vec![NodeIndex::new(0)], + vec![NodeIndex::new(1), NodeIndex::new(2)], + vec![NodeIndex::new(3), NodeIndex::new(4)] + ] + ); + } + + #[tokio::test] + async fn deps_chain_target() { + let projects = create_tasks_project_graph().await; + + let mut graph = DepGraph::default(); + graph + .run_target(&Target::new("basic", "test").unwrap(), &projects, None) + .unwrap(); + graph + .run_target(&Target::new("basic", "lint").unwrap(), &projects, None) + .unwrap(); + graph + .run_target(&Target::new("chain", "a").unwrap(), &projects, None) + .unwrap(); + + assert_snapshot!(graph.to_dot()); + assert_eq!( + graph.sort_topological().unwrap(), + vec![ + NodeIndex::new(0), + NodeIndex::new(1), + NodeIndex::new(2), // sync project + NodeIndex::new(3), // test + NodeIndex::new(4), // lint + NodeIndex::new(5), // sync project + NodeIndex::new(11), // f + NodeIndex::new(10), // e + NodeIndex::new(9), // d + NodeIndex::new(8), // c + NodeIndex::new(7), // b + NodeIndex::new(6), // a + ] + ); + assert_eq!( + sort_batches(graph.sort_batched_topological().unwrap()), + vec![ + vec![NodeIndex::new(0)], + vec![NodeIndex::new(1), NodeIndex::new(5)], + vec![NodeIndex::new(11)], + vec![NodeIndex::new(10)], + vec![NodeIndex::new(9)], + vec![NodeIndex::new(8)], + vec![NodeIndex::new(2), NodeIndex::new(7)], + vec![NodeIndex::new(3), NodeIndex::new(4), NodeIndex::new(6)] + ] + ); + } + + #[tokio::test] + async fn avoids_dupe_targets() { + let projects = create_project_graph().await; + + let mut graph = DepGraph::default(); + graph + .run_target(&Target::new("tasks", "lint").unwrap(), &projects, None) + .unwrap(); + graph + .run_target(&Target::new("tasks", "lint").unwrap(), &projects, None) + .unwrap(); + graph + .run_target(&Target::new("tasks", "lint").unwrap(), &projects, None) + .unwrap(); + + assert_snapshot!(graph.to_dot()); + + assert_eq!( + graph.sort_topological().unwrap(), + vec![ + NodeIndex::new(0), + NodeIndex::new(1), + NodeIndex::new(2), // sync project + NodeIndex::new(3), // lint + ] + ); + assert_eq!( + sort_batches(graph.sort_batched_topological().unwrap()), + vec![ + vec![NodeIndex::new(0)], + vec![NodeIndex::new(1), NodeIndex::new(2)], + vec![NodeIndex::new(3)] + ] + ); + } + + #[tokio::test] + async fn runs_all_projects_for_target_all_scope() { + let projects = create_tasks_project_graph().await; + + let mut graph = DepGraph::default(); + graph + .run_target(&Target::parse(":build").unwrap(), &projects, None) + .unwrap(); + + assert_snapshot!(graph.to_dot()); + + assert_eq!( + graph.sort_topological().unwrap(), + vec![ + NodeIndex::new(0), + NodeIndex::new(1), + NodeIndex::new(2), // sync project: basic + NodeIndex::new(3), // basic:build + NodeIndex::new(5), // sync project: build-c + NodeIndex::new(4), // sync project: build-a + NodeIndex::new(7), // build-c:build + NodeIndex::new(6), // build-a:build + NodeIndex::new(8), // sync project: build-b + NodeIndex::new(9), // build-b:build + ] + ); + assert_eq!( + sort_batches(graph.sort_batched_topological().unwrap()), + vec![ + vec![NodeIndex::new(0)], + vec![NodeIndex::new(1), NodeIndex::new(2), NodeIndex::new(5)], + vec![ + NodeIndex::new(3), + NodeIndex::new(4), + NodeIndex::new(7), + NodeIndex::new(8) + ], + vec![NodeIndex::new(6), NodeIndex::new(9)], + ] + ); + } + + #[tokio::test] + #[should_panic(expected = "Project(Target(NoProjectDepsInRunContext))")] + async fn errors_for_target_deps_scope() { + let projects = create_project_graph().await; + + let mut graph = DepGraph::default(); + graph + .run_target(&Target::parse("^:lint").unwrap(), &projects, None) + .unwrap(); + } + + #[tokio::test] + #[should_panic(expected = "Project(Target(NoProjectSelfInRunContext))")] + async fn errors_for_target_self_scope() { + let projects = create_project_graph().await; + + let mut graph = DepGraph::default(); + graph + .run_target(&Target::parse("~:lint").unwrap(), &projects, None) + .unwrap(); + } + + #[tokio::test] + #[should_panic(expected = "Project(UnconfiguredID(\"unknown\"))")] + async fn errors_for_unknown_project() { + let projects = create_project_graph().await; + + let mut graph = DepGraph::default(); + graph + .run_target(&Target::new("unknown", "test").unwrap(), &projects, None) + .unwrap(); + + assert_snapshot!(graph.to_dot()); + } + + #[tokio::test] + #[should_panic(expected = "Project(UnconfiguredTask(\"build\", \"tasks\"))")] + async fn errors_for_unknown_task() { + let projects = create_project_graph().await; + + let mut graph = DepGraph::default(); + graph + .run_target(&Target::new("tasks", "build").unwrap(), &projects, None) + .unwrap(); + + assert_snapshot!(graph.to_dot()); + } +} + +mod run_target_if_touched { + use super::*; + + #[tokio::test] + async fn skips_if_untouched_project() { + let projects = create_tasks_project_graph().await; + + let mut touched_files = HashSet::new(); + touched_files.insert(get_fixtures_dir("tasks").join("input-a/a.ts")); + touched_files.insert(get_fixtures_dir("tasks").join("input-c/c.ts")); + + let mut graph = DepGraph::default(); + graph + .run_target( + &Target::new("inputA", "a").unwrap(), + &projects, + Some(&touched_files), + ) + .unwrap(); + graph + .run_target( + &Target::new("inputB", "b").unwrap(), + &projects, + Some(&touched_files), + ) + .unwrap(); + + assert_snapshot!(graph.to_dot()); + } + + #[tokio::test] + async fn skips_if_untouched_task() { + let projects = create_tasks_project_graph().await; + + let mut touched_files = HashSet::new(); + touched_files.insert(get_fixtures_dir("tasks").join("input-a/a2.ts")); + touched_files.insert(get_fixtures_dir("tasks").join("input-b/b2.ts")); + touched_files.insert(get_fixtures_dir("tasks").join("input-c/any.ts")); + + let mut graph = DepGraph::default(); + graph + .run_target( + &Target::new("inputA", "a").unwrap(), + &projects, + Some(&touched_files), + ) + .unwrap(); + graph + .run_target( + &Target::new("inputB", "b2").unwrap(), + &projects, + Some(&touched_files), + ) + .unwrap(); + graph + .run_target( + &Target::new("inputC", "c").unwrap(), + &projects, + Some(&touched_files), + ) + .unwrap(); + + assert_snapshot!(graph.to_dot()); + } +} + +mod sync_project { + use super::*; + + #[tokio::test] + async fn isolated_projects() { + let projects = create_project_graph().await; + + let mut graph = DepGraph::default(); + graph.sync_project("advanced", &projects).unwrap(); + graph.sync_project("basic", &projects).unwrap(); + graph.sync_project("emptyConfig", &projects).unwrap(); + graph.sync_project("noConfig", &projects).unwrap(); + + assert_snapshot!(graph.to_dot()); + + assert_eq!( + graph.sort_topological().unwrap(), + vec![ + NodeIndex::new(0), + NodeIndex::new(1), // advanced + NodeIndex::new(3), // noConfig + NodeIndex::new(2), // basic + NodeIndex::new(4), // emptyConfig + ] + ); + assert_eq!( + sort_batches(graph.sort_batched_topological().unwrap()), + vec![ + vec![NodeIndex::new(0)], + vec![NodeIndex::new(3)], + vec![NodeIndex::new(1), NodeIndex::new(2), NodeIndex::new(4)] + ] + ); + } + + #[tokio::test] + async fn projects_with_deps() { + let projects = create_project_graph().await; + + let mut graph = DepGraph::default(); + graph.sync_project("foo", &projects).unwrap(); + graph.sync_project("bar", &projects).unwrap(); + graph.sync_project("baz", &projects).unwrap(); + graph.sync_project("basic", &projects).unwrap(); + + // Not deterministic! + // assert_snapshot!(graph.to_dot()); + + assert_eq!( + graph.sort_topological().unwrap(), + vec![ + NodeIndex::new(0), + NodeIndex::new(2), // baz + NodeIndex::new(3), // bar + NodeIndex::new(1), // foo + NodeIndex::new(5), // noConfig + NodeIndex::new(4), // basic + ] + ); + assert_eq!( + sort_batches(graph.sort_batched_topological().unwrap()), + vec![ + vec![NodeIndex::new(0)], + vec![NodeIndex::new(2), NodeIndex::new(3), NodeIndex::new(5)], + vec![NodeIndex::new(1), NodeIndex::new(4)] + ] + ); + } + + #[tokio::test] + async fn projects_with_tasks() { + let projects = create_project_graph().await; + + let mut graph = DepGraph::default(); + graph.sync_project("noConfig", &projects).unwrap(); + graph.sync_project("tasks", &projects).unwrap(); + + assert_snapshot!(graph.to_dot()); + + assert_eq!( + graph.sort_topological().unwrap(), + vec![ + NodeIndex::new(0), + NodeIndex::new(1), // noConfig + NodeIndex::new(2), // tasks + ] + ); + assert_eq!( + sort_batches(graph.sort_batched_topological().unwrap()), + vec![ + vec![NodeIndex::new(0)], + vec![NodeIndex::new(1), NodeIndex::new(2)] + ] + ); + } + + #[tokio::test] + async fn avoids_dupe_projects() { + let projects = create_project_graph().await; + + let mut graph = DepGraph::default(); + graph.sync_project("advanced", &projects).unwrap(); + graph.sync_project("advanced", &projects).unwrap(); + graph.sync_project("advanced", &projects).unwrap(); + + assert_snapshot!(graph.to_dot()); + } + + #[tokio::test] + #[should_panic(expected = "Project(UnconfiguredID(\"unknown\"))")] + async fn errors_for_unknown_project() { + let projects = create_project_graph().await; + + let mut graph = DepGraph::default(); + graph.sync_project("unknown", &projects).unwrap(); + + assert_snapshot!(graph.to_dot()); + } +} diff --git a/crates/action-runner/tests/snapshots/dep_graph_test__default_graph.snap b/crates/action-runner/tests/snapshots/dep_graph_test__default_graph.snap new file mode 100644 index 00000000000..e9698b06bfa --- /dev/null +++ b/crates/action-runner/tests/snapshots/dep_graph_test__default_graph.snap @@ -0,0 +1,9 @@ +--- +source: crates/action-runner/tests/dep_graph_test.rs +assertion_line: 78 +expression: graph.to_dot() +--- +digraph { + 0 [ label = "\"SetupToolchain\"" ] +} + diff --git a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target__avoids_dupe_targets.snap b/crates/action-runner/tests/snapshots/dep_graph_test__run_target__avoids_dupe_targets.snap similarity index 54% rename from crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target__avoids_dupe_targets.snap rename to crates/action-runner/tests/snapshots/dep_graph_test__run_target__avoids_dupe_targets.snap index cf16ef76096..df5a7d43cbe 100644 --- a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target__avoids_dupe_targets.snap +++ b/crates/action-runner/tests/snapshots/dep_graph_test__run_target__avoids_dupe_targets.snap @@ -1,13 +1,12 @@ --- -source: crates/workspace/src/dep_graph.rs -assertion_line: 381 +source: crates/action-runner/tests/dep_graph_test.rs +assertion_line: 213 expression: graph.to_dot() - --- digraph { 0 [ label = "\"SetupToolchain\"" ] - 1 [ label = "\"InstallNodeDeps\"" ] - 2 [ label = "\"SyncProject(tasks)\"" ] + 1 [ label = "\"InstallDeps(Node)\"" ] + 2 [ label = "\"SyncProject(Node, tasks)\"" ] 3 [ label = "\"RunTarget(tasks:lint)\"" ] 1 -> 0 [ ] 2 -> 0 [ ] diff --git a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target__deps_chain_target.snap b/crates/action-runner/tests/snapshots/dep_graph_test__run_target__deps_chain_target.snap similarity index 79% rename from crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target__deps_chain_target.snap rename to crates/action-runner/tests/snapshots/dep_graph_test__run_target__deps_chain_target.snap index 4c0ab395062..55fc9fbe1b5 100644 --- a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target__deps_chain_target.snap +++ b/crates/action-runner/tests/snapshots/dep_graph_test__run_target__deps_chain_target.snap @@ -1,16 +1,15 @@ --- -source: crates/workspace/src/dep_graph.rs -assertion_line: 439 +source: crates/action-runner/tests/dep_graph_test.rs +assertion_line: 165 expression: graph.to_dot() - --- digraph { 0 [ label = "\"SetupToolchain\"" ] - 1 [ label = "\"InstallNodeDeps\"" ] - 2 [ label = "\"SyncProject(basic)\"" ] + 1 [ label = "\"InstallDeps(Node)\"" ] + 2 [ label = "\"SyncProject(Node, basic)\"" ] 3 [ label = "\"RunTarget(basic:test)\"" ] 4 [ label = "\"RunTarget(basic:lint)\"" ] - 5 [ label = "\"SyncProject(chain)\"" ] + 5 [ label = "\"SyncProject(Node, chain)\"" ] 6 [ label = "\"RunTarget(chain:a)\"" ] 7 [ label = "\"RunTarget(chain:b)\"" ] 8 [ label = "\"RunTarget(chain:c)\"" ] diff --git a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target__runs_all_projects_for_target_all_scope.snap b/crates/action-runner/tests/snapshots/dep_graph_test__run_target__runs_all_projects_for_target_all_scope.snap similarity index 62% rename from crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target__runs_all_projects_for_target_all_scope.snap rename to crates/action-runner/tests/snapshots/dep_graph_test__run_target__runs_all_projects_for_target_all_scope.snap index 614d44cc5c1..9e78845def6 100644 --- a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target__runs_all_projects_for_target_all_scope.snap +++ b/crates/action-runner/tests/snapshots/dep_graph_test__run_target__runs_all_projects_for_target_all_scope.snap @@ -1,18 +1,18 @@ --- -source: crates/workspace/src/dep_graph.rs -assertion_line: 618 +source: crates/action-runner/tests/dep_graph_test.rs +assertion_line: 243 expression: graph.to_dot() --- digraph { 0 [ label = "\"SetupToolchain\"" ] - 1 [ label = "\"InstallNodeDeps\"" ] - 2 [ label = "\"SyncProject(basic)\"" ] + 1 [ label = "\"InstallDeps(Node)\"" ] + 2 [ label = "\"SyncProject(Node, basic)\"" ] 3 [ label = "\"RunTarget(basic:build)\"" ] - 4 [ label = "\"SyncProject(build-a)\"" ] - 5 [ label = "\"SyncProject(build-c)\"" ] + 4 [ label = "\"SyncProject(Node, build-a)\"" ] + 5 [ label = "\"SyncProject(Node, build-c)\"" ] 6 [ label = "\"RunTarget(build-a:build)\"" ] 7 [ label = "\"RunTarget(build-c:build)\"" ] - 8 [ label = "\"SyncProject(build-b)\"" ] + 8 [ label = "\"SyncProject(Node, build-b)\"" ] 9 [ label = "\"RunTarget(build-b:build)\"" ] 1 -> 0 [ ] 2 -> 0 [ ] diff --git a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target__single_targets.snap b/crates/action-runner/tests/snapshots/dep_graph_test__run_target__single_targets.snap similarity index 62% rename from crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target__single_targets.snap rename to crates/action-runner/tests/snapshots/dep_graph_test__run_target__single_targets.snap index 36272b7ae1b..7b111c5e519 100644 --- a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target__single_targets.snap +++ b/crates/action-runner/tests/snapshots/dep_graph_test__run_target__single_targets.snap @@ -1,13 +1,12 @@ --- -source: crates/workspace/src/dep_graph.rs -assertion_line: 342 +source: crates/action-runner/tests/dep_graph_test.rs +assertion_line: 128 expression: graph.to_dot() - --- digraph { 0 [ label = "\"SetupToolchain\"" ] - 1 [ label = "\"InstallNodeDeps\"" ] - 2 [ label = "\"SyncProject(tasks)\"" ] + 1 [ label = "\"InstallDeps(Node)\"" ] + 2 [ label = "\"SyncProject(Node, tasks)\"" ] 3 [ label = "\"RunTarget(tasks:test)\"" ] 4 [ label = "\"RunTarget(tasks:lint)\"" ] 1 -> 0 [ ] diff --git a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target_if_touched__skips_if_untouched_project.snap b/crates/action-runner/tests/snapshots/dep_graph_test__run_target_if_touched__skips_if_untouched_project.snap similarity index 53% rename from crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target_if_touched__skips_if_untouched_project.snap rename to crates/action-runner/tests/snapshots/dep_graph_test__run_target_if_touched__skips_if_untouched_project.snap index d26b6dca9e6..fb8104dfc1e 100644 --- a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target_if_touched__skips_if_untouched_project.snap +++ b/crates/action-runner/tests/snapshots/dep_graph_test__run_target_if_touched__skips_if_untouched_project.snap @@ -1,13 +1,12 @@ --- -source: crates/workspace/src/dep_graph.rs -assertion_line: 558 +source: crates/action-runner/tests/dep_graph_test.rs +assertion_line: 352 expression: graph.to_dot() - --- digraph { 0 [ label = "\"SetupToolchain\"" ] - 1 [ label = "\"InstallNodeDeps\"" ] - 2 [ label = "\"SyncProject(inputA)\"" ] + 1 [ label = "\"InstallDeps(Node)\"" ] + 2 [ label = "\"SyncProject(Node, inputA)\"" ] 3 [ label = "\"RunTarget(inputA:a)\"" ] 1 -> 0 [ ] 2 -> 0 [ ] diff --git a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target_if_touched__skips_if_untouched_task.snap b/crates/action-runner/tests/snapshots/dep_graph_test__run_target_if_touched__skips_if_untouched_task.snap similarity index 56% rename from crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target_if_touched__skips_if_untouched_task.snap rename to crates/action-runner/tests/snapshots/dep_graph_test__run_target_if_touched__skips_if_untouched_task.snap index 2b6447e7274..2e50c189dd5 100644 --- a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__run_target_if_touched__skips_if_untouched_task.snap +++ b/crates/action-runner/tests/snapshots/dep_graph_test__run_target_if_touched__skips_if_untouched_task.snap @@ -1,15 +1,14 @@ --- -source: crates/workspace/src/dep_graph.rs -assertion_line: 582 +source: crates/action-runner/tests/dep_graph_test.rs +assertion_line: 387 expression: graph.to_dot() - --- digraph { 0 [ label = "\"SetupToolchain\"" ] - 1 [ label = "\"InstallNodeDeps\"" ] - 2 [ label = "\"SyncProject(inputB)\"" ] + 1 [ label = "\"InstallDeps(Node)\"" ] + 2 [ label = "\"SyncProject(Node, inputB)\"" ] 3 [ label = "\"RunTarget(inputB:b2)\"" ] - 4 [ label = "\"SyncProject(inputC)\"" ] + 4 [ label = "\"SyncProject(Node, inputC)\"" ] 5 [ label = "\"RunTarget(inputC:c)\"" ] 1 -> 0 [ ] 2 -> 0 [ ] diff --git a/crates/action-runner/tests/snapshots/dep_graph_test__sync_project__avoids_dupe_projects.snap b/crates/action-runner/tests/snapshots/dep_graph_test__sync_project__avoids_dupe_projects.snap new file mode 100644 index 00000000000..0a49931bc68 --- /dev/null +++ b/crates/action-runner/tests/snapshots/dep_graph_test__sync_project__avoids_dupe_projects.snap @@ -0,0 +1,11 @@ +--- +source: crates/action-runner/tests/dep_graph_test.rs +assertion_line: 504 +expression: graph.to_dot() +--- +digraph { + 0 [ label = "\"SetupToolchain\"" ] + 1 [ label = "\"SyncProject(Node, advanced)\"" ] + 1 -> 0 [ ] +} + diff --git a/crates/action-runner/tests/snapshots/dep_graph_test__sync_project__isolated_projects.snap b/crates/action-runner/tests/snapshots/dep_graph_test__sync_project__isolated_projects.snap new file mode 100644 index 00000000000..6449804cf15 --- /dev/null +++ b/crates/action-runner/tests/snapshots/dep_graph_test__sync_project__isolated_projects.snap @@ -0,0 +1,18 @@ +--- +source: crates/action-runner/tests/dep_graph_test.rs +assertion_line: 404 +expression: graph.to_dot() +--- +digraph { + 0 [ label = "\"SetupToolchain\"" ] + 1 [ label = "\"SyncProject(Node, advanced)\"" ] + 2 [ label = "\"SyncProject(Node, basic)\"" ] + 3 [ label = "\"SyncProject(System, noConfig)\"" ] + 4 [ label = "\"SyncProject(Node, emptyConfig)\"" ] + 1 -> 0 [ ] + 2 -> 0 [ ] + 3 -> 0 [ ] + 2 -> 3 [ ] + 4 -> 0 [ ] +} + diff --git a/crates/action-runner/tests/snapshots/dep_graph_test__sync_project__projects_with_tasks.snap b/crates/action-runner/tests/snapshots/dep_graph_test__sync_project__projects_with_tasks.snap new file mode 100644 index 00000000000..32b222463df --- /dev/null +++ b/crates/action-runner/tests/snapshots/dep_graph_test__sync_project__projects_with_tasks.snap @@ -0,0 +1,13 @@ +--- +source: crates/action-runner/tests/dep_graph_test.rs +assertion_line: 475 +expression: graph.to_dot() +--- +digraph { + 0 [ label = "\"SetupToolchain\"" ] + 1 [ label = "\"SyncProject(System, noConfig)\"" ] + 2 [ label = "\"SyncProject(Node, tasks)\"" ] + 1 -> 0 [ ] + 2 -> 0 [ ] +} + diff --git a/crates/action/Cargo.toml b/crates/action/Cargo.toml new file mode 100644 index 00000000000..8fb0128d2f1 --- /dev/null +++ b/crates/action/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "moon_action" +version = "0.1.0" +edition = "2021" diff --git a/crates/workspace/src/action.rs b/crates/action/src/action.rs similarity index 92% rename from crates/workspace/src/action.rs rename to crates/action/src/action.rs index c56e719a5e6..c626bb61a3d 100644 --- a/crates/workspace/src/action.rs +++ b/crates/action/src/action.rs @@ -1,4 +1,3 @@ -use petgraph::graph::NodeIndex; use std::time::{Duration, Instant}; pub struct Attempt { @@ -43,7 +42,7 @@ pub struct Action { pub label: Option, - pub node_index: NodeIndex, + pub node_index: usize, pub start_time: Instant, @@ -51,12 +50,12 @@ pub struct Action { } impl Action { - pub fn new(node_index: NodeIndex) -> Self { + pub fn new(node_index: usize, label: Option) -> Self { Action { attempts: None, duration: None, error: None, - label: None, + label, node_index, start_time: Instant::now(), status: ActionStatus::Running, diff --git a/crates/action/src/lib.rs b/crates/action/src/lib.rs new file mode 100644 index 00000000000..8bd911fd538 --- /dev/null +++ b/crates/action/src/lib.rs @@ -0,0 +1,3 @@ +mod action; + +pub use action::*; diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index b0a7ed69150..86d2ff39ea1 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -13,6 +13,8 @@ path = "src/lib.rs" crate-type = ["rlib"] [dependencies] +moon_action = { path = "../action" } +moon_action_runner = { path = "../action-runner" } moon_config = { path = "../config" } moon_lang = { path = "../lang" } moon_lang_node = { path = "../lang-node" } diff --git a/crates/cli/src/commands/ci.rs b/crates/cli/src/commands/ci.rs index ccf3fce34c1..58b2b902cbe 100644 --- a/crates/cli/src/commands/ci.rs +++ b/crates/cli/src/commands/ci.rs @@ -3,12 +3,13 @@ use crate::enums::TouchedStatus; use crate::queries::touched_files::{query_touched_files, QueryTouchedFilesOptions}; use console::Term; use itertools::Itertools; +use moon_action::ActionStatus; +use moon_action_runner::{ActionRunner, ActionRunnerContext, DepGraph, DepGraphError}; use moon_logger::{color, debug}; use moon_project::{Target, TouchedFilePaths}; use moon_terminal::helpers::{replace_style_tokens, safe_exit}; use moon_utils::{is_ci, time}; -use moon_workspace::DepGraph; -use moon_workspace::{ActionRunner, ActionStatus, Workspace, WorkspaceError}; +use moon_workspace::{Workspace, WorkspaceError}; type TargetList = Vec; @@ -138,7 +139,7 @@ fn distribute_targets_across_jobs(options: &CiOptions, targets: TargetList) -> T fn generate_dep_graph( workspace: &Workspace, targets: &TargetList, -) -> Result { +) -> Result { print_header("Generating dependency graph"); let mut dep_graph = DepGraph::default(); @@ -180,7 +181,10 @@ pub async fn ci(options: CiOptions) -> Result<(), Box> { print_header("Running all targets"); let mut runner = ActionRunner::new(workspace); - let results = runner.run(dep_graph).await?; + + let results = runner + .run(dep_graph, ActionRunnerContext::default()) + .await?; // Print out the results and exit if an error occurs let mut error_count = 0; diff --git a/crates/cli/src/commands/run.rs b/crates/cli/src/commands/run.rs index a1afb626568..8df6a2573bf 100644 --- a/crates/cli/src/commands/run.rs +++ b/crates/cli/src/commands/run.rs @@ -1,11 +1,14 @@ use crate::enums::TouchedStatus; use crate::queries::touched_files::{query_touched_files, QueryTouchedFilesOptions}; use console::Term; +use moon_action::{Action, ActionStatus}; +use moon_action_runner::{ActionRunner, ActionRunnerContext, DepGraph}; use moon_logger::color; use moon_project::Target; use moon_terminal::ExtendedTerm; use moon_utils::time; -use moon_workspace::{Action, ActionRunner, ActionStatus, DepGraph, Workspace}; +use moon_workspace::Workspace; +use std::collections::HashSet; use std::string::ToString; use std::time::Duration; @@ -151,14 +154,14 @@ pub async fn run(target_id: &str, options: RunOptions) -> Result<(), Box &'static str { include_str!("../templates/workspace.yml") diff --git a/crates/config/src/project/mod.rs b/crates/config/src/project/mod.rs index bb49f1669a9..fd572460ba2 100644 --- a/crates/config/src/project/mod.rs +++ b/crates/config/src/project/mod.rs @@ -59,6 +59,7 @@ fn validate_channel(value: &str) -> Result<(), ValidationError> { #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "lowercase")] pub enum ProjectLanguage { + Bash, JavaScript, #[default] TypeScript, diff --git a/crates/lang/src/lib.rs b/crates/lang/src/lib.rs index 74fb2e20ddd..96771e49321 100644 --- a/crates/lang/src/lib.rs +++ b/crates/lang/src/lib.rs @@ -1,6 +1,7 @@ mod errors; pub use errors::LangError; +use std::fmt; use std::path::Path; pub type StaticString = &'static str; @@ -66,3 +67,27 @@ pub fn is_using_version_manager(base_dir: &Path, vm: &VersionManager) -> bool { false } + +#[derive(Clone, Eq, PartialEq)] +pub enum SupportedLanguage { + Node, + System, +} + +impl SupportedLanguage { + pub fn label(&self) -> String { + match self { + SupportedLanguage::Node => "Node.js".into(), + SupportedLanguage::System => "system".into(), + } + } +} + +impl fmt::Display for SupportedLanguage { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + SupportedLanguage::Node => write!(f, "Node"), + SupportedLanguage::System => write!(f, "System"), + } + } +} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index bfbe9349de5..69f8ca4a6dc 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -7,17 +7,10 @@ edition = "2021" moon_cache = { path = "../cache" } moon_config = { path = "../config" } moon_error = { path = "../error" } -moon_hasher = { path = "../hasher" } -moon_lang_node = { path = "../lang-node" } moon_logger = { path = "../logger" } moon_project = { path = "../project" } -moon_terminal = { path = "../terminal" } moon_toolchain = { path = "../toolchain" } moon_utils = { path = "../utils" } moon_vcs = { path = "../vcs" } -petgraph = "0.6.0" thiserror = "1.0.31" tokio = { version = "1.18.2", features = ["full"] } - -[dev-dependencies] -insta = "1.14.0" diff --git a/crates/workspace/src/actions/hashing/mod.rs b/crates/workspace/src/actions/hashing/mod.rs deleted file mode 100644 index d56d12bae68..00000000000 --- a/crates/workspace/src/actions/hashing/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod target; - -pub use target::create_target_hasher; diff --git a/crates/workspace/src/actions/mod.rs b/crates/workspace/src/actions/mod.rs deleted file mode 100644 index 2f6a08d4fc0..00000000000 --- a/crates/workspace/src/actions/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod hashing; -mod install_node_deps; -mod run_target; -mod setup_toolchain; -mod sync_project; - -pub use install_node_deps::install_node_deps; -pub use run_target::run_target; -pub use setup_toolchain::setup_toolchain; -pub use sync_project::sync_project; diff --git a/crates/workspace/src/dep_graph.rs b/crates/workspace/src/dep_graph.rs deleted file mode 100644 index 83bc1922ef7..00000000000 --- a/crates/workspace/src/dep_graph.rs +++ /dev/null @@ -1,894 +0,0 @@ -use crate::errors::WorkspaceError; -use moon_logger::{color, debug, trace, warn}; -use moon_project::{ - ProjectGraph, ProjectID, Target, TargetError, TargetID, TargetProject, TouchedFilePaths, -}; -use petgraph::algo::toposort; -use petgraph::dot::{Config, Dot}; -use petgraph::graph::DiGraph; -use petgraph::Graph; -use std::collections::{HashMap, HashSet}; - -pub use petgraph::graph::NodeIndex; - -const TARGET: &str = "moon:dep-graph"; - -pub enum Node { - InstallNodeDeps, - RunTarget(TargetID), - SetupToolchain, - SyncProject(ProjectID), -} - -impl Node { - pub fn label(&self) -> String { - match self { - Node::InstallNodeDeps => String::from("InstallNodeDeps"), - Node::RunTarget(id) => format!("RunTarget({})", id), - Node::SetupToolchain => String::from("SetupToolchain"), - Node::SyncProject(id) => format!("SyncProject({})", id), - } - } -} - -type GraphType = DiGraph; -type BatchedTopoSort = Vec>; - -/// A directed acyclic graph (DAG) for the work that needs to be processed, based on a -/// project or task's dependency chain. This is also known as a "task graph" (not to -/// be confused with ours) or a "dependency graph". -pub struct DepGraph { - pub graph: GraphType, - - /// Mapping of IDs to existing node indices. - index_cache: HashMap, - - /// Reference node for the "install node deps" task. - install_node_deps_index: NodeIndex, - - /// Reference node for the "setup toolchain" task. - setup_toolchain_index: NodeIndex, -} - -impl DepGraph { - pub fn default() -> Self { - debug!(target: TARGET, "Creating dependency graph",); - - let mut graph: GraphType = Graph::new(); - - // Toolchain must be setup first - let setup_toolchain_index = graph.add_node(Node::SetupToolchain); - - // Deps can be installed *after* the toolchain exists - let install_node_deps_index = graph.add_node(Node::InstallNodeDeps); - - graph.add_edge(install_node_deps_index, setup_toolchain_index, ()); - - DepGraph { - graph, - index_cache: HashMap::new(), - install_node_deps_index, - setup_toolchain_index, - } - } - - pub fn get_node_from_index(&self, index: NodeIndex) -> Option<&Node> { - self.graph.node_weight(index) - } - - pub fn sort_topological(&self) -> Result, WorkspaceError> { - let list = match toposort(&self.graph, None) { - Ok(nodes) => nodes, - Err(error) => { - return Err(WorkspaceError::DepGraphCycleDetected( - self.get_node_from_index(error.node_id()).unwrap().label(), - )); - } - }; - - Ok(list.into_iter().rev().collect()) - } - - pub fn sort_batched_topological(&self) -> Result { - let mut batches: BatchedTopoSort = vec![]; - - // Count how many times an index is referenced across nodes and edges - let mut node_counts = HashMap::::new(); - - for ix in self.graph.node_indices() { - node_counts.entry(ix).and_modify(|e| *e += 1).or_insert(0); - - for dep_ix in self.graph.neighbors(ix) { - node_counts - .entry(dep_ix) - .and_modify(|e| *e += 1) - .or_insert(0); - } - } - - // Gather root nodes (count of 0) - let mut root_nodes = HashSet::::new(); - - for (ix, count) in &node_counts { - if *count == 0 { - root_nodes.insert(*ix); - } - } - - // If no root nodes are found, but nodes exist, then we have a cycle - if root_nodes.is_empty() && !node_counts.is_empty() { - self.detect_cycle()?; - } - - while !root_nodes.is_empty() { - // Push this batch onto the list - batches.push(root_nodes.clone().into_iter().collect()); - - // Reset the root nodes and find new ones after decrementing - let mut next_root_nodes = HashSet::::new(); - - for ix in &root_nodes { - for dep_ix in self.graph.neighbors(*ix) { - let count = node_counts - .entry(dep_ix) - .and_modify(|e| *e -= 1) - .or_insert(0); - - if *count == 0 { - next_root_nodes.insert(dep_ix); - } - } - } - - root_nodes = next_root_nodes; - } - - Ok(batches.into_iter().rev().collect()) - } - - pub fn run_target( - &mut self, - target: &Target, - projects: &ProjectGraph, - touched_files: Option<&TouchedFilePaths>, - ) -> Result { - let task_id = &target.task_id; - let mut inserted_count = 0; - - match &target.project { - // :task - TargetProject::All => { - for project_id in projects.ids() { - let project = projects.load(&project_id)?; - - if project.tasks.contains_key(task_id) - && self - .insert_target(&project_id, task_id, projects, touched_files)? - .is_some() - { - inserted_count += 1; - } - } - } - // ^:task - TargetProject::Deps => { - target.fail_with(TargetError::NoProjectDepsInRunContext)?; - } - // project:task - TargetProject::Id(project_id) => { - if self - .insert_target(project_id, task_id, projects, touched_files)? - .is_some() - { - inserted_count += 1; - } - } - // ~:task - TargetProject::Own => { - target.fail_with(TargetError::NoProjectSelfInRunContext)?; - } - }; - - Ok(inserted_count) - } - - pub fn run_target_dependents( - &mut self, - target: &Target, - projects: &ProjectGraph, - ) -> Result<(), WorkspaceError> { - trace!( - target: TARGET, - "Adding dependents to run for target {}", - color::target(&target.id), - ); - - let (project_id, task_id) = target.ids()?; - let project = projects.load(&project_id)?; - let dependents = projects.get_dependents_of(&project)?; - - for dependent_id in dependents { - let dependent = projects.load(&dependent_id)?; - - if dependent.tasks.contains_key(&task_id) { - self.run_target(&Target::new(&dependent_id, &task_id)?, projects, None)?; - } - } - - Ok(()) - } - - pub fn sync_project( - &mut self, - project_id: &str, - projects: &ProjectGraph, - ) -> Result { - if self.index_cache.contains_key(project_id) { - return Ok(*self.index_cache.get(project_id).unwrap()); - } - - trace!( - target: TARGET, - "Syncing project {} configs and dependencies", - color::id(project_id), - ); - - // Force load project into the graph - let project = projects.load(project_id)?; - - // Sync can be run in parallel while deps are installing - let node_index = self - .graph - .add_node(Node::SyncProject(project_id.to_owned())); - - self.graph - .add_edge(node_index, self.setup_toolchain_index, ()); - - // Cache so we don't sync the same project multiple times - self.index_cache.insert(project_id.to_owned(), node_index); - - // But we need to wait on all dependent nodes - for dep_id in projects.get_dependencies_of(&project)? { - let dep_node_index = self.sync_project(&dep_id, projects)?; - self.graph.add_edge(node_index, dep_node_index, ()); - } - - Ok(node_index) - } - - pub fn to_dot(&self) -> String { - let graph = self.graph.map(|_, n| n.label(), |_, e| e); - let dot = Dot::with_config(&graph, &[Config::EdgeNoLabel]); - - format!("{:?}", dot) - } - - #[track_caller] - fn detect_cycle(&self) -> Result<(), WorkspaceError> { - use petgraph::algo::kosaraju_scc; - - let scc = kosaraju_scc(&self.graph); - let cycle = scc - .last() - .unwrap() - .iter() - .map(|i| self.get_node_from_index(*i).unwrap().label()) - .collect::>() - .join(" → "); - - Err(WorkspaceError::DepGraphCycleDetected(cycle)) - } - - fn insert_target( - &mut self, - project_id: &str, - task_id: &str, - projects: &ProjectGraph, - touched_files: Option<&TouchedFilePaths>, - ) -> Result, WorkspaceError> { - let target_id = Target::format(project_id, task_id)?; - - if self.index_cache.contains_key(&target_id) { - return Ok(Some(*self.index_cache.get(&target_id).unwrap())); - } - - let project = projects.load(project_id)?; - - // Compare against touched files if provided - if let Some(touched) = touched_files { - let globally_affected = projects.is_globally_affected(touched); - - if globally_affected { - warn!( - target: TARGET, - "Moon files touched, marking all targets as affected", - ); - } - - // Validate task exists for project - if !globally_affected && !project.get_task(task_id)?.is_affected(touched)? { - trace!( - target: TARGET, - "Project {} task {} not affected based on touched files, skipping", - color::id(project_id), - color::id(task_id), - ); - - return Ok(None); - } - } - - trace!( - target: TARGET, - "Target {} does not exist in the dependency graph, inserting", - color::target(&target_id), - ); - - // We should sync projects *before* running targets - let sync_project_index = self.sync_project(&project.id, projects)?; - let node = self.graph.add_node(Node::RunTarget(target_id.to_owned())); - - self.graph.add_edge(node, self.install_node_deps_index, ()); - self.graph.add_edge(node, sync_project_index, ()); - - // Also cache so we don't run the same target multiple times - self.index_cache.insert(target_id.to_owned(), node); - - // And we also need to wait on all dependent nodes - let task = project.get_task(task_id)?; - - if !task.deps.is_empty() { - let dep_names: Vec = task - .deps - .clone() - .into_iter() - .map(|d| color::symbol(&d)) - .collect(); - - trace!( - target: TARGET, - "Adding dependencies {} from target {}", - dep_names.join(", "), - color::target(&target_id), - ); - - for dep_target_id in &task.deps { - let dep_target = Target::parse(dep_target_id)?; - - if let Some(dep_node) = self.insert_target( - &dep_target.project_id.unwrap(), - &dep_target.task_id, - projects, - touched_files, - )? { - self.graph.add_edge(node, dep_node, ()); - } - } - } - - Ok(Some(node)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use insta::assert_snapshot; - use moon_cache::CacheEngine; - use moon_config::GlobalProjectConfig; - use moon_project::ProjectGraph; - use moon_utils::test::get_fixtures_dir; - use std::collections::HashMap; - - async fn create_project_graph() -> ProjectGraph { - let workspace_root = get_fixtures_dir("projects"); - - ProjectGraph::create( - &workspace_root, - GlobalProjectConfig::default(), - &HashMap::from([ - ("advanced".to_owned(), "advanced".to_owned()), - ("basic".to_owned(), "basic".to_owned()), - ("emptyConfig".to_owned(), "empty-config".to_owned()), - ("noConfig".to_owned(), "no-config".to_owned()), - ("foo".to_owned(), "deps/foo".to_owned()), - ("bar".to_owned(), "deps/bar".to_owned()), - ("baz".to_owned(), "deps/baz".to_owned()), - ("tasks".to_owned(), "tasks".to_owned()), - ]), - &CacheEngine::create(&workspace_root).await.unwrap(), - ) - .await - .unwrap() - } - - async fn create_tasks_project_graph() -> ProjectGraph { - let workspace_root = get_fixtures_dir("tasks"); - let global_config = GlobalProjectConfig { - file_groups: HashMap::from([("sources".to_owned(), vec!["src/**/*".to_owned()])]), - ..GlobalProjectConfig::default() - }; - - ProjectGraph::create( - &workspace_root, - global_config, - &HashMap::from([ - ("basic".to_owned(), "basic".to_owned()), - ("build-a".to_owned(), "build-a".to_owned()), - ("build-b".to_owned(), "build-b".to_owned()), - ("build-c".to_owned(), "build-c".to_owned()), - ("chain".to_owned(), "chain".to_owned()), - ("cycle".to_owned(), "cycle".to_owned()), - ("inputA".to_owned(), "input-a".to_owned()), - ("inputB".to_owned(), "input-b".to_owned()), - ("inputC".to_owned(), "input-c".to_owned()), - ("mergeAppend".to_owned(), "merge-append".to_owned()), - ("mergePrepend".to_owned(), "merge-prepend".to_owned()), - ("mergeReplace".to_owned(), "merge-replace".to_owned()), - ("no-tasks".to_owned(), "no-tasks".to_owned()), - ]), - &CacheEngine::create(&workspace_root).await.unwrap(), - ) - .await - .unwrap() - } - - fn sort_batches(batches: BatchedTopoSort) -> BatchedTopoSort { - let mut list: BatchedTopoSort = vec![]; - - for batch in batches { - let mut new_batch = batch.clone(); - new_batch.sort(); - list.push(new_batch); - } - - list - } - - #[test] - fn default_graph() { - let graph = DepGraph::default(); - - assert_snapshot!(graph.to_dot()); - - assert_eq!( - graph.sort_topological().unwrap(), - vec![NodeIndex::new(0), NodeIndex::new(1)] - ); - assert_eq!( - sort_batches(graph.sort_batched_topological().unwrap()), - vec![vec![NodeIndex::new(0)], vec![NodeIndex::new(1)]] - ); - } - - #[tokio::test] - #[should_panic( - expected = "CycleDetected(\"RunTarget(cycle:a) → RunTarget(cycle:b) → RunTarget(cycle:c)\")" - )] - async fn detects_cycles() { - let projects = create_tasks_project_graph().await; - - let mut graph = DepGraph::default(); - graph - .run_target(&Target::new("cycle", "a").unwrap(), &projects, None) - .unwrap(); - graph - .run_target(&Target::new("cycle", "b").unwrap(), &projects, None) - .unwrap(); - graph - .run_target(&Target::new("cycle", "c").unwrap(), &projects, None) - .unwrap(); - - assert_eq!( - sort_batches(graph.sort_batched_topological().unwrap()), - vec![vec![NodeIndex::new(0)], vec![NodeIndex::new(1)]] - ); - } - - mod run_target { - use super::*; - - #[tokio::test] - async fn single_targets() { - let projects = create_project_graph().await; - - let mut graph = DepGraph::default(); - graph - .run_target(&Target::new("tasks", "test").unwrap(), &projects, None) - .unwrap(); - graph - .run_target(&Target::new("tasks", "lint").unwrap(), &projects, None) - .unwrap(); - - assert_snapshot!(graph.to_dot()); - - assert_eq!( - graph.sort_topological().unwrap(), - vec![ - NodeIndex::new(0), - NodeIndex::new(1), - NodeIndex::new(2), // sync project - NodeIndex::new(3), // test - NodeIndex::new(4), // lint - ] - ); - assert_eq!( - sort_batches(graph.sort_batched_topological().unwrap()), - vec![ - vec![NodeIndex::new(0)], - vec![NodeIndex::new(1), NodeIndex::new(2)], - vec![NodeIndex::new(3), NodeIndex::new(4)] - ] - ); - } - - #[tokio::test] - async fn deps_chain_target() { - let projects = create_tasks_project_graph().await; - - let mut graph = DepGraph::default(); - graph - .run_target(&Target::new("basic", "test").unwrap(), &projects, None) - .unwrap(); - graph - .run_target(&Target::new("basic", "lint").unwrap(), &projects, None) - .unwrap(); - graph - .run_target(&Target::new("chain", "a").unwrap(), &projects, None) - .unwrap(); - - assert_snapshot!(graph.to_dot()); - - assert_eq!( - graph.sort_topological().unwrap(), - vec![ - NodeIndex::new(0), - NodeIndex::new(1), - NodeIndex::new(2), // sync project - NodeIndex::new(3), // test - NodeIndex::new(4), // lint - NodeIndex::new(5), // sync project - NodeIndex::new(11), // f - NodeIndex::new(10), // e - NodeIndex::new(9), // d - NodeIndex::new(8), // c - NodeIndex::new(7), // b - NodeIndex::new(6), // a - ] - ); - assert_eq!( - sort_batches(graph.sort_batched_topological().unwrap()), - vec![ - vec![NodeIndex::new(0)], - vec![NodeIndex::new(1), NodeIndex::new(5)], - vec![NodeIndex::new(11)], - vec![NodeIndex::new(10)], - vec![NodeIndex::new(9)], - vec![NodeIndex::new(8)], - vec![NodeIndex::new(2), NodeIndex::new(7)], - vec![NodeIndex::new(3), NodeIndex::new(4), NodeIndex::new(6)] - ] - ); - } - - #[tokio::test] - async fn avoids_dupe_targets() { - let projects = create_project_graph().await; - - let mut graph = DepGraph::default(); - graph - .run_target(&Target::new("tasks", "lint").unwrap(), &projects, None) - .unwrap(); - graph - .run_target(&Target::new("tasks", "lint").unwrap(), &projects, None) - .unwrap(); - graph - .run_target(&Target::new("tasks", "lint").unwrap(), &projects, None) - .unwrap(); - - assert_snapshot!(graph.to_dot()); - - assert_eq!( - graph.sort_topological().unwrap(), - vec![ - NodeIndex::new(0), - NodeIndex::new(1), - NodeIndex::new(2), // sync project - NodeIndex::new(3), // lint - ] - ); - assert_eq!( - sort_batches(graph.sort_batched_topological().unwrap()), - vec![ - vec![NodeIndex::new(0)], - vec![NodeIndex::new(1), NodeIndex::new(2)], - vec![NodeIndex::new(3)] - ] - ); - } - - #[tokio::test] - async fn runs_all_projects_for_target_all_scope() { - let projects = create_tasks_project_graph().await; - - let mut graph = DepGraph::default(); - graph - .run_target(&Target::parse(":build").unwrap(), &projects, None) - .unwrap(); - - assert_snapshot!(graph.to_dot()); - - assert_eq!( - graph.sort_topological().unwrap(), - vec![ - NodeIndex::new(0), - NodeIndex::new(1), - NodeIndex::new(2), // sync project: basic - NodeIndex::new(3), // basic:build - NodeIndex::new(5), // sync project: build-c - NodeIndex::new(4), // sync project: build-a - NodeIndex::new(7), // build-c:build - NodeIndex::new(6), // build-a:build - NodeIndex::new(8), // sync project: build-b - NodeIndex::new(9), // build-b:build - ] - ); - assert_eq!( - sort_batches(graph.sort_batched_topological().unwrap()), - vec![ - vec![NodeIndex::new(0)], - vec![NodeIndex::new(1), NodeIndex::new(2), NodeIndex::new(5)], - vec![ - NodeIndex::new(3), - NodeIndex::new(4), - NodeIndex::new(7), - NodeIndex::new(8) - ], - vec![NodeIndex::new(6), NodeIndex::new(9)], - ] - ); - } - - #[tokio::test] - #[should_panic(expected = "Project(Target(NoProjectDepsInRunContext))")] - async fn errors_for_target_deps_scope() { - let projects = create_project_graph().await; - - let mut graph = DepGraph::default(); - graph - .run_target(&Target::parse("^:lint").unwrap(), &projects, None) - .unwrap(); - } - - #[tokio::test] - #[should_panic(expected = "Project(Target(NoProjectSelfInRunContext))")] - async fn errors_for_target_self_scope() { - let projects = create_project_graph().await; - - let mut graph = DepGraph::default(); - graph - .run_target(&Target::parse("~:lint").unwrap(), &projects, None) - .unwrap(); - } - - #[tokio::test] - #[should_panic(expected = "Project(UnconfiguredID(\"unknown\"))")] - async fn errors_for_unknown_project() { - let projects = create_project_graph().await; - - let mut graph = DepGraph::default(); - graph - .run_target(&Target::new("unknown", "test").unwrap(), &projects, None) - .unwrap(); - - assert_snapshot!(graph.to_dot()); - } - - #[tokio::test] - #[should_panic(expected = "Project(UnconfiguredTask(\"build\", \"tasks\"))")] - async fn errors_for_unknown_task() { - let projects = create_project_graph().await; - - let mut graph = DepGraph::default(); - graph - .run_target(&Target::new("tasks", "build").unwrap(), &projects, None) - .unwrap(); - - assert_snapshot!(graph.to_dot()); - } - } - - mod run_target_if_touched { - use super::*; - - #[tokio::test] - async fn skips_if_untouched_project() { - let projects = create_tasks_project_graph().await; - - let mut touched_files = HashSet::new(); - touched_files.insert(get_fixtures_dir("tasks").join("input-a/a.ts")); - touched_files.insert(get_fixtures_dir("tasks").join("input-c/c.ts")); - - let mut graph = DepGraph::default(); - graph - .run_target( - &Target::new("inputA", "a").unwrap(), - &projects, - Some(&touched_files), - ) - .unwrap(); - graph - .run_target( - &Target::new("inputB", "b").unwrap(), - &projects, - Some(&touched_files), - ) - .unwrap(); - - assert_snapshot!(graph.to_dot()); - } - - #[tokio::test] - async fn skips_if_untouched_task() { - let projects = create_tasks_project_graph().await; - - let mut touched_files = HashSet::new(); - touched_files.insert(get_fixtures_dir("tasks").join("input-a/a2.ts")); - touched_files.insert(get_fixtures_dir("tasks").join("input-b/b2.ts")); - touched_files.insert(get_fixtures_dir("tasks").join("input-c/any.ts")); - - let mut graph = DepGraph::default(); - graph - .run_target( - &Target::new("inputA", "a").unwrap(), - &projects, - Some(&touched_files), - ) - .unwrap(); - graph - .run_target( - &Target::new("inputB", "b2").unwrap(), - &projects, - Some(&touched_files), - ) - .unwrap(); - graph - .run_target( - &Target::new("inputC", "c").unwrap(), - &projects, - Some(&touched_files), - ) - .unwrap(); - - assert_snapshot!(graph.to_dot()); - } - } - - mod sync_project { - use super::*; - - #[tokio::test] - async fn isolated_projects() { - let projects = create_project_graph().await; - - let mut graph = DepGraph::default(); - graph.sync_project("advanced", &projects).unwrap(); - graph.sync_project("basic", &projects).unwrap(); - graph.sync_project("emptyConfig", &projects).unwrap(); - graph.sync_project("noConfig", &projects).unwrap(); - - assert_snapshot!(graph.to_dot()); - - assert_eq!( - graph.sort_topological().unwrap(), - vec![ - NodeIndex::new(0), - NodeIndex::new(1), - NodeIndex::new(2), - NodeIndex::new(4), // noConfig - NodeIndex::new(3), // basic - NodeIndex::new(5), // emptyConfig - ] - ); - assert_eq!( - sort_batches(graph.sort_batched_topological().unwrap()), - vec![ - vec![NodeIndex::new(0)], - vec![NodeIndex::new(4)], - vec![ - NodeIndex::new(1), - NodeIndex::new(2), - NodeIndex::new(3), - NodeIndex::new(5) - ] - ] - ); - } - - #[tokio::test] - async fn projects_with_deps() { - let projects = create_project_graph().await; - - let mut graph = DepGraph::default(); - graph.sync_project("foo", &projects).unwrap(); - graph.sync_project("bar", &projects).unwrap(); - graph.sync_project("baz", &projects).unwrap(); - graph.sync_project("basic", &projects).unwrap(); - - // Not deterministic! - // assert_snapshot!(graph.to_dot()); - - assert_eq!( - graph.sort_topological().unwrap(), - vec![ - NodeIndex::new(0), - NodeIndex::new(1), - NodeIndex::new(3), // bar - NodeIndex::new(4), // baz - NodeIndex::new(2), // foo - NodeIndex::new(6), // emptyConfig - NodeIndex::new(5), // basic - ] - ); - assert_eq!( - sort_batches(graph.sort_batched_topological().unwrap()), - vec![ - vec![NodeIndex::new(0)], - vec![NodeIndex::new(3), NodeIndex::new(4), NodeIndex::new(6)], - vec![NodeIndex::new(1), NodeIndex::new(2), NodeIndex::new(5)] - ] - ); - } - - #[tokio::test] - async fn projects_with_tasks() { - let projects = create_project_graph().await; - - let mut graph = DepGraph::default(); - graph.sync_project("noConfig", &projects).unwrap(); - graph.sync_project("tasks", &projects).unwrap(); - - assert_snapshot!(graph.to_dot()); - - assert_eq!( - graph.sort_topological().unwrap(), - vec![ - NodeIndex::new(0), - NodeIndex::new(1), - NodeIndex::new(2), - NodeIndex::new(3), - ] - ); - assert_eq!( - sort_batches(graph.sort_batched_topological().unwrap()), - vec![ - vec![NodeIndex::new(0)], - vec![NodeIndex::new(1), NodeIndex::new(2), NodeIndex::new(3)] - ] - ); - } - - #[tokio::test] - async fn avoids_dupe_projects() { - let projects = create_project_graph().await; - - let mut graph = DepGraph::default(); - graph.sync_project("advanced", &projects).unwrap(); - graph.sync_project("advanced", &projects).unwrap(); - graph.sync_project("advanced", &projects).unwrap(); - - assert_snapshot!(graph.to_dot()); - } - - #[tokio::test] - #[should_panic(expected = "Project(UnconfiguredID(\"unknown\"))")] - async fn errors_for_unknown_project() { - let projects = create_project_graph().await; - - let mut graph = DepGraph::default(); - graph.sync_project("unknown", &projects).unwrap(); - - assert_snapshot!(graph.to_dot()); - } - } -} diff --git a/crates/workspace/src/errors.rs b/crates/workspace/src/errors.rs index 45bc4f02d29..a0110fc4385 100644 --- a/crates/workspace/src/errors.rs +++ b/crates/workspace/src/errors.rs @@ -7,15 +7,6 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum WorkspaceError { - #[error("A dependency cycle has been detected for {0}.")] - DepGraphCycleDetected(String), - - #[error("Unknown node {0} found in dependency graph. How did this get here?")] - DepGraphUnknownNode(usize), - - #[error("{0}")] - ActionRunnerFailure(String), - #[error( "Unable to determine workspace root. Please create a {} configuration folder.", constants::CONFIG_DIRNAME diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 681770e41f3..2f3c35bbf61 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -1,12 +1,5 @@ -mod action; -mod action_runner; -mod actions; -mod dep_graph; mod errors; mod workspace; -pub use action::{Action, ActionStatus}; -pub use action_runner::ActionRunner; -pub use dep_graph::DepGraph; pub use errors::WorkspaceError; pub use workspace::Workspace; diff --git a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__default_graph.snap b/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__default_graph.snap deleted file mode 100644 index 372ca6c6c80..00000000000 --- a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__default_graph.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/workspace/src/dep_graph.rs -assertion_line: 255 -expression: graph.to_dot() - ---- -digraph { - 0 [ label = "\"SetupToolchain\"" ] - 1 [ label = "\"InstallNodeDeps\"" ] - 1 -> 0 [ ] -} - diff --git a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__sync_project__avoids_dupe_projects.snap b/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__sync_project__avoids_dupe_projects.snap deleted file mode 100644 index f30e9b10aea..00000000000 --- a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__sync_project__avoids_dupe_projects.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/workspace/src/dep_graph.rs -assertion_line: 343 -expression: graph.to_dot() - ---- -digraph { - 0 [ label = "\"SetupToolchain\"" ] - 1 [ label = "\"InstallNodeDeps\"" ] - 2 [ label = "\"SyncProject(advanced)\"" ] - 1 -> 0 [ ] - 2 -> 0 [ ] -} - diff --git a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__sync_project__isolated_projects.snap b/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__sync_project__isolated_projects.snap deleted file mode 100644 index 7b57c4ca14b..00000000000 --- a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__sync_project__isolated_projects.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: crates/workspace/src/dep_graph.rs -assertion_line: 288 -expression: graph.to_dot() - ---- -digraph { - 0 [ label = "\"SetupToolchain\"" ] - 1 [ label = "\"InstallNodeDeps\"" ] - 2 [ label = "\"SyncProject(advanced)\"" ] - 3 [ label = "\"SyncProject(basic)\"" ] - 4 [ label = "\"SyncProject(noConfig)\"" ] - 5 [ label = "\"SyncProject(emptyConfig)\"" ] - 1 -> 0 [ ] - 2 -> 0 [ ] - 3 -> 0 [ ] - 4 -> 0 [ ] - 3 -> 4 [ ] - 5 -> 0 [ ] -} - diff --git a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__sync_project__projects_with_tasks.snap b/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__sync_project__projects_with_tasks.snap deleted file mode 100644 index 886ec02a622..00000000000 --- a/crates/workspace/src/snapshots/moon_workspace__dep_graph__tests__sync_project__projects_with_tasks.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: crates/workspace/src/dep_graph.rs -assertion_line: 312 -expression: graph.to_dot() - ---- -digraph { - 0 [ label = "\"SetupToolchain\"" ] - 1 [ label = "\"InstallNodeDeps\"" ] - 2 [ label = "\"SyncProject(noConfig)\"" ] - 3 [ label = "\"SyncProject(tasks)\"" ] - 1 -> 0 [ ] - 2 -> 0 [ ] - 3 -> 0 [ ] -} - diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1715a6ab838..edb35c4caec 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -26,7 +26,7 @@ fn find_workspace_root(current_dir: PathBuf) -> Option { .map(|dir| dir.parent().unwrap().to_path_buf()) } -// project.yml +// .moon/project.yml fn load_global_project_config(root_dir: &Path) -> Result { let config_path = root_dir .join(constants::CONFIG_DIRNAME) @@ -55,7 +55,7 @@ fn load_global_project_config(root_dir: &Path) -> Result Result { let config_path = root_dir .join(constants::CONFIG_DIRNAME) diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 6e003d76fc1..c6ee6520f03 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -9,10 +9,16 @@ - To start, `moon query touched-files` can be used to query touched files. The same files `moon ci` and `moon run` use. - Also `moon query projects` can be used to query about projects in the project graph. +- Added `bash` as a support value for the project `language` setting. + +#### 🐞 Fixes + +- Fixed an issue with a globally installed moon not being executable in PowerShell. #### ⚙️ Internal - Updated Rust to v1.62. +- Refactored our action runner to support additional languages in the future. ## 0.5.0 diff --git a/website/docs/config/project.mdx b/website/docs/config/project.mdx index bfd63dbde3b..c5e406dbbce 100644 --- a/website/docs/config/project.mdx +++ b/website/docs/config/project.mdx @@ -70,6 +70,7 @@ fileGroups: The primary programming language the project is written in. Supports the following values: +- `bash` - A [Bash]() based project. - `javascript` - A [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) based project. - `typescript` (default) - A [TypeScript](https://www.typescriptlang.org/) based project. diff --git a/website/static/schemas/project.json b/website/static/schemas/project.json index ed32edc7ffd..b79f8d2996c 100644 --- a/website/static/schemas/project.json +++ b/website/static/schemas/project.json @@ -74,6 +74,7 @@ "ProjectLanguage": { "type": "string", "enum": [ + "bash", "javascript", "typescript", "unknown"