use std::{borrow::Cow, collections::VecDeque, ffi::OsString, process::ExitCode, string::ToString};
use anyhow::{Context, bail};
use colored::Colorize;
pub use crate::config::Config;
use crate::{
channel::{Channel, UserChannel},
exec::{self, Executable, Resolver},
manifest::{Component, ComponentKind, ExecutableComponent},
state::LocalState,
toolchain::Toolchain,
};
#[derive(Debug, thiserror::Error)]
enum EnvironmentError {
#[error("invalid command '{command}': not a known alias or executable component")]
InvalidCommand { command: String },
#[error(
"the toolchain declared by this project defines an ambiguous alias: {reason}. Remove one \
of the components from your miden-toolchain.toml, or name the component directly."
)]
AmbiguousAlias { reason: String },
#[error("'{command}' requires a subcommand, one of: {}", available.join(", "))]
MissingSubcommand { command: String, available: Vec<String> },
#[error("invalid subcommand '{subcommand}' of '{command}', expected one of: {}", available.join(", "))]
InvalidSubcommand {
command: String,
subcommand: String,
available: Vec<String>,
},
#[error("invalid command '{component}': this names an executable component which is not directly callable, did you mean one of its aliases?: {}", available.join(", "))]
Hidden {
component: String,
available: Vec<String>,
},
#[error("invalid command '{component}': this names a non-executable component")]
NotExecutable { component: String },
#[error("invalid command '{component}': this component is invoked as 'miden {command_name}'")]
Renamed { component: String, command_name: String },
}
#[derive(Debug)]
enum HelpMessage<'a> {
Default,
Toolchain,
Resolve {
command: &'a str,
matches: &'a clap::ArgMatches,
},
}
#[derive(Debug)]
enum MidenArgument<'a> {
Command {
component: &'a Component,
executable: &'a Executable,
matches: &'a clap::ArgMatches,
},
Subcommand {
component: &'a Component,
format: &'a Executable,
executable: &'a Executable,
rest: Vec<OsString>,
},
Alias {
component: &'a Component,
executable: &'a Executable,
matches: &'a clap::ArgMatches,
},
Component {
component: &'a Component,
spec: &'a ExecutableComponent,
matches: &'a clap::ArgMatches,
},
}
struct ExecutionEnvironment<'a> {
argument: MidenArgument<'a>,
active_channel: &'a Channel,
}
#[derive(Debug)]
struct ToolchainEnvironment<'a> {
installed_channel: &'a Channel,
active_channel: Option<Channel>,
}
#[derive(Debug, Clone, Copy)]
enum ChannelType {
Installed,
Active,
}
impl<'a> ToolchainEnvironment<'a> {
fn new(installed_channel: &'a Channel, active_channel: Option<Channel>) -> Self {
ToolchainEnvironment { installed_channel, active_channel }
}
fn get_active_channel(&self) -> (&Channel, ChannelType) {
if let Some(active_channel) = self.active_channel.as_ref() {
(active_channel, ChannelType::Active)
} else {
(self.installed_channel, ChannelType::Installed)
}
}
fn resolve(
&'a self,
argument: &'a str,
matches: &'a clap::ArgMatches,
) -> Result<ExecutionEnvironment<'a>, EnvironmentError> {
if let Some(active_channel) = self.active_channel.as_ref() {
active_channel
.get_aliases()
.map_err(|err| EnvironmentError::AmbiguousAlias { reason: err.to_string() })?;
} else if let Err(err) = self.installed_channel.get_aliases() {
crate::warn!("{err}. Naming the component directly resolves it unambiguously.");
}
if let Some(active_channel) = self.active_channel.as_ref() {
match resolve_argument(active_channel, argument, matches) {
Ok(arg) => return Ok(ExecutionEnvironment { argument: arg, active_channel }),
Err(EnvironmentError::InvalidCommand { .. }) => {},
Err(e) => return Err(e),
}
}
let miden_argument = resolve_argument(self.installed_channel, argument, matches)?;
Ok(ExecutionEnvironment {
argument: miden_argument,
active_channel: self.installed_channel,
})
}
fn get_executables_display(&self) -> String {
self.get_active_channel()
.0
.components
.iter()
.filter(|c| c.is_callable())
.map(|c| format!(" {}\n", c.name.bold()))
.collect::<String>()
}
fn get_libraries_display(&self) -> String {
self.get_active_channel()
.0
.components
.iter()
.filter_map(|comp| match comp.kind() {
ComponentKind::Package | ComponentKind::LegacyPackage { .. } => {
Some(format!(" {}\n", comp.name.bold()))
},
_ => None,
})
.collect::<String>()
}
fn get_aliases_display(&self) -> String {
let aliases = self.get_active_channel().0.get_alias_names();
aliases
.into_iter()
.map(|alias| format!(" {}\n", alias.bold()))
.collect::<String>()
}
}
#[derive(Debug)]
enum MidenSubcommand<'a> {
Help(HelpMessage<'a>),
Version,
Resolve {
command: &'a str,
matches: &'a clap::ArgMatches,
},
}
const CLAP_HELP_FLAG: &str = "help_flag";
const CLAP_HELP_SUBCMD: &str = "help";
const CLAP_HELP_COMPONENT_ARG: &str = "alias_component";
const CLAP_VERSION_FLAG: &str = "version";
fn build_miden_command() -> clap::Command {
clap::Command::new("miden")
.about("The Miden toolchain porcelain")
.disable_help_flag(true)
.disable_help_subcommand(true)
.disable_version_flag(true)
.allow_external_subcommands(true)
.arg(clap::Arg::new(CLAP_HELP_FLAG).short('h').long("help").action(clap::ArgAction::SetTrue))
.subcommand(
clap::Command::new(CLAP_HELP_SUBCMD)
.about("Print help information")
.arg(clap::Arg::new(CLAP_HELP_COMPONENT_ARG).num_args(0..=1)),
)
.arg(clap::Arg::new(CLAP_VERSION_FLAG).long("version").action(clap::ArgAction::SetTrue))
}
fn parse_matches(matches: &clap::ArgMatches) -> MidenSubcommand<'_> {
if matches.get_flag(CLAP_HELP_FLAG) {
return MidenSubcommand::Help(HelpMessage::Default);
}
if matches.get_flag(CLAP_VERSION_FLAG) {
return MidenSubcommand::Version;
}
match matches.subcommand() {
Some((CLAP_HELP_SUBCMD, sub_matches)) => {
match sub_matches.get_one::<String>(CLAP_HELP_COMPONENT_ARG).map(String::as_str) {
None => MidenSubcommand::Help(HelpMessage::Default),
Some("toolchain") => MidenSubcommand::Help(HelpMessage::Toolchain),
Some(other) => MidenSubcommand::Help(HelpMessage::Resolve {
command: other,
matches: sub_matches,
}),
}
},
Some((comp_or_alias, matches)) => {
MidenSubcommand::Resolve { command: comp_or_alias, matches }
},
None => MidenSubcommand::Help(HelpMessage::Default),
}
}
pub fn miden_wrapper(
argv: &[OsString],
config: &Config,
state: &mut LocalState,
) -> anyhow::Result<ExitCode> {
let (toolchain_override, argv) = match argv {
[miden, first, rest @ ..]
if miden.eq_ignore_ascii_case("miden")
&& let Some(channel) = first.to_str().and_then(|s| s.strip_prefix("+")) =>
{
let mut argv = Vec::with_capacity(1 + rest.len());
argv.push(miden.clone());
argv.extend_from_slice(rest);
(Some(channel), Cow::Owned(argv))
},
argv => (None, Cow::Borrowed(argv)),
};
let matches = build_miden_command().get_matches_from(argv.as_ref());
let parsed_subcommand = parse_matches(&matches);
match parsed_subcommand {
MidenSubcommand::Help(HelpMessage::Default) => {
crate::report::prepare_stdout_color();
println!("{}", default_help());
return Ok(ExitCode::SUCCESS);
},
MidenSubcommand::Version => {
println!("{}", display_version(config));
return Ok(ExitCode::SUCCESS);
},
_ => (),
};
let (toolchain, _justification, partial_channel) =
Toolchain::ensure_current_is_installed(config, state, toolchain_override)?;
config.update_opt_symlinks_for(&toolchain)?;
let installed_channel = {
let active = config
.local_channel(&toolchain.channel)
.with_context(|| format!("channel '{}' is unavailable", toolchain.channel))?;
state
.get(&active)
.map(|installation| installation.as_channel())
.with_context(|| format!("channel '{active}' is not installed"))?
};
let toolchain_environment = ToolchainEnvironment::new(&installed_channel, partial_channel);
let requested_help = match parsed_subcommand {
MidenSubcommand::Help(HelpMessage::Default) => unreachable!(),
MidenSubcommand::Help(HelpMessage::Toolchain) => {
crate::report::prepare_stdout_color();
let help = toolchain_help(&toolchain_environment);
println!("{help}");
return Ok(ExitCode::SUCCESS);
},
MidenSubcommand::Help(HelpMessage::Resolve { .. }) => true,
_ => false,
};
let (target_exe, args, active_channel) = match parsed_subcommand {
MidenSubcommand::Version
| MidenSubcommand::Help(HelpMessage::Default)
| MidenSubcommand::Help(HelpMessage::Toolchain) => unreachable!(),
MidenSubcommand::Help(HelpMessage::Resolve {
command: resolve,
matches: subcommand_matches,
})
| MidenSubcommand::Resolve {
command: resolve,
matches: subcommand_matches,
} => {
match toolchain_environment.resolve(resolve, subcommand_matches) {
Ok(environment) => {
let active_channel = environment.active_channel;
let resolver = resolver_for(config, active_channel, &toolchain.channel);
let user_args = |matches: &clap::ArgMatches| -> Vec<OsString> {
let mut args: Vec<OsString> =
requested_help.then(|| OsString::from("--help")).into_iter().collect();
if let Some(extra) = matches.get_many::<OsString>("") {
args.extend(extra.cloned());
}
args
};
let argv = match environment.argument {
MidenArgument::Subcommand { component, format, executable, rest } => {
let mut args: Vec<OsString> = requested_help
.then(|| OsString::from("--help"))
.into_iter()
.collect();
args.extend(rest);
exec::compose(component, format, Some(executable), args, &resolver)?
},
MidenArgument::Command { component, executable, matches }
| MidenArgument::Alias { component, executable, matches } => exec::compose(
component,
executable,
None,
user_args(matches),
&resolver,
)?,
MidenArgument::Component { component, spec, matches } => {
let format = spec
.call_format
.clone()
.unwrap_or_else(Executable::default_call_format);
exec::compose(component, &format, None, user_args(matches), &resolver)?
},
};
let mut argv = VecDeque::from(argv);
let arg0 = argv.pop_front().expect("composition never yields an empty argv");
(arg0, Vec::from(argv), active_channel)
},
Err(EnvironmentError::MissingSubcommand { command, available })
if requested_help =>
{
crate::report::prepare_stdout_color();
println!("{}", format!("Subcommands of `miden {command}`:").bold());
for subcommand in available {
println!(" {subcommand}");
}
return Ok(ExitCode::SUCCESS);
},
Err(err) => {
crate::report::prepare_stderr_color();
let help_message = toolchain_help(&toolchain_environment);
let err_msg = format!(
"{}
{}",
err, help_message
);
bail!(err_msg);
},
}
},
};
let status = config.execute_command(active_channel, &target_exe, &args).with_context(|| {
let user_input = argv.iter().map(|s| s.to_string_lossy()).collect::<Vec<_>>().join(" ");
format!("failed to run '{user_input}'")
})?;
Ok(exit_code_from_status(status))
}
fn exit_code_from_status(status: std::process::ExitStatus) -> ExitCode {
if let Some(code) = status.code() {
return exit_code_from_i32(code);
}
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(signal) = status.signal() {
return exit_code_from_i32(128 + signal);
}
}
ExitCode::FAILURE
}
fn exit_code_from_i32(code: i32) -> ExitCode {
ExitCode::from(code.try_into().unwrap_or(1))
}
pub fn display_version(config: &Config) -> String {
let compiled_cargo_version = include_str!(concat!(env!("OUT_DIR"), "/cargo_version.in"));
let git_revision = include_str!(concat!(env!("OUT_DIR"), "/git_revision.in"));
let midenup_version = env!(
"CARGO_PKG_VERSION",
"CARGO_PKG_VERSION environment variable not set. This should be set by cargo by default; \
however, if not, it can be manually set using the `version` field in the Cargo.toml file"
);
let cargo_version = {
std::process::Command::new("cargo")
.arg("--version")
.output()
.map_err(|err| anyhow::anyhow!("failed to run 'cargo --version' because of {err}"))
.and_then(|output| {
String::from_utf8(output.stdout).map_err(|err| {
anyhow::anyhow!("failed to parse cargo version because of: {err}")
})
})
.inspect_err(|e| {
crate::warn!("failed to obtain the cargo version ({e}); leaving it as unknown")
})
.unwrap_or("unknown".to_string())
};
let cargo_version = cargo_version.trim();
let toolchain_version = Toolchain::current(config, None)
.inspect_err(|err| {
crate::warn!("failed to obtain the current toolchain ({err}); leaving it as unknown")
})
.map_or("unknown".to_string(), |(toolchain, _)| {
config
.local_channel(&toolchain.channel)
.map(|channel| channel.to_string())
.unwrap_or_else(|| "not installed".to_string())
});
let github_issue = {
let short_body = format!(
"<!--- (leave this at the bottom) --> midenup:{midenup_version}, toolchain: \
{toolchain_version}, cargo:{cargo_version}, rev:{git_revision}"
);
format!(
"https://github.com/0xMiden/midenup/issues/new?title=bug:<YOUR_ISSUE>&body={short_body}"
)
};
format!(
"
The Miden toolchain porcelain:
Environment:
- cargo version: {cargo_version}.
Midenup:
- midenup + miden version: {midenup_version}.
- active toolchain version: {toolchain_version}.
- midenup revision: {git_revision}.
- midenup was compiled with {compiled_cargo_version}.
Found a bug? Create an issue by copying this into your browser:
{github_issue}
"
)
}
fn toolchain_help(toolchain_environment: &ToolchainEnvironment) -> String {
let usage = "Usage:".bold().underline();
let miden = "miden".bold();
let asterisk = "*".bold();
let available_aliases_text = "Available aliases:".bold().underline();
let available_aliases: String = toolchain_environment.get_aliases_display();
let available_components_text = "Available components:".bold().underline();
let available_components: String = toolchain_environment.get_executables_display();
let available_libraries_text = "Available libraries:".bold().underline();
let available_libraries: String = toolchain_environment.get_libraries_display();
let help = "Help:".bold().underline();
format!(
"The Miden toolchain porcelain
{usage} {miden} <ALIAS|COMPONENT>
{available_aliases_text}
{available_aliases}
{available_components_text}
{available_components}
{available_libraries_text}
{available_libraries}
{help}
help Print this help message
help toolchain Print this help message {asterisk}
help <COMPONENT> Print <COMPONENTS>'s help message {asterisk}
{asterisk}: These commands will install the currently present toolchain if not installed.
",
)
}
fn default_help() -> String {
let asterisk = "*".bold();
let help = "Help:".bold().underline();
format!(
"The Miden toolchain porcelain
{help}
help Print this help message
help toolchain Print help about the currently available aliases and components {asterisk}
help <COMPONENT> Print a specific <COMPONENTS>'s help message {asterisk}
{asterisk}: These commands will install the currently present toolchain if not installed.
",
)
}
fn resolver_for(config: &Config, channel: &Channel, selector: &UserChannel) -> Resolver {
Resolver::new(
crate::paths::toolchain_link(&config.midenup_home, &channel.name),
&config.midenup_home,
selector,
)
}
fn resolve_argument<'a>(
channel: &'a Channel,
argument: &'a str,
matches: &'a clap::ArgMatches,
) -> Result<MidenArgument<'a>, EnvironmentError> {
for comp in channel.components.iter() {
match comp.kind() {
ComponentKind::Unsupported { .. } => continue,
ComponentKind::Command {
command_name: name,
format,
aliases,
subcommands,
} => {
let name = name.as_deref().unwrap_or(comp.name.as_ref());
if name == argument {
if subcommands.is_empty() {
return Ok(MidenArgument::Command {
component: comp,
executable: format,
matches,
});
}
let mut user = matches.get_many::<OsString>("").into_iter().flatten();
let Some(requested) = user.next() else {
return Err(EnvironmentError::MissingSubcommand {
command: name.to_string(),
available: subcommands.keys().cloned().collect(),
});
};
let requested = requested.to_string_lossy().into_owned();
return match subcommands.get(&requested) {
Some(exe) => Ok(MidenArgument::Subcommand {
component: comp,
format,
executable: exe,
rest: user.cloned().collect(),
}),
None => Err(EnvironmentError::InvalidSubcommand {
command: name.to_string(),
subcommand: requested,
available: subcommands.keys().cloned().collect(),
}),
};
} else if let Some(aliased) = aliases.get(argument) {
return Ok(MidenArgument::Alias {
component: comp,
executable: aliased,
matches,
});
}
},
ComponentKind::Executable { spec, .. } | ComponentKind::CargoExtension { spec, .. }
if spec.hide =>
{
if let Some(aliased) = spec.aliases.get(argument) {
return Ok(MidenArgument::Alias {
component: comp,
executable: aliased,
matches,
});
}
},
ComponentKind::Executable { spec, .. } | ComponentKind::CargoExtension { spec, .. } => {
if comp.name.as_ref() == argument {
return Ok(MidenArgument::Component { component: comp, spec, matches });
} else if let Some(aliased) = spec.aliases.get(argument) {
return Ok(MidenArgument::Alias {
component: comp,
executable: aliased,
matches,
});
}
},
ComponentKind::Package | ComponentKind::LegacyPackage { .. } | ComponentKind::Asset => {
},
}
}
if let Some(comp) = channel.get_component(argument) {
match comp.kind() {
ComponentKind::Command { command_name: Some(command_name), .. } => {
return Err(EnvironmentError::Renamed {
component: comp.name.to_string(),
command_name: command_name.clone(),
});
},
ComponentKind::Command { .. } => {
return Err(EnvironmentError::InvalidCommand { command: argument.to_string() });
},
ComponentKind::Unsupported { .. } => {
return Err(EnvironmentError::NotExecutable { component: comp.name.to_string() });
},
ComponentKind::Executable { spec, .. } | ComponentKind::CargoExtension { spec, .. } => {
return Err(EnvironmentError::Hidden {
component: comp.name.to_string(),
available: spec.aliases.keys().cloned().collect(),
});
},
ComponentKind::Package | ComponentKind::LegacyPackage { .. } | ComponentKind::Asset => {
return Err(EnvironmentError::NotExecutable { component: comp.name.to_string() });
},
}
}
Err(EnvironmentError::InvalidCommand { command: argument.to_string() })
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::*;
use crate::{profile::Profile, version::Authority};
fn command_component(name: &'static str, command_name: Option<&str>) -> Component {
Component {
name: Cow::Borrowed(name),
version: Authority::Registry { version: semver::Version::new(0, 1, 0) },
kind: ComponentKind::Command {
command_name: command_name.map(str::to_string),
format: Default::default(),
aliases: Default::default(),
subcommands: Default::default(),
},
profiles: vec![Profile::Minimal],
requires: vec![],
artifacts: Default::default(),
extra: Default::default(),
}
}
#[test]
fn component_name_of_a_renamed_command_reports_the_command_name() {
let channel = Channel::new(
semver::Version::new(0, 15, 0),
vec![command_component("foo", Some("bar"))],
);
let matches = build_miden_command().get_matches_from(["miden"]);
let Err(err) = resolve_argument(&channel, "foo", &matches) else {
panic!("naming the component rather than the command must not resolve");
};
assert!(matches!(err, EnvironmentError::Renamed { .. }), "expected Renamed, got {err:?}");
assert!(err.to_string().contains("miden bar"), "error should name the command: {err}");
}
#[test]
fn a_command_without_a_rename_still_resolves_by_component_name() {
let channel =
Channel::new(semver::Version::new(0, 15, 0), vec![command_component("foo", None)]);
let matches = build_miden_command().get_matches_from(["miden"]);
assert!(resolve_argument(&channel, "foo", &matches).is_ok());
}
}