From 4ebf88cc1883ccd75b319bdf4953248b2846b00d Mon Sep 17 00:00:00 2001 From: keinberger Date: Fri, 12 Dec 2025 18:14:18 +0200 Subject: [PATCH] feat: implement multi-command aliases --- README.md | 4 + manifest/channel-manifest.json | 576 ++++++++++++++++++++++++++++----- src/channel.rs | 104 ++++-- src/commands/update.rs | 2 + src/miden_wrapper.rs | 295 ++++++++++------- 5 files changed, 739 insertions(+), 242 deletions(-) diff --git a/README.md b/README.md index ee6a9db5..3f41eb81 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,10 @@ Here's a table with all the currently available aliases: | miden send | Send transaction (state-changing) | miden-client send | | miden simulate | Simulate transaction (no commit) | miden-client exec | +Aliases are defined at the channel level in `manifest/channel-manifest.json`. Each alias is a pipeline of steps, and each step must specify which component's executable to run. Commands use the existing `CliCommand` tokens (`executable`, `lib_path`, `var_path`, or verbatim strings). + +Pipeline steps execute in order. User-provided CLI arguments are appended to the first step only. + ### Uninstalling a toolchain diff --git a/manifest/channel-manifest.json b/manifest/channel-manifest.json index 2f650142..fbfda2fd 100644 --- a/manifest/channel-manifest.json +++ b/manifest/channel-manifest.json @@ -23,7 +23,10 @@ "name": "vm", "package": "miden-vm", "version": "0.15.0", - "features": ["executable", "concurrent"], + "features": [ + "executable", + "concurrent" + ], "installed_executable": "miden" }, { @@ -31,44 +34,140 @@ "repository_url": "https://github.com/0xMiden/miden-client.git", "crate_name": "miden-client-cli", "revision": "83df2aa115b2617e211d1d929a1189ffabbd137b", - "installed_executable": "miden-client", - "aliases": { - "account": ["executable", "new-account"], - "deploy": ["executable", "-s", "public", "--account-type", "regular-account-immutable-code"], - "faucet": ["executable", "mint"], - "new-wallet": ["executable", "new-wallet", "--deploy"], - "call": ["executable", "call", "--show"], - "send": ["executable", "send"], - "simulate": ["executable", "exec"] - } + "installed_executable": "miden-client" }, { "name": "midenc", "version": "0.1.0", "rustup_channel": "nightly-2025-03-20", - "requires": ["base", "std"], - "call_format": ["executable", "compile", "-L", "lib_path"] + "requires": [ + "base", + "std" + ], + "call_format": [ + "executable", + "compile", + "-L", + "lib_path" + ] }, { "name": "cargo-miden", "version": "0.1.0", "rustup_channel": "nightly-2025-03-20", - "aliases": { - "new": ["cargo", "miden", "new"], - "build": ["cargo", "miden", "build"] - }, "symlink_name": "cargo-miden" }, { "name": "node", "package": "miden-node", "installed_executable": "miden-node", - "version": "0.10.1", - "aliases": { - "start-node": ["executable", "bundled", "start", "--data-directory", "data", "--rpc.url", "http://0.0.0.0:57291"] - } + "version": "0.10.1" } - ] + ], + "aliases": { + "account": [ + { + "component": "client", + "command": [ + "executable", + "new-account" + ] + } + ], + "deploy": [ + { + "component": "client", + "command": [ + "executable", + "-s", + "public", + "--account-type", + "regular-account-immutable-code" + ] + } + ], + "faucet": [ + { + "component": "client", + "command": [ + "executable", + "mint" + ] + } + ], + "new-wallet": [ + { + "component": "client", + "command": [ + "executable", + "new-wallet", + "--deploy" + ] + } + ], + "call": [ + { + "component": "client", + "command": [ + "executable", + "call", + "--show" + ] + } + ], + "send": [ + { + "component": "client", + "command": [ + "executable", + "send" + ] + } + ], + "simulate": [ + { + "component": "client", + "command": [ + "executable", + "exec" + ] + } + ], + "new": [ + { + "component": "cargo-miden", + "command": [ + "cargo", + "miden", + "new" + ] + } + ], + "build": [ + { + "component": "cargo-miden", + "command": [ + "cargo", + "miden", + "build" + ] + } + ], + "start-node": [ + { + "component": "node", + "command": [ + "executable", + "bundled", + "start", + "--data-directory", + "data", + "--rpc.url", + "http://0.0.0.0:57291" + ] + } + ] + } }, { "name": "0.16.0", @@ -91,51 +190,150 @@ "name": "vm", "package": "miden-vm", "version": "0.16.2", - "features": ["executable", "concurrent"], + "features": [ + "executable", + "concurrent" + ], "installed_executable": "miden-vm" }, { "name": "client", "package": "miden-client-cli", "version": "0.10.2", - "installed_executable": "miden-client", - "aliases": { - "account": ["executable", "new-account"], - "deploy": ["executable", "-s", "public", "--account-type", "regular-account-immutable-code"], - "faucet": ["executable", "mint"], - "new-wallet": ["executable", "new-wallet", "--deploy"], - "call": ["executable", "call", "--show"], - "send": ["executable", "send"], - "simulate": ["executable", "exec"] - } + "installed_executable": "miden-client" }, { "name": "midenc", "version": "0.1.0", "rustup_channel": "nightly-2025-03-20", - "requires": ["base", "std"], - "call_format": ["executable", "compile", "-L", "lib_path"] + "requires": [ + "base", + "std" + ], + "call_format": [ + "executable", + "compile", + "-L", + "lib_path" + ] }, { "name": "cargo-miden", "version": "0.1.0", "rustup_channel": "nightly-2025-03-20", - "aliases": { - "new": ["cargo", "miden", "new"], - "build": ["cargo", "miden", "build"] - }, "symlink_name": "cargo-miden" }, { "name": "node", "package": "miden-node", "installed_executable": "miden-node", - "version": "0.10.1", - "aliases": { - "start-node": ["executable", "bundled", "start", "--data-directory", "data", "--rpc.url", "http://0.0.0.0:57291"] - } + "version": "0.10.1" } - ] + ], + "aliases": { + "account": [ + { + "component": "client", + "command": [ + "executable", + "new-account" + ] + } + ], + "deploy": [ + { + "component": "client", + "command": [ + "executable", + "-s", + "public", + "--account-type", + "regular-account-immutable-code" + ] + } + ], + "faucet": [ + { + "component": "client", + "command": [ + "executable", + "mint" + ] + } + ], + "new-wallet": [ + { + "component": "client", + "command": [ + "executable", + "new-wallet", + "--deploy" + ] + } + ], + "call": [ + { + "component": "client", + "command": [ + "executable", + "call", + "--show" + ] + } + ], + "send": [ + { + "component": "client", + "command": [ + "executable", + "send" + ] + } + ], + "simulate": [ + { + "component": "client", + "command": [ + "executable", + "exec" + ] + } + ], + "new": [ + { + "component": "cargo-miden", + "command": [ + "cargo", + "miden", + "new" + ] + } + ], + "build": [ + { + "component": "cargo-miden", + "command": [ + "cargo", + "miden", + "build" + ] + } + ], + "start-node": [ + { + "component": "node", + "command": [ + "executable", + "bundled", + "start", + "--data-directory", + "data", + "--rpc.url", + "http://0.0.0.0:57291" + ] + } + ] + } }, { "name": "0.17.2", @@ -158,39 +356,37 @@ "name": "vm", "package": "miden-vm", "version": "0.17.2", - "features": ["executable", "concurrent"], + "features": [ + "executable", + "concurrent" + ], "installed_executable": "miden-vm" }, { "name": "client", "package": "miden-client-cli", "version": "0.11.8", - "installed_executable": "miden-client", - "aliases": { - "account": ["executable", "new-account"], - "deploy": ["executable", "-s", "public", "--account-type", "regular-account-immutable-code"], - "faucet": ["executable", "mint"], - "new-wallet": ["executable", "new-wallet", "--deploy"], - "call": ["executable", "call", "--show"], - "send": ["executable", "send"], - "simulate": ["executable", "exec"] - } + "installed_executable": "miden-client" }, { "name": "midenc", "version": "0.4.1", "rustup_channel": "nightly-2025-07-20", - "requires": ["base", "std"], - "call_format": ["executable", "compile", "-L", "lib_path"] + "requires": [ + "base", + "std" + ], + "call_format": [ + "executable", + "compile", + "-L", + "lib_path" + ] }, { "name": "cargo-miden", "version": "0.4.1", "rustup_channel": "nightly-2025-07-20", - "aliases": { - "new": ["cargo", "miden", "new"], - "build": ["cargo", "miden", "build"] - }, "symlink_name": "cargo-miden" }, { @@ -198,12 +394,114 @@ "repository_url": "https://github.com/lambdaclass/miden-node.git", "crate_name": "miden-node", "branch": "fabrizioorsi/create-account-data-dirs", - "installed_executable": "miden-node", - "aliases": { - "start-node": ["executable", "bundled", "start", "--data-directory", "var_path", "data", "--rpc.url", "http://0.0.0.0:57291"] - } + "installed_executable": "miden-node" } - ] + ], + "aliases": { + "account": [ + { + "component": "client", + "command": [ + "executable", + "new-account" + ] + } + ], + "deploy": [ + { + "component": "client", + "command": [ + "executable", + "-s", + "public", + "--account-type", + "regular-account-immutable-code" + ] + } + ], + "faucet": [ + { + "component": "client", + "command": [ + "executable", + "mint" + ] + } + ], + "new-wallet": [ + { + "component": "client", + "command": [ + "executable", + "new-wallet", + "--deploy" + ] + } + ], + "call": [ + { + "component": "client", + "command": [ + "executable", + "call", + "--show" + ] + } + ], + "send": [ + { + "component": "client", + "command": [ + "executable", + "send" + ] + } + ], + "simulate": [ + { + "component": "client", + "command": [ + "executable", + "exec" + ] + } + ], + "new": [ + { + "component": "cargo-miden", + "command": [ + "cargo", + "miden", + "new" + ] + } + ], + "build": [ + { + "component": "cargo-miden", + "command": [ + "cargo", + "miden", + "build" + ] + } + ], + "start-node": [ + { + "component": "node", + "command": [ + "executable", + "bundled", + "start", + "--data-directory", + "var_path", + "data", + "--rpc.url", + "http://0.0.0.0:57291" + ] + } + ] + } }, { "name": "0.19.1", @@ -226,51 +524,151 @@ "name": "vm", "package": "miden-vm", "version": "0.19.1", - "features": ["executable", "concurrent"], + "features": [ + "executable", + "concurrent" + ], "installed_executable": "miden-vm" }, { "name": "client", "package": "miden-client-cli", "version": "0.12.3", - "installed_executable": "miden-client", - "aliases": { - "account": ["executable", "new-account"], - "deploy": ["executable", "-s", "public", "--account-type", "regular-account-immutable-code"], - "faucet": ["executable", "mint"], - "new-wallet": ["executable", "new-wallet", "--deploy"], - "call": ["executable", "call", "--show"], - "send": ["executable", "send"], - "simulate": ["executable", "exec"] - } + "installed_executable": "miden-client" }, { "name": "midenc", "version": "0.5.1", "rustup_channel": "nightly-2025-07-20", - "requires": ["base", "std"], - "call_format": ["executable", "compile", "-L", "lib_path"] + "requires": [ + "base", + "std" + ], + "call_format": [ + "executable", + "compile", + "-L", + "lib_path" + ] }, { "name": "cargo-miden", "version": "0.5.1", "rustup_channel": "nightly-2025-07-20", - "aliases": { - "new": ["cargo", "miden", "new"], - "build": ["cargo", "miden", "build"] - }, "symlink_name": "cargo-miden" }, { "name": "node", "package": "miden-node", "version": "0.12.5", - "installed_executable": "miden-node", - "aliases": { - "start-node": ["executable", "bundled", "start", "--data-directory", "var_path", "data", "--rpc.url", "http://0.0.0.0:57291"] - } + "installed_executable": "miden-node" } - ] + ], + "aliases": { + "account": [ + { + "component": "client", + "command": [ + "executable", + "new-account" + ] + } + ], + "deploy": [ + { + "component": "client", + "command": [ + "executable", + "-s", + "public", + "--account-type", + "regular-account-immutable-code" + ] + } + ], + "faucet": [ + { + "component": "client", + "command": [ + "executable", + "mint" + ] + } + ], + "new-wallet": [ + { + "component": "client", + "command": [ + "executable", + "new-wallet", + "--deploy" + ] + } + ], + "call": [ + { + "component": "client", + "command": [ + "executable", + "call", + "--show" + ] + } + ], + "send": [ + { + "component": "client", + "command": [ + "executable", + "send" + ] + } + ], + "simulate": [ + { + "component": "client", + "command": [ + "executable", + "exec" + ] + } + ], + "new": [ + { + "component": "cargo-miden", + "command": [ + "cargo", + "miden", + "new" + ] + } + ], + "build": [ + { + "component": "cargo-miden", + "command": [ + "cargo", + "miden", + "build" + ] + } + ], + "start-node": [ + { + "component": "node", + "command": [ + "executable", + "bundled", + "start", + "--data-directory", + "var_path", + "data", + "--rpc.url", + "http://0.0.0.0:57291" + ] + } + ] + } } ] -} +} \ No newline at end of file diff --git a/src/channel.rs b/src/channel.rs index 92fe79f2..765b7838 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -1,6 +1,6 @@ use std::{ borrow::Cow, - collections::HashMap, + collections::{HashMap, HashSet}, fmt::{self, Display}, hash::{Hash, Hasher}, path::PathBuf, @@ -50,6 +50,11 @@ pub struct Channel { /// The set of toolchain components available in this channel pub components: Vec, + + /// Channel-level aliases that can orchestrate multiple components. + #[serde(default)] + #[serde(skip_serializing_if = "HashMap::is_empty")] + pub aliases: HashMap, } enum InstallationMotive { @@ -73,9 +78,10 @@ impl Channel { name: semver::Version, alias: Option, components: Vec, + aliases: HashMap, tags: Vec, ) -> Self { - Self { name, alias, components, tags } + Self { name, alias, components, aliases, tags } } pub fn get_component(&self, name: impl AsRef) -> Option<&Component> { @@ -119,11 +125,8 @@ impl Channel { } /// Get all the aliases that the Channel is aware of - pub fn get_aliases(&self) -> HashMap { - self.components.iter().fold(HashMap::new(), |mut acc, component| { - acc.extend(component.aliases.clone()); - acc - }) + pub fn get_aliases(&self) -> HashMap { + self.aliases.clone() } /// Creates a "partial channel" from the original channel, given a toolchain @@ -201,11 +204,26 @@ impl Channel { } } + let allowed: HashSet<_> = + components_to_install.iter().map(|c| c.name.to_string()).collect(); + let aliases = self + .aliases + .iter() + .filter_map(|(alias, pipeline)| { + if pipeline.iter().all(|step| allowed.contains(&step.component)) { + Some((alias.clone(), pipeline.clone())) + } else { + None + } + }) + .collect(); + let partial_channel = Channel { name: self.name.clone(), alias: self.alias.clone(), tags: vec![Tags::Partial], components: components_to_install, + aliases, }; Some(partial_channel) @@ -362,7 +380,7 @@ impl Display for InstalledFile { } } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] /// Represents each possible "word" variant that is passed to the Command /// line. These are used to resolve an [[Alias]] to its associated command. @@ -450,8 +468,16 @@ pub fn resolve_command( } pub type Alias = String; -/// List of the commands that need to be run when [[Alias]] is called. -pub type CLICommand = Vec; +/// Ordered list of steps that define how an alias should be executed. +pub type AliasPipeline = Vec; +/// A single step in an alias pipeline. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct AliasStep { + /// Component whose executable should be used for this step. + pub component: String, + /// Commands to resolve and execute for this step. + pub command: Vec, +} /// An installable component of a toolchain #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Component { @@ -491,29 +517,6 @@ pub struct Component { #[serde(skip_serializing_if = "Option::is_none")] #[serde(flatten)] installed_file: Option, - /// This HashMap associates each alias to the corresponding command that - /// needs to be executed. - /// NOTE: The list of commands that is resolved can have an "arbitrary" - /// ordering: the executable associated with this command is not forced to - /// come in first. - /// - /// Here's an example aliases entry in a manifest.json: - /// - /// ```json - /// { - /// "name": "component-name", - /// "package": "component-package", - /// "version": "X.Y.Z", - /// "installed_executable": "miden-component", - /// "aliases": { - /// "alias1": [{"resolve": "component-name"}, {"verbatim": "argument"}], - /// "alias2": [{"verbatim": "cargo"}, {"resolve": "component-name"}, {"verbatim": "build"}] - /// } - /// }, - /// ``` - #[serde(default)] - #[serde(skip_serializing_if = "HashMap::is_empty")] - pub aliases: HashMap, /// The file used by midenup's 'miden' to call the components executable. /// If None, then the component's file will be saved as 'miden '. /// This distinction exists mainly for components like cargo-miden, which @@ -533,7 +536,6 @@ impl Component { call_format: vec![], rustup_channel: None, installed_file: None, - aliases: HashMap::new(), symlink_name: None, } } @@ -700,6 +702,40 @@ impl Component { } } +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn alias_pipeline_deserializes_multiple_steps() { + let pipeline_json = json!([ + { "component": "client", "command": ["executable", "first-arg"] }, + { "component": "faucet", "command": ["executable", "second", "--flag"] } + ]); + + let pipeline: AliasPipeline = + serde_json::from_value(pipeline_json).expect("pipeline should deserialize"); + + assert_eq!(pipeline.len(), 2); + assert_eq!( + pipeline[0].command, + vec![CliCommand::Executable, CliCommand::Verbatim("first-arg".into())] + ); + assert_eq!(pipeline[0].component, "client"); + assert_eq!( + pipeline[1].command, + vec![ + CliCommand::Executable, + CliCommand::Verbatim("second".into()), + CliCommand::Verbatim("--flag".into()) + ] + ); + assert_eq!(pipeline[1].component, "faucet"); + } +} + /// User-facing channel reference. The main difference with this and [Channel] /// is the definition of "stable". The definition of "stable" 'under the hood' /// is the lastest available non-nightly channel. If the user passes diff --git a/src/commands/update.rs b/src/commands/update.rs index ccc6b5a1..8b3d261a 100644 --- a/src/commands/update.rs +++ b/src/commands/update.rs @@ -59,6 +59,7 @@ midenup install stable alias: upstream_stable.alias.clone(), tags: local_stable.tags.clone(), components, + aliases: upstream_stable.aliases.clone(), } }; @@ -162,6 +163,7 @@ fn update_channel( alias: upstream_channel.alias.clone(), tags: local_channel.tags.clone(), components, + aliases: upstream_channel.aliases.clone(), } }; let mut path_warning_displayed = false; diff --git a/src/miden_wrapper.rs b/src/miden_wrapper.rs index 43af9c35..492daee1 100644 --- a/src/miden_wrapper.rs +++ b/src/miden_wrapper.rs @@ -5,7 +5,7 @@ use colored::Colorize; pub use crate::config::Config; use crate::{ - channel::{CLICommand, Channel, Component, InstalledFile, resolve_command}, + channel::{AliasPipeline, AliasStep, Channel, Component, InstalledFile, resolve_command}, manifest::Manifest, toolchain::Toolchain, }; @@ -35,15 +35,28 @@ enum MidenArgument { /// The passed argument was an Alias stored in the local [[Manifest]]. [[AliasResolution]] /// represents the list of commands that need to be executed. NOTE: Some of these might need /// to get resolved. - Alias(Component, CLICommand), + Alias(AliasPipeline, ChannelOrigin), /// The argument was the name of a component stored in the [[Manifest]]. - Component(Component), + Component(Box, ChannelOrigin), +} + +#[derive(Debug)] +struct ResolvedCommand { + program: String, + args: Vec, } enum EnvironmentError { UnkownArgument(String), } +/// Tracks where a resolution came from so we can warn when falling back to an installed channel. +#[derive(Debug, Clone, Copy)] +enum ChannelOrigin { + Active, + Installed, +} + #[derive(Debug)] struct ToolchainEnvironment<'a> { /// We use the original channel as a fallback to @@ -77,51 +90,43 @@ impl<'a> ToolchainEnvironment<'a> { } fn resolve(&self, argument: String) -> Result { - #[derive(Debug, Clone, Copy)] - enum ChannelType { - Installed, - Active, + if let Some(active_channel) = self.active_channel.as_ref() + && let Some(pipeline) = active_channel.aliases.get(&argument) + { + return Ok(MidenArgument::Alias(pipeline.clone(), ChannelOrigin::Active)); } - [ - (self.active_channel.clone(), ChannelType::Active), - (Some(self.installed_channel.clone()), ChannelType::Installed), - ] - .iter() - .filter_map(|(ch, ch_type)| ch.as_ref().map(|ch| (ch, ch_type))) - .flat_map(|(ch, ch_type)| ch.components.iter().map(move |comp| (comp, ch_type))) - .find_map(|(comp, ch_type)| { - if let Some(associated_command) = comp.aliases.get(&argument) { - Some((MidenArgument::Alias(comp.clone(), associated_command.to_owned()), ch_type)) - } else if comp.name == argument { - Some((MidenArgument::Component(comp.clone()), ch_type)) - } else { - None - } - }) - .inspect(|resolution| { - if let Some(warning_message) = match resolution { - (MidenArgument::Alias(comp, _ ), ChannelType::Installed) => Some(format!( - "{}: {} is an alias from component {}, which is installed but is not part of the current active toolchain.", - "WARNING".yellow().bold(), - argument, - comp.name, - - )), - (MidenArgument::Component(comp), ChannelType::Installed) => Some(format!( - "{}: {} is installed, but it is not part of the current active toolchain.", - "WARNING".yellow().bold(), - comp.name, - - )), - _ => None, - } { - println!("{warning_message}") - } + if let Some(pipeline) = self.installed_channel.aliases.get(&argument) { + println!( + "{}: '{}' is available in the installed toolchain but not in the current active selection.", + "WARNING".yellow().bold(), + argument, + ); + return Ok(MidenArgument::Alias(pipeline.clone(), ChannelOrigin::Installed)); } - ) - .map(|(ch, _)| ch) - .ok_or(EnvironmentError::UnkownArgument(argument)) + + if let Some(active_channel) = self.active_channel.as_ref() + && let Some(component) = active_channel.get_component(&argument) + { + return Ok(MidenArgument::Component( + Box::new(component.clone()), + ChannelOrigin::Active, + )); + } + + if let Some(component) = self.installed_channel.get_component(&argument) { + println!( + "{}: {} is installed, but it is not part of the current active toolchain.", + "WARNING".yellow().bold(), + component.name, + ); + return Ok(MidenArgument::Component( + Box::new(component.clone()), + ChannelOrigin::Installed, + )); + } + + Err(EnvironmentError::UnkownArgument(argument)) } fn get_executables_display(&self) -> String { @@ -153,6 +158,47 @@ impl<'a> ToolchainEnvironment<'a> { keys.sort(); keys.iter().map(|alias| format!(" {}\n", alias.bold())).collect::() } + + fn resolve_component_for_step( + &self, + component_name: &str, + preferred: ChannelOrigin, + ) -> anyhow::Result<(&Channel, &Component)> { + // Prefer the originating channel, but warn if we have to fall back to the other installed + // channel. + let (primary, fallback, warning_origin) = match preferred { + ChannelOrigin::Active => ( + self.active_channel.as_ref(), + Some(self.installed_channel), + "current active toolchain", + ), + ChannelOrigin::Installed => ( + Some(self.installed_channel), + self.active_channel.as_ref(), + "installed toolchain", + ), + }; + + if let Some(primary_channel) = primary + && let Some(component) = primary_channel.get_component(component_name) + { + return Ok((primary_channel, component)); + } + + if let Some(fallback_channel) = fallback + && let Some(component) = fallback_channel.get_component(component_name) + { + println!( + "{}: {} is not present in the {}; using the other installed channel definition.", + "WARNING".yellow().bold(), + component.name, + warning_origin, + ); + return Ok((fallback_channel, component)); + } + + bail!("Component '{}' is not available in the current toolchain", component_name) + } } /// These are the possible types of subcommands that `miden` is aware of. @@ -244,9 +290,7 @@ For more information, try 'miden help'. ToolchainEnvironment::new(installed_channel, partial_channel) }; - let active_channel = toolchain_environment.get_active_channel(); - - let help_flag = match parsed_subcommand { + let (help_flag, user_args, resolve_target) = match parsed_subcommand { MidenSubcommand::Help(HelpMessage::Default) => unreachable!(), MidenSubcommand::Help(HelpMessage::Toolchain) => { let help = toolchain_help(&toolchain_environment); @@ -255,95 +299,108 @@ For more information, try 'miden help'. return Ok(()); }, - MidenSubcommand::Help(HelpMessage::Resolve(_)) => { + MidenSubcommand::Help(HelpMessage::Resolve(resolve)) => { // NOTE: We rely on the different component's CLI interfaces to // recognize the "--help" flag. Currently, this relies on the fact // that clap recognizes said flag by default. // Source: https://github.com/clap-rs/clap/blob/583ba4ad9a4aea71e5b852b142715acaeaaaa050/src/_features.rs#L10 - Some(String::from("--help")) + (Some(String::from("--help")), vec![], resolve) + }, + MidenSubcommand::Resolve(resolve) => { + // argv is of the form: + // miden ... + // So we skip the first two and pass the rest to the underlying executable. + (None, argv.iter().skip(2).cloned().collect(), resolve) }, - _ => None, + _ => unreachable!(), }; - // We obtain the target executable and prefixes that are associated with the - // passed subcommand. - let (target_exe, mut prefix_args) = match parsed_subcommand { - MidenSubcommand::Version => unreachable!(), - MidenSubcommand::Help(HelpMessage::Default) => unreachable!(), - MidenSubcommand::Help(HelpMessage::Toolchain) => unreachable!(), - // Resolution, either for help or for actual execution is the same. The - // only difference is wheter we append "--help" at the end and if we - // process additional arguments. - MidenSubcommand::Help(HelpMessage::Resolve(resolve)) - | MidenSubcommand::Resolve(resolve) => { - match toolchain_environment.resolve(resolve.clone()) { - Ok(MidenArgument::Alias(component, alias_resolutions)) => { - let commands = - resolve_command(&alias_resolutions, active_channel, &component, config)?; - - // SAFETY: Safe under the assumption that every alias has an - // associated command. - let command = commands.first().unwrap().clone(); - let aliased_arguments: Vec = commands.into_iter().skip(1).collect(); - - (command, aliased_arguments) - }, - Ok(MidenArgument::Component(component)) => { - let call_convention = resolve_command( - &component.get_call_format(), - active_channel, - &component, - config, - )?; - - // SAFETY: Safe under the assumption that every call_format has at least one - // argument - let command = call_convention.first().unwrap().clone(); - let args: Vec = call_convention.into_iter().skip(1).collect(); - - (command, args) - }, - Err(EnvironmentError::UnkownArgument(argument)) => { - let help_message = toolchain_help(&toolchain_environment); - let err_msg = format!( - "Failed to resolve '{}': Neither known alias or component. + let resolved_argument = match toolchain_environment.resolve(resolve_target.clone()) { + Ok(argument) => argument, + Err(EnvironmentError::UnkownArgument(argument)) => { + let help_message = toolchain_help(&toolchain_environment); + let err_msg = format!( + "Failed to resolve '{}': Neither known alias or component. {}", - argument, help_message - ); - bail!(err_msg); - }, - } + argument, help_message + ); + bail!(err_msg); }, }; - let rest_of_args = if let Some(help_flag) = help_flag { - prefix_args.extend([help_flag]); - // If the user requested for help for a specific component, we skip any - // additional passed in arguments. - argv.iter().skip(argv.len()) - } else { - // argv is of the form: - // miden ... - // So we skip the first two and pass the rest to the underlying executable. - argv.iter().skip(2) + let mut resolved_commands: Vec = match resolved_argument { + MidenArgument::Alias(alias_pipeline, origin) => alias_pipeline + .iter() + .map(|step| { + resolve_to_command(step, &toolchain_environment, origin, config).with_context( + || format!("failed to resolve alias step for '{}'", resolve_target), + ) + }) + .collect::>()?, + MidenArgument::Component(component, origin) => { + let step = AliasStep { + component: component.name.to_string(), + command: component.get_call_format(), + }; + vec![resolve_to_command(&step, &toolchain_environment, origin, config)?] + }, }; - let prefix_args = prefix_args.iter().map(OsString::from).chain(rest_of_args.cloned()).collect(); + if let Some(help_flag) = help_flag + && let Some(first) = resolved_commands.first_mut() + { + first.args.push(help_flag); + } + + for (idx, resolved_command) in resolved_commands.iter().enumerate() { + let mut args: Vec = resolved_command.args.iter().map(OsString::from).collect(); + if idx == 0 { + args.extend(user_args.iter().cloned()); + } - let mut command = config - .execute_command(toolchain_environment.installed_channel, &target_exe, &prefix_args) - .with_context(|| format!("failed to run 'miden {subcommand}'"))?; + let mut command = config + .execute_command( + toolchain_environment.installed_channel, + &resolved_command.program, + &args, + ) + .with_context(|| format!("failed to run 'miden {subcommand}'"))?; - let status = command.wait().with_context(|| { - format!("error occurred while waiting for 'miden {subcommand}' to finish executing") - })?; + let status = command.wait().with_context(|| { + format!("error occurred while waiting for 'miden {subcommand}' to finish executing") + })?; - if status.success() { - Ok(()) - } else { - bail!("'miden {}' failed with status {}", subcommand, status.code().unwrap_or(1)) + if !status.success() { + bail!( + "'miden {}' failed while running step {} with status {}", + subcommand, + idx + 1, + status.code().unwrap_or(1) + ) + } } + + Ok(()) +} + +fn resolve_to_command( + step: &AliasStep, + toolchain_environment: &ToolchainEnvironment, + preferred_origin: ChannelOrigin, + config: &Config, +) -> anyhow::Result { + let (channel, component) = + toolchain_environment.resolve_component_for_step(&step.component, preferred_origin)?; + let resolution = resolve_command(&step.command, channel, component, config)?; + let (program, args) = resolution + .split_first() + .ok_or_else(|| anyhow!("Resolved command for {} is empty", component.name))?; + + Ok(ResolvedCommand { + program: program.clone(), + args: args.to_vec(), + }) } fn display_version(config: &Config) -> String {