Skip to content

Commit

Permalink
Merge pull request #61 from prefix-dev/fix/run_multiple_commands
Browse files Browse the repository at this point in the history
feat: extend command usage
  • Loading branch information
ruben-arts authored Jun 8, 2023
2 parents 09b45fd + 804ea64 commit e2e15b7
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 40 deletions.
29 changes: 29 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ native-tls = ["reqwest/native-tls", "rattler_repodata_gateway/native-tls", "ratt
rustls-tls = ["reqwest/rustls-tls", "rattler_repodata_gateway/rustls-tls", "rattler/rustls-tls"]

[dependencies]

anyhow = "1.0.70"
clap = { version = "4.2.4", default-features = false, features = ["derive", "usage", "wrap_help", "std"] }
clap_complete = "4.2.1"
console = { version = "0.15.5", features = ["windows-console-colors"] }
dirs = "5.0.1"
dunce = "1.0.4"
enum-iterator = "1.4.1"
futures = "0.3.28"
indicatif = "0.17.3"
Expand All @@ -37,9 +37,11 @@ rattler_virtual_packages = { version = "0.2.0", default-features = false, git =
#rattler_virtual_packages = { version = "0.2.0", default-features = false, path="../rattler/crates/rattler_virtual_packages" }
reqwest = { version = "0.11.16", default-features = false }
serde = "1.0.163"
serde_with = "3.0.0"
shlex = "1.1.0"
tempfile = "3.5.0"
tokio = { version = "1.27.0", features = ["macros", "rt-multi-thread"] }
toml_edit = "0.19.8"
toml_edit = { version = "0.19.8", features = ["serde"] }

[profile.release]
lto = true
Expand Down
1 change: 0 additions & 1 deletion src/cli/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ channels = ["{{ channel }}"]
platforms = ["{{ platform }}"]
[commands]
custom_command = "echo hello_world"
[dependencies]
"#;
Expand Down
235 changes: 216 additions & 19 deletions src/cli/run.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
use std::{io::Write, path::PathBuf};
use anyhow::Context;
use std::collections::{HashSet, VecDeque};
use std::path::Path;
use std::{fmt::Write, path::PathBuf};

use crate::Project;
use clap::Parser;
use is_executable::IsExecutable;
use rattler_conda_types::Platform;

use crate::environment::get_up_to_date_prefix;
use crate::script::{CmdArgs, Command, ProcessCmd};
use rattler_shell::activation::ActivationResult;
use rattler_shell::{
activation::{ActivationVariables, Activator},
shell::{Shell, ShellEnum},
Expand All @@ -20,7 +26,29 @@ pub struct Args {

pub async fn execute(args: Args) -> anyhow::Result<()> {
let project = Project::discover()?;
let commands = project.commands()?;

// Get the script to execute from the command line.
let (command_name, command) = args
.command
.first()
.map_or_else(
|| Ok(None),
|cmd_name| {
project
.command_opt(cmd_name)
.with_context(|| format!("failed to parse command {cmd_name}"))
.map(|cmd| cmd.map(|cmd| (Some(cmd_name.clone()), cmd)))
},
)?
.unwrap_or_else(|| {
(
None,
Command::Process(ProcessCmd {
cmd: CmdArgs::Multiple(args.command),
depends_on: vec![],
}),
)
});

// Determine the current shell
let shell: ShellEnum = ShellEnum::detect_from_environment()
Expand All @@ -30,34 +58,64 @@ pub async fn execute(args: Args) -> anyhow::Result<()> {
let prefix = get_up_to_date_prefix(&project).await?;
let activator = Activator::from_path(prefix.root(), shell.clone(), Platform::current())?;

let path = std::env::split_paths(&std::env::var("PATH").unwrap_or_default())
.map(PathBuf::from)
.collect::<Vec<_>>();

let activator_result = activator.activation(ActivationVariables {
path: Some(path),
// Get the current PATH variable
path: std::env::var_os("PATH").map(|path_var| std::env::split_paths(&path_var).collect()),

// Start from an empty prefix
conda_prefix: None,
})?;

// if args[0] is in commands, run it
let command = if let Some(command) = commands.get(&args.command[0]) {
command.split(' ').collect::<Vec<&str>>()
} else {
args.command
.iter()
.map(|s| s.as_str())
.collect::<Vec<&str>>()
};

// Generate a temporary file with the script to execute. This includes the activation of the
// environment.
let mut script = format!("{}\n", activator_result.script.trim());
shell.run_command(&mut script, command)?;

// Perform post order traversal of the commands and their `depends_on` to make sure they are
// executed in the right order.
let mut s1 = VecDeque::new();
let mut s2 = VecDeque::new();
let mut added = HashSet::new();

// Add the command specified on the command line first
s1.push_back(command);
if let Some(command_name) = command_name {
added.insert(command_name);
}

while let Some(command) = s1.pop_back() {
// Get the dependencies of the command
let depends_on = match &command {
Command::Process(process) => process.depends_on.as_slice(),
Command::Alias(alias) => &alias.depends_on,
_ => &[],
};

// Locate the dependencies in the project and add them to the stack
for dependency in depends_on.iter() {
if !added.contains(dependency) {
let cmd = project
.command_opt(dependency)
.with_context(|| format!("failed to parse command {dependency}"))?
.ok_or_else(|| anyhow::anyhow!("failed to find dependency {}", dependency))?;

s1.push_back(cmd);
added.insert(dependency.clone());
}
}

s2.push_back(command)
}

while let Some(command) = s2.pop_back() {
// Write the invocation of the command into the script.
command.write_invoke_script(&mut script, &shell, &project, &activator_result)?;
}

// Write the contents of the script to a temporary file that we can execute with the shell.
let mut temp_file = tempfile::Builder::new()
.suffix(&format!(".{}", shell.extension()))
.tempfile()?;
temp_file.write_all(script.as_bytes())?;
std::io::Write::write_all(&mut temp_file, script.as_bytes())?;

// Execute the script with the shell
let mut command = shell
Expand All @@ -67,3 +125,142 @@ pub async fn execute(args: Args) -> anyhow::Result<()> {

std::process::exit(command.wait()?.code().unwrap_or(1));
}

/// Given a command and arguments to invoke it, format it so that it is as generalized as possible.
///
/// The executable is also canonicalized. This means the executable path is looked up. If the
/// executable is not found either in the environment or in the project root an error is returned.
fn format_execute_command(
project: &Project,
path: &[PathBuf],
args: &[String],
) -> anyhow::Result<Vec<String>> {
// Determine the command location
let command = args
.first()
.ok_or_else(|| anyhow::anyhow!("empty command"))?;
let command_path = find_command(command, project.root(), path.iter().map(|p| p.as_path()))
.ok_or_else(|| anyhow::anyhow!("could not find executable '{command}'"))?;

// Format all the commands and quote them properly.
Ok([command_path.to_string_lossy().as_ref()]
.into_iter()
.chain(args.iter().skip(1).map(|x| x.as_ref()))
.map(|arg| shlex::quote(arg).into_owned())
.collect())
}

// Locate the specified command name in the project or environment
fn find_command<'a>(
executable_name: &str,
project_root: &'a Path,
prefix_paths: impl IntoIterator<Item = &'a Path>,
) -> Option<PathBuf> {
let executable_path = Path::new(executable_name);

// Iterate over all search paths
for search_path in [project_root].into_iter().chain(prefix_paths) {
let absolute_executable_path = search_path.join(executable_path);

// Try to locate an executable at this location
if let Some(executable_path) = find_canonical_executable_path(&absolute_executable_path) {
return Some(executable_path);
}
}

None
}

// Given a relative executable path, try to find the canonical path
fn find_canonical_executable_path(path: &Path) -> Option<PathBuf> {
// If the path already points to an existing executable there is nothing to do.
match dunce::canonicalize(path) {
Ok(path) if path.is_executable() => return Some(path),
_ => {}
}

// Get executable extensions and see if by adding the extension we can turn it into a valid
// path.
for ext in executable_extensions() {
let with_ext = path.with_extension(ext);
match dunce::canonicalize(with_ext) {
Ok(path) if path.is_executable() => return Some(path),
_ => {}
}
}

None
}

/// Returns all file extensions that are considered for executable files.
#[cfg(windows)]
fn executable_extensions() -> &'static [String] {
use once_cell::sync::Lazy;
static PATHEXT: Lazy<Vec<String>> = Lazy::new(|| {
if let Some(pathext) = std::env::var_os("PATHEXT") {
pathext
.to_string_lossy()
.split(';')
// Filter out empty tokens and ';' at the end
.filter(|f| f.len() > 1)
// Cut off the leading '.' character
.map(|ext| ext[1..].to_string())
.collect::<Vec<_>>()
} else {
Vec::new()
}
});
PATHEXT.as_slice()
}

/// Returns all file extensions that are considered for executable files.
#[cfg(not(windows))]
fn executable_extensions() -> &'static [String] {
&[]
}

impl Command {
/// Write the invocation of this command to the specified script.
pub fn write_invoke_script(
&self,
contents: &mut String,
shell: &ShellEnum,
project: &Project,
activation_result: &ActivationResult,
) -> anyhow::Result<()> {
let args = match self {
Command::Plain(cmd) => {
let args = shlex::split(cmd)
.ok_or_else(|| anyhow::anyhow!("invalid quoted command arguments"))?;
Some(format_execute_command(
project,
&activation_result.path,
&args,
)?)
}
Command::Process(cmd) => {
let args = match &cmd.cmd {
CmdArgs::Single(str) => shlex::split(str)
.ok_or_else(|| anyhow::anyhow!("invalid quoted command arguments"))?,
CmdArgs::Multiple(args) => args.to_vec(),
};
Some(format_execute_command(
project,
&activation_result.path,
&args,
)?)
}
_ => None,
};

// If we have a command to execute, add it to the script.
if let Some(args) = args {
shell
.run_command(contents, args.iter().map(|arg| arg.as_ref()))
.expect("failed to write script");
writeln!(contents).expect("failed to write script");
}

Ok(())
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod prefix;
mod progress;
mod project;
mod repodata;
mod script;
mod virtual_packages;

pub use project::Project;
Expand Down
Loading

0 comments on commit e2e15b7

Please sign in to comment.