499 lines
16 KiB
Rust
Executable File
499 lines
16 KiB
Rust
Executable File
//! Post-extraction hooks: apply patches and run commands
|
|
|
|
use crate::package::{PackageSpec, Source};
|
|
use anyhow::{Context, Result, bail};
|
|
use indicatif::{ProgressBar, ProgressStyle};
|
|
use std::fs;
|
|
use std::io::{Read, Write};
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
use crate::builder::state::{BuildStep, StateTracker};
|
|
|
|
fn hook_env_vars(
|
|
spec: &PackageSpec,
|
|
shell_helpers: &crate::shell_helpers::ShellHelpers,
|
|
) -> crate::builder::EnvVars {
|
|
let mut env_vars = crate::builder::standard_build_env(spec, None, true, true);
|
|
shell_helpers.apply_to_env_vars(&mut env_vars);
|
|
env_vars
|
|
}
|
|
|
|
/// Apply patches and run `post_extract` commands in the extracted source tree.
|
|
pub fn post_extract(
|
|
spec: &PackageSpec,
|
|
source: &Source,
|
|
src_dir: &Path,
|
|
cache_dir: &Path,
|
|
) -> Result<()> {
|
|
let mut state = StateTracker::new(src_dir)?;
|
|
|
|
if !state.is_done(BuildStep::PatchesApplied) {
|
|
apply_patches(spec, source, src_dir, cache_dir)?;
|
|
state.mark_done(BuildStep::PatchesApplied)?;
|
|
}
|
|
|
|
if !state.is_done(BuildStep::PostExtractDone) {
|
|
run_post_extract_commands(spec, source, src_dir)?;
|
|
state.mark_done(BuildStep::PostExtractDone)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn apply_patches(
|
|
spec: &PackageSpec,
|
|
source: &Source,
|
|
src_dir: &Path,
|
|
cache_dir: &Path,
|
|
) -> Result<()> {
|
|
if source.patches.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let patch_cache_dir = cache_dir.join("patches").join(&spec.package.name);
|
|
fs::create_dir_all(&patch_cache_dir).with_context(|| {
|
|
format!(
|
|
"Failed to create patch cache dir: {}",
|
|
patch_cache_dir.display()
|
|
)
|
|
})?;
|
|
|
|
crate::log_info!("Applying {} patch(es)...", source.patches.len());
|
|
|
|
for p in &source.patches {
|
|
let p = spec.expand_vars(p);
|
|
let patch_path = resolve_patch_path(spec, &p, &patch_cache_dir)?;
|
|
|
|
crate::log_info!(" patch: {}", patch_path.display());
|
|
|
|
// Apply with patch(1). Keep it simple: -p1 is the common case.
|
|
let status = Command::new("patch")
|
|
.current_dir(src_dir)
|
|
.env("PATH", crate::runtime_env::safe_script_path())
|
|
.arg("-p1")
|
|
.arg("-i")
|
|
.arg(&patch_path)
|
|
.status()
|
|
.with_context(|| format!("Failed to execute patch for {}", patch_path.display()))?;
|
|
|
|
if !status.success() {
|
|
bail!("Patch failed: {}", patch_path.display());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn run_post_extract_commands(spec: &PackageSpec, source: &Source, src_dir: &Path) -> Result<()> {
|
|
if source.post_extract.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
crate::log_info!(
|
|
"Running {} post-extract command(s)...",
|
|
source.post_extract.len()
|
|
);
|
|
let helper_root = tempfile::tempdir().context("Failed to create post-extract helper root")?;
|
|
let shell_helpers = crate::shell_helpers::ShellHelpers::new(helper_root.path())?;
|
|
let env_vars = hook_env_vars(spec, &shell_helpers);
|
|
|
|
for cmd in &source.post_extract {
|
|
let cmd_str = spec.expand_vars(cmd);
|
|
crate::log_info!(" post_extract: {}", cmd_str);
|
|
let wrapped_cmd = crate::shell_helpers::wrap_shell_command(&cmd_str);
|
|
|
|
// Use a shell for convenience; this is a package manager, so specs are trusted input.
|
|
let mut shell_cmd = Command::new("sh");
|
|
shell_cmd.current_dir(src_dir);
|
|
crate::builder::prepare_command(&mut shell_cmd, &env_vars);
|
|
let status = shell_cmd
|
|
.arg("-c")
|
|
.arg(&wrapped_cmd)
|
|
.status()
|
|
.with_context(|| format!("Failed to run post_extract command: {}", cmd_str))?;
|
|
|
|
if !status.success() {
|
|
bail!("post_extract command failed: {}", cmd_str);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Run post-compile commands (after make, before make install).
|
|
pub fn run_post_configure_commands(
|
|
spec: &PackageSpec,
|
|
src_dir: &Path,
|
|
destdir: &Path,
|
|
) -> Result<()> {
|
|
let commands = &spec.build.flags.post_configure;
|
|
if commands.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
crate::log_info!("Running {} post-configure command(s)...", commands.len());
|
|
let shell_helpers = crate::shell_helpers::ShellHelpers::new(destdir)?;
|
|
let mut env_vars = hook_env_vars(spec, &shell_helpers);
|
|
crate::builder::set_env_var(
|
|
&mut env_vars,
|
|
"DESTDIR",
|
|
destdir.to_string_lossy().into_owned(),
|
|
);
|
|
|
|
for cmd in commands {
|
|
let cmd_str = spec.expand_vars(cmd);
|
|
crate::log_info!(" post_configure: {}", cmd_str);
|
|
let wrapped_cmd = crate::shell_helpers::wrap_shell_command(&cmd_str);
|
|
|
|
let mut shell_cmd = Command::new("sh");
|
|
shell_cmd.current_dir(src_dir);
|
|
crate::builder::prepare_command(&mut shell_cmd, &env_vars);
|
|
let status = shell_cmd
|
|
.arg("-c")
|
|
.arg(&wrapped_cmd)
|
|
.status()
|
|
.with_context(|| format!("Failed to run post_configure command: {}", cmd_str))?;
|
|
|
|
if !status.success() {
|
|
bail!("post_configure command failed: {}", cmd_str);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Run post-compile commands (after make, before make install).
|
|
pub fn run_post_compile_commands(spec: &PackageSpec, src_dir: &Path, destdir: &Path) -> Result<()> {
|
|
let commands = &spec.build.flags.post_compile;
|
|
if commands.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
crate::log_info!("Running {} post-compile command(s)...", commands.len());
|
|
let shell_helpers = crate::shell_helpers::ShellHelpers::new(destdir)?;
|
|
let mut env_vars = hook_env_vars(spec, &shell_helpers);
|
|
crate::builder::set_env_var(
|
|
&mut env_vars,
|
|
"DESTDIR",
|
|
destdir.to_string_lossy().into_owned(),
|
|
);
|
|
|
|
for cmd in commands {
|
|
let cmd_str = spec.expand_vars(cmd);
|
|
crate::log_info!(" post_compile: {}", cmd_str);
|
|
let wrapped_cmd = crate::shell_helpers::wrap_shell_command(&cmd_str);
|
|
|
|
let mut shell_cmd = Command::new("sh");
|
|
shell_cmd.current_dir(src_dir);
|
|
crate::builder::prepare_command(&mut shell_cmd, &env_vars);
|
|
let status = shell_cmd
|
|
.arg("-c")
|
|
.arg(&wrapped_cmd)
|
|
.status()
|
|
.with_context(|| format!("Failed to run post_compile command: {}", cmd_str))?;
|
|
|
|
if !status.success() {
|
|
bail!("post_compile command failed: {}", cmd_str);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Run post-install commands (after make install) from the provided working directory.
|
|
pub fn run_post_install_commands_in_dir(
|
|
spec: &PackageSpec,
|
|
work_dir: &Path,
|
|
destdir: &Path,
|
|
) -> Result<()> {
|
|
let commands = &spec.build.flags.post_install;
|
|
if commands.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
crate::log_info!("Running {} post-install command(s)...", commands.len());
|
|
let shell_helpers = crate::shell_helpers::ShellHelpers::new(destdir)?;
|
|
let mut env_vars = hook_env_vars(spec, &shell_helpers);
|
|
crate::builder::set_env_var(
|
|
&mut env_vars,
|
|
"DESTDIR",
|
|
destdir.to_string_lossy().into_owned(),
|
|
);
|
|
|
|
for cmd in commands {
|
|
let cmd_str = spec.expand_vars(cmd);
|
|
crate::log_info!(" post_install: {}", cmd_str);
|
|
let wrapped_cmd = crate::shell_helpers::wrap_shell_command(&cmd_str);
|
|
|
|
let mut shell_cmd = Command::new("sh");
|
|
shell_cmd.current_dir(work_dir);
|
|
crate::builder::prepare_command(&mut shell_cmd, &env_vars);
|
|
let status = shell_cmd
|
|
.arg("-c")
|
|
.arg(&wrapped_cmd)
|
|
.status()
|
|
.with_context(|| format!("Failed to run post_install command: {}", cmd_str))?;
|
|
|
|
if !status.success() {
|
|
bail!("post_install command failed: {}", cmd_str);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Run post-install commands (after make install).
|
|
pub fn run_post_install_commands(spec: &PackageSpec, src_dir: &Path, destdir: &Path) -> Result<()> {
|
|
run_post_install_commands_in_dir(spec, src_dir, destdir)
|
|
}
|
|
|
|
fn resolve_patch_path(spec: &PackageSpec, patch: &str, patch_cache_dir: &Path) -> Result<PathBuf> {
|
|
if patch.starts_with("http://") || patch.starts_with("https://") {
|
|
let filename = patch
|
|
.split('/')
|
|
.rfind(|s| !s.is_empty())
|
|
.unwrap_or("patch.patch");
|
|
let dest = patch_cache_dir.join(filename);
|
|
if dest.exists() {
|
|
return Ok(dest);
|
|
}
|
|
download(patch, &dest).with_context(|| format!("Failed to download patch: {}", patch))?;
|
|
return Ok(dest);
|
|
}
|
|
|
|
// Treat as local path, relative to the spec file.
|
|
let local = spec.spec_dir.join(patch);
|
|
if !local.exists() {
|
|
bail!(
|
|
"Patch not found: {} (resolved to {})",
|
|
patch,
|
|
local.display()
|
|
);
|
|
}
|
|
Ok(local)
|
|
}
|
|
|
|
fn download(url: &str, dest: &Path) -> Result<()> {
|
|
if let Some(parent) = dest.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
|
|
let client = reqwest::blocking::Client::new();
|
|
let mut response = client
|
|
.get(url)
|
|
.send()
|
|
.with_context(|| format!("Failed to fetch: {}", url))?;
|
|
|
|
let total_size = response.content_length().unwrap_or(0);
|
|
let pb = ProgressBar::new(total_size);
|
|
pb.set_style(
|
|
ProgressStyle::default_bar()
|
|
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
|
|
.unwrap()
|
|
.progress_chars("#>-")
|
|
);
|
|
|
|
let mut file =
|
|
fs::File::create(dest).with_context(|| format!("Failed to create: {}", dest.display()))?;
|
|
|
|
let mut buffer = [0u8; 8192];
|
|
let mut downloaded = 0u64;
|
|
|
|
loop {
|
|
let bytes_read = response.read(&mut buffer)?;
|
|
if bytes_read == 0 {
|
|
break;
|
|
}
|
|
file.write_all(&buffer[..bytes_read])?;
|
|
downloaded += bytes_read as u64;
|
|
pb.set_position(downloaded);
|
|
}
|
|
|
|
pb.finish_with_message("Patch download complete");
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
resolve_patch_path, run_post_extract_commands, run_post_install_commands,
|
|
run_post_install_commands_in_dir,
|
|
};
|
|
use crate::package::{
|
|
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
|
|
};
|
|
|
|
fn dummy_spec(spec_dir: &std::path::Path) -> PackageSpec {
|
|
PackageSpec {
|
|
package: PackageInfo {
|
|
name: "foo".into(),
|
|
version: "1.0".into(),
|
|
revision: 1,
|
|
description: "d".into(),
|
|
homepage: "h".into(),
|
|
license: vec!["MIT".into()],
|
|
},
|
|
packages: Vec::new(),
|
|
alternatives: Alternatives::default(),
|
|
manual_sources: Vec::new(),
|
|
source: vec![Source {
|
|
url: "https://example.com/foo.tar.gz".into(),
|
|
sha256: "skip".into(),
|
|
extract_dir: "foo".into(),
|
|
patches: Vec::new(),
|
|
post_extract: Vec::new(),
|
|
cherry_pick: Vec::new(),
|
|
}],
|
|
build: Build {
|
|
build_type: BuildType::Custom,
|
|
flags: BuildFlags::default(),
|
|
},
|
|
dependencies: Dependencies::default(),
|
|
package_alternatives: Default::default(),
|
|
package_dependencies: Default::default(),
|
|
spec_dir: spec_dir.to_path_buf(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_patch_relative_to_spec_dir() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let spec_dir = tmp.path().join("spec");
|
|
std::fs::create_dir_all(&spec_dir).unwrap();
|
|
let patch_path = spec_dir.join("fix.patch");
|
|
std::fs::write(&patch_path, "diff --git a/a b/a\n").unwrap();
|
|
|
|
let spec = dummy_spec(&spec_dir);
|
|
let cache = tmp.path().join("cache");
|
|
std::fs::create_dir_all(&cache).unwrap();
|
|
let resolved = resolve_patch_path(&spec, "fix.patch", &cache).unwrap();
|
|
assert_eq!(resolved, patch_path);
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_patch_url_uses_cached_file_if_present() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let spec_dir = tmp.path().join("spec");
|
|
std::fs::create_dir_all(&spec_dir).unwrap();
|
|
let spec = dummy_spec(&spec_dir);
|
|
|
|
let cache = tmp.path().join("cache");
|
|
let patch_cache_dir = cache.join("patches").join("foo");
|
|
std::fs::create_dir_all(&patch_cache_dir).unwrap();
|
|
|
|
// URL-derived filename is the last segment.
|
|
let cached = patch_cache_dir.join("fix.patch");
|
|
std::fs::write(&cached, "dummy").unwrap();
|
|
|
|
let url = "https://example.com/fix.patch";
|
|
let resolved = resolve_patch_path(&spec, url, &patch_cache_dir).unwrap();
|
|
assert_eq!(resolved, cached);
|
|
}
|
|
|
|
#[test]
|
|
fn post_install_commands_can_haul_into_output_staging() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let spec_dir = tmp.path().join("spec");
|
|
let src_dir = tmp.path().join("src");
|
|
let destdir = tmp.path().join("dest");
|
|
std::fs::create_dir_all(&spec_dir).unwrap();
|
|
std::fs::create_dir_all(&src_dir).unwrap();
|
|
std::fs::create_dir_all(destdir.join("usr/lib")).unwrap();
|
|
std::fs::write(destdir.join("usr/lib/libLLVM.so.1"), "llvm").unwrap();
|
|
|
|
let mut spec = dummy_spec(&spec_dir);
|
|
spec.build.flags.post_install = vec!["haul llvm-libs 'usr/lib/libLLVM*.so*'".into()];
|
|
|
|
run_post_install_commands(&spec, &src_dir, &destdir).unwrap();
|
|
|
|
assert!(!destdir.join("usr/lib/libLLVM.so.1").exists());
|
|
assert!(
|
|
destdir
|
|
.join(".depot/outputs/llvm-libs/usr/lib/libLLVM.so.1")
|
|
.exists()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn post_install_commands_can_run_from_build_dir() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let spec_dir = tmp.path().join("spec");
|
|
let src_dir = tmp.path().join("src");
|
|
let build_dir = tmp.path().join("build");
|
|
let destdir = tmp.path().join("dest");
|
|
std::fs::create_dir_all(&spec_dir).unwrap();
|
|
std::fs::create_dir_all(&src_dir).unwrap();
|
|
std::fs::create_dir_all(build_dir.join("destdir/usr/include/gnu")).unwrap();
|
|
std::fs::create_dir_all(destdir.join("usr/include/gnu")).unwrap();
|
|
std::fs::write(
|
|
build_dir.join("destdir/usr/include/gnu/lib-names-32.h"),
|
|
"lib32",
|
|
)
|
|
.unwrap();
|
|
|
|
let mut spec = dummy_spec(&spec_dir);
|
|
spec.build.flags.post_install = vec![
|
|
"install -m644 destdir/usr/include/gnu/lib-names-32.h \"$DESTDIR/usr/include/gnu/\""
|
|
.into(),
|
|
];
|
|
|
|
run_post_install_commands_in_dir(&spec, &build_dir, &destdir).unwrap();
|
|
|
|
assert!(destdir.join("usr/include/gnu/lib-names-32.h").exists());
|
|
assert!(
|
|
!src_dir
|
|
.join("destdir/usr/include/gnu/lib-names-32.h")
|
|
.exists()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn post_extract_commands_can_call_python_build_helper() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let spec_dir = tmp.path().join("spec");
|
|
let src_dir = tmp.path().join("src");
|
|
std::fs::create_dir_all(&spec_dir).unwrap();
|
|
std::fs::create_dir_all(&src_dir).unwrap();
|
|
|
|
let fake_depot = tmp.path().join("fake-depot");
|
|
let log_path = tmp.path().join("python-build.log");
|
|
std::fs::write(
|
|
&fake_depot,
|
|
format!(
|
|
"#!/bin/sh\nset -eu\nprintf '%s\\n' \"$@\" > '{}'\n",
|
|
log_path.display()
|
|
),
|
|
)
|
|
.unwrap();
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let mut perms = std::fs::metadata(&fake_depot).unwrap().permissions();
|
|
perms.set_mode(0o755);
|
|
std::fs::set_permissions(&fake_depot, perms).unwrap();
|
|
}
|
|
|
|
let spec = dummy_spec(&spec_dir);
|
|
let source = Source {
|
|
url: "https://example.com/foo.tar.gz".into(),
|
|
sha256: "skip".into(),
|
|
extract_dir: "foo".into(),
|
|
patches: Vec::new(),
|
|
post_extract: vec![format!(
|
|
"DEPOT_EXECUTABLE='{}' python_build --src-dir . --dist-dir dist",
|
|
fake_depot.display()
|
|
)],
|
|
cherry_pick: Vec::new(),
|
|
};
|
|
|
|
run_post_extract_commands(&spec, &source, &src_dir).unwrap();
|
|
|
|
let logged = std::fs::read_to_string(&log_path).unwrap();
|
|
assert!(logged.contains("internal"));
|
|
assert!(logged.contains("python-build"));
|
|
assert!(logged.contains("--src-dir"));
|
|
assert!(logged.contains("--dist-dir"));
|
|
}
|
|
}
|