diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0ecbebd3..a094da4d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -193,6 +193,7 @@ jobs: - name: Test run: | cargo test -p=playdate-build-utils --all-features + cargo test -p=playdate-build --no-default-features -- --nocapture cargo test -p=playdate-build --all-features -- --nocapture cargo test -p=playdate-device cargo test -p=playdate-tool --all-features diff --git a/Cargo.lock b/Cargo.lock index 5b393b22..12bb9dda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -742,7 +742,7 @@ dependencies = [ [[package]] name = "cargo-playdate" -version = "0.4.13" +version = "0.4.14" dependencies = [ "anstyle", "anyhow", @@ -4112,7 +4112,7 @@ dependencies = [ [[package]] name = "playdate-build" -version = "0.2.9" +version = "0.3.0" dependencies = [ "crate-metadata", "dirs", diff --git a/Cargo.toml b/Cargo.toml index a8a6747e..a7070298 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.3", path = "api/sys", package = "playdate-sys", default-features = false } tool = { version = "0.1", path = "support/tool", package = "playdate-tool" } -build = { version = "0.2", path = "support/build", package = "playdate-build", default-features = false } +build = { version = "0.3", 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 94ff1d73..04ac5cb7 100644 --- a/cargo/Cargo.toml +++ b/cargo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-playdate" -version = "0.4.13" +version = "0.4.14" readme = "README.md" description = "Build tool for neat yellow console." keywords = ["playdate", "build", "cargo", "plugin", "cargo-subcommand"] diff --git a/cargo/src/assets/pdc.rs b/cargo/src/assets/pdc.rs index adf13edd..54d50c1b 100644 --- a/cargo/src/assets/pdc.rs +++ b/cargo/src/assets/pdc.rs @@ -5,7 +5,7 @@ use std::process::Command; use anyhow::bail; use cargo::CargoResult; use cargo::core::Package; -use playdate::io::soft_link_checked; +use playdate::fs::soft_link_checked; use playdate::layout::Layout; use crate::config::Config; diff --git a/cargo/src/build/mod.rs b/cargo/src/build/mod.rs index 9d0ac42b..2d084e22 100644 --- a/cargo/src/build/mod.rs +++ b/cargo/src/build/mod.rs @@ -18,7 +18,7 @@ use cargo::core::compiler::CrateType; use cargo::util::command_prelude::CompileMode; use playdate::compile::dylib_suffix_for_host_opt; use playdate::compile::static_lib_suffix; -use playdate::io::soft_link_checked; +use playdate::fs::soft_link_checked; use playdate::toolchain::gcc::ArmToolchain; use playdate::toolchain::sdk::Sdk; use anstyle::AnsiColor as Color; diff --git a/cargo/src/package/mod.rs b/cargo/src/package/mod.rs index 0d1ad432..b409d3ea 100644 --- a/cargo/src/package/mod.rs +++ b/cargo/src/package/mod.rs @@ -14,7 +14,7 @@ use cargo::core::profiles::DebugInfo; use cargo::core::profiles::Profiles; use cargo_util_schemas::manifest::TomlDebugInfo; use clap_lex::OsStrExt; -use playdate::io::soft_link_checked; +use playdate::fs::soft_link_checked; use playdate::layout::Layout; use playdate::layout::Name; use playdate::manifest::ManifestDataSource; @@ -112,7 +112,9 @@ fn package_single_target<'p>(config: &Config, } // manifest: - build_manifest(config, &product.layout, product.package, assets)?; + let ext_id = product.example.then(|| format!("dev.{}", product.name).into()); + let ext_name = product.example.then_some(product.name.as_str().into()); + build_manifest(config, &product.layout, product.package, assets, ext_id, ext_name)?; // finally call pdc and pack: let mut artifact = execute_pdc(config, &product.layout)?; @@ -217,7 +219,7 @@ fn package_multi_target<'p>(config: &Config, } crate::layout::Layout::prepare(&mut layout.as_mut())?; - let mut has_dev = Default::default(); + let mut dev = Default::default(); for product in &products { log::debug!("Preparing binaries for packaging {}", product.presentable_name()); assert_eq!(package, product.package, "package must be same"); @@ -225,7 +227,7 @@ fn package_multi_target<'p>(config: &Config, soft_link_checked(&product.path, &dst, true, layout.as_inner().target())?; if product.example { - has_dev = true; + dev = Some(product); } } @@ -237,7 +239,7 @@ fn package_multi_target<'p>(config: &Config, prepare_assets( config, assets, - has_dev, + dev.is_some(), layout.build(), true, layout.as_inner().target(), @@ -245,7 +247,9 @@ fn package_multi_target<'p>(config: &Config, } // manifest: - build_manifest(config, &layout, package, assets)?; + let ext_id = dev.and_then(|p| p.example.then(|| format!("dev.{}", p.name).into())); + let ext_name = dev.and_then(|p| p.example.then_some(p.name.as_str().into())); + build_manifest(config, &layout, package, assets, ext_id, ext_name)?; // finally call pdc and pack: let mut artifact = execute_pdc(config, &layout)?; @@ -273,21 +277,41 @@ fn package_multi_target<'p>(config: &Config, fn build_manifest(config: &Config, layout: &Layout, package: &Package, - assets: Option<&AssetsArtifact<'_>>) + assets: Option<&AssetsArtifact<'_>>, + id_suffix: Option>, + name_override: Option>) -> CargoResult<()> { config.log().verbose(|mut log| { let msg = format!("building package manifest for {}", package.package_id()); log.status("Manifest", msg); }); - let manifest = if let Some(metadata) = assets.and_then(|a| a.metadata.as_ref()) { - Manifest::try_from_source(ManifestSource { package, - metadata: metadata.into() }) - } else { - let metadata = playdate_metadata(package); - Manifest::try_from_source(ManifestSource { package, - metadata: metadata.as_ref() }) - }.map_err(|err| anyhow!(err))?; + let mut manifest = if let Some(metadata) = assets.and_then(|a| a.metadata.as_ref()) { + let source = ManifestSource { package, + metadata: metadata.into() }; + Manifest::try_from_source(source) + } else { + let metadata = playdate_metadata(package); + let source = ManifestSource { package, + metadata: metadata.as_ref() }; + Manifest::try_from_source(source) + }.map_err(|err| anyhow!(err))?; + + // Override fields. This is a hacky not-so-braking hot-fix for issue #354. + // This is a temporary solution only until full metadata inheritance is implemented. + if id_suffix.is_some() || name_override.is_some() { + if let Some(id) = id_suffix { + log::trace!("Overriding bundle_id from {}", manifest.bundle_id); + manifest.bundle_id.push_str(".example."); + manifest.bundle_id.push_str(&id); + log::trace!(" to {}", manifest.bundle_id); + } + if let Some(name) = name_override { + log::trace!("Overriding program name {} -> {name}", manifest.name); + manifest.name = name.into_owned(); + } + } + std::fs::write(layout.manifest(), manifest.to_manifest_string())?; Ok(()) } diff --git a/support/build/Cargo.toml b/support/build/Cargo.toml index 4547c717..e729dd39 100644 --- a/support/build/Cargo.toml +++ b/support/build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "playdate-build" -version = "0.2.9" +version = "0.3.0" readme = "README.md" description = "Utils that help to build package for Playdate" keywords = ["playdate", "package", "encoding", "manifest", "assets"] @@ -14,8 +14,7 @@ repository.workspace = true [dependencies] log.workspace = true -# TODO: make serde optional! -serde = { workspace = true, features = ["derive"] } +serde = { workspace = true, features = ["derive"], optional = true } serde_json = { workspace = true, optional = true } toml = { workspace = true, optional = true } dirs.workspace = true @@ -35,8 +34,10 @@ features = ["log"] [features] -default = ["toml", "serde_json"] -cargo = ["utils/cargo-message", "serde_json"] +default = [] +toml = ["serde", "dep:toml"] +serde_json = ["serde", "dep:serde_json"] +crate-metadata = ["serde_json", "dep:crate-metadata"] assets-report = [] diff --git a/support/build/src/assets/mod.rs b/support/build/src/assets/mod.rs index c5a17a3a..37ce2287 100644 --- a/support/build/src/assets/mod.rs +++ b/support/build/src/assets/mod.rs @@ -5,15 +5,13 @@ use std::io::{Error as IoError, ErrorKind as IoErrorKind}; use wax::{LinkBehavior, WalkError}; use fs_extra::error::Error as FsExtraError; -use crate::io::soft_link_checked; +use crate::fs::soft_link_checked; use crate::metadata::format::AssetsBuildMethod; use crate::metadata::format::AssetsOptions; pub mod plan; pub mod resolver; -mod tests; - use self::plan::*; @@ -21,8 +19,8 @@ pub fn apply_build_plan<'l, 'r, P: AsRef>(plan: BuildPlan<'l, 'r>, target_root: P, assets_options: &AssetsOptions) -> Result, FsExtraError> { - use crate::io::parent_of; - use crate::io::ensure_dir_exists; + use crate::fs::parent_of; + use crate::fs::ensure_dir_exists; let target_root = target_root.as_ref(); let build_method = assets_options.method; diff --git a/support/build/src/assets/plan.rs b/support/build/src/assets/plan.rs index 424835e2..d7aafd57 100644 --- a/support/build/src/assets/plan.rs +++ b/support/build/src/assets/plan.rs @@ -266,7 +266,8 @@ fn possibly_matching>(path: &Path, expr: P) -> bool { /// Assets Build Plan for a crate. -#[derive(Debug, PartialEq, Eq, Hash, serde::Serialize)] +#[derive(Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct BuildPlan<'left, 'right> { /// Instructions - what file where to put plan: Vec>, @@ -291,39 +292,37 @@ impl<'left, 'right> AsRef<[Mapping<'left, 'right>]> for BuildPlan<'left, 'right> fn as_ref(&self) -> &[Mapping<'left, 'right>] { &self.plan[..] } } -impl BuildPlan<'_, '_> { - pub fn print(&self) { - info!("assets build plan:"); - - let print = |inc: &Match, (left, right): &(Expr, Expr)| { - info!( - " {} <- {} ({left} = {right})", - inc.target().display(), - inc.source().display(), - left = left.original(), - right = right.original() - ) + +impl std::fmt::Display for BuildPlan<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut print = |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(); + write!(f, "{target:#?} <- {source:#?} ({left} = {right})") }; - self.as_inner().iter().for_each(|mapping| { - match mapping { - Mapping::AsIs(inc, exprs) => print(inc, exprs), - Mapping::Into(inc, exprs) => print(inc, exprs), - Mapping::ManyInto { sources, - target, - exprs, - .. } => { - sources.iter().for_each(|inc| { - print( - &Match::new(inc.source(), target.join(inc.target())), - exprs, - ); - }) - }, - }; - }); + for item in self.as_inner() { + match item { + Mapping::AsIs(inc, exprs) => print(inc, exprs)?, + Mapping::Into(inc, exprs) => print(inc, exprs)?, + Mapping::ManyInto { sources, + target, + exprs, + .. } => { + for inc in sources { + print(&Match::new(inc.source(), target.join(inc.target())), exprs)? + } + }, + } + } + + Ok(()) } +} +impl BuildPlan<'_, '_> { pub fn targets(&self) -> impl Iterator> { self.as_inner().iter().flat_map(|mapping| { match mapping { @@ -368,7 +367,8 @@ impl BuildPlan<'_, '_> { } -#[derive(Debug, PartialEq, Eq, Hash, serde::Serialize)] +#[derive(Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] pub enum Mapping<'left, 'right> where Self: 'left + 'right { // if right part exact path to ONE existing fs item @@ -438,7 +438,8 @@ impl Mapping<'_, '_> { } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] pub enum MappingKind { /// Copy source __to__ target. AsIs, @@ -457,3 +458,531 @@ impl std::fmt::Display for MappingKind { } } } + + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::collections::HashSet; + use std::path::{PathBuf, Path}; + + use crate::config::Env; + use crate::value::default::Value; + use crate::assets::resolver::Expr; + use crate::metadata::format::PlayDateMetadataAssets; + use super::*; + + + fn crate_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } + + + mod abs_if_existing { + use std::borrow::Cow; + + use super::*; + use super::abs_if_existing; + + + #[test] + fn local() { + let roots = [ + Cow::from(Path::new(env!("CARGO_MANIFEST_DIR"))), + crate_root().into(), + ]; + let paths = ["Cargo.toml", "src/lib.rs"]; + + for root in roots { + for test in paths { + let path = Path::new(test); + + let crated = abs_if_existing(path, &root).unwrap(); + assert!(crated.is_some(), "{crated:?} should be exist (src: {path:?})"); + + let crated = crated.unwrap(); + let expected = root.join(path); + assert_eq!(expected, crated); + } + } + } + + #[test] + fn external_rel() { + let roots = [ + Cow::from(Path::new(env!("CARGO_MANIFEST_DIR"))), + crate_root().into(), + ]; + let paths = ["../utils/Cargo.toml", "./../utils/src/lib.rs"]; + + for root in roots { + for test in paths { + let path = Path::new(test); + + let crated = abs_if_existing(path, &root).unwrap(); + assert!(crated.is_some(), "{crated:?} should be exist (src: {path:?})"); + + let crated = crated.unwrap(); + let expected = root.join(path); + assert_eq!(expected, crated); + } + } + } + + #[test] + fn external_abs() { + let roots = [ + Cow::from(Path::new(env!("CARGO_MANIFEST_DIR"))), + crate_root().into(), + ]; + let paths = ["utils", "utils/Cargo.toml"]; + + for root in roots { + for test in paths { + let path = root.parent().unwrap().join(test); + + let crated = abs_if_existing(&path, &root).unwrap(); + assert!(crated.is_some(), "{crated:?} should be exist (src: {path:?})"); + + let crated = crated.unwrap(); + let expected = path.as_path(); + assert_eq!(expected, crated); + } + } + } + } + + + mod plan { + use super::*; + use std::env::temp_dir; + + + fn prepared_tmp(test_name: &str) -> (PathBuf, PathBuf, [&'static str; 4], Env) { + let temp = temp_dir().join(env!("CARGO_PKG_NAME")) + .join(env!("CARGO_PKG_VERSION")) + .join(test_name); + + let sub = temp.join("dir"); + + if !temp.exists() { + println!("creating temp dir: {temp:?}") + } else { + println!("temp dir: {temp:?}") + } + std::fs::create_dir_all(&temp).unwrap(); + std::fs::create_dir_all(&sub).unwrap(); + + // add temp files + let files = ["foo.txt", "bar.txt", "dir/baz.txt", "dir/boo.txt"]; + for name in files { + std::fs::write(temp.join(name), []).unwrap(); + } + + let env = { + let mut env = Env::try_default().unwrap(); + env.vars.insert("TMP".into(), temp.to_string_lossy().into_owned()); + env.vars.insert("SUB".into(), sub.to_string_lossy().into_owned()); + env + }; + + (temp, sub, files, env) + } + + + mod list { + use super::*; + + + mod as_is { + use super::*; + + + #[test] + fn local_exact() { + let env = Env::try_default().unwrap(); + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect(); + + let exprs = tests.iter().map(|s| s.to_string()).collect(); + let assets = PlayDateMetadataAssets::List::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + for pair in plan.as_inner() { + assert!(matches!( + pair, + Mapping::AsIs(_, (Expr::Original(left), Expr::Original(right))) + if right == "true" && tests.contains(left.as_str()) + )); + } + } + + + #[test] + fn resolve_local_abs() { + let env = { + let mut env = Env::try_default().unwrap(); + env.vars.insert( + "SRC_ABS".into(), + concat!(env!("CARGO_MANIFEST_DIR"), "/src").into(), + ); + env + }; + + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + let tests: HashMap<_, _> = { + let man_abs = PathBuf::from("Cargo.toml").canonicalize() + .unwrap() + .to_string_lossy() + .to_string(); + let lib_abs = PathBuf::from("src/lib.rs").canonicalize() + .unwrap() + .to_string_lossy() + .to_string(); + vec![ + ("${CARGO_MANIFEST_DIR}/Cargo.toml", man_abs), + ("${SRC_ABS}/lib.rs", lib_abs), + ].into_iter() + .collect() + }; + + let exprs = tests.keys().map(|s| s.to_string()).collect(); + let assets = PlayDateMetadataAssets::List::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + for pair in plan.as_inner() { + assert!(matches!( + pair, + Mapping::AsIs(matched, (Expr::Modified{original, actual}, Expr::Original(right))) + if right == "true" + && tests[original.as_str()] == actual.as_ref() + && matched.source() == Path::new(&tests[original.as_str()]).canonicalize().unwrap() + )); + } + } + + + #[test] + fn resolve_local() { + let env = { + let mut env = Env::try_default().unwrap(); + env.vars.insert("SRC".into(), "src".into()); + env + }; + + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + let tests: HashMap<_, _> = { vec![("${SRC}/lib.rs", "src/lib.rs"),].into_iter().collect() }; + + let exprs = tests.keys().map(|s| s.to_string()).collect(); + let assets = PlayDateMetadataAssets::List::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + for pair in plan.as_inner() { + if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) = + pair + { + assert_eq!("true", right); + assert_eq!(tests[original.as_str()], actual.as_ref()); + assert_eq!( + matched.source().canonicalize().unwrap(), + Path::new(&tests[original.as_str()]).canonicalize().unwrap() + ); + assert_eq!(matched.target(), Path::new(&tests[original.as_str()])); + } else { + panic!("pair is not matching: {pair:#?}"); + } + } + } + + + #[test] + #[cfg_attr(windows, should_panic)] + fn resolve_exact_external_abs() { + let (temp, sub, _files, env) = prepared_tmp("as_is-resolve_external"); + + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + + // tests: + + let tests: HashMap<_, _> = { + vec![ + ("${TMP}/foo.txt", (temp.join("foo.txt"), "foo.txt")), + ("${TMP}/bar.txt", (temp.join("bar.txt"), "bar.txt")), + ("${SUB}/baz.txt", (sub.join("baz.txt"), "baz.txt")), + ("${TMP}/dir/boo.txt", (sub.join("boo.txt"), "boo.txt")), + ].into_iter() + .collect() + }; + + let exprs = tests.keys().map(|s| s.to_string()).collect(); + let assets = PlayDateMetadataAssets::List::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + // check targets len + { + let targets = plan.targets().collect::>(); + let expected = tests.values().map(|(_, name)| name).collect::>(); + assert_eq!(expected.len(), targets.len()); + } + + // full check + for pair in plan.as_inner() { + if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) = + pair + { + assert_eq!("true", right); + assert_eq!(tests[original.as_str()].0.to_string_lossy(), actual.as_ref()); + assert_eq!(matched.source(), tests[original.as_str()].0); + assert_eq!(matched.target().to_string_lossy(), tests[original.as_str()].1); + } else { + panic!("pair is not matching: {pair:#?}"); + } + } + } + + + #[test] + #[cfg_attr(windows, should_panic)] + fn resolve_glob_external_many() { + let (_, _, files, env) = prepared_tmp("as_is-resolve_external_many"); + + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + let exprs = ["${TMP}/*.txt", "${SUB}/*.txt"]; + + let assets = PlayDateMetadataAssets::List::(exprs.iter().map(|s| s.to_string()).collect()); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + // check targets len + { + let targets = plan.targets().collect::>(); + assert_eq!(files.len(), targets.len()); + } + + // full check + for pair in plan.as_inner() { + if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) = + pair + { + assert!(exprs.contains(&original.as_str())); + assert!(Path::new(actual.as_ref()).is_absolute()); + assert_eq!("true", right); + + if let Match::Pair { source, target } = matched { + // target is just filename: + assert_eq!(1, target.components().count()); + assert_eq!(target.file_name(), source.file_name()); + } else { + panic!("pair.matched is not matching: {matched:#?}"); + } + } else { + panic!("pair is not matching: {pair:#?}"); + } + } + } + } + } + + + mod map { + use super::*; + + + mod as_is { + use super::*; + + + #[test] + fn local_exact() { + let env = Env::try_default().unwrap(); + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect(); + + let exprs = tests.iter() + .map(|s| (s.to_string(), Value::Boolean(true))) + .collect(); + let assets = PlayDateMetadataAssets::Map::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + for pair in plan.as_inner() { + if let Mapping::AsIs(matched, (Expr::Original(left), Expr::Original(right))) = pair { + assert_eq!("true", right); + assert!(tests.contains(left.as_str())); + assert_eq!( + left.as_str(), + sanitize_path_pattern(matched.target().to_string_lossy().as_ref()) + ); + } else { + panic!("pair is not matching: {pair:#?}"); + } + } + } + + + #[test] + fn local_exact_target() { + let env = Env::try_default().unwrap(); + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + // left hand of rule: + let targets = ["trg", "/trg", "//trg"]; + // right hand of rule: + let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect(); + // latest because there is no to files into one target, so "into" will be used + + for trg in targets { + let stripped_trg = &trg.replace('/', "").trim().to_owned(); + + let exprs = tests.iter() + .map(|s| (trg.to_string(), Value::String(s.to_string()))) + .collect(); + let assets = PlayDateMetadataAssets::Map::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + for pair in plan.as_inner() { + if let Mapping::AsIs( + Match::Pair { source, target }, + (Expr::Original(left), Expr::Original(right)), + ) = pair + { + assert_eq!(left, stripped_trg); + assert!(tests.contains(right.as_str())); + assert_eq!(source, Path::new(right)); + assert_eq!(target, Path::new(stripped_trg)); + } else { + panic!("pair is not matching: {pair:#?}"); + } + } + } + } + } + + + mod one_into { + use super::*; + + + #[test] + fn local_exact_target() { + let env = Env::try_default().unwrap(); + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + // left hand of rule: + let targets = ["trg/", "trg//", "/trg/", "//trg/"]; + let targets_rel = ["trg/", "trg//"]; // non-abs targets + // right hand of rule: + let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect(); + + for trg in targets { + let exprs = tests.iter() + .map(|s| (trg.to_string(), Value::String(s.to_string()))) + .collect(); + let assets = PlayDateMetadataAssets::Map::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + for pair in plan.as_inner() { + if let Mapping::Into( + Match::Pair { source, target }, + (Expr::Original(left), Expr::Original(right)), + ) = pair + { + assert_eq!(left, target.to_string_lossy().as_ref()); + assert!(targets_rel.contains(&left.as_str())); + assert!(tests.contains(right.as_str())); + assert_eq!(source, Path::new(right)); + } else { + panic!("pair is not matching: {pair:#?}"); + } + } + } + } + } + + + mod many_into { + use super::*; + + #[test] + #[cfg_attr(windows, should_panic)] + fn glob_local_target() { + let env = Env::try_default().unwrap(); + let opts = AssetsOptions::default(); + + let root = crate_root(); + let root = Some(root.as_path()); + + // left hand of rule: + let targets = ["/trg/", "//trg/", "/trg", "trg"]; + let targets_rel = ["trg/", "trg"]; // non-abs targets + // right hand of rule: + let tests: HashSet<_> = vec!["Cargo.tom*", "src/lib.*"].into_iter().collect(); + // latest because there is no to files into one target, so "into" will be used + + for trg in targets { + let exprs = tests.iter() + .map(|s| (trg.to_string(), Value::String(s.to_string()))) + .collect(); + let assets = PlayDateMetadataAssets::Map::(exprs); + + let plan = build_plan(&env, &assets, &opts, root).unwrap(); + + for pair in plan.as_inner() { + if let Mapping::ManyInto { sources, + target, + #[cfg(feature = "assets-report")] + excluded, + exprs: (Expr::Original(left), Expr::Original(right)), } = pair + { + assert!(targets_rel.contains(&target.to_string_lossy().as_ref())); + assert_eq!(&target.to_string_lossy(), left); + + assert_eq!(1, sources.len()); + assert!(tests.contains(right.as_str())); + + #[cfg(feature = "assets-report")] + assert_eq!(0, excluded.len()); + } else { + panic!("pair is not matching: {pair:#?}"); + } + } + } + } + } + } + } +} diff --git a/support/build/src/assets/resolver.rs b/support/build/src/assets/resolver.rs index 48174c45..c4d5a450 100644 --- a/support/build/src/assets/resolver.rs +++ b/support/build/src/assets/resolver.rs @@ -6,7 +6,6 @@ use std::path::{Path, PathBuf, MAIN_SEPARATOR}; use regex::Regex; use wax::{Glob, LinkBehavior, WalkError, WalkEntry}; -use crate::cargo; use crate::config::Env; use super::log_err; use super::Error; @@ -129,7 +128,6 @@ impl EnvResolver { if let Some(captures) = re.captures(replaced.as_str()) { let full = &captures[0]; let name = &captures[2]; - cargo!(env "{name}"); let var = env.vars .get(name) @@ -154,7 +152,6 @@ impl EnvResolver { if let Some(captures) = re.captures(replaced.as_str()) { let full = &captures[0]; let name = &captures[2]; - cargo!(env "{name}"); let var = std::env::var(name).map_err(log_err) .map(Cow::from) @@ -192,6 +189,7 @@ pub enum Match { }, } +#[cfg(feature = "serde")] impl serde::Serialize for Match { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer { @@ -359,6 +357,7 @@ impl<'e> Expr<'e> { } +#[cfg(feature = "serde")] impl<'e> serde::Serialize for Expr<'e> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer { diff --git a/support/build/src/assets/tests.rs b/support/build/src/assets/tests.rs deleted file mode 100644 index d293c6e1..00000000 --- a/support/build/src/assets/tests.rs +++ /dev/null @@ -1,525 +0,0 @@ -#![cfg(test)] -use std::collections::HashMap; -use std::collections::HashSet; -use std::path::{PathBuf, Path}; - -use crate::config::Env; -use crate::assets::resolver::Expr; -use crate::metadata::format::PlayDateMetadataAssets; -use super::*; - -use resolver::sanitize_path_pattern; -use resolver::Match; -use toml::Value; - - -fn crate_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } - - -mod abs_if_existing { - use std::borrow::Cow; - - use super::*; - use super::abs_if_existing; - - - #[test] - fn local() { - let roots = [ - Cow::from(Path::new(env!("CARGO_MANIFEST_DIR"))), - crate_root().into(), - ]; - let paths = ["Cargo.toml", "src/lib.rs"]; - - for root in roots { - for test in paths { - let path = Path::new(test); - - let crated = abs_if_existing(path, &root).unwrap(); - assert!(crated.is_some(), "{crated:?} should be exist (src: {path:?})"); - - let crated = crated.unwrap(); - let expected = root.join(path); - assert_eq!(expected, crated); - } - } - } - - #[test] - fn external_rel() { - let roots = [ - Cow::from(Path::new(env!("CARGO_MANIFEST_DIR"))), - crate_root().into(), - ]; - let paths = ["../utils/Cargo.toml", "./../utils/src/lib.rs"]; - - for root in roots { - for test in paths { - let path = Path::new(test); - - let crated = abs_if_existing(path, &root).unwrap(); - assert!(crated.is_some(), "{crated:?} should be exist (src: {path:?})"); - - let crated = crated.unwrap(); - let expected = root.join(path); - assert_eq!(expected, crated); - } - } - } - - #[test] - fn external_abs() { - let roots = [ - Cow::from(Path::new(env!("CARGO_MANIFEST_DIR"))), - crate_root().into(), - ]; - let paths = ["utils", "utils/Cargo.toml"]; - - for root in roots { - for test in paths { - let path = root.parent().unwrap().join(test); - - let crated = abs_if_existing(&path, &root).unwrap(); - assert!(crated.is_some(), "{crated:?} should be exist (src: {path:?})"); - - let crated = crated.unwrap(); - let expected = path.as_path(); - assert_eq!(expected, crated); - } - } - } -} - - -mod plan { - use super::*; - use std::env::temp_dir; - - - fn prepared_tmp(test_name: &str) -> (PathBuf, PathBuf, [&'static str; 4], Env) { - let temp = temp_dir().join(env!("CARGO_PKG_NAME")) - .join(env!("CARGO_PKG_VERSION")) - .join(test_name); - - let sub = temp.join("dir"); - - if !temp.exists() { - println!("creating temp dir: {temp:?}") - } else { - println!("temp dir: {temp:?}") - } - std::fs::create_dir_all(&temp).unwrap(); - std::fs::create_dir_all(&sub).unwrap(); - - // add temp files - let files = ["foo.txt", "bar.txt", "dir/baz.txt", "dir/boo.txt"]; - for name in files { - std::fs::write(temp.join(name), []).unwrap(); - } - - let env = { - let mut env = Env::try_default().unwrap(); - env.vars.insert("TMP".into(), temp.to_string_lossy().into_owned()); - env.vars.insert("SUB".into(), sub.to_string_lossy().into_owned()); - env - }; - - (temp, sub, files, env) - } - - - mod list { - use super::*; - - - mod as_is { - use super::*; - - - #[test] - fn local_exact() { - let env = Env::try_default().unwrap(); - let opts = AssetsOptions::default(); - - let root = crate_root(); - let root = Some(root.as_path()); - - let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect(); - - let exprs = tests.iter().map(|s| s.to_string()).collect(); - let assets = PlayDateMetadataAssets::List::(exprs); - - let plan = build_plan(&env, &assets, &opts, root).unwrap(); - - for pair in plan.as_inner() { - assert!(matches!( - pair, - Mapping::AsIs(_, (Expr::Original(left), Expr::Original(right))) - if right == "true" && tests.contains(left.as_str()) - )); - } - } - - - #[test] - fn resolve_local_abs() { - let env = { - let mut env = Env::try_default().unwrap(); - env.vars.insert( - "SRC_ABS".into(), - concat!(env!("CARGO_MANIFEST_DIR"), "/src").into(), - ); - env - }; - - let opts = AssetsOptions::default(); - - let root = crate_root(); - let root = Some(root.as_path()); - - let tests: HashMap<_, _> = { - let man_abs = PathBuf::from("Cargo.toml").canonicalize() - .unwrap() - .to_string_lossy() - .to_string(); - let lib_abs = PathBuf::from("src/lib.rs").canonicalize() - .unwrap() - .to_string_lossy() - .to_string(); - vec![ - ("${CARGO_MANIFEST_DIR}/Cargo.toml", man_abs), - ("${SRC_ABS}/lib.rs", lib_abs), - ].into_iter() - .collect() - }; - - let exprs = tests.keys().map(|s| s.to_string()).collect(); - let assets = PlayDateMetadataAssets::List::(exprs); - - let plan = build_plan(&env, &assets, &opts, root).unwrap(); - - for pair in plan.as_inner() { - assert!(matches!( - pair, - Mapping::AsIs(matched, (Expr::Modified{original, actual}, Expr::Original(right))) - if right == "true" - && tests[original.as_str()] == actual.as_ref() - && matched.source() == Path::new(&tests[original.as_str()]).canonicalize().unwrap() - )); - } - } - - - #[test] - fn resolve_local() { - let env = { - let mut env = Env::try_default().unwrap(); - env.vars.insert("SRC".into(), "src".into()); - env - }; - - let opts = AssetsOptions::default(); - - let root = crate_root(); - let root = Some(root.as_path()); - - let tests: HashMap<_, _> = { vec![("${SRC}/lib.rs", "src/lib.rs"),].into_iter().collect() }; - - let exprs = tests.keys().map(|s| s.to_string()).collect(); - let assets = PlayDateMetadataAssets::List::(exprs); - - let plan = build_plan(&env, &assets, &opts, root).unwrap(); - - for pair in plan.as_inner() { - if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) = pair - { - assert_eq!("true", right); - assert_eq!(tests[original.as_str()], actual.as_ref()); - assert_eq!( - matched.source().canonicalize().unwrap(), - Path::new(&tests[original.as_str()]).canonicalize().unwrap() - ); - assert_eq!(matched.target(), Path::new(&tests[original.as_str()])); - } else { - panic!("pair is not matching: {pair:#?}"); - } - } - } - - - #[test] - #[cfg_attr(windows, should_panic)] - fn resolve_exact_external_abs() { - let (temp, sub, _files, env) = prepared_tmp("as_is-resolve_external"); - - let opts = AssetsOptions::default(); - - let root = crate_root(); - let root = Some(root.as_path()); - - - // tests: - - let tests: HashMap<_, _> = { - vec![ - ("${TMP}/foo.txt", (temp.join("foo.txt"), "foo.txt")), - ("${TMP}/bar.txt", (temp.join("bar.txt"), "bar.txt")), - ("${SUB}/baz.txt", (sub.join("baz.txt"), "baz.txt")), - ("${TMP}/dir/boo.txt", (sub.join("boo.txt"), "boo.txt")), - ].into_iter() - .collect() - }; - - let exprs = tests.keys().map(|s| s.to_string()).collect(); - let assets = PlayDateMetadataAssets::List::(exprs); - - let plan = build_plan(&env, &assets, &opts, root).unwrap(); - - // check targets len - { - let targets = plan.targets().collect::>(); - let expected = tests.values().map(|(_, name)| name).collect::>(); - assert_eq!(expected.len(), targets.len()); - } - - // full check - for pair in plan.as_inner() { - if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) = pair - { - assert_eq!("true", right); - assert_eq!(tests[original.as_str()].0.to_string_lossy(), actual.as_ref()); - assert_eq!(matched.source(), tests[original.as_str()].0); - assert_eq!(matched.target().to_string_lossy(), tests[original.as_str()].1); - } else { - panic!("pair is not matching: {pair:#?}"); - } - } - } - - - #[test] - #[cfg_attr(windows, should_panic)] - fn resolve_glob_external_many() { - let (_, _, files, env) = prepared_tmp("as_is-resolve_external_many"); - - let opts = AssetsOptions::default(); - - let root = crate_root(); - let root = Some(root.as_path()); - - let exprs = ["${TMP}/*.txt", "${SUB}/*.txt"]; - - let assets = - PlayDateMetadataAssets::List::(exprs.iter().map(|s| s.to_string()).collect()); - - let plan = build_plan(&env, &assets, &opts, root).unwrap(); - - // check targets len - { - let targets = plan.targets().collect::>(); - assert_eq!(files.len(), targets.len()); - } - - // full check - for pair in plan.as_inner() { - if let Mapping::AsIs(matched, (Expr::Modified { original, actual }, Expr::Original(right))) = pair - { - assert!(exprs.contains(&original.as_str())); - assert!(Path::new(actual.as_ref()).is_absolute()); - assert_eq!("true", right); - - if let Match::Pair { source, target } = matched { - // target is just filename: - assert_eq!(1, target.components().count()); - assert_eq!(target.file_name(), source.file_name()); - } else { - panic!("pair.matched is not matching: {matched:#?}"); - } - } else { - panic!("pair is not matching: {pair:#?}"); - } - } - } - } - } - - - mod map { - use super::*; - - - mod as_is { - use super::*; - - - #[test] - fn local_exact() { - let env = Env::try_default().unwrap(); - let opts = AssetsOptions::default(); - - let root = crate_root(); - let root = Some(root.as_path()); - - let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect(); - - let exprs = tests.iter() - .map(|s| (s.to_string(), Value::Boolean(true))) - .collect(); - let assets = PlayDateMetadataAssets::Map::(exprs); - - let plan = build_plan(&env, &assets, &opts, root).unwrap(); - - for pair in plan.as_inner() { - if let Mapping::AsIs(matched, (Expr::Original(left), Expr::Original(right))) = pair { - assert_eq!("true", right); - assert!(tests.contains(left.as_str())); - assert_eq!( - left.as_str(), - sanitize_path_pattern(matched.target().to_string_lossy().as_ref()) - ); - } else { - panic!("pair is not matching: {pair:#?}"); - } - } - } - - - #[test] - fn local_exact_target() { - let env = Env::try_default().unwrap(); - let opts = AssetsOptions::default(); - - let root = crate_root(); - let root = Some(root.as_path()); - - // left hand of rule: - let targets = ["trg", "/trg", "//trg"]; - // right hand of rule: - let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect(); - // latest because there is no to files into one target, so "into" will be used - - for trg in targets { - let stripped_trg = &trg.replace('/', "").trim().to_owned(); - - let exprs = tests.iter() - .map(|s| (trg.to_string(), Value::String(s.to_string()))) - .collect(); - let assets = PlayDateMetadataAssets::Map::(exprs); - - let plan = build_plan(&env, &assets, &opts, root).unwrap(); - - for pair in plan.as_inner() { - if let Mapping::AsIs( - Match::Pair { source, target }, - (Expr::Original(left), Expr::Original(right)), - ) = pair - { - assert_eq!(left, stripped_trg); - assert!(tests.contains(right.as_str())); - assert_eq!(source, Path::new(right)); - assert_eq!(target, Path::new(stripped_trg)); - } else { - panic!("pair is not matching: {pair:#?}"); - } - } - } - } - } - - - mod one_into { - use super::*; - - - #[test] - fn local_exact_target() { - let env = Env::try_default().unwrap(); - let opts = AssetsOptions::default(); - - let root = crate_root(); - let root = Some(root.as_path()); - - // left hand of rule: - let targets = ["trg/", "trg//", "/trg/", "//trg/"]; - let targets_rel = ["trg/", "trg//"]; // non-abs targets - // right hand of rule: - let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect(); - - for trg in targets { - let exprs = tests.iter() - .map(|s| (trg.to_string(), toml::Value::String(s.to_string()))) - .collect(); - let assets = PlayDateMetadataAssets::Map::(exprs); - - let plan = build_plan(&env, &assets, &opts, root).unwrap(); - - for pair in plan.as_inner() { - if let Mapping::Into( - Match::Pair { source, target }, - (Expr::Original(left), Expr::Original(right)), - ) = pair - { - assert_eq!(left, target.to_string_lossy().as_ref()); - assert!(targets_rel.contains(&left.as_str())); - assert!(tests.contains(right.as_str())); - assert_eq!(source, Path::new(right)); - } else { - panic!("pair is not matching: {pair:#?}"); - } - } - } - } - } - - - mod many_into { - use super::*; - - #[test] - #[cfg_attr(windows, should_panic)] - fn glob_local_target() { - let env = Env::try_default().unwrap(); - let opts = AssetsOptions::default(); - - let root = crate_root(); - let root = Some(root.as_path()); - - // left hand of rule: - let targets = ["/trg/", "//trg/", "/trg", "trg"]; - let targets_rel = ["trg/", "trg"]; // non-abs targets - // right hand of rule: - let tests: HashSet<_> = vec!["Cargo.tom*", "src/lib.*"].into_iter().collect(); - // latest because there is no to files into one target, so "into" will be used - - for trg in targets { - let exprs = tests.iter() - .map(|s| (trg.to_string(), toml::Value::String(s.to_string()))) - .collect(); - let assets = PlayDateMetadataAssets::Map::(exprs); - - let plan = build_plan(&env, &assets, &opts, root).unwrap(); - - for pair in plan.as_inner() { - if let Mapping::ManyInto { sources, - target, - #[cfg(feature = "assets-report")] - excluded, - exprs: (Expr::Original(left), Expr::Original(right)), } = pair - { - assert!(targets_rel.contains(&target.to_string_lossy().as_ref())); - assert_eq!(&target.to_string_lossy(), left); - - assert_eq!(1, sources.len()); - assert!(tests.contains(right.as_str())); - - #[cfg(feature = "assets-report")] - assert_eq!(0, excluded.len()); - } else { - panic!("pair is not matching: {pair:#?}"); - } - } - } - } - } - } -} diff --git a/support/build/src/config.rs b/support/build/src/config.rs index 0bed6374..1aeabff2 100644 --- a/support/build/src/config.rs +++ b/support/build/src/config.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::collections::BTreeMap; use std::env; use std::path::Path; @@ -48,30 +47,3 @@ impl Env { pub fn manifest_path(&self) -> PathBuf { self.cargo_manifest_dir().join(&self.cargo_manifest_filename) } } - - -pub trait Package { - type Value: crate::value::Value; - - fn name(&self) -> &str; - fn authors(&self) -> &[String]; - fn version(&self) -> Cow; - fn description(&self) -> Option<&str>; - fn manifest_path(&self) -> &Path; // XXX: not used - fn metadata(&self) -> Option<&crate::metadata::format::Metadata>; - fn target_directory(&self) -> &Path; // XXX: not used -} - -#[cfg(feature = "crate-metadata")] -impl Package for crate::metadata::PackageInfo { - type Value = T; - fn name(&self) -> &str { &self.package.name } - fn authors(&self) -> &[String] { self.package.authors.as_slice() } - fn version(&self) -> Cow { Cow::Borrowed(self.package.version.as_str()) } - fn description(&self) -> Option<&str> { self.package.description.as_deref() } - fn manifest_path(&self) -> &Path { Path::new(&self.package.manifest_path) } - fn metadata(&self) -> Option<&crate::metadata::format::Metadata> { - self.package.metadata.as_ref() - } - fn target_directory(&self) -> &Path { &self.target_directory } -} diff --git a/support/build/src/io.rs b/support/build/src/fs.rs similarity index 100% rename from support/build/src/io.rs rename to support/build/src/fs.rs diff --git a/support/build/src/lib.rs b/support/build/src/lib.rs index 180b50de..a62a574c 100644 --- a/support/build/src/lib.rs +++ b/support/build/src/lib.rs @@ -7,27 +7,10 @@ extern crate log; pub use utils::*; -pub mod io; +pub mod fs; pub mod value; pub mod layout; pub mod config; pub mod assets; pub mod metadata; pub mod manifest; - - -#[cfg(feature = "cargo")] -#[macro_export(local_inner_macros)] -macro_rules! cargo { - (env $($arg:tt)+) => (std::println!("cargo:rerun-if-env-changed={}", std::format_args!($($arg)+))); - (path $($arg:tt)+) => (std::println!("cargo:rerun-if-changed={}", std::format_args!($($arg)+))); - ($($arg:tt)+) => (std::println!("cargo:{}", std::format_args!($($arg)+))) -} - -#[cfg(not(feature = "cargo"))] -#[macro_export(local_inner_macros)] -macro_rules! cargo { - (env $($arg:tt)+) => ((/* no-op */)); - (path $($arg:tt)+) => ((/* no-op */)); - ($($arg:tt)+) => ((/* no-op */)); -} diff --git a/support/build/src/manifest/format.rs b/support/build/src/manifest/format.rs new file mode 100644 index 00000000..13c257ce --- /dev/null +++ b/support/build/src/manifest/format.rs @@ -0,0 +1,73 @@ +use std::collections::HashMap; + +#[cfg(feature = "serde")] +use serde::{Serialize, Deserialize}; + + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "serde", serde(bound(deserialize = "Value: Deserialize<'de>")))] +pub struct Manifest { + pub name: String, + pub author: String, + pub description: String, + #[cfg_attr(feature = "serde", serde(rename = "bundleID"))] + pub bundle_id: String, + pub version: String, + pub build_number: Option, + pub image_path: Option, + pub launch_sound_path: Option, + pub content_warning: Option, + pub content_warning2: Option, + + /// Manifest extra fields, e.g: `pdxversion=20000` + #[cfg_attr(feature = "serde", serde(flatten))] + pub extra: HashMap, +} + + +impl Manifest { + pub fn to_manifest_string(&self) -> String { + let mut result = String::new(); + + fn to_row, V: AsRef>(key: K, value: V) -> String { + if !value.as_ref().trim().is_empty() { + format!("{}={}\n", key.as_ref(), value.as_ref()) + } else { + String::with_capacity(0) + } + } + + result.push_str(&to_row("name", &self.name)); + result.push_str(&to_row("author", &self.author)); + result.push_str(&to_row("description", &self.description)); + result.push_str(&to_row("bundleID", &self.bundle_id)); + result.push_str(&to_row("version", &self.version)); + if let Some(value) = self.build_number { + result.push_str(&to_row("buildNumber", value.to_string())); + } + if let Some(ref value) = self.image_path { + result.push_str(&to_row("imagePath", value)); + } + if let Some(ref value) = self.launch_sound_path { + result.push_str(&to_row("launchSoundPath", value)); + } + if let Some(ref value) = self.content_warning { + result.push_str(&to_row("contentWarning", value)); + if let Some(ref value) = self.content_warning2 { + result.push_str(&to_row("contentWarning2", value)); + } + } + for (key, value) in &self.extra { + if let Some(value) = value.as_str() { + result.push_str(&to_row(key, value)); + } else if let Some(value) = value.as_bool() { + result.push_str(&to_row(key, format!("{value}"))); + } else { + warn!("Manifest extra field `{key}={value}` has unsupported type"); + } + } + result + } +} diff --git a/support/build/src/manifest.rs b/support/build/src/manifest/mod.rs similarity index 73% rename from support/build/src/manifest.rs rename to support/build/src/manifest/mod.rs index 3728c1ff..bc833aed 100644 --- a/support/build/src/manifest.rs +++ b/support/build/src/manifest/mod.rs @@ -1,76 +1,11 @@ use std::borrow::Cow; -use std::path::PathBuf; pub use crate::compile::PDX_PKG_MANIFEST_FILENAME; use crate::metadata::format::PlayDateMetadata; use self::format::Manifest; -pub struct SavedPlaydateManifest { - pub manifest: Manifest, - pub path: PathBuf, -} - - -pub mod format { - use serde::Deserialize; - use serde::Serialize; - - - #[derive(Serialize, Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - pub struct Manifest { - // pdxversion=20000 - pub name: String, - pub author: String, - pub description: String, - #[serde(rename = "bundleID")] - pub bundle_id: String, - pub version: String, - pub build_number: Option, - pub image_path: Option, - pub launch_sound_path: Option, - pub content_warning: Option, - pub content_warning2: Option, - } - - - impl Manifest { - pub fn to_manifest_string(&self) -> String { - let mut result = String::new(); - - fn to_row, V: AsRef>(key: K, value: V) -> String { - if !value.as_ref().trim().is_empty() { - format!("{}={}\n", key.as_ref(), value.as_ref()) - } else { - String::new() - } - } - - result.push_str(&to_row("name", &self.name)); - result.push_str(&to_row("author", &self.author)); - result.push_str(&to_row("description", &self.description)); - result.push_str(&to_row("bundleID", &self.bundle_id)); - result.push_str(&to_row("version", &self.version)); - if let Some(value) = self.build_number { - result.push_str(&to_row("buildNumber", value.to_string())); - } - if let Some(ref value) = self.image_path { - result.push_str(&to_row("imagePath", value)); - } - if let Some(ref value) = self.launch_sound_path { - result.push_str(&to_row("launchSoundPath", value)); - } - if let Some(ref value) = self.content_warning { - result.push_str(&to_row("contentWarning", value)); - if let Some(ref value) = self.content_warning2 { - result.push_str(&to_row("contentWarning2", value)); - } - } - result - } - } -} +pub mod format; pub trait ManifestDataSource { @@ -84,7 +19,7 @@ pub trait ManifestDataSource { } -impl<'t, T> TryFrom> for Manifest where T: ManifestDataSource { +impl<'t, T> TryFrom> for Manifest where T: ManifestDataSource { type Error = &'static str; fn try_from(source: SourceRef<'t, T>) -> Result { @@ -106,14 +41,15 @@ impl<'t, T> TryFrom> for Manifest where T: ManifestDataSource { image_path: metadata.image_path.to_owned(), launch_sound_path: metadata.launch_sound_path.to_owned(), content_warning: metadata.content_warning.to_owned(), - content_warning2: metadata.content_warning2.to_owned() }; + content_warning2: metadata.content_warning2.to_owned(), + extra: metadata.extra.to_owned() }; Ok(manifest) } } -impl Manifest { +impl Manifest { pub fn try_from_source(source: T) -> Result - where T: ManifestDataSource { + where T: ManifestDataSource { SourceRef(&source).try_into() } } @@ -142,7 +78,7 @@ impl<'t, T> ManifestDataSource for SourceRef<'t, T> where T: ManifestDataSource #[cfg(test)] mod tests { - #[cfg(any(feature = "toml", feature = "serde_json"))] + use std::collections::HashMap; use std::ops::Deref; use crate::metadata::format::PlayDateMetadata; use super::*; @@ -151,6 +87,8 @@ mod tests { use serde_json::Value; #[cfg(all(feature = "toml", not(feature = "serde_json")))] use toml::Value; + #[cfg(all(not(feature = "toml"), not(feature = "serde_json")))] + use crate::value::default::Value; struct ManifestSource { @@ -176,7 +114,7 @@ mod tests { } - fn minimal_metadata() -> PlayDateMetadata { + fn metadata_minimal() -> PlayDateMetadata { PlayDateMetadata { bundle_id: "bundle.id".to_owned(), name: Default::default(), version: Default::default(), @@ -190,9 +128,10 @@ mod tests { assets: Default::default(), dev_assets: Default::default(), options: Default::default(), - support: Default::default() } + support: Default::default(), + extra: Default::default() } } - fn maximal_metadata() -> PlayDateMetadata { + fn metadata_maximal() -> PlayDateMetadata { PlayDateMetadata { bundle_id: "bundle.id".to_owned(), name: Some("name".to_owned()), version: Some("0.42.0".to_owned()), @@ -206,17 +145,24 @@ mod tests { assets: Default::default(), dev_assets: Default::default(), options: Default::default(), - support: Default::default() } + support: Default::default(), + extra: Default::default() } + } + fn metadata_extra() -> PlayDateMetadata { + let mut data = metadata_minimal(); + data.extra = HashMap::new(); + data.extra.insert("key".to_owned(), "value".to_string().into()); + data } #[test] - fn manifest_data_source_minimal() { + fn from_data_source_minimal() { let source = ManifestSource { name: "name", authors: vec!["author".to_owned()], version: "0.42.0", description: Some("description"), - metadata: Some(minimal_metadata()) }; + metadata: Some(metadata_minimal()) }; let manifest = Manifest::try_from_source(source).expect("manifest"); assert_eq!(&manifest.name, "name"); @@ -233,12 +179,12 @@ mod tests { } #[test] - fn manifest_data_source_maximal() { + fn from_data_source_maximal() { let source = ManifestSource { name: "crate-name", authors: vec!["crate-author".to_owned()], version: "0.0.0", description: Some("crate-description"), - metadata: Some(maximal_metadata()) }; + metadata: Some(metadata_maximal()) }; let manifest = Manifest::try_from_source(source).expect("manifest"); assert_eq!(&manifest.name, "name"); @@ -253,4 +199,17 @@ mod tests { assert_eq!(manifest.content_warning2.as_deref(), Some("content_warning2")); assert_eq!(manifest.build_number, Some(42)); } + + #[test] + fn from_data_source_extra() { + let source = ManifestSource { name: "-", + version: "0.0.0", + description: Some("-"), + authors: vec!["-".to_owned()], + metadata: Some(metadata_extra()) }; + let manifest = Manifest::try_from_source(source).expect("manifest"); + + assert_eq!(Some(&Value::from("value".to_string())), manifest.extra.get("key")); + assert_eq!(1, manifest.extra.len()); + } } diff --git a/support/build/src/metadata.rs b/support/build/src/metadata.rs deleted file mode 100644 index 23d09b94..00000000 --- a/support/build/src/metadata.rs +++ /dev/null @@ -1,305 +0,0 @@ -use crate::value::Value; - - -pub const METADATA_FIELD: &str = "playdate"; - - -#[cfg(feature = "crate-metadata")] -pub use self::cargo::*; - -mod cargo { - #![cfg(feature = "crate-metadata")] - use std::path::PathBuf; - pub use crate_metadata::Package; - use super::format::Metadata; - use crate::config::Env; - use super::Value; - use super::error::Error; - - - #[derive(Debug)] - pub struct PackageInfo { - pub package: Package>, - pub target_directory: PathBuf, - } - - pub fn crate_metadata(env: &Env) -> Result, Error> { - let name = env.cargo_pkg_name(); - let manifest = env.manifest_path(); - manifest.try_exists()? - .then(|| ()) - .ok_or("unable to find crate manifest")?; - - let metadata = crate_metadata::crate_metadata::>().unwrap(); - let result = metadata.packages - .into_iter() - .find(|p| &p.name == name) - .map(|package| { - PackageInfo { package, - target_directory: metadata.target_directory } - }); - result.ok_or("package not found".into()) - } -} - - -pub mod error { - use std::io::Error as IoError; - - #[derive(Debug)] - pub enum Error { - Io(IoError), - Err(&'static str), - #[cfg(feature = "serde_json")] - Json(serde_json::error::Error), - #[cfg(feature = "toml")] - Toml(toml::de::Error), - } - - impl From<&'static str> for Error { - fn from(value: &'static str) -> Self { Self::Err(value) } - } - - impl From for Error { - fn from(err: IoError) -> Self { Self::Io(err) } - } - - #[cfg(feature = "serde_json")] - impl From for Error { - fn from(err: serde_json::error::Error) -> Self { Self::Json(err) } - } - - #[cfg(feature = "toml")] - impl From for Error { - fn from(err: toml::de::Error) -> Self { Self::Toml(err) } - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::Io(err) => err.fmt(f), - Error::Err(err) => err.fmt(f), - #[cfg(feature = "serde_json")] - Error::Json(err) => err.fmt(f), - #[cfg(feature = "toml")] - Error::Toml(err) => err.fmt(f), - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Err(_) => None, - Error::Io(err) => Some(err), - #[cfg(feature = "serde_json")] - Error::Json(err) => Some(err), - #[cfg(feature = "toml")] - Error::Toml(err) => Some(err), - } - } - } -} - - -pub mod format { - use super::Value; - use super::error::Error; - use std::borrow::Cow; - use std::collections::HashMap; - use serde::Deserialize; - - - #[derive(Deserialize, Debug)] - #[serde(bound(deserialize = "T: Deserialize<'de>"))] - #[serde(deny_unknown_fields)] - pub struct Metadata { - // TODO: test deserialization with `crate::metadata::METADATA_FIELD` for field name. - pub playdate: Option>, - } - - - #[derive(Deserialize, Debug, Clone)] - #[serde(bound(deserialize = "T: Deserialize<'de>"))] - #[serde(deny_unknown_fields)] - pub struct PlayDateMetadata { - pub name: Option, - pub version: Option, - pub author: Option, - #[serde(alias = "bundle-id", rename = "bundle-id")] - pub bundle_id: String, - pub description: Option, - #[serde(alias = "image-path", rename = "image-path")] - pub image_path: Option, - #[serde(alias = "launch-sound-path", rename = "launch-sound-path")] - pub launch_sound_path: Option, - #[serde(alias = "content-warning", rename = "content-warning")] - pub content_warning: Option, - #[serde(alias = "content-warning2", rename = "content-warning2")] - pub content_warning2: Option, - #[serde(alias = "build-number", rename = "build-number")] - pub build_number: Option, - #[serde(default = "PlayDateMetadataAssets::::default")] - pub assets: PlayDateMetadataAssets, - #[serde(alias = "dev-assets", rename = "dev-assets")] - pub dev_assets: Option>, - #[serde(default)] - pub options: Options, - #[serde(default)] - pub support: Support, - } - - - impl PlayDateMetadata where Error: From<>::Error> { - pub fn merge_opts(&mut self) -> Result<(), Error> { - let opts = if let Some(res) = self.assets.extract_options() { - Some(res?) - } else { - Default::default() - }; - - match (self.options.assets.is_some(), opts) { - (_, None) => Ok(()), - (true, Some(_)) => { - Err(concat!( - "[package.metadata.playdate.assets.options]", - " conflicts with ", - "[package.metadata.playdate.options.assets]" - ).into()) - }, - (false, Some(opts)) => { - let _ = self.options.assets.insert(opts); - Ok(()) - }, - } - } - } - - impl PlayDateMetadata { - pub fn assets_options(&self) -> Cow<'_, crate::metadata::format::AssetsOptions> { - self.options - .assets - .as_ref() - .map_or_else(Default::default, Cow::Borrowed) - } - } - - - impl PlayDateMetadataAssets where Error: From<>::Error> { - fn extract_options(&mut self) -> Option> { - match self { - PlayDateMetadataAssets::Map(map) => { - // Remove only value that have `table/map` (not bool or str) type: - if map.get("options") - .filter(|v| v.as_str().is_none() && v.as_bool().is_none()) - .is_some() - { - map.remove("options") - .map(|v| v.try_into()) - .map(|res| res.map_err(Into::into)) - } else { - None - } - }, - _ => None, - } - } - } - - #[cfg(feature = "serde_json")] - impl TryFrom for AssetsOptions { - type Error = serde_json::error::Error; - fn try_from(value: serde_json::Value) -> std::result::Result { - serde_json::from_value(value) - } - } - - #[cfg(feature = "toml")] - impl TryFrom for AssetsOptions { - type Error = toml::de::Error; - fn try_from(value: toml::Value) -> std::result::Result { - toml::Value::try_into::(value) - } - } - - - #[derive(Deserialize, Debug, Clone, Default)] - #[serde(deny_unknown_fields)] - pub struct Options { - pub assets: Option, - // This is temporary removed: - // #[serde(alias = "dry-run", rename = "dry-run", default)] - // pub dry_run: bool, - // #[serde(alias = "cross-target", rename = "cross-target", default)] - // pub cross_target: bool, - } - - - #[derive(Deserialize, Debug, Clone, Default)] - #[serde(deny_unknown_fields)] - pub struct AssetsOptions { - #[serde(alias = "override", default = "bool::")] - pub overwrite: bool, - - #[serde( - alias = "follow-symlinks", - rename = "follow-symlinks", - default = "bool::" - )] - pub follow_symlinks: bool, - - #[serde(alias = "build-method", default)] - pub method: AssetsBuildMethod, - - /// Allow building assets for dependencies - #[serde(default = "bool::")] - pub dependencies: bool, - } - - #[derive(Deserialize, Debug, Clone, Copy)] - #[serde(rename_all = "kebab-case")] - pub enum AssetsBuildMethod { - Copy, - Link, - } - - impl Default for AssetsBuildMethod { - fn default() -> Self { Self::Link } - } - - - #[derive(Deserialize, Debug, Clone)] - #[serde(bound(deserialize = "T: Deserialize<'de>"))] - #[serde(untagged)] - #[serde(deny_unknown_fields)] - pub enum PlayDateMetadataAssets { - /// List of paths to include. - List(Vec), - /// Rules & queries used to resolve paths to include. - Map(HashMap), - } - - impl Default for PlayDateMetadataAssets { - fn default() -> Self { Self::List(Vec::with_capacity(0)) } - } - - impl PlayDateMetadataAssets { - pub fn is_empty(&self) -> bool { - match self { - PlayDateMetadataAssets::List(list) => list.is_empty(), - PlayDateMetadataAssets::Map(map) => map.is_empty(), - } - } - } - - - #[derive(Deserialize, Debug, Clone, Default)] - pub struct Support { - // #[serde(rename = "crank-manifest")] - // #[serde(alias = "crank-manifest")] - // pub crank_manifest: Option, - // pub crank_manifest_rules: Option, - } - - const fn bool() -> bool { V } -} diff --git a/support/build/src/metadata/cargo.rs b/support/build/src/metadata/cargo.rs new file mode 100644 index 00000000..d4adda94 --- /dev/null +++ b/support/build/src/metadata/cargo.rs @@ -0,0 +1,61 @@ +#![cfg(feature = "crate-metadata")] +use std::borrow::Cow; +use std::path::PathBuf; +pub use crate_metadata::Package; +use super::format::PlayDateMetadata; +use crate::config::Env; +use crate::manifest::ManifestDataSource; +use super::Value; +use super::error::Error; + + +#[derive(Debug)] +pub struct PackageInfo { + pub package: Package>, + pub target_directory: PathBuf, +} + + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound(deserialize = "T: serde::Deserialize<'de>")))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct Metadata { + pub playdate: Option>, +} + + +pub fn crate_metadata(env: &Env) -> Result, Error> { + let name = env.cargo_pkg_name(); + let manifest = env.manifest_path(); + manifest.try_exists()? + .then(|| ()) + .ok_or("unable to find crate manifest")?; + + let metadata = crate_metadata::crate_metadata::>().unwrap(); + let result = metadata.packages + .into_iter() + .find(|p| &p.name == name) + .map(|package| { + PackageInfo { package, + target_directory: metadata.target_directory } + }); + result.ok_or("package not found".into()) +} + + +impl ManifestDataSource for PackageInfo { + type Value = T; + + fn name(&self) -> &str { &self.package.name } + + fn authors(&self) -> &[String] { &self.package.authors } + + fn version(&self) -> Cow { Cow::Borrowed(&self.package.version) } + + fn description(&self) -> Option<&str> { self.package.description.as_deref() } + + fn metadata(&self) -> Option<&PlayDateMetadata> { + self.package.metadata.as_ref().and_then(|m| m.playdate.as_ref()) + } +} diff --git a/support/build/src/metadata/error.rs b/support/build/src/metadata/error.rs new file mode 100644 index 00000000..41fafed8 --- /dev/null +++ b/support/build/src/metadata/error.rs @@ -0,0 +1,55 @@ +use std::io::Error as IoError; + +#[derive(Debug)] +pub enum Error { + Io(IoError), + Err(&'static str), + #[cfg(feature = "serde_json")] + Json(serde_json::error::Error), + #[cfg(feature = "toml")] + Toml(toml::de::Error), +} + +impl From<&'static str> for Error { + fn from(value: &'static str) -> Self { Self::Err(value) } +} + +impl From for Error { + fn from(err: IoError) -> Self { Self::Io(err) } +} + +#[cfg(feature = "serde_json")] +impl From for Error { + fn from(err: serde_json::error::Error) -> Self { Self::Json(err) } +} + +#[cfg(feature = "toml")] +impl From for Error { + fn from(err: toml::de::Error) -> Self { Self::Toml(err) } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io(err) => err.fmt(f), + Error::Err(err) => err.fmt(f), + #[cfg(feature = "serde_json")] + Error::Json(err) => err.fmt(f), + #[cfg(feature = "toml")] + Error::Toml(err) => err.fmt(f), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Err(_) => None, + Error::Io(err) => Some(err), + #[cfg(feature = "serde_json")] + Error::Json(err) => Some(err), + #[cfg(feature = "toml")] + Error::Toml(err) => Some(err), + } + } +} diff --git a/support/build/src/metadata/format.rs b/support/build/src/metadata/format.rs new file mode 100644 index 00000000..21b04dc7 --- /dev/null +++ b/support/build/src/metadata/format.rs @@ -0,0 +1,203 @@ +use super::Value; +use super::error::Error; +use std::borrow::Cow; +use std::collections::HashMap; +#[cfg(feature = "serde")] +use serde::Deserialize; + + +/// Package Metadata, contains: +/// - Package Manifest fields +/// - Assets tables - `assets` & `dev-assets` +/// - Configuration table - `options` +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[cfg_attr(feature = "serde", serde(bound(deserialize = "T: Deserialize<'de>")))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct PlayDateMetadata { + pub name: Option, + pub version: Option, + pub author: Option, + #[cfg_attr(feature = "serde", serde(alias = "bundle-id"))] + pub bundle_id: String, + pub description: Option, + #[cfg_attr(feature = "serde", serde(alias = "image-path"))] + pub image_path: Option, + #[cfg_attr(feature = "serde", serde(alias = "launch-sound-path"))] + pub launch_sound_path: Option, + #[cfg_attr(feature = "serde", serde(alias = "content-warning"))] + pub content_warning: Option, + #[cfg_attr(feature = "serde", serde(alias = "content-warning2"))] + pub content_warning2: Option, + #[cfg_attr(feature = "serde", serde(alias = "build-number"))] + pub build_number: Option, + #[cfg_attr(feature = "serde", serde(default = "PlayDateMetadataAssets::::default"))] + pub assets: PlayDateMetadataAssets, + #[cfg_attr(feature = "serde", serde(alias = "dev-assets"))] + pub dev_assets: Option>, + #[cfg_attr(feature = "serde", serde(default))] + pub options: Options, + #[cfg_attr(feature = "serde", serde(default))] + pub support: Support, + + /// Package Manifest extra fields. + // It could be `serde::flatten`, but if so we should remove `deny_unknown_fields` from entire struct + // and break other fields validation, so it isn't good idea. + #[cfg_attr(feature = "serde", serde(default))] + pub extra: HashMap, +} + + +impl PlayDateMetadata where Error: From<>::Error> { + pub fn merge_opts(&mut self) -> Result<(), Error> { + let opts = if let Some(res) = self.assets.extract_options() { + Some(res?) + } else { + Default::default() + }; + + match (self.options.assets.is_some(), opts) { + (_, None) => Ok(()), + (true, Some(_)) => { + Err(concat!( + "[package.metadata.playdate.assets.options]", + " conflicts with ", + "[package.metadata.playdate.options.assets]" + ).into()) + }, + (false, Some(opts)) => { + let _ = self.options.assets.insert(opts); + Ok(()) + }, + } + } +} + +impl PlayDateMetadata { + pub fn assets_options(&self) -> Cow<'_, crate::metadata::format::AssetsOptions> { + self.options + .assets + .as_ref() + .map_or_else(Default::default, Cow::Borrowed) + } +} + + +impl PlayDateMetadataAssets where Error: From<>::Error> { + fn extract_options(&mut self) -> Option> { + match self { + PlayDateMetadataAssets::Map(map) => { + // Remove only value that have `table/map` (not bool or str) type: + if map.get("options") + .filter(|v| v.as_str().is_none() && v.as_bool().is_none()) + .is_some() + { + map.remove("options") + .map(|v| v.try_into()) + .map(|res| res.map_err(Into::into)) + } else { + None + } + }, + _ => None, + } + } +} + +#[cfg(feature = "serde_json")] +impl TryFrom for AssetsOptions { + type Error = serde_json::error::Error; + fn try_from(value: serde_json::Value) -> std::result::Result { + serde_json::from_value(value) + } +} + +#[cfg(feature = "toml")] +impl TryFrom for AssetsOptions { + type Error = toml::de::Error; + fn try_from(value: toml::Value) -> std::result::Result { + toml::Value::try_into::(value) + } +} + + +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct Options { + pub assets: Option, + // Output layout ctrl, temporary removed: + // #[serde(alias = "cross-target", default)] + // pub cross_target: bool, +} + + +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct AssetsOptions { + #[cfg_attr(feature = "serde", serde(alias = "override", default = "bool::"))] + pub overwrite: bool, + + #[cfg_attr(feature = "serde", serde(alias = "follow-symlinks", default = "bool::"))] + pub follow_symlinks: bool, + + #[cfg_attr(feature = "serde", serde(alias = "build-method", default))] + pub method: AssetsBuildMethod, + + /// Allow building assets for dependencies + #[cfg_attr(feature = "serde", serde(default = "bool::"))] + pub dependencies: bool, +} + +#[cfg(feature = "serde")] +const fn bool() -> bool { V } + + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))] +pub enum AssetsBuildMethod { + Copy, + Link, +} + +impl Default for AssetsBuildMethod { + fn default() -> Self { Self::Link } +} + + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[cfg_attr(feature = "serde", serde(bound(deserialize = "T: Deserialize<'de>")))] +#[cfg_attr(feature = "serde", serde(untagged))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub enum PlayDateMetadataAssets { + /// List of paths to include. + List(Vec), + /// Rules & queries used to resolve paths to include. + Map(HashMap), +} + +impl Default for PlayDateMetadataAssets { + fn default() -> Self { Self::List(Vec::with_capacity(0)) } +} + +impl PlayDateMetadataAssets { + pub fn is_empty(&self) -> bool { + match self { + PlayDateMetadataAssets::List(list) => list.is_empty(), + PlayDateMetadataAssets::Map(map) => map.is_empty(), + } + } +} + + +/// Compatibility options. +/// e.g. Crank manifest path. +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +pub struct Support { + // #[serde(alias = "crank-manifest")] + // pub crank_manifest: Option, // bool +} diff --git a/support/build/src/metadata/mod.rs b/support/build/src/metadata/mod.rs new file mode 100644 index 00000000..5000b379 --- /dev/null +++ b/support/build/src/metadata/mod.rs @@ -0,0 +1,8 @@ +pub mod error; +pub mod format; +pub mod cargo; + +use crate::value::Value; + + +pub const METADATA_FIELD: &str = "playdate"; diff --git a/support/build/src/value.rs b/support/build/src/value.rs index 7d3ba6de..6ac608e3 100644 --- a/support/build/src/value.rs +++ b/support/build/src/value.rs @@ -1,10 +1,20 @@ -use serde::de::Deserialize; use std::fmt::Debug; use std::fmt::Display; use crate::metadata::format::AssetsOptions; -pub trait Value: for<'de> Deserialize<'de> + Clone + Debug + Display +/// Value that can be __one of__ `bool` or `String`. +#[cfg(feature = "serde")] +pub trait Value: for<'de> serde::de::Deserialize<'de> + Clone + Debug + Display + where Self: TryInto { + fn as_bool(&self) -> Option; + fn as_str(&self) -> Option<&str>; +} + +/// Value that can be __one of__ `bool` or `String`, +/// without `serde::Deserialize` requirement. +#[cfg(not(feature = "serde"))] +pub trait Value: for<'de> Clone + Debug + Display where Self: TryInto { fn as_bool(&self) -> Option; fn as_str(&self) -> Option<&str>; @@ -22,3 +32,67 @@ impl Value for toml::Value { fn as_bool(&self) -> Option { toml::Value::as_bool(self) } fn as_str(&self) -> Option<&str> { toml::Value::as_str(self) } } + + +#[cfg(test)] +pub mod default { + use super::AssetsOptions; + + #[derive(Debug, Clone, PartialEq)] + pub enum Value { + Boolean(bool), + String(String), + } + + /// Fake `Value` for tests. + impl super::Value for Value { + fn as_bool(&self) -> Option { + match self { + Value::Boolean(v) => Some(*v), + Value::String(_) => None, + } + } + + fn as_str(&self) -> Option<&str> { + match self { + Value::Boolean(_) => None, + Value::String(s) => Some(s), + } + } + } + + impl std::fmt::Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Value::Boolean(v) => v.fmt(f), + Value::String(v) => v.fmt(f), + } + } + } + + #[cfg(feature = "serde")] + impl<'t> serde::Deserialize<'t> for Value { + fn deserialize(_: D) -> Result + where D: serde::Deserializer<'t> { + unreachable!() + } + } + + impl TryInto for Value { + type Error = &'static str; + fn try_into(self) -> Result { unreachable!() } + } + + + impl From for Value { + fn from(value: bool) -> Self { Self::Boolean(value) } + } + + impl From<&str> for Value { + fn from(value: &str) -> Self { value.to_string().into() } + } + + impl From for Value { + fn from(value: String) -> Self { Self::String(value) } + } +}