use crate::commands::install::{internal_install, InstallArgs};
use crate::error::ProtoCliError;
use clap::Args;
use miette::IntoDiagnostic;
use proto_core::{detect_version, load_tool, Id, ProtoError, Tool, UnresolvedVersionSpec};
use proto_pdk_api::{ExecutableConfig, RunHook};
use starbase::system;
use std::env;
use std::ffi::OsStr;
use std::process::exit;
use system_env::create_process_command;
use tokio::process::Command;
use tracing::debug;
#[derive(Args, Clone, Debug)]
pub struct RunArgs {
#[arg(required = true, help = "ID of tool")]
id: Id,
#[arg(help = "Version or alias of tool")]
spec: Option<UnresolvedVersionSpec>,
#[arg(long, help = "Name of an alternate (secondary) binary to run")]
alt: Option<String>,
#[arg(long, help = "Path to a tool directory relative file to run")]
bin: Option<String>,
#[arg(
last = true,
help = "Arguments to pass through to the underlying command"
)]
passthrough: Vec<String>,
}
fn is_trying_to_self_upgrade(tool: &Tool, args: &[String]) -> bool {
if tool.metadata.self_upgrade_commands.is_empty() {
return false;
}
for arg in args {
if arg.starts_with('-') {
continue;
}
return tool.metadata.self_upgrade_commands.contains(arg);
}
false
}
fn get_executable(tool: &Tool, args: &RunArgs) -> miette::Result<ExecutableConfig> {
let tool_dir = tool.get_tool_dir();
if let Some(alt_bin) = &args.bin {
let alt_path = tool_dir.join(alt_bin);
debug!(bin = alt_bin, path = ?alt_path, "Received a relative binary to run with");
if alt_path.exists() {
return Ok(ExecutableConfig {
exe_path: Some(alt_path),
..ExecutableConfig::default()
});
} else {
return Err(ProtoCliError::MissingRunAltBin {
bin: alt_bin.to_owned(),
path: alt_path,
}
.into());
}
}
if let Some(alt_name) = &args.alt {
for location in tool.get_shim_locations()? {
if location.name == *alt_name {
let Some(exe_path) = &location.config.exe_path else {
continue;
};
let alt_path = tool_dir.join(exe_path);
if alt_path.exists() {
debug!(
bin = alt_name,
path = ?alt_path,
"Received an alternate binary to run with",
);
return Ok(ExecutableConfig {
exe_path: Some(alt_path),
..location.config
});
}
}
}
return Err(ProtoCliError::MissingRunAltBin {
bin: alt_name.to_owned(),
path: tool_dir,
}
.into());
}
let mut config = tool
.get_exe_location()?
.expect("Required executable information missing!")
.config;
config.exe_path = Some(tool_dir.join(config.exe_path.as_ref().unwrap()));
Ok(config)
}
fn create_command<I: IntoIterator<Item = A>, A: AsRef<OsStr>>(
exe_config: &ExecutableConfig,
args: I,
) -> Command {
let exe_path = exe_config.exe_path.as_ref().unwrap();
let command = if let Some(parent_exe) = &exe_config.parent_exe_name {
let mut exe_args = vec![exe_path.as_os_str().to_os_string()];
exe_args.extend(args.into_iter().map(|arg| arg.as_ref().to_os_string()));
create_process_command(parent_exe, exe_args)
} else {
create_process_command(exe_path, args)
};
Command::from(command)
}
#[system]
pub async fn run(args: ArgsRef<RunArgs>) -> SystemResult {
let mut tool = load_tool(&args.id).await?;
if is_trying_to_self_upgrade(&tool, &args.passthrough) {
return Err(ProtoCliError::NoSelfUpgrade {
command: format!("proto install {} --pin", tool.id),
tool: tool.get_name().to_owned(),
}
.into());
}
let version = detect_version(&tool, args.spec.clone()).await?;
let user_config = tool.proto.load_user_config()?;
if !tool.is_setup(&version).await? {
if !user_config.auto_install {
return Err(ProtoError::MissingToolForRun {
tool: tool.get_name().to_owned(),
version: version.to_string(),
command: format!("proto install {} {}", tool.id, tool.get_resolved_version()),
}
.into());
}
debug!("Auto-install setting is configured, attempting to install");
tool = internal_install(
InstallArgs {
canary: false,
id: args.id.clone(),
pin: false,
passthrough: vec![],
spec: Some(tool.get_resolved_version().to_unresolved_spec()),
},
Some(tool),
)
.await?;
}
let exe_config = get_executable(&tool, args)?;
let exe_path = exe_config.exe_path.as_ref().unwrap();
debug!(bin = ?exe_path, args = ?args.passthrough, "Running {}", tool.get_name());
tool.run_hook("pre_run", || RunHook {
context: tool.create_context(),
passthrough_args: args.passthrough.clone(),
})?;
let status = create_command(&exe_config, &args.passthrough)
.env(
format!("{}_VERSION", tool.get_env_var_prefix()),
tool.get_resolved_version().to_string(),
)
.env(
format!("{}_BIN", tool.get_env_var_prefix()),
exe_path.to_string_lossy().to_string(),
)
.spawn()
.into_diagnostic()?
.wait()
.await
.into_diagnostic()?;
if status.success() {
tool.run_hook("post_run", || RunHook {
context: tool.create_context(),
passthrough_args: args.passthrough.clone(),
})?;
}
if env::var("PROTO_SKIP_USED_AT").is_err() {
tokio::spawn(async move {
tool.manifest.track_used_at(tool.get_resolved_version());
let _ = tool.manifest.save();
});
}
if !status.success() {
exit(status.code().unwrap_or(1));
}
}