feat: add Cub cook autodetect strategy

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-05-07 21:19:24 +01:00
parent c3ba6ad91b
commit 98affe0191
@@ -6,6 +6,13 @@ use std::process::Command;
use crate::error::CubError; use crate::error::CubError;
use crate::sandbox::SandboxConfig; use crate::sandbox::SandboxConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CookStrategy {
Require,
Skip,
AutoDetect,
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct CookResult { pub struct CookResult {
pub pkgar_path: PathBuf, pub pkgar_path: PathBuf,
@@ -18,12 +25,31 @@ pub fn cook_recipe(
target: &str, target: &str,
repo_dir: &Path, repo_dir: &Path,
) -> Result<CookResult, CubError> { ) -> Result<CookResult, CubError> {
let repo_binary = find_repo_binary_with_hint(repo_dir).ok_or_else(|| { match cook_or_skip(recipe_dir, target, repo_dir, CookStrategy::Require)? {
CubError::BuildFailed( Some(result) => Ok(result),
"repo binary not found. Build tools must be installed inside Red Bear OS to cook recipes." None => Err(CubError::BuildFailed(
.to_string(), "cooking was skipped despite CookStrategy::Require".to_string(),
) )),
})?; }
}
pub fn cook_or_skip(
recipe_dir: &Path,
target: &str,
repo_dir: &Path,
strategy: CookStrategy,
) -> Result<Option<CookResult>, CubError> {
let repo_binary = match strategy {
CookStrategy::Require => Some(
find_repo_binary_with_hint(repo_dir).ok_or_else(cook_unavailable_with_message_error)?,
),
CookStrategy::Skip => return Ok(None),
CookStrategy::AutoDetect => find_repo_binary_with_hint(repo_dir),
};
let Some(repo_binary) = repo_binary else {
return Ok(None);
};
let mut sandbox = SandboxConfig::new(recipe_dir); let mut sandbox = SandboxConfig::new(recipe_dir);
sandbox.target = target.to_string(); sandbox.target = target.to_string();
@@ -56,11 +82,11 @@ pub fn cook_recipe(
let toml_path = find_repo_artifact(repo_dir, target, &recipe_name, "toml")? let toml_path = find_repo_artifact(repo_dir, target, &recipe_name, "toml")?
.unwrap_or_else(|| recipe_dir.join("recipe.toml")); .unwrap_or_else(|| recipe_dir.join("recipe.toml"));
Ok(CookResult { Ok(Some(CookResult {
pkgar_path, pkgar_path,
toml_path, toml_path,
stage_dir: sandbox.stage_dir, stage_dir: sandbox.stage_dir,
}) }))
} }
pub fn is_repo_available() -> bool { pub fn is_repo_available() -> bool {
@@ -83,11 +109,37 @@ pub fn cook_available() -> Result<(), CubError> {
} }
} }
pub fn cook_with_setup_instructions() -> String {
concat!(
"Build tools are not available in this environment. To cook recipes, you need:\n",
"1. The Red Bear OS build toolchain (cross-compiler for x86_64-unknown-redox)\n",
"2. The 'repo' cookbook binary in your PATH or at ./target/release/repo\n",
"\n",
"Until build tools are ported inside Red Bear OS, you can:\n",
"- Use 'cub -G <pkg>' to fetch and convert AUR packages to recipes\n",
"- Build packages on a Linux build host and transfer the .pkgar files\n",
"- Install pre-built packages directly with 'cub -S <pkg>'"
)
.to_string()
}
pub fn cook_available_with_message() -> Result<(), CubError> {
if is_repo_available() {
Ok(())
} else {
Err(cook_unavailable_with_message_error())
}
}
fn find_repo_binary_with_hint(repo_dir: &Path) -> Option<PathBuf> { fn find_repo_binary_with_hint(repo_dir: &Path) -> Option<PathBuf> {
let cwd = std::env::current_dir().ok(); let cwd = std::env::current_dir().ok();
find_repo_binary_from(std::env::var_os("PATH"), cwd.as_deref(), Some(repo_dir)) find_repo_binary_from(std::env::var_os("PATH"), cwd.as_deref(), Some(repo_dir))
} }
fn cook_unavailable_with_message_error() -> CubError {
CubError::BuildFailed(cook_with_setup_instructions())
}
fn find_repo_binary_from( fn find_repo_binary_from(
path_env: Option<OsString>, path_env: Option<OsString>,
cwd: Option<&Path>, cwd: Option<&Path>,
@@ -244,7 +296,10 @@ fn render_command_failure(stdout: &[u8], stderr: &[u8]) -> String {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{find_repo_binary, is_repo_available}; use super::{
cook_available_with_message, cook_or_skip, cook_with_setup_instructions,
find_repo_binary, is_repo_available, CookStrategy,
};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock}; use std::sync::{Mutex, OnceLock};
@@ -356,6 +411,68 @@ mod tests {
assert!(!is_repo_available()); assert!(!is_repo_available());
} }
#[test]
fn test_cook_or_skip_returns_none_with_skip_strategy() {
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);
let result = cook_or_skip(
&temp.path().join("recipe"),
"x86_64-unknown-redox",
&temp.path().join("repo-root"),
CookStrategy::Skip,
)
.expect("skip strategy should succeed");
assert_eq!(result, None);
}
#[test]
fn test_cook_or_skip_returns_none_with_autodetect_when_repo_missing() {
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);
let result = cook_or_skip(
&temp.path().join("recipe"),
"x86_64-unknown-redox",
&temp.path().join("repo-root"),
CookStrategy::AutoDetect,
)
.expect("autodetect strategy should succeed when repo is missing");
assert_eq!(result, None);
}
#[test]
fn test_cook_available_with_message_returns_instructions() {
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);
let error = cook_available_with_message().expect_err("repo should be unavailable");
let message = error.to_string();
assert!(message.contains("Build tools are not available in this environment"));
assert!(message.contains("The Red Bear OS build toolchain"));
assert!(message.contains("'repo' cookbook binary"));
}
#[test]
fn test_cook_with_setup_instructions_is_nonempty() {
assert!(!cook_with_setup_instructions().trim().is_empty());
}
fn create_executable(path: &Path) { fn create_executable(path: &Path) {
fs::write(path, b"#!/bin/sh\nexit 0\n").expect("write repo binary"); fs::write(path, b"#!/bin/sh\nexit 0\n").expect("write repo binary");