This commit is contained in:
2026-05-20 00:30:38 +02:00
parent 312750c61b
commit 16d81f509f
36 changed files with 13292 additions and 273 deletions
+172 -29
View File
@@ -26,6 +26,36 @@ use crate::{
recipe::{GitSource, OutputPackage, Recipe, RecipeKind, RecipeSet, Source},
};
const SPLIT_SUBPACKAGE_SCRIPT: &str = r#"
base=$1
dest=$2
package=$3
shift 3
mkdir -p "$dest"
for pattern do
matches=$(mktemp)
find "$base" -depth -path "$base/$pattern" -print > "$matches"
if ! [ -s "$matches" ]; then
echo "subpackage $package: glob '$pattern' matched no files under $base" >&2
rm -f "$matches"
exit 1
fi
while IFS= read -r path; do
[ "$path" != "$base" ] || continue
rel=${path#"$base"/}
target=$dest/$rel
mkdir -p "$(dirname "$target")"
echo "$rel"
mv "$path" "$target"
done < "$matches"
rm -f "$matches"
done
find "$base" -depth -type d -empty -delete
"#;
#[derive(Debug)]
pub struct Builder {
root: PathBuf,
@@ -52,7 +82,7 @@ impl Builder {
let requested = filter_skipped(recipes, requested);
let plan = TaskPlanner::new(&self.root, &self.config.arch, recipes)
.build_plan(&requested, rebuild)?;
self.print_plan(&plan);
self.print_plan(recipes, &plan)?;
if dry_run {
return Ok(());
}
@@ -68,7 +98,7 @@ impl Builder {
let requested = filter_skipped(recipes, requested);
let plan =
TaskPlanner::new(&self.root, &self.config.arch, recipes).fetch_plan(&requested)?;
self.print_plan(&plan);
self.print_plan(recipes, &plan)?;
if dry_run {
return Ok(());
}
@@ -128,6 +158,11 @@ impl Builder {
let recipe = recipes.recipe(key)?;
self.task_prepare_sources(layout, recipe)
}
TaskId::PrepareRecipe(key) => {
let recipe = recipes.recipe(key)?;
let active = active.expect("recipe prepare task requires an active container");
self.task_prepare_recipe(layout, recipe, active)
}
TaskId::ConfigureRecipe(key) => {
let recipe = recipes.recipe(key)?;
let active = active.expect("configure task requires an active container");
@@ -206,7 +241,7 @@ impl Builder {
let mut tmp = tempfile::NamedTempFile::new_in(&cache_dir)
.with_context(|| format!("creating temp file in {}", cache_dir.display()))?;
let mut hasher = Sha256::new();
let mut buf = [0u8; 64 * 1024];
let mut buf: [u8; 65536] = [0u8; 64 * 1024];
loop {
let n = std::io::Read::read(&mut response, &mut buf)
.with_context(|| format!("reading response body for {url}"))?;
@@ -235,12 +270,6 @@ impl Builder {
})?;
}
if unknown {
bail!(
"{}: source sha256 is unknown; update the recipe with sha256 = \"{hash}\"",
recipe.key()
);
}
Ok(())
}
@@ -418,6 +447,22 @@ impl Builder {
Ok(())
}
fn task_prepare_recipe(
&self,
layout: &Layout<'_>,
recipe: &Recipe,
active: &ActiveContainer,
) -> anyhow::Result<()> {
let Some(func) = recipe.phases().prepare() else {
return Ok(());
};
log::step("prepare", &format!("{} (recipe phase)", recipe.key()));
let ctx = phase_context_for(recipe);
self.invoke_with_runtime(active, &[PhaseArg::Ctx(ctx)], func)?;
self.write_recipe_stamp(layout, recipe, "prepare")
}
fn task_configure(
&self,
layout: &Layout<'_>,
@@ -452,6 +497,10 @@ impl Builder {
active: &ActiveContainer,
) -> anyhow::Result<()> {
log::step("install", &output.key());
if !output.is_base() {
return self.task_split_subpackage(layout, recipe, output, active);
}
let dest = format!("/dest/{}", output.name());
active.container.borrow().exec(
&["mkdir".to_owned(), "-p".to_owned(), dest.clone()],
@@ -468,6 +517,36 @@ impl Builder {
self.write_output_stamp(layout, recipe, output, "install")
}
fn task_split_subpackage(
&self,
layout: &Layout<'_>,
recipe: &Recipe,
output: &OutputPackage,
active: &ActiveContainer,
) -> anyhow::Result<()> {
let base = recipe
.base_output()
.ok_or_else(|| anyhow::anyhow!("recipe `{}` has no base output", recipe.key()))?;
let base_dest = format!("/dest/{}", base.name());
let dest = format!("/dest/{}", output.name());
let mut argv: Vec<String> = vec![
"sh".to_owned(),
"-eu".to_owned(),
"-c".to_owned(),
SPLIT_SUBPACKAGE_SCRIPT.to_owned(),
"split-subpackage".to_owned(),
base_dest,
dest,
output.name().to_owned(),
];
argv.extend(output.file_globs().iter().cloned());
active
.container
.borrow()
.exec(&argv, &base_env(&active.base_path), "/")?;
self.write_output_stamp(layout, recipe, output, "install")
}
fn task_install_host(
&self,
layout: &Layout<'_>,
@@ -594,6 +673,10 @@ impl Builder {
}
let env = base_env(&active.base_path);
log::step(
"sysroot",
&format!("{} from {} target apk(s)", recipe.key(), deps.len()),
);
for dep in deps {
let output = recipes.output(dep)?;
let owning = recipes.recipe(output.recipe())?;
@@ -616,6 +699,7 @@ impl Builder {
"apk".to_owned(),
"extract".to_owned(),
"--allow-untrusted".to_owned(),
"--force-overwrite".to_owned(),
"--destination".to_owned(),
"/sysroot".to_owned(),
format!("/pkgs/{file_name}"),
@@ -693,10 +777,10 @@ impl Builder {
let bare = dep_key.strip_prefix("host:").unwrap_or(dep_key);
mounts.push(Mount {
host: install,
container: format!("/tools/{bare}/usr/local"),
container: format!("/tools/{bare}"),
read_only: true,
});
tools_bins.push(format!("/tools/{bare}/usr/local/bin"));
tools_bins.push(format!("/tools/{bare}/bin"));
}
let name = format!(
@@ -837,23 +921,54 @@ impl Builder {
Ok(hex::encode(hasher.finalize()))
}
fn print_plan(&self, plan: &TaskPlan) {
fn print_plan(&self, recipes: &RecipeSet, plan: &TaskPlan) -> anyhow::Result<()> {
if plan.is_empty() {
log::skip("plan", "nothing to do");
return;
return Ok(());
}
let task_count = plan.order().len();
let edge_count = plan.dependency_count();
log::step(
"plan",
&format!(
"{} active task(s), {} edge(s)",
plan.order().len(),
plan.dependency_count()
"{} {}, {} {}",
task_count,
plural(task_count, "task", "tasks"),
edge_count,
plural(edge_count, "dependency edge", "dependency edges")
),
);
for task in plan.order() {
println!("{task}");
let step_width = task_count.to_string().len() * 2 + 1;
let action_width = plan
.order()
.iter()
.map(|task| plan_task_parts(task).0.len())
.max()
.unwrap_or(0);
let mut current_recipe: Option<String> = None;
for (index, task) in plan.order().iter().enumerate() {
let recipe_slug = task_recipe_slug(task, recipes)?;
if current_recipe.as_deref() != Some(recipe_slug.as_str()) {
if current_recipe.is_some() {
println!();
}
println!(" {recipe_slug}");
current_recipe = Some(recipe_slug.clone());
}
let step = format!("{}/{}", index + 1, task_count);
let (action, target) = plan_task_parts(task);
if target == recipe_slug {
println!(" {step:>step_width$} {action}");
} else {
println!(" {step:>step_width$} {action:<action_width$} {target}");
}
}
Ok(())
}
fn abs_config_path(&self, path: &Path) -> PathBuf {
@@ -874,14 +989,7 @@ struct ActiveContainer {
fn filter_skipped(recipes: &RecipeSet, requested: &[String]) -> Vec<String> {
requested
.iter()
.filter(|key| {
if recipes.is_skipped(key) {
log::skip("skip", &format!("{key} (build_if returned false)"));
false
} else {
true
}
})
.filter(|key| !recipes.is_skipped(key))
.cloned()
.collect()
}
@@ -894,9 +1002,27 @@ fn task_needs_container(task: &TaskId) -> bool {
| TaskId::InstallPackageFiles(_)
| TaskId::ProduceApk(_)
| TaskId::InstallHostRecipe(_)
| TaskId::PrepareRecipe(_)
)
}
fn plan_task_parts(task: &TaskId) -> (&'static str, &str) {
match task {
TaskId::FetchSources(recipe) => ("fetch sources", recipe),
TaskId::PrepareSources(recipe) => ("prepare sources", recipe),
TaskId::PrepareRecipe(recipe) => ("recipe prepare", recipe),
TaskId::ConfigureRecipe(recipe) => ("configure", recipe),
TaskId::BuildRecipe(recipe) => ("build", recipe),
TaskId::InstallPackageFiles(output) => ("install files", output),
TaskId::ProduceApk(output) => ("produce apk", output),
TaskId::InstallHostRecipe(recipe) => ("install host", recipe),
}
}
fn plural<'a>(count: usize, singular: &'a str, plural: &'a str) -> &'a str {
if count == 1 { singular } else { plural }
}
fn prefix_for(kind: RecipeKind) -> &'static str {
match kind {
RecipeKind::Package => "/usr",
@@ -937,13 +1063,30 @@ fn default_jobs() -> i32 {
.unwrap_or(1)
}
const TERMINAL_ENV_VARS: &[&str] = &[
"TERM",
"COLORTERM",
"NO_COLOR",
"CLICOLOR",
"CLICOLOR_FORCE",
"FORCE_COLOR",
"TERM_PROGRAM",
"TERM_PROGRAM_VERSION",
];
fn bare_env() -> Vec<(String, String)> {
vec![
let mut env = vec![
("PATH".to_owned(), String::new()),
("HOME".to_owned(), "/tmp".to_owned()),
("TERM".to_owned(), "dumb".to_owned()),
("LC_ALL".to_owned(), "C".to_owned()),
]
];
env.extend(TERMINAL_ENV_VARS.iter().filter_map(|key| {
std::env::var(key)
.ok()
.filter(|value| !value.is_empty())
.map(|value| ((*key).to_owned(), value))
}));
env
}
fn base_env(path: &str) -> Vec<(String, String)> {