use crate::error::{Result, WaxError};
use crate::formula_parser::{BuildSystem, ParsedFormula};
use indicatif::ProgressBar;
use std::path::{Path, PathBuf};
use std::process::Command;
use tracing::{debug, info, instrument};
pub struct Builder {
num_cores: usize,
use_ccache: bool,
}
impl Builder {
pub fn new() -> Self {
let num_cores = Self::detect_cpu_cores();
let use_ccache = Self::detect_ccache();
info!(
"Builder initialized: {} cores, ccache: {}",
num_cores, use_ccache
);
Self {
num_cores,
use_ccache,
}
}
fn detect_cpu_cores() -> usize {
num_cpus::get().saturating_sub(1).max(1)
}
fn detect_ccache() -> bool {
Command::new("which")
.arg("ccache")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
#[instrument(skip(self, progress))]
pub async fn build_from_source(
&self,
formula: &ParsedFormula,
source_tarball: &Path,
build_dir: &Path,
install_prefix: &Path,
progress: Option<&ProgressBar>,
) -> Result<()> {
info!("Building {} from source", formula.name);
if let Some(pb) = progress {
pb.set_message("Extracting source...");
}
self.extract_source(source_tarball, build_dir).await?;
let source_dir = self.find_source_directory(build_dir)?;
if let Some(pb) = progress {
pb.set_message("Configuring build...");
}
match formula.build_system {
BuildSystem::Autotools => {
self.build_autotools(&source_dir, install_prefix, &formula.configure_args)
.await?
}
BuildSystem::CMake => {
self.build_cmake(&source_dir, install_prefix, &formula.configure_args)
.await?
}
BuildSystem::Meson => {
self.build_meson(&source_dir, install_prefix, &formula.configure_args)
.await?
}
BuildSystem::Make => self.build_make(&source_dir, install_prefix).await?,
BuildSystem::Cargo => self.build_cargo(&source_dir, install_prefix).await?,
BuildSystem::Unknown => {
return Err(WaxError::BuildError(
"Unknown build system - cannot build from source".to_string(),
))
}
}
if let Some(pb) = progress {
pb.set_message("Build complete");
}
Ok(())
}
#[instrument(skip(self, progress))]
pub async fn build_from_directory(
&self,
formula: &ParsedFormula,
source_dir: &Path,
install_prefix: &Path,
progress: Option<&ProgressBar>,
) -> Result<()> {
info!("Building {} from directory {:?}", formula.name, source_dir);
match formula.build_system {
BuildSystem::Autotools => {
self.build_autotools(source_dir, install_prefix, &formula.configure_args)
.await?
}
BuildSystem::CMake => {
self.build_cmake(source_dir, install_prefix, &formula.configure_args)
.await?
}
BuildSystem::Meson => {
self.build_meson(source_dir, install_prefix, &formula.configure_args)
.await?
}
BuildSystem::Make => self.build_make(source_dir, install_prefix).await?,
BuildSystem::Cargo => self.build_cargo(source_dir, install_prefix).await?,
BuildSystem::Unknown => {
return Err(WaxError::BuildError(
"Unknown build system - cannot build from source".to_string(),
))
}
}
if let Some(pb) = progress {
pb.set_message("Build complete");
}
Ok(())
}
async fn extract_source(&self, tarball: &Path, dest: &Path) -> Result<()> {
debug!("Extracting {:?} to {:?}", tarball, dest);
tokio::fs::create_dir_all(dest).await?;
let output = Command::new("tar")
.arg("xzf")
.arg(tarball)
.arg("-C")
.arg(dest)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(WaxError::BuildError(format!(
"Failed to extract source: {}",
stderr
)));
}
Ok(())
}
fn find_source_directory(&self, build_dir: &Path) -> Result<PathBuf> {
let entries = std::fs::read_dir(build_dir)?
.filter_map(|e| e.ok())
.collect::<Vec<_>>();
if entries.len() == 1 {
let entry = &entries[0];
if entry.file_type()?.is_dir() {
return Ok(entry.path());
}
}
Ok(build_dir.to_path_buf())
}
async fn build_autotools(
&self,
source_dir: &Path,
prefix: &Path,
configure_args: &[String],
) -> Result<()> {
info!("Building with autotools");
let configure_script = source_dir.join("configure");
if !configure_script.exists() {
let bootstrap = source_dir.join("bootstrap");
if bootstrap.exists() {
self.run_command(source_dir, "./bootstrap", &[], "Bootstrapping")
.await?;
} else {
let autogen = source_dir.join("autogen.sh");
if autogen.exists() {
self.run_command(source_dir, "./autogen.sh", &[], "Generating build files")
.await?;
}
}
}
let mut args = vec![format!("--prefix={}", prefix.display())];
args.extend(configure_args.iter().cloned());
self.run_command(source_dir, "./configure", &args, "Configuring")
.await?;
let make_args = vec![format!("-j{}", self.num_cores)];
self.run_command(source_dir, "make", &make_args, "Compiling")
.await?;
self.run_command(source_dir, "make", &["install".to_string()], "Installing")
.await?;
Ok(())
}
async fn build_cmake(
&self,
source_dir: &Path,
prefix: &Path,
configure_args: &[String],
) -> Result<()> {
info!("Building with CMake");
let build_dir = source_dir.join("build");
tokio::fs::create_dir_all(&build_dir).await?;
let prefer_ninja = Self::has_ninja();
let generator = if prefer_ninja {
"Ninja"
} else {
"Unix Makefiles"
};
let homebrew_prefix = crate::bottle::homebrew_prefix();
let mut args = vec![
"-S".to_string(),
source_dir.display().to_string(),
"-B".to_string(),
build_dir.display().to_string(),
format!("-DCMAKE_INSTALL_PREFIX={}", prefix.display()),
format!("-G{}", generator),
format!("-DCMAKE_INSTALL_RPATH={}/lib", homebrew_prefix.display()),
"-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON".to_string(),
];
args.extend(configure_args.iter().cloned());
self.run_command(source_dir, "cmake", &args, "Configuring CMake")
.await?;
let build_args = vec![
"--build".to_string(),
build_dir.display().to_string(),
"--parallel".to_string(),
self.num_cores.to_string(),
];
self.run_command(source_dir, "cmake", &build_args, "Building")
.await?;
let install_args = vec!["--install".to_string(), build_dir.display().to_string()];
self.run_command(source_dir, "cmake", &install_args, "Installing")
.await?;
Ok(())
}
async fn build_meson(
&self,
source_dir: &Path,
prefix: &Path,
configure_args: &[String],
) -> Result<()> {
info!("Building with Meson");
let build_dir = source_dir.join("build");
let mut args = vec![
"setup".to_string(),
build_dir.display().to_string(),
format!("--prefix={}", prefix.display()),
];
args.extend(configure_args.iter().cloned());
self.run_command(source_dir, "meson", &args, "Configuring Meson")
.await?;
let ninja_args = vec![
"-C".to_string(),
build_dir.display().to_string(),
format!("-j{}", self.num_cores),
];
self.run_command(source_dir, "ninja", &ninja_args, "Building")
.await?;
let install_args = vec![
"-C".to_string(),
build_dir.display().to_string(),
"install".to_string(),
];
self.run_command(source_dir, "ninja", &install_args, "Installing")
.await?;
Ok(())
}
async fn build_cargo(&self, source_dir: &Path, prefix: &Path) -> Result<()> {
info!("Building with Cargo");
let install_args = vec![
"install".to_string(),
"--path".to_string(),
".".to_string(),
"--root".to_string(),
prefix.display().to_string(),
"--jobs".to_string(),
self.num_cores.to_string(),
];
self.run_command(source_dir, "cargo", &install_args, "Building")
.await?;
Ok(())
}
async fn build_make(&self, source_dir: &Path, prefix: &Path) -> Result<()> {
info!("Building with Make");
let make_args = vec![
format!("PREFIX={}", prefix.display()),
format!("-j{}", self.num_cores),
];
self.run_command(source_dir, "make", &make_args, "Building")
.await?;
let install_args = vec![
format!("PREFIX={}", prefix.display()),
"install".to_string(),
];
self.run_command(source_dir, "make", &install_args, "Installing")
.await?;
Ok(())
}
async fn run_command(
&self,
work_dir: &Path,
program: &str,
args: &[String],
phase: &str,
) -> Result<()> {
debug!("{}: {} {:?}", phase, program, args);
let work_dir = work_dir.to_path_buf();
let program = program.to_string();
let args = args.to_vec();
let use_ccache = self.use_ccache;
let num_cores = self.num_cores;
let phase = phase.to_string();
tokio::task::spawn_blocking(move || {
let mut cmd = Command::new(&program);
cmd.current_dir(&work_dir);
for arg in &args {
cmd.arg(arg);
}
if use_ccache && (program == "gcc" || program == "clang" || program == "cc") {
let ccache_path = Command::new("which")
.arg("ccache")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|_| "ccache".to_string());
cmd.env("CC", format!("{} {}", ccache_path, program));
}
if use_ccache && (program == "g++" || program == "clang++" || program == "c++") {
let ccache_path = Command::new("which")
.arg("ccache")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|_| "ccache".to_string());
cmd.env("CXX", format!("{} {}", ccache_path, program));
}
cmd.env("MAKEFLAGS", format!("-j{}", num_cores));
let output = cmd.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let last_lines: Vec<&str> = stderr.lines().rev().take(50).collect();
return Err(WaxError::BuildError(format!(
"{} failed:\n{}",
phase,
last_lines.into_iter().rev().collect::<Vec<_>>().join("\n")
)));
}
Ok(())
})
.await
.map_err(|e| WaxError::BuildError(format!("Build task panicked: {}", e)))?
}
fn has_ninja() -> bool {
Command::new("which")
.arg("ninja")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
}
impl Default for Builder {
fn default() -> Self {
Self::new()
}
}