diff --git a/Cargo.lock b/Cargo.lock index 844d1fd2..04780e82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "cargo-playdate" -version = "0.5.0-beta.3" +version = "0.5.0-beta.4" dependencies = [ "anstyle", "anyhow", @@ -2759,9 +2759,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8720bf4c5bfb5b6c350840c4cd14b787bf00ed51c148c857fbf7a6ddb7062764" +checksum = "9f3935c160d00ac752e09787e6e6bfc26494c2183cc922f1bc678a60d4733bc2" [[package]] name = "httpdate" @@ -4182,7 +4182,7 @@ dependencies = [ [[package]] name = "playdate-build" -version = "0.4.0-pre2" +version = "0.4.0-pre3" dependencies = [ "dirs", "fs_extra", diff --git a/Cargo.toml b/Cargo.toml index a0eae196..27a1330c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ system = { version = "0.3", path = "api/system", package = "playdate-system", de sys = { version = "0.4", path = "api/sys", package = "playdate-sys", default-features = false } tool = { version = "0.1", path = "support/tool", package = "playdate-tool" } -build = { version = "=0.4.0-pre2", path = "support/build", package = "playdate-build", default-features = false } +build = { version = "=0.4.0-pre3", path = "support/build", package = "playdate-build", default-features = false } utils = { version = "0.3", path = "support/utils", package = "playdate-build-utils", default-features = false } device = { version = "0.2", path = "support/device", package = "playdate-device" } simulator = { version = "0.1", path = "support/sim-ctrl", package = "playdate-simulator-utils", default-features = false } diff --git a/cargo/Cargo.toml b/cargo/Cargo.toml index 9cda1354..d845e5da 100644 --- a/cargo/Cargo.toml +++ b/cargo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-playdate" -version = "0.5.0-beta.3" +version = "0.5.0-beta.4" readme = "README.md" description = "Build tool for neat yellow console." keywords = ["playdate", "build", "cargo", "plugin", "cargo-subcommand"] diff --git a/cargo/src/assets/mod.rs b/cargo/src/assets/mod.rs index 10ac7e80..f42e7a3c 100644 --- a/cargo/src/assets/mod.rs +++ b/cargo/src/assets/mod.rs @@ -38,7 +38,7 @@ pub type AssetsArtifacts<'cfg> = HashMap<&'cfg Package, AssetsArtifact>; pub mod proto { use super::*; - use plan::proto::MultiKey; + use plan::proto::RootKey; use plan::Difference; use playdate::assets::plan::BuildPlan; use playdate::assets::BuildReport; @@ -59,7 +59,7 @@ pub mod proto { pub struct AssetsArtifactsNew<'t, 'cfg> { artifacts: Vec, - index: BTreeMap>, + index: BTreeMap>, tree: &'t MetaDeps<'cfg>, } @@ -316,11 +316,17 @@ pub mod proto { cfg.log_extra_verbose(|mut logger| { artifacts.iter().for_each(|(root, arts)| { + use cargo::core::compiler::CompileKind; + let ct: Cow<_> = match root.node().unit().platform { + CompileKind::Host => "host".into(), + CompileKind::Target(kind) => kind.short_name().to_owned().into(), + }; + let root = format!( - "({}) {}::{}", + "{} {} of {} for {ct}", root.node().target().kind().description(), - root.node().package_id().name(), root.node().target().name, + root.node().package_id().name(), ); logger.status("Assets", format!("artifacts for {root}:")); arts.for_each(|art| { @@ -354,6 +360,15 @@ pub mod proto { fn plan_cache(path: PathBuf, plan: &BuildPlan<'_, '_>) -> CargoResult { let mut serializable = plan.iter_flatten_meta().collect::>(); serializable.sort(); + + #[derive(serde::Serialize)] + struct SerializablePlan<'t> { + items: &'t [(playdate::assets::plan::MappingKind, PathBuf, (PathBuf, Option))], + env: &'t BTreeMap, + } + + let serializable = SerializablePlan { items: &serializable, + env: plan.used_env_vars() }; let json = serde_json::to_string(&serializable)?; let difference = if path.try_exists()? { diff --git a/cargo/src/assets/plan.rs b/cargo/src/assets/plan.rs index 18c2bbe0..49a4df31 100644 --- a/cargo/src/assets/plan.rs +++ b/cargo/src/assets/plan.rs @@ -93,9 +93,10 @@ pub mod proto { /// per-dep ?dev plan pub index: BTreeMap, /// per-root plans to merge - pub targets: BTreeMap>, + pub targets: BTreeMap>, } + /// Target-agnostic package key. #[derive(Debug, Clone, Copy, Hash, PartialEq, PartialOrd, Eq, Ord)] pub struct Key { pub id: PackageId, @@ -116,20 +117,21 @@ pub mod proto { } + /// Target-agnostic root-package key with all dependencies. #[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Eq, Ord)] - pub struct MultiKey { + pub struct RootKey { /// Dependencies id: Vec, /// Primary target is dev dev: bool, } - impl From<&'_ RootNode<'_>> for MultiKey { + impl From<&'_ RootNode<'_>> for RootKey { fn from(root: &'_ RootNode<'_>) -> Self { Self { dev: root.node().target().is_dev(), id: root.deps().iter().map(|d| d.package_id().to_owned()).collect() } } } - impl MultiKey { + impl RootKey { pub fn dev(&self) -> bool { self.dev } pub fn is_for(&self, root: &'_ RootNode<'_>) -> bool { @@ -138,7 +140,6 @@ pub mod proto { .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]) } } @@ -176,7 +177,8 @@ pub mod proto { }; - for root in tree.roots() { + // use target-agnostic selection of roots: + for root in tree.roots_compile_target_agnostic() { let meta_source = root.as_source(); let options = meta_source.assets_options(); @@ -191,7 +193,7 @@ pub mod proto { log::debug!(" dependencies are allowed: {}", options.dependencies()); - let plan_key = MultiKey::from(root); + let plan_key = RootKey::from(root); if plans.targets.contains_key(&plan_key) { log::debug!(" skip: already done"); continue; @@ -222,12 +224,12 @@ pub mod proto { .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; let is_dev = key.dev; let dev_index = plans.plans.len(); + let compile_target_agnostic = plan.compile_target_agnostic(); plans.index.insert(key, dev_index); plans.plans.push(plan); indices.push(dev_index); @@ -235,7 +237,12 @@ pub mod proto { log::debug!(" done: +#{dev_index} (dev:{is_dev})"); cfg.log().verbose(|mut log| { log.status("Plan", format_args!("{name_log}assets for {pid} planned",)) - }) + }); + + if !compile_target_agnostic { + cfg.log() + .error("Assets is not compile-target-agnostic, this is not supported"); + } }, Err(err) => { cfg.log() @@ -307,7 +314,7 @@ pub mod proto { // check: for root in tree.roots() { - let key = MultiKey::from(root); + let key = RootKey::from(root); debug_assert!(plans.targets.contains_key(&key)); } @@ -322,7 +329,7 @@ pub mod proto { plans: &AssetsPlans<'cfg>) -> CargoResult<()> { // prepare context: - let mut root_package: HashMap<&MultiKey, HashSet<&PackageId>> = HashMap::with_capacity(tree.roots().len()); + let mut root_package: HashMap<&RootKey, HashSet<&PackageId>> = HashMap::with_capacity(tree.roots().len()); let mut root_options: HashMap<&PackageId, AssetsOptions> = HashMap::with_capacity(tree.roots().len()); plans.targets diff --git a/cargo/src/main.rs b/cargo/src/main.rs index f0c1b885..4a42619b 100644 --- a/cargo/src/main.rs +++ b/cargo/src/main.rs @@ -64,7 +64,8 @@ fn main() -> CargoResult<()> { fn execute(config: &Config) -> CargoResult<()> { match config.cmd { cli::cmd::Cmd::Assets => { - let _result = assets::build(config)?; + let deps_tree = crate::utils::cargo::meta_deps::meta_deps(config)?; + assets::proto::build_all(config, &deps_tree)?; }, cli::cmd::Cmd::Build => { diff --git a/cargo/src/utils/cargo/meta_deps.rs b/cargo/src/utils/cargo/meta_deps.rs index ad7d7d86..f91df888 100644 --- a/cargo/src/utils/cargo/meta_deps.rs +++ b/cargo/src/utils/cargo/meta_deps.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::path::Path; use cargo::core::compiler::CompileMode; @@ -251,6 +251,40 @@ impl<'t> MetaDeps<'t> { pub fn roots(&self) -> &[RootNode<'t>] { self.roots.as_slice() } + + /// Groups of root-units by compile-target. + /// + /// Possible groups: + /// 1. Contains just one root-unit; + /// 2. Contains two units with __same cargo-target__ and different rustc-targets __including__ playdate. + pub fn root_groups(&self) { unimplemented!() } + + + /// Returns first root for each group by [`roots_by_compile_target`][Self::roots_by_compile_target]. + pub fn roots_compile_target_agnostic(&self) -> impl Iterator> { + self.roots_by_compile_target().into_iter().flat_map(|(key, _)| { + self.roots().iter().find(|n| { + n.node().unit().package_id == + *key.package_id && + &n.node().unit().target == key.target + }) + }) + } + + /// Groups of root-units by target. + /// + /// Grouping: (package_id + cargo-target) => [rustc-target]. + pub fn roots_by_compile_target(&self) -> BTreeMap> { + self.roots.iter().fold(BTreeMap::new(), |mut acc, root| { + let key = TargetKey::from(root); + acc.entry(key) + .or_insert(BTreeSet::new()) + .insert(root.node().unit().platform); + acc + }) + } + + pub fn root_for(&self, id: &PackageId, tk: &TargetKindWild, tname: &str) -> CargoResult<&RootNode<'t>> { self.roots .iter() @@ -280,6 +314,28 @@ impl<'t> MetaDeps<'t> { } +#[derive(Debug, Hash, PartialEq, PartialOrd, Eq, Ord)] +pub struct TargetKey<'t> { + package_id: &'t PackageId, + target: &'t UnitTarget, +} + +impl<'t> From<&'t RootNode<'t>> for TargetKey<'t> { + fn from(node: &'t RootNode<'t>) -> Self { TargetKey::from(node.node()) } +} + +impl<'t> From<&'t Node<'t>> for TargetKey<'t> { + fn from(node: &'t Node<'t>) -> Self { TargetKey::from(node.unit()) } +} + +impl<'t> From<&'t Unit> for TargetKey<'t> { + fn from(unit: &'t Unit) -> Self { + TargetKey { package_id: &unit.package_id, + target: &unit.target } + } +} + + pub trait DependenciesAllowed { fn deps_allowed(&self) -> bool; } diff --git a/support/build/Cargo.toml b/support/build/Cargo.toml index 8c5e96bc..cb31db59 100644 --- a/support/build/Cargo.toml +++ b/support/build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "playdate-build" -version = "0.4.0-pre2" +version = "0.4.0-pre3" readme = "README.md" description = "Utils that help to build package for Playdate" keywords = ["playdate", "package", "encoding", "manifest", "assets"] diff --git a/support/build/src/assets/plan.rs b/support/build/src/assets/plan.rs index 4fb32159..28090dd4 100644 --- a/support/build/src/assets/plan.rs +++ b/support/build/src/assets/plan.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::hash::Hash; use std::borrow::Cow; use std::str::FromStr; @@ -17,7 +18,7 @@ pub fn build_plan<'l, 'r, S>(env: &Env, options: &AssetsOptions, crate_root: Option>) -> Result, super::Error> - where S: Eq + Hash + ToString + where S: Eq + Hash + ToString + AsRef { // copy_unresolved => get all files with glob // include_unresolved => same @@ -32,7 +33,7 @@ pub fn build_plan<'l, 'r, S>(env: &Env, const PATH_SEPARATOR: [char; 2] = [MAIN_SEPARATOR, '/']; - let enver = EnvResolver::new(); + let enver = EnvResolver::with_cache(); let crate_root = crate_root.unwrap_or_else(|| env.cargo_manifest_dir().into()); let link_behavior = options.link_behavior(); @@ -52,6 +53,13 @@ pub fn build_plan<'l, 'r, S>(env: &Env, } }; + + let assets_requirements = assets.env_required(); + let compile_target_agnostic = !assets_requirements.contains(&"TARGET"); + log::debug!("assets required env vars: {assets_requirements:?}"); + log::debug!("compile-target-agnostic: {compile_target_agnostic}"); + + match assets { AssetsRules::List(vec) => { include_unresolved.extend( @@ -195,8 +203,14 @@ pub fn build_plan<'l, 'r, S>(env: &Env, // TODO: find source duplicates and warn! + + // get all used env vars: + let vars = enver.into_cache().unwrap_or_default(); + Ok(BuildPlan { plan: mappings, - crate_root: crate_root.to_path_buf() }) + crate_root: crate_root.to_path_buf(), + compile_target_agnostic, + vars }) } @@ -273,6 +287,11 @@ pub struct BuildPlan<'left, 'right> { plan: Vec>, /// Root directory of associated crate crate_root: PathBuf, + + /// `true` if assets requires `TARGET` env var + compile_target_agnostic: bool, + /// Environment variables used by assets + vars: BTreeMap, } impl<'left, 'right> BuildPlan<'left, 'right> { @@ -285,6 +304,9 @@ impl<'left, 'right> BuildPlan<'left, 'right> { let old = std::mem::replace(&mut self.crate_root, path.into()); old } + + pub fn compile_target_agnostic(&self) -> bool { self.compile_target_agnostic } + pub fn used_env_vars(&self) -> &BTreeMap { &self.vars } } impl<'left, 'right> AsRef<[Mapping<'left, 'right>]> for BuildPlan<'left, 'right> { @@ -500,6 +522,79 @@ impl std::fmt::Display for MappingKind { } +pub trait EnvRequired<'t> { + type Item; + type Output: IntoIterator; + + fn env_required(&'t self) -> Self::Output; +} + + +impl<'t> EnvRequired<'t> for Mapping<'_, '_> where Self: 't { + type Item = &'t str; + type Output = Vec; + + fn env_required(&'t self) -> Self::Output { + let resolver = EnvResolver::new(); + + let keys = match self { + Mapping::AsIs(_, (l, r)) => [l.original(), r.original()], + Mapping::Into(_, (l, r)) => [l.original(), r.original()], + Mapping::ManyInto { exprs: (l, r), .. } => [l.original(), r.original()], + }; + + keys.into_iter() + .flat_map(|s| resolver.matches(s)) + .map(|m| m.as_str()) + .collect() + } +} + + +impl<'s, S: 's + Eq + Hash + AsRef> EnvRequired<'s> for AssetsRules { + type Item = &'s str; + type Output = Vec; + + fn env_required(&'s self) -> Self::Output { + let resolver = EnvResolver::new(); + self.env_required_with(&resolver).collect() + } +} + + +impl> AssetsRules { + pub fn all_keys(&self) -> impl Iterator { + let mut a = None; + let mut b = None; + match self { + AssetsRules::List(list) => { + a = Some(list.into_iter().map(AsRef::as_ref)); + }, + AssetsRules::Map(map) => { + let keys = map.keys().into_iter().map(AsRef::as_ref); + let values = map.values().into_iter().filter_map(|v| { + match v { + RuleValue::String(s) => Some(s.as_str()), + RuleValue::Boolean(_) => None, + } + }); + b = Some(keys.chain(values)); + }, + }; + + a.into_iter().flatten().chain(b.into_iter().flatten()) + } + + pub fn env_required_with<'t: 'r, 'r>(&'t self, + resolver: &'r EnvResolver) + -> impl Iterator + 'r { + self.all_keys() + .flat_map(|s| resolver.matches(s)) + .map(|m| m.as_str()) + } +} + + #[cfg(test)] mod tests { use std::collections::HashMap; diff --git a/support/build/src/assets/resolver.rs b/support/build/src/assets/resolver.rs index 1299d7b3..4a09c1a4 100644 --- a/support/build/src/assets/resolver.rs +++ b/support/build/src/assets/resolver.rs @@ -1,3 +1,5 @@ +use std::cell::RefCell; +use std::collections::BTreeMap; use std::hash::Hash; use std::borrow::Cow; use std::str; @@ -109,23 +111,47 @@ pub fn sanitize_path_pattern(path: &str) -> Cow<'_, str> { } -// TODO: use config.env .unwrap_or -pub struct EnvResolver(Regex); +pub struct EnvResolver(Regex, Option>>); impl EnvResolver { - pub fn new() -> Self { Self(Regex::new(r"(\$\{([^}]+)\})").unwrap()) } + pub fn new() -> Self { Self(Regex::new(r"(\$\{([^}]+)\})").unwrap(), None) } + + pub fn with_cache() -> Self { + let mut this = Self::new(); + this.1 = Some(RefCell::new(BTreeMap::new())); + this + } + + pub fn cache(&self) -> Option>> { self.1.as_ref().map(|v| v.borrow()) } + pub fn into_cache(self) -> Option> { self.1.map(|cell| cell.into_inner()) } } impl Default for EnvResolver { fn default() -> Self { Self::new() } } impl EnvResolver { + /// Do not uses cache. + pub fn matches<'t, 's: 't>(&'t self, s: &'s str) -> impl Iterator> + 't { + self.0.captures_iter(s.as_ref()).flat_map(|caps| caps.get(2)) + } + + /// Uses cache if it has been set. pub fn str>(&self, s: S, env: &Env) -> String { let re = &self.0; + let cache = self.1.as_ref(); // Possible recursion for case "${VAR}" where $VAR="${VAR}" let mut anti_recursion_counter: u8 = 42; let mut replaced = String::from(s.as_ref()); + + fn resolve<'a>(name: &'a str, env: &'a Env) -> Cow<'a, str> { + env.vars + .get(name) + .map(Cow::from) + .or_else(|| std::env::var(name).map_err(log_err).ok().map(Cow::from)) + .unwrap_or_else(|| name.into()) + } + while re.is_match(replaced.as_str()) && anti_recursion_counter > 0 { anti_recursion_counter -= 1; @@ -133,13 +159,20 @@ impl EnvResolver { let full = &captures[0]; let name = &captures[2]; - let var = env.vars - .get(name) - .map(Cow::from) - .or_else(|| std::env::var(name).map_err(log_err).ok().map(Cow::from)) - .unwrap_or_else(|| name.into()); - - replaced = replaced.replace(full, &var); + // use cache if it is: + if let Some(cache) = cache { + replaced = replaced.replace( + full, + // get from cache or resolve+insert and then use it: + cache.borrow_mut() + .entry(name.to_owned()) + .or_insert_with(|| resolve(name, env).into_owned()) + .as_str(), + ); + } else { + let s = resolve(name, env); + replaced = replaced.replace(full, &s); + } } else { break; } @@ -147,6 +180,7 @@ impl EnvResolver { replaced } + /// Do not uses cache. pub fn str_only<'c, S: AsRef>(&self, s: S) -> Cow<'c, str> { let re = &self.0; @@ -165,6 +199,7 @@ impl EnvResolver { replaced.into() } + /// Uses cache if it has been set. pub fn expr<'e, Ex: AsMut>>(&self, mut expr: Ex, env: &Env) -> Ex { let editable = expr.as_mut(); let replaced = self.str(editable.actual(), env); @@ -237,15 +272,16 @@ impl Match { // TODO: tests for `Match::set_target` fn set_target>(&mut self, path: P) { - trace!("old target: {}", self.target().display()); + let path = path.into(); + debug!("match: update target: {:?} <- {:?}", self.target(), path); match self { Match::Match(entry) => { let mut new = Self::Pair { source: entry.path().into(), - target: path.into() }; + target: path }; std::mem::swap(self, &mut new); }, Match::Pair { target, .. } => { - let _ = std::mem::replace(target, path.into()); + let _ = std::mem::replace(target, path); }, } }