900fefc46e
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
372 lines
10 KiB
Rust
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");
|
|
}
|
|
}
|
|
}
|