From 2e6704516af9c68ac2c4935492e83877755e5c20 Mon Sep 17 00:00:00 2001 From: Marvin Friedrich Date: Sat, 23 May 2026 00:10:08 +0200 Subject: [PATCH] recipe: Implement run --- Cargo.lock | 1 + Cargo.toml | 1 + src/cli.rs | 50 ++++++++++++++--- src/container/mod.rs | 37 +++++++----- src/eval/mod.rs | 17 +++++- src/eval/recipe.rs | 131 ++++++++++++++++++++++++++++++++++++++++++- src/log.rs | 3 - src/recipe.rs | 82 ++++++++++++++++++++------- 8 files changed, 275 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38decb5..14b1533 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,6 +183,7 @@ dependencies = [ "allocative", "anyhow", "clap", + "either", "petgraph 0.8.3", "smallvec", "starlark", diff --git a/Cargo.toml b/Cargo.toml index 1b20b46..2e043bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" allocative = "0.3.4" anyhow = "1.0.102" clap = { version = "4.6.1", features = ["derive"] } +either = "1.16.0" petgraph = "0.8.3" smallvec = "1.15.1" starlark = "0.13.0" diff --git a/src/cli.rs b/src/cli.rs index 8c50b34..2c16c78 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,11 +1,20 @@ use crate::{ container::{ContainerManager, PodmanRuntime}, - eval::{Config, ContainerConfig, config_globals, eval_files, types_globals}, + eval::{ + Config, ContainerConfig, ContainerManagerWrapper, Context, Path, config_globals, + eval_files, types_globals, + }, + log, + plan::{Plan, PlanKey}, recipe::RecipeSet, }; use clap::{Parser, Subcommand}; -use starlark::environment::GlobalsBuilder; +use starlark::{ + environment::{GlobalsBuilder, Module}, + eval, + values::Value, +}; use std::{cell::Cell, path::PathBuf, sync::Arc}; #[derive(Debug, Parser)] @@ -80,7 +89,7 @@ pub fn run() -> anyhow::Result<()> { ContainerConfig::Podman(_) => Arc::new(PodmanRuntime::new()?), }; - let mut container_manager = ContainerManager::new(container_runtime); + let container_manager = ContainerManager::new(container_runtime); let mut recipes = RecipeSet::new(&config); recipes.load_recipes( @@ -88,14 +97,41 @@ pub fn run() -> anyhow::Result<()> { &root_path.join(config.host_recipes_dir()), )?; - for (name, recipe) in recipes.packages.iter() { - println!("{name}: {:#?}", recipe); - } + // let wrapper = ContainerManagerWrapper(&container_manager); + // for (name, recipe) in recipes.packages.iter() { + // println!("{name}: {:#?}", recipe); + + // let mo = Module::new(); + // let mut eval = eval::Evaluator::new(&mo); + // eval.extra = Some(&wrapper); + // eval.eval_function( + // recipe.build.unwrap().0.to_value(), + // &[mo.heap().alloc(Context { + // source_dir: Path::new("/source"), + // build_dir: Path::new("/build"), + // jobs: 4, + // })], + // &[], + // ) + // .unwrap(); + // } + + let mut plan = Plan::new(&recipes); match cli.command { Command::Fetch(_) => {} - Command::Build(_) => {} + Command::Build(cmd) => { + for recipe in cmd.recipes.iter() { + plan.add_wanted(if let Some(recipe) = recipe.strip_prefix("host:") { + PlanKey::ToolInstall(recipe.to_string()) + } else { + PlanKey::PkgPackage(recipe.clone()) + }); + } + } } + log!("plan", "{:#?}", plan.steps()); + Ok(()) } diff --git a/src/container/mod.rs b/src/container/mod.rs index 7b97b10..3201579 100644 --- a/src/container/mod.rs +++ b/src/container/mod.rs @@ -1,9 +1,14 @@ -use std::{collections::HashMap, path::Path, sync::Arc}; +use std::{ + collections::HashMap, + path::Path, + sync::{Arc, Mutex}, +}; mod podman; pub use podman::PodmanRuntime; +#[derive(Clone)] pub struct Container { id: String, runtime: Arc, @@ -51,6 +56,10 @@ pub trait ContainerRuntime { } pub struct ContainerManager { + inner: Mutex, +} + +struct ContainerManagerInner { containers: HashMap, runtime: Arc, } @@ -58,31 +67,33 @@ pub struct ContainerManager { impl ContainerManager { pub fn new(runtime: Arc) -> Self { Self { - containers: HashMap::new(), - runtime, + inner: Mutex::new(ContainerManagerInner { + 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", &[])?; + pub fn container(&self, name: &str) -> anyhow::Result { + let mut inner = self.inner.lock().unwrap(); + if inner.containers.get(name).is_none() { + let container_id = inner.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()), - ); + let container = Container::new(container_id, inner.runtime.clone()); + inner.containers.insert(name.into(), container); } - Ok(self.containers.get(name).unwrap()) + Ok(inner.containers.get(name).cloned().unwrap()) } } impl Drop for ContainerManager { fn drop(&mut self) { - for (_, container) in self.containers.iter() { - self.runtime.stop_container(container.id()); + let inner = self.inner.lock().unwrap(); + for (_, container) in inner.containers.iter() { + inner.runtime.stop_container(container.id()); } } } diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 601bec6..d70da04 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -1,9 +1,9 @@ -use anyhow::Context; use starlark::{ any::AnyLifetime, environment::{FrozenModule, Globals, Module}, eval::Evaluator, syntax::{AstModule, Dialect, DialectTypes}, + values::{UnpackValue, Value, type_repr::StarlarkTypeRepr}, }; use std::path::Path as StdPath; @@ -18,6 +18,19 @@ pub use recipe::*; #[allow(unused_imports)] pub use types::*; +pub trait UnpackCloned: Sized + StarlarkTypeRepr { + fn unpack_cloned(value: Value<'_>) -> Option; +} + +impl UnpackCloned for T +where + for<'v> T: UnpackValue<'v>, +{ + fn unpack_cloned(value: Value<'_>) -> Option { + T::unpack_value(value).unwrap() + } +} + pub fn eval_files( path: &[&StdPath], globals: &Globals, @@ -25,6 +38,8 @@ pub fn eval_files( config: Option<&Config>, extra: Option<&dyn AnyLifetime>, ) -> anyhow::Result { + use anyhow::Context; + let module = Module::new(); if let Some(lib_module) = lib_module { diff --git a/src/eval/recipe.rs b/src/eval/recipe.rs index da0f004..9867d90 100644 --- a/src/eval/recipe.rs +++ b/src/eval/recipe.rs @@ -1,10 +1,24 @@ +use std::cell::Cell; + use allocative::Allocative; use starlark::{ - environment::GlobalsBuilder, starlark_module, starlark_simple_value, values::StarlarkValue, + environment::{GlobalsBuilder, Methods, MethodsBuilder, MethodsStatic}, + eval::Evaluator, + starlark_module, starlark_simple_value, + typing::Ty, + values::{ + Heap, StarlarkValue, UnpackValue, Value, ValueLike, none::NoneType, tuple::UnpackTuple, + type_repr::StarlarkTypeRepr, + }, }; use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value}; -use crate::recipe::Source; +use crate::{ + container::{Container, ContainerManager}, + eval::{Path, UnpackCloned}, + log, + recipe::Source, +}; #[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)] pub struct TarballSource { @@ -24,6 +38,12 @@ starlark_simple_value!(TarballSource); #[starlark_value(type = "tarball")] impl<'v> StarlarkValue<'v> for TarballSource {} +impl UnpackCloned for TarballSource { + fn unpack_cloned(value: Value<'_>) -> Option { + value.downcast_ref().cloned() + } +} + impl Source for TarballSource {} #[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)] @@ -45,6 +65,113 @@ starlark_simple_value!(Metadata); #[starlark_value(type = "metadata")] impl<'v> StarlarkValue<'v> for Metadata {} +impl UnpackCloned for Metadata { + fn unpack_cloned(value: Value<'_>) -> Option { + value.downcast_ref().cloned() + } +} + +#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)] +pub struct Context { + pub source_dir: Path, + pub build_dir: Path, + pub jobs: i32, +} + +impl std::fmt::Display for Context { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "context") + } +} + +starlark_simple_value!(Context); + +#[derive(Debug)] +struct RunArg(pub String); + +impl UnpackValue<'_> for RunArg { + type Error = anyhow::Error; + + fn unpack_value_impl(value: Value) -> anyhow::Result> { + Ok(if let Some(str) = value.unpack_str() { + Some(RunArg(str.to_owned())) + } else if let Some(int) = value.unpack_i32() { + Some(RunArg(int.to_string())) + } else if let Some(path) = value.downcast_ref::() { + Some(RunArg(path.path().to_str().unwrap_or("").to_string())) + } else { + None + }) + } +} + +impl StarlarkTypeRepr for RunArg { + type Canonical = Self; + + fn starlark_type_repr() -> starlark::typing::Ty { + Ty::string() + } +} + +#[derive(ProvidesStaticType)] +pub struct ContainerManagerWrapper<'a>(pub &'a ContainerManager); + +#[starlark_module] +fn context_methods(b: &mut MethodsBuilder) { + fn run( + #[starlark(this)] this: &Context, + #[starlark(args)] args: UnpackTuple, + eval: &mut Evaluator, + ) -> anyhow::Result { + let ContainerManagerWrapper(container_manager) = eval + .extra + .and_then(|extra| extra.downcast_ref()) + .ok_or_else(|| anyhow::anyhow!("`config` called outside of config.star"))?; + + let argv = args.items.iter().map(|x| x.0.as_str()).collect::>(); + + log!("run", "Running command: {argv:?}"); + + container_manager + .container("changeme")? // TODO + .exec(argv, [], std::path::Path::new("/"))?; + + Ok(NoneType) + } +} + +#[starlark_value(type = "context")] +impl<'v> StarlarkValue<'v> for Context { + fn get_methods() -> Option<&'static Methods> { + static RES: MethodsStatic = MethodsStatic::new(); + RES.methods(context_methods) + } + + fn has_attr(&self, attr: &str, _heap: &Heap) -> bool { + match attr { + "source_dir" => true, + "build_dir" => true, + "jobs" => true, + _ => false, + } + } + + fn get_attr(&self, attr: &str, heap: &'v Heap) -> Option> { + match attr { + "source_dir" => Some(heap.alloc(self.source_dir.clone())), + "build_dir" => Some(heap.alloc(self.build_dir.clone())), + "jobs" => Some(heap.alloc(self.jobs)), + _ => None, + } + } +} + +impl UnpackCloned for Context { + fn unpack_cloned(value: Value<'_>) -> Option { + value.downcast_ref().cloned() + } +} + #[starlark_module] pub fn recipe_globals(b: &mut GlobalsBuilder) { fn tarball( diff --git a/src/log.rs b/src/log.rs index 18af983..d40989f 100644 --- a/src/log.rs +++ b/src/log.rs @@ -14,9 +14,6 @@ pub fn __emit(color: &str, action: &str, args: std::fmt::Arguments) { #[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)*)); }}; diff --git a/src/recipe.rs b/src/recipe.rs index 439662b..071880e 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -1,14 +1,20 @@ use anyhow::Context; use starlark::{ - environment::{GlobalsBuilder, Module}, - values::UnpackValue, + environment::{FrozenModule, GlobalsBuilder, Module}, + eval, + values::{ + UnpackValue, + typing::{FrozenStarlarkCallable, StarlarkCallable, StarlarkCallableParamSpec}, + }, }; use std::{ collections::HashMap, path::{Path, PathBuf}, }; -use crate::eval::{Config, Metadata, TarballSource, eval_files, recipe_globals, types_globals}; +use crate::eval::{ + Config, Metadata, TarballSource, UnpackCloned, eval_files, recipe_globals, types_globals, +}; pub struct SourceRecipe { pub name: String, @@ -33,6 +39,10 @@ pub struct PackageRecipe { pub sources: Vec, pub tools_wanted: Vec, pub pkgs_wanted: Vec, + pub module: FrozenModule, + pub configure: Option, + pub build: Option, + pub install: Option, } pub struct RecipeSet<'a> { @@ -80,19 +90,22 @@ impl<'a> RecipeSet<'a> { None, )?; - let version: String = get_value_required(&module, "version")?; + let module = module.freeze().map_err(|err| anyhow::anyhow!("{err:?}"))?; - let revision = get_value_option(&module, "revision")?.unwrap_or(1); + let version: String = get_value(&module, "version")?; + let revision: u32 = get_value_option(&module, "revision")?.unwrap_or(1); + let metadata: Option = get_value_option(&module, "metadata")?; + let source: TarballSource = get_value(&module, "source")?; - let metadata: Option<&Metadata> = get_value_option(&module, "metadata")?; - - let source: &TarballSource = get_value_required(&module, "source")?; + let configure = get_frozen_callable(&module, "configure")?; + let build = get_frozen_callable(&module, "build")?; + let install = get_frozen_callable(&module, "install")?; self.add_source( name, SourceRecipe { name: name.to_string(), - source: Box::new(source.clone()), + source: Box::new(source), }, )?; @@ -100,12 +113,16 @@ impl<'a> RecipeSet<'a> { name, PackageRecipe { name: name.to_string(), - meta: metadata.cloned(), - version: version.to_string(), - revision: revision as u32, + meta: metadata, + version, + revision, sources: vec![name.to_string()], tools_wanted: vec![], pkgs_wanted: vec![], + module, + configure, + build, + install, }, ) } @@ -183,34 +200,57 @@ fn get_recipe_name_and_patch( Ok(None) } -fn get_value_option<'v, T: UnpackValue<'v>>( - module: &'v Module, +fn get_value_option( + module: &FrozenModule, name: &str, ) -> anyhow::Result> { module - .get(name) + .get_option(name)? .map(|value| { - T::unpack_value(value).unwrap().ok_or_else(|| { + T::unpack_cloned(value.value()).ok_or_else(|| { anyhow::anyhow!( "`{name}` should be of type `{}` but got `{}`", T::starlark_type_repr(), - value.get_type() + value.value().get_type() ) }) }) .transpose() } -fn get_value_required<'v, T: UnpackValue<'v>>(module: &'v Module, name: &str) -> anyhow::Result { +fn get_value(module: &FrozenModule, name: &str) -> anyhow::Result { let value = module - .get(name) + .get_option(name)? .ok_or_else(|| anyhow::anyhow!("`{name}` is required"))?; - T::unpack_value(value).unwrap().ok_or_else(|| { + T::unpack_cloned(value.value()).ok_or_else(|| { anyhow::anyhow!( "`{name}` should be of type `{}` but got `{}`", T::starlark_type_repr(), - value.get_type() + value.value().get_type() ) }) } + +fn get_frozen_callable( + module: &FrozenModule, + name: &str, +) -> anyhow::Result>> { + let Some(value) = module.get_option(name)? else { + return Ok(None); + }; + + let callable = StarlarkCallable::unpack_value(value.value()) + .map_err(|err| anyhow::anyhow!("{err}"))? + .ok_or_else(|| { + anyhow::anyhow!( + "`{name}` should be callable but got `{}`", + value.value().get_type() + ) + })?; + + callable + .unpack_frozen() + .ok_or_else(|| anyhow::anyhow!("`{name}` was callable but not frozen")) + .map(Some) +}