This commit is contained in:
2026-05-18 20:39:21 +02:00
parent 0d610fd2de
commit 0c9a3fde94
9 changed files with 930 additions and 22 deletions
@@ -1,23 +1,22 @@
name = "gcc"
version = "16.1.0" version = "16.1.0"
revision = 1 revision = 1
description = "GNU GCC cross-compiler (bootstrap stage, C/C++ only)" metadata = meta(
license = "GPL-3.0-or-later" description = "GNU GCC cross-compiler (bootstrap stage, C/C++ only)",
license = "GPL-3.0-or-later",
source = { )
"url": f"https://ftp.gnu.org/gnu/gcc/gcc-{version}/gcc-{version}.tar.xz", source = tarball_source(
"sha256": "50efb4d94c3397aff3b0d61a5abd748b4dd31d9d3f2ab7be05b171d36a510f79", url = f"https://ftp.gnu.org/gnu/gcc/gcc-{version}/gcc-{version}.tar.xz",
"strip_components": 1, sha256 = "50efb4d94c3397aff3b0d61a5abd748b4dd31d9d3f2ab7be05b171d36a510f79",
} strip_components = 1,
)
host_deps = ["binutils"] host_deps = ["binutils"]
def configure(ctx): def configure(ctx):
ctx.run([ ctx.run([
ctx.source_dir + "/configure", ctx.source_dir + "/configure",
"--target=" + options.target_triple,
"--prefix=" + ctx.prefix, "--prefix=" + ctx.prefix,
"--target=" + OPTIONS.target_triple, "--with-sysroot=" + ctx.sysroot,
"--with-sysroot=" + ctx.prefix + "/" + OPTIONS.target_triple,
"--without-headers", "--without-headers",
"--with-newlib", "--with-newlib",
"--enable-languages=c,c++", "--enable-languages=c,c++",
@@ -33,9 +32,9 @@ def configure(ctx):
"--disable-libvtv", "--disable-libvtv",
"--disable-multilib", "--disable-multilib",
], env = { ], env = {
"CFLAGS": OPTIONS.host_cflags, "CFLAGS": options.host_cflags,
"CXXFLAGS": OPTIONS.host_cxxflags, "CXXFLAGS": options.host_cxxflags,
"LDFLAGS": OPTIONS.host_ldflags, "LDFLAGS": options.host_ldflags,
}) })
def build(ctx): def build(ctx):
@@ -44,5 +43,5 @@ def build(ctx):
ctx.run(["make", jobs, "all-target-libgcc"]) ctx.run(["make", jobs, "all-target-libgcc"])
def install(ctx, pkg): def install(ctx, pkg):
ctx.run(["make", "DESTDIR=" + pkg.destdir, "install-gcc"]) ctx.run(["make", "install-gcc"], env = {"DESTDIR": pkg.dest_dir})
ctx.run(["make", "DESTDIR=" + pkg.destdir, "install-target-libgcc"]) ctx.run(["make", "install-target-libgcc"], env = {"DESTDIR": pkg.dest_dir})
+178 -1
View File
@@ -1 +1,178 @@
pub struct Builder; use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
use anyhow::{Context, bail};
use sha2::{Digest, Sha256};
use crate::{
config::Config,
graph::{TaskPlan, TaskPlanner},
log,
recipe::RecipeSet,
};
#[derive(Debug)]
pub struct Builder {
root: PathBuf,
config: Config,
container_ready: bool,
}
impl Builder {
pub fn new(root: PathBuf, config: Config) -> Self {
Self {
root,
config,
container_ready: false,
}
}
pub fn build(
&mut self,
recipes: &RecipeSet,
requested: &[String],
rebuild: bool,
dry_run: bool,
) -> anyhow::Result<()> {
let plan = TaskPlanner::new(&self.root, &self.config.arch, recipes)
.build_plan(requested, rebuild)?;
self.print_plan(&plan);
if !dry_run {
bail!("task execution is not implemented yet");
}
Ok(())
}
pub fn fetch(
&mut self,
recipes: &RecipeSet,
requested: &[String],
dry_run: bool,
) -> anyhow::Result<()> {
let plan =
TaskPlanner::new(&self.root, &self.config.arch, recipes).fetch_plan(requested)?;
self.print_plan(&plan);
if !dry_run {
bail!("task execution is not implemented yet");
}
Ok(())
}
pub fn ensure_container_ready(&mut self) -> anyhow::Result<()> {
if !self.container_ready {
self.ensure_container_image(&self.abs_config_path(&self.config.container_dockerfile))?;
self.container_ready = true;
}
Ok(())
}
fn ensure_container_image(&self, dockerfile: &Path) -> anyhow::Result<()> {
if !dockerfile.exists() {
bail!(
"configured container Dockerfile does not exist: {}",
dockerfile.display()
);
}
let hash = self.container_build_hash(dockerfile)?;
let stamp = self.root.join("build/container-image.hash");
if fs::read_to_string(&stamp).ok().as_deref() == Some(hash.as_str())
&& self.container_image_exists()?
{
log::skip(
"image",
&format!("using cached {}", self.config.container_image),
);
return Ok(());
}
log::step(
"image",
&format!(
"building {} from {}",
self.config.container_image,
dockerfile.display()
),
);
let runtime = self.config.container_runtime.as_str();
let status = Command::new(runtime)
.arg("build")
.arg("-f")
.arg(dockerfile)
.arg("-t")
.arg(&self.config.container_image)
.arg(&self.root)
.status()
.with_context(|| {
format!(
"failed to build container image `{}` from {}",
self.config.container_image,
dockerfile.display()
)
})?;
if !status.success() {
bail!(
"container image build failed for `{}` with {status}",
self.config.container_image
);
}
fs::create_dir_all(stamp.parent().unwrap())?;
fs::write(stamp, hash)?;
Ok(())
}
fn container_image_exists(&self) -> anyhow::Result<bool> {
let runtime = self.config.container_runtime.as_str();
let status = Command::new(runtime)
.arg("image")
.arg("exists")
.arg(&self.config.container_image)
.status()
.with_context(|| {
format!(
"failed to inspect container image `{}`",
self.config.container_image
)
})?;
Ok(status.success())
}
fn container_build_hash(&self, dockerfile: &Path) -> anyhow::Result<String> {
let mut hasher = Sha256::new();
hasher.update(fs::read(dockerfile)?);
hasher.update(self.config.container_image.as_bytes());
Ok(hex::encode(hasher.finalize()))
}
fn print_plan(&self, plan: &TaskPlan) {
if plan.is_empty() {
log::skip("plan", "nothing to do");
return;
}
log::step(
"plan",
&format!(
"{} active task(s), {} edge(s)",
plan.order().len(),
plan.dependency_count()
),
);
for task in plan.order() {
println!("{task}");
}
}
fn abs_config_path(&self, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
self.root.join(path)
}
}
}
+18 -4
View File
@@ -46,6 +46,8 @@ struct BuildCommand {
enum Command { enum Command {
Fetch(FetchCommand), Fetch(FetchCommand),
Build(BuildCommand), Build(BuildCommand),
#[command(about = "Create or refresh the configured build container image")]
Image,
} }
pub fn run() -> anyhow::Result<()> { pub fn run() -> anyhow::Result<()> {
@@ -53,11 +55,23 @@ pub fn run() -> anyhow::Result<()> {
let root_path = cli.root.canonicalize().unwrap_or(cli.root); let root_path = cli.root.canonicalize().unwrap_or(cli.root);
let config = Config::load(&root_path.join("config.star"))?; let config = Config::load(&root_path.join("config.star"))?;
let lib = eval::eval_lib(&root_path.join("lib"), Some(&config.options))?;
let recipes = RecipeSet::load(&root_path, &config.options, lib.as_ref())?;
match cli.command { match cli.command {
Command::Fetch { .. } => todo!(), Command::Fetch(command) => {
Command::Build { .. } => todo!(), let lib = eval::eval_lib(&root_path.join("lib"), Some(&config.options))?;
let recipes = RecipeSet::load(&root_path, &config.options, lib.as_ref())?;
let mut builder = Builder::new(root_path, config);
builder.fetch(&recipes, &command.recipes, command.dry_run)
}
Command::Build(command) => {
let lib = eval::eval_lib(&root_path.join("lib"), Some(&config.options))?;
let recipes = RecipeSet::load(&root_path, &config.options, lib.as_ref())?;
let mut builder = Builder::new(root_path, config);
builder.build(&recipes, &command.recipes, command.rebuild, command.dry_run)
}
Command::Image => {
let mut builder = Builder::new(root_path, config);
builder.ensure_container_ready()
}
} }
} }
+17
View File
@@ -12,14 +12,31 @@ use crate::{
#[derive(Debug)] #[derive(Debug)]
pub enum ContainerRuntime { pub enum ContainerRuntime {
Docker,
Podman, Podman,
} }
impl ContainerRuntime {
pub fn as_str(&self) -> &'static str {
match self {
Self::Docker => "docker",
Self::Podman => "podman",
}
}
}
impl std::fmt::Display for ContainerRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl TryFrom<&str> for ContainerRuntime { impl TryFrom<&str> for ContainerRuntime {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(value: &str) -> anyhow::Result<Self> { fn try_from(value: &str) -> anyhow::Result<Self> {
match value { match value {
"docker" => Ok(Self::Docker),
"podman" => Ok(Self::Podman), "podman" => Ok(Self::Podman),
_ => anyhow::bail!("invalid runtime: {value}"), _ => anyhow::bail!("invalid runtime: {value}"),
} }
+552
View File
@@ -0,0 +1,552 @@
use std::{
collections::{BTreeMap, BTreeSet},
fmt, fs,
path::{Path, PathBuf},
};
use anyhow::{Context, bail};
use sha2::{Digest, Sha256};
use crate::recipe::{OutputPackage, Recipe, RecipeKind, RecipeSet};
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum TaskId {
FetchSources(String),
PrepareSources(String),
ConfigureRecipe(String),
BuildRecipe(String),
InstallPackageFiles(String),
ProduceApk(String),
InstallHostRecipe(String),
}
impl fmt::Display for TaskId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::FetchSources(recipe) => write!(f, "fetch sources {recipe}"),
Self::PrepareSources(recipe) => write!(f, "prepare sources {recipe}"),
Self::ConfigureRecipe(recipe) => write!(f, "configure {recipe}"),
Self::BuildRecipe(recipe) => write!(f, "build {recipe}"),
Self::InstallPackageFiles(output) => write!(f, "install package files {output}"),
Self::ProduceApk(output) => write!(f, "produce apk {output}"),
Self::InstallHostRecipe(recipe) => write!(f, "install host recipe {recipe}"),
}
}
}
#[derive(Debug)]
pub struct TaskPlan {
dependencies: BTreeMap<TaskId, Vec<TaskId>>,
order: Vec<TaskId>,
}
impl TaskPlan {
pub fn order(&self) -> &[TaskId] {
&self.order
}
pub fn is_empty(&self) -> bool {
self.order.is_empty()
}
pub fn dependency_count(&self) -> usize {
self.dependencies.values().map(Vec::len).sum()
}
#[cfg(test)]
pub fn dependencies(&self, task: &TaskId) -> Option<&[TaskId]> {
self.dependencies.get(task).map(Vec::as_slice)
}
}
pub struct TaskPlanner<'a> {
root: &'a Path,
arch: &'a str,
recipes: &'a RecipeSet,
force: bool,
dependencies: BTreeMap<TaskId, Vec<TaskId>>,
inactive: BTreeSet<TaskId>,
visiting: BTreeSet<TaskId>,
visited: BTreeSet<TaskId>,
}
impl<'a> TaskPlanner<'a> {
pub fn new(root: &'a Path, arch: &'a str, recipes: &'a RecipeSet) -> Self {
Self {
root,
arch,
recipes,
force: false,
dependencies: BTreeMap::new(),
inactive: BTreeSet::new(),
visiting: BTreeSet::new(),
visited: BTreeSet::new(),
}
}
pub fn build_plan(mut self, requests: &[String], force: bool) -> anyhow::Result<TaskPlan> {
self.force = force;
for request in requests {
let recipe = self.recipes.recipe(request)?;
match recipe.kind() {
RecipeKind::Package => {
for output in recipe.outputs() {
self.visit(TaskId::ProduceApk(output.key()))?;
}
}
RecipeKind::HostPackage => {
self.visit(TaskId::InstallHostRecipe(recipe.key()))?;
}
}
}
self.into_plan()
}
pub fn fetch_plan(mut self, requests: &[String]) -> anyhow::Result<TaskPlan> {
for request in requests {
let recipe = self.recipes.recipe(request)?;
self.visit(TaskId::FetchSources(recipe.key()))?;
}
self.into_plan()
}
fn visit(&mut self, task: TaskId) -> anyhow::Result<()> {
if self.visited.contains(&task) || self.inactive.contains(&task) {
return Ok(());
}
if !self.is_active(&task)? {
self.inactive.insert(task);
return Ok(());
}
if !self.visiting.insert(task.clone()) {
bail!("task dependency cycle involving `{task}`");
}
let dependencies = self.dependencies(&task)?;
let mut active_dependencies = Vec::new();
for dependency in dependencies {
self.visit(dependency.clone())?;
if self.dependencies.contains_key(&dependency) {
active_dependencies.push(dependency);
}
}
self.visiting.remove(&task);
self.visited.insert(task.clone());
self.dependencies.insert(task, active_dependencies);
Ok(())
}
fn into_plan(self) -> anyhow::Result<TaskPlan> {
let mut order = Vec::new();
let mut visiting = BTreeSet::new();
let mut visited = BTreeSet::new();
for task in self.dependencies.keys() {
topo_visit(
task,
&self.dependencies,
&mut visiting,
&mut visited,
&mut order,
)?;
}
Ok(TaskPlan {
dependencies: self.dependencies,
order,
})
}
fn dependencies(&self, task: &TaskId) -> anyhow::Result<Vec<TaskId>> {
match task {
TaskId::FetchSources(_) => Ok(Vec::new()),
TaskId::PrepareSources(recipe) => Ok(vec![TaskId::FetchSources(recipe.clone())]),
TaskId::ConfigureRecipe(recipe) => {
let recipe = self.recipes.recipe(recipe)?;
let mut deps = vec![TaskId::PrepareSources(recipe.key())];
deps.extend(
recipe
.host_deps()
.iter()
.map(|dep| TaskId::InstallHostRecipe(RecipeKind::HostPackage.key(dep))),
);
deps.extend(
recipe
.build_deps()
.iter()
.chain(recipe.deps().iter())
.map(|dep| TaskId::ProduceApk(dep.clone())),
);
Ok(deps)
}
TaskId::BuildRecipe(recipe) => Ok(vec![TaskId::ConfigureRecipe(recipe.clone())]),
TaskId::InstallPackageFiles(output) => {
let output = self.recipes.output(output)?;
Ok(vec![TaskId::BuildRecipe(output.recipe().to_owned())])
}
TaskId::ProduceApk(output) => {
let output = self.recipes.output(output)?;
let recipe = self.recipes.recipe(output.recipe())?;
let mut deps = vec![TaskId::InstallPackageFiles(output.key())];
deps.extend(
recipe
.deps()
.iter()
.chain(recipe.run_deps().iter())
.map(|dep| TaskId::ProduceApk(dep.clone())),
);
Ok(deps)
}
TaskId::InstallHostRecipe(recipe) => {
self.recipes.recipe(recipe)?;
Ok(vec![TaskId::BuildRecipe(recipe.clone())])
}
}
}
fn is_active(&self, task: &TaskId) -> anyhow::Result<bool> {
match task {
TaskId::FetchSources(recipe) => self.fetch_sources_active(self.recipes.recipe(recipe)?),
TaskId::PrepareSources(recipe) => {
self.prepare_sources_active(self.recipes.recipe(recipe)?)
}
TaskId::ConfigureRecipe(recipe) => {
self.recipe_task_active(self.recipes.recipe(recipe)?, "configure")
}
TaskId::BuildRecipe(recipe) => {
self.recipe_task_active(self.recipes.recipe(recipe)?, "build")
}
TaskId::InstallPackageFiles(output) => {
let output = self.recipes.output(output)?;
let recipe = self.recipes.recipe(output.recipe())?;
self.output_task_active(recipe, output, "install")
}
TaskId::ProduceApk(output) => {
let output = self.recipes.output(output)?;
let recipe = self.recipes.recipe(output.recipe())?;
self.produce_apk_active(recipe, output)
}
TaskId::InstallHostRecipe(recipe) => {
let recipe = self.recipes.recipe(recipe)?;
self.install_host_recipe_active(recipe)
}
}
}
fn fetch_sources_active(&self, recipe: &Recipe) -> anyhow::Result<bool> {
Ok(recipe.sources().entries().iter().any(|(_, source)| {
source.is_unknown_cache_key() || !self.source_cache_path(source.cache_key()).exists()
}))
}
fn prepare_sources_active(&self, recipe: &Recipe) -> anyhow::Result<bool> {
if self.force {
return Ok(true);
}
let want_version = format!("{}-r{}", recipe.version(), recipe.revision());
if fs::read_to_string(self.source_stamp(recipe, "version"))
.ok()
.as_deref()
!= Some(want_version.as_str())
{
return Ok(true);
}
if self.recipe_has_patches(recipe)? && !self.source_stamp(recipe, "patched").exists() {
return Ok(true);
}
Ok(false)
}
fn recipe_task_active(&self, recipe: &Recipe, kind: &str) -> anyhow::Result<bool> {
if self.force {
return Ok(true);
}
Ok(fs::read_to_string(self.recipe_task_stamp(recipe, kind))
.ok()
.as_deref()
!= Some(self.recipe_fingerprint(recipe)?.as_str()))
}
fn output_task_active(
&self,
recipe: &Recipe,
output: &OutputPackage,
kind: &str,
) -> anyhow::Result<bool> {
if self.force {
return Ok(true);
}
Ok(fs::read_to_string(self.output_task_stamp(output, kind))
.ok()
.as_deref()
!= Some(self.output_fingerprint(recipe, output)?.as_str()))
}
fn produce_apk_active(&self, recipe: &Recipe, output: &OutputPackage) -> anyhow::Result<bool> {
if self.force {
return Ok(true);
}
if !self.apk_path(recipe, output).exists() {
return Ok(true);
}
Ok(fs::read_to_string(self.output_task_stamp(output, "apk"))
.ok()
.as_deref()
!= Some(self.output_fingerprint(recipe, output)?.as_str()))
}
fn install_host_recipe_active(&self, recipe: &Recipe) -> anyhow::Result<bool> {
if self.force {
return Ok(true);
}
if !self.host_install_dir(recipe).exists() {
return Ok(true);
}
Ok(
fs::read_to_string(self.recipe_task_stamp(recipe, "host-install"))
.ok()
.as_deref()
!= Some(self.recipe_fingerprint(recipe)?.as_str()),
)
}
fn recipe_fingerprint(&self, recipe: &Recipe) -> anyhow::Result<String> {
let mut hasher = Sha256::new();
hasher.update(self.arch.as_bytes());
hasher.update(recipe.key().as_bytes());
hasher.update(recipe.version().as_bytes());
hasher.update(recipe.revision().to_le_bytes());
hasher.update(
fs::read(recipe.path())
.with_context(|| format!("reading recipe {}", recipe.path().display()))?,
);
for (name, source) in recipe.sources().entries() {
hasher.update(name.unwrap_or("").as_bytes());
hasher.update(source.url().as_bytes());
hasher.update(source.cache_key().as_bytes());
}
for patch in self.recipe_patches(recipe)? {
hasher.update(patch.display().to_string().as_bytes());
hasher
.update(fs::read(&patch).with_context(|| format!("reading {}", patch.display()))?);
}
Ok(hex::encode(hasher.finalize()))
}
fn output_fingerprint(
&self,
recipe: &Recipe,
output: &OutputPackage,
) -> anyhow::Result<String> {
let mut hasher = Sha256::new();
hasher.update(self.recipe_fingerprint(recipe)?.as_bytes());
hasher.update(output.key().as_bytes());
Ok(hex::encode(hasher.finalize()))
}
fn recipe_has_patches(&self, recipe: &Recipe) -> anyhow::Result<bool> {
Ok(!self.recipe_patches(recipe)?.is_empty())
}
fn recipe_patches(&self, recipe: &Recipe) -> anyhow::Result<Vec<PathBuf>> {
let patches_dir = recipe.dir().join("patches");
if !patches_dir.exists() {
return Ok(Vec::new());
}
let mut patches = Vec::new();
for entry in fs::read_dir(&patches_dir)
.with_context(|| format!("reading patches directory {}", patches_dir.display()))?
{
let entry = entry?;
let path = entry.path();
if path.is_file() {
patches.push(path);
}
}
patches.sort();
Ok(patches)
}
fn source_cache_path(&self, key: &str) -> PathBuf {
self.root.join("build/cache/sources").join(key)
}
fn source_stamp(&self, recipe: &Recipe, kind: &str) -> PathBuf {
self.root
.join("build/sources")
.join(format!("{}.{kind}", recipe.slug()))
}
fn recipe_task_stamp(&self, recipe: &Recipe, kind: &str) -> PathBuf {
self.root
.join("build/tasks")
.join(format!("{}.{kind}", recipe.slug()))
}
fn output_task_stamp(&self, output: &OutputPackage, kind: &str) -> PathBuf {
self.root
.join("build/tasks")
.join(format!("{}.{kind}", output.key().replace(':', "-")))
}
fn apk_path(&self, recipe: &Recipe, output: &OutputPackage) -> PathBuf {
self.root.join("build/pkgs").join(self.arch).join(format!(
"{}-{}-r{}.apk",
output.name(),
recipe.version(),
recipe.revision()
))
}
fn host_install_dir(&self, recipe: &Recipe) -> PathBuf {
self.root
.join("build/host-pkgs")
.join(recipe.slug())
.join("usr/local")
}
}
fn topo_visit(
task: &TaskId,
dependencies: &BTreeMap<TaskId, Vec<TaskId>>,
visiting: &mut BTreeSet<TaskId>,
visited: &mut BTreeSet<TaskId>,
order: &mut Vec<TaskId>,
) -> anyhow::Result<()> {
if visited.contains(task) {
return Ok(());
}
if !visiting.insert(task.clone()) {
bail!("task dependency cycle involving `{task}`");
}
for dependency in dependencies.get(task).into_iter().flatten() {
topo_visit(dependency, dependencies, visiting, visited, order)?;
}
visiting.remove(task);
visited.insert(task.clone());
order.push(task.clone());
Ok(())
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::TempDir;
use crate::{config::Config, eval, recipe::RecipeSet};
use super::{TaskId, TaskPlanner};
fn write_config(root: &TempDir) {
fs::write(
root.path().join("config.star"),
r#"
container_runtime = "podman"
container_image = "local/test:latest"
container_dockerfile = "Dockerfile"
arch = "x86_64"
options = dict(target_arch = arch)
"#,
)
.unwrap();
fs::write(root.path().join("Dockerfile"), "FROM scratch\n").unwrap();
}
fn write_recipe(root: &TempDir, dir: &str, name: &str, extra: &str) {
let recipe_dir = root.path().join(dir);
fs::create_dir_all(&recipe_dir).unwrap();
fs::write(
recipe_dir.join(format!("{name}.star")),
format!(
r#"
version = "1.0"
revision = 1
source = tarball_source(url = "file:///tmp/{name}.tar", sha256 = "hash-{name}")
{extra}
def build(ctx):
ctx.run(["true"])
def install(ctx, pkg):
ctx.run(["true"])
"#
),
)
.unwrap();
}
fn load(root: &TempDir) -> (Config, RecipeSet) {
let config = Config::load(&root.path().join("config.star")).unwrap();
let lib = eval::eval_lib(&root.path().join("lib"), Some(&config.options)).unwrap();
let recipes = RecipeSet::load(root.path(), &config.options, lib.as_ref()).unwrap();
(config, recipes)
}
#[test]
fn inactive_seed_does_not_pull_dependencies() {
let root = TempDir::new().unwrap();
write_config(&root);
write_recipe(&root, "recipes", "dep", "");
write_recipe(&root, "recipes", "app", r#"deps = ["dep"]"#);
let (config, recipes) = load(&root);
let planner = TaskPlanner::new(root.path(), &config.arch, &recipes);
let output = recipes.output("app").unwrap();
let recipe = recipes.recipe(output.recipe()).unwrap();
fs::create_dir_all(root.path().join("build/pkgs/x86_64")).unwrap();
fs::write(root.path().join("build/pkgs/x86_64/app-1.0-r1.apk"), "").unwrap();
fs::create_dir_all(root.path().join("build/tasks")).unwrap();
fs::write(
root.path().join("build/tasks/app.apk"),
planner.output_fingerprint(recipe, output).unwrap(),
)
.unwrap();
let plan = TaskPlanner::new(root.path(), &config.arch, &recipes)
.build_plan(&["app".to_owned()], false)
.unwrap();
assert!(plan.is_empty());
}
#[test]
fn active_target_recipe_keeps_edges_and_topo_order() {
let root = TempDir::new().unwrap();
write_config(&root);
write_recipe(&root, "recipes", "app", "");
let (config, recipes) = load(&root);
let plan = TaskPlanner::new(root.path(), &config.arch, &recipes)
.build_plan(&["app".to_owned()], false)
.unwrap();
assert_eq!(
plan.order(),
&[
TaskId::FetchSources("app".to_owned()),
TaskId::PrepareSources("app".to_owned()),
TaskId::ConfigureRecipe("app".to_owned()),
TaskId::BuildRecipe("app".to_owned()),
TaskId::InstallPackageFiles("app".to_owned()),
TaskId::ProduceApk("app".to_owned()),
]
);
assert_eq!(
plan.dependencies(&TaskId::ProduceApk("app".to_owned())),
Some([TaskId::InstallPackageFiles("app".to_owned())].as_slice())
);
}
#[test]
fn host_dependencies_are_installed_as_host_recipes() {
let root = TempDir::new().unwrap();
write_config(&root);
write_recipe(&root, "host-recipes", "binutils", "");
write_recipe(&root, "recipes", "app", r#"host_deps = ["binutils"]"#);
let (config, recipes) = load(&root);
let plan = TaskPlanner::new(root.path(), &config.arch, &recipes)
.build_plan(&["app".to_owned()], false)
.unwrap();
assert!(
plan.order()
.contains(&TaskId::InstallHostRecipe("host:binutils".to_owned()))
);
}
}
+26
View File
@@ -0,0 +1,26 @@
use std::{io::IsTerminal, sync::LazyLock};
static IS_STDERR_TERMINAL: LazyLock<bool> = LazyLock::new(|| std::io::stderr().is_terminal());
const ARROW: &str = "==>";
fn emit(color: &str, action: &str, details: &str) {
if *IS_STDERR_TERMINAL {
eprintln!("\x1b[{color}m{ARROW} \x1b[1m{action} \x1b[0m{details}");
} else {
eprintln!("{ARROW} {action} {details}");
}
}
pub fn step(action: &str, details: &str) {
emit("1;34", action, details);
}
pub fn skip(action: &str, details: &str) {
emit("1;33", action, details);
}
#[allow(dead_code)]
pub fn info(action: &str, details: &str) {
emit("1;32", action, details);
}
+2
View File
@@ -2,6 +2,8 @@ mod builder;
mod cli; mod cli;
mod config; mod config;
mod eval; mod eval;
mod graph;
mod log;
mod options; mod options;
mod recipe; mod recipe;
+101
View File
@@ -36,6 +36,10 @@ impl RecipeKind {
Self::HostPackage => format!("host:{name}"), Self::HostPackage => format!("host:{name}"),
} }
} }
pub fn slug(self, name: &str) -> String {
self.key(name).replace(':', "-")
}
} }
#[derive(Debug)] #[derive(Debug)]
@@ -44,7 +48,25 @@ pub enum Sources {
Multiple(HashMap<String, source::Source>), Multiple(HashMap<String, source::Source>),
} }
impl Sources {
pub fn entries(&self) -> Vec<(Option<&str>, &source::Source)> {
match self {
Self::Single(source) => vec![(None, source)],
Self::Multiple(sources) => {
let mut entries = sources
.iter()
.map(|(name, source)| (Some(name.as_str()), source))
.collect::<Vec<_>>();
entries.sort_by_key(|(name, _)| *name);
entries
}
}
}
}
pub struct Recipe { pub struct Recipe {
/// Recipe name without namespace prefix.
name: String,
/// Path to the recipe's .star file. /// Path to the recipe's .star file.
path: PathBuf, path: PathBuf,
/// What kind of a recipe is that? /// What kind of a recipe is that?
@@ -173,6 +195,7 @@ impl Recipe {
let phases = RecipePhases::load(&module)?; let phases = RecipePhases::load(&module)?;
Ok(Recipe { Ok(Recipe {
name: name.to_owned(),
path: path.to_path_buf(), path: path.to_path_buf(),
kind, kind,
version, version,
@@ -190,6 +213,58 @@ impl Recipe {
pub fn phases(&self) -> &RecipePhases { pub fn phases(&self) -> &RecipePhases {
&self.phases &self.phases
} }
pub fn key(&self) -> String {
self.kind.key(&self.name)
}
pub fn slug(&self) -> String {
self.kind.slug(&self.name)
}
pub fn dir(&self) -> &Path {
self.path.parent().unwrap_or_else(|| Path::new("."))
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn kind(&self) -> RecipeKind {
self.kind
}
pub fn version(&self) -> &str {
&self.version
}
pub fn revision(&self) -> i32 {
self.revision
}
pub fn sources(&self) -> &Sources {
&self.sources
}
pub fn outputs(&self) -> &[OutputPackage] {
&self.outputs
}
pub fn host_deps(&self) -> &[String] {
&self.host_deps
}
pub fn build_deps(&self) -> &[String] {
&self.build_deps
}
pub fn deps(&self) -> &[String] {
&self.deps
}
pub fn run_deps(&self) -> &[String] {
&self.run_deps
}
} }
fn optional_string_list(module: &Module, key: &str) -> anyhow::Result<Vec<String>> { fn optional_string_list(module: &Module, key: &str) -> anyhow::Result<Vec<String>> {
@@ -281,6 +356,20 @@ pub struct OutputPackage {
metadata: Metadata, metadata: Metadata,
} }
impl OutputPackage {
pub fn key(&self) -> String {
self.name.clone()
}
pub fn recipe(&self) -> &str {
&self.recipe
}
pub fn name(&self) -> &str {
&self.name
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct RecipeSet { pub struct RecipeSet {
recipes: HashMap<String, Recipe>, recipes: HashMap<String, Recipe>,
@@ -329,6 +418,18 @@ impl RecipeSet {
Ok(Self { recipes, outputs }) Ok(Self { recipes, outputs })
} }
pub fn recipe(&self, key: &str) -> anyhow::Result<&Recipe> {
self.recipes
.get(key)
.ok_or_else(|| anyhow::anyhow!("unknown recipe `{key}`"))
}
pub fn output(&self, key: &str) -> anyhow::Result<&OutputPackage> {
self.outputs
.get(key)
.ok_or_else(|| anyhow::anyhow!("unknown output package `{key}`"))
}
} }
/// Find all recipe `.star` files under `dir`, returning a map of recipe name /// Find all recipe `.star` files under `dir`, returning a map of recipe name
+20
View File
@@ -89,3 +89,23 @@ starlark::starlark_simple_value!(Source);
#[starlark_value(type = "source")] #[starlark_value(type = "source")]
impl<'v> StarlarkValue<'v> for Source {} impl<'v> StarlarkValue<'v> for Source {}
impl Source {
pub fn url(&self) -> &str {
match self {
Self::Tarball(source) => source.url(),
Self::Git(source) => source.url(),
}
}
pub fn cache_key(&self) -> &str {
match self {
Self::Tarball(source) => source.sha256(),
Self::Git(source) => source.commit(),
}
}
pub fn is_unknown_cache_key(&self) -> bool {
matches!(self.cache_key(), "?" | "???")
}
}