diff --git a/Cargo.lock b/Cargo.lock index bf42ecf5..0c44e843 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,11 +4,11 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ - "gimli", + "gimli 0.29.0", ] [[package]] @@ -117,9 +117,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arbitrary" @@ -155,22 +155,21 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.2.1" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", - "event-listener 5.3.0", - "event-listener-strategy 0.5.2", + "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" +checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" dependencies = [ "async-task", "concurrent-queue", @@ -185,10 +184,10 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.1", "async-executor", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.3.3", + "async-lock 3.4.0", "blocking", "futures-lite 2.3.0", "once_cell", @@ -217,17 +216,17 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" dependencies = [ - "async-lock 3.3.0", + "async-lock 3.4.0", "cfg-if", "concurrent-queue", "futures-io", "futures-lite 2.3.0", "parking", - "polling 3.7.0", + "polling 3.7.1", "rustix 0.38.34", "slab", "tracing", @@ -245,12 +244,12 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "event-listener 4.0.3", - "event-listener-strategy 0.4.0", + "event-listener 5.3.1", + "event-listener-strategy", "pin-project-lite", ] @@ -273,12 +272,12 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" +checksum = "329972aa325176e89114919f2a80fdae4f4c040f66a370b1a1159c6c0f94e7aa" dependencies = [ - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.3.3", + "async-lock 3.4.0", "atomic-waker", "cfg-if", "futures-core", @@ -335,7 +334,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -352,7 +351,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -423,9 +422,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" dependencies = [ "addr2line", "cc", @@ -488,7 +487,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.63", + "syn 2.0.66", "which 4.4.2", ] @@ -524,12 +523,11 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ - "async-channel 2.2.1", - "async-lock 3.3.0", + "async-channel 2.3.1", "async-task", "futures-io", "futures-lite 2.3.0", @@ -742,7 +740,7 @@ dependencies = [ [[package]] name = "cargo-playdate" -version = "0.4.14" +version = "0.5.0-beta.1" dependencies = [ "anstyle", "anyhow", @@ -759,7 +757,7 @@ dependencies = [ "fs_extra", "futures-lite 2.3.0", "log", - "nix 0.28.0", + "nix 0.29.0", "once_cell", "playdate-build", "playdate-device", @@ -771,10 +769,10 @@ dependencies = [ "serde_json", "target", "toml", - "toml_edit 0.22.12", + "toml_edit 0.22.13", "try-lazy-init", "walkdir", - "zip 1.2.3", + "zip 1.1.4", ] [[package]] @@ -818,9 +816,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" dependencies = [ "jobserver", "libc", @@ -850,9 +848,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "cipher" @@ -866,9 +864,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", @@ -909,7 +907,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -951,7 +949,7 @@ dependencies = [ "nom", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1018,9 +1016,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.11.3" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ba00838774b4ab0233e355d26710fbfc8327a05c017f6dc4873f876d1f79f78" +checksum = "94fb8a24a26d37e1ffd45343323dc9fe6654ceea44c12f2fcb3d7ac29e610bc6" dependencies = [ "cfg-if", "cpufeatures", @@ -1101,16 +1099,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crate-metadata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a4577b4d5dd1ba39e4c8b9ece7eb83fa4fb2e504c883504925a12f85b14126" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "crates-io" version = "0.40.1" @@ -1142,18 +1130,18 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -1188,9 +1176,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crypto-bigint" @@ -1296,7 +1284,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1340,7 +1328,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1392,9 +1380,9 @@ dependencies = [ [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" dependencies = [ "serde", ] @@ -1490,11 +1478,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b73807008a3c7f171cc40312f37d95ef0396e048b5848d775f54b1a4dd4a0d3" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" dependencies = [ "serde", + "typeid", ] [[package]] @@ -1532,43 +1521,22 @@ dependencies = [ [[package]] name = "event-listener" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "5.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] -[[package]] -name = "event-listener-strategy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" -dependencies = [ - "event-listener 4.0.3", - "pin-project-lite", -] - [[package]] name = "event-listener-strategy" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ - "event-listener 5.3.0", + "event-listener 5.3.1", "pin-project-lite", ] @@ -1623,9 +1591,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38793c55593b33412e3ae40c2c9781ffaa6f438f6f8c10f24e71846fbd7ae01e" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" @@ -1743,7 +1711,7 @@ checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", - "parking_lot 0.12.2", + "parking_lot 0.12.3", ] [[package]] @@ -1788,7 +1756,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1855,6 +1823,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + [[package]] name = "git2" version = "0.18.3" @@ -1930,7 +1904,7 @@ dependencies = [ "gix-validate", "gix-worktree", "once_cell", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "prodash", "smallvec", "thiserror", @@ -1988,9 +1962,9 @@ dependencies = [ [[package]] name = "gix-command" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90009020dc4b3de47beed28e1334706e0a330ddd17f5cfeb097df3b15a54b77" +checksum = "6c22e086314095c43ffe5cdc5c0922d5439da4fd726f3b0438c56147c34dc225" dependencies = [ "bstr", "gix-path", @@ -2065,9 +2039,9 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180b130a4a41870edfbd36ce4169c7090bca70e195da783dea088dd973daa59c" +checksum = "367ee9093b0c2b04fd04c5c7c8b6a1082713534eab537597ae343663a518fa99" dependencies = [ "bstr", "itoa", @@ -2116,7 +2090,7 @@ dependencies = [ "gix-trace", "libc", "once_cell", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "prodash", "sha1_smol", "thiserror", @@ -2183,7 +2157,7 @@ checksum = "7ddf80e16f3c19ac06ce415a38b8591993d3f73aede049cb561becb5b3a8e242" dependencies = [ "gix-hash", "hashbrown 0.14.5", - "parking_lot 0.12.2", + "parking_lot 0.12.3", ] [[package]] @@ -2236,13 +2210,13 @@ dependencies = [ [[package]] name = "gix-macros" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dff438f14e67e7713ab9332f5fd18c8f20eb7eb249494f6c2bf170522224032" +checksum = "999ce923619f88194171a67fb3e6d613653b8d4d6078b529b15a765da0edcc17" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -2294,7 +2268,7 @@ dependencies = [ "gix-pack", "gix-path", "gix-quote", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "tempfile", "thiserror", ] @@ -2314,7 +2288,7 @@ dependencies = [ "gix-path", "gix-tempfile", "memmap2", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "smallvec", "thiserror", ] @@ -2373,13 +2347,13 @@ dependencies = [ [[package]] name = "gix-prompt" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5325eb17ce7b5e5d25dec5c2315d642a09d55b9888b3bf46b7d72e1621a55d8" +checksum = "fddabbc7c51c241600ab3c4623b19fa53bde7c1a2f637f61043ed5fcadf000cc" dependencies = [ "gix-command", "gix-config-value", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "rustix 0.38.34", "thiserror", ] @@ -2515,7 +2489,7 @@ dependencies = [ "gix-fs", "libc", "once_cell", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "tempfile", ] @@ -2586,9 +2560,9 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e39fc6e06044985eac19dd34d474909e517307582e462b2eb4c8fa51b6241545" +checksum = "82c27dd34a49b1addf193c92070bcbf3beaf6e10f16a78544de6372e146a0acf" dependencies = [ "bstr", "thiserror", @@ -2975,9 +2949,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -3132,7 +3106,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -3166,9 +3140,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libgit2-sys" @@ -3277,9 +3251,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.16" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" dependencies = [ "cc", "libc", @@ -3301,9 +3275,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" @@ -3315,6 +3289,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.21" @@ -3398,7 +3378,7 @@ checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -3468,7 +3448,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -3485,9 +3465,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -3570,9 +3550,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.5.0", "cfg-if", @@ -3648,6 +3628,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -3676,9 +3677,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" dependencies = [ "memchr", ] @@ -3807,9 +3808,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core 0.9.10", @@ -4018,7 +4019,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -4035,9 +4036,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" dependencies = [ "atomic-waker", "fastrand 2.1.0", @@ -4099,7 +4100,7 @@ dependencies = [ "proc-macro2", "quote", "semver", - "syn 2.0.63", + "syn 2.0.66", "which 6.0.1", ] @@ -4112,14 +4113,14 @@ dependencies = [ [[package]] name = "playdate-build" -version = "0.3.0" +version = "0.4.0-pre1" dependencies = [ - "crate-metadata", "dirs", "fs_extra", "log", "playdate-build-utils", "regex", + "semver", "serde", "serde_json", "symlink", @@ -4368,9 +4369,9 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" +checksum = "5e6a007746f34ed64099e88783b0ae369eaa3da6392868ba262e2af9b8fbaea1" dependencies = [ "cfg-if", "concurrent-queue", @@ -4415,7 +4416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -4427,11 +4428,20 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + [[package]] name = "proc-macro2" -version = "1.0.82" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -4442,7 +4452,7 @@ version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" dependencies = [ - "parking_lot 0.12.2", + "parking_lot 0.12.3", ] [[package]] @@ -4463,9 +4473,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", "prost-derive", @@ -4473,22 +4483,22 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9554e3ab233f0a932403704f1a1d08c30d5ccd931adfdfa1e8b5a19b52c1d55a" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "prost-types" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ "prost", ] @@ -4723,15 +4733,15 @@ dependencies = [ "bitflags 2.5.0", "errno", "libc", - "linux-raw-sys 0.4.13", + "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] [[package]] name = "rustversion" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" @@ -4780,7 +4790,7 @@ checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -4831,21 +4841,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde-untagged" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a160535368dfc353348e7eaa299156bd508c60c45a9249725f5f6d370d82a66" +checksum = "2676ba99bd82f75cae5cbd2c8eda6fa0b8760f18978ea840e980dd5567b5c5b6" dependencies = [ "erased-serde", "serde", + "typeid", ] [[package]] @@ -4860,13 +4871,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -4891,9 +4902,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -5235,7 +5246,7 @@ checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" dependencies = [ "new_debug_unreachable", "once_cell", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "phf_shared 0.10.0", "precomputed-hash", "serde", @@ -5330,13 +5341,13 @@ dependencies = [ "elsa", "fallible-iterator 0.3.0", "flate2", - "gimli", + "gimli 0.28.1", "goblin", "lazy_static", "nom", "nom-supreme", "once_cell", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "pdb-addr2line", "regex", "scroll", @@ -5398,9 +5409,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.63" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -5425,9 +5436,9 @@ dependencies = [ [[package]] name = "target" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba852e71502340e2eaf2fa51f9b3ec6aa25750da1aa65771491c69d67789b05c" +checksum = "1e8f05f774b2db35bdad5a8237a90be1102669f8ea013fea9777b366d34ab145" [[package]] name = "tempfile" @@ -5475,22 +5486,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -5553,16 +5564,16 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2 0.5.7", @@ -5583,13 +5594,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -5631,21 +5642,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.12", + "toml_edit 0.22.13", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] @@ -5665,15 +5676,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.12" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.8", + "winnow 0.6.9", ] [[package]] @@ -5755,7 +5766,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -5810,10 +5821,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "typed-arena" -version = "2.0.2" +name = "typeid" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" +checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf" [[package]] name = "typenum" @@ -5988,9 +5999,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" [[package]] name = "walkdir" @@ -6038,7 +6049,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "wasm-bindgen-shared", ] @@ -6072,7 +6083,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6226,7 +6237,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -6237,7 +6248,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -6439,9 +6450,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6" dependencies = [ "memchr", ] @@ -6480,28 +6491,14 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "zeroize" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.63", -] +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zip" @@ -6517,9 +6514,9 @@ dependencies = [ [[package]] name = "zip" -version = "1.2.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c700ea425e148de30c29c580c1f9508b93ca57ad31c9f4e96b83c194c37a7a8f" +checksum = "9cc23c04387f4da0374be4533ad1208cbb091d5c11d070dfef13676ad6497164" dependencies = [ "aes", "arbitrary", @@ -6533,26 +6530,27 @@ dependencies = [ "hmac", "indexmap 2.2.6", "lzma-rs", + "num_enum", "pbkdf2", - "rand", "sha1", "thiserror", "time", - "zeroize", "zopfli", "zstd", ] [[package]] name = "zopfli" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1f48f3508a3a3f2faee01629564400bc12260f6214a056d06a3aaaa6ef0736" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" dependencies = [ + "bumpalo", "crc32fast", + "lockfree-object-pool", "log", + "once_cell", "simd-adler32", - "typed-arena", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2c5da288..1fc08570 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.3", path = "support/build", package = "playdate-build", default-features = false } +build = { version = "=0.4.0-pre1", 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 04ac5cb7..b3e9d693 100644 --- a/cargo/Cargo.toml +++ b/cargo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-playdate" -version = "0.4.14" +version = "0.5.0-beta.1" readme = "README.md" description = "Build tool for neat yellow console." keywords = ["playdate", "build", "cargo", "plugin", "cargo-subcommand"] @@ -59,7 +59,7 @@ async-std = { version = "1.12", features = ["tokio1"] } [dependencies.build] workspace = true default-features = false -features = ["assets-report", "toml"] +features = ["assets-report", "toml", "json"] [dependencies.device] workspace = true @@ -89,11 +89,11 @@ features = [ [dev-dependencies] -target = "2.0.0" +target = "2.0.1" rand = "0.8" [target.'cfg(unix)'.dev-dependencies] -nix = { version = "0.28", features = ["signal"] } +nix = { version = "0.29", features = ["signal"] } [features] diff --git a/cargo/src/assets/mod.rs b/cargo/src/assets/mod.rs index 712e08ed..fc931e5f 100644 --- a/cargo/src/assets/mod.rs +++ b/cargo/src/assets/mod.rs @@ -5,7 +5,9 @@ use std::path::{PathBuf, Path}; use anstyle::AnsiColor as Color; use anyhow::bail; use cargo::CargoResult; -use cargo::core::{Package, Verbosity}; +use cargo::core::{Package, PackageId, Verbosity}; +use playdate::manifest::ManifestSourceOpt as _; +use playdate::metadata::source::MetadataSource as _; use playdate::metadata::METADATA_FIELD; use playdate::layout::Layout; @@ -15,7 +17,7 @@ use crate::layout::{PlaydateAssets, LayoutLockable, Layout as _, CrossTargetLayo use crate::logger::LogErr; use crate::utils::LazyBuildContext; use crate::utils::path::AsRelativeTo; -use self::plan::TomlMetadata; +use self::plan::Metadata; mod plan; @@ -23,15 +25,15 @@ mod pdc; #[derive(Debug)] -pub struct AssetsArtifact<'cfg> { - pub package: &'cfg Package, +pub struct AssetsArtifact { + pub package_id: PackageId, pub layout: PlaydateAssets, /// Cached metadata - pub metadata: Option, + pub metadata: Option, } /// One artifact per package. -pub type AssetsArtifacts<'cfg> = HashMap<&'cfg Package, AssetsArtifact<'cfg>>; +pub type AssetsArtifacts<'cfg> = HashMap<&'cfg Package, AssetsArtifact>; pub fn build<'cfg>(config: &'cfg Config) -> CargoResult> { @@ -41,7 +43,7 @@ pub fn build<'cfg>(config: &'cfg Config) -> CargoResult> { for (package, targets, ..) in config.possible_targets()? { let env = plan::LazyEnvBuilder::new(config, package); let mut plans: HashMap<&Package, _> = Default::default(); - let global_layout = CrossTargetLayout::new(config, package, None)?; + let global_layout = CrossTargetLayout::new(config, package.package_id(), None)?; let mut layout = global_layout.assets_layout(config); let mut options = HashMap::new(); @@ -53,6 +55,7 @@ pub fn build<'cfg>(config: &'cfg Config) -> CargoResult> { layout.clean()?; } + // primary top-level package let target_pid = package.package_id(); let has_dev = targets.iter() .any(|t| t.is_example() || t.is_test() || t.is_bench()); @@ -111,7 +114,7 @@ pub fn build<'cfg>(config: &'cfg Config) -> CargoResult> { .map(|plan| (plan, AssetKind::Package)) .chain(plan.dev.as_ref().into_iter().map(|plan| (plan, AssetKind::Dev))) { - let message = plan.printable_serializable(package, kind); + let message = plan.printable_serializable(package.package_id(), kind); config.workspace.config().shell().print_json(&message)?; } } @@ -147,7 +150,7 @@ pub fn build<'cfg>(config: &'cfg Config) -> CargoResult> { let mut has_errors = false; let mut targets = HashMap::new(); - let mut check_duplicates = |package: &Package, target_kind: AssetKind, plan| { + let mut check_duplicates = |package_id: PackageId, target_kind: AssetKind, plan| { for target in plan { if let Some((pid, kind)) = targets.get::>(&target) { has_errors = true; @@ -158,7 +161,7 @@ pub fn build<'cfg>(config: &'cfg Config) -> CargoResult> { } }; let a = err_msg(pid, *kind); - let b = err_msg(&package.package_id(), target_kind); + let b = err_msg(&package_id, target_kind); let message = format!( "Duplicate dev-asset destination: '{}':\n\t{a}\n\t{b}", target.as_relative_to_root(config).display(), @@ -166,19 +169,20 @@ pub fn build<'cfg>(config: &'cfg Config) -> CargoResult> { config.log().error(message); } else { - targets.insert(target, (package.package_id(), target_kind)); + targets.insert(target, (package_id, target_kind)); } } }; for (package, plan) in plans.iter() { + let package_id = package.package_id(); if let Some(plan) = plan.main.as_ref() { - check_duplicates(package, AssetKind::Package, plan.as_inner().targets()); + check_duplicates(package_id, AssetKind::Package, plan.as_inner().targets()); } - if package.package_id() == target_pid { + if package_id == target_pid { if let Some(plan) = plan.dev.as_ref() { - check_duplicates(package, AssetKind::Dev, plan.as_inner().targets()); + check_duplicates(package_id, AssetKind::Dev, plan.as_inner().targets()); } } } @@ -232,6 +236,7 @@ pub fn build<'cfg>(config: &'cfg Config) -> CargoResult> { }); + // FIXME: use primary (top-level) assets-options, but not options of dependency! let metadata = options.get(dependency).expect("Metadata is gone, impossible!"); let report = plan.apply(&dest, &metadata.assets_options(), config)?; @@ -348,7 +353,7 @@ pub fn build<'cfg>(config: &'cfg Config) -> CargoResult> { let metadata = options.remove(package); artifacts.insert( package, - AssetsArtifact { package, + AssetsArtifact { package_id: package.package_id(), layout, metadata, }, ); @@ -369,7 +374,7 @@ pub fn build<'cfg>(config: &'cfg Config) -> CargoResult> { fn deps_tree_metadata<'cfg: 'r, 't: 'r, 'r>(package: &'cfg Package, bcx: &'t LazyBuildContext<'t, 'cfg>, config: &Config<'_>) - -> CargoResult> { + -> CargoResult> { let mut packages = HashMap::new(); if let Some(metadata) = playdate_metadata(package) { // if explicitly allowed collect deps => scan deps-tree @@ -470,11 +475,10 @@ fn deps_tree_metadata<'cfg: 'r, 't: 'r, 'r>(package: &'cfg Package, } -pub fn playdate_metadata(package: &Package) -> Option { +pub fn playdate_metadata(package: &Package) -> Option { package.manifest() .custom_metadata() .and_then(|m| m.as_table().map(|t| t.get(METADATA_FIELD))) .flatten() - .and_then(|v| v.to_owned().try_into::().log_err().ok()) - .and_then(|mut m| m.merge_opts().map(|_| m).log_err().ok()) + .and_then(|v| v.to_owned().try_into::().log_err().ok()) } diff --git a/cargo/src/assets/plan.rs b/cargo/src/assets/plan.rs index d8be5198..9f01e849 100644 --- a/cargo/src/assets/plan.rs +++ b/cargo/src/assets/plan.rs @@ -7,11 +7,12 @@ use playdate::assets::BuildReport; use playdate::assets::apply_build_plan; use playdate::config::Env; use playdate::metadata::format::AssetsOptions; +use playdate::metadata::source::MetadataSource as _; use crate::config::Config; use crate::utils::path::AsRelativeTo; use playdate::consts::SDK_ENV_VAR; use cargo::util::CargoResult; -use playdate::metadata::format::PlayDateMetadata; +pub use playdate::metadata::format::Metadata; use playdate::assets::plan::BuildPlan as AssetsPlan; use playdate::assets::plan::build_plan as assets_build_plan; use try_lazy_init::Lazy; @@ -19,9 +20,6 @@ use try_lazy_init::Lazy; use crate::layout::{PlaydateAssets, LayoutLock}; -pub type TomlMetadata = PlayDateMetadata; - - pub struct LazyEnvBuilder<'a, 'cfg> { config: &'a Config<'cfg>, package: &'cfg Package, @@ -68,15 +66,15 @@ pub type LockedLayout<'t> = LayoutLock<&'t mut PlaydateAssets>; /// Returns `None` if there is no `assets` metadata. pub fn plan_for<'cfg, 'env, 'l>(config: &'cfg Config, package: &'cfg Package, - metadata: &TomlMetadata, + metadata: &Metadata, env: &'cfg LazyEnvBuilder<'env, 'cfg>, layout: &'l LockedLayout<'l>, with_dev: bool) -> CargoResult> { let opts = metadata.assets_options(); - let has_dev_assets = with_dev && metadata.dev_assets.iter().any(|t| !t.is_empty()); - let is_empty = metadata.assets.is_empty() && !has_dev_assets; + let has_dev_assets = with_dev && !metadata.dev_assets().is_empty(); + let is_empty = metadata.assets().is_empty() && !has_dev_assets; if is_empty { return Ok(PackageAssetsPlan { main: None, @@ -88,11 +86,11 @@ pub fn plan_for<'cfg, 'env, 'l>(config: &'cfg Config, .parent() .ok_or(anyhow!("No parent of manifest-path"))?; - let main = if !metadata.assets.is_empty() { - let plan = assets_build_plan(env, &metadata.assets, opts.as_ref(), Some(root))?; + let main = if !metadata.assets().is_empty() { + let plan = assets_build_plan(env, metadata.assets(), opts.as_ref(), Some(root))?; // main-assets plan: - let path = layout.as_inner().assets_plan_for(config, package); + let path = layout.as_inner().assets_plan_for(config, &package.package_id()); let mut cached = CachedPlan::new(path, plan)?; if config.compile_options.build_config.force_rebuild { cached.difference = Difference::Missing; @@ -105,11 +103,12 @@ pub fn plan_for<'cfg, 'env, 'l>(config: &'cfg Config, // dev-assets plan: - let dev = if has_dev_assets && metadata.dev_assets.is_some() { - let assets = metadata.dev_assets.as_ref().unwrap(); + let dev = if has_dev_assets && !metadata.dev_assets().is_empty() { + let assets = metadata.dev_assets(); let dev_plan = assets_build_plan(env, assets, opts.as_ref(), Some(root))?; - let path = layout.as_inner().assets_plan_for_dev(config, package); + let path = layout.as_inner() + .assets_plan_for_dev(config, &package.package_id()); let mut dev_cached = CachedPlan::new(path, dev_plan)?; // Inheritance, if main is stale or missing - this one is too: @@ -224,8 +223,8 @@ impl<'t, 'cfg> CachedPlan<'t, 'cfg> { } - pub fn printable_serializable(&self, source: &Package, kind: AssetKind) -> SerializablePlan<'_, 't, 'cfg> { - SerializablePlan { package: source.package_id(), + pub fn printable_serializable(&self, source: PackageId, kind: AssetKind) -> SerializablePlan<'_, 't, 'cfg> { + SerializablePlan { package: source, plan: &self.plan, difference: &self.difference, path: &self.path, diff --git a/cargo/src/build/mod.rs b/cargo/src/build/mod.rs index 2d084e22..0ff11a95 100644 --- a/cargo/src/build/mod.rs +++ b/cargo/src/build/mod.rs @@ -39,7 +39,6 @@ use crate::utils::path::AsRelativeTo; use crate::utils::workspace::PossibleTargets; -pub mod plan; pub mod rustflags; diff --git a/cargo/src/build/plan/format.rs b/cargo/src/build/plan/format.rs deleted file mode 100644 index b484b017..00000000 --- a/cargo/src/build/plan/format.rs +++ /dev/null @@ -1,145 +0,0 @@ -use std::collections::BTreeMap; -use std::path::PathBuf; -use cargo::core::compiler::CompileTarget; -use cargo::core::compiler::CrateType; -use cargo::util::command_prelude::CompileMode; -use cargo::core::compiler::CompileKind; -use serde::Deserialize; -use serde::Deserializer; -use serde::Serialize; -use serde::Serializer; - - -#[derive(Debug, Serialize, Deserialize)] -pub struct BuildPlan { - /// Program invocations needed to build the target (along with dependency information). - pub invocations: Vec, - /// List of Cargo manifests involved in the build. - pub inputs: Vec, -} - -/// A tool invocation. -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct Invocation { - /// The package this invocation is building a part of. - pub package_name: String, - /// Version of the package that is being built. - pub package_version: semver::Version, - /// The kind of artifact this invocation creates. - pub target_kind: TargetKind, - /// Whether the files created by this invocation are for the host or target system. - #[serde(serialize_with = "CompileKind::serialize")] - #[serde(deserialize_with = "deserialize_compile_kind")] - pub kind: CompileKind, - #[serde(serialize_with = "CompileMode::serialize")] - #[serde(deserialize_with = "CompileModeProxy::deserialize")] - pub compile_mode: CompileMode, - /// List of invocations this invocation depends on. - /// - /// The vector contains indices into the [`BuildPlan::invocations`] list. - /// - /// [`BuildPlan::invocations`]: struct.BuildPlan.html#structfield.invocations - pub deps: Vec, - /// List of output artifacts (binaries/libraries) created by this invocation. - pub outputs: Vec, - /// Hardlinks of output files that should be placed. - pub links: BTreeMap, - /// The program to invoke. - pub program: String, - /// Arguments to pass to the program. - pub args: Vec, - /// Map of environment variables. - pub env: BTreeMap, - /// The working directory in which to execute the program. - pub cwd: Option, -} - -#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[serde(rename_all = "kebab-case")] -#[serde(remote = "CompileMode")] -pub enum CompileModeProxy { - /// A target being built for a test. - Test, - /// Building a target with `rustc` (lib or bin). - Build, - /// Building a target with `rustc` to emit `rmeta` metadata only. If - /// `test` is true, then it is also compiled with `--test` to check it like - /// a test. - Check { test: bool }, - /// Used to indicate benchmarks should be built. This is not used in - /// `Unit`, because it is essentially the same as `Test` (indicating - /// `--test` should be passed to rustc) and by using `Test` instead it - /// allows some de-duping of Units to occur. - Bench, - /// A target that will be documented with `rustdoc`. - /// If `deps` is true, then it will also document all dependencies. - Doc { deps: bool, json: bool }, - /// A target that will be tested with `rustdoc`. - Doctest, - /// An example or library that will be scraped for function calls by `rustdoc`. - Docscrape, - /// A marker for Units that represent the execution of a `build.rs` script. - RunCustomBuild, -} - - -fn deserialize_compile_kind<'de, D>(deserializer: D) -> Result - where D: Deserializer<'de> { - let res = if let Some(s) = Option::<&str>::deserialize(deserializer)? { - let target = CompileTarget::new(s).map_err(serde::de::Error::custom)?; - CompileKind::Target(target) - } else { - CompileKind::Host - }; - Ok(res) -} - - -#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] -// #[serde(remote = "cargo::core::TargetKind")] -pub enum TargetKind { - Lib(Vec), - Bin, - Test, - Bench, - Example, - CustomBuild, -} - -impl<'de> Deserialize<'de> for TargetKind { - fn deserialize(deserializer: D) -> Result - where D: Deserializer<'de> { - use self::TargetKind::*; - - let raw = Vec::<&str>::deserialize(deserializer)?; - Ok(match *raw { - [] => return Err(serde::de::Error::invalid_length(0, &"at least one target kind")), - ["bin"] => Bin, - ["example"] => Example, - ["test"] => Test, - ["custom-build"] => CustomBuild, - ["bench"] => Bench, - ref lib_kinds => { - Lib(lib_kinds.iter() - .cloned() - .map(|s| CrateType::from(&s.to_owned())) - .collect()) - }, - }) - } -} - -impl Serialize for TargetKind { - fn serialize(&self, s: S) -> Result - where S: Serializer { - use self::TargetKind::*; - match self { - Lib(kinds) => s.collect_seq(kinds.iter().map(|t| t.to_string())), - Bin => ["bin"].serialize(s), - Example => ["example"].serialize(s), - Test => ["test"].serialize(s), - CustomBuild => ["custom-build"].serialize(s), - Bench => ["bench"].serialize(s), - } - } -} diff --git a/cargo/src/build/plan/mod.rs b/cargo/src/build/plan/mod.rs deleted file mode 100644 index 7df97738..00000000 --- a/cargo/src/build/plan/mod.rs +++ /dev/null @@ -1,83 +0,0 @@ -use cargo::CargoResult; -use cargo::core::PackageId; -use cargo::util::command_prelude::CompileMode; -use crate::cli::cmd::Cmd; -use crate::config::Config; -use crate::proc::args_line_for_proc; -use crate::proc::cargo_proxy_cmd; -use self::format::TargetKind; - -pub mod format; - - -pub fn build_plan(cfg: &Config) -> CargoResult { - let config = cfg.workspace.config(); - let mut cargo = cargo_proxy_cmd(cfg, &Cmd::Build)?; - - if !cfg.compile_options.build_config.build_plan { - cargo.args(["--build-plan", "-Zunstable-options"]); - } - - cfg.log() - .verbose(|mut log| log.status("Cargo", args_line_for_proc(&cargo))); - - let output = cargo.output()?; - if !output.status.success() { - config.shell().err().write_all(&output.stderr)?; - output.status.exit_ok()?; - } - - let stdout = std::str::from_utf8(&output.stdout)?; - - // parse only last line of output: - let line = stdout.lines() - .find(|s| { - let s = s.trim(); - !s.is_empty() && s.starts_with('{') - }) - .unwrap_or("{}"); - - let value: format::BuildPlan = serde_json::de::from_str(line)?; - Ok(value) -} - - -impl format::BuildPlan { - pub fn build_package_invocations<'plan: 'i, 'p: 'i, 'i>( - &'plan self, - package: &'p PackageId) - -> impl Iterator + 'i { - self.invocations - .iter() - .filter(move |item| { - item.package_name == package.name().as_str() && package.version() == &item.package_version - }) - .filter(|item| item.compile_mode == CompileMode::Build) - } -} - - -#[allow(dead_code)] -pub enum TargetKindWild { - Lib, - Bin, - Test, - Bench, - ExampleLib, - ExampleBin, - CustomBuild, -} - -impl PartialEq for TargetKindWild { - fn eq(&self, other: &TargetKind) -> bool { - match self { - TargetKindWild::Lib => matches!(other, TargetKind::Lib(_)), - TargetKindWild::Bin => matches!(other, TargetKind::Bin), - TargetKindWild::Test => matches!(other, TargetKind::Test), - TargetKindWild::Bench => matches!(other, TargetKind::Bench), - TargetKindWild::ExampleLib => matches!(other, TargetKind::Example), - TargetKindWild::ExampleBin => matches!(other, TargetKind::Example), - TargetKindWild::CustomBuild => matches!(other, TargetKind::CustomBuild), - } - } -} diff --git a/cargo/src/config.rs b/cargo/src/config.rs index 1e6369cf..33874c6f 100644 --- a/cargo/src/config.rs +++ b/cargo/src/config.rs @@ -16,7 +16,6 @@ use crate::cli::cmd::Cmd; use crate::cli::deps::Dependency; use crate::cli::ide::Ide; use crate::cli::opts::Mount; -use crate::utils::LazyBuildContext; pub struct Config<'cfg> { @@ -62,7 +61,9 @@ pub struct Config<'cfg> { sdk: Lazy, gcc: Lazy, rustflags: Lazy, - build_plan: Lazy, + build_plan: Lazy, + unit_graph: Lazy, + ws_metadata: Lazy, target_infos: Lazy>>, pub rustc: Rustc, @@ -130,6 +131,8 @@ impl<'cfg> Config<'cfg> { gcc: Lazy::new(), rustflags: Lazy::new(), build_plan: Lazy::new(), + unit_graph: Lazy::new(), + ws_metadata: Lazy::new(), target_infos: Lazy::new(), rustup: Default::default() } } @@ -163,9 +166,19 @@ impl<'cfg> Config<'cfg> { }) } - pub fn build_plan(&self) -> CargoResult<&crate::build::plan::format::BuildPlan> { + pub fn build_plan(&self) -> CargoResult<&crate::utils::cargo::build_plan::format::BuildPlan> { self.build_plan - .try_get_or_create(|| crate::build::plan::build_plan(self)) + .try_get_or_create(|| crate::utils::cargo::build_plan::build_plan(self)) + } + + pub fn unit_graph(&self) -> CargoResult<&crate::utils::cargo::unit_graph::format::UnitGraph> { + self.unit_graph + .try_get_or_create(|| crate::utils::cargo::unit_graph::unit_graph(self)) + } + + pub fn metadata(&self) -> CargoResult<&crate::utils::cargo::metadata::CargoMetadataPd> { + self.ws_metadata + .try_get_or_create(|| crate::utils::cargo::metadata::metadata(self)) } pub fn target_info_for(&self, kind: CompileKind) -> CargoResult<&TargetInfo> { @@ -179,11 +192,6 @@ impl<'cfg> Config<'cfg> { .map(|v| v.try_get_or_create(|| self.target_info(kind))) .ok_or_else(|| anyhow::anyhow!("Target-info for unexpected {kind:?}, not prepared."))? } - - - pub fn create_bcx<'t: 'cfg>(&'t self) -> CargoResult> { - LazyBuildContext::new(self) - } } diff --git a/cargo/src/init/mod.rs b/cargo/src/init/mod.rs index aa7e7dd1..485247ae 100644 --- a/cargo/src/init/mod.rs +++ b/cargo/src/init/mod.rs @@ -89,7 +89,7 @@ pub fn new_or_init<'cfg>(config: &'cfg Config<'cfg>) -> CargoResult<()> { for dep in deps_to_add { // TODO call cargo add WITH PWD=path - let mut cargo = proc::cargo(config.workspace.config().into())?; + let mut cargo = proc::cargo(config.into())?; cargo.current_dir(path); cargo.arg("add"); cargo.arg(dep.as_ref()); @@ -242,7 +242,7 @@ fn cargo_add<'s>(config: &Config<'_>, rename: Option<&str>, features: Option>) -> CargoResult<()> { - let mut cargo = proc::cargo(config.workspace.config().into())?; + let mut cargo = proc::cargo(config.into())?; cargo.current_dir(pwd); cargo.arg("add"); diff --git a/cargo/src/layout/playdate.rs b/cargo/src/layout/playdate.rs index 73585e40..583bc64c 100644 --- a/cargo/src/layout/playdate.rs +++ b/cargo/src/layout/playdate.rs @@ -4,10 +4,10 @@ use std::path::Path; use std::path::PathBuf; use std::hash::{Hash, Hasher}; +use cargo::core::PackageId; use cargo::util::StableHasher; use cargo_util::paths; use cargo::CargoResult; -use cargo::core::Package; use cargo::core::profiles::Profiles; pub use playdate::layout::Name as TargetName; use crate::config::Config; @@ -59,10 +59,10 @@ impl ForTargetLayout { #[derive(Debug, Clone)] -pub struct CrossTargetLayout<'cfg> { +pub struct CrossTargetLayout { /// Target name (e.g. `game-example-name`) name: TargetName, - package: &'cfg Package, + package: PackageId, /// The root directory: `/$target-dir/playdate`. root: PathBuf, @@ -76,20 +76,20 @@ pub struct CrossTargetLayout<'cfg> { assets: PathBuf, } -impl<'cfg> CrossTargetLayout<'cfg> { - pub fn new(config: &'cfg Config, package: &'cfg Package, name: Option) -> CargoResult { +impl<'cfg> CrossTargetLayout { + pub fn new(config: &'cfg Config, package_id: PackageId, name: Option) -> CargoResult { let profiles = Profiles::new( &config.workspace, config.compile_options.build_config.requested_profile, )?; let profile = profiles.get_dir_name().as_str(); - let name = name.unwrap_or_else(|| TargetName::with_package(package.name().as_str())); + let name = name.unwrap_or_else(|| TargetName::with_package(package_id.name().as_str())); let root = config.workspace.target_dir().as_path_unlocked().join("playdate"); let dest = root.join(profile); let target = dest.join(name.as_path()); let assets = root.join("assets"); Ok(Self { name, - package, + package: package_id, root, dest, target, @@ -101,11 +101,11 @@ impl<'cfg> CrossTargetLayout<'cfg> { /// Global assets layout for cross-target & cross-profile assets build. pub fn assets_layout(&self, config: &Config) -> PlaydateAssets { - PlaydateAssets::global(self, config, self.package) + PlaydateAssets::global(self, config, &self.package) } } -impl Layout for CrossTargetLayout<'_> { +impl Layout for CrossTargetLayout { fn root(&self) -> &Path { self.root.as_ref() } fn dest(&self) -> Cow { self.dest.as_path().into() } @@ -130,7 +130,7 @@ impl Layout for CrossTargetLayout<'_> { } -impl playdate::layout::Layout for CrossTargetLayout<'_> { +impl playdate::layout::Layout for CrossTargetLayout { fn name(&self) -> &TargetName { &self.name } fn root(&self) -> &Path { self.dest.as_path() } fn dest(&self) -> Cow { self.target.as_path().into() } @@ -154,7 +154,7 @@ impl playdate::layout::Layout for CrossTargetLayout<'_> { } } -impl crate::layout::LayoutLockable for CrossTargetLayout<'_> { +impl crate::layout::LayoutLockable for CrossTargetLayout { /// The lockfile filename for a build fn lockfilename(&self) -> Cow<'static, str> { ".playdate-lock".into() } } @@ -170,20 +170,20 @@ pub struct PlaydateAssets { } impl PlaydateAssets { - fn global(root: &CrossTargetLayout<'_>, config: &Config, package: &Package) -> Self { - let name = Self::name_for_package(config, package); + fn global(root: &CrossTargetLayout, config: &Config, package_id: &PackageId) -> Self { + let name = Self::name_for_package(config, package_id); Self { name, root: root.assets.to_owned() } } - pub fn assets_plan_for(&self, config: &Config, package: &Package) -> PathBuf { + pub fn assets_plan_for(&self, config: &Config, package_id: &PackageId) -> PathBuf { use playdate::layout::Layout; - let name = Self::name_for_package(config, package); + let name = Self::name_for_package(config, package_id); self.assets_plan().with_file_name(name).with_extension("json") } - pub fn assets_plan_for_dev(&self, config: &Config, package: &Package) -> PathBuf { - let mut path = self.assets_plan_for(config, package); + pub fn assets_plan_for_dev(&self, config: &Config, package_id: &PackageId) -> PathBuf { + let mut path = self.assets_plan_for(config, package_id); let mut name = path.file_stem() .map(std::ffi::OsStr::to_string_lossy) .unwrap_or_default() @@ -197,12 +197,12 @@ impl PlaydateAssets { path } - fn name_for_package(config: &Config, package: &Package) -> TargetName { + fn name_for_package(config: &Config, package_id: &PackageId) -> TargetName { let mut hasher = StableHasher::new(); - let stable = package.package_id().stable_hash(config.workspace.root()); + let stable = package_id.stable_hash(config.workspace.root()); stable.hash(&mut hasher); let hash = hasher.finish(); - TargetName::with_package(format!("{}-{hash:016x}", package.name())) + TargetName::with_package(format!("{}-{hash:016x}", package_id.name())) } } diff --git a/cargo/src/main.rs b/cargo/src/main.rs index 7ac28210..9ac39dd6 100644 --- a/cargo/src/main.rs +++ b/cargo/src/main.rs @@ -4,6 +4,7 @@ #![feature(btree_extract_if)] #![feature(byte_slice_trim_ascii)] #![feature(const_trait_impl)] +#![feature(let_chains)] extern crate build as playdate; diff --git a/cargo/src/package/mod.rs b/cargo/src/package/mod.rs index b409d3ea..858857ee 100644 --- a/cargo/src/package/mod.rs +++ b/cargo/src/package/mod.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; use std::collections::HashMap; +use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; use std::process::Command; @@ -17,8 +18,11 @@ use clap_lex::OsStrExt; use playdate::fs::soft_link_checked; use playdate::layout::Layout; use playdate::layout::Name; -use playdate::manifest::ManifestDataSource; -use playdate::manifest::format::Manifest; +use playdate::manifest::format::ManifestFmt; +use playdate::manifest::CrateInfoSource; +use playdate::metadata::format::Metadata; +use playdate::metadata::validation::Validate; +use playdate::metadata::validation::ValidateCrate; use crate::assets::AssetsArtifact; use crate::assets::AssetsArtifacts; @@ -98,7 +102,7 @@ fn package_single_target<'p>(config: &Config, ); if let Some(assets) = assets { - assert_eq!(assets.package, product.package); + assert_eq!(assets.package_id, product.package.package_id()); log::debug!("Preparing assets for packaging {}", product.presentable_name()); prepare_assets( @@ -112,9 +116,16 @@ fn package_single_target<'p>(config: &Config, } // manifest: - 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)?; + let cargo_target = (matches!(product.src_ct, CrateType::Bin) || product.example).then_some(&product.name) + .map(Cow::from); + build_manifest( + config, + &product.layout, + product.package, + assets, + cargo_target, + product.example, + )?; // finally call pdc and pack: let mut artifact = execute_pdc(config, &product.layout)?; @@ -157,6 +168,23 @@ fn package_multi_target<'p>(config: &Config, .map(|p| format!("{}", p.dst_ct)) .collect::>() .join(", "); + + let cargo_targets = products.iter().fold(HashSet::new(), |mut set, product| { + set.insert(product.name.as_str()); + set + }); + if cargo_targets.len() > 1 { + // TODO: instead of this, group them by cargo-target - one or two for single cargo-target. + let list = cargo_targets.into_iter().collect::>().join(", "); + let msg = "Multiple cargo-targets not supported:"; + if !config.compile_options.build_config.keep_going { + bail!("{msg} [{list}]"); + } else { + config.log() + .error(format!("{msg} [{list}] (sources: {src_cts}, targets: {dst_cts})",)); + } + } + config.log().status( "Packaging", format!( @@ -209,7 +237,8 @@ fn package_multi_target<'p>(config: &Config, // cross-target layout: let layout_target_name = Name::with_names(package.name().as_str(), products.first().map(|p| &p.name)); let mut layout = - CrossTargetLayout::new(config, package, Some(layout_target_name))?.lock(config.workspace.config())?; + CrossTargetLayout::new(config, package.package_id(), Some(layout_target_name))?.lock(config.workspace + .config())?; if let Some(assets) = assets { debug_assert_eq!( layout.as_ref().assets_layout(config).root(), @@ -234,8 +263,8 @@ fn package_multi_target<'p>(config: &Config, // Then the same as for single-product package: if let Some(assets) = assets { - log::debug!("Preparing assets for packaging {}", assets.package.name()); - assert_eq!(package, assets.package, "package must be same"); + log::debug!("Preparing assets for packaging {}", assets.package_id.name()); + assert_eq!(package.package_id(), assets.package_id, "package must be same"); prepare_assets( config, assets, @@ -247,9 +276,17 @@ fn package_multi_target<'p>(config: &Config, } // manifest: - 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)?; + let cargo_target = + (matches!(products[0].src_ct, CrateType::Bin) || products[0].example).then_some(products[0].name.as_str()) + .map(Cow::from); + build_manifest( + config, + &layout, + package, + assets, + cargo_target, + products[0].example, + )?; // finally call pdc and pack: let mut artifact = execute_pdc(config, &layout)?; @@ -277,42 +314,64 @@ fn package_multi_target<'p>(config: &Config, fn build_manifest(config: &Config, layout: &Layout, package: &Package, - assets: Option<&AssetsArtifact<'_>>, - id_suffix: Option>, - name_override: Option>) + assets: Option<&AssetsArtifact>, + cargo_target: Option>, + dev: bool) -> CargoResult<()> { config.log().verbose(|mut log| { let msg = format!("building package manifest for {}", package.package_id()); log.status("Manifest", msg); }); - 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); + use ::playdate::metadata::validation::Problem; + let log_problem = |pre: &str, problem: Problem| { + let msg = format!("{pre}: {problem}"); + if problem.is_err() { + config.log().error(msg); + } else { + config.log().warn(msg); } - if let Some(name) = name_override { - log::trace!("Overriding program name {} -> {name}", manifest.name); - manifest.name = name.into_owned(); + }; + let log_src_problem = |problem: Problem| { + let msg = "Manifest validation".to_string(); + log_problem(&msg, problem) + }; + let log_meta_problem = |problem: Problem| { + let msg = "Metadata validation".to_string(); + log_problem(&msg, problem) + }; + + let validate = |src: &ManifestSource| { + if let Some(target) = &cargo_target { + src.validate_for(target) + .into_iter() + // .filter(Problem::is_err) + .for_each(log_meta_problem); + } else { + src.validate() + .into_iter() + // .filter(Problem::is_err) + .for_each(log_meta_problem); } - } + }; + + let manifest = if let Some(metadata) = assets.and_then(|a| a.metadata.as_ref()) { + let source = ManifestSource::new(package, metadata.into()); + // This validation not needed at this step. May be earlier: + validate(&source); + source.manifest_for_opt(cargo_target.as_deref(), dev) + } else { + let metadata = playdate_metadata(package); + let source = ManifestSource::new(package, metadata.as_ref()); + // This validation not needed at this step. May be earlier: + validate(&source); + source.manifest_for_opt(cargo_target.as_deref(), dev) + }; - std::fs::write(layout.manifest(), manifest.to_manifest_string())?; + // validation, lints + manifest.validate().into_iter().for_each(log_src_problem); + + std::fs::write(layout.manifest(), manifest.to_manifest_string()?)?; Ok(()) } @@ -491,15 +550,65 @@ impl<'cfg> TryFrom> for SuccessfulBuildProduct<'cfg> { struct ManifestSource<'cfg, 'm> { package: &'cfg Package, - metadata: Option<&'m playdate::metadata::format::PlayDateMetadata>, + authors: Vec<&'cfg str>, + bins: Vec<&'cfg str>, + examples: Vec<&'cfg str>, + metadata: Option<&'m Metadata>, } -impl ManifestDataSource for ManifestSource<'_, '_> { - type Value = toml::Value; +impl<'cfg, 'm> ManifestSource<'cfg, 'm> { + fn new(package: &'cfg Package, metadata: Option<&'m Metadata>) -> Self { + log::debug!("new manifest-source for {}", package.package_id()); + + let mut bins = Vec::new(); + let mut examples = Vec::new(); + + package.targets() + .iter() + .inspect(|t| log::trace!("target: {} ({:?})", t.description_named(), t.crate_name())) + .filter(|t| !t.is_custom_build()) + .for_each(|t| { + if t.is_bin() { + bins.push(t.name()); + log::debug!("+bin: {}", t.description_named()); + } else if t.is_example() { + examples.push(t.name()); + log::debug!("+example: {}", t.description_named()); + } + }); + + Self { authors: package.manifest() + .metadata() + .authors + .iter() + .map(|s| s.as_str()) + .collect(), + bins, + examples, + package, + metadata } + } +} - fn name(&self) -> &str { self.package.name().as_str() } - fn authors(&self) -> &[String] { &self.package.manifest().metadata().authors } +impl<'cfg> CrateInfoSource for ManifestSource<'cfg, '_> { + type Authors = [&'cfg str]; + + fn name(&self) -> Cow { self.package.name().as_str().into() } + fn authors(&self) -> &[&'cfg str] { &self.authors } fn version(&self) -> Cow { self.package.version().to_string().into() } - fn description(&self) -> Option<&str> { self.package.manifest().metadata().description.as_deref() } - fn metadata(&self) -> Option<&playdate::metadata::format::PlayDateMetadata> { self.metadata } + fn description(&self) -> Option> { + self.package + .manifest() + .metadata() + .description + .as_deref() + .map(|s: &str| s.into()) + } + + fn bins(&self) -> &[&str] { &self.bins } + fn examples(&self) -> &[&str] { &self.examples } + + fn metadata(&self) -> Option { self.metadata } + + fn manifest_path(&self) -> Cow { Cow::Borrowed(self.package.manifest_path()) } } diff --git a/cargo/src/proc/reader.rs b/cargo/src/proc/reader.rs index a01e0f4a..c25b9be6 100644 --- a/cargo/src/proc/reader.rs +++ b/cargo/src/proc/reader.rs @@ -119,17 +119,16 @@ impl SerializedTarget { pub mod format { #![allow(dead_code)] - use std::path::Path; use std::path::PathBuf; use cargo::core::compiler::CrateType; - use cargo::core::SourceId; use cargo::util::interning::InternedString; use cargo::util::machine_message::Message; use serde::Serialize; use serde::Deserialize; - use serde::Deserializer; use cargo::core::PackageId; - pub use crate::build::plan::format::TargetKind; + use crate::utils::cargo::build_plan::format::deserialize_crate_types; + use crate::utils::cargo::format::deserialize_package_id; + pub use crate::utils::cargo::format::TargetKind; #[derive(Serialize, Deserialize)] @@ -173,48 +172,6 @@ pub mod format { pub fresh: bool, } - /// Try deserialize using actual deserializer. - /// If fails, try to deserialize as old format. - /// Fixes breaking change between old and new format in cargo ~0.78.1. - fn deserialize_package_id<'de, D>(deserializer: D) -> Result - where D: Deserializer<'de> { - use serde::de::Error; - - let mut line = String::deserialize(deserializer)?; - // wrap into quotes for deserializer: - line.insert(0, '\"'); - line.push('\"'); - // preserve original value: - let value = &line[1..(line.len() - 1)]; - - // try actual format first: - let res = serde_json::from_str::(&line).map_err(Error::custom); - - // otherwise try old formats: - res.or_else(move |err| { - if let Some((uri, name_ver)) = value.split_once('#') { - let sid = SourceId::from_url(uri).map_err(Error::custom)?; - - if let Some((name, ver)) = name_ver.split_once('@') { - let ver = ver.parse().map_err(Error::custom)?; - let id = PackageId::new(name.into(), ver, sid); - return Ok(id); - } else { - let sid_temp = SourceId::from_url(value).map_err(Error::custom)?; - let url = sid_temp.url(); - if let Some(ver) = url.fragment() { - let ver = ver.parse().map_err(Error::custom)?; - let name = Path::new(url.path()).file_name() - .ok_or_else(|| Error::custom("Package name missed"))? - .to_string_lossy(); - let id = PackageId::new(name.as_ref().into(), ver, sid); - return Ok(id); - } - } - } - Err(err) - }) - } impl Message for Artifact { fn reason(&self) -> &str { "compiler-artifact" } @@ -228,7 +185,7 @@ pub mod format { pub kind: TargetKind, /// Corresponds to `--crate-type` compiler attribute. /// See - #[serde(deserialize_with = "deserialize_crate_type_vec")] + #[serde(deserialize_with = "deserialize_crate_types")] pub crate_types: Vec, pub name: InternedString, pub src_path: Option, @@ -242,15 +199,6 @@ pub mod format { pub test: bool, } - fn deserialize_crate_type_vec<'de, D>(deserializer: D) -> Result, D::Error> - where D: Deserializer<'de> { - let strings = Vec::<&str>::deserialize(deserializer)?; - let res = strings.into_iter() - .map(|s| CrateType::from(&s.to_owned())) - .collect(); - Ok(res) - } - #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ArtifactProfile { @@ -291,40 +239,10 @@ pub mod format { use super::*; - #[derive(Debug, Serialize, Deserialize)] - pub struct PackageIdWrapped { - #[serde(deserialize_with = "super::deserialize_package_id")] - pub package_id: PackageId, - } - - - /// Before cargo 0.78 - #[test] - fn message_format_old_a() { - let msg = r#"{"package_id": "path+file:///Users/U/Developer/Projects/Playdate/playdate-rs/api/sys#playdate-sys@0.3.3"}"#; - serde_json::from_str::(msg).unwrap(); - } - - /// Before cargo 0.78 - #[test] - fn message_format_old_b() { - let msg = r#"{"package_id": "path+file:///Users/U/Developer/Projects/Playdate/playdate-rs/cargo/tests/crates/simple/with-cfg#0.1.0"}"#; - serde_json::from_str::(msg).unwrap(); - } - - - /// From cargo 0.78 - #[test] - fn message_format_new() { - let msg = r#"{"package_id": "playdate-sys 0.3.3 (path+file:///Users/U/Developer/Projects/Playdate/playdate-rs/api/sys)"}"#; - serde_json::from_str::(msg).unwrap(); - } - - /// Before cargo 0.78 #[test] fn msg_message_format_old() { - let msg = r#"{"reason":"compiler-artifact","package_id":"path+file:///Users/U/Developer/Projects/Playdate/playdate-rs/api/sys#playdate-sys@0.3.3","manifest_path":"/Users/U/Developer/Projects/Playdate/playdate-rs/api/sys/Cargo.toml","target":{"kind":["example"],"crate_types":["dylib","staticlib"],"name":"hello-world","src_path":"/Users/U/Developer/Projects/Playdate/playdate-rs/api/sys/examples/hello-world.rs","edition":"2021","required-features":["lang-items"],"doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["allocator","arrayvec","bindgen","bindgen-runtime","bindings-derive-debug","default","eh-personality","lang-items","panic-handler"],"filenames":["/Users/U/Developer/Projects/Playdate/playdate-rs/target/aarch64-apple-darwin/debug/examples/libhello_world.dylib","/Users/U/Developer/Projects/Playdate/playdate-rs/target/aarch64-apple-darwin/debug/examples/libhello_world.a"],"executable":null,"fresh":false}"#; + let msg = r#"{"reason":"compiler-artifact","package_id":"path+file:///Users/U/Dev/playdate-rs/api/sys#playdate-sys@0.3.3","manifest_path":"/Users/U/Dev/playdate-rs/api/sys/Cargo.toml","target":{"kind":["example"],"crate_types":["dylib","staticlib"],"name":"hello-world","src_path":"/Users/U/Dev/playdate-rs/api/sys/examples/hello-world.rs","edition":"2021","required-features":["lang-items"],"doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["allocator","arrayvec","bindgen","bindgen-runtime","bindings-derive-debug","default","eh-personality","lang-items","panic-handler"],"filenames":["/Users/U/Dev/playdate-rs/target/aarch64-apple-darwin/debug/examples/libhello_world.dylib","/Users/U/Dev/playdate-rs/target/aarch64-apple-darwin/debug/examples/libhello_world.a"],"executable":null,"fresh":false}"#; serde_json::from_str::(msg).unwrap(); } } diff --git a/cargo/src/proc/utils.rs b/cargo/src/proc/utils.rs index e03cb256..9ef7e76d 100644 --- a/cargo/src/proc/utils.rs +++ b/cargo/src/proc/utils.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::ffi::OsStr; use std::ffi::OsString; use std::path::*; use std::env; @@ -6,17 +7,28 @@ use std::process::Command; use build::consts::SDK_ENV_VAR; use cargo::CargoResult; -use cargo::Config as CargoConfig; +use serde::de::DeserializeOwned; use crate::cli::cmd::Cmd; use crate::config::Config; +use crate::logger::LogErr; pub fn cargo_proxy_cmd(cfg: &Config, cmd: &Cmd) -> CargoResult { + cargo_proxy_with(cfg, cmd.as_str(), true) +} + + +pub fn cargo_proxy_with>(cfg: &Config, + cmd: S, + cfg_args: bool) + -> CargoResult { let rustflags = cfg.rustflags()?.rustflags_to_args_from(cfg); - let mut proc = cargo(Some(cfg.workspace.config()))?; - proc.arg(cmd.as_ref()); - proc.args(&cfg.args); + let mut proc = cargo(Some(cfg))?; + proc.arg(cmd); + if cfg_args { + proc.args(&cfg.args); + } proc.args(&rustflags); if let Some(path) = cfg.sdk_path.as_deref() { @@ -27,16 +39,10 @@ pub fn cargo_proxy_cmd(cfg: &Config, cmd: &Cmd) -> CargoResult) -> CargoResult { - let cargo: Cow = - config.map_or_else( - || Some(PathBuf::from(env::var_os("CARGO").unwrap_or("cargo".into())).into()), - |cfg| cfg.cargo_exe().ok().map(Cow::from), - ) - .expect("Unable to get cargo bin from config"); - let mut proc = std::process::Command::new(cargo.as_ref()); +pub fn cargo(cfg: Option<&Config>) -> CargoResult { + let mut proc = cargo_cmd(cfg); - if let Some(cfg) = &config { + if let Some(cfg) = cfg.map(|cfg| cfg.workspace.config()) { // transparent env: cfg.env_config()?.iter().for_each(|(k, v)| { let value = v.resolve(cfg); @@ -55,7 +61,6 @@ pub fn cargo(config: Option<&CargoConfig>) -> CargoResult "never" }; proc.env("CARGO_TERM_COLOR", color); - proc.arg(format!("--color={color}")); } // disable progress bar: @@ -64,6 +69,37 @@ pub fn cargo(config: Option<&CargoConfig>) -> CargoResult } +pub fn cargo_cmd(cfg: Option<&Config>) -> std::process::Command { + fn cargo_path<'t>(cfg: Option<&'t Config<'t>>) -> (Cow<'t, Path>, Option<&str>) { + if let Some(cfg) = cfg { + let path = cfg.workspace.config().cargo_exe().log_err().ok().map(Cow::from); + if path.is_some() && path == std::env::current_exe().log_err().ok().map(Into::into) { + // Seems to we're in standalone mode. + cargo_path(None) + } else if let Some(path) = path { + (path, None) + } else { + cargo_path(None) + } + } else if let Some(path) = env::var_os("CARGO") { + (PathBuf::from(path).into(), None) + } else { + let arg = cfg.and_then(|cfg| cfg.rustup.as_os_str()).map(|_| "+nightly"); + (Path::new("cargo").into(), arg) + } + } + + let (bin, arg) = cargo_path(cfg); + + let mut proc = std::process::Command::new(bin.as_ref()); + if let Some(arg) = arg { + proc.arg(arg); + } + + proc +} + + pub fn args_line_for_proc(proc: &Command) -> String { proc.get_args() .collect::>() @@ -71,3 +107,27 @@ pub fn args_line_for_proc(proc: &Command) -> String { .to_string_lossy() .to_string() } + + +pub fn read_cargo_json(cfg: &Config, mut cmd: Command) -> CargoResult { + cfg.log() + .verbose(|mut log| log.status("Cargo", args_line_for_proc(&cmd))); + + let output = cmd.output()?; + if !output.status.success() { + cfg.workspace.config().shell().err().write_all(&output.stderr)?; + output.status.exit_ok()?; + } + + let stdout = std::str::from_utf8(&output.stdout)?; + + // parse only last line of output: + let line = stdout.lines() + .find(|s| { + let s = s.trim(); + !s.is_empty() && s.starts_with('{') + }) + .unwrap_or("{}"); + + Ok(serde_json::de::from_str::(line)?) +} diff --git a/cargo/src/utils/cargo/build_plan.rs b/cargo/src/utils/cargo/build_plan.rs new file mode 100644 index 00000000..9826dd2a --- /dev/null +++ b/cargo/src/utils/cargo/build_plan.rs @@ -0,0 +1,116 @@ +use cargo::CargoResult; +use cargo::core::PackageId; +use cargo::util::command_prelude::CompileMode; +use crate::cli::cmd::Cmd; +use crate::config::Config; +use crate::proc::cargo_proxy_cmd; +use crate::proc::read_cargo_json; +use self::format::TargetKind; + + +pub fn build_plan(cfg: &Config) -> CargoResult { + let mut cargo = cargo_proxy_cmd(cfg, &Cmd::Build)?; + + if !cfg.compile_options.build_config.build_plan { + cargo.args(["--build-plan", "-Zunstable-options"]); + } + + read_cargo_json(cfg, cargo) +} + + +impl format::BuildPlan { + pub fn build_package_invocations<'plan: 'i, 'p: 'i, 'i>( + &'plan self, + package: &'p PackageId) + -> impl Iterator + 'i { + self.invocations + .iter() + .filter(move |item| { + item.package_name == package.name().as_str() && package.version() == &item.package_version + }) + .filter(|item| item.compile_mode == CompileMode::Build) + } +} + + +#[allow(dead_code)] +pub enum TargetKindWild { + Lib, + Bin, + Test, + Bench, + ExampleLib, + ExampleBin, + CustomBuild, +} + +impl PartialEq for TargetKindWild { + fn eq(&self, other: &TargetKind) -> bool { + match self { + TargetKindWild::Lib => matches!(other, TargetKind::Lib(_)), + TargetKindWild::Bin => matches!(other, TargetKind::Bin), + TargetKindWild::Test => matches!(other, TargetKind::Test), + TargetKindWild::Bench => matches!(other, TargetKind::Bench), + TargetKindWild::ExampleLib => matches!(other, TargetKind::Example), + TargetKindWild::ExampleBin => matches!(other, TargetKind::Example), + TargetKindWild::CustomBuild => matches!(other, TargetKind::CustomBuild), + } + } +} + + +pub mod format { + use std::collections::BTreeMap; + use std::path::PathBuf; + use cargo::util::command_prelude::CompileMode; + use cargo::core::compiler::CompileKind; + use serde::{Serialize, Deserialize}; + + pub use super::super::format::*; + + + #[derive(Debug, Serialize, Deserialize)] + pub struct BuildPlan { + /// Program invocations needed to build the target (along with dependency information). + pub invocations: Vec, + /// List of Cargo manifests involved in the build. + pub inputs: Vec, + } + + /// A tool invocation. + #[derive(Debug, Serialize, Deserialize, PartialEq)] + pub struct Invocation { + /// The package this invocation is building a part of. + pub package_name: String, + /// Version of the package that is being built. + pub package_version: semver::Version, + /// The kind of artifact this invocation creates. + pub target_kind: TargetKind, + /// Whether the files created by this invocation are for the host or target system. + #[serde(serialize_with = "CompileKind::serialize")] + #[serde(deserialize_with = "deserialize_compile_kind")] + pub kind: CompileKind, + #[serde(serialize_with = "CompileMode::serialize")] + #[serde(deserialize_with = "CompileModeProxy::deserialize")] + pub compile_mode: CompileMode, + /// List of invocations this invocation depends on. + /// + /// The vector contains indices into the [`BuildPlan::invocations`] list. + /// + /// [`BuildPlan::invocations`]: struct.BuildPlan.html#structfield.invocations + pub deps: Vec, + /// List of output artifacts (binaries/libraries) created by this invocation. + pub outputs: Vec, + /// Hardlinks of output files that should be placed. + pub links: BTreeMap, + /// The program to invoke. + pub program: String, + /// Arguments to pass to the program. + pub args: Vec, + /// Map of environment variables. + pub env: BTreeMap, + /// The working directory in which to execute the program. + pub cwd: Option, + } +} diff --git a/cargo/src/utils/cargo/format.rs b/cargo/src/utils/cargo/format.rs new file mode 100644 index 00000000..57693449 --- /dev/null +++ b/cargo/src/utils/cargo/format.rs @@ -0,0 +1,202 @@ +use std::path::Path; + +use cargo::core::compiler::CompileKind; +use cargo::core::compiler::CompileMode; +use cargo::core::compiler::CompileTarget; +use cargo::core::compiler::CrateType; +use cargo::core::PackageId; +use cargo::core::SourceId; +use serde::{Serialize, Deserialize}; +use serde::{Serializer, Deserializer}; + + +#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[serde(rename_all = "kebab-case")] +#[serde(remote = "CompileMode")] +pub enum CompileModeProxy { + /// A target being built for a test. + Test, + /// Building a target with `rustc` (lib or bin). + Build, + /// Building a target with `rustc` to emit `rmeta` metadata only. If + /// `test` is true, then it is also compiled with `--test` to check it like + /// a test. + Check { test: bool }, + /// Used to indicate benchmarks should be built. This is not used in + /// `Unit`, because it is essentially the same as `Test` (indicating + /// `--test` should be passed to rustc) and by using `Test` instead it + /// allows some de-duping of Units to occur. + Bench, + /// A target that will be documented with `rustdoc`. + /// If `deps` is true, then it will also document all dependencies. + Doc { deps: bool, json: bool }, + /// A target that will be tested with `rustdoc`. + Doctest, + /// An example or library that will be scraped for function calls by `rustdoc`. + Docscrape, + /// A marker for Units that represent the execution of a `build.rs` script. + RunCustomBuild, +} + + +/// Remote-type for [`CompileMode`](cargo::core::TargetKind). +#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum TargetKind { + Lib(Vec), + Bin, + Test, + Bench, + Example, + CustomBuild, +} + +impl<'de> Deserialize<'de> for TargetKind { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de> { + use self::TargetKind::*; + + let raw = Vec::<&str>::deserialize(deserializer)?; + Ok(match *raw { + [] => return Err(serde::de::Error::invalid_length(0, &"at least one target kind")), + ["bin"] => Bin, + ["example"] => Example, + ["test"] => Test, + ["custom-build"] => CustomBuild, + ["bench"] => Bench, + ref lib_kinds => { + Lib(lib_kinds.iter() + .cloned() + .map(|s| CrateType::from(&s.to_owned())) + .collect()) + }, + }) + } +} + +impl Serialize for TargetKind { + fn serialize(&self, s: S) -> Result + where S: Serializer { + use self::TargetKind::*; + match self { + Lib(kinds) => s.collect_seq(kinds.iter().map(|t| t.to_string())), + Bin => ["bin"].serialize(s), + Example => ["example"].serialize(s), + Test => ["test"].serialize(s), + CustomBuild => ["custom-build"].serialize(s), + Bench => ["bench"].serialize(s), + } + } +} + + +pub fn deserialize_package_ids<'de, D>(deserializer: D) -> Result, D::Error> + where D: Deserializer<'de> { + let items = Vec::::deserialize(deserializer)?; + let mut ids = Vec::with_capacity(items.len()); + for item in items { + ids.push(string_to_package_id::(item)?); + } + Ok(ids) +} + +pub fn deserialize_package_id<'de, D>(deserializer: D) -> Result + where D: Deserializer<'de> { + string_to_package_id(String::deserialize(deserializer)?) +} + +/// Try deserialize using actual deserializer. +/// If fails, try to deserialize as old format. +/// Fixes breaking change between old and new format in cargo ~0.78.1. +pub fn string_to_package_id(mut line: String) -> Result { + // wrap into quotes for deserializer: + line.insert(0, '\"'); + line.push('\"'); + // preserve original value: + let value = &line[1..(line.len() - 1)]; + + // try actual format first: + let res = serde_json::from_str::(&line).map_err(Error::custom); + + // otherwise try old formats: + res.or_else(move |err| { + if let Some((uri, name_ver)) = value.split_once('#') { + let sid = SourceId::from_url(uri).map_err(Error::custom)?; + + if let Some((name, ver)) = name_ver.split_once('@') { + let ver = ver.parse().map_err(Error::custom)?; + let id = PackageId::new(name.into(), ver, sid); + return Ok(id); + } else { + let sid_temp = SourceId::from_url(value).map_err(Error::custom)?; + let url = sid_temp.url(); + if let Some(ver) = url.fragment() { + let ver = ver.parse().map_err(Error::custom)?; + let name = Path::new(url.path()).file_name() + .ok_or_else(|| Error::custom("Package name missed"))? + .to_string_lossy(); + let id = PackageId::new(name.as_ref().into(), ver, sid); + return Ok(id); + } + } + } + Err(err) + }) +} + + +pub fn deserialize_crate_types<'de, D>(deserializer: D) -> Result, D::Error> + where D: Deserializer<'de> { + let kinds = Vec::<&str>::deserialize(deserializer)?; + let kinds = kinds.into_iter() + .map(|s| CrateType::from(&s.to_owned())) + .collect(); + Ok(kinds) +} + + +pub fn deserialize_compile_kind<'de, D>(deserializer: D) -> Result + where D: Deserializer<'de> { + let res = if let Some(s) = Option::<&str>::deserialize(deserializer)? { + let target = CompileTarget::new(s).map_err(serde::de::Error::custom)?; + CompileKind::Target(target) + } else { + CompileKind::Host + }; + Ok(res) +} + + +#[cfg(test)] +mod tests { + use super::*; + + + #[derive(Debug, Serialize, Deserialize)] + pub struct PackageIdWrapped { + #[serde(deserialize_with = "super::deserialize_package_id")] + pub package_id: PackageId, + } + + + /// Before cargo 0.78 + #[test] + fn message_format_old_a() { + let msg = r#"{"package_id": "path+file:///Users/U/Developer/Projects/Playdate/playdate-rs/api/sys#playdate-sys@0.3.3"}"#; + serde_json::from_str::(msg).unwrap(); + } + + /// Before cargo 0.78 + #[test] + fn message_format_old_b() { + let msg = r#"{"package_id": "path+file:///Users/U/Developer/Projects/Playdate/playdate-rs/cargo/tests/crates/simple/with-cfg#0.1.0"}"#; + serde_json::from_str::(msg).unwrap(); + } + + + /// From cargo 0.78 + #[test] + fn message_format_new() { + let msg = r#"{"package_id": "playdate-sys 0.3.3 (path+file:///Users/U/Developer/Projects/Playdate/playdate-rs/api/sys)"}"#; + serde_json::from_str::(msg).unwrap(); + } +} diff --git a/cargo/src/utils/cargo/meta_deps.rs b/cargo/src/utils/cargo/meta_deps.rs new file mode 100644 index 00000000..86f1ac27 --- /dev/null +++ b/cargo/src/utils/cargo/meta_deps.rs @@ -0,0 +1,392 @@ +use std::collections::HashMap; + +use cargo::core::compiler::CompileMode; +use cargo::core::PackageId; +use cargo::util::interning::InternedString; +use cargo::CargoResult; +use playdate::manifest::CrateInfoSource; +use playdate::metadata::source::MetadataSource; +use serde::Deserialize; +use serde::de::IntoDeserializer; + +use crate::config::Config; +use crate::logger::LogErr; + +use super::build_plan::format::TargetKind; +use super::build_plan::TargetKindWild; +use super::metadata::format::{Package, Metadata}; +use super::metadata::CargoMetadataPd; +use super::unit_graph::format::Unit; +use super::unit_graph::format::UnitGraph; + + +pub fn meta_deps<'cfg>(cfg: &'cfg Config<'cfg>) -> CargoResult> { + let units = cfg.unit_graph()?; + let meta = cfg.metadata()?; + Ok(MetaDeps::new(units, meta)) +} + + +pub struct MetaDeps<'cfg> { + units: &'cfg UnitGraph, + meta: &'cfg CargoMetadataPd, + roots: Vec>, +} + +#[derive(Debug, Clone)] +pub struct RootNode<'cfg> { + node: Node<'cfg>, + deps: Vec>, +} + + +#[derive(Debug, Clone, Copy)] +pub struct Node<'cfg> { + meta: Option<&'cfg Package>>, + unit: &'cfg Unit, +} + +impl<'t> RootNode<'t> { + pub fn package_id(&self) -> &'t PackageId { self.node.package_id() } + + pub fn node(&self) -> &Node<'t> { &self.node } + + /// Dependencies _with assets_ of the root node, + /// in topological order, + /// including the root node if it has assets. + pub fn deps(&self) -> &[Node<'t>] { &self.deps } +} + +impl<'t> Node<'t> { + pub fn package_id(&self) -> &'t PackageId { &self.unit.package_id } +} + + +impl<'t> MetaDeps<'t> { + pub fn new(units: &'t UnitGraph, meta: &'t CargoMetadataPd) -> Self { + let mode_is_build = |u: &&Unit| matches!(u.mode, CompileMode::Build); + let is_prime_tk = |u: &&Unit| { + matches!( + u.target.kind, + TargetKind::Lib(_) | TargetKind::Bin | TargetKind::Example + ) + }; + let is_sub_tk = |u: &&Unit| matches!(u.target.kind, TargetKind::Lib(_)); + + + let mut roots = units.roots + .iter() + .map(|i| &units.units[*i]) + .filter(mode_is_build) + .filter(is_prime_tk) + .map(|u| { + let m = meta.packages.iter().find(|p| p.id == u.package_id); + Node::<'t> { meta: m, unit: u } + }) + .map(|node| { + RootNode::<'t> { node, + deps: Vec::with_capacity(0) } + }) + .collect::>(); + + + let deps_of = |u: &'t Unit| { + log::trace!("deps of {}::{}:", u.package_id.name(), u.target.name); + u.dependencies + .iter() + .map(|d| &units.units[d.index]) + .filter(mode_is_build) + .filter(is_sub_tk) + .map(|u| { + let m = meta.packages.iter().find(|p| p.id == u.package_id); + Node::<'t> { meta: m, unit: u } + }) + .inspect(|n| { + log::trace!(" {} (meta: {})", n.package_id().name(), n.meta.is_some(),); + }) + }; + + + // flat meta-deps-tree: + for root in roots.iter_mut() { + let deps_allowed = root.deps_allowed(); + let root_is_dev = matches!(root.node.unit.target.kind, TargetKind::Example); + log::trace!("root (dev: {root_is_dev}) {}", root.package_id().name()); + log::trace!("deps allowed: {deps_allowed} for {}", root.package_id().name()); + + let mut deps = Vec::with_capacity(units.units.len()); + + if deps_allowed { + // level 0: + deps.extend(deps_of(root.node.unit)); + + let mut from = 0; + let mut end = deps.len(); + + // level 1..-: + let mut level = 1; + while from != end { + log::trace!("depth level: {level}"); + + let next: Vec<_> = deps[from..].iter().map(|n| n.unit).flat_map(deps_of).collect(); + deps.extend(next.into_iter()); + + from = end; + end = deps.len(); + level += 1; + } + + log::debug!( + "Total deps for {}::{} ({:?}) : {}", + root.package_id().name(), + root.node.unit.target.name, + root.node.unit.target.kind, + deps.len() + ); + + + // Pre-filter. Remove following: + // - units without pd-meta + // - units without assets in the pd-meta + // - remove units with id eq root's + let removed = deps.extract_if(|n| { + n.package_id() == root.package_id() || + n.meta + .and_then(|m| m.metadata.as_ref()) + .and_then(|m| m.inner.as_ref()) + .filter(|m| !m.assets().is_empty()) + .is_none() + }) + .count(); + log::debug!("removed {removed} without metadata or assets"); + + + // dedup: remove first ones, leave a last one: + let mut dups = deps.iter().fold(HashMap::new(), |mut acc, n| { + acc.entry(n.package_id()).and_modify(|v| *v += 1).or_insert(0); + acc + }); + let removed = deps.extract_if(move |n| { + let v = dups[n.package_id()]; + if v > 0 { + dups.insert(n.package_id(), v - 1); + true + } else { + false + } + }) + .count(); + log::debug!("removed {removed} duplicates"); + + log::debug!( + "Total reduced deps for {}::{} ({:?}) : {}", + root.package_id().name(), + root.node.unit.target.name, + root.node.unit.target.kind, + deps.len() + ); + } else { + log::debug!( + "No deps for {}::{} 🤷🏻‍♂️", + root.package_id().name(), + root.node.unit.target.name + ); + } + + + // add the root: + if root.node + .meta + .and_then(|m| m.metadata.as_ref()) + .and_then(|m| m.inner.as_ref()) + .filter(|m| !m.assets().is_empty() || (root_is_dev && !m.dev_assets().is_empty())) + .is_some() + { + log::trace!( + "add root too because it has assets for {}", + root.node.unit.target.name + ); + + deps.insert(0, root.node); + } + + + deps.iter().enumerate().for_each(|(i, n)| { + log::trace!( + "{i}: {}::{} ({:?}), meta: {}", + root.package_id().name(), + root.node.unit.target.name, + root.node.unit.target.kind, + n.meta.is_some() + ); + }); + + log::debug!( + "Total finally deps for {}::{} ({:?}) : {}", + root.package_id().name(), + root.node.unit.target.name, + root.node.unit.target.kind, + deps.len() + ); + + root.deps = deps; + } + + Self { units, meta, roots } + } + + + pub fn roots(&self) -> &[RootNode<'t>] { self.roots.as_slice() } + + pub fn root_for(&self, id: &PackageId, tk: &TargetKindWild, tname: &str) -> CargoResult<&RootNode<'t>> { + self.roots + .iter() + .find(|n| n.package_id() == id && *tk == n.node.unit.target.kind && n.node.unit.target.name == tname) + .ok_or_else(|| anyhow::anyhow!("Root not found for {id}::{tname}")) + } + + pub fn units(&self) -> &'t UnitGraph { self.units } + pub fn meta(&self) -> &'t CargoMetadataPd { self.meta } + + + pub fn deps_allowed_for(&self, root: PackageId) -> bool { + self.roots + .iter() + .find(|u| u.package_id() == &root) + .and_then(|u| { + u.node + .meta + .as_ref() + .and_then(|m| m.metadata.as_ref()) + .and_then(|m| m.inner.as_ref()) + .map(|m| m.assets_options()) + }) + .unwrap_or_default() + .dependencies + } +} + + +pub trait DependenciesAllowed { + fn deps_allowed(&self) -> bool; +} + + +impl DependenciesAllowed for RootNode<'_> { + fn deps_allowed(&self) -> bool { self.node.deps_allowed() } +} + +impl DependenciesAllowed for Node<'_> { + fn deps_allowed(&self) -> bool { + self.meta + .as_ref() + .and_then(|m| m.metadata.as_ref()) + .and_then(|m| m.inner.as_ref()) + .map(|m| m.assets_options()) + .unwrap_or_default() + .dependencies + } +} + + +impl DependenciesAllowed for cargo::core::Package { + fn deps_allowed(&self) -> bool { + self.manifest() + .custom_metadata() + .and_then(|v| { + Metadata::::deserialize(v.to_owned().into_deserializer()).log_err() + .ok() + }) + .and_then(|m| m.inner) + .map(|m| m.assets_options().dependencies) + .unwrap_or_default() + } +} + + +impl<'t> Node<'t> { + pub fn into_source(self) -> impl CrateInfoSource + 't { CrateNode::from(self) } + pub fn as_source(&self) -> impl CrateInfoSource + 't { self.to_owned().into_source() } +} + +impl<'t> RootNode<'t> { + pub fn into_source(self) -> impl CrateInfoSource + 't { CrateNode::from(self.node) } + pub fn as_source(&self) -> impl CrateInfoSource + 't { self.to_owned().into_source() } +} + + +struct CrateNode<'t> { + node: Node<'t>, + bins: Vec<&'t str>, + examples: Vec<&'t str>, +} + +impl<'t> From> for CrateNode<'t> { + fn from(node: Node<'t>) -> Self { + Self { node, + bins: node.meta + .as_ref() + .into_iter() + .flat_map(|m| m.targets.iter()) + .filter(|t| t.kind == TargetKind::Bin) + .map(|t| t.name.as_str()) + .collect(), + examples: node.meta + .as_ref() + .into_iter() + .flat_map(|m| m.targets.iter()) + .filter(|t| t.kind == TargetKind::Example) + .map(|t| t.name.as_str()) + .collect() } + } +} + +impl CrateInfoSource for CrateNode<'_> { + type Authors = [String]; + + fn name(&self) -> std::borrow::Cow { self.node.package_id().name().as_str().into() } + + fn authors(&self) -> &Self::Authors { + self.node + .meta + .as_ref() + .map(|m| m.authors.as_slice()) + .unwrap_or_default() + } + + fn version(&self) -> std::borrow::Cow { + self.node + .meta + .as_ref() + .map(|m| m.version.as_str().into()) + .unwrap_or_default() + } + + fn description(&self) -> Option> { + self.node + .meta + .as_ref() + .and_then(|m| m.description.as_deref()) + .map(Into::into) + } + + fn metadata(&self) -> Option { + self.node + .meta + .as_ref() + .and_then(|m| m.metadata.as_ref()) + .and_then(|m| m.inner.as_ref()) + } + + fn bins(&self) -> &[&str] { &self.bins } + + fn examples(&self) -> &[&str] { &self.examples } + + fn manifest_path(&self) -> std::borrow::Cow { + self.node + .meta + .as_ref() + .map(|m| m.manifest_path.as_path().into()) + .unwrap_or_default() + } +} diff --git a/cargo/src/utils/cargo/metadata.rs b/cargo/src/utils/cargo/metadata.rs new file mode 100644 index 00000000..157fb3e1 --- /dev/null +++ b/cargo/src/utils/cargo/metadata.rs @@ -0,0 +1,167 @@ +use std::ffi::OsStr; + +use cargo::util::interning::InternedString; +use cargo::CargoResult; + +use crate::config::Config; +use crate::proc::cargo_proxy_with; +use crate::proc::read_cargo_json; + + +pub type CargoMetadataPd = format::Report>; + + +pub fn metadata(cfg: &Config) -> CargoResult { + let mut cargo = cargo_proxy_with(cfg, "metadata", false)?; + + cargo.arg("--format-version=1"); + + let kinds = &cfg.compile_options.build_config.requested_kinds[..]; + if kinds.len() == 1 && + let Some(kind) = kinds.first() + { + match kind { + cargo::core::compiler::CompileKind::Target(target) if target != &cfg.host_target => { + cargo.args(["--filter-platform", &target.rustc_target()]); + }, + _ => (), + } + } + + // add manifest options: + { + const MANIFEST_PATH: &str = "--manifest-path"; + let expected = &[ + OsStr::new("--locked"), + OsStr::new("--offline"), + OsStr::new("--frozen"), + ]; + let args = cfg.args.iter().enumerate().filter(|(_, arg)| { + expected.contains(&arg.as_os_str()) || + arg.as_os_str() == MANIFEST_PATH || + arg.to_string_lossy().starts_with(MANIFEST_PATH) + }); + + args.for_each(|(i, arg)| { + cargo.arg(arg); + if arg.as_os_str() == MANIFEST_PATH { + cargo.arg(&cfg.args[i + 1]); + } + }); + + // if !cfg.workspace.ignore_lock() && !cargo.get_args().any(|arg| arg == "--locked") { + // cargo.arg("--locked"); + // } + } + + read_cargo_json::(cfg, cargo) +} + + +pub mod format { + #![allow(dead_code)] + use std::path::PathBuf; + + use cargo::core::dependency::DepKind; + use cargo::core::PackageId; + use cargo::core::SourceId; + use serde::Deserialize; + use serde::Deserializer; + + use crate::utils::cargo::unit_graph::format::UnitTarget; + + pub use super::super::format::*; + + pub use playdate::metadata::format::CrateMetadata as Metadata; + + + /// `cargo metadata` output __v1__, + /// just necessary fields. + #[derive(Debug, Deserialize)] + #[serde(bound(deserialize = "Metadata: Deserialize<'de>"))] + pub struct Report { + pub version: usize, + pub packages: Vec>, + pub target_directory: PathBuf, + + pub workspace_members: Vec, + pub workspace_default_members: Vec, + pub workspace_root: PathBuf, + #[serde(alias = "metadata")] + pub workspace_metadata: Option, + + pub resolve: Resolve, + } + + #[derive(Deserialize, Debug)] + pub struct Resolve { + pub nodes: Vec, + pub root: Option, + } + + #[derive(Deserialize, Debug)] + pub struct ResolveNode { + #[serde(deserialize_with = "deserialize_package_id")] + pub id: PackageId, + #[serde(deserialize_with = "deserialize_package_ids")] + pub dependencies: Vec, + pub deps: Vec, + } + + + #[derive(Deserialize, Debug)] + #[serde(bound(deserialize = "Metadata: Deserialize<'de>"))] + pub struct Package { + #[serde(deserialize_with = "deserialize_package_id")] + pub id: PackageId, + pub source: Option, + pub dependencies: Vec, + + pub name: String, + pub authors: Vec, + pub version: String, + pub description: Option, + pub manifest_path: PathBuf, + pub targets: Vec, + pub metadata: Option, + } + + #[derive(Deserialize, Debug)] + pub struct PackageDep { + pub name: String, + pub rename: Option, + + pub source: Option, + pub req: semver::VersionReq, + #[serde(serialize_with = "DepKind::serialize")] + #[serde(deserialize_with = "deserialize_dep_kind")] + pub kind: DepKind, + + pub optional: bool, + // ... features, target, registry + // pub target: Option, + } + + #[derive(Deserialize, Debug)] + pub struct NodeDep { + pub name: String, + #[serde(deserialize_with = "deserialize_package_id")] + pub pkg: PackageId, + pub dep_kinds: serde_json::Value, + } + + pub fn deserialize_dep_kind<'de, D>(deserializer: D) -> Result + where D: Deserializer<'de> { + let kind = Option::<&str>::deserialize(deserializer)?; + let kind = match kind { + Some("dev") => DepKind::Development, + Some("build") => DepKind::Build, + None => DepKind::Normal, + kind => { + log::error!("Unknown dep kind: {kind:?}"); + DepKind::Normal + }, + }; + Ok(kind) + } +} diff --git a/cargo/src/utils/cargo.rs b/cargo/src/utils/cargo/mod.rs similarity index 87% rename from cargo/src/utils/cargo.rs rename to cargo/src/utils/cargo/mod.rs index e52e6db8..120de93a 100644 --- a/cargo/src/utils/cargo.rs +++ b/cargo/src/utils/cargo/mod.rs @@ -3,6 +3,14 @@ use cargo::core::compiler::CompileTarget; use playdate::consts::DEVICE_TARGET; +/// Shared format +pub(crate) mod format; +pub mod build_plan; +pub mod unit_graph; +pub mod meta_deps; +pub mod metadata; + + pub trait CompileKindExt { fn is_playdate(&self) -> bool; fn is_simulator(&self) -> bool; diff --git a/cargo/src/utils/cargo/unit_graph.rs b/cargo/src/utils/cargo/unit_graph.rs new file mode 100644 index 00000000..709105a8 --- /dev/null +++ b/cargo/src/utils/cargo/unit_graph.rs @@ -0,0 +1,73 @@ +use cargo::CargoResult; + +use crate::cli::cmd::Cmd; +use crate::config::Config; +use crate::proc::cargo_proxy_cmd; +use crate::proc::read_cargo_json; +use self::format::UnitGraph; + + +pub fn unit_graph(cfg: &Config) -> CargoResult { + let mut cargo = cargo_proxy_cmd(cfg, &Cmd::Build)?; + + cargo.args(["--unit-graph", "-Zunstable-options"]); + + let value: UnitGraph = read_cargo_json(cfg, cargo)?; + Ok(value) +} + + +pub mod format { + #![allow(dead_code)] + use cargo::core::PackageId; + use cargo::util::command_prelude::CompileMode; + use cargo::core::compiler::CompileKind; + use cargo::core::compiler::CrateType; + use serde::Deserialize; + + pub use super::super::format::*; + + + #[derive(Debug, Deserialize)] + pub struct UnitGraph { + pub version: usize, + pub units: Vec, + pub roots: Vec, + } + + #[derive(Debug, Deserialize, Eq, Hash, PartialEq)] + pub struct Unit { + #[serde(deserialize_with = "deserialize_package_id", alias = "pkg_id")] + pub package_id: PackageId, + pub target: UnitTarget, + #[serde(serialize_with = "CompileKind::serialize")] + #[serde(deserialize_with = "deserialize_compile_kind")] + pub platform: CompileKind, + #[serde(serialize_with = "CompileMode::serialize")] + #[serde(deserialize_with = "CompileModeProxy::deserialize")] + pub mode: CompileMode, + pub dependencies: Vec, + // ... + // pub features: Vec, + // pub profile: crate::proc::reader::format::ArtifactProfile, + } + + #[derive(Debug, Deserialize, Eq, Hash, PartialEq)] + pub struct UnitTarget { + pub kind: TargetKind, + #[serde(deserialize_with = "deserialize_crate_types")] + pub crate_types: Vec, + pub name: String, + pub src_path: String, + // ... + } + + #[derive(Debug, Deserialize, Eq, Hash, PartialEq)] + pub struct UnitDep { + pub index: usize, + pub extern_crate_name: String, + pub public: bool, + #[serde(alias = "noprelude")] + pub no_prelude: bool, + } +} diff --git a/cargo/src/utils/mod.rs b/cargo/src/utils/mod.rs index 2d6c04bb..fb89f99d 100644 --- a/cargo/src/utils/mod.rs +++ b/cargo/src/utils/mod.rs @@ -16,7 +16,8 @@ pub mod path; pub mod logging; -// TODO: It used several times, make it global. +#[deprecated(since = "0.5", + note = "TODO: use crate::utils::cargo:: unit_graph with metadata instead")] pub struct LazyBuildContext<'a, 'cfg> { workspace: &'cfg Workspace<'cfg>, bcx: Lazy>, diff --git a/cargo/tests/crates/metadata/Cargo.toml b/cargo/tests/crates/metadata/Cargo.toml index 74e8dd4e..b9f2a6d1 100644 --- a/cargo/tests/crates/metadata/Cargo.toml +++ b/cargo/tests/crates/metadata/Cargo.toml @@ -16,7 +16,7 @@ description = "test" [package.metadata.playdate.assets] "main/" = "Cargo.toml" -[package.metadata.playdate.assets.options] +[package.metadata.playdate.options.assets] dependencies = true overwrite = true diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 64c81e34..01652420 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "nightly-2024-04-12" +channel = "nightly-2024-05-28" profile = "minimal" targets = ["thumbv7em-none-eabihf"] components = [ diff --git a/support/build/Cargo.toml b/support/build/Cargo.toml index e729dd39..82e2d952 100644 --- a/support/build/Cargo.toml +++ b/support/build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "playdate-build" -version = "0.3.0" +version = "0.4.0-pre1" readme = "README.md" description = "Utils that help to build package for Playdate" keywords = ["playdate", "package", "encoding", "manifest", "assets"] @@ -14,18 +14,20 @@ repository.workspace = true [dependencies] log.workspace = true -serde = { workspace = true, features = ["derive"], optional = true } -serde_json = { workspace = true, optional = true } -toml = { workspace = true, optional = true } dirs.workspace = true fs_extra.workspace = true regex.workspace = true +semver.workspace = true wax = "0.6" symlink = "0.1" -[dependencies.crate-metadata] -version = "0.1" +toml = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } + +[dependencies.serde] +workspace = true optional = true +features = ["derive"] [dependencies.utils] workspace = true @@ -36,8 +38,8 @@ features = ["log"] [features] default = [] toml = ["serde", "dep:toml"] +json = ["serde_json"] serde_json = ["serde", "dep:serde_json"] -crate-metadata = ["serde_json", "dep:crate-metadata"] assets-report = [] diff --git a/support/build/README.md b/support/build/README.md index a3b78b6d..7f4417d9 100644 --- a/support/build/README.md +++ b/support/build/README.md @@ -12,23 +12,60 @@ Here is the metadata format explanation in examples ### Package Info +The following fields are used to generate the package manifest: + ```toml # Playdate Package Info # official doc: https://sdk.play.date/#pdxinfo [package.metadata.playdate] -name = "{name}" # optional, default is package.name -author = "{author}" # optional, default is package.authors -version = "{version}" # optional, default is package.version -description = "{description}" # optional, default is package.description -bundle-id = "com.yourcompany.{bundle_id}" +bundle-id = "com.yourcompany.game" +name = "My Game" # default is package.name +author = "Alex" # default is package.authors +version = "0.0" # default is package.version +description = "short about" # default is package.description + +image-path = "img/system" +launch-sound-path = "sfx/jump" + +content-warning = "This game contains mild realistic, violence and bloodshed." +content-warning2 = "Really scary game." -image-path = "img/system" # optional -launch-sound-path = "sfx/jump" # optional +build-number = 42 # also can be string, e.g "42" -content-warning = "This game contains mild realistic, violence and bloodshed." # optional -content-warning2 = "Really scary game." # optional +# also extra fields are supported +# acceptable types of values: string, number, boolean +foo = "bar" ``` +_Note, only `bundle-id` is required, other fields are optional._ + + +#### Target-specific Package Info + +Main [Package Info](#package-info) can be overridden with special _table_ for a `bin` or `example`. +All manifest fields are acceptable, but optional. + +Two formats are supported. +First is like a cargo's targets: + +```toml +[[package.metadata.playdate.example]] +target = "existing-example-name" # pointing to cargo-target name +# next is same as for main manifest fields, all are optional: +bundle-id = "com.yourcompany.game.example" +name = "My Example" +``` + +Second if just a table: + +```toml +[package.metadata.playdate.example.existing-example-name] +bundle-id = "com.yourcompany.game.example" +content-warning = "Scary experimental stuff." +``` + +_Important: you should not mix these two formats in the same document._ + ### Assets @@ -103,15 +140,11 @@ Also this way supports simple include and exclude instructions: #### Assets Options -There is some options where to set asset options: -- `[package.metadata.playdate.assets.options]` -- `[package.metadata.playdate.options.assets]` - -Both are equal but should not be both in one crate. +This is how assets will be collected for your package. ```toml -[package.metadata.playdate.assets.options] -dependencies = true # allow to build assets for dependencies (default is `true`) +[package.metadata.playdate.options.assets] +dependencies = true # allow to build assets for dependencies (default is `false`) overwrite = true # overwrite existing assets in build dir (default is `true`) method = "link" # "copy" or "link" (default is `link`) - how assets should be collected, make symlinks or copy files follow-symlinks = true # follow symlinks (default is `true`) diff --git a/support/build/src/assets/plan.rs b/support/build/src/assets/plan.rs index d7aafd57..48ae5b43 100644 --- a/support/build/src/assets/plan.rs +++ b/support/build/src/assets/plan.rs @@ -6,20 +6,18 @@ use std::path::{Path, PathBuf, MAIN_SEPARATOR}; use wax::{Glob, Pattern}; use crate::config::Env; -use crate::metadata::format::AssetsOptions; -use crate::metadata::format::PlayDateMetadataAssets; -use crate::value::Value; +use crate::metadata::format::{AssetsOptions, AssetsRules, RuleValue}; use super::resolver::*; /// Create build plan for assets. -pub fn build_plan<'l, 'r, 'c: 'l, V>(env: &'c Env, - assets: &PlayDateMetadataAssets, - options: &AssetsOptions, - crate_root: Option<&'c Path>) - -> Result, super::Error> - where V: Value +pub fn build_plan<'l, 'r, 'c: 'l, 'v, S>(env: &'c Env, + assets: &AssetsRules, + options: &AssetsOptions, + crate_root: Option<&'c Path>) + -> Result, super::Error> + where S: Eq + Hash + ToString { // copy_unresolved => get all files with glob // include_unresolved => same @@ -38,8 +36,9 @@ pub fn build_plan<'l, 'r, 'c: 'l, V>(env: &'c Env, let crate_root = crate_root.unwrap_or_else(|| env.cargo_manifest_dir()); let link_behavior = options.link_behavior(); - let to_relative = |s: &String| -> String { - let p = Path::new(s); + let to_relative = |s: &S| -> String { + let s = s.to_string(); + let p = Path::new(&s); if p.is_absolute() || p.has_root() { let trailing_sep = p.components().count() > 1 && s.ends_with(PATH_SEPARATOR); let mut s = p.components().skip(1).collect::().display().to_string(); @@ -54,7 +53,7 @@ pub fn build_plan<'l, 'r, 'c: 'l, V>(env: &'c Env, }; match assets { - PlayDateMetadataAssets::List(vec) => { + AssetsRules::List(vec) => { include_unresolved.extend( vec.iter() .map(to_relative) @@ -62,18 +61,19 @@ pub fn build_plan<'l, 'r, 'c: 'l, V>(env: &'c Env, .map(|e| enver.expr(e, env)), ) }, - PlayDateMetadataAssets::Map(map) => { + AssetsRules::Map(map) => { for (k, v) in map { let k = to_relative(k); - if let Some(v) = v.as_bool() { - match v { - true => include_unresolved.push(enver.expr(Expr::from(k), env)), - false => exclude_exprs.push(enver.expr(Expr::from(k), env)), - } - } else if let Some(from) = v.as_str() { - map_unresolved.push((enver.expr(Expr::from(k), env), enver.expr(Expr::from(from), env))) - } else { - return Err(format!("not supported type of value: {v} for key: {k}").into()); + match v { + RuleValue::Boolean(v) => { + match v { + true => include_unresolved.push(enver.expr(Expr::from(k), env)), + false => exclude_exprs.push(enver.expr(Expr::from(k), env)), + } + }, + RuleValue::String(from) => { + map_unresolved.push((enver.expr(Expr::from(k), env), enver.expr(Expr::from(from), env))) + }, } } }, @@ -467,9 +467,9 @@ mod tests { 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 crate::metadata::format::RuleValue; + use crate::metadata::format::AssetsRules; use super::*; @@ -607,7 +607,7 @@ mod tests { 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 assets = AssetsRules::List(exprs); let plan = build_plan(&env, &assets, &opts, root).unwrap(); @@ -654,7 +654,7 @@ mod tests { }; let exprs = tests.keys().map(|s| s.to_string()).collect(); - let assets = PlayDateMetadataAssets::List::(exprs); + let assets = AssetsRules::List(exprs); let plan = build_plan(&env, &assets, &opts, root).unwrap(); @@ -686,7 +686,7 @@ mod tests { 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 assets = AssetsRules::List(exprs); let plan = build_plan(&env, &assets, &opts, root).unwrap(); @@ -732,7 +732,7 @@ mod tests { }; let exprs = tests.keys().map(|s| s.to_string()).collect(); - let assets = PlayDateMetadataAssets::List::(exprs); + let assets = AssetsRules::List(exprs); let plan = build_plan(&env, &assets, &opts, root).unwrap(); @@ -771,7 +771,7 @@ mod tests { let exprs = ["${TMP}/*.txt", "${SUB}/*.txt"]; - let assets = PlayDateMetadataAssets::List::(exprs.iter().map(|s| s.to_string()).collect()); + let assets = AssetsRules::List(exprs.iter().map(|s| s.to_string()).collect()); let plan = build_plan(&env, &assets, &opts, root).unwrap(); @@ -825,9 +825,10 @@ mod tests { let tests: HashSet<_> = vec!["Cargo.toml", "src/lib.rs"].into_iter().collect(); let exprs = tests.iter() - .map(|s| (s.to_string(), Value::Boolean(true))) + .map(|s| (s.to_string(), RuleValue::Boolean(true))) .collect(); - let assets = PlayDateMetadataAssets::Map::(exprs); + + let assets = AssetsRules::Map(exprs); let plan = build_plan(&env, &assets, &opts, root).unwrap(); @@ -864,9 +865,10 @@ mod tests { let stripped_trg = &trg.replace('/', "").trim().to_owned(); let exprs = tests.iter() - .map(|s| (trg.to_string(), Value::String(s.to_string()))) + .map(|s| (trg.to_string(), RuleValue::String(s.to_string()))) .collect(); - let assets = PlayDateMetadataAssets::Map::(exprs); + + let assets = AssetsRules::Map(exprs); let plan = build_plan(&env, &assets, &opts, root).unwrap(); @@ -909,9 +911,10 @@ mod tests { for trg in targets { let exprs = tests.iter() - .map(|s| (trg.to_string(), Value::String(s.to_string()))) + .map(|s| (trg.to_string(), RuleValue::String(s.to_string()))) .collect(); - let assets = PlayDateMetadataAssets::Map::(exprs); + + let assets = AssetsRules::Map(exprs); let plan = build_plan(&env, &assets, &opts, root).unwrap(); @@ -955,9 +958,10 @@ mod tests { for trg in targets { let exprs = tests.iter() - .map(|s| (trg.to_string(), Value::String(s.to_string()))) + .map(|s| (trg.to_string(), RuleValue::String(s.to_string()))) .collect(); - let assets = PlayDateMetadataAssets::Map::(exprs); + + let assets = AssetsRules::Map(exprs); let plan = build_plan(&env, &assets, &opts, root).unwrap(); diff --git a/support/build/src/assets/resolver.rs b/support/build/src/assets/resolver.rs index c4d5a450..f20f68c0 100644 --- a/support/build/src/assets/resolver.rs +++ b/support/build/src/assets/resolver.rs @@ -122,9 +122,13 @@ impl EnvResolver { pub fn str<'c, S: AsRef>(&self, s: S, env: &'c Env) -> Cow<'c, str> { let re = &self.0; - // XXX: possible recursion for case "${VAR}" where $VAR="${VAR}" + // Possible recursion for case "${VAR}" where $VAR="${VAR}" + let mut anti_recursion_counter: u8 = 42; + let mut replaced = String::from(s.as_ref()); - while re.is_match(replaced.as_str()) { + while re.is_match(replaced.as_str()) && anti_recursion_counter > 0 { + anti_recursion_counter -= 1; + if let Some(captures) = re.captures(replaced.as_str()) { let full = &captures[0]; let name = &captures[2]; @@ -133,12 +137,11 @@ impl EnvResolver { .get(name) .map(Cow::from) .or_else(|| std::env::var(name).map_err(log_err).ok().map(Cow::from)) - .unwrap_or_else(|| { - // XXX: should we panic here? - panic!("Env var \"{name}\" not found") - }); + .unwrap_or_else(|| name.into()); replaced = replaced.replace(full, &var); + } else { + break; } } replaced.into() @@ -155,10 +158,7 @@ impl EnvResolver { let var = std::env::var(name).map_err(log_err) .map(Cow::from) - .unwrap_or_else(|_| { - // XXX: should we panic here? - panic!("Env var \"{name}\" not found") - }); + .unwrap_or_else(|_| name.into()); replaced = replaced.replace(full, &var); } } @@ -534,11 +534,23 @@ mod tests { } #[test] - #[should_panic] fn resolver_missed() { let resolver = EnvResolver::new(); let env = Env::try_default().unwrap(); - let expr = Expr::from("${MISSED}/file.txt"); + let mut expr = Expr::from("${MISSED}/file.txt"); + resolver.expr(&mut expr, &env); + + assert_eq!("MISSED/file.txt", expr.actual()); + assert_eq!("MISSED/file.txt", expr.as_str()); + assert_eq!("${MISSED}/file.txt", expr.original()); + } + + #[test] + fn resolver_recursion() { + let resolver = EnvResolver::new(); + let mut env = Env::try_default().unwrap(); + env.vars.insert("VAR".into(), "${VAR}".into()); + let expr = Expr::from("${VAR}/file.txt"); resolver.expr(expr, &env); } } diff --git a/support/build/src/lib.rs b/support/build/src/lib.rs index a62a574c..a1d3a7d1 100644 --- a/support/build/src/lib.rs +++ b/support/build/src/lib.rs @@ -1,5 +1,7 @@ #![feature(extract_if)] #![feature(io_error_more)] +#![feature(slice_concat_trait)] +#![cfg_attr(test, feature(assert_matches))] #[macro_use] extern crate log; @@ -8,7 +10,6 @@ pub use utils::*; pub mod fs; -pub mod value; pub mod layout; pub mod config; pub mod assets; diff --git a/support/build/src/manifest/format.rs b/support/build/src/manifest/format.rs index 13c257ce..2632febf 100644 --- a/support/build/src/manifest/format.rs +++ b/support/build/src/manifest/format.rs @@ -1,73 +1,142 @@ -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, -} +pub use crate::metadata::format::Manifest; +use crate::metadata::source::ManifestSourceOptExt; + + +pub trait ManifestFmt { + fn write_to(&self, to: &mut W) -> std::io::Result<()> + where Self: ManifestSourceOptExt { + let mut buf = String::new(); + self.write_to_fmt(&mut buf).map_err(std::io::Error::other)?; + to.write_all(buf.as_bytes()) + } + + fn write_to_fmt(&self, to: &mut W) -> std::fmt::Result + where Self: ManifestSourceOptExt { + let data = self; -impl Manifest { - pub fn to_manifest_string(&self) -> String { - let mut result = String::new(); + let is_not_empty = |s: &&str| !s.trim().is_empty(); - 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) + { + let mut write_fmt = |k: &str, v: &str| to.write_fmt(format_args!("{}={}\n", k.trim(), v.trim())); + + if let Some(s) = data.name().filter(is_not_empty) { + write_fmt("name", s)?; + } + if let Some(s) = data.version().filter(is_not_empty) { + write_fmt("version", s)? + } + if let Some(s) = data.author().filter(is_not_empty) { + write_fmt("author", s)? + } + if let Some(s) = data.bundle_id().filter(is_not_empty) { + write_fmt("bundleID", s)? + } + if let Some(s) = data.description().filter(is_not_empty) { + write_fmt("description", s)? + } + if let Some(s) = data.image_path().filter(is_not_empty) { + write_fmt("imagePath", s)? + } + if let Some(s) = data.launch_sound_path().filter(is_not_empty) { + write_fmt("launchSoundPath", s)? + } + if let Some(s) = data.content_warning().filter(is_not_empty) { + write_fmt("contentWarning", s)? + } + if let Some(s) = data.content_warning2().filter(is_not_empty) { + write_fmt("contentWarning2", s)? } } - 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(v) = data.build_number() { + to.write_fmt(format_args!("{}={}\n", "buildNumber", v))? } - 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"); + + if let Some(extra) = data.iter_extra() { + for (key, value) in extra.into_iter() { + let (key, value) = (key.as_ref(), value.as_ref()); + if is_not_empty(&key) && !value.is_empty() { + to.write_fmt(format_args!("{}={}\n", key.trim(), value))? + } } } - result + + Ok(()) + } + + + fn to_manifest_string(&self) -> Result + where Self: ManifestSourceOptExt { + let mut buf = String::new(); + self.write_to_fmt(&mut buf)?; + Ok(buf) + } +} + + +impl ManifestFmt for T {} + + +#[cfg(test)] +mod tests { + use crate::metadata::format::Ext; + use crate::metadata::format::ExtraFields; + use super::ManifestFmt; + use super::Manifest; + + + #[test] + fn fmt_empty() { + let m = Manifest::<&str>::default(); + assert!(m.to_manifest_string().unwrap().is_empty()); + } + + #[test] + fn fmt_full() { + let m = Manifest::<&str> { name: "name".into(), + version: "version".into(), + author: "author".into(), + bundle_id: "bundle_id".into(), + description: "description".into(), + image_path: "image_path".into(), + launch_sound_path: "launch_sound_path".into(), + content_warning: "content_warning".into(), + content_warning2: "content_warning2".into(), + build_number: 42.into() }; + let s = m.to_manifest_string().unwrap(); + assert_eq!( + "name=name\nversion=version\nauthor=author\nbundleID=bundle_id\ndescription=description\nimagePath=image_path\nlaunchSoundPath=launch_sound_path\ncontentWarning=content_warning\ncontentWarning2=content_warning2\nbuildNumber=42\n", + s + ); + } + + #[test] + fn fmt_ext() { + let main = Manifest::<&str> { bundle_id: "bundle_id".into(), + ..Default::default() }; + let mut extra = ExtraFields::new(); + extra.insert("foo".to_owned(), "bar".to_owned().into()); + + let m = Ext::new(main, extra); + let s = m.to_manifest_string().unwrap(); + assert_eq!("bundleID=bundle_id\nfoo=bar\n", s); + } + + #[test] + fn fmt_trim() { + let main = Manifest::<&str> { name: " name".into(), + bundle_id: "bundle_id ".into(), + description: " description ".into(), + ..Default::default() }; + let mut extra = ExtraFields::new(); + extra.insert(" foo ".to_owned(), " bar ".to_owned().into()); + + let m = Ext::new(main, extra); + let s = m.to_manifest_string().unwrap(); + assert_eq!( + "name=name\nbundleID=bundle_id\ndescription=description\nfoo=bar\n", + s + ); } } diff --git a/support/build/src/manifest/mod.rs b/support/build/src/manifest/mod.rs index bc833aed..adc434aa 100644 --- a/support/build/src/manifest/mod.rs +++ b/support/build/src/manifest/mod.rs @@ -1,215 +1,5 @@ -use std::borrow::Cow; - pub use crate::compile::PDX_PKG_MANIFEST_FILENAME; -use crate::metadata::format::PlayDateMetadata; -use self::format::Manifest; - +pub use crate::metadata::source::CrateInfoSource; +pub use crate::metadata::source::{ManifestSourceOpt, ManifestSourceOptExt}; pub mod format; - - -pub trait ManifestDataSource { - type Value: crate::value::Value; - - fn name(&self) -> &str; - fn authors(&self) -> &[String]; - fn version(&self) -> Cow; - fn description(&self) -> Option<&str>; - fn metadata(&self) -> Option<&PlayDateMetadata>; -} - - -impl<'t, T> TryFrom> for Manifest where T: ManifestDataSource { - type Error = &'static str; - - fn try_from(source: SourceRef<'t, T>) -> Result { - let metadata = source.metadata() - .ok_or("[package.metadata.playdate] not found in the manifest file Cargo.toml")?; - - let description = metadata.description - .to_owned() - .or(source.description().map(ToOwned::to_owned)) - .ok_or("description not found")?; - let manifest = Manifest { name: metadata.name.to_owned().unwrap_or(source.name().to_owned()), - author: metadata.author.to_owned().unwrap_or(source.authors().join(", ")), - description, - bundle_id: metadata.bundle_id.to_owned(), - version: metadata.version - .to_owned() - .unwrap_or(source.version().to_string()), - build_number: metadata.build_number.as_ref().and_then(|v| v.parse().ok()), - 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(), - extra: metadata.extra.to_owned() }; - Ok(manifest) - } -} - -impl Manifest { - pub fn try_from_source(source: T) -> Result - where T: ManifestDataSource { - SourceRef(&source).try_into() - } -} - - -struct SourceRef<'t, T: ManifestDataSource>(pub &'t T); -impl<'t, T: ManifestDataSource> SourceRef<'t, T> { - #![allow(dead_code)] - pub fn inner(&self) -> &'t T { self.0 } - pub fn into_inner(self) -> &'t T { self.0 } -} - -impl<'t, T> From<&'t T> for SourceRef<'t, T> where T: ManifestDataSource { - fn from(value: &'t T) -> Self { Self(value) } -} - -impl<'t, T> ManifestDataSource for SourceRef<'t, T> where T: ManifestDataSource { - type Value = T::Value; - fn name(&self) -> &str { self.inner().name() } - fn authors(&self) -> &[String] { self.inner().authors() } - fn version(&self) -> Cow { self.inner().version() } - fn description(&self) -> Option<&str> { self.inner().description() } - fn metadata(&self) -> Option<&PlayDateMetadata> { self.inner().metadata() } -} - - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - use std::ops::Deref; - use crate::metadata::format::PlayDateMetadata; - use super::*; - - #[cfg(feature = "serde_json")] - 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 { - name: Name, - authors: Vec, - version: Ver, - description: Option, - metadata: Option>, - } - - impl ManifestDataSource for ManifestSource - where N: AsRef, - V: AsRef, - D: Deref - { - type Value = Value; - - fn name(&self) -> &str { self.name.as_ref() } - fn authors(&self) -> &[String] { &self.authors } - fn version(&self) -> Cow { self.version.as_ref().into() } - fn description(&self) -> Option<&str> { self.description.as_deref() } - fn metadata(&self) -> Option<&PlayDateMetadata> { self.metadata.as_ref() } - } - - - fn metadata_minimal() -> PlayDateMetadata { - PlayDateMetadata { bundle_id: "bundle.id".to_owned(), - name: Default::default(), - version: Default::default(), - author: Default::default(), - description: Default::default(), - image_path: Default::default(), - launch_sound_path: Default::default(), - content_warning: Default::default(), - content_warning2: Default::default(), - build_number: Default::default(), - assets: Default::default(), - dev_assets: Default::default(), - options: Default::default(), - support: Default::default(), - extra: Default::default() } - } - fn metadata_maximal() -> PlayDateMetadata { - PlayDateMetadata { bundle_id: "bundle.id".to_owned(), - name: Some("name".to_owned()), - version: Some("0.42.0".to_owned()), - author: Some("author".to_owned()), - description: Some("description".to_owned()), - image_path: Some("image_path".to_owned()), - launch_sound_path: Some("launch_sound_path".to_owned()), - content_warning: Some("content_warning".to_owned()), - content_warning2: Some("content_warning2".to_owned()), - build_number: Some("42".to_owned()), - assets: Default::default(), - dev_assets: Default::default(), - options: 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 from_data_source_minimal() { - let source = ManifestSource { name: "name", - authors: vec!["author".to_owned()], - version: "0.42.0", - description: Some("description"), - metadata: Some(metadata_minimal()) }; - let manifest = Manifest::try_from_source(source).expect("manifest"); - - assert_eq!(&manifest.name, "name"); - assert_eq!(&manifest.author, "author"); - assert_eq!(&manifest.version, "0.42.0"); - assert_eq!(&manifest.description, "description"); - assert_eq!(&manifest.bundle_id, "bundle.id"); - - assert!(manifest.image_path.is_none()); - assert!(manifest.launch_sound_path.is_none()); - assert!(manifest.content_warning.is_none()); - assert!(manifest.content_warning2.is_none()); - assert!(manifest.build_number.is_none()); - } - - #[test] - 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(metadata_maximal()) }; - let manifest = Manifest::try_from_source(source).expect("manifest"); - - assert_eq!(&manifest.name, "name"); - assert_eq!(&manifest.author, "author"); - assert_eq!(&manifest.version, "0.42.0"); - assert_eq!(&manifest.description, "description"); - assert_eq!(&manifest.bundle_id, "bundle.id"); - - assert_eq!(manifest.image_path.as_deref(), Some("image_path")); - assert_eq!(manifest.launch_sound_path.as_deref(), Some("launch_sound_path")); - assert_eq!(manifest.content_warning.as_deref(), Some("content_warning")); - 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/format.rs b/support/build/src/metadata/format.rs index 21b04dc7..3dab6396 100644 --- a/support/build/src/metadata/format.rs +++ b/support/build/src/metadata/format.rs @@ -1,160 +1,589 @@ -use super::Value; -use super::error::Error; +use std::ops::Deref; +use std::cmp::Eq; +use std::hash::Hash; use std::borrow::Cow; use std::collections::HashMap; + #[cfg(feature = "serde")] -use serde::Deserialize; +use serde::{Deserialize, Deserializer}; + +use super::source::*; + + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[cfg_attr(feature = "serde", + serde(bound(deserialize = "S: Deserialize<'de> + Eq + Hash")))] +pub struct CrateMetadata { + #[cfg_attr(feature = "serde", serde(rename = "playdate"))] + pub inner: Option>, +} + +/// Just ensure that `METADATA_FIELD` is not changed and something missed. +#[cfg(test)] +#[cfg_attr(test, test)] +fn eq_metadata_field() { + assert_eq!(super::METADATA_FIELD, "playdate"); +} + + +/// Package Playdate Metadata, contains: +/// - Package Manifest fields +/// - Assets tables - `assets` & `dev-assets` +/// - Configuration table - `options` +#[derive(Debug, Clone, PartialEq)] + +pub struct Metadata { + pub(super) inner: MetadataInner, +} + + +#[cfg(feature = "serde")] +impl<'de, S: Deserialize<'de> + Eq + Hash> Deserialize<'de> for Metadata { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de> { + let meta = MetadataInner::::deserialize(deserializer)?; + // here is should be some validation + Ok(Self { inner: meta }) + } +} + + +impl MetadataSource for Metadata + where S: Eq + Hash + AsRef, + Override: ManifestSourceOptExt, + Ext>: ManifestSourceOptExt, + for<'t> &'t Ext>: ManifestSourceOptExt +{ + type S = S; + type Manifest = Ext>; + type TargetManifest = Override; + + + fn manifest(&self) -> &Self::Manifest { &self.inner.manifest } + + fn bins(&self) -> &[Self::TargetManifest] { self.inner.bins.as_slice() } + fn examples(&self) -> &[Self::TargetManifest] { self.inner.examples.as_slice() } + + fn bin_targets(&self) -> impl IntoIterator { self.inner.bins.iter().map(|o| o.target.as_ref()) } + fn example_targets(&self) -> impl IntoIterator { + self.inner.examples.iter().map(|o| o.target.as_ref()) + } + + fn assets(&self) -> &AssetsRules { &self.inner.assets } + fn dev_assets(&self) -> &AssetsRules { &self.inner.dev_assets } + + + fn options(&self) -> &Options { &self.inner.options } + + fn assets_options(&self) -> Cow<'_, AssetsOptions> { + self.options() + .assets + .as_ref() + .map_or_else(Default::default, Cow::Borrowed) + } + + fn support(&self) -> &Support { &self.inner.support } +} /// Package Metadata, contains: /// - Package Manifest fields /// - Assets tables - `assets` & `dev-assets` /// - Configuration table - `options` -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[cfg_attr(feature = "serde", + serde(bound(deserialize = "S: Deserialize<'de> + Eq + Hash")))] +pub(super) struct MetadataInner { + #[cfg_attr(feature = "serde", serde(flatten))] + pub(super) manifest: Ext>, + + #[cfg_attr(feature = "serde", serde(default))] + #[cfg_attr(feature = "serde", serde(deserialize_with = "one_of::assets_rules"))] + pub(super) assets: AssetsRules, + #[cfg_attr(feature = "serde", serde(default, alias = "dev-assets"))] + #[cfg_attr(feature = "serde", serde(deserialize_with = "one_of::assets_rules"))] + pub(super) dev_assets: AssetsRules, + + #[cfg_attr(feature = "serde", serde(default))] + pub(super) options: Options, + #[cfg_attr(feature = "serde", serde(default))] + pub(super) support: Support, + + #[cfg_attr(feature = "serde", serde(default, alias = "bin", rename = "bin"))] + #[cfg_attr(feature = "serde", serde(deserialize_with = "one_of::targets_overrides"))] + pub(super) bins: Vec>, + #[cfg_attr(feature = "serde", serde(default, alias = "example", rename = "example"))] + #[cfg_attr(feature = "serde", serde(deserialize_with = "one_of::targets_overrides"))] + pub(super) examples: Vec>, +} + + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[cfg_attr(feature = "serde", serde(bound(deserialize = "Main: Deserialize<'de>")))] +pub struct Ext
{ + #[cfg_attr(feature = "serde", serde(flatten))] + pub(super) main: Main, + #[cfg_attr(feature = "serde", serde(flatten))] + pub(super) extra: ExtraFields, +} + +impl Ext { + pub fn new(main: T, extra: ExtraFields) -> Self { Self { main, extra } } +} + +impl Ext { + pub fn inner(&self) -> &T { &self.main } + pub fn extra(&self) -> &ExtraFields { &self.extra } +} + +impl Ext> where S: ToOwned { + pub fn clone_owned(&self) -> Ext::Owned>> { + Ext { main: self.main.clone_owned(), + extra: self.extra.to_owned() } + } +} + + +#[derive(Debug, Clone, PartialEq)] #[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(bound(deserialize = "S: Deserialize<'de>")))] +pub struct Manifest { + pub name: Option, + pub version: Option, + pub author: Option, #[cfg_attr(feature = "serde", serde(alias = "bundle-id"))] - pub bundle_id: String, - pub description: Option, + pub bundle_id: Option, + pub description: Option, #[cfg_attr(feature = "serde", serde(alias = "image-path"))] - pub image_path: Option, + pub image_path: Option, #[cfg_attr(feature = "serde", serde(alias = "launch-sound-path"))] - pub launch_sound_path: Option, + pub launch_sound_path: Option, #[cfg_attr(feature = "serde", serde(alias = "content-warning"))] - pub content_warning: Option, + 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, + pub content_warning2: Option, + #[cfg_attr(feature = "serde", serde(default, alias = "build-number"))] + #[cfg_attr(feature = "serde", serde(deserialize_with = "one_of::usize_or_from_str"))] + pub build_number: Option, +} - /// 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<'t, S> Cob<'t> for Manifest where S: Cob<'t> { + type Output = Manifest<>::Output>; + + fn as_borrow(&'t self) -> Self::Output { + Manifest { name: self.name.as_ref().map(Cob::as_borrow), + version: self.version.as_ref().map(Cob::as_borrow), + author: self.author.as_ref().map(Cob::as_borrow), + bundle_id: self.bundle_id.as_ref().map(Cob::as_borrow), + description: self.description.as_ref().map(Cob::as_borrow), + image_path: self.image_path.as_ref().map(Cob::as_borrow), + launch_sound_path: self.launch_sound_path.as_ref().map(Cob::as_borrow), + content_warning: self.content_warning.as_ref().map(Cob::as_borrow), + content_warning2: self.content_warning2.as_ref().map(Cob::as_borrow), + build_number: self.build_number } + } } +impl<'t, T> Cob<'t> for Ext where T: Cob<'t> { + type Output = Ext<>::Output>; -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(()) - }, - } + fn as_borrow(&'t self) -> Self::Output { + let main = self.main.as_borrow(); + Ext { main, + extra: self.extra + .iter() + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect() } } } -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<'t, T> Cob<'t> for Override where T: Cob<'t> { + type Output = Override<>::Output>; + + fn as_borrow(&'t self) -> Self::Output { + let Override { target, manifest } = self; + Override { target: target.as_borrow(), + manifest: manifest.as_borrow() } } } -impl PlayDateMetadataAssets where Error: From<>::Error> { - fn extract_options(&mut self) -> Option> { +impl IntoOwned::Owned>> for Manifest> { + fn into_owned(self) -> Manifest<::Owned> { + Manifest { name: self.name.map(|s| s.into_owned()), + version: self.version.map(|s| s.into_owned()), + author: self.author.map(|s| s.into_owned()), + bundle_id: self.bundle_id.map(|s| s.into_owned()), + description: self.description.map(|s| s.into_owned()), + image_path: self.image_path.map(|s| s.into_owned()), + launch_sound_path: self.launch_sound_path.map(|s| s.into_owned()), + content_warning: self.content_warning.map(|s| s.into_owned()), + content_warning2: self.content_warning2.map(|s| s.into_owned()), + build_number: self.build_number } + } +} + +impl Manifest where S: ToOwned { + pub fn clone_owned(&self) -> Manifest<::Owned> { + Manifest { name: self.name.as_ref().map(|s| s.to_owned()), + version: self.version.as_ref().map(|s| s.to_owned()), + author: self.author.as_ref().map(|s| s.to_owned()), + bundle_id: self.bundle_id.as_ref().map(|s| s.to_owned()), + description: self.description.as_ref().map(|s| s.to_owned()), + image_path: self.image_path.as_ref().map(|s| s.to_owned()), + launch_sound_path: self.launch_sound_path.as_ref().map(|s| s.to_owned()), + content_warning: self.content_warning.as_ref().map(|s| s.to_owned()), + content_warning2: self.content_warning2.as_ref().map(|s| s.to_owned()), + build_number: self.build_number } + } +} + + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[cfg_attr(feature = "serde", serde(bound(deserialize = "S: Deserialize<'de>")))] +pub struct Override { + /// Associated cargo-target name + #[cfg_attr(feature = "serde", serde(rename = "id", alias = "target"))] + pub(super) target: S, + #[cfg_attr(feature = "serde", serde(flatten))] + pub(super) manifest: Ext>, +} + +impl> Override { + pub fn into_parts(self) -> (S, Ext>) { + let Override { target, manifest } = self; + (target, manifest) + } + + pub fn as_parts(&self) -> (&S, &Ext>) { + let Override { target, manifest } = self; + (target, manifest) + } +} + +impl> TargetId for Override { + fn target(&self) -> &str { self.target.as_ref() } +} + + +impl<'t> IntoOwned> for Override> { + fn into_owned(self) -> Override { + Override { target: self.target.into_owned(), + manifest: self.manifest.into_owned() } + } +} + +impl Override where S: ToOwned { + pub fn clone_owned(&self) -> Override<::Owned> { + Override { target: self.target.to_owned(), + manifest: self.manifest.clone_owned() } + } +} + + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[cfg_attr(feature = "serde", serde(untagged, deny_unknown_fields))] +#[cfg_attr(feature = "serde", + serde(bound(deserialize = "S: Deserialize<'de> + Default + Eq + Hash")))] +pub enum AssetsRules { + /// List of paths to include. + List(Vec), + /// Rules & queries used to resolve paths to include. + Map(HashMap), +} + +impl Default for AssetsRules { + fn default() -> Self { Self::List(Vec::with_capacity(0)) } +} + +impl AssetsRules { + pub fn is_empty(&self) -> bool { 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, + Self::List(list) => list.is_empty(), + Self::Map(map) => map.is_empty(), } } } -#[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) + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[cfg_attr(feature = "serde", serde(untagged))] +pub enum RuleValue { + Boolean(bool), + String(String), +} + +impl Default for RuleValue { + fn default() -> Self { Self::Boolean(true) } +} + + +pub type ExtraFields = HashMap; + + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[cfg_attr(feature = "serde", serde(untagged))] +pub enum ExtraValue { + Boolean(bool), + String(String), + Int(i64), +} + +impl ExtraValue { + pub fn is_empty(&self) -> bool { + match self { + Self::String(s) => s.trim().is_empty(), + _ => false, + } } } -#[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) +impl std::fmt::Display for ExtraValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Boolean(v) => v.fmt(f), + Self::String(v) => v.trim().fmt(f), + Self::Int(v) => v.fmt(f), + } } } +impl From for ExtraValue { + fn from(value: bool) -> Self { Self::Boolean(value) } +} +impl From for ExtraValue { + fn from(value: i64) -> Self { Self::Int(value) } +} +impl From for ExtraValue { + fn from(value: isize) -> Self { Self::Int(value as _) } +} +impl From for ExtraValue { + fn from(value: u64) -> Self { Self::Int(value as _) } +} +impl From for ExtraValue { + fn from(value: usize) -> Self { Self::Int(value as _) } +} +impl From for ExtraValue { + fn from(value: String) -> Self { Self::String(value) } +} +impl From<&str> for ExtraValue { + fn from(value: &str) -> Self { Self::String(value.to_string()) } +} +impl<'t> From> for ExtraValue { + fn from(value: Cow<'t, str>) -> Self { Self::String(value.into_owned()) } +} + +impl AsRef for ExtraValue { + fn as_ref(&self) -> &ExtraValue { self } +} +impl AsMut for ExtraValue { + fn as_mut(&mut self) -> &mut ExtraValue { self } +} + + +impl ManifestSourceOpt for Manifest where S: Deref { + const MAY_BE_INCOMPLETE: bool = true; + + fn name(&self) -> Option<&str> { self.name.as_deref() } + fn version(&self) -> Option<&str> { self.version.as_deref() } + fn author(&self) -> Option<&str> { self.author.as_deref() } + fn bundle_id(&self) -> Option<&str> { self.bundle_id.as_deref() } + fn description(&self) -> Option<&str> { self.description.as_deref() } + fn image_path(&self) -> Option<&str> { self.image_path.as_deref() } + fn launch_sound_path(&self) -> Option<&str> { self.launch_sound_path.as_deref() } + fn content_warning(&self) -> Option<&str> { self.content_warning.as_deref() } + fn content_warning2(&self) -> Option<&str> { self.content_warning2.as_deref() } + fn build_number(&self) -> Option { self.build_number } +} + +impl ManifestSourceOpt for Ext { + const MAY_BE_INCOMPLETE: bool = Manifest::::MAY_BE_INCOMPLETE; + + fn name(&self) -> Option<&str> { self.inner().name() } + fn version(&self) -> Option<&str> { self.inner().version() } + fn author(&self) -> Option<&str> { self.inner().author() } + fn bundle_id(&self) -> Option<&str> { self.inner().bundle_id() } + fn description(&self) -> Option<&str> { self.inner().description() } + fn image_path(&self) -> Option<&str> { self.inner().image_path() } + fn launch_sound_path(&self) -> Option<&str> { self.inner().launch_sound_path() } + fn content_warning(&self) -> Option<&str> { self.inner().content_warning() } + fn content_warning2(&self) -> Option<&str> { self.inner().content_warning2() } + fn build_number(&self) -> Option { self.inner().build_number() } +} +impl ManifestSourceOpt for &Ext { + const MAY_BE_INCOMPLETE: bool = T::MAY_BE_INCOMPLETE; -#[derive(Debug, Clone, Default)] + fn name(&self) -> Option<&str> { (*self).name() } + fn version(&self) -> Option<&str> { (*self).version() } + fn author(&self) -> Option<&str> { (*self).author() } + fn bundle_id(&self) -> Option<&str> { (*self).bundle_id() } + fn description(&self) -> Option<&str> { (*self).description() } + fn image_path(&self) -> Option<&str> { (*self).image_path() } + fn launch_sound_path(&self) -> Option<&str> { (*self).launch_sound_path() } + fn content_warning(&self) -> Option<&str> { (*self).content_warning() } + fn content_warning2(&self) -> Option<&str> { (*self).content_warning2() } + fn build_number(&self) -> Option { (*self).build_number() } +} + + +impl ManifestSourceOptExt for Ext { + const MAY_HAVE_EXTRA: bool = true; + + fn has_extra(&self) -> bool { !self.extra.is_empty() } + fn iter_extra(&self) -> Option, impl AsRef)>> { + if self.extra.is_empty() { + None + } else { + Some(self.extra.iter()) + } + } +} + +impl ManifestSourceOptExt for Manifest where S: Deref { + const MAY_HAVE_EXTRA: bool = false; + + fn has_extra(&self) -> bool { false } + fn iter_extra(&self) -> Option, impl AsRef)>> { + None::> + } +} + +impl<'s, T: ManifestSourceOpt, S: From<&'s str>> From<&'s T> for Manifest { + fn from(source: &'s T) -> Self { + Self { name: source.name().map(Into::into), + version: source.version().map(Into::into), + author: source.author().map(Into::into), + bundle_id: source.bundle_id().map(Into::into), + description: source.description().map(Into::into), + image_path: source.image_path().map(Into::into), + launch_sound_path: source.launch_sound_path().map(Into::into), + content_warning: source.content_warning().map(Into::into), + content_warning2: source.content_warning2().map(Into::into), + build_number: source.build_number() } + } +} + + +impl From<&T> for Ext> { + fn from(source: &T) -> Self { + let main = Manifest::from(source); + Ext { main, + extra: source.iter_extra() + .map(|i| { + i.into_iter() + .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().clone())) + .collect() + }) + .unwrap_or_default() } + } +} + +impl<'t, T: ManifestSourceOptExt> From<&'t T> for Ext>> { + fn from(source: &'t T) -> Self { + Ext { main: Manifest::from(source), + extra: source.iter_extra() + .map(|i| { + i.into_iter() + .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().clone())) + .collect() + }) + .unwrap_or_default() } + } +} + +impl<'t, T: ManifestSourceOptExt + 't> IntoOwned>> for T { + fn into_owned(self) -> Ext> { + Ext { main: Manifest::from(&self).into_owned(), + extra: self.iter_extra() + .map(|i| { + i.into_iter() + .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().clone())) + .collect() + }) + .unwrap_or_default() } + } +} + + +impl ManifestSourceOpt for Override where Manifest: ManifestSourceOpt { + const MAY_BE_INCOMPLETE: bool = Manifest::::MAY_BE_INCOMPLETE; + + fn name(&self) -> Option<&str> { self.manifest.name() } + fn version(&self) -> Option<&str> { self.manifest.version() } + fn author(&self) -> Option<&str> { self.manifest.author() } + fn bundle_id(&self) -> Option<&str> { self.manifest.bundle_id() } + fn description(&self) -> Option<&str> { self.manifest.description() } + fn image_path(&self) -> Option<&str> { self.manifest.image_path() } + fn launch_sound_path(&self) -> Option<&str> { self.manifest.launch_sound_path() } + fn content_warning(&self) -> Option<&str> { self.manifest.content_warning() } + fn content_warning2(&self) -> Option<&str> { self.manifest.content_warning2() } + fn build_number(&self) -> Option { self.manifest.build_number() } +} + +impl ManifestSourceOptExt for Override where Manifest: ManifestSourceOpt { + const MAY_HAVE_EXTRA: bool = Ext::>::MAY_HAVE_EXTRA; + + fn iter_extra(&self) -> Option, impl AsRef)>> { + self.manifest.iter_extra() + } +} + + +#[derive(Debug, Clone, Default, PartialEq)] #[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, + // Output layout ctrl, temporary removed. } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, PartialEq)] #[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::"))] + #[cfg_attr(feature = "serde", serde(alias = "override"))] + #[cfg_attr(feature = "serde", serde(default = "AssetsOptions::default_overwrite"))] pub overwrite: bool, - #[cfg_attr(feature = "serde", serde(alias = "follow-symlinks", default = "bool::"))] + #[cfg_attr(feature = "serde", serde(alias = "follow-symlinks"))] + #[cfg_attr(feature = "serde", serde(default = "AssetsOptions::default_follow_symlinks"))] 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::"))] + #[cfg_attr(feature = "serde", serde(default = "AssetsOptions::default_dependencies"))] pub dependencies: bool, } -#[cfg(feature = "serde")] -const fn bool() -> bool { V } +impl AssetsOptions { + const fn default_overwrite() -> bool { true } + const fn default_follow_symlinks() -> bool { true } + const fn default_dependencies() -> bool { false } +} + +impl Default for AssetsOptions { + fn default() -> Self { + Self { overwrite: Self::default_overwrite(), + follow_symlinks: Self::default_follow_symlinks(), + dependencies: Self::default_dependencies(), + method: Default::default() } + } +} -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))] pub enum AssetsBuildMethod { @@ -167,37 +596,860 @@ impl Default for AssetsBuildMethod { } -#[derive(Debug, Clone)] +/// Compatibility options. +/// e.g. Crank manifest path. +#[derive(Debug, Clone, Default, PartialEq)] #[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), +pub struct Support { + // #[serde(alias = "crank-manifest")] + // pub crank_manifest: Option } -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(), +/// Because serde's error for untagged enum with various inner types is not pretty helpful, +/// like "data did not match any variant of untagged enum AssetsRules", +/// we need some custom implementations with more detailed error messages. +#[cfg(feature = "serde")] +mod one_of { + use std::marker::PhantomData; + + use std::fmt; + use serde::de; + use serde::de::MapAccess; + use serde::de::SeqAccess; + use serde::de::Visitor; + + use super::*; + + + pub fn usize_or_from_str<'de, D>(deserializer: D) -> Result, D::Error> + where D: Deserializer<'de> { + struct OneOf; + + impl<'de> Visitor<'de> for OneOf { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("unsigned integer or string with it") + } + + fn visit_u8(self, v: u8) -> Result { Ok(Some(v as _)) } + fn visit_u16(self, v: u16) -> Result { Ok(Some(v as _)) } + fn visit_u32(self, v: u32) -> Result { Ok(Some(v as _)) } + + fn visit_u64(self, v: u64) -> Result { + Ok(Some(v.try_into().map_err(de::Error::custom)?)) + } + + fn visit_u128(self, v: u128) -> Result { + Ok(Some(v.try_into().map_err(de::Error::custom)?)) + } + + fn visit_i64(self, v: i64) -> Result { + if v.is_negative() { + Err(de::Error::invalid_type(de::Unexpected::Signed(v), &self)) + } else { + Ok(Some(v.try_into().map_err(de::Error::custom)?)) + } + } + + fn visit_str(self, s: &str) -> Result { + let v = s.parse().map_err(serde::de::Error::custom)?; + Ok(Some(v)) + } } + + deserializer.deserialize_any(OneOf) + } + + + pub fn assets_rules<'de, S, D>(deserializer: D) -> Result, D::Error> + where D: Deserializer<'de>, + S: Deserialize<'de> + Eq + Hash { + struct OneOf(PhantomData); + + impl<'de, S> Visitor<'de> for OneOf where S: Deserialize<'de> + Eq + Hash { + type Value = super::AssetsRules; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("list of includes or map of rules") + } + + fn visit_seq>(self, seq: A) -> Result { + let deserializer = de::value::SeqAccessDeserializer::new(seq); + let res: Vec = Deserialize::deserialize(deserializer)?; + Ok(super::AssetsRules::List(res)) + } + + fn visit_map>(self, map: M) -> Result { + let deserializer = de::value::MapAccessDeserializer::new(map); + let res: HashMap = Deserialize::deserialize(deserializer)?; + Ok(super::AssetsRules::Map(res)) + } + } + + deserializer.deserialize_any(OneOf::(PhantomData)) + } + + + pub fn targets_overrides<'de, S, D>(deserializer: D) -> Result>, D::Error> + where D: Deserializer<'de>, + S: Deserialize<'de> + Eq + Hash { + struct OneOf(PhantomData); + + impl<'de, S> Visitor<'de> for OneOf where S: Deserialize<'de> + Eq + Hash { + type Value = Vec>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("list of includes or map of rules") + } + + fn visit_seq>(self, seq: A) -> Result { + let deserializer = de::value::SeqAccessDeserializer::new(seq); + Deserialize::deserialize(deserializer) + } + + fn visit_map>(self, map: M) -> Result { + use super::{Ext, Manifest}; + + let deserializer = de::value::MapAccessDeserializer::new(map); + let res: HashMap>> = Deserialize::deserialize(deserializer)?; + Ok(res.into_iter() + .map(|(k, v)| { + Override:: { target: k, + manifest: v } + }) + .collect()) + } + } + + deserializer.deserialize_any(OneOf::(PhantomData)) } } -/// 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 +#[cfg(test)] +#[cfg(feature = "toml")] +mod tests { + use super::*; + use crate::manifest::format::ManifestFmt; + + use std::assert_matches::assert_matches; + + + type ManifestWithAny = Ext>; + type ManifestStrict = Manifest; + type ManifestStrictRef<'t> = Manifest>; + + + #[test] + fn minimal_strict() { + let src = r#" + bundle-id = "test.workspace.main.crate" + description = "test" + "#; + let m: ManifestStrict = toml::from_str(src).unwrap(); + assert!(m.bundle_id.is_some()); + let m: ManifestStrictRef = toml::from_str(src).unwrap(); + assert!(m.bundle_id.is_some()); + } + + #[test] + fn minimal_strict_err() { + let src = r#" + bundle-id = "test.workspace.main.crate" + foo = "bar" + "#; + assert!(toml::from_str::(src).is_err()); + + let src = r#"foo = "bar""#; + assert!(toml::from_str::(src).is_err()); + assert!(toml::from_str::(src).is_err()); + } + + #[test] + fn minimal_extra() { + let src = r#"bundle-id = "test.workspace.main.crate""#; + assert!(toml::from_str::(src).is_ok()); + + + let src = r#" + bundle-id = "test.workspace.main.crate" + description = "test" + foo = "bar" + "#; + + let m: ManifestWithAny = toml::from_str(src).unwrap(); + + assert!(m.inner().bundle_id.is_some()); + assert!(m.inner().description.is_some()); + assert!(m.extra().get("foo").is_some()); + } + + #[test] + fn meta_minimal() { + assert!(toml::from_str::("").is_ok()); + + let src = r#" + bundle-id = "test.workspace.main.crate" + description = "test" + "#; + + let m = toml::from_str::(src).unwrap(); + assert!(m.inner.manifest.inner().bundle_id.is_some()); + assert!(m.inner.manifest.inner().description.is_some()); + assert!(m.inner.manifest.extra.is_empty()); + } + + + #[test] + fn meta_extra() { + let src = r#" + bundle-id = "test.workspace.main.crate" + description = "test" + boo = 42 + [assets] + foo = "bar" + "#; + let expected_id = Some("test.workspace.main.crate"); + + let m: super::MetadataInner = toml::from_str(src).unwrap(); + assert_eq!(expected_id, m.manifest.inner().bundle_id.as_deref()); + assert!(m.manifest.inner().description.is_some()); + assert!(m.manifest.extra().get("boo").is_some()); + + let m: Metadata = toml::from_str(src).unwrap(); + assert_eq!(expected_id, m.inner.manifest.inner().bundle_id.as_deref()); + assert!(m.inner.manifest.inner().description.is_some()); + assert!(m.inner.manifest.extra().get("boo").is_some()); + + let src = r#" + bundle-id = "test.workspace.main.crate" + description = "test" + foo = "bar" + assets.target = "source" + "#; + let m: Metadata = toml::from_str(src).unwrap(); + assert_eq!(expected_id, m.inner.manifest.inner().bundle_id.as_deref()); + assert!(m.inner.manifest.inner().description.is_some()); + assert!(m.inner.manifest.extra().get("foo").is_some()); + assert!(!m.inner.assets.is_empty()); + } + + + #[test] + fn meta_strict_bins() { + let src = r#" + bundle-id = "test.workspace.main.crate" + description = "test" + [[bin]] + target = "cargo-target-name" + name = "Other Name" + [[bin]] + target = "cargo-another-target" + name = "Another Name" + "#; + + let m = toml::from_str::(src).unwrap(); + assert!(m.inner.manifest.inner().bundle_id.is_some()); + assert!(m.inner.manifest.inner().description.is_some()); + assert_eq!(2, m.inner.bins.len()); + } + + #[test] + fn meta_extra_bins() { + let src = r#" + bundle-id = "test.workspace.main.crate" + foo = "bar" + + [[bin]] + target = "cargo-target-name" + name = "Other Name" + boo = "bar" + "#; + + let m = toml::from_str::(src).unwrap(); + assert!(m.inner.manifest.inner().bundle_id.is_some()); + assert!(m.inner.manifest.extra().get("foo").is_some()); + assert_eq!(1, m.inner.bins.len()); + assert!( + m.inner + .bins + .first() + .unwrap() + .manifest + .extra() + .get("boo") + .is_some() + ); + } + + #[test] + fn meta_strict_examples() { + let src = r#" + bundle-id = "test.workspace.main.crate" + description = "test" + [[example]] + target = "cargo-target-name" + name = "Other Name" + [[example]] + target = "cargo-another-target" + name = "Another Name" + "#; + + let m = toml::from_str::(src).unwrap(); + assert!(m.inner.manifest.inner().bundle_id.is_some()); + assert!(m.inner.manifest.inner().description.is_some()); + assert_eq!(2, m.inner.examples.len()); + } + + #[test] + fn meta_strict_examples_map() { + let src = r#" + bundle-id = "test.workspace.main.crate" + description = "test" + [example.cargo-target-name] + name = "Other Name" + [example.cargo-another-target] + name = "Another Name" + "#; + + let m = toml::from_str::(src).unwrap(); + assert!(m.inner.manifest.inner().bundle_id.is_some()); + assert!(m.inner.manifest.inner().description.is_some()); + assert_eq!(2, m.inner.examples.len()); + } + + #[test] + fn meta_strict_examples_mix_err() { + let src = r#" + bundle-id = "test.workspace.main.crate" + description = "test" + [example.cargo-target-name] + name = "Other Name" + [[example]] + target = "cargo-another-target" + name = "Another Name" + "#; + + assert!(toml::from_str::(src).is_err()); + } + + #[test] + fn meta_extra_examples_mix_err() { + let src = r#" + bundle-id = "test.workspace.main.crate" + foo = "bar" + [example.cargo-target-name] + name = "Other Name" + [[example]] + target = "cargo-another-target" + name = "Another Name" + "#; + + assert!(toml::from_str::(src).is_err()); + } + + + #[test] + fn assets_num_err() { + let src = r#" + [playdate] + bundle-id = "test.workspace.main.crate" + [playdate.assets] + foo = "bar" # ok + num = 42 # err + "#; + assert!(toml::from_str::(src).is_err()); + } + + + #[test] + fn options_empty() { + let m = toml::from_str::("").unwrap(); + assert!(m.assets.is_none()); + } + + #[test] + fn options_assets_deps() { + // default is false + assert!(!AssetsOptions::default_dependencies()); + let src = r#" [assets] "#; + let m = toml::from_str::(src).unwrap(); + assert_matches!( + m.assets, + Some(AssetsOptions { dependencies: false, + .. }) + ); + + // overrides default + let src = r#" + [assets] + dependencies = true + "#; + let m = toml::from_str::(src).unwrap(); + assert_matches!( + m.assets, + Some(AssetsOptions { dependencies: true, + .. }) + ); + } + + #[test] + fn assets_rules_empty() { + let m = toml::from_str::("").unwrap(); + assert!(m.is_empty()); + match m { + AssetsRules::List(rules) => assert!(rules.is_empty()), + AssetsRules::Map(rules) => assert!(rules.is_empty()), + } + } + + #[test] + fn assets_rules_list_wrapped() { + #[derive(Debug, Clone, PartialEq, Deserialize)] + pub(super) struct Temp { + assets: AssetsRules, + } + + let src = r#" + assets = ["one", "two"] + "#; + let m = toml::from_str::(src).unwrap(); + assert!(!m.assets.is_empty()); + assert_matches!(m.assets, AssetsRules::List(rules) if rules.len() == 2); + } + + #[test] + fn assets_rules_map() { + let src = r#" + included = true + excluded = false + "into/" = "files.*" + "#; + let m = toml::from_str::(src).unwrap(); + assert_matches!(m, AssetsRules::Map(rules) if rules.len() == 3); + } + + + #[test] + fn assets_rules_map_wrapped() { + #[derive(Debug, Clone, PartialEq, Deserialize)] + pub(super) struct Temp { + assets: AssetsRules, + } + let src = r#" + [assets] + included = true + excluded = false + "into/" = "files.*" + "#; + let m = toml::from_str::(src).unwrap(); + assert_matches!(m.assets, AssetsRules::Map(rules) if rules.len() == 3); + } + + + #[test] + fn options_assets_err() { + let src = r#" + [playdate] + bundle-id = "test.workspace.main.crate" + [playdate.options.assets] + foo = "bar" # err + "#; + let result = toml::from_str::(src); + assert!(result.is_err(), "must be err, but {result:?}"); + assert!(result.as_ref() + .unwrap_err() + .to_string() + .contains("unknown field `foo`")); + } + + #[test] + fn assets_options_err() { + let src = r#" + [playdate] + bundle-id = "test.workspace.main.crate" + [playdate.assets] + foo = "bar" + options = { dependencies = true } + "#; + let result = toml::from_str::(src); + assert!(result.is_err(), "must be err, but {result:?}"); + + let src = r#" + [playdate] + bundle-id = "test.workspace.main.crate" + [playdate.assets.options] + dependencies = true + "#; + let result = toml::from_str::(src); + assert!(result.is_err(), "must be err, but {result:?}"); + } + + #[test] + fn meta_assets_options() { + let src = r#" + bundle-id = "test.workspace.main.crate" + [options.assets] + [assets] + "#; + assert!(toml::from_str::(src).is_ok()); + + let src = r#" + bundle-id = "test.workspace.main.crate" + [options.assets] + dependencies = true + "#; + let m = toml::from_str::(src).unwrap(); + assert!(m.assets.is_empty()); + assert_matches!( + m.options.assets, + Some(AssetsOptions { dependencies: true, + .. }) + ); + } + + #[test] + fn meta_assets_options_legacy() { + let src = r#" + bundle-id = "test.workspace.main.crate" + [assets] + options = {} + "#; + assert!(toml::from_str::(src).is_err()); + + let src = r#" + bundle-id = "test.workspace.main.crate" + [assets] + options = { dependencies = true } + "#; + assert!(toml::from_str::(src).is_err()); + + let src = r#" + bundle-id = "test.workspace.main.crate" + [assets] + foo = "bar" + boo = true + options = { } + "#; + assert!(toml::from_str::(src).is_err()); + + + let src = r#" + [playdate] + bundle-id = "test.workspace.main.crate" + [playdate.assets] + [playdate.assets.options] # err + "#; + assert!(toml::from_str::(src).is_err()); + } + + #[test] + fn meta_options_assets() { + let src = r#" + bundle-id = "test.workspace.main.crate" + [options] + assets = {} + "#; + + assert!(toml::from_str::(src).is_ok()); + } + + #[test] + fn meta_assets_options_mix() { + let src = r#" + bundle-id = "test.workspace.main.crate" + [options] + assets = {} + [assets] + options = {} + "#; + + assert!(toml::from_str::(src).is_err()); + } + + + #[test] + fn meta_assets_maps() { + let src = r#" + [assets] + included = true + excluded = false + other = "from/path" + [dev-assets] + a = true + b = false + c = "/c/path" + "#; + + let m = toml::from_str::(src).unwrap(); + + assert_matches!(m.assets(), AssetsRules::Map(_)); + match m.assets() { + AssetsRules::Map(rules) => { + assert_eq!(3, rules.len()); + assert_eq!(Some(&RuleValue::Boolean(true)), rules.get("included")); + assert_eq!(Some(&RuleValue::Boolean(false)), rules.get("excluded")); + assert_eq!(Some(&RuleValue::String("from/path".into())), rules.get("other")); + }, + _ => unreachable!(), + } + + assert_matches!(m.dev_assets(), AssetsRules::Map(_)); + match m.dev_assets() { + AssetsRules::Map(rules) => { + assert_eq!(3, rules.len()); + assert_eq!(Some(&RuleValue::Boolean(true)), rules.get("a")); + assert_eq!(Some(&RuleValue::Boolean(false)), rules.get("b")); + assert_eq!(Some(&RuleValue::String("/c/path".into())), rules.get("c")); + }, + _ => unreachable!(), + } + } + + #[test] + fn meta_assets_lists() { + let src = r#" + assets = ["a", "b", "c"] + dev-assets = ["d", "e", "f"] + "#; + + let m = toml::from_str::(src).unwrap(); + + assert_matches!(m.assets(), AssetsRules::List(_)); + assert_matches!(m.dev_assets(), AssetsRules::List(_)); + match m.assets() { + AssetsRules::List(rules) => assert_eq!(&["a", "b", "c"], &rules[..]), + _ => unreachable!(), + } + match m.dev_assets() { + AssetsRules::List(rules) => assert_eq!(&["d", "e", "f"], &rules[..]), + _ => unreachable!(), + } + } + + #[test] + fn meta_assets_mix() { + let src = r#" + assets = ["d", "e", "f"] + [dev-assets] + a = true + b = true + "#; + + let m = toml::from_str::(src).unwrap(); + + assert_matches!(m.assets(), AssetsRules::List(_)); + match m.assets() { + AssetsRules::List(rules) => { + assert_eq!(3, rules.len()); + assert_eq!(&["d", "e", "f"], &rules[..]); + }, + _ => unreachable!(), + } + + assert_matches!(m.dev_assets(), AssetsRules::Map(_)); + match m.dev_assets() { + AssetsRules::Map(rules) => { + assert_eq!(2, rules.len()); + assert_eq!(Some(&RuleValue::Boolean(true)), rules.get("a")); + assert_eq!(Some(&RuleValue::Boolean(true)), rules.get("b")); + }, + _ => unreachable!(), + } + } + + + #[test] + fn meta_full() { + let src = r#" + foo = "bar" # custom field + name = "Crate Name" + version = "0.1" + bundle-id = "test.workspace.main.crate" + description = "Crate description" + author = "Crate Author" + image-path = "image/path" + launch-sound-path = "launch-sound/path" + content-warning = "Attention!" + content-warning2 = "Alarm!" + build-number = 42 + options.assets.dependencies = true + [assets] + included = true + excluded = false + other = "from/path" + [dev-assets] + "dev-included" = true + [[bin]] + target = "cargo-target-bin-name" + name = "Bin Name" + bundle-id = "test.workspace.main.bin" + description = "This is a bin" + [[example]] + target = "cargo-target-example-name" + name = "Example Name" + bundle-id = "test.workspace.main.example" + description = "This is an example" + example-extra = 101 + "#; + + let m = toml::from_str::(src).unwrap(); + assert_eq!(Some("Crate Name"), m.manifest().name()); + assert_eq!(Some("0.1"), m.manifest().version()); + assert_eq!(Some("test.workspace.main.crate"), m.manifest().bundle_id()); + assert_eq!(Some("Crate description"), m.manifest().description()); + assert_eq!(Some("Crate Author"), m.manifest().author()); + assert_eq!(Some("image/path"), m.manifest().image_path()); + assert_eq!(Some("launch-sound/path"), m.manifest().launch_sound_path()); + assert_eq!(Some("Attention!"), m.manifest().content_warning()); + assert_eq!(Some("Alarm!"), m.manifest().content_warning2()); + + { + let s = m.manifest().to_manifest_string().unwrap(); + println!("meta manifest:\n{}", s.trim()) + } + + + let opts = m.assets_options(); + assert!(opts.dependencies); + assert!(!AssetsOptions::default_dependencies()); + + assert_matches!(m.assets(), AssetsRules::Map(_)); + match m.assets() { + AssetsRules::Map(rules) => { + assert_eq!(3, rules.len()); + assert_eq!(Some(&RuleValue::Boolean(true)), rules.get("included")); + assert_eq!(Some(&RuleValue::Boolean(false)), rules.get("excluded")); + assert_eq!(Some(&RuleValue::String("from/path".into())), rules.get("other")); + }, + _ => unreachable!(), + } + assert_matches!(m.dev_assets(), AssetsRules::Map(rules) if rules.get("dev-included").is_some()); + + assert_eq!(1, m.bins().len()); + assert_eq!(1, m.examples().len()); + + let bin_trg = m.bin_targets().into_iter().next().unwrap(); + assert_eq!("cargo-target-bin-name", bin_trg); + + let example_trg = m.example_targets().into_iter().next().unwrap(); + assert_eq!("cargo-target-example-name", example_trg); + + let (bin_trg_by_iter, bin) = m.bins_iter().and_then(|mut i| i.next()).unwrap().as_parts(); + assert_eq!(bin_trg, bin_trg_by_iter); + + let (example_trg_by_iter, example) = m.examples_iter().and_then(|mut i| i.next()).unwrap().as_parts(); + assert_eq!(example_trg, example_trg_by_iter); + + + assert_eq!(Some("Bin Name"), bin.name()); + assert_eq!(Some("test.workspace.main.bin"), bin.bundle_id()); + assert_eq!(Some("This is a bin"), bin.description()); + assert!(bin.version().is_none()); + assert!(bin.author().is_none()); + assert!(bin.image_path().is_none()); + assert!(bin.launch_sound_path().is_none()); + assert!(bin.content_warning().is_none()); + assert!(bin.content_warning2().is_none()); + assert!(!bin.has_extra()); + + { + let s = bin.to_manifest_string().unwrap(); + println!("bin over:\n{}", s.trim()) + } + + + assert_eq!(Some("Example Name"), example.name()); + assert_eq!(Some("test.workspace.main.example"), example.bundle_id()); + assert_eq!(Some("This is an example"), example.description()); + assert!(example.version().is_none()); + assert!(example.author().is_none()); + assert!(example.image_path().is_none()); + assert!(example.launch_sound_path().is_none()); + assert!(example.content_warning().is_none()); + assert!(example.content_warning2().is_none()); + assert!(example.has_extra()); + let example_extra: HashMap<_, _> = example.iter_extra() + .unwrap() + .into_iter() + .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().to_owned())) + .collect(); + assert_eq!(1, example_extra.len()); + assert_eq!(Some(&ExtraValue::Int(101)), example_extra.get("example-extra")); + + + { + let s = example.to_manifest_string().unwrap(); + println!("example over:\n{}", s.trim()) + } + + + // test merged + + let bin = m.manifest_for_target(bin_trg, false).unwrap(); + assert_eq!(Some("Bin Name"), bin.name()); + assert_eq!(Some("0.1"), bin.version()); + assert_eq!(Some("test.workspace.main.bin"), bin.bundle_id()); + assert_eq!(Some("This is a bin"), bin.description()); + assert_eq!(Some("Crate Author"), bin.author()); + assert_eq!(Some("image/path"), bin.image_path()); + assert_eq!(Some("launch-sound/path"), bin.launch_sound_path()); + assert_eq!(Some("Attention!"), bin.content_warning()); + assert_eq!(Some("Alarm!"), bin.content_warning2()); + { + let s = bin.to_manifest_string().unwrap(); + println!("bin manifest:\n{}", s.trim()) + } + + let example = m.manifest_for_target(example_trg, true).unwrap(); + assert_eq!(Some("Example Name"), example.name()); + assert_eq!(Some("0.1"), example.version()); + assert_eq!(Some("test.workspace.main.example"), example.bundle_id()); + assert_eq!(Some("This is an example"), example.description()); + assert_eq!(Some("Crate Author"), example.author()); + assert_eq!(Some("image/path"), example.image_path()); + assert_eq!(Some("launch-sound/path"), example.launch_sound_path()); + assert_eq!(Some("Attention!"), example.content_warning()); + assert_eq!(Some("Alarm!"), example.content_warning2()); + { + let s = example.to_manifest_string().unwrap(); + println!("example manifest:\n{}", s.trim()) + } + + + // test merged any kind of target, just named + + let example = m.manifest_for_target_any(example_trg).unwrap(); + assert_eq!(Some("Example Name"), example.name()); + assert_eq!(Some("0.1"), example.version()); + assert_eq!(Some("test.workspace.main.example"), example.bundle_id()); + assert_eq!(Some("This is an example"), example.description()); + assert_eq!(Some("Crate Author"), example.author()); + assert_eq!(Some("image/path"), example.image_path()); + assert_eq!(Some("launch-sound/path"), example.launch_sound_path()); + assert_eq!(Some("Attention!"), example.content_warning()); + assert_eq!(Some("Alarm!"), example.content_warning2()); + { + let s = example.to_manifest_string().unwrap(); + println!("example manifest:\n{}", s.trim()) + } + + let missing = m.manifest_for_target_any("missing, wrong name").unwrap(); + assert_eq!(Some("Crate Name"), missing.name()); + assert_eq!(Some("0.1"), missing.version()); + assert_eq!(Some("test.workspace.main.crate"), missing.bundle_id()); + assert_eq!(Some("Crate description"), missing.description()); + assert_eq!(Some("Crate Author"), missing.author()); + assert_eq!(Some("image/path"), missing.image_path()); + assert_eq!(Some("launch-sound/path"), missing.launch_sound_path()); + assert_eq!(Some("Attention!"), missing.content_warning()); + assert_eq!(Some("Alarm!"), missing.content_warning2()); + { + let s = missing.to_manifest_string().unwrap(); + println!("missing (base meta) manifest:\n{}", s.trim()) + } + assert_eq!(m.manifest().into_owned(), missing.into_owned()); + } } diff --git a/support/build/src/metadata/mod.rs b/support/build/src/metadata/mod.rs index 5000b379..bd519a3c 100644 --- a/support/build/src/metadata/mod.rs +++ b/support/build/src/metadata/mod.rs @@ -1,8 +1,7 @@ pub mod error; pub mod format; -pub mod cargo; - -use crate::value::Value; +pub mod source; +pub mod validation; pub const METADATA_FIELD: &str = "playdate"; diff --git a/support/build/src/metadata/source.rs b/support/build/src/metadata/source.rs new file mode 100644 index 00000000..63c93d09 --- /dev/null +++ b/support/build/src/metadata/source.rs @@ -0,0 +1,637 @@ +use std::hash::Hash; +use std::borrow::Cow; +use std::path::Path; + +use super::format::{AssetsOptions, AssetsRules, Ext, ExtraFields, ExtraValue, Manifest, Options, Support}; + + +pub trait CrateInfoSource { + type Authors: ?Sized + std::slice::Join<&'static str, Output = String>; + + /// Crate name. + fn name(&self) -> Cow; + /// Crate authors. + fn authors(&self) -> &Self::Authors; + // fn authors(&self) -> &[&str]; + /// Crate version (semver). + fn version(&self) -> Cow; + /// Crate description. + fn description(&self) -> Option>; + /// Crate metadata - `playdate` table. + fn metadata(&self) -> Option; + + /// Names of `bin` cargo-targets. + fn bins(&self) -> &[&str]; + /// Names of `example` cargo-targets. + fn examples(&self) -> &[&str]; + + /// Crate manifest path (Cargo.toml). + fn manifest_path(&self) -> Cow; + + + fn manifest_for_crate(&self) -> impl ManifestSourceOptExt { + use super::format::Manifest; + use std::slice::Join; + + let author = { + let author = Join::join(self.authors(), ", "); + if author.trim().is_empty() { + None + } else { + Some(author.into()) + } + }; + let version = Some(self.version()); + let package = Manifest { name: Some(self.name()), + description: self.description(), + author, + version, + bundle_id: None, + image_path: None, + launch_sound_path: None, + content_warning: None, + content_warning2: None, + build_number: None }; + + if let Some(meta) = self.metadata() { + let manifest = meta.manifest(); + let base = Ext { main: package, + extra: Default::default() }; + // TODO: Reduce coping, return associated type instead with all strings in the Cow<'self>. + // Also get merged manifest with refs, using `override_with_extra_ref` + let result = base.override_with_extra(manifest); + Ext { main: Manifest::from(&result), + extra: result.iter_extra() + .map(|m| { + m.into_iter() + .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().clone())) + .collect() + }) + .unwrap_or_default() } + } else { + Ext { main: package.into_owned(), + extra: Default::default() } + } + } + + + /// Returns `None` if manifest for `target` not found, no fallback. + fn manifest_for(&self, target: &str, dev: bool) -> Option>> { + let base = self.manifest_for_crate(); + + if let Some(root) = self.metadata() { + if dev { + if let Some(man) = root.example(target) { + Some(base.override_with_extra(man).into_owned()) + } else { + log::debug!("target not found: {}", target); + None + } + } else if let Some(man) = root.bin(target) { + Some(base.override_with_extra(man).into_owned()) + } else { + log::debug!("target not found: {}", target); + None + } + } else { + Some(base.into_owned()) + } + } + + /// Returns manifest for specified `target`. If not found, returns manifest for crate. + fn manifest_for_opt(&self, target: Option<&str>, dev: bool) -> Ext> { + target.and_then(|target| self.manifest_for(target, dev)) + .unwrap_or_else(|| self.manifest_for_crate().into_owned()) + } +} + + +pub trait MetadataSource { + type S: Eq + Hash; + type Manifest: ManifestSourceOptExt; + type TargetManifest: ManifestSourceOptExt + TargetId; + + /// Main manifest, default and base for all cargo-targets. + fn manifest(&self) -> &Self::Manifest; + + /// All manifests for "bin" cargo-targets. + /// Overrides main manifest field-by-field. + fn bins(&self) -> &[Self::TargetManifest]; + /// All manifests for "example" cargo-targets. + /// Overrides main manifest field-by-field. + fn examples(&self) -> &[Self::TargetManifest]; + + /// Manifest for specified "bin" cargo-target. + /// Overrides main manifest field-by-field. + fn bin<'t>(&'t self, target: &'_ str) -> Option<&'t Self::TargetManifest> { + self.bins().iter().find(|b| b.target() == target) + } + /// Manifest for specified "example" cargo-target. + /// Overrides main manifest field-by-field. + fn example<'t>(&'t self, target: &'_ str) -> Option<&'t Self::TargetManifest> { + self.examples().iter().find(|b| b.target() == target) + } + + fn bin_targets(&self) -> impl IntoIterator; + fn example_targets(&self) -> impl IntoIterator; + fn all_targets(&self) -> impl IntoIterator { + self.bin_targets().into_iter().chain(self.example_targets()) + } + + fn bins_iter(&self) -> Option> { + (!self.bins().is_empty()).then_some(self.bins().iter()) + } + fn examples_iter(&self) -> Option> { + (!self.examples().is_empty()).then_some(self.examples().iter()) + } + + fn all_targets_iter(&self) -> impl Iterator { + self.bins_iter() + .into_iter() + .flatten() + .chain(self.examples_iter().into_iter().flatten()) + } + + fn assets(&self) -> &AssetsRules; + fn dev_assets(&self) -> &AssetsRules; + + fn options(&self) -> &Options; + fn assets_options(&self) -> Cow<'_, AssetsOptions>; + + fn support(&self) -> &Support; + + /// Make a manifest for a specific target, merged with base manifest for package. + /// Returns `None` if the target is not found. + fn manifest_for_target(&self, target: &str, dev: bool) -> Option { + // manifest() returns T without lifetime, so can't be associated with `&self`, + // that should be fixed + let base = self.manifest(); + + if dev { + if let Some(target) = self.example(target) { + let trg = base.override_with_extra_ref(target); + Some(trg.into_owned()) + } else { + None + } + } else if let Some(target) = self.bin(target) { + let trg = base.override_with_extra_ref(target); + Some(trg.into_owned()) + } else { + None + } + } + + fn manifest_for_target_any(&self, target: &str) -> Option { + self.manifest_for_target(target, false) + .or_else(|| self.manifest_for_target(target, true)) + .map(|m| m.into_manifest()) + .or_else(|| Some(Ext::>::from(self.manifest()))) + } +} + + +impl MetadataSource for &T { + type S = ::S; + type Manifest = ::Manifest; + type TargetManifest = ::TargetManifest; + + + fn manifest(&self) -> &Self::Manifest { (*self).manifest() } + + fn bins(&self) -> &[Self::TargetManifest] { ::bins(*self) } + fn examples(&self) -> &[Self::TargetManifest] { ::examples(*self) } + + fn bin_targets(&self) -> impl IntoIterator { (*self).bin_targets() } + fn example_targets(&self) -> impl IntoIterator { (*self).example_targets() } + + fn assets(&self) -> &AssetsRules { (*self).assets() } + fn dev_assets(&self) -> &AssetsRules { (*self).dev_assets() } + fn options(&self) -> &Options { (*self).options() } + fn assets_options(&self) -> Cow<'_, AssetsOptions> { (*self).assets_options() } + fn support(&self) -> &Support { (*self).support() } +} + + +pub trait ManifestSource { + fn name(&self) -> &str; + fn version(&self) -> &str; + fn author(&self) -> &str; + fn bundle_id(&self) -> &str; + fn description(&self) -> &str; + fn image_path(&self) -> &str; + fn launch_sound_path(&self) -> &str; + fn content_warning(&self) -> &str; + fn content_warning2(&self) -> &str; + fn build_number(&self) -> Option; +} + + +pub trait ManifestSourceOpt { + /// Possibly incomplete, that means that some of values could be `None`. + const MAY_BE_INCOMPLETE: bool; + + fn name(&self) -> Option<&str>; + fn version(&self) -> Option<&str>; + fn author(&self) -> Option<&str>; + fn bundle_id(&self) -> Option<&str>; + fn description(&self) -> Option<&str>; + fn image_path(&self) -> Option<&str>; + fn launch_sound_path(&self) -> Option<&str>; + fn content_warning(&self) -> Option<&str>; + fn content_warning2(&self) -> Option<&str>; + fn build_number(&self) -> Option; + + fn override_with<'a, Over>(&'a self, over: &'a Over) -> impl ManifestSourceOpt + 'a + where Over: ManifestSourceOpt { + use super::format::Manifest; + + Manifest::> { name: over.name().or(self.name()).map(Into::into), + version: over.version().or(self.version()).map(Into::into), + author: over.author().or(self.author()).map(Into::into), + bundle_id: over.bundle_id().or(self.bundle_id()).map(Into::into), + description: over.description().or(self.description()).map(Into::into), + image_path: over.image_path().or(self.image_path()).map(Into::into), + launch_sound_path: over.launch_sound_path() + .or(self.launch_sound_path()) + .map(Into::into), + content_warning: over.content_warning().or(self.content_warning()).map(Into::into), + content_warning2: over.content_warning2() + .or(self.content_warning2()) + .map(Into::into), + build_number: over.build_number().or(self.build_number()) } + } +} + + +pub trait ManifestSourceOptExt: ManifestSourceOpt { + const MAY_HAVE_EXTRA: bool; + + fn has_extra(&self) -> bool { Self::MAY_HAVE_EXTRA && self.iter_extra().is_some() } + fn iter_extra(&self) -> Option, impl AsRef)>>; + + fn override_with_extra_ref<'t, Over>(&'t self, over: &'t Over) -> impl ManifestSourceOptExt + 't + where Over: ManifestSourceOptExt { + let manifest = self.override_with(over); + let extra = if over.has_extra() || self.has_extra() { + match (self.iter_extra(), over.iter_extra()) { + (None, None) => None, + (None, Some(extra)) => { + let result = extra.into_iter() + .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().clone())); + Some(result.collect()) + }, + (Some(extra), None) => { + let result = extra.into_iter() + .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().clone())); + Some(result.collect()) + }, + (Some(base), Some(extra)) => { + let mut result: ExtraFields = base.into_iter() + .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().clone())) + .collect(); + result.extend( + extra.into_iter() + .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().clone())), + ); + Some(result) + }, + }.unwrap_or_else(|| ExtraFields::with_capacity(0)) + } else { + ExtraFields::with_capacity(0) + }; + + Ext { main: manifest, + extra } + } + + + fn override_with_extra(&self, + overrider: &Over) + -> impl ManifestSourceOptExt + Cob<'static> { + self.override_with_extra_ref(overrider).into_manifest() + } +} + + +impl ManifestSourceOpt for T { + const MAY_BE_INCOMPLETE: bool = false; + fn name(&self) -> Option<&str> { Some(ManifestSource::name(self)) } + fn version(&self) -> Option<&str> { Some(ManifestSource::version(self)) } + fn author(&self) -> Option<&str> { Some(ManifestSource::author(self)) } + fn bundle_id(&self) -> Option<&str> { Some(ManifestSource::bundle_id(self)) } + fn description(&self) -> Option<&str> { Some(ManifestSource::description(self)) } + fn image_path(&self) -> Option<&str> { Some(ManifestSource::image_path(self)) } + fn launch_sound_path(&self) -> Option<&str> { Some(ManifestSource::launch_sound_path(self)) } + fn content_warning(&self) -> Option<&str> { Some(ManifestSource::content_warning(self)) } + fn content_warning2(&self) -> Option<&str> { Some(ManifestSource::content_warning2(self)) } + fn build_number(&self) -> Option { ManifestSource::build_number(self) } +} + +impl ManifestSourceOpt for Cow<'_, T> { + const MAY_BE_INCOMPLETE: bool = T::MAY_BE_INCOMPLETE; + + fn name(&self) -> Option<&str> { self.as_ref().name() } + fn version(&self) -> Option<&str> { self.as_ref().version() } + fn author(&self) -> Option<&str> { self.as_ref().author() } + fn bundle_id(&self) -> Option<&str> { self.as_ref().bundle_id() } + fn description(&self) -> Option<&str> { self.as_ref().description() } + fn image_path(&self) -> Option<&str> { self.as_ref().image_path() } + fn launch_sound_path(&self) -> Option<&str> { self.as_ref().launch_sound_path() } + fn content_warning(&self) -> Option<&str> { self.as_ref().content_warning() } + fn content_warning2(&self) -> Option<&str> { self.as_ref().content_warning2() } + fn build_number(&self) -> Option { self.as_ref().build_number() } +} + +impl<'t, T: ManifestSourceOptExt> ManifestSourceOptExt for &'t T where &'t T: ManifestSourceOpt { + const MAY_HAVE_EXTRA: bool = true; + + fn iter_extra(&self) -> Option, impl AsRef)>> { + (*self).iter_extra() + } +} + + +pub trait TargetId { + fn target(&self) -> &str; +} + + +pub trait IntoManifest: Sized + ManifestSourceOptExt { + fn into_manifest(self) -> Ext> { self.into_owned() } +} +impl IntoManifest for T {} + + +pub(super) trait IntoOwned { + fn into_owned(self) -> T; +} + + +/// Cob as CopyBorrow - partially copy, partially borrow. +/// Used to produce instance of type with internally borrowed things from `self`. +pub trait Cob<'t> + where Self::Output: 't { + type Output; + fn as_borrow(&'t self) -> Self::Output; +} + +impl<'t> Cob<'t> for str where Self: 't { + type Output = Cow<'t, str>; + fn as_borrow(&'t self) -> Self::Output { self.into() } +} + +impl<'t, S: AsRef> Cob<'t> for S { + type Output = Cow<'t, str>; + fn as_borrow(&'t self) -> Self::Output { self.as_ref().into() } +} + + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use crate::metadata::format::Manifest; + use crate::metadata::format::Override; + use crate::metadata::format::Metadata; + use crate::metadata::format::MetadataInner; + use super::*; + + + // Default impl needed for tests only! + impl Default for Manifest { + fn default() -> Self { + Self { name: Default::default(), + version: Default::default(), + author: Default::default(), + bundle_id: Default::default(), + description: Default::default(), + image_path: Default::default(), + launch_sound_path: Default::default(), + content_warning: Default::default(), + content_warning2: Default::default(), + build_number: Default::default() } + } + } + + + #[test] + fn manifest_override() { + let base = Manifest { name: Some("Name"), + bundle_id: Some("dev.foo.bar"), + ..Default::default() }; + + let over = Manifest { name: Some("Over"), + bundle_id: None, + description: Some("description"), + ..Default::default() }; + + + { + let res = base.override_with(&over); + assert_eq!(Some("Over"), res.name()); + assert_eq!(Some("dev.foo.bar"), res.bundle_id()); + assert_eq!(Some("description"), res.description()); + } + + { + let res = base.override_with(&over); + assert_eq!(Some("Over"), res.name()); + assert_eq!(Some("dev.foo.bar"), res.bundle_id()); + assert_eq!(Some("description"), res.description()); + } + + { + let res = base.override_with_extra(&over); + assert_eq!(Some("Over"), res.name()); + assert_eq!(Some("dev.foo.bar"), res.bundle_id()); + assert_eq!(Some("description"), res.description()); + assert!(res.iter_extra().is_none()); + } + } + + + #[test] + fn manifest_override_ext() { + let base = Manifest { name: Some("Name"), + bundle_id: Some("dev.foo.bar"), + ..Default::default() }; + + let mut extra = ExtraFields::with_capacity(1); + extra.insert("foo".into(), "bar".into()); + + + let base = Ext { main: base, extra }; + + let over = Manifest { name: Some("Over"), + bundle_id: None, + description: Some("description"), + ..Default::default() }; + + + { + let res = base.override_with(&over); + assert_eq!(Some("Over"), res.name()); + assert_eq!(Some("dev.foo.bar"), res.bundle_id()); + assert_eq!(Some("description"), res.description()); + } + + { + let res = base.override_with(&over); + assert_eq!(Some("Over"), res.name()); + assert_eq!(Some("dev.foo.bar"), res.bundle_id()); + assert_eq!(Some("description"), res.description()); + } + + { + let res = base.override_with_extra(&over); + assert_eq!(Some("Over"), res.name()); + assert_eq!(Some("dev.foo.bar"), res.bundle_id()); + assert_eq!(Some("description"), res.description()); + + assert!(res.iter_extra().is_some()); + let (k, v) = res.iter_extra().unwrap().into_iter().next().unwrap(); + assert_eq!("foo", k.as_ref()); + assert_eq!(&ExtraValue::String("bar".into()), v.as_ref()); + } + } + + + struct CrateInfoNoMeta; + impl CrateInfoSource for CrateInfoNoMeta { + type Authors = [&'static str]; + + fn name(&self) -> Cow { "Name".into() } + fn authors(&self) -> &Self::Authors { &["John"] } + fn version(&self) -> Cow { "0.0.0".into() } + fn description(&self) -> Option> { None } + fn bins(&self) -> &[&str] { &[SOME_TARGET] } + fn examples(&self) -> &[&str] { &[] } + fn metadata(&self) -> Option { None:: } + + fn manifest_path(&self) -> Cow { Cow::Borrowed(Path::new("Cargo.toml")) } + } + + #[test] + fn manifest_for_base() { + let base = CrateInfoNoMeta.manifest_for_crate(); + let spec = CrateInfoNoMeta.manifest_for_opt("target".into(), false); + let opt = CrateInfoNoMeta.manifest_for("target", false); + assert_eq!(opt, Some(spec.to_owned())); + assert_eq!(spec, base.into_owned()); + } + + + struct CrateInfo; + impl CrateInfoSource for CrateInfo { + type Authors = [&'static str]; + + fn name(&self) -> Cow { "Crate Name".into() } + fn authors(&self) -> &[&'static str] { &["John"] } + fn version(&self) -> Cow { "0.0.0".into() } + fn description(&self) -> Option> { None } + + fn bins(&self) -> &[&str] { &[SOME_TARGET] } + fn examples(&self) -> &[&str] { &[] } + + fn manifest_path(&self) -> Cow { Cow::Borrowed(Path::new("Cargo.toml")) } + + fn metadata(&self) -> Option { + let base = Manifest { name: Some("Meta Name"), + bundle_id: Some("crate.id"), + ..Default::default() }; + + let mut extra = ExtraFields::with_capacity(1); + extra.insert("foo".into(), "bar".into()); + assert!(!extra.is_empty()); + + let manifest = Ext { main: base, extra }.into_owned(); + assert!(manifest.has_extra()); + + let bins = { + let base = Manifest { name: Some("Bin Name"), + author: Some("Alex"), + bundle_id: Some("bin.id"), + description: Some("description"), + ..Default::default() }; + + let mut extra = ExtraFields::with_capacity(1); + extra.insert("boo".into(), 42_usize.into()); + + + let manifest = Ext { main: base, extra }.into_owned(); + vec![Override { target: SOME_TARGET.to_owned(), + manifest }] + }; + + let meta = Metadata { inner: MetadataInner { manifest, + bins, + examples: vec![], + assets: Default::default(), + dev_assets: Default::default(), + options: Default::default(), + support: Default::default() } }; + + Some(meta) + } + } + + const SOME_TARGET: &str = "some-target"; + + #[test] + fn manifest_for_crate() { + let base = CrateInfo.manifest_for_crate(); + assert_eq!(Some("Meta Name"), base.name()); + assert_eq!(Some("John"), base.author()); + assert_eq!(Some("0.0.0"), base.version()); + assert_eq!(Some("crate.id"), base.bundle_id()); + assert!(base.description().is_none()); + assert!(base.has_extra()); + let extra = base.iter_extra() + .unwrap() + .into_iter() + .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().to_owned())) + .collect::>(); + assert_eq!(1, extra.len()); + assert_eq!(Some(&"bar".into()), extra.get("foo")); + } + + #[test] + fn manifest_for_target_wrong_no_meta() { + let spec = CrateInfoNoMeta.manifest_for_opt(Some("WRONG"), false); + + assert_eq!(Some("Name"), spec.name()); + assert_eq!(Some("John"), spec.author()); + assert_eq!(Some("0.0.0"), spec.version()); + assert!(spec.bundle_id().is_none()); + } + + #[test] + fn manifest_for_target_wrong() { + let base = CrateInfo.manifest_for_crate(); + let spec = CrateInfo.manifest_for_opt(Some("WRONG"), false); + assert_eq!(Some("Meta Name"), spec.name()); + assert_eq!(Some("John"), spec.author()); + assert_eq!(Some("0.0.0"), spec.version()); + assert_eq!(Some("crate.id"), spec.bundle_id()); + assert_eq!(spec, base.into_owned()); + } + + #[test] + fn manifest_for_target_bin() { + let spec = CrateInfo.manifest_for_opt(SOME_TARGET.into(), false); + assert_eq!(Some("Bin Name"), spec.name()); + assert_eq!(Some("Alex"), spec.author()); + assert_eq!(Some("0.0.0"), spec.version()); + assert_eq!(Some("bin.id"), spec.bundle_id()); + assert_eq!(Some("description"), spec.description()); + let extra = spec.iter_extra() + .unwrap() + .into_iter() + .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().to_owned())) + .collect::>(); + assert_eq!(2, extra.len()); + assert_eq!(Some(&"bar".into()), extra.get("foo")); + assert_eq!(Some(&42_usize.into()), extra.get("boo")); + } +} diff --git a/support/build/src/metadata/validation.rs b/support/build/src/metadata/validation.rs new file mode 100644 index 00000000..4398aafe --- /dev/null +++ b/support/build/src/metadata/validation.rs @@ -0,0 +1,268 @@ +use std::fmt::Display; + +use super::source::CrateInfoSource; +use super::source::ManifestSourceOptExt; +use super::source::MetadataSource; + + +#[derive(Debug, Clone)] +pub enum Problem { + UnknownTarget { name: String }, + MissingField { field: String }, + Warning(Warning), +} + +#[derive(Debug, Clone)] +pub enum Warning { + MissingMetadata, + StrangeValue { + field: String, + value: String, + reason: Option<&'static str>, + }, + UnknownField { + field: String, + reason: Option<&'static str>, + }, + MissingField { + field: String, + reason: Option<&'static str>, + }, +} + +impl Display for Warning { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MissingMetadata => write!(f, "Metadata not found"), + Self::StrangeValue { field, value, reason } => { + write!(f, "Strange value {value:?} for field '{field}'")?; + if let Some(reason) = reason { + write!(f, ", {reason}") + } else { + Ok(()) + } + }, + Self::UnknownField { field, reason } => { + write!(f, "Unknown field '{field}'")?; + if let Some(reason) = reason { + write!(f, ", {reason}") + } else { + Ok(()) + } + }, + Self::MissingField { field, reason } => { + write!(f, "Missing field '{field}'")?; + if let Some(reason) = reason { + write!(f, ", {reason}") + } else { + Ok(()) + } + }, + } + } +} + + +impl Problem { + pub fn is_err(&self) -> bool { + match self { + Problem::Warning(_) => false, + _ => true, + } + } + + pub fn is_warn(&self) -> bool { !self.is_err() } +} + +impl Display for Problem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::UnknownTarget { name } => write!(f, "Unknown cargo-target {name:?}"), + Self::MissingField { field } => write!(f, "Missing field: {field:?}"), + Self::Warning(warning) => warning.fmt(f), + } + } +} + + +/// Check the implementor validity. +pub trait Validate { + /// Check critical requirements, returns it as errors. + /// Also returns warnings fo not so critical problems. + /// Use it before render the final result. + fn validate(&self) -> impl IntoIterator; +} + +impl Validate for T { + fn validate(&self) -> impl IntoIterator { + let is_not_empty = |s: &&str| !s.trim().is_empty(); + + fn check_some(name: &'static str, v: Option) -> Option { + v.is_none().then(|| Problem::MissingField { field: name.into() }) + } + + fn warn_none(name: &'static str, v: Option, warn_msg: Option<&'static str>) -> Option { + v.is_none().then(|| { + Problem::Warning(Warning::MissingField { field: name.into(), + reason: warn_msg }) + }) + } + + + let missed = [ + ( + "build-number", + self.build_number().is_some(), + Some("required for sideloaded games."), + ), + ("description", self.description().is_some(), None), + ].into_iter() + .filter_map(|(k, v, msg)| warn_none(k, v.then_some(()), msg)); + + + let unknown = self.iter_extra().into_iter().flatten().map(|(k, _)| { + Problem::Warning(Warning::UnknownField { field: k.as_ref() + .to_owned(), + reason: None }) + }); + + + // required fields + let errors = [ + ("name", self.name().filter(is_not_empty)), + ("version", self.version().filter(is_not_empty)), + ("bundle-id", self.bundle_id().filter(is_not_empty)), + ].into_iter() + .filter_map(|(k, v)| check_some(k, v)); + + + errors.chain(missed) + .chain(self.version().into_iter().filter_map(validate_version)) + .chain(unknown) + } +} + + +fn validate_version(value: &str) -> Option { + let re = regex::Regex::new(r"^\d+(?:\.\d+){0,2}$").unwrap(); + if !re.is_match(value.trim()) { + if semver::Version::parse(value).is_err() { + Some(Problem::Warning(Warning::StrangeValue { field: "version".into(), + value: value.into(), + reason: Some("can be confusing.") })) + } else { + None + } + } else { + None + } +} + + +/// Lint the crate-level source. +pub trait ValidateCrate: CrateInfoSource { + fn validate<'t>(&'t self) -> impl IntoIterator + 't { + // - main manifest missing fields + // - main manifest fields in bad format + // - for each final target manifest: + // -> same as for the main manifest + + + // Check that all targets are exists + // - search the target in the crate for each in meta.all_targets + let missed = + self.metadata().into_iter().flat_map(|meta| { + let bins = meta.bin_targets() + .into_iter() + .filter(|name| !self.bins().contains(name)) + .map(|name| Problem::UnknownTarget { name: name.to_owned() }); + + let examples = meta.example_targets() + .into_iter() + .filter(|name| !self.examples().contains(name)) + .map(|name| Problem::UnknownTarget { name: name.to_owned() }); + + bins.chain(examples).collect::>() + }); + + let crate_name_eq = + self.metadata().into_iter().flat_map(|meta| { + let mut targets = meta.all_targets().into_iter(); + if let Some(name) = targets.find(|name| name == &self.name()) { + let msg = "target name is the same as the crate name"; + Some(Problem::Warning(Warning::StrangeValue { field: + "target".into(), + value: name.into(), + reason: Some(msg) })) + } else { + None + } + }); + + let no_meta = self.metadata() + .is_none() + .then(|| Problem::Warning(Warning::MissingMetadata)); + + + missed.into_iter().chain(crate_name_eq).chain(no_meta) + } + + + fn validate_for(&self, target: &str) -> impl IntoIterator { + println!("TODO: validate_for(target={target:?}) not implemented yet!"); + [] + } +} + +impl ValidateCrate for T where T: CrateInfoSource {} + + +#[cfg(test)] +mod tests { + use super::Validate; + use super::super::format::Manifest; + + + #[test] + fn validate_version() { + let cases = [ + ("0", true), + ("0.0", true), + ("0.0.0", true), + ("0.0.0-pre", true), + ("", false), + ("0.0.a", false), + ("beta", false), + ]; + + for (i, (ver, ok)) in cases.iter().enumerate() { + let result = super::validate_version(ver); + assert_eq!(*ok, result.is_none(), "{i}: {result:?}"); + } + } + + + #[test] + fn manifest_empty() { + let m = Manifest::<&str>::default(); + let errors = m.validate().into_iter().collect::>(); + assert_eq!(5, errors.len(), "{:#?}", errors); + assert_eq!(3, errors.iter().filter(|e| e.is_err()).count()); + } + + #[test] + fn manifest_valid() { + let m = Manifest::<&str> { name: "name".into(), + version: "0.0".into(), + author: "author".into(), + bundle_id: "bundle.id".into(), + description: "description".into(), + image_path: "image_path".into(), + launch_sound_path: "launch_sound_path".into(), + content_warning: "content_warning".into(), + content_warning2: "content_warning2".into(), + build_number: 42.into() }; + let errors = m.validate().into_iter().collect::>(); + assert!(errors.is_empty(), "{:#?}", errors); + } +} diff --git a/support/build/src/value.rs b/support/build/src/value.rs deleted file mode 100644 index 6ac608e3..00000000 --- a/support/build/src/value.rs +++ /dev/null @@ -1,98 +0,0 @@ -use std::fmt::Debug; -use std::fmt::Display; - -use crate::metadata::format::AssetsOptions; - -/// 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>; -} - - -#[cfg(feature = "serde_json")] -impl Value for serde_json::Value { - fn as_bool(&self) -> Option { serde_json::Value::as_bool(self) } - fn as_str(&self) -> Option<&str> { serde_json::Value::as_str(self) } -} - -#[cfg(feature = "toml")] -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) } - } -}