Skip to content

Commit

Permalink
feat: refactor script and add perl as an interpreter (#1229)
Browse files Browse the repository at this point in the history
  • Loading branch information
wolfv authored Nov 30, 2024
1 parent c09e575 commit 4265e0e
Show file tree
Hide file tree
Showing 8 changed files with 491 additions and 384 deletions.
3 changes: 1 addition & 2 deletions src/render/solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ fn print_as_table(packages: &[RepoDataRecord]) {
package
.channel
.as_ref()
.map(|s| s.rsplit('/').find(|s| !s.is_empty()))
.flatten()
.and_then(|s| s.rsplit('/').find(|s| !s.is_empty()))
.expect("expected channel to be defined and contain '/'")
.to_string()
} else {
Expand Down
66 changes: 66 additions & 0 deletions src/script/interpreter/bash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::path::PathBuf;

use rattler_conda_types::Platform;
use rattler_shell::shell;

use crate::script::{interpreter::DEBUG_HELP, run_process_with_replacements, ExecutionArgs};

use super::{find_interpreter, Interpreter};

const BASH_PREAMBLE: &str = r#"#!/bin/bash
## Start of bash preamble
if [ -z ${CONDA_BUILD+x} ]; then
source ((script_path))
fi
# enable debug mode for the rest of the script
set -x
## End of preamble
"#;

pub(crate) struct BashInterpreter;

impl Interpreter for BashInterpreter {
async fn run(&self, args: ExecutionArgs) -> Result<(), std::io::Error> {
let script = self.get_script(&args, shell::Bash).unwrap();

let build_env_path = args.work_dir.join("build_env.sh");
let build_script_path = args.work_dir.join("conda_build.sh");

tokio::fs::write(&build_env_path, script).await?;

let preamble = BASH_PREAMBLE.replace("((script_path))", &build_env_path.to_string_lossy());
let script = format!("{}\n{}", preamble, args.script.script());
tokio::fs::write(&build_script_path, script).await?;

let build_script_path_str = build_script_path.to_string_lossy().to_string();
let cmd_args = ["bash", "-e", &build_script_path_str];

let output = run_process_with_replacements(
&cmd_args,
&args.work_dir,
&args.replacements("$((var))"),
)
.await?;

if !output.status.success() {
let status_code = output.status.code().unwrap_or(1);
tracing::error!("Script failed with status {}", status_code);
tracing::error!("Work directory: '{}'", args.work_dir.display());
tracing::error!("{}", DEBUG_HELP);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Script failed".to_string(),
));
}

Ok(())
}

async fn find_interpreter(
&self,
build_prefix: Option<&PathBuf>,
platform: &Platform,
) -> Result<Option<PathBuf>, which::Error> {
find_interpreter("bash", build_prefix, platform)
}
}
82 changes: 82 additions & 0 deletions src/script/interpreter/cmd_exe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use std::path::PathBuf;

use rattler_conda_types::Platform;
use rattler_shell::shell;

use crate::script::{interpreter::DEBUG_HELP, run_process_with_replacements, ExecutionArgs};

use super::{find_interpreter, Interpreter};

const CMDEXE_PREAMBLE: &str = r#"
@chcp 65001 > nul
@echo on
IF "%CONDA_BUILD%" == "" (
@rem special behavior from conda-build for Windows
call ((script_path))
)
@rem re-enable echo because the activation scripts might have messed with it
@echo on
"#;

pub(crate) struct CmdExeInterpreter;

impl Interpreter for CmdExeInterpreter {
async fn run(&self, args: ExecutionArgs) -> Result<(), std::io::Error> {
let script = self.get_script(&args, shell::CmdExe).unwrap();

let build_env_path = args.work_dir.join("build_env.bat");
let build_script_path = args.work_dir.join("conda_build.bat");

tokio::fs::write(&build_env_path, script).await?;

let build_script = format!(
"{}\n{}",
CMDEXE_PREAMBLE.replace("((script_path))", &build_env_path.to_string_lossy()),
args.script.script()
);
tokio::fs::write(
&build_script_path,
&build_script.replace('\n', "\r\n").as_bytes(),
)
.await?;

let build_script_path_str = build_script_path.to_string_lossy().to_string();
let cmd_args = ["cmd.exe", "/d", "/c", &build_script_path_str];

let output = run_process_with_replacements(
&cmd_args,
&args.work_dir,
&args.replacements("%((var))%"),
)
.await?;

if !output.status.success() {
let status_code = output.status.code().unwrap_or(1);
tracing::error!("Script failed with status {}", status_code);
tracing::error!("Work directory: '{}'", args.work_dir.display());
tracing::error!("{}", DEBUG_HELP);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Script failed".to_string(),
));
}

