diff --git a/Cargo.lock b/Cargo.lock index 08e60182bf..99d1ad7838 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,9 +145,11 @@ dependencies = [ "anyhow", "c2rust-build-paths", "c2rust-transpile", + "camino", "clap 2.34.0", "env_logger", "git-testament", + "is_executable", "log", "regex", "shlex", @@ -759,6 +761,15 @@ dependencies = [ "similar", ] +[[package]] +name = "is_executable" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8" +dependencies = [ + "winapi", +] + [[package]] name = "instant" version = "0.1.12" diff --git a/c2rust/Cargo.toml b/c2rust/Cargo.toml index 16303e3f51..33f89d82a9 100644 --- a/c2rust/Cargo.toml +++ b/c2rust/Cargo.toml @@ -18,10 +18,12 @@ azure-devops = { project = "immunant/c2rust", pipeline = "immunant.c2rust", buil [dependencies] anyhow = "1.0" -clap = {version = "2.34", features = ["yaml"]} -log = "0.4" +camino = "1.0" +clap = { version = "2.34", features = ["yaml"] } env_logger = "0.9" git-testament = "0.2.1" +is_executable = "1.0" +log = "0.4" regex = "1.3" shlex = "1.1" c2rust-transpile = { version = "0.16.0", path = "../c2rust-transpile" } diff --git a/c2rust/src/main.rs b/c2rust/src/main.rs index a58220c931..9a447cc843 100644 --- a/c2rust/src/main.rs +++ b/c2rust/src/main.rs @@ -1,56 +1,147 @@ -use clap::{crate_authors, load_yaml, App, AppSettings, SubCommand}; +use anyhow::anyhow; +use camino::Utf8Path; +use clap::{crate_authors, App, AppSettings, Arg}; use git_testament::{git_testament, render_testament}; +use is_executable::IsExecutable; +use std::borrow::Cow; +use std::collections::HashMap; use std::env; use std::ffi::OsStr; -use std::process::{exit, Command}; +use std::path::{Path, PathBuf}; +use std::process; +use std::process::Command; +use std::str; git_testament!(TESTAMENT); -fn main() { - let subcommand_yamls = [load_yaml!("transpile.yaml")]; - let matches = App::new("C2Rust") - .version(&*render_testament!(TESTAMENT)) - .author(crate_authors!(", ")) - .setting(AppSettings::SubcommandRequiredElseHelp) - .subcommands( - subcommand_yamls - .iter() - .map(|yaml| SubCommand::from_yaml(yaml)), - ) - .get_matches(); +/// A `c2rust` sub-command. +struct SubCommand { + /// The path to the [`SubCommand`]'s executable, + /// if it was found (see [`Self::find_all`]). + /// Otherwise [`None`] if it is a known [`SubCommand`] (see [`Self::known`]). + path: Option, + /// The name of the [`SubCommand`], i.e. in `c2rust-{name}`. + name: Cow<'static, str>, +} - let mut os_args = env::args_os(); - os_args.next(); // Skip executable name - let arg_name = os_args.next().and_then(|name| name.into_string().ok()); - match (&arg_name, matches.subcommand_name()) { - (Some(arg_name), Some(subcommand)) if arg_name == subcommand => { - invoke_subcommand(subcommand, os_args); - } - _ => { - eprintln!("{:?}", arg_name); - panic!("Could not match subcommand"); +impl SubCommand { + /// Find all [`SubCommand`]s adjacent to the current (`c2rust`) executable. + /// They are of the form `c2rust-{name}`. + pub fn find_all() -> anyhow::Result> { + let c2rust = env::current_exe()?; + let c2rust_name: &Utf8Path = c2rust + .file_name() + .map(Path::new) + .ok_or_else(|| anyhow!("no file name: {}", c2rust.display()))? + .try_into()?; + let c2rust_name = c2rust_name.as_str(); + let dir = c2rust + .parent() + .ok_or_else(|| anyhow!("no directory: {}", c2rust.display()))?; + let mut sub_commands = Vec::new(); + for entry in dir.read_dir()? { + let entry = entry?; + let file_type = entry.file_type()?; + let path = entry.path(); + let name = path + .file_name() + .and_then(|name| name.to_str()) + .and_then(|name| name.strip_prefix(c2rust_name)) + .and_then(|name| name.strip_prefix('-')) + .map(|name| name.to_owned()) + .map(Cow::from) + .filter(|_| file_type.is_file() || file_type.is_symlink()) + .filter(|_| path.is_executable()); + if let Some(name) = name { + sub_commands.push(Self { + path: Some(path), + name, + }); + } } - }; + Ok(sub_commands) + } + + /// Get all known [`SubCommand`]s. These have no [`SubCommand::path`]. + /// Even if the subcommand executables aren't there, we can still suggest them. + pub fn known() -> impl Iterator { + ["transpile", "instrument", "pdg", "analyze"] + .into_iter() + .map(|name| Self { + path: None, + name: name.into(), + }) + } + + /// Get all known ([`Self::known`]) and actual, found ([`Self::find_all`]) subcommands, + /// putting the known ones first so that the found ones overwrite them and take precedence. + pub fn all() -> anyhow::Result> { + Ok(Self::known().chain(Self::find_all()?)) + } + + pub fn invoke(&self, args: I) -> anyhow::Result<()> + where + I: IntoIterator, + S: AsRef, + { + let path = self.path.as_ref().ok_or_else(|| { + anyhow!( + "known subcommand not found (probably not built): {}", + self.name + ) + })?; + let status = Command::new(&path).args(args).status()?; + process::exit(status.code().unwrap_or(1)); + } } -fn invoke_subcommand(subcommand: &str, args: I) -where - I: IntoIterator, - S: AsRef, -{ - // Assumes the subcommand executable is in the same directory as this driver - // program. - let cmd_path = std::env::current_exe().expect("Cannot get current executable path"); - let mut cmd_path = cmd_path.as_path().canonicalize().unwrap(); - cmd_path.pop(); // remove current executable - cmd_path.push(format!("c2rust-{}", subcommand)); - assert!(cmd_path.exists(), "{:?} is missing", cmd_path); - exit( - Command::new(cmd_path.into_os_string()) - .args(args) - .status() - .expect("SubCommand failed to start") - .code() - .unwrap_or(-1), - ); +fn main() -> anyhow::Result<()> { + let sub_commands = SubCommand::all()?.collect::>(); + let sub_commands = sub_commands + .iter() + .map(|cmd| (cmd.name.as_ref(), cmd)) + .collect::>(); + + // If the subcommand matches, don't use `clap` at all. + // + // I can't seem to get `clap` to pass through all arguments as is, + // like the ones with hyphens like `--metadata`, + // even though I've set [`Arg::allow_hyphen_values`]. + // This is faster anyways. + // I also tried a single "subcommand" argument with [`Arg::possible_values`], + // but that had the same problem passing through all arguments as well. + // + // Furthermore, doing it this way correctly forwards `--help` through to the subcommand + // instead of `clap` intercepting it and displaying the top-level `--help`. + let mut args = env::args_os(); + let sub_command = args.nth(1); + let sub_command = sub_command + .as_ref() + .and_then(|arg| arg.to_str()) + .and_then(|name| sub_commands.get(name)); + + if let Some(sub_command) = sub_command { + return sub_command.invoke(args); + } + + // If we didn't get a subcommand, then use `clap` for parsing and error/help messages. + let matches = App::new("C2Rust") + .version(&*render_testament!(TESTAMENT)) + .author(crate_authors!(", ")) + .settings(&[ + AppSettings::SubcommandRequiredElseHelp, + AppSettings::AllowExternalSubcommands, + ]) + .subcommands(sub_commands.keys().map(|name| { + clap::SubCommand::with_name(name).arg( + Arg::with_name("args") + .multiple(true) + .allow_hyphen_values(true), + ) + })) + .get_matches(); + let sub_command_name = matches + .subcommand_name() + .ok_or_else(|| anyhow!("no subcommand"))?; + sub_commands[sub_command_name].invoke(args) }