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 {
UnkownArgument(String),
LibraryAsExecutable(String),
AliasOnly(String),
}
impl Display for EnvironmentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self {
EnvironmentError::UnkownArgument(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> {
[
(self.active_channel.as_ref() , ChannelType::Active),
(Some(self.installed_channel), ChannelType::Installed),
]
.into_iter()
.filter_map(|(ch, ch_type)| ch.map(|ch| (ch, ch_type)))
.flat_map(|(ch, ch_type)| ch.components.iter().map(move |comp| (comp, ch_type, ch)))
.find_map(|(comp, ch_type, ch)| {
if let Some(associated_command) = comp.aliases.get(&argument) {
Some(Ok((ch, (MidenArgument::Alias(comp.clone(), associated_command.to_owned()), ch_type))))
} else if comp.name == argument {
match comp.get_installed_file() {
InstalledFile::Executable { alias_only: false, binary_name: _ } => {
Some(Ok((ch, (MidenArgument::Component(comp.clone()), ch_type))))
},
InstalledFile::Executable { alias_only: true, binary_name: _ } => {
let aliases = comp.aliases.keys().map(|alias| format!("'{}'", alias)).collect::<Vec<_>>().join(", ");
Some(Err(EnvironmentError::AliasOnly(format!("'{}' is not intended to be called via 'miden', but rather by its aliases: {aliases}", comp.name))))
},
InstalledFile::Library { library_name, library_struct: _ } => {
Some(Err(EnvironmentError::LibraryAsExecutable(
format!("'{}' installs the {} library. It is not intended to be executed as a binary.", comp.name, library_name))))
},
}
} else {
None
}
})
.inspect(|resolution| {
if let Some(warning_message) = match resolution {
Ok((_, (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,
)),
Ok((_, (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}")
}
}
)
.unwrap_or(Err(EnvironmentError::UnkownArgument(format!("Failed to resolve '{}': Neither known alias or component.", argument))))
.map(|(channel, (argument, _))| Ok(ExecutionEnvironment { argument, active_channel: 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),
}
fn parse_subcommand(subcommand: &str, argv: &[OsString]) -> MidenSubcommand {
if subcommand == "help" {
match argv.get(2).and_then(|c| c.to_str()) {
None => MidenSubcommand::Help(HelpMessage::Default),
Some("toolchain") => MidenSubcommand::Help(HelpMessage::Toolchain),
Some(other) => MidenSubcommand::Help(HelpMessage::Resolve(other.to_string())),
}
} else if subcommand == "--version" {
MidenSubcommand::Version
} else {
MidenSubcommand::Resolve(subcommand.to_string())
}
}
pub fn miden_wrapper(
argv: Vec<OsString>,
config: &Config,
local_manifest: &mut Manifest,
) -> anyhow::Result<()> {
let subcommand = {
let subcommand = argv.get(1).with_context(|| {
format!(
"
{}: '{}' requires a subcommand but one was not provided
{} {} <ALIAS|COMMAND>
For more information, try 'miden help'.
",
"error:".red().bold(),
"miden".yellow().bold(),
"Usage".bold().underline(),
"miden".bold(),
)
})?;
subcommand.to_str().expect("Invalid command name: {subcommand}")
};
let parsed_subcommand = parse_subcommand(subcommand, &argv);
match parsed_subcommand {
MidenSubcommand::Help(HelpMessage::Default) => {
println!("{}", default_help());
return Ok(());
},
MidenSubcommand::Version => {
println!("{}", display_version(config));
return Ok(());
},
_ => (),
}
let (toolchain, _, 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 help_flag = 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(_)) => {
Some(String::from("--help"))
},
_ => None,
};
let (target_exe, mut prefix_args, active_channel) = match parsed_subcommand {
MidenSubcommand::Version => unreachable!(),
MidenSubcommand::Help(HelpMessage::Default) => unreachable!(),
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 command = commands.first().unwrap().clone();
let aliased_arguments: Vec<String> = commands.into_iter().skip(1).collect();
(command, aliased_arguments, active_channel)
},
Ok(ExecutionEnvironment {
argument: MidenArgument::Component(component),
active_channel,
}) => {
let call_convention = resolve_command(
&component.get_call_format(),
active_channel,
&component,
config,
)?;
let command = call_convention.first().unwrap().clone();
let args: Vec<String> = call_convention.into_iter().skip(1).collect();
(command, args, active_channel)
},
Err(err) => {
let help_message = toolchain_help(&toolchain_environment);
let err_msg = format!(
"{}
{}",
err, help_message
);
bail!(err_msg);
},
}
},
};
let rest_of_args = if let Some(help_flag) = help_flag {
prefix_args.extend([help_flag]);
argv.iter().skip(argv.len())
} else {
argv.iter().skip(2)
};
let prefix_args = prefix_args.iter().map(OsString::from).chain(rest_of_args.cloned()).collect();
let mut command = config
.execute_command(active_channel, &target_exe, &prefix_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")
})?;
if status.success() {
Ok(())
} else {
bail!("'miden {}' failed with status {}", subcommand, 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.
",
)
}