Ok(())
}

async fn find_interpreter(
&self,
build_prefix: Option<&PathBuf>,
platform: &Platform,
) -> Result<Option<PathBuf>, which::Error> {
// check if COMSPEC is set to cmd.exe
if let Ok(comspec) = std::env::var("COMSPEC") {
if comspec.to_lowercase().contains("cmd.exe") {
return Ok(Some(PathBuf::from(comspec)));
}
}

// check if cmd.exe is in PATH
find_interpreter("cmd", build_prefix, platform)
}
}
100 changes: 100 additions & 0 deletions src/script/interpreter/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
mod bash;
mod cmd_exe;
mod nushell;
mod perl;
mod python;

use std::path::PathBuf;

pub(crate) use bash::BashInterpreter;
pub(crate) use cmd_exe::CmdExeInterpreter;
pub(crate) use nushell::NuShellInterpreter;
pub(crate) use perl::PerlInterpreter;
pub(crate) use python::PythonInterpreter;

use rattler_conda_types::Platform;
use rattler_shell::{
activation::{prefix_path_entries, ActivationError, ActivationVariables, Activator},
shell::{self, Shell},
};

use super::ExecutionArgs;

const DEBUG_HELP : &str = "To debug the build, run it manually in the work directory (execute the `./conda_build.sh` or `conda_build.bat` script)";

fn find_interpreter(
name: &str,
build_prefix: Option<&PathBuf>,
platform: &Platform,
) -> Result<Option<PathBuf>, which::Error> {
let exe_name = format!("{}{}", name, std::env::consts::EXE_SUFFIX);

let path = std::env::var("PATH").unwrap_or_default();
if let Some(build_prefix) = build_prefix {
let mut prepend_path = prefix_path_entries(build_prefix, platform)
.into_iter()
.collect::<Vec<_>>();
prepend_path.extend(std::env::split_paths(&path));
return Ok(
which::which_in_global(exe_name, std::env::join_paths(prepend_path).ok())?.next(),
);
}

Ok(which::which_in_global(exe_name, Some(path))?.next())
}

pub trait Interpreter {
fn get_script<T: Shell + Copy + 'static>(
&self,
args: &ExecutionArgs,
shell_type: T,
) -> Result<String, ActivationError> {
let mut shell_script = shell::ShellScript::new(shell_type, Platform::current());
for (k, v) in args.env_vars.iter() {
shell_script.set_env_var(k, v)?;
}
let host_prefix_activator =
Activator::from_path(&args.run_prefix, shell_type, args.execution_platform)?;

let current_path = std::env::var(shell_type.path_var(&args.execution_platform))
.ok()
.map(|p| std::env::split_paths(&p).collect::<Vec<_>>());
let conda_prefix = std::env::var("CONDA_PREFIX").ok().map(|p| p.into());

let activation_vars = ActivationVariables {
conda_prefix,
path: current_path,
path_modification_behavior: Default::default(),
};

let host_activation = host_prefix_activator.activation(activation_vars)?;

if let Some(build_prefix) = &args.build_prefix {
let build_prefix_activator =
Activator::from_path(build_prefix, shell_type, args.execution_platform)?;

let activation_vars = ActivationVariables {
conda_prefix: None,
path: Some(host_activation.path.clone()),
path_modification_behavior: Default::default(),
};

let build_activation = build_prefix_activator.activation(activation_vars)?;
shell_script.append_script(&host_activation.script);
shell_script.append_script(&build_activation.script);
} else {
shell_script.append_script(&host_activation.script);
}

Ok(shell_script.contents()?)
}

async fn run(&self, args: ExecutionArgs) -> Result<(), std::io::Error>;

#[allow(dead_code)]
async fn find_interpreter(
&self,
build_prefix: Option<&PathBuf>,
platform: &Platform,
) -> Result<Option<PathBuf>, which::Error>;
}
Loading

0 comments on commit 4265e0e

Please sign in to comment.