meta: initial rewrite (final)
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
/build
|
||||||
Generated
+1813
File diff suppressed because it is too large
Load Diff
+14
@@ -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
@@ -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),
|
||||||
|
)
|
||||||
@@ -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()
|
||||||
@@ -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
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user