From 857fd02429c910f17b07e45c92e5c13eb8e740e5 Mon Sep 17 00:00:00 2001 From: Alexander Koz Date: Sun, 9 Jun 2024 03:03:31 +0400 Subject: [PATCH] partially rewritten assets-builder: now lazy, supports dependencies, supports cargo's auto-targets (#355); prototype of usage new source of unit-tree --- cargo/src/assets/mod.rs | 399 +++++++++++++++++++++++++ cargo/src/assets/plan.rs | 421 ++++++++++++++++++++++++++- cargo/src/main.rs | 1 + support/build/src/assets/plan.rs | 95 +++--- support/build/src/metadata/source.rs | 17 ++ 5 files changed, 895 insertions(+), 38 deletions(-) diff --git a/cargo/src/assets/mod.rs b/cargo/src/assets/mod.rs index f6a9f97b..e937b12f 100644 --- a/cargo/src/assets/mod.rs +++ b/cargo/src/assets/mod.rs @@ -35,6 +35,380 @@ pub struct AssetsArtifact { pub type AssetsArtifacts<'cfg> = HashMap<&'cfg Package, AssetsArtifact>; +pub mod proto { + use super::*; + + use plan::proto::MultiKey; + use plan::Difference; + use playdate::assets::plan::BuildPlan; + use playdate::assets::BuildReport; + use playdate::layout::Layout as _; + use playdate::metadata::format::AssetsOptions; + + use crate::utils::cargo::meta_deps::{MetaDeps, RootNode}; + use crate::layout::{PlaydateAssets, Layout}; + + + #[derive(Debug)] + pub struct AssetsArtifact { + pub package_id: PackageId, + pub layout: PlaydateAssets, + pub kind: AssetKind, + } + + + pub struct AssetsArtifactsNew<'t, 'cfg> { + artifacts: Vec, + index: BTreeMap>, + tree: &'t MetaDeps<'cfg>, + } + + + impl AssetsArtifactsNew<'_, '_> { + pub fn iter(&self) -> impl Iterator)> { + self.index + .iter() + .flat_map(|(key, index)| { + self.tree + .roots() + .into_iter() + .filter(|r| key.is_for(r)) + .map(|root| (root, index.as_slice())) + }) + .map(|(root, index)| { + let arts = index.into_iter().map(|i| &self.artifacts[*i]); + (root, arts) + }) + } + } + + impl core::fmt::Debug for AssetsArtifactsNew<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AssetsArtifacts") + .field_with("artifacts", |f| { + self.artifacts + .iter() + .enumerate() + .collect::>() + .fmt(f) + }) + .field("index", &self.index) + .finish_non_exhaustive() + } + } + + + pub fn build_all<'t, 'cfg>(cfg: &Config<'cfg>, + tree: &'t MetaDeps<'cfg>) + -> CargoResult> { + // planning: + let plans = plan::proto::plan_all(cfg, tree)?; + + // validation: + if let Err(err) = plan::proto::merge_all_virtually(cfg, tree, &plans) && + !cfg.compile_options.build_config.keep_going + { + return Err(err.context("Assets validation failed")); + } + + // results: + let mut artifacts = AssetsArtifactsNew { artifacts: Vec::with_capacity(plans.plans.len()), + index: Default::default(), + tree }; + + + // checking cache, apply each plan: + for (index, plan) in plans.plans.into_iter().enumerate() { + let key = plans.index.iter().find(|(_, v)| **v == index).expect("key").0; + + log::debug!("#{index} build (dev:{}) {}", key.dev, key.id); + + + let global_layout = CrossTargetLayout::new(cfg, key.id, None)?; + let mut layout = global_layout.assets_layout(cfg); + + + // clean layout if needed: + if !cfg.dry_run && cfg.compile_options.build_config.force_rebuild { + if !matches!(cfg.workspace.config().shell().verbosity(), Verbosity::Quiet) { + cfg.log().status("Clean", format!("assets for {}", key.id)); + } + layout.clean()?; + } + + + let mut locked = layout.lock_mut(cfg.workspace.config())?; + locked.prepare()?; + + // path of build-plan file: + let path = if key.dev { + locked.as_inner().assets_plan_for_dev(cfg, &key.id) + } else { + locked.as_inner().assets_plan_for(cfg, &key.id) + }; + + let mut cache = plan_cache(path, &plan)?; + if cfg.compile_options.build_config.force_rebuild { + cache.difference = Difference::Missing; + } + + + let dest = if key.dev { + locked.as_inner().assets_dev() + } else { + locked.as_inner().assets() + }; + + + // kind of assets just for log: + let kind_prefix = key.dev.then_some("dev-").unwrap_or_default(); + + + // build if needed: + if cache.difference.is_same() { + cfg.log().status( + "Skip", + format!( + "{} {kind_prefix}assets cache state is {:?}", + key.id, &cache.difference + ), + ); + } else { + cfg.log() + .status("Build", format!("{kind_prefix}assets for {}", key.id)); + cfg.log().verbose(|mut log| { + let dep_root = plan.crate_root(); + let dest = format!("destination: {:?}", dest.as_relative_to_root(cfg)); + log.status("", dest); + let src = format!("root: {:?}", dep_root.as_relative_to_root(cfg)); + log.status("", src); + }); + + + // Since we build each plan separately independently, the default options are sufficient. + // The options are needed further when merging assets into a package. + let dep_opts = Default::default(); + let report = apply(cache, plan, &dest, &dep_opts, cfg)?; + + + // print report: + for (x, (m, results)) in report.results.iter().enumerate() { + let results = results.iter().enumerate(); + let expr = m.exprs(); + let incs = m.sources(); + + for (y, res) in results { + let path = incs[y].target(); + let path = path.as_relative_to_root(cfg); + match res { + Ok(op) => { + cfg.log().verbose(|mut log| { + let msg = format!("asset [{x}:{y}] {}", path.display()); + log.status(format!("{op:?}"), msg) + }) + }, + Err(err) => { + use fs_extra::error::ErrorKind as FsExtraErrorKind; + + let error = match &err.kind { + FsExtraErrorKind::Io(err) => format!("IO: {err}"), + FsExtraErrorKind::StripPrefix(err) => format!("StripPrefix: {err}"), + FsExtraErrorKind::OsString(err) => format!("OsString: {err:?}"), + _ => err.to_string(), + }; + let message = format!( + "Asset [{x}:{y}], rule: '{} <- {} | {}', {error}", + expr.0.original(), + expr.1.original(), + path.display() + ); + + cfg.log().status_with_color("Error", message, Color::Red) + }, + }; + } + } + + // TODO: log report.exclusions + + if report.has_errors() && !cfg.compile_options.build_config.keep_going { + use anyhow::Error; + + #[derive(Debug)] + pub struct Mapping(String); + impl std::error::Error for Mapping {} + impl std::fmt::Display for Mapping { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } + } + + let err = report.results + .into_iter() + .filter_map(|(map, res)| { + if res.iter().any(|res| res.is_err()) { + let err = Mapping(map.pretty_print_compact()); + let err = res.into_iter() + .filter_map(|res| res.err()) + .fold(Error::new(err), Error::context); + Some(err) + } else { + None + } + }) + .fold(Error::msg("Assets build failed"), Error::context); + return Err(err); + } + + + // finally build with pdc: + // if not disallowed explicitly + if cfg.skip_prebuild { + const REASON: &str = "as requested"; + let msg = format!("{kind_prefix}assets pre-build for {}, {REASON}.", key.id); + cfg.log().status("Skip", msg); + } else { + let kind = key.dev.then_some(AssetKind::Dev).unwrap_or(AssetKind::Package); + match pdc::build(cfg, &key.id, locked.as_inner(), kind) { + Ok(_) => { + let msg = format!("{kind_prefix}assets for {}", key.id); + cfg.log().status("Finished", msg); + }, + Err(err) => { + let msg = format!("build {kind_prefix}assets with pdc failed: {err}"); + cfg.log().status_with_color("Error", msg, Color::Red); + if !cfg.compile_options.build_config.keep_going { + bail!("Assets build failed."); + } + }, + } + } + } + + + // Finale: + + locked.unlock(); + + + let kind = key.dev.then_some(AssetKind::Dev).unwrap_or(AssetKind::Package); + let art_index = artifacts.artifacts.len(); + artifacts.artifacts.push(AssetsArtifact { kind, + package_id: key.id, + layout: layout.clone() }); + + log::debug!( + "Assets artifact for {} at {:?}", + key.id, + crate::layout::Layout::dest(&layout).as_relative_to_root(cfg) + ); + + for (r_key, index) in plans.targets.iter().filter(|(_, i)| i.contains(&index)) { + artifacts.index + .entry(r_key.to_owned()) + .or_insert(Vec::with_capacity(index.len())) + .push(art_index); + } + } + + + cfg.log_extra_verbose(|mut logger| { + artifacts.iter().for_each(|(root, arts)| { + let root = format!( + "({}) {}::{}", + root.node().target().kind().description(), + root.node().package_id().name(), + root.node().target().name, + ); + logger.status("Assets", format!("artifacts for {root}:")); + arts.for_each(|art| { + let dest = match art.kind { + AssetKind::Package => art.layout.assets(), + AssetKind::Dev => art.layout.assets_dev(), + }; + let msg = format!( + "[{:?}] {} - {:?}", + art.kind, + art.package_id.name(), + dest.as_relative_to_root(cfg) + ); + logger.status("", msg); + }); + }); + }); + + Ok(artifacts) + } + + + struct PlanCache { + pub difference: Difference, + pub serialized: Option, + pub path: PathBuf, + } + + + #[must_use = "Cached plan must be used"] + fn plan_cache(path: PathBuf, plan: &BuildPlan<'_, '_>) -> CargoResult { + let mut serializable = plan.iter_flatten_meta().collect::>(); + serializable.sort(); + let json = serde_json::to_string(&serializable)?; + + let difference = if path.try_exists()? { + if std::fs::read_to_string(&path)? == json { + log::debug!("Cached plan is the same"); + Difference::Same + } else { + log::debug!("Cache mismatch, need diff & rebuild"); + Difference::Different + } + } else { + log::debug!("Cache mismatch, full rebuilding"); + Difference::Missing + }; + + let serialized = (!difference.is_same()).then_some(json); + + Ok(PlanCache { path, + difference, + serialized }) + } + + + fn apply<'l, 'r>(cache: PlanCache, + plan: BuildPlan<'l, 'r>, + dest: &Path, + options: &AssetsOptions, + config: &Config) + -> CargoResult> { + use crate::playdate::assets::apply_build_plan; + + let report = apply_build_plan(plan, dest, options)?; + // and finally save cache of just successfully applied plan: + // only if there is no errors + if !report.has_errors() { + if let Some(data) = cache.serialized.as_deref() { + log::trace!("writing cache to {:?}", cache.path); + std::fs::write(&cache.path, data)?; + config.log().verbose(|mut log| { + let path = cache.path.as_relative_to_root(config); + log.status("Cache", format_args!("saved to {}", path.display())); + }); + } else { + config.log().verbose(|mut log| { + log.status("Cache", "nothing to save"); + }); + } + } else { + config.log().verbose(|mut log| { + let message = "build has errors, so cache was not saved"; + log.status("Cache", message); + }); + } + + Ok(report) + } +} + + pub fn build<'cfg>(config: &'cfg Config) -> CargoResult> { let bcx = LazyBuildContext::new(config)?; let mut artifacts = AssetsArtifacts::new(); @@ -64,6 +438,31 @@ pub fn build<'cfg>(config: &'cfg Config) -> CargoResult> { let packages = deps_tree_metadata(package, &bcx, config)?; + // XXX: compare with beta-proto + #[cfg(debug_assertions)] + { + let tree = crate::utils::cargo::meta_deps::meta_deps(config)?; + + // planning: + let plans = plan::proto::plan_all(config, &tree)?; + + + for (p, _) in packages.iter() + .filter(|(_, m)| !m.assets().is_empty() || !m.dev_assets().is_empty()) + { + let id = p.package_id(); + assert!(plans.index.keys().any(|k| k.id == id), "not found: {id}"); + } + + // validation: + if let Err(err) = plan::proto::merge_all_virtually(config, &tree, &plans) && + !config.compile_options.build_config.keep_going + { + return Err(err.context("Assets validation failed")); + } + } + + // TODO: list deps in the plan for (package, metadata) in packages { diff --git a/cargo/src/assets/plan.rs b/cargo/src/assets/plan.rs index fac19243..501bbcdc 100644 --- a/cargo/src/assets/plan.rs +++ b/cargo/src/assets/plan.rs @@ -69,6 +69,425 @@ impl<'a, 'cfg> LazyEnvBuilder<'a, 'cfg> { pub type LockedLayout<'t> = LayoutLock<&'t mut PlaydateAssets>; +pub mod proto { + use std::borrow::Cow; + use std::collections::{BTreeMap, HashMap, HashSet}; + use std::path::Path; + + use super::{PackageId, Config, CargoResult}; + use super::assets_build_plan; + + use playdate::assets::plan::BuildPlan; + use playdate::consts::SDK_ENV_VAR; + use playdate::manifest::PackageSource as _; + use playdate::metadata::format::{AssetsOptions, Options}; + use playdate::metadata::source::MetadataSource as _; + + use crate::utils::cargo::meta_deps::MetaDeps; + use crate::utils::cargo::meta_deps::{Node, RootNode}; + use crate::utils::path::AsRelativeTo; + + + pub struct AssetsPlans<'cfg> { + pub plans: Vec>, + /// per-dep ?dev plan + pub index: BTreeMap, + /// per-root plans to merge + pub targets: BTreeMap>, + } + + #[derive(Debug, Hash, PartialEq, PartialOrd, Eq, Ord)] + pub struct Key { + pub id: PackageId, + pub dev: bool, + } + + impl Key { + fn with_dev(&self, dev: bool) -> Self { + Self { id: self.id.to_owned(), + dev } + } + } + impl From<&'_ Node<'_>> for Key { + fn from(node: &'_ Node<'_>) -> Self { + Key { id: node.package_id().to_owned(), + dev: node.target().is_dev() } + } + } + + + #[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Eq, Ord)] + pub struct MultiKey { + /// Dependencies + id: Vec, + /// Primary target is dev + dev: bool, + } + impl From<&'_ RootNode<'_>> for MultiKey { + fn from(root: &'_ RootNode<'_>) -> Self { + Self { dev: root.node().target().is_dev(), + id: root.deps() + .into_iter() + .map(|d| d.package_id().to_owned()) + .collect() } + } + } + impl MultiKey { + pub fn dev(&self) -> bool { self.dev } + + pub fn is_for(&self, root: &'_ RootNode<'_>) -> bool { + root.node().target().is_dev() == self.dev && + root.deps() + .into_iter() + .enumerate() + .all(|(i, d)| self.id.get(i).filter(|k| *k == d.package_id()).is_some()) + // root.deps().into_iter().enumerate().all(|(i, d)| d.package_id() == &self.id[i]) + } + } + + + pub fn plan_all<'cfg>(cfg: &Config<'cfg>, tree: &MetaDeps<'cfg>) -> CargoResult> { + // results: + let mut plans = AssetsPlans { plans: Vec::with_capacity(tree.roots().len() * 2), + index: BTreeMap::new(), + targets: BTreeMap::new() }; + + // prepare env: + let global_env: BTreeMap<_, _> = + std::env::vars().into_iter() + .map(|(k, v)| (k, v)) + .chain({ + cfg.sdk() + .map(|sdk| sdk.path()) + .ok() + .or_else(|| cfg.sdk_path.as_deref()) + .map(|p| (SDK_ENV_VAR.to_string(), p.display().to_string())) + .into_iter() + }) + .collect(); + let env = |id: &PackageId, root: &Path| { + use playdate::config::Env; + + let vars = [ + (Cow::from("CARGO_PKG_NAME"), Cow::from(id.name().as_str())), + (Cow::from("CARGO_MANIFEST_DIR"), root.display().to_string().into()), + ]; + + let iter = global_env.iter() + .map(|(k, v)| (Cow::Borrowed(k.as_str()), Cow::Borrowed(v.as_str()))) + .chain(vars.into_iter()); + + Env::try_from_iter(iter).map_err(|err| anyhow::anyhow!("{err}")) + }; + + + for root in tree.roots() { + let meta_source = root.as_source(); + + let options = meta_source.assets_options(); + + let root_is_dev = root.node().target().is_dev(); + + log::debug!( + "planning for {} ({}) +dev:{root_is_dev}", + root.package_id(), + root.node().target().kind().description() + ); + log::debug!(" dependencies are allowed: {}", options.dependencies); + + + let plan_key = MultiKey::from(root); + if plans.targets.contains_key(&plan_key) { + log::debug!(" skip: already done"); + continue; + } + + + let mut indices = Vec::::with_capacity(root.deps().len()); + + for dep in root.deps().iter().rev() { + log::debug!(" planning dep: {}", dep.package_id()); + + let crate_root = dep.manifest_path() + .and_then(|p| p.parent()) + .ok_or_else(|| anyhow::anyhow!("Unable to get crate root"))?; + + let env = env(dep.package_id(), crate_root)?; + + let with_dev = root_is_dev && dep.package_id() == root.package_id(); + + let dep_key = Key::from(dep).with_dev(with_dev); + + + let plan_for = + |plans: &mut AssetsPlans, indices: &mut Vec, key: Key, dev: bool| -> anyhow::Result<()> { + let source = dep.as_source(); + let name_log = dev.then_some("dev-").unwrap_or_default(); + if let Some(assets) = source.metadata() + .map(|m| if dev { m.dev_assets() } else { m.assets() }) && + !assets.is_empty() + { + // let plan = + match assets_build_plan(&env, assets, &options, Some(crate_root.into())) { + Ok(plan) => { + let pid = key.id.clone(); + let is_dev = key.dev; + let dev_index = plans.plans.len(); + plans.index.insert(key, dev_index); + plans.plans.push(plan); + indices.push(dev_index); + + log::debug!(" done: +#{dev_index} (dev:{is_dev})"); + cfg.log().verbose(|mut log| { + log.status("Plan", format_args!("{name_log}assets for {pid} planned",)) + }) + }, + Err(err) => { + cfg.log() + .error(format_args!("{err}, caused when planning {name_log}assets for {}", key.id)); + return cfg.compile_options + .build_config + .keep_going + .then_some(()) + .ok_or(err.into()); + }, + } + } else { + cfg.log().verbose(|mut log| { + log.status( + "Skip", + format_args!( + "{name_log}assets for {} without plan, reason: empty", + key.id + ), + ) + }) + } + Ok(()) + }; + + if let Some(i) = plans.index.get(&dep_key) { + // we already have plan for this dep + log::debug!(" done (~#{i}) (dev:{})", dep_key.dev); + indices.push(*i); + } else if with_dev && let Some(base_index) = plans.index.get(&dep_key.with_dev(false)).copied() { + // we already have plan for this dep, but not for dev part + indices.push(base_index); + log::debug!(" done (~#{base_index}) (dev:{})", dep_key.dev); + + plan_for(&mut plans, &mut indices, dep_key, true)?; + } else { + // else just build a plan + plan_for(&mut plans, &mut indices, dep_key, false)?; + + // TODO: it must be norm+dev assets, if dev needed - `with_dev` + if with_dev { + log::warn!(" TODO: WITH DEV") + } + } + } + + + plans.targets + .entry(plan_key) + .and_modify(|vec| vec.append(&mut indices)) + .or_insert(indices); + } + + + // report: + cfg.log() + .status("Assets", "planning complete for all requested targets"); + cfg.log_extra_verbose(|mut logger| { + for (k, v) in &plans.targets { + let dev = k.dev.then_some(" +dev").unwrap_or_default(); + let key = k.id.iter().map(|p| p.name()).collect::>().join(", "); + logger.status("Plans", format_args!("for{dev} {key}")); + for i in v { + let plan = &plans.plans[*i]; + logger.status("Plan", format_args!("#{i}:\n{plan:>10}")); + } + } + }); + + // check: + for root in tree.roots() { + let key = MultiKey::from(root); + debug_assert!(plans.targets.contains_key(&key)); + } + + Ok(plans) + } + + + /// Try to merge virtually and validate. + /// Emits warnings and errors, returns errors-chain. + pub fn merge_all_virtually<'cfg>(cfg: &Config<'cfg>, + tree: &MetaDeps<'cfg>, + plans: &AssetsPlans<'cfg>) + -> CargoResult<()> { + // prepare context: + let mut root_package: HashMap<&MultiKey, HashSet<&PackageId>> = HashMap::with_capacity(tree.roots().len()); + let mut root_options: HashMap<&PackageId, AssetsOptions> = HashMap::with_capacity(tree.roots().len()); + + plans.targets + .iter() + .flat_map(|(key, _)| { + tree.roots() + .into_iter() + .filter(|r| key.is_for(r)) + .map(move |r| (key, r)) + }) + .for_each(|(key, root)| { + root_package.entry(key) + .or_insert_with(|| HashSet::with_capacity(tree.roots().len())) + .insert(root.package_id()); + + if !root_options.contains_key(root.package_id()) { + let options = root.as_source().assets_options(); + root_options.insert(root.package_id(), options); + } + }); + + + // Buffered errors: + let mut overrides = Vec::new(); + + + // merge, analyse: + for (key, index) in plans.targets.iter() { + // Note, correct ordering in `index` guaranteed by the planner. + + // Need merge many into one: + let _many = index.len() > 1; + + for root_id in root_package.get(key).into_iter().flat_map(|set| set.iter()) { + use playdate::assets::plan::*; + + log::trace!("v-merging for {} (dev:{})", root_id.name(), key.dev); + + let options = &root_options[root_id]; + + let mut _plan: Vec = Default::default(); + + + let mut targets = BTreeMap::new(); + let mut sources = BTreeMap::new(); + + + for i in index { + let next = &plans.plans[*i]; + + for (kind, dst, src) in next.iter_flatten() { + let target: Cow<_> = match kind { + MappingKind::AsIs | MappingKind::ManyInto => dst, + MappingKind::Into => dst.join(src.file_name().expect("filename")), + }.into(); + + // Save for future check if we already have this source: + sources.entry(Cow::from(src)) + .or_insert_with(|| Vec::with_capacity(2)) + .push(*i); + + // Check if we already have this target: + targets.entry(target.clone()) + .or_insert_with(|| Vec::with_capacity(2)) + .push(*i); + + if let Some(past) = targets.get(&target) && + past.len() > 1 + { + let id = past.into_iter() + .flat_map(|x| plans.index.iter().find_map(|(key, i)| (i == x).then_some(key))) + .collect::>(); + debug_assert!(!id.is_empty()); + + let this = id.last().unwrap(); + let other = &id[..id.len() - 1]; + + let dev = this.dev.then_some("dev-").unwrap_or_default(); + let others = other.is_empty() + .then_some("by itself") + .map(Cow::from) + .unwrap_or_else(|| { + other.into_iter() + .map(|k| { + let dev = k.dev.then_some("dev-").unwrap_or_default(); + format!("{}'s '{dev}assets'", k.id.name()) + }) + .collect::>() + .join(", ") + .into() + }); + + let name = this.id.name(); + let root_name = root_id.name(); + let why = format!("but that's not allowed by the top-level crate {root_name}"); + let msg = format!("{name}'s `{dev}assets.{target:?}` overrides {others}, {why}"); + + if options.overwrite { + cfg.log().warn(msg) + } else { + cfg.log().error(&msg); + overrides.push(msg); + } + } + } + } + + // Check if we already have this source: + for (src, index) in sources { + if index.len() < 2 { + continue; + } + + let id = index.into_iter() + .flat_map(|x| plans.index.iter().find_map(|(key, i)| (*i == x).then_some(key))) + .collect::>(); + debug_assert!(!id.is_empty()); + + let src_rel = src.as_relative_to_root(cfg); + let others = id.is_empty() + .then_some("itself") + .map(Cow::from) + .unwrap_or_else(|| { + id.into_iter() + .map(|k| { + let dev = k.dev.then_some("dev-").unwrap_or_default(); + format!("{}'s '{dev}assets'", k.id.name()) + }) + .collect::>() + .join(", ") + .into() + }); + let msg = format!("asset {src_rel:?} used multiple times in {others}"); + cfg.log().warn(msg); + } + } + } + + + { + use err::Override; + use anyhow::Error; + overrides.is_empty() + .then_some(()) + .ok_or_else(|| overrides.into_iter().fold(Error::new(Override), Error::context)) + } + } + + + mod err { + #[derive(Debug)] + pub struct Override; + impl std::error::Error for Override {} + impl std::fmt::Display for Override { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { "Not allowed override".fmt(f) } + } + } +} + + /// Returns `None` if there is no `assets` metadata. pub fn plan_for<'cfg, 'env, 'l>(config: &'cfg Config, package: &'cfg Package, @@ -171,7 +590,7 @@ pub struct CachedPlan<'t, 'cfg> { impl<'t, 'cfg> CachedPlan<'t, 'cfg> { #[must_use = "Cached plan must be used"] fn new(path: PathBuf, plan: AssetsPlan<'t, 'cfg>) -> CargoResult { - let mut serializable = plan.iter_flatten().collect::>(); + let mut serializable = plan.iter_flatten_meta().collect::>(); serializable.sort_by_key(|(_, _, (p, _))| p.to_string_lossy().to_string()); let json = serde_json::to_string(&serializable)?; diff --git a/cargo/src/main.rs b/cargo/src/main.rs index fa12cd80..71c0eed9 100644 --- a/cargo/src/main.rs +++ b/cargo/src/main.rs @@ -4,6 +4,7 @@ #![feature(btree_extract_if)] #![feature(const_trait_impl)] #![feature(let_chains)] +#![feature(debug_closure_helpers)] extern crate build as playdate; diff --git a/support/build/src/assets/plan.rs b/support/build/src/assets/plan.rs index a365a468..4fb32159 100644 --- a/support/build/src/assets/plan.rs +++ b/support/build/src/assets/plan.rs @@ -311,27 +311,36 @@ impl std::fmt::Display for BuildPlan<'_, '_> { }; - let print = - |f: &mut std::fmt::Formatter<'_>, inc: &Match, (left, right): &(Expr, Expr)| -> std::fmt::Result { - let target = inc.target(); - let source = inc.source(); - let left = left.original(); - let right = right.original(); - align(f)?; - write!(f, "{target:#?} <- {source:#?} ({left} = {right})") - }; + let print = |f: &mut std::fmt::Formatter<'_>, + inc: &Match, + (left, right): &(Expr, Expr), + br: bool| + -> std::fmt::Result { + let target = inc.target(); + let source = inc.source(); + let left = left.original(); + let right = right.original(); + align(f)?; + write!(f, "{target:#?} <- {source:#?} ({left} = {right})")?; + if br { writeln!(f) } else { Ok(()) } + }; - for item in self.as_inner() { + let items = self.as_inner(); + let len = items.len(); + for (i, item) in items.into_iter().enumerate() { + let last = i == len - 1; match item { - Mapping::AsIs(inc, exprs) => print(f, inc, exprs)?, - Mapping::Into(inc, exprs) => print(f, inc, exprs)?, + Mapping::AsIs(inc, exprs) => print(f, inc, exprs, !last)?, + Mapping::Into(inc, exprs) => print(f, inc, exprs, !last)?, Mapping::ManyInto { sources, target, exprs, .. } => { - for inc in sources { - print(f, &Match::new(inc.source(), target.join(inc.target())), exprs)?; - writeln!(f)?; + let len = sources.len(); + for (i_in, inc) in sources.iter().enumerate() { + let last = last && i_in == len - 1; + let m = Match::new(inc.source(), target.join(inc.target())); + print(f, &m, exprs, !last)?; } }, } @@ -357,31 +366,33 @@ impl BuildPlan<'_, '_> { }) } - pub fn iter_flatten( - &self) - -> impl Iterator))> + '_ { + pub fn iter_flatten(&self) -> impl Iterator + '_ { let pair = |inc: &Match| { (inc.target().to_path_buf(), abs_if_existing_any(inc.source(), &self.crate_root).to_path_buf()) }; - self.as_inner() - .iter() - .flat_map(move |mapping| { - let mut rows = Vec::new(); - let kind = mapping.kind(); - match mapping { - Mapping::AsIs(inc, _) | Mapping::Into(inc, _) => rows.push(pair(inc)), - Mapping::ManyInto { sources, target, .. } => { - rows.extend(sources.iter() - .map(|inc| pair(&Match::new(inc.source(), target.join(inc.target()))))); - }, - }; - rows.into_iter().map(move |(l, r)| (kind, l, r)) - }) - .map(|(k, t, p)| { - let time = p.metadata().ok().and_then(|m| m.modified().ok()); - (k, t, (p, time)) - }) + self.as_inner().iter().flat_map(move |mapping| { + let mut rows = Vec::new(); + let kind = mapping.kind(); + match mapping { + Mapping::AsIs(inc, _) | Mapping::Into(inc, _) => rows.push(pair(inc)), + Mapping::ManyInto { sources, target, .. } => { + rows.extend(sources.iter().map(|inc| { + pair(&Match::new(inc.source(), target.join(inc.target()))) + })); + }, + }; + rows.into_iter().map(move |(l, r)| (kind, l, r)) + }) + } + + pub fn iter_flatten_meta( + &self) + -> impl Iterator))> + '_ { + self.iter_flatten().map(|(k, t, p)| { + let time = p.metadata().ok().and_then(|m| m.modified().ok()); + (k, t, (p, time)) + }) } } @@ -454,10 +465,20 @@ impl Mapping<'_, '_> { Mapping::ManyInto { .. } => MappingKind::ManyInto, } } + + + pub fn pretty_print_compact(&self) -> String { + let (k, l, r) = match self { + Mapping::AsIs(_, (l, r)) => ('=', l, r), + Mapping::Into(_, (l, r)) => ('I', l, r), + Mapping::ManyInto { exprs: (l, r), .. } => ('M', l, r), + }; + format!("{{{k}:{:?}={:?}}}", l.original(), r.original()) + } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] pub enum MappingKind { /// Copy source __to__ target. diff --git a/support/build/src/metadata/source.rs b/support/build/src/metadata/source.rs index 6b5e1870..758ab74f 100644 --- a/support/build/src/metadata/source.rs +++ b/support/build/src/metadata/source.rs @@ -31,6 +31,23 @@ pub trait PackageSource { /// If this __and__ `metadata.options` is `None` - [`Options::default()`] is used. fn default_options(&self) -> Option<&OptionsDefault> { None } + /// Cloned or default [`AssetsOptions`] from `metadata.options`, + /// merged with `default_options`, + /// if `metadata.options.workspace` is `true`. + fn assets_options(&self) -> AssetsOptions { + // TODO: impl assets-options merge instead just choosing one + self.metadata() + .and_then(|m| { + m.options().assets.clone().or_else(|| { + m.options() + .workspace + .then(|| self.default_options().and_then(|o| o.assets.clone())) + .flatten() + }) + }) + .unwrap_or_default() + } + /// Names of `bin` cargo-targets. fn bins(&self) -> &[&str]; /// Names of `example` cargo-targets.