Skip to content

Commit

Permalink
Add support for forc CLI plugins (FuelLabs#1178)
Browse files Browse the repository at this point in the history
Adds support for treating external executables as `forc` plugins,
provided they are of the name `forc-*` and are available via `PATH`.

For example, this allows for using an installed `forc-foo <args>` plugin
via `forc foo <args>`.

This should enable us to cut down on some of `forc`'s bloat by moving
support for things like the block explorer, language server and running
a fuel node into dedicated plugins.
  • Loading branch information
mitchmindtree authored Apr 7, 2022
1 parent b0ab3f8 commit 4fcce7a
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 6 deletions.
29 changes: 23 additions & 6 deletions forc/src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
use anyhow::Result;
use clap::Parser;

mod commands;
use self::commands::{
addr2line, build, clean, completions, deploy, explorer, format, init, json_abi, lsp,
parse_bytecode, run, test, update,
};

use addr2line::Command as Addr2LineCommand;
use anyhow::{anyhow, Result};
pub use build::Command as BuildCommand;
use clap::Parser;
pub use clean::Command as CleanCommand;
pub use completions::Command as CompletionsCommand;
pub use deploy::Command as DeployCommand;
Expand All @@ -22,6 +19,9 @@ pub use run::Command as RunCommand;
use test::Command as TestCommand;
pub use update::Command as UpdateCommand;

mod commands;
mod plugin;

#[derive(Debug, Parser)]
#[clap(name = "forc", about = "Fuel Orchestrator", version)]
struct Opt {
Expand Down Expand Up @@ -49,11 +49,20 @@ enum Forc {
Update(UpdateCommand),
JsonAbi(JsonAbiCommand),
Lsp(LspCommand),
/// This is a catch-all for unknown subcommands and their arguments.
///
/// When we receive an unknown subcommand, we check for a plugin exe named
/// `forc-<unknown-subcommand>` and try to execute it:
///
/// ```ignore
/// forc-<unknown-subcommand> <args>
/// ```
#[clap(external_subcommand)]
Plugin(Vec<String>),
}

pub async fn run_cli() -> Result<()> {
let opt = Opt::parse();

match opt.command {
Forc::Addr2Line(command) => addr2line::exec(command),
Forc::Build(command) => build::exec(command),
Expand All @@ -69,5 +78,13 @@ pub async fn run_cli() -> Result<()> {
Forc::Update(command) => update::exec(command).await,
Forc::JsonAbi(command) => json_abi::exec(command),
Forc::Lsp(command) => lsp::exec(command).await,
Forc::Plugin(args) => {
let output = plugin::execute_external_subcommand(args)?;
let code = output
.status
.code()
.ok_or_else(|| anyhow!("plugin exit status unknown"))?;
std::process::exit(code);
}
}
}
62 changes: 62 additions & 0 deletions forc/src/cli/plugin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Items related to plugin support for `forc`.
use anyhow::{bail, Result};
use std::{
env, fs,
path::{Path, PathBuf},
process,
};

/// Attempt to execute the unknown subcommand as an external plugin.
///
/// The subcommand is assumed to be the first element, with the following elements representing
/// following arguments to the external subcommand.
///
/// E.g. given `foo bar baz` where `foo` is an unrecognized subcommand to `forc`, tries to execute
/// `forc-foo bar baz`.
pub(crate) fn execute_external_subcommand(args: Vec<String>) -> Result<process::Output> {
let cmd = args.get(0).expect("`args` must not be empty");
let args = &args[1..];
let path = find_external_subcommand(cmd);
let command = match path {
Some(command) => command,
None => bail!("no such subcommand: `{}`", cmd),
};
let output = process::Command::new(&command)
.stdin(process::Stdio::inherit())
.stdout(process::Stdio::inherit())
.stderr(process::Stdio::inherit())
.args(args)
.output()?;
Ok(output)
}

/// Find an exe called `forc-<cmd>` and return its path.
fn find_external_subcommand(cmd: &str) -> Option<PathBuf> {
let command_exe = format!("forc-{}{}", cmd, env::consts::EXE_SUFFIX);
search_directories()
.iter()
.map(|dir| dir.join(&command_exe))
.find(|file| is_executable(file))
}

/// Search the user's `PATH` for `forc-*` exes.
fn search_directories() -> Vec<PathBuf> {
if let Some(val) = env::var_os("PATH") {
return env::split_paths(&val).collect();
}
vec![]
}

#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
use std::os::unix::prelude::*;
fs::metadata(path)
.map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}

#[cfg(windows)]
fn is_executable(path: &Path) -> bool {
path.is_file()
}

0 comments on commit 4fcce7a

Please sign in to comment.