meta: initial rewrite (final)

This commit is contained in:
2026-05-22 18:48:11 +02:00
commit a525868969
16 changed files with 3057 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
/target
/build
Generated
+1813
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "builder"
version = "0.1.0"
edition = "2024"
[dependencies]
allocative = "0.3.4"
anyhow = "1.0.102"
clap = { version = "4.6.1", features = ["derive"] }
petgraph = "0.8.3"
smallvec = "1.15.1"
starlark = "0.13.0"
starlark_derive = "0.13.0"
thiserror = "2.0.18"
+64
View File
@@ -0,0 +1,64 @@
arch = "x86_64"
libc = "glibc"
if libc == "glibc":
env = "gnu"
elif libc == "musl":
env = "musl"
else:
fail(f"Unknown libc: {libc}")
prefix = path("/usr")
host_cflags = ["-O2", "-pipe"]
host_cxxflags = host_cflags + []
host_ldflags = ["-Wl,-O1", "-Wl,--sort-common", "-Wl,--as-needed"]
target_cflags = host_cflags + []
target_cxxflags = host_cxxflags + []
target_ldflags = host_ldflags + ["-Wl,-z,now"]
if arch == "x86_64":
flags = [
"-march=x86-64-v3",
"-mtune=generic",
"-fstack-clash-protection",
"-fstack-protector-strong",
"-fcf-protection",
]
target_cflags += flags
target_cxxflags += flags
target_ldflags += ["-Wl,-z,pack-relative-relocs"]
config(
arch = arch,
recipes_dir = path("./recipes"),
host_recipes_dir = path("./host-recipes"),
container = podman(
image = "local/builder:latest",
dockerfile = path("./Dockerfile"),
),
target_arch = arch,
target_triple = f"{arch}-orchid-linux-{env}",
libc = libc,
prefix = prefix,
bindir = prefix / "bin",
sbindir = prefix / "bin",
libdir = prefix / "lib",
libexecdir = prefix / "libexec",
includedir = prefix / "include",
sysconfdir = path("/etc"),
localstatedir = path("/var"),
host_cflags = " ".join(host_cflags),
host_cxxflags = " ".join(host_cxxflags),
host_ldflags = " ".join(host_ldflags),
cflags = " ".join(target_cflags),
cxxflags = " ".join(target_cxxflags),
ldflags = " ".join(target_ldflags),
)
+39
View File
@@ -0,0 +1,39 @@
version = "2.46.0"
revision = 1
metadata = meta(
description = "GNU binutils cross-compiled for the target triple",
license = "GPL-3.0-or-later",
)
source = tarball(
url = f"https://ftp.gnu.org/gnu/binutils/binutils-{version}.tar.xz",
sha256 = "d75a94f4d73e7a4086f7513e67e439e8fcdcbb726ffe63f4661744e6256b2cf2",
strip_components = 1,
)
def configure(ctx):
ctx.run(
ctx.source_dir / "configure",
"--prefix=" + cfg.prefix,
"--target=" + cfg.target_triple,
"--with-sysroot=" + ctx.sysroot_dir,
"--with-pic",
"--enable-cet",
"--enable-default-execstack=no",
"--enable-deterministic-archives",
"--enable-ld=default",
"--enable-new-dtags",
"--enable-plugins",
"--enable-relro",
"--enable-separate-code",
"--enable-threads",
"--disable-nls",
"--disable-werror",
# gprofng's libcollector relies on glibc-specific internals.
"--disable-gprofng",
env = {
"CFLAGS": cfg.host_cflags,
"CXXFLAGS": cfg.host_cxxflags,
"LDFLAGS": cfg.host_ldflags,
})
_, build, install = autotools()
+20
View File
@@ -0,0 +1,20 @@
version = "7.0.9"
revision = 1
metadata = meta(
description = "Linux kernel headers for userspace development",
license = "GPL-2.0-only",
)
source = tarball(
url = f"https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-{version}.tar.xz",
sha256 = "ac07acdf76cf4621cc5187a2670270a1a699533c8a6b225e4878c416ad83f1c4",
strip_components = 1,
)
def build(ctx):
ctx.run("cp", "-rp", ctx.source_dir / ".", ctx.build_dir)
ctx.run("make", "headers_install", "ARCH=" + cfg.target_arch)
ctx.run("find", ctx.build_dir / "usr" / "include", "-type", "f", "!", "-name", "*.h", "-delete")
def install(ctx):
ctx.run("mkdir", "-p", ctx.dest_dir / options.prefix)
ctx.run("cp", "-rp", ctx.build_dir / "usr" / "include", ctx.dest_dir / options.prefix)
+99
View File
@@ -0,0 +1,99 @@
use crate::{
container::{ContainerManager, PodmanRuntime},
eval::{Config, ContainerConfig, config_globals, eval_files, types_globals},
recipe::RecipeSet,
};
use clap::{Parser, Subcommand};
use starlark::environment::GlobalsBuilder;
use std::{cell::Cell, path::PathBuf, sync::Arc};
#[derive(Debug, Parser)]
struct Cli {
#[arg(
long,
short = 'C',
default_value = ".",
help = "Directory containing the configuration and recipe files"
)]
root: PathBuf,
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Parser)]
#[command(about = "Fetch sources for the given recipes")]
struct FetchCommand {
#[arg(
required = true,
help = "List of recipes to fetch, host recipes should be prefixed with `host:`"
)]
recipes: Vec<String>,
#[arg(long, short = 'n', help = "Print what will be done and exit")]
dry_run: bool,
}
#[derive(Debug, Parser)]
#[command(about = "Build the given recipes")]
struct BuildCommand {
#[arg(
required = true,
help = "List of recipes to build, host recipes should be prefixed with `host:`"
)]
recipes: Vec<String>,
#[arg(long, short, help = "Perform a full rebuild of the given recipes")]
rebuild: bool,
#[arg(long, short = 'n', help = "Print what will be done and exit")]
dry_run: bool,
}
#[derive(Debug, Subcommand)]
enum Command {
Fetch(FetchCommand),
Build(BuildCommand),
}
pub fn run() -> anyhow::Result<()> {
let cli = Cli::parse();
let root_path = cli.root.canonicalize().unwrap_or(cli.root);
let config: Config = {
let cell = Cell::new(None);
let config_path = root_path.join("config.star");
eval_files(
&[&config_path],
&GlobalsBuilder::standard()
.with(types_globals)
.with(config_globals)
.build(),
None,
None,
Some(&cell),
)?;
cell.take()
.ok_or_else(|| anyhow::anyhow!("`config` was not called"))?
};
let container_runtime = match config.container() {
ContainerConfig::Podman(_) => Arc::new(PodmanRuntime::new()?),
};
let mut container_manager = ContainerManager::new(container_runtime);
let mut recipes = RecipeSet::default();
recipes.load_recipes(
&root_path.join(config.recipes_dir()),
&root_path.join(config.host_recipes_dir()),
)?;
println!("{:?}", recipes);
match cli.command {
Command::Fetch(_) => {}
Command::Build(_) => {}
}
Ok(())
}
+88
View File
@@ -0,0 +1,88 @@
use std::{collections::HashMap, path::Path, sync::Arc};
mod podman;
pub use podman::PodmanRuntime;
pub struct Container {
id: String,
runtime: Arc<dyn ContainerRuntime>,
}
impl Container {
fn new(id: String, runtime: Arc<dyn ContainerRuntime>) -> Self {
Self { id, runtime }
}
pub fn id(&self) -> &str {
&self.id
}
pub fn exec<'a, 'e, 'c>(
&self,
argv: impl Into<Vec<&'a str>>,
env: impl Into<Vec<(&'e str, &'e str)>>,
cwd: impl Into<&'c Path>,
) -> anyhow::Result<()> {
self.runtime
.exec(self.id(), argv.into(), env.into(), cwd.into())
}
}
pub trait ContainerRuntime {
/// Starts a new container, returns the ID.
fn start_container(
&self,
image_name: &str,
mounts: &[(&Path, &str, bool)],
) -> anyhow::Result<String>;
/// Stops a container.
fn stop_container(&self, container_id: &str);
/// Executes a command in a container.
fn exec(
&self,
container_id: &str,
argv: Vec<&str>,
env: Vec<(&str, &str)>,
cwd: &Path,
) -> anyhow::Result<()>;
}
pub struct ContainerManager {
containers: HashMap<String, Container>,
runtime: Arc<dyn ContainerRuntime>,
}
impl ContainerManager {
pub fn new(runtime: Arc<dyn ContainerRuntime>) -> Self {
Self {
containers: HashMap::new(),
runtime,
}
}
pub fn container(&mut self, name: &str) -> anyhow::Result<&Container> {
if self.containers.get(name).is_none() {
let container_id = self.runtime.start_container("alpine:edge", &[])?;
crate::log!("info", "Started new container ({container_id})");
self.containers.insert(
name.into(),
Container::new(container_id, self.runtime.clone()),
);
}
Ok(self.containers.get(name).unwrap())
}
}
impl Drop for ContainerManager {
fn drop(&mut self) {
for (_, container) in self.containers.iter() {
self.runtime.stop_container(container.id());
}
}
}
+101
View File
@@ -0,0 +1,101 @@
use crate::container::ContainerRuntime;
use anyhow::Context;
use std::{
path::Path,
process::{Command, Stdio},
};
pub struct PodmanRuntime;
impl PodmanRuntime {
pub fn new() -> anyhow::Result<Self> {
let output = Command::new("podman").arg("--version").output()?;
if output.status.success() {
Ok(Self)
} else {
anyhow::bail!(
"Could not execute `podman --version` - make sure you have podman installed."
);
}
}
}
impl ContainerRuntime for PodmanRuntime {
fn start_container(
&self,
image_name: &str,
mounts: &[(&Path, &str, bool)],
) -> anyhow::Result<String> {
let mut cmd = Command::new("podman");
cmd.arg("run");
cmd.arg("--detach");
cmd.arg("--read-only");
cmd.arg("--network=none");
cmd.arg("--userns=keep-id");
cmd.arg("--tmpfs").arg("/builder/dest");
cmd.arg("--tmpfs").arg("/builder/sysroot");
for &(host, container, read_only) in mounts {
cmd.arg("--volume").arg(format!(
"{}:{}{}",
host.display(),
container,
if read_only { ":ro" } else { "" }
));
}
cmd.arg(image_name);
cmd.arg("sleep").arg("infinity");
let output = cmd.output()?;
if output.status.success() {
Ok(String::from_utf8(output.stdout)
.context("container ID is not valid UTF-8")?
.trim()
.into())
} else {
todo!()
}
}
fn stop_container(&self, container_id: &str) {
Command::new("podman")
.arg("kill")
.arg(container_id)
.output()
.unwrap();
}
fn exec(
&self,
container_id: &str,
argv: Vec<&str>,
env: Vec<(&str, &str)>,
cwd: &Path,
) -> anyhow::Result<()> {
let mut cmd = Command::new("podman");
cmd.arg("exec");
cmd.arg("--workdir").arg(cwd);
for (key, value) in env {
cmd.arg("--env").arg(format!("{key}={value}"));
}
cmd.arg(container_id);
cmd.args(argv);
cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
let output = cmd.output()?;
if output.status.success() {
Ok(())
} else {
anyhow::bail!("Failed to execute command");
}
}
}
+154
View File
@@ -0,0 +1,154 @@
use crate::eval::Path;
use allocative::Allocative;
use starlark::{
collections::SmallMap,
environment::GlobalsBuilder,
eval::Evaluator,
starlark_module, starlark_simple_value,
values::{StarlarkValue, Value, ValueLike, none::NoneType},
};
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
use std::{
cell::Cell,
collections::HashMap,
path::{Path as StdPath, PathBuf},
};
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
pub struct PodmanConfig {
image: String,
dockerfile: PathBuf,
}
impl std::fmt::Display for PodmanConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "podman_config")
}
}
starlark_simple_value!(PodmanConfig);
#[starlark_value(type = "podman_config")]
impl<'v> StarlarkValue<'v> for PodmanConfig {}
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
pub enum ContainerConfig {
Podman(PodmanConfig),
}
impl std::fmt::Display for ContainerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "container_config")
}
}
starlark_simple_value!(ContainerConfig);
#[starlark_value(type = "container_config")]
impl<'v> StarlarkValue<'v> for ContainerConfig {}
#[derive(Debug, Clone, Allocative, ProvidesStaticType)]
pub enum ConfigValue {
String(String),
Integer(i32),
Bool(bool),
Path(Path),
}
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
pub struct Config {
arch: String,
container: ContainerConfig,
recipes_dir: Path,
host_recipes_dir: Path,
options: HashMap<String, ConfigValue>,
}
impl Config {
pub fn arch(&self) -> &str {
&self.arch
}
pub fn container(&self) -> &ContainerConfig {
&self.container
}
pub fn recipes_dir(&self) -> &StdPath {
&self.recipes_dir.path()
}
pub fn host_recipes_dir(&self) -> &StdPath {
&self.host_recipes_dir.path()
}
pub fn options(&self) -> &HashMap<String, ConfigValue> {
&self.options
}
}
impl std::fmt::Display for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "container_config")
}
}
starlark_simple_value!(Config);
#[starlark_value(type = "config")]
impl<'v> StarlarkValue<'v> for Config {}
#[starlark_module]
pub fn config_globals(b: &mut GlobalsBuilder) {
fn podman(
#[starlark(require = named)] image: &str,
#[starlark(require = named)] dockerfile: &Path,
) -> anyhow::Result<ContainerConfig> {
Ok(ContainerConfig::Podman(PodmanConfig {
image: image.to_string(),
dockerfile: dockerfile.path().to_owned(),
}))
}
fn config(
#[starlark(require = named)] arch: &str,
#[starlark(require = named)] container: &ContainerConfig,
#[starlark(require = named)] recipes_dir: &Path,
#[starlark(require = named)] host_recipes_dir: &Path,
#[starlark(kwargs)] kwargs: SmallMap<&str, Value>,
eval: &mut Evaluator,
) -> anyhow::Result<NoneType> {
let config = eval
.extra
.and_then(|extra| extra.downcast_ref::<Cell<Option<Config>>>())
.ok_or_else(|| anyhow::anyhow!("`config` called outside of config.star"))?;
config.set(Some(Config {
arch: arch.to_string(),
container: container.clone(),
recipes_dir: recipes_dir.clone(),
host_recipes_dir: host_recipes_dir.clone(),
options: kwargs
.iter()
.map(|(&k, v)| {
let value = if let Some(str) = v.unpack_str() {
ConfigValue::String(str.to_string())
} else if let Some(num) = v.unpack_i32() {
ConfigValue::Integer(num)
} else if let Some(bool) = v.unpack_bool() {
ConfigValue::Bool(bool)
} else if let Some(path) = v.downcast_ref::<Path>() {
ConfigValue::Path(path.clone())
} else {
anyhow::bail!("config option must be a `string`, `int`, `bool` or `path`");
};
Ok((k.to_string(), value))
})
.collect::<Result<_, _>>()?,
}));
Ok(NoneType)
}
}
+78
View File
@@ -0,0 +1,78 @@
use anyhow::Context;
use starlark::{
any::AnyLifetime,
environment::{FrozenModule, Globals, Module},
eval::Evaluator,
syntax::{AstModule, Dialect, DialectTypes},
};
use std::path::Path as StdPath;
mod config;
mod types;
#[allow(unused_imports)]
pub use config::*;
#[allow(unused_imports)]
pub use types::*;
pub fn eval_files(
path: &[&StdPath],
globals: &Globals,
lib_module: Option<&FrozenModule>,
config: Option<&Config>,
extra: Option<&dyn AnyLifetime>,
) -> anyhow::Result<Module> {
let module = Module::new();
if let Some(lib_module) = lib_module {
module.import_public_symbols(lib_module);
}
if let Some(config) = config {
module.set("cfg", module.heap().alloc(config.clone()));
}
let mut paths = path.to_vec();
paths.sort();
let ast_modules = paths
.iter()
.map(|&path| {
let module = AstModule::parse_file(path, &default_dialect())
.map_err(|err| anyhow::anyhow!("{err}"))
.context(format!("parsing file {:?}", path.display()))?;
Ok((path, module))
})
.collect::<anyhow::Result<Vec<(&StdPath, AstModule)>>>()?;
for (path, ast) in ast_modules {
let mut eval = Evaluator::new(&module);
if let Some(extra) = extra {
eval.extra = Some(extra);
}
eval.eval_module(ast, globals)
.map_err(|err| anyhow::anyhow!("{err}"))
.context(format!("evaluating file {:?}", path.display()))?;
}
Ok(module)
}
fn default_dialect() -> Dialect {
Dialect {
enable_def: true,
enable_lambda: true,
enable_load: false,
enable_keyword_only_arguments: false,
enable_positional_only_arguments: false,
enable_types: DialectTypes::Disable,
enable_load_reexport: false,
enable_top_level_stmt: true,
enable_f_strings: true,
..Dialect::Standard
}
}
+64
View File
@@ -0,0 +1,64 @@
use allocative::Allocative;
use starlark::{
environment::GlobalsBuilder,
starlark_module, starlark_simple_value,
values::{Heap, StarlarkValue, Value, ValueLike},
};
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
use std::path::{Path as StdPath, PathBuf};
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
pub struct Path {
inner: PathBuf,
}
impl Path {
pub fn new(value: impl Into<PathBuf>) -> Self {
Self {
inner: value.into(),
}
}
pub fn path(&self) -> &StdPath {
&self.inner
}
}
impl std::fmt::Display for Path {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "path({:?})", self.inner)
}
}
starlark_simple_value!(Path);
#[starlark_value(type = "path")]
impl<'v> StarlarkValue<'v> for Path {
fn div(&self, other: Value<'v>, heap: &'v Heap) -> starlark::Result<Value<'v>> {
let rhs = if let Some(str) = other.unpack_str() {
str.to_string()
} else if let Some(path) = other.downcast_ref::<Path>() {
path.inner.to_str().unwrap_or("").to_string()
} else {
return Err(starlark::Error::new_other(anyhow::anyhow!(
"expected a `string` or `path`, got `{}`",
other.get_type()
)));
};
Ok(heap.alloc(Path {
inner: self
.inner
.join(rhs.trim_start_matches(std::path::MAIN_SEPARATOR_STR)),
}))
}
}
#[starlark_module]
pub fn types_globals(b: &mut GlobalsBuilder) {
fn path(value: &str) -> anyhow::Result<Path> {
Ok(Path {
inner: PathBuf::from(value),
})
}
}
+23
View File
@@ -0,0 +1,23 @@
use std::{io::IsTerminal, sync::LazyLock};
static IS_STDERR_TERMINAL: LazyLock<bool> = LazyLock::new(|| std::io::stderr().is_terminal());
pub fn __emit(color: &str, action: &str, args: std::fmt::Arguments) {
const ARROW: &str = "==>";
if *IS_STDERR_TERMINAL {
eprintln!("\x1b[{color}m{ARROW}\x1b[0m \x1b[1m{action}\x1b[0m {args}");
} else {
eprintln!("{ARROW} {action} {args}");
}
}
#[macro_export]
macro_rules! log {
($action:literal) => {{
$crate::log::__emit("1;34", $action, format_args!());
}};
($action:literal, $($arg:tt)*) => {{
$crate::log::__emit("1;34", $action, format_args!($($arg)*));
}};
}
+136
View File
@@ -0,0 +1,136 @@
use crate::{
container::{ContainerManager, PodmanRuntime},
plan::{Plan, PlanKey},
recipe::{PackageRecipe, RecipeSet, SourceRecipe, ToolRecipe},
};
use std::{path::Path, sync::Arc};
mod cli;
mod container;
mod eval;
mod log;
mod plan;
mod recipe;
fn main() -> anyhow::Result<()> {
cli::run()
// let podman_runtime = Arc::new(PodmanRuntime::new()?);
// let mut container_manager = ContainerManager::new(podman_runtime);
// container_manager.container("example")?.exec(
// vec!["sh", "-c", "uname -a && id"],
// vec![],
// Path::new("/"),
// )?;
// let mut recipes = RecipeSet::default();
// recipes.load_recipes(Path::new("./recipes"), Path::new("./host-recipes"))?;
// recipes.add_source(
// "binutils-2.46",
// SourceRecipe {
// name: "binutils-2.46".into(),
// },
// );
// recipes.add_source(
// "gcc-16.1.0",
// SourceRecipe {
// name: "gcc-16.1.0".into(),
// },
// );
// recipes.add_source(
// "linux-7.0.9",
// SourceRecipe {
// name: "linux-7.0.9".into(),
// },
// );
// recipes.add_source(
// "glibc-2.41",
// SourceRecipe {
// name: "glibc-2.41".into(),
// },
// );
// recipes.add_source(
// "bash-5.3",
// SourceRecipe {
// name: "bash-5.3".into(),
// },
// );
// recipes.add_tool(
// "binutils",
// ToolRecipe {
// name: "binutils".into(),
// sources: vec!["binutils-2.46".into()],
// tools_wanted: vec![],
// pkgs_wanted: vec![],
// },
// );
// recipes.add_tool(
// "gcc-bootstrap",
// ToolRecipe {
// name: "gcc-bootstrap".into(),
// sources: vec!["gcc-16.1.0".into()],
// tools_wanted: vec!["binutils".into()],
// pkgs_wanted: vec![],
// },
// );
// recipes.add_tool(
// "gcc",
// ToolRecipe {
// name: "gcc".into(),
// sources: vec!["gcc-16.1.0".into()],
// tools_wanted: vec!["binutils".into()],
// pkgs_wanted: vec!["glibc".into()],
// },
// );
// recipes.add_package(
// "linux-headers",
// PackageRecipe {
// name: "linux-headers".into(),
// sources: vec!["linux-7.0.9".into()],
// tools_wanted: vec![],
// pkgs_wanted: vec![],
// },
// );
// recipes.add_package(
// "glibc",
// PackageRecipe {
// name: "glibc".into(),
// sources: vec!["glibc-2.41".into()],
// tools_wanted: vec!["gcc-bootstrap".into()],
// pkgs_wanted: vec!["linux-headers".into()],
// },
// );
// recipes.add_package(
// "bash",
// PackageRecipe {
// name: "bash".into(),
// sources: vec!["bash-5.3".into()],
// tools_wanted: vec!["gcc".into()],
// pkgs_wanted: vec!["glibc".into()],
// },
// );
// let mut plan = Plan::new(&recipes);
// plan.add_wanted(PlanKey::PkgPackage(
// recipes.package("bash").expect("back package"),
// ));
// println!("{:#?}", plan.steps()?);
// Ok(())
}
+217
View File
@@ -0,0 +1,217 @@
use crate::recipe::{PackageRecipe, RecipeSet, SourceRecipe, ToolRecipe};
use petgraph::{
Direction,
graph::{DiGraph, NodeIndex},
};
use smallvec::{SmallVec, smallvec};
use std::{
cmp::Reverse,
collections::{BinaryHeap, HashMap, HashSet},
};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PlanError {
#[error("missing source recipe '{0}'")]
MissingSource(String),
#[error("missing tool recipe '{0}'")]
MissingTool(String),
#[error("missing package recipe '{0}'")]
MissingPackage(String),
#[error("plan cycle detected")]
CycleDetected,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PlanKey<'a> {
SourceFetch(&'a SourceRecipe),
SourcePatch(&'a SourceRecipe),
SourcePrepare(&'a SourceRecipe),
ToolConfigure(&'a ToolRecipe),
ToolBuild(&'a ToolRecipe),
ToolInstall(&'a ToolRecipe),
PkgConfigure(&'a PackageRecipe),
PkgBuild(&'a PackageRecipe),
PkgInstall(&'a PackageRecipe),
PkgPackage(&'a PackageRecipe),
}
impl<'a> PlanKey<'a> {
fn weight(&self) -> i8 {
match self {
PlanKey::SourceFetch(_) => 0,
PlanKey::SourcePatch(_) => 1,
PlanKey::SourcePrepare(_) => 2,
PlanKey::ToolConfigure(_) => 3,
PlanKey::ToolBuild(_) => 4,
PlanKey::ToolInstall(_) => 5,
PlanKey::PkgConfigure(_) => 6,
PlanKey::PkgBuild(_) => 7,
PlanKey::PkgInstall(_) => 8,
PlanKey::PkgPackage(_) => 9,
}
}
fn dependencies(
&self,
recipes: &'a RecipeSet,
) -> Result<SmallVec<[PlanKey<'a>; 8]>, PlanError> {
match self {
PlanKey::SourceFetch(_) => Ok(smallvec![]),
PlanKey::SourcePatch(recipe) => Ok(smallvec![PlanKey::SourceFetch(recipe)]),
PlanKey::SourcePrepare(recipe) => Ok(smallvec![PlanKey::SourcePatch(recipe)]),
PlanKey::ToolConfigure(recipe) => {
let source_deps = recipe.sources.iter().map(|name| {
recipes
.source(name)
.map(PlanKey::SourcePrepare)
.ok_or(PlanError::MissingSource(name.clone()))
});
let tool_deps = recipe.tools_wanted.iter().map(|name| {
recipes
.tool(name)
.map(PlanKey::ToolInstall)
.ok_or(PlanError::MissingTool(name.clone()))
});
let pkg_deps = recipe.pkgs_wanted.iter().map(|name| {
recipes
.package(name)
.map(PlanKey::PkgPackage)
.ok_or(PlanError::MissingPackage(name.clone()))
});
source_deps.chain(tool_deps).chain(pkg_deps).collect()
}
PlanKey::ToolBuild(recipe) => Ok(smallvec![PlanKey::ToolConfigure(recipe)]),
PlanKey::ToolInstall(recipe) => Ok(smallvec![PlanKey::ToolBuild(recipe)]),
PlanKey::PkgConfigure(recipe) => {
let source_deps = recipe.sources.iter().map(|name| {
recipes
.source(name)
.map(PlanKey::SourcePrepare)
.ok_or(PlanError::MissingSource(name.clone()))
});
let tool_deps = recipe.tools_wanted.iter().map(|name| {
recipes
.tool(name)
.map(PlanKey::ToolInstall)
.ok_or(PlanError::MissingTool(name.clone()))
});
let pkg_deps = recipe.pkgs_wanted.iter().map(|name| {
recipes
.package(name)
.map(PlanKey::PkgPackage)
.ok_or(PlanError::MissingPackage(name.clone()))
});
source_deps.chain(tool_deps).chain(pkg_deps).collect()
}
PlanKey::PkgBuild(recipe) => Ok(smallvec![PlanKey::PkgConfigure(recipe)]),
PlanKey::PkgInstall(recipe) => Ok(smallvec![PlanKey::PkgBuild(recipe)]),
PlanKey::PkgPackage(recipe) => Ok(smallvec![PlanKey::PkgInstall(recipe)]),
}
}
fn is_active(&self) -> bool {
true
}
}
pub struct Plan<'a> {
recipes: &'a RecipeSet,
wanted: HashSet<PlanKey<'a>>,
}
impl<'a> Plan<'a> {
pub fn new(recipes: &'a RecipeSet) -> Self {
Self {
recipes,
wanted: HashSet::new(),
}
}
pub fn add_wanted(&mut self, key: PlanKey<'a>) {
self.wanted.insert(key);
}
pub fn steps(&self) -> Result<Vec<PlanKey<'a>>, PlanError> {
let mut stack: Vec<_> = self.wanted.iter().copied().collect();
let mut graph: DiGraph<_, ()> = DiGraph::new();
let mut nodes = HashMap::new();
while let Some(node) = stack.pop() {
let node_idx = match nodes.get(&node) {
Some(&idx) => idx,
None => {
let idx = graph.add_node(node);
nodes.insert(node, idx);
idx
}
};
for dep in node.dependencies(self.recipes)?.iter().copied() {
let dep_idx = match nodes.get(&dep) {
Some(&idx) => idx,
None => {
let idx = graph.add_node(dep);
nodes.insert(dep, idx);
stack.push(dep);
idx
}
};
graph.update_edge(dep_idx, node_idx, ());
}
}
// petgraph::algo::toposort(&graph, None)
// .and_then(|nodes| {
// Ok(nodes
// .iter()
// .map(|&k| graph[k])
// .filter(|node| node.is_active())
// .collect())
// })
// .map_err(|_| PlanError::CycleDetected)
let mut in_degree: HashMap<NodeIndex, usize> = graph
.node_indices()
.map(|i| (i, graph.neighbors_directed(i, Direction::Incoming).count()))
.collect();
let mut heap: BinaryHeap<(Reverse<i8>, NodeIndex)> = in_degree
.iter()
.filter(|&(_, d)| *d == 0)
.map(|(&i, _)| (Reverse(graph[i].weight()), i))
.collect();
let mut result = Vec::with_capacity(graph.node_count());
while let Some((_, idx)) = heap.pop() {
result.push(graph[idx]);
for neighbor in graph.neighbors_directed(idx, Direction::Outgoing) {
let d = in_degree.get_mut(&neighbor).unwrap();
*d -= 1;
if *d == 0 {
heap.push((Reverse(graph[neighbor].weight()), neighbor));
}
}
}
if result.len() != graph.node_count() {
Err(PlanError::CycleDetected)
} else {
result.retain(|node| node.is_active());
Ok(result)
}
}
}
+145
View File
@@ -0,0 +1,145 @@
use anyhow::Context;
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct SourceRecipe {
pub name: String,
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct ToolRecipe {
pub name: String,
pub sources: Vec<String>,
pub tools_wanted: Vec<String>,
pub pkgs_wanted: Vec<String>,
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct PackageRecipe {
pub name: String,
pub sources: Vec<String>,
pub tools_wanted: Vec<String>,
pub pkgs_wanted: Vec<String>,
}
impl std::fmt::Debug for SourceRecipe {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "source:{}", self.name)
}
}
impl std::fmt::Debug for ToolRecipe {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "tool:{}", self.name)
}
}
impl std::fmt::Debug for PackageRecipe {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "package:{}", self.name)
}
}
#[derive(Default, Debug)]
pub struct RecipeSet {
sources: HashMap<String, SourceRecipe>,
tools: HashMap<String, ToolRecipe>,
packages: HashMap<String, PackageRecipe>,
}
impl RecipeSet {
fn add_source(&mut self, name: &str, recipe: SourceRecipe) -> anyhow::Result<()> {
if self.sources.insert(name.to_string(), recipe).is_some() {
anyhow::bail!("source '{name}' already exists");
}
Ok(())
}
fn add_tool(&mut self, name: &str, recipe: ToolRecipe) -> anyhow::Result<()> {
if self.tools.insert(name.to_string(), recipe).is_some() {
anyhow::bail!("tool '{name}' already exists");
}
Ok(())
}
fn add_package(&mut self, name: &str, recipe: PackageRecipe) -> anyhow::Result<()> {
if self.packages.insert(name.to_string(), recipe).is_some() {
anyhow::bail!("package '{name}' already exists");
}
Ok(())
}
fn load_tool_recipe(&mut self, name: &str, path: &Path) -> anyhow::Result<()> {
Ok(())
}
fn load_package_recipe(&mut self, name: &str, path: &Path) -> anyhow::Result<()> {
Ok(())
}
pub fn load_recipes(
&mut self,
recipes_dir: &Path,
host_recipes_dir: &Path,
) -> anyhow::Result<()> {
for (dir, tool_recipe) in [(recipes_dir, false), (host_recipes_dir, true)] {
for entry in std::fs::read_dir(dir)? {
let entry = entry.context("reading directory entry")?;
if let Some((name, path)) = get_recipe_name_and_patch(&entry)? {
if tool_recipe {
self.load_tool_recipe(&name, &path)?;
} else {
self.load_package_recipe(&name, &path)?;
}
}
}
}
Ok(())
}
pub fn source(&self, name: &str) -> Option<&SourceRecipe> {
self.sources.get(name)
}
pub fn tool(&self, name: &str) -> Option<&ToolRecipe> {
self.tools.get(name)
}
pub fn package(&self, name: &str) -> Option<&PackageRecipe> {
self.packages.get(name)
}
}
fn get_recipe_name_and_patch(
entry: &std::fs::DirEntry,
) -> anyhow::Result<Option<(String, PathBuf)>> {
let file_type = entry.file_type()?;
let path = entry.path();
if file_type.is_dir() {
let recipe_path = path.join("recipe.star");
if recipe_path.exists() {
return Ok(Some((
entry.file_name().to_str().unwrap_or("").to_string(),
recipe_path,
)));
}
} else {
let name = path.file_stem().unwrap().to_str().unwrap_or("").to_string();
let extension = path
.extension()
.ok_or(anyhow::anyhow!("File did not have an extension"))?;
if extension == "star" {
return Ok(Some((name, path)));
}
}
Ok(None)
}