use std::{ borrow::Cow, cmp::Ordering, collections::{HashMap, HashSet}, fmt::Display, hash::Hash, ops::Index, rc::Rc, result, }; use common::{ error::{self, Contextualizable}, string_hash::ArchiveHash, types::TaskID, }; use crate::{ args::*, parse, paths, pin::{self, Pins}, task::*, }; #[derive(Debug, Clone, Copy)] pub enum ErrorKind<'ctx> { TaskNotFound, InvalidArgument(&'ctx str), InvalidArgRef(&'ctx str), } #[derive(Debug, Clone, Copy)] pub struct Error<'ctx> { pub task: &'ctx TaskID, pub kind: ErrorKind<'ctx>, } impl<'ctx> Display for Error<'ctx> { 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<'ctx> From> for error::Error { fn from(err: Error) -> Self { error::Error::new(err) } } pub type Result<'ctx, T> = result::Result>; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct TaskRef<'ctx> { pub id: &'ctx TaskID, pub args: Rc, } 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 pv_arg = match self.args.get("pv") { 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(pv) = pv_arg { write!(f, "@{}", pv)?; } 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 { 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, globals: TaskArgs, tasks: HashMap>, rootfs: (ArchiveHash, String), } impl Context { pub fn new(mut tasks: HashMap>, pins: Pins) -> error::Result { 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)> { 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>, provides: Vec, 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(TaskID { recipe: recipe.to_string(), task: task.to_string(), }) .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_with_args<'ctx>(&'ctx self, id: &'ctx TaskID, args: &TaskArgs) -> Result<&TaskDef> { self.tasks .get(id) .and_then(|tasks| Self::select_task(tasks, args)) .ok_or(Error { task: id, kind: ErrorKind::TaskNotFound, }) } pub fn get<'ctx>(&'ctx self, task: &TaskRef<'ctx>) -> Result<&TaskDef> { self.get_with_args(task.id, task.args.as_ref()) } fn task_ref<'ctx>(&'ctx self, id: &'ctx TaskID, args: &TaskArgs) -> Result { 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("pn", Some(task_def.meta.name.clone())); new_args.set("pv", task_def.meta.version.clone()); Ok(TaskRef { id, args: Rc::new(new_args), }) } pub fn parse<'ctx>(&'ctx self, s: &str) -> error::Result { let parsed = parse::parse_task(s).context("Invalid task syntax")?; let recipe = parsed.recipe.to_string(); let task = parsed.task.to_string(); let id = TaskID { recipe, task }; let (ctx_id, _) = self .tasks .get_key_value(&id) .with_context(|| format!("Task {} not found", id))?; let mut args = self.globals.clone(); if let Some(host) = parsed.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) = parsed.target { let plat = self .platforms .get(target) .with_context(|| format!("Platform '{}' not found", target))?; args.set("target", Some(plat)); } self.task_ref(ctx_id, &args) .with_context(|| format!("Failed to instantiate task {}", id)) } fn map_args<'ctx, 'args>( task: &'ctx TaskID, 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 inherit_ref<'ctx>(&'ctx self, dep: &'ctx InheritDep, args: &TaskArgs) -> Result { let mapped_args = Context::map_args(&dep.dep.id, &dep.dep.args, args, false)?; self.task_ref(&dep.dep.id, mapped_args.as_ref()) } pub fn output_ref<'ctx>( &'ctx self, dep: &'ctx OutputDep, args: &TaskArgs, build_dep: bool, ) -> Result> { let mapped_args = Context::map_args(&dep.dep.id, &dep.dep.args, args, build_dep)?; Ok(OutputRef { task: self.task_ref(&dep.dep.id, mapped_args.as_ref())?, output: &dep.output, }) } pub fn get_inherit_depend<'ctx>( &'ctx self, task_ref: &TaskRef<'ctx>, ) -> Result> { let task = self.get(task_ref)?; let inherit = match &task.inherit { Some(inherit) => inherit, None => return Ok(None), }; Some(self.inherit_ref(inherit, &task_ref.args)).transpose() } fn inherit_iter<'ctx>( &'ctx self, task_ref: &TaskRef<'ctx>, ) -> impl Iterator> { struct Iter<'ctx>(&'ctx Context, Option>>); impl<'ctx> Iterator for Iter<'ctx> { type Item = Result<'ctx, TaskRef<'ctx>>; fn next(&mut self) -> Option { let task_ref = match self.1.take()? { Ok(task_ref) => task_ref, Err(err) => return Some(Err(err)), }; self.1 = self.0.get_inherit_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> { let mut ret = HashSet::new(); let mut allow_noinherit = true; for current in self.inherit_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(dep, ¤t_ref.args, true)) .collect::>>()?; ret.extend(entries); allow_noinherit = false; } Ok(ret) } pub fn get_host_depends<'ctx>( &'ctx self, task_ref: &TaskRef<'ctx>, ) -> Result> { let mut ret = HashSet::new(); let mut allow_noinherit = true; for current in self.inherit_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(dep, ¤t_ref.args, false)) .collect::>>()?; ret.extend(entries); allow_noinherit = false; } Ok(ret) } } impl<'ctx> Index<&TaskRef<'ctx>> for &'ctx Context { type Output = TaskDef; fn index(&self, index: &TaskRef<'ctx>) -> &TaskDef { self.get(index).expect("Invalid TaskRef") } }