diff options
Diffstat (limited to 'crates/rebel-resolve/src/context.rs')
-rw-r--r-- | crates/rebel-resolve/src/context.rs | 549 |
1 files changed, 549 insertions, 0 deletions
diff --git a/crates/rebel-resolve/src/context.rs b/crates/rebel-resolve/src/context.rs new file mode 100644 index 0000000..996d981 --- /dev/null +++ b/crates/rebel-resolve/src/context.rs @@ -0,0 +1,549 @@ +use std::{ + borrow::Cow, + cmp::Ordering, + collections::{HashMap, HashSet}, + fmt::Display, + hash::Hash, + ops::Index, + rc::Rc, + result, +}; + +use rebel_common::{ + error::{self, Contextualizable}, + string_hash::ArchiveHash, + types::TaskIDRef, +}; + +use crate::{ + args::*, + paths, + pin::{self, Pins}, + task::*, +}; + +#[derive(Debug, Clone, Copy)] +pub enum ErrorKind<'a> { + TaskNotFound, + InvalidArgument(&'a str), + InvalidArgRef(&'a str), +} + +#[derive(Debug, Clone, Copy)] +pub struct Error<'a> { + pub task: TaskIDRef<'a>, + pub kind: ErrorKind<'a>, +} + +impl<'a> Display for Error<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Error { task, kind } = self; + match kind { + ErrorKind::TaskNotFound => write!(f, "Task '{}' not found", task), + ErrorKind::InvalidArgument(arg) => write!( + f, + "Invalid or missing argument '{}' for task '{}'", + arg, task + ), + ErrorKind::InvalidArgRef(arg) => write!( + f, + "Invalid reference for argument '{}' of task '{}'", + arg, task + ), + } + } +} + +impl<'a> From<Error<'a>> for error::Error { + fn from(err: Error) -> Self { + error::Error::new(err) + } +} + +pub type Result<'a, T> = result::Result<T, Error<'a>>; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct TaskRef<'ctx> { + pub id: TaskIDRef<'ctx>, + pub args: Rc<TaskArgs>, +} + +impl<'ctx> Display for TaskRef<'ctx> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if !f.alternate() { + return self.id.fmt(f); + } + + let version_arg = match self.args.get("version") { + Some(Arg::String(s)) => Some(s), + _ => None, + }; + let host_arg = match self.args.get("host") { + Some(Arg::Platform(platform)) => Some(platform), + _ => None, + }; + let target_arg = match self.args.get("target") { + Some(Arg::Platform(platform)) => Some(platform), + _ => None, + }; + + write!(f, "{}", self.id.recipe)?; + if let Some(version) = version_arg { + write!(f, "#{}", version)?; + } + write!(f, "::{}", self.id.task)?; + + if host_arg.is_some() || target_arg.is_some() { + write!(f, "@")?; + } + + if let Some(host) = host_arg { + write!(f, "{}", host.short)?; + } + if let Some(target) = target_arg { + write!(f, ":{}", target.short)?; + } + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct OutputRef<'ctx> { + pub task: TaskRef<'ctx>, + pub output: &'ctx str, +} + +fn platform_relation(args: &TaskArgs, from: &str, to: &str) -> Option<PlatformRelation> { + let plat_from = match args.get(from)? { + Arg::Platform(plat) => plat, + _ => return None, + }; + let plat_to = match args.get(to)? { + Arg::Platform(plat) => plat, + _ => return None, + }; + + let plat_rel = if plat_from == plat_to { + PlatformRelation { + is_same: true, + sysroot: "".to_string(), + cross_compile: "".to_string(), + } + } else { + PlatformRelation { + is_same: false, + sysroot: paths::TASK_SYSROOT.to_string(), + cross_compile: format!("{}/bin/{}-", plat_from.prefix, plat_to.gnu_triplet), + } + }; + Some(plat_rel) +} + +#[derive(Debug)] +pub struct Context { + platforms: HashMap<String, Arg>, + globals: TaskArgs, + tasks: HashMap<String, HashMap<String, Vec<TaskDef>>>, + rootfs: (ArchiveHash, String), +} + +impl Context { + pub fn new( + mut tasks: HashMap<String, HashMap<String, Vec<TaskDef>>>, + pins: Pins, + ) -> error::Result<Self> { + let platforms: HashMap<_, _> = [ + arg( + "build", + Platform { + short: "build".to_string(), + gnu_triplet: "x86_64-linux-gnu".to_string(), + karch: "x86_64".to_string(), + prefix: "/opt/toolchain".to_string(), + }, + ), + arg( + "aarch64", + Platform { + short: "aarch64".to_string(), + gnu_triplet: "aarch64-linux-gnu".to_string(), + karch: "arm64".to_string(), + prefix: "/usr".to_string(), + }, + ), + ] + .into_iter() + .collect(); + + let globals = TaskArgs::from_iter([ + ("build".to_string(), platforms["build"].clone()), + arg("workdir", paths::TASK_WORKDIR.to_string()), + arg("dldir", paths::TASK_DLDIR.to_string()), + arg("destdir", paths::TASK_DESTDIR.to_string()), + arg("sysroot", paths::TASK_SYSROOT.to_string()), + ]); + let (rootfs, rootfs_provides) = + Context::handle_pins(pins).context("Failed to process pin list")?; + + Context::add_rootfs_tasks(&mut tasks, rootfs_provides, &globals) + .context("Failed to determine rootfs-provided tasks from pin list")?; + + Ok(Context { + platforms, + globals, + tasks, + rootfs, + }) + } + + fn handle_pins(pins: Pins) -> error::Result<((ArchiveHash, String), Vec<pin::Provides>)> { + let mut ret = None; + + for (name, pin) in pins { + if pin.is_rootfs { + if ret.is_some() { + return Err(error::Error::new("Multiple is-rootfs pins")); + } + let hash = pin.hash.context("is-rootfs pin without hash")?; + + ret = Some(((hash, name), pin.provides)); + } + } + + ret.context("No is-rootfs pins") + } + + fn add_rootfs_tasks( + tasks: &mut HashMap<String, HashMap<String, Vec<TaskDef>>>, + provides: Vec<pin::Provides>, + globals: &TaskArgs, + ) -> error::Result<()> { + let build = globals.get("build").unwrap(); + + for pin::Provides { + recipe, + task, + output, + args, + } in provides + { + let mut task_def = TaskDef::default(); + + if let Some(host) = args.host { + if host != "build" { + return Err(error::Error::new(format!("Invalid host value '{}'", host))); + } + task_def.args.insert("host".to_string(), build.into()); + task_def.arg_match.set("host", Some(build)); + } + + if let Some(target) = args.target { + if target != "build" { + return Err(error::Error::new(format!( + "Invalid target value '{}'", + target + ))); + } + task_def.args.insert("target".to_string(), build.into()); + task_def.arg_match.set("target", Some(build)); + } + + for output_entry in output { + task_def + .output + .insert(output_entry.to_string(), Output::default()); + } + + task_def.priority = i32::MAX; + + tasks + .entry(recipe) + .or_default() + .entry(task) + .or_default() + .push(task_def); + } + + Ok(()) + } + + pub fn get_rootfs(&self) -> &(ArchiveHash, String) { + &self.rootfs + } + + fn match_task(task: &TaskDef, args: &TaskArgs) -> bool { + task.arg_match + .iter() + .all(|(key, value)| args.get(key) == Some(value)) + } + + fn compare_tasks(task1: &TaskDef, task2: &TaskDef) -> Ordering { + task1 + .priority + .cmp(&task2.priority) + .then(deb_version::compare_versions( + task1.meta.version.as_deref().unwrap_or_default(), + task2.meta.version.as_deref().unwrap_or_default(), + )) + } + + fn select_task<'ctx>(tasks: &'ctx [TaskDef], args: &TaskArgs) -> Option<&'ctx TaskDef> { + tasks + .iter() + .filter(|task| Self::match_task(task, args)) + .max_by(|task1, task2| Self::compare_tasks(task1, task2)) + } + + fn get_by_ref(&self, id: TaskIDRef) -> Option<&[TaskDef]> { + Some(self.tasks.get(id.recipe)?.get(id.task)?) + } + + fn get_with_args<'a>(&self, id: TaskIDRef<'a>, args: &TaskArgs) -> Result<'a, &TaskDef> { + self.get_by_ref(id) + .and_then(|tasks| Self::select_task(tasks, args)) + .ok_or(Error { + task: id, + kind: ErrorKind::TaskNotFound, + }) + } + + pub fn get<'a>(&self, task: &TaskRef<'a>) -> Result<'a, &TaskDef> { + self.get_with_args(task.id, task.args.as_ref()) + } + + fn task_ref<'ctx>(&'ctx self, id: TaskIDRef<'ctx>, args: &TaskArgs) -> Result<TaskRef> { + let task_def = self.get_with_args(id, args)?; + + let mut arg_def: HashMap<_, _> = task_def.args.iter().map(|(k, &v)| (k, v)).collect(); + for (key, arg) in &self.globals { + // TODO: Handle conflicts between explicit args and globals + arg_def.insert(key, ArgType::from(arg)); + } + + let mut new_args = TaskArgs::default(); + + for (key, typ) in arg_def { + if let Some(arg) = args.get(key) { + if ArgType::from(arg) == typ { + new_args.set(key, Some(arg)); + continue; + } + } + return Err(Error { + task: id, + kind: ErrorKind::InvalidArgument(key), + }); + } + + let build_to_host = platform_relation(&new_args, "build", "host"); + let host_to_target = platform_relation(&new_args, "host", "target"); + let build_to_target = platform_relation(&new_args, "build", "target"); + + let cross_compile = build_to_host + .as_ref() + .map(|build_to_host| build_to_host.cross_compile.clone()); + + new_args.set("build_to_host", build_to_host); + new_args.set("host_to_target", host_to_target); + new_args.set("build_to_target", build_to_target); + + new_args.set("cross_compile", cross_compile); + + new_args.set("basename", Some(task_def.meta.basename.clone())); + new_args.set("recipename", Some(task_def.meta.recipename.clone())); + new_args.set("recipe", Some(task_def.meta.recipe.clone())); + new_args.set("name", Some(task_def.meta.name.clone())); + new_args.set("version", task_def.meta.version.clone()); + + Ok(TaskRef { + id, + args: Rc::new(new_args), + }) + } + + pub fn lookup( + &self, + id: TaskIDRef, + host: Option<&str>, + target: Option<&str>, + ) -> error::Result<TaskRef> { + let (ctx_recipe, recipe_tasks) = self + .tasks + .get_key_value(id.recipe) + .with_context(|| format!("Task {} not found", id))?; + let (ctx_task, _) = recipe_tasks + .get_key_value(id.task) + .with_context(|| format!("Task {} not found", id))?; + let ctx_id = TaskIDRef { + recipe: ctx_recipe, + task: ctx_task, + }; + + let mut args = self.globals.clone(); + + if let Some(host) = host { + let plat = self + .platforms + .get(host) + .with_context(|| format!("Platform '{}' not found", host))?; + args.set("host", Some(plat)); + args.set("target", Some(plat)); + } + if let Some(target) = target { + let plat = self + .platforms + .get(target) + .with_context(|| format!("Platform '{}' not found", target))?; + args.set("target", Some(plat)); + } + + let task_ref = self + .task_ref(ctx_id, &args) + .with_context(|| format!("Failed to instantiate task {}", id))?; + + Ok(task_ref) + } + + fn map_args<'ctx, 'args>( + task: TaskIDRef<'ctx>, + mapping: &'ctx ArgMapping, + args: &'args TaskArgs, + build_dep: bool, + ) -> Result<'ctx, Cow<'args, TaskArgs>> { + if mapping.0.is_empty() && !build_dep { + return Ok(Cow::Borrowed(args)); + } + + let mut ret = args.clone(); + + if build_dep { + ret.set("host", args.get("build")); + ret.set("target", args.get("host")); + } + + for (to, from) in &mapping.0 { + let value = args.get(from).ok_or(Error { + task, + kind: ErrorKind::InvalidArgRef(to), + })?; + ret.set(to, Some(value.clone())); + } + + Ok(Cow::Owned(ret)) + } + + fn parent_ref<'ctx>( + &'ctx self, + dep_of: TaskIDRef<'ctx>, + dep: &'ctx ParentDep, + args: &TaskArgs, + ) -> Result<TaskRef> { + let id = dep.dep.id(dep_of.recipe); + let mapped_args = Context::map_args(id, &dep.dep.args, args, false)?; + self.task_ref(id, mapped_args.as_ref()) + } + + pub fn output_ref<'ctx>( + &'ctx self, + dep_of: TaskIDRef<'ctx>, + dep: &'ctx OutputDep, + args: &TaskArgs, + build_dep: bool, + ) -> Result<OutputRef<'ctx>> { + let id = dep.dep.id(dep_of.recipe); + let mapped_args = Context::map_args(id, &dep.dep.args, args, build_dep)?; + Ok(OutputRef { + task: self.task_ref(id, mapped_args.as_ref())?, + output: &dep.output, + }) + } + + pub fn get_parent_depend<'ctx>( + &'ctx self, + task_ref: &TaskRef<'ctx>, + ) -> Result<Option<TaskRef>> { + let task = self.get(task_ref)?; + let Some(parent) = &task.parent else { + return Ok(None); + }; + Some(self.parent_ref(task_ref.id, parent, &task_ref.args)).transpose() + } + + fn ancestor_iter<'ctx>( + &'ctx self, + task_ref: &TaskRef<'ctx>, + ) -> impl Iterator<Item = Result<TaskRef>> { + struct Iter<'ctx>(&'ctx Context, Option<Result<'ctx, TaskRef<'ctx>>>); + + impl<'ctx> Iterator for Iter<'ctx> { + type Item = Result<'ctx, TaskRef<'ctx>>; + + fn next(&mut self) -> Option<Self::Item> { + let task_ref = match self.1.take()? { + Ok(task_ref) => task_ref, + Err(err) => return Some(Err(err)), + }; + self.1 = self.0.get_parent_depend(&task_ref).transpose(); + Some(Ok(task_ref)) + } + } + + Iter(self, Some(Ok(task_ref.clone()))) + } + + pub fn get_build_depends<'ctx>( + &'ctx self, + task_ref: &TaskRef<'ctx>, + ) -> Result<HashSet<OutputRef>> { + let mut ret = HashSet::new(); + let mut allow_noinherit = true; + + for current in self.ancestor_iter(task_ref) { + let current_ref = current?; + let task = self.get(¤t_ref)?; + let entries = task + .build_depends + .iter() + .filter(|dep| allow_noinherit || !dep.noinherit) + .map(|dep| self.output_ref(task_ref.id, dep, ¤t_ref.args, true)) + .collect::<Result<Vec<_>>>()?; + ret.extend(entries); + + allow_noinherit = false; + } + + Ok(ret) + } + + pub fn get_host_depends<'ctx>( + &'ctx self, + task_ref: &TaskRef<'ctx>, + ) -> Result<HashSet<OutputRef>> { + let mut ret = HashSet::new(); + let mut allow_noinherit = true; + + for current in self.ancestor_iter(task_ref) { + let current_ref = current?; + let task = self.get(¤t_ref)?; + let entries = task + .depends + .iter() + .filter(|dep| allow_noinherit || !dep.noinherit) + .map(|dep| self.output_ref(task_ref.id, dep, ¤t_ref.args, false)) + .collect::<Result<Vec<_>>>()?; + ret.extend(entries); + + allow_noinherit = false; + } + + Ok(ret) + } +} + +impl Index<&TaskRef<'_>> for Context { + type Output = TaskDef; + + fn index(&self, index: &TaskRef) -> &TaskDef { + self.get(index).expect("Invalid TaskRef") + } +} |