Files
RedBear-OS/local/recipes/system/cub/source/cub-lib/src/cook.rs
T
2026-05-07 20:57:11 +01:00

372 lines
10 KiB
Rust

use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::error::CubError;
use crate::sandbox::SandboxConfig;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CookResult {
pub pkgar_path: PathBuf,
pub toml_path: PathBuf,
pub stage_dir: PathBuf,
}
pub fn cook_recipe(
recipe_dir: &Path,
target: &str,
repo_dir: &Path,
) -> Result<CookResult, CubError> {
let repo_binary = find_repo_binary_with_hint(repo_dir).ok_or_else(|| {
CubError::BuildFailed(
"repo binary not found. Build tools must be installed inside Red Bear OS to cook recipes."
.to_string(),
)
})?;
let mut sandbox = SandboxConfig::new(recipe_dir);
sandbox.target = target.to_string();
sandbox.gnu_target = gnu_target_for(target);
sandbox.setup()?;
let output = Command::new(&repo_binary)
.arg("cook")
.arg(recipe_dir)
.current_dir(repo_dir)
.envs(sandbox.env_vars())
.output()?;
if !output.status.success() {
return Err(CubError::BuildFailed(render_command_failure(
&output.stdout,
&output.stderr,
)));
}
let recipe_name = recipe_name(recipe_dir)?;
let pkgar_path =
find_repo_artifact(repo_dir, target, &recipe_name, "pkgar")?.ok_or_else(|| {
CubError::BuildFailed(format!(
"repo cook succeeded, but no .pkgar artifact was found for recipe '{}' under {}",
recipe_name,
repo_dir.display()
))
})?;
let toml_path = find_repo_artifact(repo_dir, target, &recipe_name, "toml")?
.unwrap_or_else(|| recipe_dir.join("recipe.toml"));
Ok(CookResult {
pkgar_path,
toml_path,
stage_dir: sandbox.stage_dir,
})
}
pub fn is_repo_available() -> bool {
find_repo_binary().is_some()
}
pub fn find_repo_binary() -> Option<PathBuf> {
let cwd = std::env::current_dir().ok();
find_repo_binary_from(std::env::var_os("PATH"), cwd.as_deref(), None)
}
pub fn cook_available() -> Result<(), CubError> {
if is_repo_available() {
Ok(())
} else {
Err(CubError::BuildFailed(
"repo binary not found. Build tools must be installed inside Red Bear OS to cook recipes."
.to_string(),
))
}
}
fn find_repo_binary_with_hint(repo_dir: &Path) -> Option<PathBuf> {
let cwd = std::env::current_dir().ok();
find_repo_binary_from(std::env::var_os("PATH"), cwd.as_deref(), Some(repo_dir))
}
fn find_repo_binary_from(
path_env: Option<OsString>,
cwd: Option<&Path>,
repo_dir: Option<&Path>,
) -> Option<PathBuf> {
if let Some(path_env) = path_env {
for entry in std::env::split_paths(&path_env) {
let candidate = entry.join("repo");
if is_executable_file(&candidate) {
return Some(candidate);
}
}
}
if let Some(repo_dir) = repo_dir {
let candidate = repo_dir.join("target/release/repo");
if is_executable_file(&candidate) {
return Some(candidate);
}
}
if let Some(cwd) = cwd {
let candidate = cwd.join("target/release/repo");
if is_executable_file(&candidate) {
return Some(candidate);
}
}
None
}
fn find_repo_artifact(
repo_dir: &Path,
target: &str,
recipe_name: &str,
extension: &str,
) -> Result<Option<PathBuf>, CubError> {
for directory in repo_artifact_dirs(repo_dir, target) {
if let Some(path) = find_artifact_in_dir(&directory, recipe_name, extension)? {
return Ok(Some(path));
}
}
Ok(None)
}
fn repo_artifact_dirs(repo_dir: &Path, target: &str) -> Vec<PathBuf> {
let mut dirs = Vec::new();
for dir in [
repo_dir.join("repo").join(target),
repo_dir.join(target),
repo_dir.to_path_buf(),
] {
if !dirs.iter().any(|existing| existing == &dir) {
dirs.push(dir);
}
}
dirs
}
fn find_artifact_in_dir(
dir: &Path,
recipe_name: &str,
extension: &str,
) -> Result<Option<PathBuf>, CubError> {
if !dir.is_dir() {
return Ok(None);
}
let exact = dir.join(format!("{recipe_name}.{extension}"));
if exact.is_file() {
return Ok(Some(exact));
}
let mut latest: Option<(std::time::SystemTime, PathBuf)> = None;
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() || path.extension().and_then(|ext| ext.to_str()) != Some(extension) {
continue;
}
let modified = entry
.metadata()
.and_then(|metadata| metadata.modified())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
match &latest {
Some((latest_modified, _)) if modified <= *latest_modified => {}
_ => latest = Some((modified, path)),
}
}
Ok(latest.map(|(_, path)| path))
}
fn is_executable_file(path: &Path) -> bool {
let metadata = match fs::metadata(path) {
Ok(metadata) => metadata,
Err(_) => return false,
};
if !metadata.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
metadata.permissions().mode() & 0o111 != 0
}
#[cfg(not(unix))]
{
true
}
}
fn gnu_target_for(target: &str) -> String {
if let Some(prefix) = target.strip_suffix("-unknown-redox") {
format!("{prefix}-redox")
} else {
target.to_string()
}
}
fn recipe_name(recipe_dir: &Path) -> Result<String, CubError> {
recipe_dir
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.to_string())
.ok_or_else(|| {
CubError::BuildFailed(format!(
"could not determine recipe name from {}",
recipe_dir.display()
))
})
}
fn render_command_failure(stdout: &[u8], stderr: &[u8]) -> String {
let stderr = String::from_utf8_lossy(stderr).trim().to_string();
let stdout = String::from_utf8_lossy(stdout).trim().to_string();
if !stderr.is_empty() {
stderr
} else if !stdout.is_empty() {
stdout
} else {
"repo cook failed without diagnostic output".to_string()
}
}
#[cfg(test)]
mod tests {
use super::{find_repo_binary, is_repo_available};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use tempfile::tempdir;
fn env_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
struct TestEnvGuard {
previous_path: Option<std::ffi::OsString>,
previous_cwd: PathBuf,
}
impl TestEnvGuard {
fn new(path: Option<&Path>, cwd: &Path) -> Self {
let previous_path = std::env::var_os("PATH");
let previous_cwd = std::env::current_dir().expect("current dir");
if let Some(path) = path {
// SAFETY: tests serialize PATH mutations with a process-wide mutex.
unsafe { std::env::set_var("PATH", path) }
} else {
// SAFETY: tests serialize PATH mutations with a process-wide mutex.
unsafe { std::env::remove_var("PATH") }
}
std::env::set_current_dir(cwd).expect("set current dir");
Self {
previous_path,
previous_cwd,
}
}
}
impl Drop for TestEnvGuard {
fn drop(&mut self) {
if let Some(path) = &self.previous_path {
// SAFETY: tests serialize PATH mutations with a process-wide mutex.
unsafe { std::env::set_var("PATH", path) }
} else {
// SAFETY: tests serialize PATH mutations with a process-wide mutex.
unsafe { std::env::remove_var("PATH") }
}
std::env::set_current_dir(&self.previous_cwd).expect("restore current dir");
}
}
#[test]
fn find_repo_binary_uses_path_entry() {
let _guard = env_lock().lock().expect("lock env");
let temp = tempdir().expect("tempdir");
let bin_dir = temp.path().join("bin");
let cwd = temp.path().join("cwd");
fs::create_dir_all(&bin_dir).expect("create bin dir");
fs::create_dir_all(&cwd).expect("create cwd dir");
let repo_binary = bin_dir.join("repo");
create_executable(&repo_binary);
let _env = TestEnvGuard::new(Some(&bin_dir), &cwd);
assert_eq!(find_repo_binary(), Some(repo_binary));
}
#[test]
fn find_repo_binary_uses_local_release_binary() {
let _guard = env_lock().lock().expect("lock env");
let temp = tempdir().expect("tempdir");
let cwd = temp.path().join("project");
let local_repo = cwd.join("target/release/repo");
fs::create_dir_all(local_repo.parent().expect("local repo parent"))
.expect("create target dir");
create_executable(&local_repo);
let _env = TestEnvGuard::new(None, &cwd);
assert_eq!(find_repo_binary(), Some(local_repo));
}
#[test]
fn is_repo_available_reports_true_when_local_binary_exists() {
let _guard = env_lock().lock().expect("lock env");
let temp = tempdir().expect("tempdir");
let cwd = temp.path().join("project");
let local_repo = cwd.join("target/release/repo");
fs::create_dir_all(local_repo.parent().expect("local repo parent"))
.expect("create target dir");
create_executable(&local_repo);
let _env = TestEnvGuard::new(None, &cwd);
assert!(is_repo_available());
}
#[test]
fn is_repo_available_reports_false_without_repo_binary() {
let _guard = env_lock().lock().expect("lock env");
let temp = tempdir().expect("tempdir");
let cwd = temp.path().join("project");
fs::create_dir_all(&cwd).expect("create cwd dir");
let _env = TestEnvGuard::new(None, &cwd);
assert!(!is_repo_available());
}
fn create_executable(path: &Path) {
fs::write(path, b"#!/bin/sh\nexit 0\n").expect("write repo binary");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut permissions = fs::metadata(path).expect("metadata").permissions();
permissions.set_mode(0o755);
fs::set_permissions(path, permissions).expect("set permissions");
}
}
}