use std::{ffi::OsString, fmt::Display, string::ToString};
use anyhow::{Context, anyhow, bail};
use colored::Colorize;
pub use crate::config::Config;
use crate::{
channel::{Channel, CliCommands, Component, InstalledFile, resolve_command},
manifest::Manifest,
toolchain::Toolchain,
};
enum HelpMessage {
Default,
Toolchain,
Resolve(String),
}
#[derive(Debug)]
enum MidenArgument {
Alias(Component, CliCommands),
Component(Component),
}
struct ExecutionEnvironment<'a> {
argument: MidenArgument,
active_channel: &'a Channel,
}
enum EnvironmentError {
UnknownArgument(String),
LibraryAsExecutable(String),
AliasOnly(String),
}
impl Display for EnvironmentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self {
EnvironmentError::UnknownArgument(err) => write!(f, "{err}"),
EnvironmentError::LibraryAsExecutable(err) => write!(f, "{err}"),
EnvironmentError::AliasOnly(err) => write!(f, "{err}"),
}
}
}
#[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(&self, argument: String) -> Result<ExecutionEnvironment<'_>, EnvironmentError> {
let fallback_motive = if let Some(active_channel) = self.active_channel.as_ref() {
match resolve_argument(active_channel, &argument) {
Ok(arg) => return Ok(ExecutionEnvironment { argument: arg, active_channel }),
Err(EnvironmentError::UnknownArgument(_)) => {
FallbackMotive::ArgumentNotInActiveChannel
},
Err(e) => return Err(e),
}
} else {
FallbackMotive::NoActiveChannel
};
{
let miden_argument = resolve_argument(self.installed_channel, &argument)?;
let not_found_in_active =
matches!(fallback_motive, FallbackMotive::ArgumentNotInActiveChannel);
let warning_message = match (&miden_argument, not_found_in_active) {
(MidenArgument::Alias(comp, _), true) => 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), true) => Some(format!(
"{}: {} is installed, but it is not part of the current active toolchain.",
"WARNING".yellow().bold(),
comp.name,
)),
_ => None,
};
if let Some(warning) = warning_message {
println!("{warning}")
};
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| {
matches!(
c.get_installed_file(),
InstalledFile::Executable { binary_name: _, alias_only: false }
)
})
.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.get_installed_file() {
InstalledFile::Library { library_name, .. } => {
let display_name = format!(" {}\n", library_name);
Some(display_name)
},
_ => None,
})
.collect::<String>()
}
fn get_aliases_display(&self) -> String {
let aliases = self.get_active_channel().0.get_aliases();
let mut keys: Vec<_> = aliases.keys().collect();
keys.sort();
keys.iter().map(|alias| format!(" {}\n", alias.bold())).collect::<String>()
}
}
enum MidenSubcommand {
Help(HelpMessage),
Version,
Resolve(String),
}
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(other.to_string())),
}
},
Some((comp_or_alias, _)) => MidenSubcommand::Resolve(comp_or_alias.to_string()),
None => MidenSubcommand::Help(HelpMessage::Default),
}
}
pub fn miden_wrapper(
argv: &[OsString],
config: &Config,
local_manifest: &mut Manifest,
) -> anyhow::Result<()> {
let matches = build_miden_command().get_matches_from(argv);
let parsed_subcommand = parse_matches(&matches);
let user_input = argv.iter().map(|s| s.to_string_lossy()).collect::<Vec<_>>().join(" ");
match parsed_subcommand {
MidenSubcommand::Help(HelpMessage::Default) => {
println!("{}", default_help());
return Ok(());
},
MidenSubcommand::Version => {
println!("{}", display_version(config));
return Ok(());
},
_ => (),
}
let (toolchain, _justification, partial_channel) =
Toolchain::ensure_current_is_installed(config, local_manifest)?;
let toolchain_environment = {
let installed_channel = local_manifest
.get_channel(&toolchain.channel)
.context("Couldn't find active toolchain in the manifest.")?;
ToolchainEnvironment::new(installed_channel, partial_channel)
};
let requested_help = match parsed_subcommand {
MidenSubcommand::Help(HelpMessage::Default) => unreachable!(),
MidenSubcommand::Help(HelpMessage::Toolchain) => {
let help = toolchain_help(&toolchain_environment);
println!("{help}");
return Ok(());
},
MidenSubcommand::Help(HelpMessage::Resolve(_)) => true,
_ => false,
};
let (target_exe, prefix_args, active_channel) = match parsed_subcommand {
MidenSubcommand::Version
| MidenSubcommand::Help(HelpMessage::Default)
| MidenSubcommand::Help(HelpMessage::Toolchain) => unreachable!(),
MidenSubcommand::Help(HelpMessage::Resolve(resolve))
| MidenSubcommand::Resolve(resolve) => {
match toolchain_environment.resolve(resolve.clone()) {
Ok(ExecutionEnvironment {
argument: MidenArgument::Alias(component, alias_resolutions),
active_channel,
}) => {
let commands =
resolve_command(&alias_resolutions, active_channel, &component, config)?;
let mut commands = std::collections::VecDeque::from(commands);
let command = commands.pop_front().unwrap();
let aliased_arguments = commands;
(command, aliased_arguments, active_channel)
},
Ok(ExecutionEnvironment {
argument: MidenArgument::Component(component),
active_channel,
}) => {
let mut call_convention = std::collections::VecDeque::from(resolve_command(
&component.get_call_format(),
active_channel,
&component,
config,
)?);
let command = call_convention.pop_front().unwrap();
let args = call_convention;
(command, args, active_channel)
},
Err(err) => {
let help_message = toolchain_help(&toolchain_environment);
let err_msg = format!(
"{}
{}",
err, help_message
);
bail!(err_msg);
},
}
},
};
let remaining_args = if requested_help {
vec![std::ffi::OsStr::new("--help").to_os_string()]
} else {
matches
.subcommand()
.and_then(|(_, sub_matches)| sub_matches.get_many::<OsString>(""))
.map(|vals| vals.map(OsString::clone).collect())
.unwrap_or_default()
};
let args = prefix_args.into_iter().chain(remaining_args).collect::<Vec<_>>();
let mut command = config
.execute_command(active_channel, &target_exe, &args)
.with_context(|| format!("failed to run '{user_input}'"))?;
let status = command.wait().with_context(|| {
format!("error occurred while waiting for '{user_input}' to finish executing")
})?;
if status.success() {
Ok(())
} else {
bail!("'{}' failed with status {}", user_input, status.code().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| {
println!("Failed to obtain cargo version:");
println!("{}", e);
println!("Leaving as unknown")
})
.unwrap_or("unknown".to_string())
};
let cargo_version = cargo_version.trim();
let toolchain_version = Toolchain::current(config)
.and_then(|(toolchain, _)| {
config
.manifest
.get_channel(&toolchain.channel)
.map(|channel| channel.name.to_string())
.ok_or(anyhow!("channel: {} doesn't exist or isn't available ", toolchain.channel))
})
.inspect_err(|err| {
println!(
"failed to obtain current toolchain error because of: {err}, leaving as unknown"
)
})
.unwrap_or("unknown".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 resolve_argument(channel: &Channel, argument: &str) -> Result<MidenArgument, EnvironmentError> {
let mut resolution = Err(EnvironmentError::UnknownArgument(format!(
"Failed to resolve '{}': Neither known alias or component.",
argument
)));
for comp in channel.components.iter() {
if let Some(associated_command) = comp.aliases.get(argument) {
return Ok(MidenArgument::Alias(comp.clone(), associated_command.to_owned()));
} else if comp.name == argument {
match comp.get_installed_file() {
InstalledFile::Executable { alias_only: false, binary_name: _ } => {
resolution = Ok(MidenArgument::Component(comp.clone()));
break;
},
InstalledFile::Executable { alias_only: true, binary_name: _ } => {
let aliases = comp
.aliases
.keys()
.map(|alias| format!("'{}'", alias))
.collect::<Vec<_>>()
.join(", ");
resolution = Err(EnvironmentError::AliasOnly(format!(
"'{}' is not intended to be called via 'miden', but rather by its \
aliases: {aliases}",
comp.name
)));
break;
},
InstalledFile::Library { library_name, library_struct: _ } => {
return Err(EnvironmentError::LibraryAsExecutable(format!(
"'{}' installs the {} library. It is not intended to be executed as a \
binary.",
comp.name, library_name
)));
},
}
}
}
resolution
}
enum FallbackMotive {
NoActiveChannel,
ArgumentNotInActiveChannel,
}