From 5957eb9cba0f45c84159ef19ab87093828107bfe Mon Sep 17 00:00:00 2001 From: Wouter Date: Thu, 14 Aug 2025 20:50:18 +0200 Subject: [PATCH 01/10] feat: handlebars support also refactored the single-file codebase into more manageable modules. --- .gitignore | 16 +- .prettierrc | 11 + Cargo.lock | 1244 +++++++++- Cargo.toml | 20 +- README.md | 18 +- conformance/src/main.rs | 25 +- src/config_json.rs | 32 + src/error.rs | 16 + src/html.rs | 216 -- src/main.rs | 1992 +++-------------- src/margo_config/mod.rs | 53 + src/margo_config/v1.rs | 41 + src/margo_config/v2.rs | 59 + src/registry/error.rs | 86 + src/registry/index.rs | 274 +++ src/registry/mod.rs | 10 + src/registry/packaged_cargo_toml.rs | 66 + src/registry/packaged_crate.rs | 184 ++ src/registry/registry.rs | 549 +++++ src/template.rs | 446 ++++ src/template_reference.rs | 55 + src/util.rs | 144 ++ templates/bright/pack.js | 14 + templates/bright/package.json | 21 + templates/bright/pnpm-lock.yaml | 1218 ++++++++++ .../src/fonts/JetBrainsMono-Regular.woff2 | Bin 0 -> 92164 bytes templates/bright/src/fonts/Neris-Black.woff | Bin 0 -> 30828 bytes templates/bright/src/fonts/Neris-Light.woff | Bin 0 -> 35084 bytes .../bright/src/fonts/Neris-SemiBold.woff | Bin 0 -> 34788 bytes templates/bright/src/index.html | 115 + .../bright/src/js/code-block.element.tsx | 197 ++ templates/bright/src/js/jsx.ts | 71 + templates/bright/src/js/nav-highlight.ts | 87 + templates/bright/src/jsx.d.ts | 8 + templates/bright/src/main.css | 55 + templates/bright/src/main.ts | 10 + templates/bright/tsconfig.json | 17 + templates/bright/vite.config.ts | 14 + templates/classic/pack.js | 9 + templates/classic/package.json | 19 + templates/classic/pnpm-lock.yaml | 1158 ++++++++++ templates/classic/src/index.html | 92 + templates/classic/src/main.css | 13 + templates/classic/src/main.ts | 35 + templates/classic/tsconfig.json | 14 + templates/classic/vite.config.ts | 11 + 46 files changed, 6761 insertions(+), 1974 deletions(-) create mode 100644 src/config_json.rs create mode 100644 src/error.rs delete mode 100644 src/html.rs create mode 100644 src/margo_config/mod.rs create mode 100644 src/margo_config/v1.rs create mode 100644 src/margo_config/v2.rs create mode 100644 src/registry/error.rs create mode 100644 src/registry/index.rs create mode 100644 src/registry/mod.rs create mode 100644 src/registry/packaged_cargo_toml.rs create mode 100644 src/registry/packaged_crate.rs create mode 100644 src/registry/registry.rs create mode 100644 src/template.rs create mode 100644 src/template_reference.rs create mode 100644 src/util.rs create mode 100644 templates/bright/pack.js create mode 100644 templates/bright/package.json create mode 100644 templates/bright/pnpm-lock.yaml create mode 100644 templates/bright/src/fonts/JetBrainsMono-Regular.woff2 create mode 100644 templates/bright/src/fonts/Neris-Black.woff create mode 100644 templates/bright/src/fonts/Neris-Light.woff create mode 100644 templates/bright/src/fonts/Neris-SemiBold.woff create mode 100644 templates/bright/src/index.html create mode 100644 templates/bright/src/js/code-block.element.tsx create mode 100644 templates/bright/src/js/jsx.ts create mode 100644 templates/bright/src/js/nav-highlight.ts create mode 100644 templates/bright/src/jsx.d.ts create mode 100644 templates/bright/src/main.css create mode 100644 templates/bright/src/main.ts create mode 100644 templates/bright/tsconfig.json create mode 100644 templates/bright/vite.config.ts create mode 100644 templates/classic/pack.js create mode 100644 templates/classic/package.json create mode 100644 templates/classic/pnpm-lock.yaml create mode 100644 templates/classic/src/index.html create mode 100644 templates/classic/src/main.css create mode 100644 templates/classic/src/main.ts create mode 100644 templates/classic/tsconfig.json create mode 100644 templates/classic/vite.config.ts diff --git a/.gitignore b/.gitignore index e21d352..7531446 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ -/.parcel-cache -/node_modules -/src/html/assets.rs -/target -/ui/dist +.idea/ +.parcel-cache/ + +node_modules/ +target/ +dist/ + +src/html/assets.rs +src/templates/* + +.DS_Store \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index e69de29..55052fc 100644 --- a/.prettierrc +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "bracketSpacing": true, + "experimentalOperatorPosition": "start", + "htmlWhitespaceSensitivity": "css", + "quoteProps": "consistent", + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "useTabs": true, + "tabWidth": 4 +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8b8fae4..e0649c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,62 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + [[package]] name = "argh" version = "0.1.13" @@ -56,12 +112,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a464143cc82dedcdc3928737445362466b7674b5db4e2eb8e869846d6d84f4f6" [[package]] -name = "ascii" -version = "1.1.0" +name = "assert_fs" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +checksum = "a652f6cb1f516886fcfee5e7a5c078b9ade62cfcb889524efe5a64d682dd27a9" dependencies = [ - "serde", + "anstyle", + "doc-comment", + "globwalk", + "predicates", + "predicates-core", + "predicates-tree", + "tempfile", ] [[package]] @@ -186,6 +248,30 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -207,18 +293,169 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "3.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d9ef19ae5263a138da9a86871eca537478ab0332a7770bac7e3f08b801f89f" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577ae008f2ca11ca7641bd44601002ee5ab49ef0af64846ce1ab6057218a5cc1" +dependencies = [ + "darling 0.21.1", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + [[package]] name = "bytes" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +[[package]] +name = "cargo-util-schemas" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dc1a6f7b5651af85774ae5a34b4e8be397d9cf4bc063b7e6dbd99a841837830" +dependencies = [ + "semver", + "serde", + "serde-untagged", + "serde-value", + "thiserror 2.0.12", + "toml", + "unicode-xid", + "url", +] + +[[package]] +name = "caseless" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8" +dependencies = [ + "unicode-normalization", +] + +[[package]] +name = "cc" +version = "1.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "comrak" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c3278f396e5707769a68bc0943e9b8f84a172836b173827810918279621747" +dependencies = [ + "bon", + "caseless", + "clap", + "emojis", + "entities", + "memchr", + "shell-words", + "slug", + "syntect", + "typed-arena", + "unicode_categories", + "xdg", +] + [[package]] name = "conformance" version = "0.1.0" @@ -263,6 +500,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.6" @@ -273,6 +535,122 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b136475da5ef7b6ac596c0e956e37bad51b85b987ff3d5e230e964936736b2" +dependencies = [ + "darling_core 0.21.1", + "darling_macro 0.21.1", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b44ad32f92b75fb438b04b68547e521a548be8acc339a6dacc4a7121488f53e6" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5be8a7a562d315a5b92a630c30cec6bcf663e6673f00fbb69cca66a6f521b9" +dependencies = [ + "darling_core 0.21.1", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + [[package]] name = "dialoguer" version = "0.11.0" @@ -281,9 +659,15 @@ checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" dependencies = [ "console", "shell-words", - "thiserror", + "thiserror 1.0.69", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.10.7" @@ -305,18 +689,75 @@ dependencies = [ "syn", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "emojis" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e1f1df1f181f2539bac8bf027d31ca5ffbf9e559e3f2d09413b9107b5c02f4" +dependencies = [ + "phf", +] + [[package]] name = "encode_unicode" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + +[[package]] +name = "enum-iterator" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c280b9e6b3ae19e152d8e31cf47f18389781e119d4013a2a2bb0180e5facc635" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" version = "0.3.10" @@ -327,6 +768,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -355,6 +806,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -454,12 +914,48 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.9.0", + "ignore", + "walkdir", +] + [[package]] name = "h2" version = "0.4.8" @@ -479,6 +975,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "handlebars" +version = "6.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" +dependencies = [ + "derive_builder", + "log", + "num-order", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror 2.0.12", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -727,6 +1239,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -748,6 +1266,22 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "2.7.1" @@ -758,12 +1292,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "indoc" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" - [[package]] name = "inotify" version = "0.9.6" @@ -784,6 +1312,12 @@ dependencies = [ "libc", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.14" @@ -810,6 +1344,12 @@ dependencies = [ "libc", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.170" @@ -827,12 +1367,24 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "litemap" version = "0.7.5" @@ -849,20 +1401,27 @@ checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" name = "margo" version = "0.1.6" dependencies = [ + "anyhow", "argh", - "ascii", + "assert_fs", + "cargo-util-schemas", + "colored", + "comrak", "dialoguer", + "enum-iterator", "flate2", + "handlebars", "hex", - "indoc", - "maud", + "lazy_static", + "predicates", + "rayon", "registry-conformance", "semver", "serde", "serde_json", "sha2", - "snafu", "tar", + "thiserror 2.0.12", "tokio", "toml", "url", @@ -875,28 +1434,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[package]] -name = "maud" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8156733e27020ea5c684db5beac5d1d611e1272ab17901a49466294b84fc217e" -dependencies = [ - "itoa", - "maud_macros", -] - -[[package]] -name = "maud_macros" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7261b00f3952f617899bc012e3dbd56e4f0110a038175929fa5d18e5a19913ca" -dependencies = [ - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn", -] - [[package]] name = "memchr" version = "2.7.4" @@ -936,7 +1473,7 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -947,10 +1484,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "notify" version = "6.1.1" @@ -969,6 +1512,36 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.7" @@ -984,12 +1557,111 @@ version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.9.0", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +dependencies = [ + "memchr", + "thiserror 2.0.12", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1002,6 +1674,71 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +dependencies = [ + "base64 0.22.1", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -1012,15 +1749,12 @@ dependencies = [ ] [[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" +name = "quick-xml" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4" dependencies = [ - "proc-macro2", - "quote", - "syn", - "version_check", + "memchr", ] [[package]] @@ -1032,6 +1766,32 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.9" @@ -1107,10 +1867,23 @@ dependencies = [ "bitflags 2.9.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", +] + [[package]] name = "rustversion" version = "1.0.19" @@ -1150,6 +1923,27 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.218" @@ -1210,6 +2004,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -1219,6 +2019,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -1228,6 +2034,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + [[package]] name = "smallvec" version = "1.14.0" @@ -1271,6 +2087,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.99" @@ -1299,6 +2121,29 @@ dependencies = [ "syn", ] +[[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", + "walkdir", + "yaml-rust", +] + [[package]] name = "tar" version = "0.4.44" @@ -1317,18 +2162,44 @@ checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", - "rustix", + "rustix 0.38.44", + "windows-sys 0.59.0", +] + +[[package]] +name = "terminal_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +dependencies = [ + "rustix 1.0.8", "windows-sys 0.59.0", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -1342,6 +2213,48 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -1352,6 +2265,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.43.0" @@ -1498,12 +2426,30 @@ dependencies = [ "once_cell", ] +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicase" version = "2.8.1" @@ -1516,12 +2462,33 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-width" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "url" version = "2.5.4" @@ -1546,6 +2513,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version_check" version = "0.9.5" @@ -1568,6 +2541,73 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi-util" version = "0.1.9" @@ -1577,6 +2617,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-sys" version = "0.48.0" @@ -1604,6 +2650,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1628,13 +2683,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1647,6 +2719,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -1659,6 +2737,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -1671,12 +2755,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -1689,6 +2785,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -1701,6 +2803,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1713,6 +2821,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1725,6 +2839,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.7.3" @@ -1734,6 +2854,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -1746,6 +2875,12 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + [[package]] name = "xtask" version = "0.1.0" @@ -1758,6 +2893,15 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index a5a2d8f..78b2409 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ repository = "https://github.com/integer32llc/static-registry" [features] default = ["html"] -html = ["dep:maud", "dep:indoc"] +html = ["dep:handlebars", "dep:comrak"] [workspace] members = [ @@ -28,7 +28,7 @@ unused_crate_dependencies = "deny" lint_groups_priority = { level = "allow", priority = 1 } # Remove after 1.80. https://github.com/rust-lang/rust-clippy/issues/12270 [workspace.dependencies] -argh = { version = "0.1.12", default-features = false } +argh = { version = "0.1.13", default-features = false } registry-conformance = { version = "0.5.3", registry = "registry-conformance" } snafu = { version = "0.8.2", default-features = false, features = ["rust_1_65", "std"] } tokio = { version = "1.37.0", default-features = false, features = ["macros", "process", "rt-multi-thread"] } @@ -38,22 +38,30 @@ workspace = true [dependencies] argh.workspace = true -ascii = { version = "1.1.0", default-features = false, features = ["serde", "std"] } +argh.features = ["help"] +cargo-util-schemas = "0.8.2" +colored = "3.0.0" +comrak = { version = "0.40", optional = true, features = ["shortcodes"] } dialoguer = { version = "0.11.0", default-features = false } +enum-iterator = "2.1.0" flate2 = { version = "1.0.28", default-features = false, features = ["rust_backend"] } +handlebars = { version = "6.3.2", optional = true } hex = { version = "0.4.3", default-features = false, features = ["std"] } -indoc = { version = "2.0.5", default-features = false, optional = true } -maud = { version = "0.27.0", default-features = false, optional = true } +lazy_static = "1.5.0" +rayon = "1.10" semver = { version = "1.0.23", default-features = false, features = ["serde", "std"] } serde = { version = "1.0.197", default-features = false, features = ["derive", "std"] } serde_json = { version = "1.0.115", default-features = false, features = ["std"] } sha2 = { version = "0.10.8", default-features = false } -snafu.workspace = true tar = { version = "0.4.40", default-features = false } toml = { version = "0.8.12", default-features = false, features = ["parse", "display"] } url = { version = "2.5.0", default-features = false, features = ["serde"] } walkdir = { version = "2.5.0", default-features = false } +anyhow = "1" +thiserror = "2" [dev-dependencies] registry-conformance.workspace = true tokio.workspace = true +assert_fs = "1.1" +predicates = "3.1" \ No newline at end of file diff --git a/README.md b/README.md index bb99723..2d345ba 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ This will create a new registry in the directory `https://my-registry.example.com`. ```bash -margo init my-registry-directory --base-url https://my-registry.example.com +margo my-registry-directory init --base-url https://my-registry.example.com ``` ### Add a crate to the registry @@ -36,7 +36,7 @@ to publish. ```bash # Acquire a crate package, such as by running `cargo package` -margo add --registry my-registry-directory some-crate/target/package/some-crate-1.2.3.crate +margo my-registry-directory add some-crate/target/package/some-crate-1.2.3.crate ``` ### Serve the registry files with your choice of webserver @@ -67,7 +67,7 @@ EOF ### Add your crate ```bash -cargo add --registry my-registry some-crate +cargo my-registry add path/to/some-crate/1.0.7.crate ``` ## Other Margo commands @@ -78,13 +78,19 @@ registry directory directly. ### List the crates in the registry ```bash -margo list --registry my-registry +margo my-registry list ``` -### Remove a crate +### Yank a crate version ```bash -margo rm --registry my-registry some-crate --version x.y.z +margo my-registry yank some-crate 1.0.7 +``` + +### Remove a crate version + +```bash +margo my-registry rm some-crate 1.0.7 ``` ## Key differences from Crates.io diff --git a/conformance/src/main.rs b/conformance/src/main.rs index e3272a6..4e7b6b9 100644 --- a/conformance/src/main.rs +++ b/conformance/src/main.rs @@ -92,18 +92,16 @@ impl MargoBuilder { let mut cmd = this.command(); - cmd.arg("init") + cmd.arg(&this.directory) + .arg("init") .args(["--base-url", &format!("http://{webserver_address}")]) - .arg("--defaults"); + .arg("--use-defaults"); if auth_required { cmd.args(["--auth-required", "true"]); } - cmd.arg(&this.directory) - .expect_success() - .await - .context(ExecutionSnafu)?; + cmd.expect_success().await.context(ExecutionSnafu)?; Ok(this) } @@ -174,9 +172,8 @@ impl Margo { let package_path = crate_.package().await.context(PackageSnafu)?; self.command() - .arg("add") - .arg("--registry") .arg(&self.directory) + .arg("add") .arg(package_path) .expect_success() .await @@ -189,11 +186,10 @@ impl Margo { use remove_error::*; self.command() - .arg("rm") - .arg("--registry") .arg(&self.directory) + .arg("rm") .arg(crate_.name()) - .args(["--version", crate_.version()]) + .arg(crate_.version()) .expect_success() .await .context(ExecutionSnafu)?; @@ -205,11 +201,10 @@ impl Margo { use yank_error::*; let mut cmd = self.command(); - cmd.arg("yank") - .arg("--registry") - .arg(&self.directory) + cmd.arg(&self.directory) + .arg("yank") .arg(crate_.name()) - .args(["--version", crate_.version()]); + .arg(crate_.version()); if !yanked { cmd.arg("--undo"); diff --git a/src/config_json.rs b/src/config_json.rs new file mode 100644 index 0000000..4de4ea7 --- /dev/null +++ b/src/config_json.rs @@ -0,0 +1,32 @@ +use crate::margo_config::LatestConfig; +use crate::prelude::*; +use serde::Serialize; + +/// The config.json file required for the registry. +#[derive(Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct ConfigJson { + // This field cannot be a `url::Url` because that type + // percent-escapes the `{` and `}` characters. Cargo performs + // string-replacement on this value based on those literal `{` + // and `}` characters. + pub dl: String, + + pub api: Option, // Modified + + /// A private registry requires all operations to be authenticated. + /// + /// This includes API requests, crate downloads and sparse + /// index updates. + pub auth_required: bool, +} + +impl ConfigJson { + pub fn new(config: &LatestConfig) -> Result { + Ok(ConfigJson { + dl: format!("{base}/crates/{{lowerprefix}}/{{crate}}/{{version}}.crate", base = config.base_url), + api: None, + auth_required: config.auth_required, + }) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..5d07f0f --- /dev/null +++ b/src/error.rs @@ -0,0 +1,16 @@ +use cargo_util_schemas::manifest::PackageName; +use semver::Version; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum MargoError { + #[cfg(not(feature = "html"))] + #[error("Please reinstall Margo with the `html` feature enabled in order to render HTML index pages.")] + NoHtml, + + #[error("The template archive doesn't contain an index file (index.html or index.hbs), or the index file is empty.")] + MissingTemplateIndex, + + #[error("Version {1} of package {0} already exists in the index.")] + DuplicateVersion(PackageName, Version), +} diff --git a/src/html.rs b/src/html.rs deleted file mode 100644 index e2e54e7..0000000 --- a/src/html.rs +++ /dev/null @@ -1,216 +0,0 @@ -use indoc::formatdoc; -use maud::{html, Markup, PreEscaped, DOCTYPE}; -use semver::Version; -use snafu::prelude::*; -use std::{fs, io, path::PathBuf}; - -use crate::{index_entry, ConfigV1, Index, ListAll, Registry}; - -#[rustfmt::skip] -mod assets; - -pub fn write(registry: &Registry) -> Result<(), Error> { - use error::*; - - let crates = registry.list_all()?; - let index = index(®istry.config, &crates).into_string(); - let index_path = registry.path.join("index.html"); - fs::write(&index_path, index).context(WriteIndexSnafu { path: index_path })?; - - let assets_dir = registry.path.join("assets"); - fs::create_dir_all(&assets_dir).context(AssetDirSnafu { path: &assets_dir })?; - - let css_path = assets_dir.join(assets::CSS_NAME); - fs::write(&css_path, assets::CSS).context(CssSnafu { path: &css_path })?; - - let css_map_path = { - let mut css_map_path = css_path; - css_map_path.as_mut_os_string().push(".map"); - css_map_path - }; - fs::write(&css_map_path, assets::CSS_MAP).context(CssMapSnafu { - path: &css_map_path, - })?; - - let js_path = assets_dir.join(assets::JS_NAME); - fs::write(&js_path, assets::JS).context(JsSnafu { path: &js_path })?; - - let js_map_path = { - let mut js_map_path = js_path; - js_map_path.as_mut_os_string().push(".map"); - js_map_path - }; - fs::write(&js_map_path, assets::JS_MAP).context(JsMapSnafu { path: &js_map_path })?; - - Ok(()) -} - -#[derive(Debug, Snafu)] -#[snafu(module)] -pub enum Error { - #[snafu(display("Could not list the crates"))] - #[snafu(context(false))] - ListAll { source: crate::ListAllError }, - - #[snafu(display("Could not write the HTML index page to {}", path.display()))] - WriteIndex { source: io::Error, path: PathBuf }, - - #[snafu(display("Could not create the HTML asset directory at {}", path.display()))] - AssetDir { source: io::Error, path: PathBuf }, - - #[snafu(display("Could not write the CSS file to {}", path.display()))] - Css { source: io::Error, path: PathBuf }, - - #[snafu(display("Could not write the CSS sourcemap file to {}", path.display()))] - CssMap { source: io::Error, path: PathBuf }, - - #[snafu(display("Could not write the JS file to {}", path.display()))] - Js { source: io::Error, path: PathBuf }, - - #[snafu(display("Could not write the JS sourcemap file to {}", path.display()))] - JsMap { source: io::Error, path: PathBuf }, -} - -const CARGO_DOCS: &str = - "https://doc.rust-lang.org/cargo/reference/registries.html#using-an-alternate-registry"; - -fn index(config: &ConfigV1, crates: &ListAll) -> Markup { - let base_url = &config.base_url; - let suggested_name = config.html.suggested_registry_name(); - - let asset_head_elements = PreEscaped(assets::INDEX); - - fn link(href: &str, content: &str) -> Markup { - html! { - a href=(href) class="underline text-blue-600 hover:text-blue-800 visited:text-purple-600" { - (content) - } - } - } - - fn section(name: &str, id: &str, content: Markup) -> Markup { - html! { - section class="p-1" { - h1 class="text-2xl" { - a class="hover:after:content-['_ยง']" id=(id) href={"#" (id)} { - (name) - } - } - - (content) - } - } - } - - fn code_block(content: impl AsRef) -> Markup { - let content = content.as_ref(); - - let span_class = "col-start-1 row-start-1 leading-none p-1"; - - html! { - mg-copy { - pre class="relative border border-black bg-theme-rose-light m-1 p-1 overflow-x-auto" { - button class="hidden absolute top-0 right-0 grid" data-target="copy" { - span class=(span_class) data-target="state0" { "Copy" } - span class={(span_class) " invisible"} data-target="state1" { "Copied" } - } - code data-target="content" { (content) } - } - } - } - } - - let config_stanza = formatdoc! {r#" - [registries] - {suggested_name} = {{ index = "sparse+{base_url}" }} - "#}; - - let cargo_add_stanza = formatdoc! {" - cargo add --registry {suggested_name} some-crate-name - "}; - - html! { - (DOCTYPE) - html lang="en-US" { - head { - meta charset="utf-8"; - meta name="viewport" content="width=device-width, initial-scale=1"; - title { "Margo Crate Registry" }; - (asset_head_elements); - } - - body class="flex flex-col min-h-screen bg-theme-salmon-light" { - header { - h1 class="text-3xl font-bold bg-theme-purple text-theme-salmon-light p-2 drop-shadow-xl" { - "Margo Crate Registry" - } - } - - (section("Getting started", "getting-started", html! { - ol class="list-inside list-decimal" { - li { - "Add the registry definition to your " - code { ".cargo/config.toml" } - ":" - - (code_block(config_stanza)) - } - - li { - "Add your dependency to your project:" - - (code_block(cargo_add_stanza)) - } - } - - "For complete details, check the " - (link(CARGO_DOCS, "Cargo documentation")) - "." - })) - - (section("Available crates", "crates", html! { - table class="table-fixed w-full" { - thead { - tr { - th class="w-4/5 text-left" { "Name" } - th { "Versions" } - } - } - - tbody { - @for (c, v) in crates { - tr class="hover:bg-theme-orange" { - td { - span class="truncate" { (c.as_str()) } - } - td { - select class="w-full bg-white" name="version" { - @for (v, c, select) in most_interesting(v) { - @let suffix = if c.yanked { " (yanked)" } else { "" }; - option selected[select] { (v) (suffix) } - } - } - } - } - } - } - } - })) - - footer class="grow place-content-end text-center" { - span class="border-t border-dashed border-theme-purple" { - "Powered by " - (link("https://github.com/integer32llc/margo", "Margo")) - } - } - } - } - } -} - -fn most_interesting(i: &Index) -> impl Iterator { - let last_non_yanked = i.iter().rfind(|(_, c)| !c.yanked).map(|(v, _)| v); - - i.iter() - .map(move |(v, c)| (v, c, Some(v) == last_non_yanked)) -} diff --git a/src/main.rs b/src/main.rs index 471b74b..4d4a102 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,74 @@ -use common::CrateName; +use crate::margo_config::MargoConfig; +use crate::margo_config::{LatestConfig, LatestConfigIndex}; +use crate::registry::{Registry, MARGO_CONFIG_FILE_NAME}; +use crate::template_reference::{BuiltInTemplate, TemplateReference}; +use anyhow::bail; +use cargo_util_schemas::manifest::PackageName; +use colored::Colorize; +use dialoguer::{Confirm, Input, Select}; use semver::Version; -use serde::{Deserialize, Serialize}; -use snafu::prelude::*; -use std::{ - collections::{BTreeMap, BTreeSet}, - env, fmt, - fs::{self, File}, - io::{self, BufRead, BufReader, BufWriter, Read, Write}, - path::{Component, Path, PathBuf}, - str, -}; +use std::env::current_dir; +use std::{path::PathBuf, str}; use url::Url; +use crate::prelude::*; + +mod prelude { + pub use crate::error::MargoError; + pub use crate::util::*; + pub use anyhow::{Context, Result}; +} + +mod config_json; +mod error; +mod margo_config; +mod registry; #[cfg(feature = "html")] -mod html; +mod template; +mod template_reference; +mod util; + +fn main() -> Result<()> { + let args: Args = argh::from_env(); + + // Print the Margo program title and version. + println!( + "{title} v{version}", + title = "Margo".yellow().bold(), + version = env!("CARGO_PKG_VERSION"), + ); + + // Resolve and normalise the given path into an absolute path + // in the current working directory. + let mut registry_path = args.registry_path.clone(); + if !registry_path.is_absolute() { + registry_path = current_dir()?.join(registry_path).canonicalize()?; + } + + // Print the resolved registry path. + println!(" in {}", registry_path.display().to_string().dimmed()); + + match args.subcommand { + Subcommand::Init(init) => init.handle(registry_path)?, + Subcommand::Add(add) => add.handle(registry_path)?, + Subcommand::Remove(rm) => rm.handle(registry_path)?, + Subcommand::Yank(yank) => yank.handle(registry_path)?, + Subcommand::List(list) => list.handle(registry_path)?, + Subcommand::RebuildIndex(rebuild) => rebuild.handle(registry_path)?, + #[cfg(feature = "html")] + Subcommand::GenerateHtml(html) => html.handle(registry_path)?, + } + + Ok(()) +} -#[derive(Debug, argh::FromArgs)] /// Manage a static crate registry +#[derive(Debug, argh::FromArgs)] struct Args { + /// path to the registry directory, defaults to the current working directory. + #[argh(positional, default = "std::env::current_dir().unwrap()")] + registry_path: PathBuf, + #[argh(subcommand)] subcommand: Subcommand, } @@ -25,1742 +76,331 @@ struct Args { #[derive(Debug, argh::FromArgs)] #[argh(subcommand)] enum Subcommand { - Init(InitArgs), - Add(AddArgs), - Remove(RemoveArgs), - Yank(YankArgs), - List(ListArgs), - GenerateHtml(GenerateHtmlArgs), + Init(Init), + Add(Add), + Remove(Remove), + Yank(Yank), + List(List), + RebuildIndex(RebuildIndex), + #[cfg(feature = "html")] + GenerateHtml(GenerateHtml), } -/// Initialize a new registry +/// Initialise a new registry #[derive(Debug, argh::FromArgs)] -#[argh(subcommand)] -#[argh(name = "init")] -struct InitArgs { +#[argh(subcommand, name = "init")] +struct Init { + /// use default values where possible, instead of prompting for them + #[argh(switch, short = 'y')] + use_defaults: bool, + /// the URL that the registry is hosted at #[argh(option)] base_url: Option, - /// use default values where possible, instead of prompting for them - #[argh(switch)] - defaults: bool, - /// require HTTP authentication to access crates #[argh(option)] auth_required: Option, - /// generate an HTML file showing crates in the index + /// whether to render an HTML index page #[argh(option)] html: Option, - /// name you'd like to suggest other people call your registry - #[argh(option)] - html_suggested_registry_name: Option, - - #[argh(positional)] - path: PathBuf, -} - -/// Add a crate to the registry -#[derive(Debug, argh::FromArgs)] -#[argh(subcommand)] -#[argh(name = "add")] -struct AddArgs { - /// path to the registry to modify - #[argh(option)] - registry: Option, - - #[argh(positional)] - path: Vec, -} - -/// Remove a crate from the registry -#[derive(Debug, argh::FromArgs)] -#[argh(subcommand)] -#[argh(name = "rm")] -struct RemoveArgs { - /// path to the registry to modify - #[argh(option)] - registry: Option, - - // FUTURE: Allow removing all versions at once? - /// the version of the crate - #[argh(option)] - version: Version, - - #[argh(positional)] - name: CrateName, -} - -/// Generate an HTML index for the registry -#[derive(Debug, argh::FromArgs)] -#[argh(subcommand)] -#[argh(name = "generate-html")] -struct GenerateHtmlArgs { - /// path to the registry to modify - #[argh(option)] - registry: Option, -} - -/// Yank a version of a crate from the registry -#[derive(Debug, argh::FromArgs)] -#[argh(subcommand)] -#[argh(name = "yank")] -struct YankArgs { - /// path to the registry to modify + /// path to the template tarball to use, if `html` is true #[argh(option)] - registry: Option, - - /// undo a previous yank - #[argh(switch)] - undo: bool, + template: Option, - /// the version of the crate + /// title of the HTML index page, if `html` is true #[argh(option)] - version: Version, - - /// the name of the crate - #[argh(positional)] - name: CrateName, -} + title: Option, -/// List all crates and their versions in the registry -#[derive(Debug, argh::FromArgs)] -#[argh(subcommand)] -#[argh(name = "list")] -struct ListArgs { - /// path to the registry to list + /// name you'd like to suggest other people call your registry #[argh(option)] - registry: Option, -} - -#[snafu::report] -fn main() -> Result<(), Error> { - let args: Args = argh::from_env(); - - let global = Global::new()?; - let global = Box::leak(Box::new(global)); - - match args.subcommand { - Subcommand::Init(init) => do_init(global, init)?, - Subcommand::Add(add) => do_add(global, add)?, - Subcommand::Remove(rm) => do_remove(global, rm)?, - Subcommand::Yank(yank) => do_yank(global, yank)?, - Subcommand::List(list) => do_list(global, list)?, - Subcommand::GenerateHtml(html) => do_generate_html(global, html)?, - } - - Ok(()) -} - -#[derive(Debug, Snafu)] -enum Error { - #[snafu(display("Could not initialize global variables"))] - #[snafu(context(false))] - Global { - #[snafu(source(from(GlobalError, Box::new)))] - source: Box, - }, - - #[snafu(transparent)] - Initialize { - #[snafu(source(from(DoInitializeError, Box::new)))] - source: Box, - }, - - #[snafu(transparent)] - Open { - #[snafu(source(from(DiscoverRegistryError, Box::new)))] - source: Box, - }, - - #[snafu(transparent)] - Add { - #[snafu(source(from(AddError, Box::new)))] - source: Box, - }, - - #[snafu(transparent)] - Remove { - #[snafu(source(from(RemoveError, Box::new)))] - source: Box, - }, - - #[snafu(transparent)] - Html { - #[snafu(source(from(HtmlError, Box::new)))] - source: Box, - }, - - #[snafu(transparent)] - Yank { - #[snafu(source(from(YankError, Box::new)))] - source: Box, - }, -} - -trait UnwrapOrDialog { - fn apply_default(self, use_default: bool, value: impl Into) -> Self; - - fn unwrap_or_dialog(self, f: impl FnOnce() -> dialoguer::Result) -> dialoguer::Result; + suggested_registry_name: Option, } -impl UnwrapOrDialog for Option { - fn apply_default(self, use_default: bool, value: impl Into) -> Self { - if self.is_none() && use_default { - Some(value.into()) - } else { - self - } - } - - fn unwrap_or_dialog(self, f: impl FnOnce() -> dialoguer::Result) -> dialoguer::Result { - match self { - Some(v) => Ok(v), - None => f(), +impl Init { + fn handle(self, registry_path: PathBuf) -> Result<()> { + if registry_path.join(MARGO_CONFIG_FILE_NAME).exists() { + bail!( + "Can't create a new registry in {path}, it already contains a {filename} file.", + path = registry_path.display(), + filename = MARGO_CONFIG_FILE_NAME + ); } - } -} -fn do_init(_global: &Global, init: InitArgs) -> Result<(), DoInitializeError> { - use do_initialize_error::*; + let defaults = MargoConfig::default().into_latest(); - let base_url = init - .base_url - .unwrap_or_dialog(|| { - dialoguer::Input::new() - .with_prompt("What URL will the registry be served from") - .interact() - }) - .context(BaseUrlSnafu)?; - - let auth_required = init - .auth_required - .apply_default(init.defaults, ConfigV1::USER_DEFAULT_AUTH_REQUIRED) - .unwrap_or_dialog(|| { - dialoguer::Confirm::new() - .default(ConfigV1::USER_DEFAULT_AUTH_REQUIRED) - .show_default(true) - .with_prompt("Require HTTP authentication to access crates?") + // Gather configuration values + let base_url = self.base_url.unwrap_or_dialog("base-url", || { + Input::new() + .with_prompt("Which URL will the registry be hosted at?") .interact() - }) - .context(AuthRequiredSnafu)?; - - let enabled = init - .html - .apply_default(init.defaults, ConfigV1Html::USER_DEFAULT_ENABLED) - .unwrap_or_dialog(|| { - dialoguer::Confirm::new() - .default(ConfigV1Html::USER_DEFAULT_ENABLED) - .show_default(true) - .with_prompt("Enable HTML index generation?") - .interact() - }) - .context(HtmlEnabledSnafu)?; - - let suggested_registry_name = if enabled { - let name = init - .html_suggested_registry_name - .apply_default( - init.defaults, - ConfigV1Html::USER_DEFAULT_SUGGESTED_REGISTRY_NAME, - ) - .unwrap_or_dialog(|| { - dialoguer::Input::new() - .default(ConfigV1Html::USER_DEFAULT_SUGGESTED_REGISTRY_NAME.to_owned()) + })?; + + let auth_required = self + .auth_required + .apply_default(self.use_defaults, defaults.auth_required) + .unwrap_or_dialog("auth-required", || { + Confirm::new() + .default(defaults.auth_required) .show_default(true) - .with_prompt("Name you'd like to suggest other people call your registry") + .with_prompt("Require HTTP authentication to access crates?") .interact() - }) - .context(HtmlSuggestedRegistryNameSnafu)?; - - Some(name) - } else { - None - }; - - let config = ConfigV1 { - base_url, - auth_required, - html: ConfigV1Html { - enabled, - suggested_registry_name, - }, - }; - - let r = Registry::initialize(config, &init.path)?; - - if r.config.html.enabled { - let res = r.generate_html(); - - if cfg!(feature = "html") { - res?; - } else if let Err(e) = res { - eprintln!("Warning: {e}"); - } - } - - Ok(()) -} - -#[derive(Debug, Snafu)] -#[snafu(module)] -enum DoInitializeError { - #[snafu(display("Could not determine the base URL"))] - BaseUrl { source: dialoguer::Error }, - - #[snafu(display("Could not determine if HTTP authorization is required"))] - AuthRequired { source: dialoguer::Error }, + })?; + + let html_enabled = match self.html { + Some(i) => i, + None if self.use_defaults => defaults.html.is_some(), + None => Confirm::new() + .with_prompt("Render an HTML index page for the registry?") + .default(true) + .interact()?, + }; - #[snafu(display("Could not determine if HTML generation is enabled"))] - HtmlEnabled { source: dialoguer::Error }, + // If the `html` feature isn't enabled, show an error if the user tries to enable the index. + #[cfg(not(feature = "html"))] + ensure!(has_index == false, NoHtmlSnafu); + + // Gather the index HTML configuration, if one is needed. + let index = if html_enabled && self.use_defaults { + defaults.html + } else if html_enabled { + let defaults = defaults.html.expect("Default config must have an index."); + + let template = match self.template { + Some(t) => t, + None => { + // Prompting to get the template is a two-step process. First, the user can + // select from one of the built-in templates or choose a custom value. If + // they select custom, they're then asked to provide a path. + + let mut all = BuiltInTemplate::all().collect::>(); + + let built_in = Select::new() + .with_prompt("Which template do you want to use for the HTML index page?") + .items(&all.iter().map(|t| t.name()).collect::>()) + .item("") + .default(match defaults.template { + TemplateReference::BuiltIn(b) => { + all.iter().position(|t| &b == t).unwrap() + } + TemplateReference::File(_) => all.len(), + }) + .interact()?; + + if built_in < all.len() { + TemplateReference::BuiltIn(all.swap_remove(built_in)) + } else { + TemplateReference::File( + Input::::new() + .with_prompt("Path to your custom template tarball") + .interact()? + .into(), + ) + } + } + }; - #[snafu(display("Could not determine the suggested registry name"))] - HtmlSuggestedRegistryName { source: dialoguer::Error }, + let title = self + .title + .apply_default(self.use_defaults, &defaults.title) + .unwrap_or_dialog("title", || { + Input::new() + .with_prompt("Title to use for the HTML index page") + .default(defaults.title) + .show_default(true) + .interact() + })?; - #[snafu(transparent)] - Initialize { source: InitializeError }, + let suggested_registry_name = self + .suggested_registry_name + .apply_default(self.use_defaults, &defaults.suggested_registry_name) + .unwrap_or_dialog("suggested-registry-name", || { + Input::new() + .with_prompt("Name you'd like to suggest other people call your registry") + .default(defaults.suggested_registry_name) + .show_default(true) + .interact() + })?; - #[snafu(transparent)] - Html { source: HtmlError }, -} + Some(LatestConfigIndex { + template, + title, + suggested_registry_name, + }) + } else { + None + }; -fn do_add(global: &Global, add: AddArgs) -> Result<(), Error> { - let r = discover_registry(add.registry)?; + let config = LatestConfig { + base_url, + auth_required, + html: index, + }; + let registry = Registry::initialise(registry_path, config)?; + registry.maybe_generate_html()?; - for i in add.path { - r.add(global, i)?; + Ok(()) } - r.maybe_generate_html()?; - - Ok(()) -} - -fn do_remove(_global: &Global, rm: RemoveArgs) -> Result<(), Error> { - let r = discover_registry(rm.registry)?; - - r.remove(rm.name, rm.version)?; - r.maybe_generate_html()?; - - Ok(()) -} - -fn do_generate_html(_global: &Global, html: GenerateHtmlArgs) -> Result<(), Error> { - let r = discover_registry(html.registry)?; - r.generate_html()?; - Ok(()) } -fn do_yank(_global: &Global, yank: YankArgs) -> Result<(), Error> { - let r = discover_registry(yank.registry)?; - - r.yank(yank.name, yank.version, !yank.undo)?; - r.maybe_generate_html()?; - - Ok(()) +/// Add a crate to the registry +#[derive(Debug, argh::FromArgs)] +#[argh(subcommand, name = "add")] +struct Add { + /// path to the .crate file to add + #[argh(positional)] + crate_path: Vec, } -fn do_list(_global: &Global, list: ListArgs) -> Result<(), Error> { - let r = discover_registry(list.registry)?; - - let crates = r.list_all().unwrap(); - - #[derive(Default)] - struct Max(usize, String); - - impl Max { - fn push(&mut self, v: impl fmt::Display) { - use std::fmt::Write; - - let Self(m, s) = self; - - s.clear(); - _ = write!(s, "{v}"); - *m = usize::max(*m, s.len()); - } +impl Add { + fn handle(self, registry_path: PathBuf) -> Result<()> { + let registry = Registry::open(registry_path)?; - fn max(&self) -> usize { - self.0 + for path in self.crate_path { + registry.add(path)?; } - } - - let mut max_c = Max::default(); - let mut max_v = Max::default(); + registry.maybe_generate_html()?; - for (crate_, versions) in &crates { - max_c.push(crate_); - for version in versions.keys() { - max_v.push(version); - } + Ok(()) } +} - let max_c = max_c.max(); - let max_v = max_v.max(); - - for (crate_, versions) in crates { - for version in versions.keys() { - println!("{crate_:) -> Result { - use discover_registry_error::*; +impl Remove { + fn handle(self, registry_path: PathBuf) -> Result<()> { + let registry = Registry::open(registry_path)?; - match path { - Some(p) => Registry::open(p).context(OpenSnafu), - None => { - let cwd = env::current_dir().context(CurrentDirSnafu)?; + registry.remove(&self.name, &self.version)?; + registry.maybe_generate_html()?; - match Registry::open(cwd) { - Ok(r) => Ok(r), - Err(e) if e.is_not_found() => FallbackNotFoundSnafu.fail(), - Err(e) => Err(e).context(FallbackOpenSnafu)?, - } - } + Ok(()) } } -#[derive(Debug, Snafu)] -#[snafu(module)] -enum DiscoverRegistryError { - #[snafu(display("Could not open the specified registry"))] - Open { source: OpenError }, - - #[snafu(display("Could not determine the current directory, {}", Self::TRY_THIS))] - CurrentDir { source: io::Error }, - - #[snafu(display( - "The current directory does not contain a registry, {}", - Self::TRY_THIS, - ))] - FallbackNotFound, - - #[snafu(display("Could not open the registry in the current directory"))] - FallbackOpen { source: OpenError }, -} +/// Yank a version of a crate from the registry +#[derive(Debug, argh::FromArgs)] +#[argh(subcommand, name = "yank")] +struct Yank { + /// undo a previous yank + #[argh(switch)] + undo: bool, -impl DiscoverRegistryError { - const TRY_THIS: &'static str = "please use the `--registry` command line option"; -} + /// the name of the crate + #[argh(positional)] + name: PackageName, -#[derive(Debug)] -struct Registry { - path: PathBuf, - config: ConfigV1, + /// the version of the crate + #[argh(positional)] + version: Version, } -type Index = BTreeMap; -type ListAll = BTreeMap; - -impl Registry { - fn initialize(config: ConfigV1, path: impl Into) -> Result { - use initialize_error::*; - - let config = config.normalize(); - let path = path.into(); - - println!("Initializing registry in `{}`", path.display()); - - fs::create_dir_all(&path).context(RegistryCreateSnafu)?; - - let config_toml_path = path.join(CONFIG_FILE_NAME); - let config = Config::V1(config); - let config_toml = toml::to_string(&config).context(ConfigTomlSerializeSnafu)?; - fs::write(&config_toml_path, config_toml).context(ConfigTomlWriteSnafu { - path: &config_toml_path, - })?; - - let Config::V1(config) = config; - - let dl = format!( - "{base_url}crates/{{lowerprefix}}/{{crate}}/{{version}}.crate", - base_url = config.base_url, - ); - let auth_required = config.auth_required; - - let this = Self { path, config }; - - let config_json_path = this.config_json_path(); - let config_json = config_json::Root { - dl, - api: None, - auth_required, - }; - let config_json = serde_json::to_string(&config_json).context(ConfigJsonSerializeSnafu)?; - fs::write(&config_json_path, config_json).context(ConfigJsonWriteSnafu { - path: &config_json_path, - })?; - - Ok(this) - } - - fn open(path: impl Into) -> Result { - use open_error::*; - - let path = path.into(); - - let config_path = path.join(CONFIG_FILE_NAME); - let config = fs::read_to_string(&config_path).context(ReadSnafu { path: &config_path })?; - let Config::V1(config) = - toml::from_str(&config).context(DeserializeSnafu { path: &config_path })?; - - Ok(Self { path, config }) - } - - fn add(&self, global: &Global, crate_path: impl AsRef) -> Result<(), AddError> { - use add_error::*; - - let crate_path = crate_path.as_ref(); - - println!("Adding crate `{}` to registry", crate_path.display()); - - let crate_file = fs::read(crate_path).context(ReadCrateSnafu)?; - - use sha2::Digest; - let checksum = sha2::Sha256::digest(&crate_file); - let checksum_hex = hex::encode(checksum); - - let cargo_toml = extract_root_cargo_toml(&crate_file)?.context(CargoTomlMissingSnafu)?; - - let cargo_toml = String::from_utf8(cargo_toml).context(CargoTomlUtf8Snafu)?; - let cargo_toml = toml::from_str(&cargo_toml).context(CargoTomlMalformedSnafu)?; - - let index_entry = - adapt_cargo_toml_to_index_entry(global, &self.config, cargo_toml, checksum_hex); - - let index_path = self.index_file_path_for(&index_entry.name); - if let Some(path) = index_path.parent() { - fs::create_dir_all(path).context(IndexDirSnafu { path })?; - } - - let crate_file_path = self.crate_file_path_for(&index_entry.name, &index_entry.vers); - if let Some(path) = crate_file_path.parent() { - fs::create_dir_all(path).context(CrateDirSnafu { path })?; - } - - // FUTURE: Stronger file system consistency (atomic file overwrites, rollbacks on error) - // FUTURE: "transactional" adding of multiple crates +impl Yank { + fn handle(&self, registry_path: PathBuf) -> Result<()> { + let registry = Registry::open(registry_path)?; - self.read_modify_write(&index_entry.name.clone(), |index_file| { - index_file.insert(index_entry.vers.clone(), index_entry); - Ok::<_, AddError>(()) - })?; - - println!("Wrote crate index to `{}`", index_path.display()); - - fs::write(&crate_file_path, &crate_file).context(CrateWriteSnafu { - path: &crate_file_path, - })?; - println!("Wrote crate to `{}`", crate_file_path.display()); + registry.yank(&self.name, &self.version, !self.undo)?; + registry.maybe_generate_html()?; Ok(()) } +} - fn remove(&self, name: CrateName, version: Version) -> Result<(), RemoveError> { - use remove_error::*; +/// List all crates and their versions in the registry +#[derive(Debug, argh::FromArgs)] +#[argh(subcommand, name = "list")] +struct List {} + +impl List { + fn handle(self, registry_path: PathBuf) -> Result<()> { + let registry = Registry::open(registry_path)?; + + let indexes = registry.list_indexes()?; + + let packages_count = indexes.len(); + let total_versions_count = indexes + .iter() + .map(|i| i.entries.len()) + .reduce(|total, i| total + i) + .unwrap_or(0); + + println!( + "{packages} package{packages_s} in the registry ({versions} total version{versions_s}):", + packages = packages_count.to_string().bold(), + packages_s = if packages_count == 1 { "" } else { "s" }, + versions = total_versions_count.to_string().bold(), + versions_s = if total_versions_count == 1 { "" } else { "s" }, + ); - self.read_modify_write(&name, |index| { - index.remove(&version); - Ok::<_, RemoveError>(()) - })?; + for i in indexes { + // Display the latest non-yanked version on top. + let latest_version_str = i + .latest_non_yanked_version() + .map(|v| v.to_string()) + .unwrap_or_default(); - let crate_file = self.crate_file_path_for(&name, &version); - match fs::remove_file(&crate_file) { - Ok(()) => Ok(()), - Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()), - Err(e) => Err(e).context(DeleteSnafu { path: crate_file }), - } - } + println!(" {} {}", i.name.cyan().bold(), latest_version_str); - #[cfg(feature = "html")] - fn generate_html(&self) -> Result<(), HtmlError> { - html::write(self) - } + let indent = i.name.len(); - #[cfg(not(feature = "html"))] - fn generate_html(&self) -> Result<(), HtmlError> { - Err(HtmlError) - } + for entry in i.entries.values() { + let version = entry.vers.to_string(); + if version == latest_version_str { + continue; + } - fn maybe_generate_html(&self) -> Result<(), HtmlError> { - if self.config.html.enabled { - self.generate_html() - } else { - Ok(()) + if entry.yanked { + println!( + " {indent} {version} {yanked}", + indent = " ".repeat(indent), + version = version.dimmed().strikethrough(), + yanked = "yanked".red().dimmed() + ); + } else { + println!(" {indent} {version}", indent = " ".repeat(indent)); + } + } } - } - fn yank(&self, name: CrateName, version: Version, yanked: bool) -> Result<(), YankError> { - use yank_error::*; - - self.read_modify_write(&name, |index| { - let entry = index.get_mut(&version).context(VersionSnafu)?; - entry.yanked = yanked; - Ok(()) - }) - } - - fn read_modify_write( - &self, - name: &CrateName, - modify: impl FnOnce(&mut Index) -> Result, - ) -> Result - where - E: From, - { - use read_modify_write_error::*; - - let path = self.index_file_path_for(name); - let mut index = Self::parse_index_file(&path).context(IndexParseSnafu { path: &path })?; - - let val = modify(&mut index)?; - - Self::write_index_file(index, &path).context(IndexWriteSnafu { path })?; - - Ok(val) + Ok(()) } +} - fn list_crate_files( - crate_dir: &Path, - ) -> impl Iterator> { - walkdir::WalkDir::new(crate_dir) - .into_iter() - .flat_map(|entry| { - let Ok(entry) = entry else { return Some(entry) }; - - let fname = entry.path().file_name()?; - let fname = Path::new(fname); +/// Rebuild the Margo index by scanning the crate files on disk. +#[derive(Debug, argh::FromArgs)] +#[argh(subcommand, name = "rebuild-index")] +struct RebuildIndex {} - let extension = fname.extension()?; - if extension == "crate" { - Some(Ok(entry)) - } else { - None - } - }) +impl RebuildIndex { + fn handle(self, registry_path: PathBuf) -> Result<()> { + let mut registry = Registry::open(registry_path)?; + registry.refresh_index_from_disk()?; + Ok(()) } +} - fn list_index_files(&self) -> Result, ListIndexFilesError> { - use list_index_files_error::*; - - let crate_dir = self.crate_dir(); - - let index_files = Self::list_crate_files(&crate_dir) - .map(|entry| { - let entry = entry.context(WalkdirSnafu { path: &crate_dir })?; - - let mut path = entry.into_path(); - path.pop(); - - let subdir = path.strip_prefix(&crate_dir).context(PrefixSnafu { - path: &path, - prefix: &crate_dir, - })?; - let index_path = self.path.join(subdir); - Ok(index_path) - }) - .collect::, ListIndexFilesError>>(); - - match index_files { - Err(e) if e.is_not_found() => Ok(Default::default()), - r => r, - } - } - - fn list_all(&self) -> Result { - use list_all_error::*; - - let mut crates = BTreeMap::new(); - - for path in self.list_index_files()? { - let index = Self::parse_index_file(&path).context(ParseSnafu { path })?; - - if let Some(entry) = index.values().next() { - crates.insert(entry.name.clone(), index); - } - } - - Ok(crates) - } - - fn parse_index_file(path: &Path) -> Result { - use parse_index_error::*; - - let index_file = match File::open(path) { - Ok(f) => f, - Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Default::default()), - Err(e) => Err(e).context(OpenSnafu)?, - }; - let index_file = BufReader::new(index_file); - - let mut index = BTreeMap::new(); - - for (i, line) in index_file.lines().enumerate() { - let line = line.context(ReadSnafu { line: i })?; - let entry = - serde_json::from_str::(&line).context(ParseSnafu { line: i })?; - - index.insert(entry.vers.clone(), entry); - } - - Ok(index) - } - - fn write_index_file(index_file: Index, path: &Path) -> Result<(), WriteIndexError> { - use write_index_error::*; - - let file = File::create(path).context(OpenSnafu)?; - let mut file = BufWriter::new(file); - - for entry in index_file.values() { - serde_json::to_writer(&mut file, entry).context(EntrySerializeSnafu)?; - file.write_all(b"\n").context(EntryNewlineSnafu)?; - } +/// Generate an HTML index for the registry +#[derive(Debug, argh::FromArgs)] +#[argh(subcommand, name = "generate-html")] +struct GenerateHtml {} +impl GenerateHtml { + fn handle(self, registry_path: PathBuf) -> Result<()> { + let registry = Registry::open(registry_path)?; + registry.generate_html()?; Ok(()) } - - fn crate_dir(&self) -> PathBuf { - self.path.join(CRATE_DIR_NAME) - } - - #[cfg(test)] - fn margo_config_toml_path(&self) -> PathBuf { - self.path.join(CONFIG_FILE_NAME) - } - - fn config_json_path(&self) -> PathBuf { - self.path.join("config.json") - } - - fn index_file_path_for(&self, name: &CrateName) -> PathBuf { - let mut index_path = self.path.clone(); - name.append_prefix_directories(&mut index_path); - index_path.push(name); - index_path - } - - fn crate_dir_for(&self, name: &CrateName) -> PathBuf { - let mut crate_dir = self.crate_dir(); - name.append_prefix_directories(&mut crate_dir); - crate_dir.push(name); - crate_dir - } - - fn crate_file_path_for(&self, name: &CrateName, version: &Version) -> PathBuf { - let mut crate_file_path = self.crate_dir_for(name); - crate_file_path.push(format!("{}.crate", version)); - crate_file_path - } -} - -#[derive(Debug, Snafu)] -#[snafu(module)] -enum InitializeError { - #[snafu(display("Could not create the registry directory"))] - RegistryCreate { source: io::Error }, - - #[snafu(display("Could not serialize the registry's internal configuration"))] - ConfigTomlSerialize { source: toml::ser::Error }, - - #[snafu(display("Could not write the registry's internal configuration to {}", path.display()))] - ConfigTomlWrite { source: io::Error, path: PathBuf }, - - #[snafu(display("Could not serialize the registry's public configuration"))] - ConfigJsonSerialize { source: serde_json::Error }, - - #[snafu(display("Could not write the registry's public configuration to {}", path.display()))] - ConfigJsonWrite { source: io::Error, path: PathBuf }, -} - -#[derive(Debug, Snafu)] -#[snafu(module)] -enum OpenError { - #[snafu(display("Could not open the registry's internal configuration at {}", path.display()))] - Read { source: io::Error, path: PathBuf }, - - #[snafu(display("Could not deserialize the registry's internal configuration at {}", path.display()))] - Deserialize { - source: toml::de::Error, - path: PathBuf, - }, -} - -impl OpenError { - fn is_not_found(&self) -> bool { - match self { - Self::Read { source, .. } => source.kind() == io::ErrorKind::NotFound, - Self::Deserialize { .. } => false, - } - } -} - -#[derive(Debug, Snafu)] -#[snafu(module)] -enum AddError { - #[snafu(display("Could not read the crate package"))] - ReadCrate { source: io::Error }, - - #[snafu(transparent)] - CargoTomlExtract { source: ExtractRootCargoTomlError }, - - #[snafu(display("The crate package does not contain a Cargo.toml file"))] - CargoTomlMissing, - - #[snafu(display("The crate's Cargo.toml is not valid UTF-8"))] - CargoTomlUtf8 { source: std::string::FromUtf8Error }, - - #[snafu(display("The crate's Cargo.toml is malformed"))] - CargoTomlMalformed { source: toml::de::Error }, - - #[snafu(display("Could not create the crate's index directory {}", path.display()))] - IndexDir { source: io::Error, path: PathBuf }, - - #[snafu(transparent)] - IndexModify { source: ReadModifyWriteError }, - - #[snafu(display("Could not create the crate directory {}", path.display()))] - CrateDir { source: io::Error, path: PathBuf }, - - #[snafu(display("Could not write the crate {}", path.display()))] - CrateWrite { source: io::Error, path: PathBuf }, -} - -#[derive(Debug, Snafu)] -#[snafu(module)] -enum RemoveError { - #[snafu(transparent)] - IndexModify { source: ReadModifyWriteError }, - - #[snafu(display("Could not delete the crate file {}", path.display()))] - Delete { source: io::Error, path: PathBuf }, -} - -#[cfg(feature = "html")] -use html::Error as HtmlError; - -#[cfg(not(feature = "html"))] -#[derive(Debug, Snafu)] -#[snafu(display("Margo was not compiled with the HTML feature enabled. This binary will not be able to generate HTML files"))] -struct HtmlError; - -#[derive(Debug, Snafu)] -#[snafu(module)] -enum YankError { - #[snafu(display("The version does not exist in the index"))] - Version, - - #[snafu(transparent)] - Modify { source: ReadModifyWriteError }, -} - -#[derive(Debug, Snafu)] -#[snafu(module)] -enum ReadModifyWriteError { - #[snafu(display("Could not parse the crate's index file {}", path.display()))] - IndexParse { - source: ParseIndexError, - path: PathBuf, - }, - - #[snafu(display("Could not write the crate's index file {}", path.display()))] - IndexWrite { - source: WriteIndexError, - path: PathBuf, - }, -} - -#[derive(Debug, Snafu)] -#[snafu(module)] -enum ListIndexFilesError { - #[snafu(display("Could not enumerate the crate directory `{}`", path.display()))] - Walkdir { - source: walkdir::Error, - path: PathBuf, - }, - - #[snafu(display( - "Could not remove the path prefix `{prefix}` from the crate package entry `{path}`", - prefix = prefix.display(), - path = path.display(), - ))] - Prefix { - source: std::path::StripPrefixError, - path: PathBuf, - prefix: PathBuf, - }, -} - -impl ListIndexFilesError { - fn is_not_found(&self) -> bool { - if let Self::Walkdir { source, .. } = self { - if let Some(e) = source.io_error() { - if e.kind() == io::ErrorKind::NotFound { - return true; - } - } - } - - false - } -} - -#[derive(Debug, Snafu)] -#[snafu(module)] -enum ListAllError { - #[snafu(display("Unable to list the crate index files"))] - #[snafu(context(false))] - ListIndex { source: ListIndexFilesError }, - - #[snafu(display("Unable to parse the crate index file at `{}`", path.display()))] - Parse { - source: ParseIndexError, - path: PathBuf, - }, -} - -#[derive(Debug, Snafu)] -#[snafu(module)] -enum ParseIndexError { - #[snafu(display("Could not open the file"))] - Open { source: io::Error }, - - #[snafu(display("Could not read line {line}"))] - Read { source: io::Error, line: usize }, - - #[snafu(display("Could not parse line {line}"))] - Parse { - source: serde_json::Error, - line: usize, - }, -} - -#[derive(Debug, Snafu)] -#[snafu(module)] -enum WriteIndexError { - #[snafu(display("Could not open the file"))] - Open { source: io::Error }, - - #[snafu(display("Could not serialize the entry"))] - EntrySerialize { source: serde_json::Error }, - - #[snafu(display("Could not write the entry's newline"))] - EntryNewline { source: io::Error }, -} - -fn extract_root_cargo_toml( - crate_data: &[u8], -) -> Result>, ExtractRootCargoTomlError> { - use extract_root_cargo_toml_error::*; - - let crate_data = flate2::read::GzDecoder::new(crate_data); - let mut crate_data = tar::Archive::new(crate_data); - - let entries = crate_data.entries().context(EntriesSnafu)?; - - let mut dirname = None; - - for entry in entries { - let mut entry = entry.context(EntrySnafu)?; - let path = entry.path().context(PathSnafu)?; - - let dirname = match &mut dirname { - Some(v) => v, - None => { - let Some(Component::Normal(first)) = path.components().next() else { - return MalformedSnafu.fail(); - }; - - dirname.insert(first.to_owned()) - } - }; - - let fname = path.strip_prefix(dirname).context(PrefixSnafu)?; - - if fname == Path::new("Cargo.toml") { - let mut data = vec![]; - entry.read_to_end(&mut data).context(ReadSnafu)?; - return Ok(Some(data)); - } - } - - Ok(None) -} - -#[derive(Debug, Snafu)] -#[snafu(module)] -enum ExtractRootCargoTomlError { - #[snafu(display("Could not get the entries of the crate package"))] - Entries { source: io::Error }, - - #[snafu(display("Could not get the next crate package entry"))] - Entry { source: io::Error }, - - #[snafu(display("Could not get the path of the crate package entry"))] - Path { source: io::Error }, - - #[snafu(display("The crate package was malformed"))] - Malformed, - - #[snafu(display("Could not remove the path prefix from the crate package entry"))] - Prefix { source: std::path::StripPrefixError }, - - #[snafu(display("Could not read the crate package entry for Cargo.toml"))] - Read { source: io::Error }, -} - -fn adapt_cargo_toml_to_index_entry( - global: &Global, - config: &ConfigV1, - mut cargo_toml: cargo_toml::Root, - checksum_hex: String, -) -> index_entry::Root { - // Remove features that refer to dev-dependencies as we don't - // track those anyway. - { - // Ignore dependencies that also occur as a regular or build - // dependency, as we *do* track those. - let reg_dep_names = cargo_toml.dependencies.keys(); - let build_dep_names = cargo_toml.build_dependencies.keys(); - let mut only_dev_dep_names = cargo_toml.dev_dependencies.keys().collect::>(); - for name in reg_dep_names.chain(build_dep_names) { - only_dev_dep_names.remove(name); - } - - for name in only_dev_dep_names { - // We don't care about the official package name here as the - // feature syntax has to match the user-specified dependency - // name. - let prefix = format!("{name}/"); - - for enabled in cargo_toml.features.values_mut() { - enabled.retain(|enable| !enable.starts_with(&prefix)); - } - } - } - - let mut deps: Vec<_> = cargo_toml - .dependencies - .into_iter() - .map(|(name, dep)| adapt_dependency(global, config, dep, name)) - .collect(); - - let build_deps = cargo_toml - .build_dependencies - .into_iter() - .map(|(name, dep)| { - let mut dep = adapt_dependency(global, config, dep, name); - dep.kind = index_entry::DependencyKind::Build; - dep - }); - deps.extend(build_deps); - - for (target, defn) in cargo_toml.target { - let target_deps = defn.dependencies.into_iter().map(|(name, dep)| { - let mut dep = adapt_dependency(global, config, dep, name); - dep.target = Some(target.clone()); - dep - }); - deps.extend(target_deps); - } - - // FUTURE: Opt-in to checking that all dependencies already exist - - index_entry::Root { - name: cargo_toml.package.name, - vers: cargo_toml.package.version, - deps, - cksum: checksum_hex, - features: cargo_toml.features, - yanked: false, - links: cargo_toml.package.links, - v: 2, - features2: Default::default(), - rust_version: cargo_toml.package.rust_version, - } -} - -fn adapt_dependency( - global: &Global, - config: &ConfigV1, - dep: cargo_toml::Dependency, - name: String, -) -> index_entry::Dependency { - let cargo_toml::Dependency { - version, - features, - optional, - default_features, - registry_index, - package, - } = dep; - - index_entry::Dependency { - name, - req: version, - features, - optional, - default_features, - target: None, - kind: index_entry::DependencyKind::Normal, - registry: adapt_index(global, config, registry_index), - package, - } -} - -fn adapt_index(global: &Global, config: &ConfigV1, registry_index: Option) -> Option { - // The dependency is in... - match registry_index { - // ...crates.io - None => Some(global.crates_io_index_url.clone()), - - // ...this registry - Some(url) if url == config.base_url => None, - - // ...another registry - r => r, - } -} - -/// Only intended for the normalized Cargo.toml created for the -/// packaged crate. -mod cargo_toml { - use semver::{Version, VersionReq}; - use serde::Deserialize; - use std::collections::BTreeMap; - use url::Url; - - use crate::common::{CrateName, RustVersion}; - - pub type Dependencies = BTreeMap; - - #[derive(Debug, Deserialize)] - #[serde(rename_all = "kebab-case")] - pub struct Root { - pub package: Package, - - #[serde(default)] - pub features: BTreeMap>, - - #[serde(default)] - pub dependencies: Dependencies, - - #[serde(default)] - pub build_dependencies: Dependencies, - - #[serde(default)] - pub dev_dependencies: Dependencies, - - #[serde(default)] - pub target: BTreeMap, - } - - #[derive(Debug, Deserialize)] - #[serde(rename_all = "kebab-case")] - pub struct Package { - pub name: CrateName, - - pub version: Version, - - #[serde(default)] - pub links: Option, - - #[serde(default)] - pub rust_version: Option, - } - - #[derive(Debug, Deserialize)] - #[serde(rename_all = "kebab-case")] - pub struct Dependency { - pub version: VersionReq, - - #[serde(default)] - pub features: Vec, - - #[serde(default)] - pub optional: bool, - - #[serde(default = "true_def")] - pub default_features: bool, - - #[serde(default)] - pub registry_index: Option, - - #[serde(default)] - pub package: Option, - } - - #[derive(Debug, Deserialize)] - pub struct Target { - #[serde(default)] - pub dependencies: Dependencies, - } - - fn true_def() -> bool { - true - } -} - -const CONFIG_FILE_NAME: &str = "margo-config.toml"; -const CRATE_DIR_NAME: &str = "crates"; - -const CRATES_IO_INDEX_URL: &str = "https://github.com/rust-lang/crates.io-index"; - -#[derive(Debug)] -struct Global { - crates_io_index_url: Url, -} - -impl Global { - fn new() -> Result { - use global_error::*; - - Ok(Self { - crates_io_index_url: CRATES_IO_INDEX_URL.parse().context(CratesIoIndexUrlSnafu)?, - }) - } -} - -#[derive(Debug, Snafu)] -#[snafu(module)] -enum GlobalError { - #[snafu(display("Could not parse the crates.io index URL"))] - CratesIoIndexUrl { source: url::ParseError }, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "version")] -enum Config { - #[serde(rename = "1")] - V1(ConfigV1), -} - -#[derive(Debug, Serialize, Deserialize)] -struct ConfigV1 { - base_url: Url, - - #[serde(default)] - auth_required: bool, - - #[serde(default)] - html: ConfigV1Html, -} - -impl ConfigV1 { - const USER_DEFAULT_AUTH_REQUIRED: bool = false; - - fn normalize(mut self) -> ConfigV1 { - ensure_last_segment_empty(&mut self.base_url); - - self - } -} - -fn ensure_last_segment_empty(url: &mut Url) { - if let Ok(mut s) = url.path_segments_mut() { - s.pop_if_empty().push(""); - } -} - -#[derive(Debug, Default, Serialize, Deserialize)] -struct ConfigV1Html { - #[serde(default)] - enabled: bool, - #[serde(default)] - suggested_registry_name: Option, -} - -impl ConfigV1Html { - const USER_DEFAULT_ENABLED: bool = true; - const USER_DEFAULT_SUGGESTED_REGISTRY_NAME: &'static str = "my-awesome-registry"; - - fn suggested_registry_name(&self) -> &str { - self.suggested_registry_name - .as_deref() - .unwrap_or(Self::USER_DEFAULT_SUGGESTED_REGISTRY_NAME) - } -} - -mod config_json { - use serde::Serialize; - - #[derive(Debug, Serialize)] - #[serde(rename_all = "kebab-case")] - pub struct Root { - // This field cannot be a `url::Url` because that type - // percent-escapes the `{` and `}` characters. Cargo performs - // string-replacement on this value based on those literal `{` - // and `}` characters. - pub dl: String, - - pub api: Option, // Modified - - /// A private registry requires all operations to be authenticated. - /// - /// This includes API requests, crate downloads and sparse - /// index updates. - pub auth_required: bool, - } -} - -mod index_entry { - use semver::{Version, VersionReq}; - use serde::{Deserialize, Serialize}; - use std::collections::BTreeMap; - use url::Url; - - use crate::common::{CrateName, RustVersion}; - - #[derive(Debug, Serialize, Deserialize)] - pub struct Root { - /// The name of the package. - pub name: CrateName, - - /// The version of the package this row is describing. - /// - /// This must be a valid version number according to the - /// Semantic Versioning 2.0.0 spec at https://semver.org/. - pub vers: Version, - - /// Direct dependencies of the package. - pub deps: Vec, - - /// A SHA256 checksum of the `.crate` file. - pub cksum: String, - - /// Set of features defined for the package. - /// - /// Each feature maps to features or dependencies it enables. - pub features: BTreeMap>, - - /// Boolean of whether or not this version has been yanked. - pub yanked: bool, - - /// The `links` value from the package's manifest. - #[serde(skip_serializing_if = "Option::is_none")] - pub links: Option, - - /// The schema version of this entry. - // - /// If this not specified, it should be interpreted as the default of 1. - // - /// Cargo (starting with version 1.51) will ignore versions it does not - /// recognize. This provides a method to safely introduce changes to index - /// entries and allow older versions of cargo to ignore newer entries it - /// doesn't understand. Versions older than 1.51 ignore this field, and - /// thus may misinterpret the meaning of the index entry. - // - /// The current values are: - // - /// * 1: The schema as documented here, not including newer additions. - /// This is honored in Rust version 1.51 and newer. - /// * 2: The addition of the `features2` field. - /// This is honored in Rust version 1.60 and newer. - pub v: u32, - - /// Features with new, extended syntax, such as namespaced - /// features (`dep:`) and weak dependencies (`pkg?/feat`). - // - /// This is separated from `features` because versions older than 1.19 - /// will fail to load due to not being able to parse the new syntax, even - /// with a `Cargo.lock` file. - // - /// Cargo will merge any values listed here with the "features" field. - // - /// If this field is included, the "v" field should be set to at least 2. - // - /// Registries are not required to use this field for extended feature - /// syntax, they are allowed to include those in the "features" field. - /// Using this is only necessary if the registry wants to support cargo - /// versions older than 1.19, which in practice is only crates.io since - /// those older versions do not support other registries. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub features2: BTreeMap>, - - /// The minimal supported Rust version - /// - /// This must be a valid version requirement without an operator (e.g. no `=`) - #[serde(skip_serializing_if = "Option::is_none")] - pub rust_version: Option, - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct Dependency { - /// Name of the dependency. - /// - /// If the dependency is renamed from the original package - /// name, this is the new name. The original package name is - /// stored in the `package` field. - pub name: String, - - /// The SemVer requirement for this dependency. - /// - /// This must be a valid version requirement defined at - /// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html. - pub req: VersionReq, - - /// Features enabled for this dependency. - pub features: Vec, - - /// Whether or not this is an optional dependency. - pub optional: bool, - - /// Whether or not default features are enabled. - pub default_features: bool, - - /// The target platform for the dependency. - /// - /// A string such as `cfg(windows)`. - #[serde(skip_serializing_if = "Option::is_none")] - pub target: Option, - - /// The dependency kind. - /// - /// Note: this is a required field, but a small number of entries - /// exist in the crates.io index with either a missing or null - /// `kind` field due to implementation bugs. - pub kind: DependencyKind, - - /// The URL of the index of the registry where this dependency - /// is from. - /// - /// If not specified or null, it is assumed the dependency is - /// in the current registry. - #[serde(skip_serializing_if = "Option::is_none")] - pub registry: Option, - - /// If the dependency is renamed, this is the actual package - /// name. - /// - /// If not specified or null, this dependency is not renamed. - #[serde(skip_serializing_if = "Option::is_none")] - pub package: Option, - } - - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "snake_case")] - pub enum DependencyKind { - #[allow(unused)] - // Stored in the index, but not actually used by Cargo - Dev, - Build, - Normal, - } -} - -mod common { - use ascii::{AsciiChar, AsciiStr, AsciiString}; - use semver::Version; - use serde::{de::Error, Deserialize, Serialize}; - use snafu::prelude::*; - use std::{ - borrow::Cow, - fmt, ops, - path::{Path, PathBuf}, - str::FromStr, - }; - - /// Contains only alphanumeric, `-`, or `_` characters. - #[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)] - pub struct CrateName(AsciiString); - - impl CrateName { - pub fn as_str(&self) -> &str { - self.0.as_str() - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn append_prefix_directories(&self, index_path: &mut PathBuf) { - match self.len() { - 0 => unreachable!(), - 1 => index_path.push("1"), - 2 => index_path.push("2"), - 3 => { - let a = &self[0..1]; - - index_path.push("3"); - index_path.push(a.as_str()); - } - _ => { - let ab = &self[0..2]; - let cd = &self[2..4]; - - index_path.push(ab.as_str()); - index_path.push(cd.as_str()); - } - }; - } - } - - impl fmt::Display for CrateName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } - } - - impl FromStr for CrateName { - type Err = CrateNameError; - - fn from_str(s: &str) -> Result { - s.try_into() - } - } - - impl TryFrom<&str> for CrateName { - type Error = CrateNameError; - - fn try_from(value: &str) -> Result { - value.to_owned().try_into() - } - } - - impl TryFrom for CrateName { - type Error = CrateNameError; - - fn try_from(value: String) -> Result { - AsciiString::from_ascii(value) - .map_err(|e| e.ascii_error())? - .try_into() - } - } - - impl TryFrom for CrateName { - type Error = CrateNameError; - - fn try_from(value: AsciiString) -> Result { - use crate_name_error::*; - - let first = value.first().context(EmptySnafu)?; - ensure!(first.is_alphabetic(), InitialAlphaSnafu); - - if let Some(chr) = value.chars().find(|&chr| !valid_crate_name_char(chr)) { - return ContainsInvalidCharSnafu { chr }.fail(); - } - - Ok(Self(value)) - } - } - - #[derive(Debug, Snafu)] - #[snafu(module)] - pub enum CrateNameError { - #[snafu(display("The crate name cannot be empty"))] - Empty, - - #[snafu(display("The crate name must start with an alphabetic character"))] - InitialAlpha, - - #[snafu(display("The crate name must only contain alphanumeric characters, hyphen (-) or underscore (_), not {chr}"))] - ContainsInvalidChar { chr: char }, - - #[snafu(transparent)] - NotAscii { source: ascii::AsAsciiStrError }, - } - - impl<'de> Deserialize<'de> for CrateName { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let ascii: AsciiString = Deserialize::deserialize(deserializer)?; - Self::try_from(ascii).map_err(D::Error::custom) - } - } - - impl ops::Index> for CrateName { - type Output = AsciiStr; - - fn index(&self, index: ops::Range) -> &Self::Output { - self.0.index(index) - } - } - - impl AsRef for CrateName { - fn as_ref(&self) -> &Path { - self.0.as_str().as_ref() - } - } - - fn valid_crate_name_char(chr: AsciiChar) -> bool { - chr.is_alphanumeric() || chr == AsciiChar::UnderScore || chr == AsciiChar::Minus - } - - #[derive(Debug)] - pub struct RustVersion(Version); - - impl FromStr for RustVersion { - type Err = RustVersionError; - - fn from_str(s: &str) -> Result { - use rust_version_error::*; - - let v: Version = match s.parse() { - Ok(v) => v, - Err(e) => { - let version = [s, ".0"].concat(); - match version.parse() { - Ok(v) => v, - Err(_) => return Err(e)?, - } - } - }; - - ensure!(v.pre.is_empty(), PrereleaseSnafu); - ensure!(v.build.is_empty(), BuildSnafu); - - Ok(Self(v)) - } - } - - #[derive(Debug, Snafu)] - #[snafu(module)] - pub enum RustVersionError { - #[snafu(transparent)] - Semver { source: semver::Error }, - - #[snafu(display("May not specify a prerelease version"))] - Prerelease, - - #[snafu(display("May not specify a version with build metadata"))] - Build, - } - - impl From for Version { - fn from(value: RustVersion) -> Self { - value.0 - } - } - - impl<'de> serde::Deserialize<'de> for RustVersion { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let version = Cow::::deserialize(deserializer)?; - version.parse().map_err(D::Error::custom) - } - } - - impl serde::Serialize for RustVersion { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - self.0.serialize(serializer) - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use registry_conformance::{Crate, ScratchSpace}; - - fn default_config() -> ConfigV1 { - ConfigV1 { - base_url: "http://example.com".parse().unwrap(), - auth_required: false, - html: ConfigV1Html { - enabled: false, - suggested_registry_name: None, - }, - } - } - - #[tokio::test] - async fn adding_duplicate_crate() { - let global = Global::new().unwrap(); - let scratch = ScratchSpace::new().await.unwrap(); - - let config = default_config(); - - let r = Registry::initialize(config, scratch.registry()).unwrap(); - - let c = Crate::new("duplicated", "1.0.0") - .lib_rs(r#"pub const ID: u8 = 1;"#) - .create_in(&scratch) - .await - .unwrap(); - let p = c.package().await.unwrap(); - - r.add(&global, &p).unwrap(); - r.add(&global, &p).unwrap(); - - let name = CrateName::try_from(c.name()).unwrap(); - let index_file_path = r.index_file_path_for(&name); - let index_contents = fs::read_to_string(index_file_path).unwrap(); - - assert_eq!(1, index_contents.lines().count()); - } - - #[tokio::test] - async fn base_url_requires_trailing_slash() { - let scratch = ScratchSpace::new().await.unwrap(); - - let config = ConfigV1 { - base_url: "http://example.com/path/to/index".parse().unwrap(), - ..default_config() - }; - - let r = Registry::initialize(config, scratch.registry()).unwrap(); - - let paths = [r.config_json_path(), r.margo_config_toml_path()]; - - for path in paths { - let contents = fs::read_to_string(&path).unwrap(); - - assert!( - contents.contains("/path/to/index/"), - "{path} does not have the trailing slash:\n{contents}", - path = path.display(), - ); - } - } - - #[tokio::test] - async fn removing_a_crate_deletes_from_disk() { - let global = Global::new().unwrap(); - let scratch = ScratchSpace::new().await.unwrap(); - - let config = default_config(); - - let r = Registry::initialize(config, scratch.registry()).unwrap(); - - let name = "to-go-away"; - let version = "1.0.0"; - - let c = Crate::new(name, version) - .lib_rs(r#"pub const ID: u8 = 1;"#) - .create_in(&scratch) - .await - .unwrap(); - let p = c.package().await.unwrap(); - - let name = name.parse().unwrap(); - let version = version.parse().unwrap(); - let crate_path = r.crate_file_path_for(&name, &version); - - r.add(&global, p).unwrap(); - - assert!( - crate_path.exists(), - "The crate file should be in the registry at {}", - crate_path.display(), - ); - - r.remove(name, version).unwrap(); - - assert!( - !crate_path.exists(), - "The crate file should not be in the registry at {}", - crate_path.display(), - ); - } } diff --git a/src/margo_config/mod.rs b/src/margo_config/mod.rs new file mode 100644 index 0000000..d0bdb9e --- /dev/null +++ b/src/margo_config/mod.rs @@ -0,0 +1,53 @@ +mod v1; +mod v2; + +use serde::{Deserialize, Serialize}; +use std::str; + +use self::v1::ConfigV1; +pub use self::v2::ConfigV2 as LatestConfig; +pub use self::v2::ConfigV2Html as LatestConfigIndex; + +/// Supported margo_config versions for backwards compatibility. +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "version")] +pub enum MargoConfig { + #[serde(rename = "1")] + V1(ConfigV1), + #[serde(rename = "2")] + V2(LatestConfig), +} + +impl MargoConfig { + /// Converts the loaded configuration file into the latest version. + /// + /// The returned margo_config is normalised. + pub fn into_latest(self) -> LatestConfig { + match self { + MargoConfig::V1(c) => LatestConfig::from(c).normalised(), + MargoConfig::V2(c) => c.normalised(), + } + } + + /// Returns a new Config enum containing the converted configuration using [Self::into_latest]. + pub fn with_latest(self) -> Self { + Self::V2(self.into_latest()) + } + + /// True if the contained config is the latest version. + pub fn is_latest(&self) -> bool { + matches!(self, MargoConfig::V2(_)) + } +} + +impl Default for MargoConfig { + fn default() -> Self { + MargoConfig::V1(ConfigV1::default()).with_latest() + } +} + +impl From for MargoConfig { + fn from(config: LatestConfig) -> Self { + MargoConfig::V2(config) + } +} diff --git a/src/margo_config/v1.rs b/src/margo_config/v1.rs new file mode 100644 index 0000000..d90da21 --- /dev/null +++ b/src/margo_config/v1.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; +use std::str; +use url::Url; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ConfigV1 { + pub base_url: Url, + + #[serde(default)] + pub auth_required: bool, + + #[serde(default)] + pub html: ConfigV1Html, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct ConfigV1Html { + #[serde(default)] + pub enabled: Option, + #[serde(default)] + pub suggested_registry_name: Option, +} + +impl ConfigV1Html { + pub const USER_DEFAULT_SUGGESTED_REGISTRY_NAME: &'static str = "my-awesome-registry"; +} + +impl Default for ConfigV1 { + fn default() -> Self { + ConfigV1 { + base_url: Url::parse("http://example.com").unwrap(), + auth_required: false, + html: ConfigV1Html { + enabled: Some(true), + suggested_registry_name: Some( + ConfigV1Html::USER_DEFAULT_SUGGESTED_REGISTRY_NAME.into(), + ), + }, + } + } +} diff --git a/src/margo_config/v2.rs b/src/margo_config/v2.rs new file mode 100644 index 0000000..18a3a9c --- /dev/null +++ b/src/margo_config/v2.rs @@ -0,0 +1,59 @@ +use crate::template_reference::{BuiltInTemplate, TemplateReference}; +use crate::util::UrlExt; +use serde::{Deserialize, Serialize}; +use std::str; +use url::Url; + +use super::v1::{ConfigV1, ConfigV1Html}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ConfigV2 { + /// The public URL that the registry is hosted at. + pub base_url: Url, + + /// True if authentication is required to download crates (i.e. this is a private registry). + pub auth_required: bool, + + /// HTML rendering configuration, or None if no html pages should be rendered. + pub html: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ConfigV2Html { + /// Template to use when rendering the index page. + pub template: TemplateReference, + + /// Page title to display. + pub title: String, + + /// Suggested name for the registry. + pub suggested_registry_name: String, +} + +impl ConfigV2 { + pub fn normalised(mut self) -> Self { + self.base_url.ensure_trailing_slash(); + self + } +} + +impl From for ConfigV2 { + fn from(v1: ConfigV1) -> Self { + ConfigV2 { + base_url: v1.base_url, + auth_required: v1.auth_required, + html: match v1.html.enabled { + Some(true) => Some(ConfigV2Html { + template: TemplateReference::BuiltIn(BuiltInTemplate::Classic), + title: "Margo Crate Registry".into(), + suggested_registry_name: v1 + .html + .suggested_registry_name + .unwrap_or(ConfigV1Html::USER_DEFAULT_SUGGESTED_REGISTRY_NAME.to_string()), + }), + _ => None, + }, + } + .normalised() + } +} diff --git a/src/registry/error.rs b/src/registry/error.rs new file mode 100644 index 0000000..48d5a08 --- /dev/null +++ b/src/registry/error.rs @@ -0,0 +1,86 @@ +// use snafu::Snafu; +// use std::backtrace::Backtrace; +// use std::io; +// use std::path::PathBuf; +// +// #[derive(Debug, Snafu)] +// #[snafu(visibility(pub))] +// pub enum RegistryError { +// #[snafu(display("Couldn't create directory {}.", path.display()))] +// CreateDir { source: io::Error, path: PathBuf }, +// +// #[snafu(display("Missing Margo configuration file."))] +// MissingMargoConfig { source: io::Error }, +// +// #[snafu(display("Error while parsing the Margo configuration file."))] +// ParseMargoConfig { source: toml::de::Error }, +// +// #[snafu(display("Error while serialising the Margo configuration."))] +// SerialiseMargoConfig { source: toml::ser::Error }, +// +// #[snafu(display("Error while saving the Margo configuration file."))] +// WriteMargoConfig { source: io::Error }, +// +// #[snafu(display("Error while serialising the registry config.json file."))] +// SerialiseConfigJson { source: serde_json::Error }, +// +// #[snafu(display("Error while saving the registry config.json file."))] +// WriteConfigJson { source: io::Error }, +// +// #[snafu(display("Error while reading the index file at {}.", path.display()))] +// ReadIndex { source: io::Error, path: PathBuf }, +// +// #[snafu(display("Error while parsing the index file at {} (line {}).", path.display(), line))] +// ParseIndex { +// source: serde_json::Error, +// path: PathBuf, +// line: usize, +// }, +// +// #[snafu(display("Couldn't modify the index at {} for version {}", path.display(), version))] +// ModifyIndex { +// path: PathBuf, +// version: String, +// backtrace: Backtrace, +// }, +// +// #[snafu(display("Error while serialising the index file at {} (line {}).", path.display(), line))] +// SerialiseIndex { +// source: serde_json::Error, +// path: PathBuf, +// line: usize, +// }, +// +// #[snafu(display("Error while saving the index file at {}.", path.display()))] +// SaveIndex { source: io::Error, path: PathBuf }, +// +// #[snafu(display("Couldn't find or open the crate package {}.", path.display()))] +// ReadCrate { source: io::Error, path: PathBuf }, +// +// #[snafu(display("Error while trying to read the contents of the crate package {}.", path.display()))] +// ParseCrate { source: io::Error, path: PathBuf }, +// +// #[snafu(display("The crate package {} is invalid. Make sure it contains at least a Cargo.toml.", path.display()))] +// InvalidCrate { path: PathBuf, backtrace: Backtrace }, +// +// #[snafu(display("Error while parsing the Cargo.toml inside the crate package {}.", path.display()))] +// ParseCrateToml { +// source: toml::de::Error, +// path: PathBuf, +// }, +// +// #[snafu(display("Missing or invalid data in the Cargo.toml inside the crate package {}.", path.display()))] +// InvalidCrateToml { path: PathBuf, backtrace: Backtrace }, +// +// #[snafu(display("Error while writing the crate package to {}.", path.display()))] +// WriteCrate { source: io::Error, path: PathBuf }, +// +// #[snafu(display("Error while deleting the file at {}.", path.display()))] +// DeleteFile { source: io::Error, path: PathBuf }, +// +// #[snafu(display("Invalid version."))] +// InvalidVersion { source: semver::Error }, +// +// #[snafu(display("Invalid URL."))] +// InvalidUrl { source: url::ParseError }, +// } diff --git a/src/registry/index.rs b/src/registry/index.rs new file mode 100644 index 0000000..603b095 --- /dev/null +++ b/src/registry/index.rs @@ -0,0 +1,274 @@ +use super::Registry; +use crate::prelude::*; +use cargo_util_schemas::manifest::{FeatureName, PackageName, RustVersion}; +use colored::Colorize; +use semver::{Version, VersionReq}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::fs::File; +use std::io::{BufRead, BufReader, BufWriter, Write}; +use std::path::PathBuf; +use std::{fs, io}; +use url::Url; + +/// A registry index file, representing a series of version entries relating to a crate. +pub struct Index { + pub name: PackageName, + pub path: PathBuf, + pub entries: BTreeMap, +} + +impl Index { + pub fn latest_non_yanked_version(&self) -> Option<&Version> { + self.entries.values().rfind(|e| !e.yanked).map(|e| &e.vers) + } + + pub fn set_yanked(&mut self, version: &Version, yanked: bool) -> Result<()> { + let entry = self.entries.get_mut(version).context(format!( + "Version {version} doesn't exist in the index for this crate.", + version = version.to_string(), + ))?; + + entry.yanked = yanked; + + Ok(()) + } + + pub fn add(&mut self, entry: IndexEntry) { + self.entries.insert(entry.vers.clone(), entry); + } + + pub fn remove(&mut self, version: &Version) { + if self.entries.remove(version).is_none() { + println!( + "Version {version} doesn't exist in index. Nothing changed.", + version = version.to_string().magenta().bold(), + ); + } + } + + pub fn contains_version(&self, version: &Version) -> bool { + self.entries.contains_key(version) + } + + /// Open the index file for a package, or create a new empty index if + /// no index file exists for the given package name. + pub fn open_or_new(name: PackageName, registry: &Registry) -> Result { + println!("Opening index for {name}", name = name.cyan()); + + fs::create_dir_all(®istry.index_dir_for(&name))?; + let path = registry.index_file_path_for(&name); + + let index_file = match File::open(&path) { + Ok(f) => BufReader::new(f), + // If the index file doesn't exist, return an empty index - no further parsing necessary. + Err(e) if e.kind() == io::ErrorKind::NotFound => { + return Ok(Self { + name, + path, + entries: BTreeMap::new(), + }) + } + Err(e) => Err(e).context(format!( + "Can't open index file for {name} ({path}).", + path = path.display() + ))?, + }; + + let mut entries = BTreeMap::new(); + + for (line, json) in index_file.lines().enumerate() { + let json = json.context(format!( + "Can't read next line #{line} in index file for {name} ({path}).", + path = path.display() + ))?; + let entry = serde_json::from_str::(&json).context(format!( + "Invalid JSON on line #{line} in index for {name} ({path}).", + path = path.display() + ))?; + + entries.insert(entry.vers.clone(), entry); + } + + Ok(Self { + name, + path, + entries, + }) + } + + /// Write the entries of this index file to disk. + /// + /// **Caution**: this will replace any existing contents of the index file. + pub fn save(&self) -> Result<()> { + println!("Saving index for {name}", name = self.name.cyan()); + + let path = &self.path; + + if self.entries.is_empty() { + if path.exists() { + fs::remove_file(path).context(format!( + "Can't delete empty index file at {}.", + path.display() + ))?; + } + path.remove_dirs_if_empty()?; + } else { + let file = File::create(path) + .context(format!("Can't create index file at {}.", path.display()))?; + let mut file = BufWriter::new(file); + + for (line, entry) in self.entries.values().enumerate() { + serde_json::to_writer(&mut file, entry).context(format!( + "Can't write line #{line} to index file at {}.", + path.display() + ))?; + file.write_all(b"\n").context(format!( + "Can't write EOL at line #{line} to index file at {}.", + path.display() + ))?; + } + + file.flush().context("Can't write index file.")?; + println!("Wrote crate index to `{}`", path.display()); + } + + Ok(()) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct IndexEntry { + /// The name of the package. + pub name: PackageName, + + /// The version of the package this entry is describing. + /// + /// This must be a valid version according to the + /// Semantic Versioning 2.0.0 spec at https://semver.org/. + pub vers: Version, + + /// Direct dependencies of the package. + pub deps: Vec, + + /// A SHA256 checksum of the `.crate` file. + pub cksum: String, + + /// Set of features defined for the package. + /// + /// Each feature maps to features or dependencies it enables. + pub features: BTreeMap>, + + /// Boolean of whether this version has been yanked. + pub yanked: bool, + + /// The `links` value from the package's manifest. + #[serde(skip_serializing_if = "Option::is_none")] + pub links: Option, + + /// The schema version of this entry. + /// + /// If this is not specified, it should be interpreted as the default of 1. + /// + /// Cargo (starting with version 1.51) will ignore versions it does not + /// recognise. This provides a method to safely introduce changes to index + /// entries and allow older versions of cargo to ignore newer entries it + /// doesn't understand. Versions older than 1.51 ignore this field, and + /// thus may misinterpret the meaning of the index entry. + /// + /// The current values are: + /// + /// * 1: The schema as documented here, not including newer additions. + /// This is honoured in Rust version 1.51 and newer. + /// * 2: The addition of the `features2` field. + /// This is honoured in Rust version 1.60 and newer. + pub v: u32, + + /// Features with new, extended syntax, such as namespaced + /// features (`dep:`) and weak dependencies (`pkg?/feat`). + /// + /// This is separated from `features` because versions older than 1.19 + /// will fail to load due to not being able to parse the new syntax, even + /// with a `Cargo.lock` file. + /// + /// Cargo will merge any values listed here with the "features" field. + /// + /// If this field is included, the "v" field should be set to at least 2. + /// + /// Registries are not required to use this field for extended feature + /// syntax, they are allowed to include those in the "features" field. + /// Using this is only necessary if the registry wants to support cargo + /// versions older than 1.19, which in practice is only crates.io since + /// those older versions do not support other registries. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub features2: BTreeMap>, + + /// The minimal supported Rust version + /// + /// This must be a valid version requirement without an operator (e.g. no `=`) + #[serde(skip_serializing_if = "Option::is_none")] + pub rust_version: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Dependency { + /// Name of the dependency. + /// + /// If the dependency is renamed from the original package + /// name, this is the new name. The original package name is + /// stored in the `package` field. + pub name: PackageName, + + /// The SemVer requirement for this dependency. + /// + /// This must be a valid version requirement defined at + /// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html. + pub req: VersionReq, + + /// Features enabled for this dependency. + pub features: Vec, + + /// Whether this is an optional dependency. + pub optional: bool, + + /// Whether default features are enabled. + pub default_features: bool, + + /// The target platform for the dependency. + /// + /// A string such as `cfg(windows)`. + #[serde(skip_serializing_if = "Option::is_none")] + pub target: Option, + + /// The dependency kind. + /// + /// Note: this is a required field, but a small number of entries + /// exist in the crates.io index with either a missing or null + /// `kind` field due to implementation bugs. + pub kind: DependencyKind, + + /// The URL of the index of the registry where this dependency + /// is from. + /// + /// If not specified or null, it is assumed the dependency is + /// in the current registry. + #[serde(skip_serializing_if = "Option::is_none")] + pub registry: Option, + + /// If the dependency is renamed, this is the actual package + /// name. + /// + /// If not specified or null, this dependency is not renamed. + #[serde(skip_serializing_if = "Option::is_none")] + pub package: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DependencyKind { + #[allow(unused)] + // Stored in the index but not used by Cargo + Dev, + Build, + Normal, +} diff --git a/src/registry/mod.rs b/src/registry/mod.rs new file mode 100644 index 0000000..75b0cf3 --- /dev/null +++ b/src/registry/mod.rs @@ -0,0 +1,10 @@ +mod error; +mod index; +mod packaged_crate; +mod registry; +mod packaged_cargo_toml; + +pub use index::Index; +pub use packaged_crate::PackagedCrate; +pub use registry::{Registry, MARGO_CONFIG_FILE_NAME}; +pub use packaged_cargo_toml::PackagedCargoToml; \ No newline at end of file diff --git a/src/registry/packaged_cargo_toml.rs b/src/registry/packaged_cargo_toml.rs new file mode 100644 index 0000000..c0c7557 --- /dev/null +++ b/src/registry/packaged_cargo_toml.rs @@ -0,0 +1,66 @@ +use cargo_util_schemas::manifest::{FeatureName, PackageName, RustVersion}; +use semver::{Version, VersionReq}; +use serde::Deserialize; +use std::collections::BTreeMap; +use url::Url; + +pub type DependencyMap = BTreeMap; + +/// A normalised Cargo.toml manifest in a packaged .crate file. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct PackagedCargoToml { + pub package: Package, + + #[serde(default)] + pub features: BTreeMap>, + + #[serde(default)] + pub dependencies: DependencyMap, + + #[serde(default)] + pub build_dependencies: DependencyMap, + + #[serde(default)] + pub dev_dependencies: DependencyMap, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Package { + pub name: PackageName, + + pub version: Version, + + #[serde(default)] + pub description: Option, + + #[serde(default)] + pub rust_version: Option, + + #[serde(default)] + pub repository: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Dependency { + pub version: VersionReq, + + #[serde(default)] + pub features: Vec, + + #[serde(default)] + pub optional: bool, + + #[serde(default = "default_features")] + pub default_features: bool, + + #[serde(default)] + pub registry_index: Option, +} + +/// Default value for the `default_features` field in [Dependency]. +fn default_features() -> bool { + true +} diff --git a/src/registry/packaged_crate.rs b/src/registry/packaged_crate.rs new file mode 100644 index 0000000..507a464 --- /dev/null +++ b/src/registry/packaged_crate.rs @@ -0,0 +1,184 @@ +use super::index::{Dependency, DependencyKind, IndexEntry}; +use crate::margo_config::LatestConfig; +use crate::prelude::*; +use crate::registry::packaged_cargo_toml::PackagedCargoToml; +use lazy_static::lazy_static; +use sha2::Digest; +use std::collections::BTreeMap; +use std::fs; +use std::io::Read; +use std::path::{Path, PathBuf}; +use url::Url; + +lazy_static! { + /// The global crates.io index. + pub static ref CRATES_IO_INDEX_URL: Url = "https://github.com/rust-lang/crates.io-index" + .parse() + .expect("Invalid crates.io index URL"); +} + +/// Information about a packaged .crate file. +pub struct PackagedCrate { + /// Contents of the .crate file. + contents: Vec, + + /// Checksum representing the .crate file. + checksum: String, + + /// Contents of the readme.md file, if one is present. + pub readme: Option, + + /// File paths contained in the .crate file. + pub files: Vec, + + /// Root Cargo.toml manifest inside the package. + pub manifest: PackagedCargoToml, +} + +impl PackagedCrate { + /// Open a .crate file at a given path and collect its metadata. + pub fn open(path: &Path) -> Result { + let contents = + fs::read(path).context(format!("Can't read .crate file at {}.", path.display()))?; + + let checksum = sha2::Sha256::digest(&contents); + let checksum = hex::encode(checksum); + + // Read the files in the archive to populate the files set and extract the cargo.toml. + let crate_archive = flate2::read::GzDecoder::new(&contents[..]); + let mut crate_archive = tar::Archive::new(crate_archive); + + // Variables to collect the data we need while going through the entries in the archive. + let mut files = Vec::new(); + let mut manifest = None; + let mut readme = None; + + let entries = crate_archive + .entries() + .context("Can't read the contents of the crate.")?; + + for entry in entries { + let mut entry = entry?; + let entry_path = entry.path()?; + + // Path.is_file() doesn't seem to work accurately inside a tarball archive but if + // the entry has an extension it's probably a file. + if entry_path.extension().is_some() { + files.push(entry_path.to_path_buf()); + } + + // Cargo packages the crate in a root dir inside the .crate archive, + // so to determine whether an entry is in the root dir, it should have + // at most one parent directory. + let is_in_root_dir = entry_path + .parent() + .and_then(|p| p.parent()) + .and_then(|p| p.to_str()) + .map(|p| p == "") + .unwrap_or(false); + + if !is_in_root_dir { + continue; + } + + // Collect the Cargo.toml manifest when we encounter it. + if entry_path.ends_with("Cargo.toml") { + let mut data = String::with_capacity(entry.size() as usize); + entry + .read_to_string(&mut data) + .context("Can't read the Cargo.toml file from the crate.")?; + + manifest = Some( + toml::from_str::(&data) + .context("Can't parse the Cargo.toml in the crate.")?, + ); + } else if entry_path.ends_with("README.md") { + let mut data = String::with_capacity(entry.size() as usize); + entry + .read_to_string(&mut data) + .context("Can't read the README.md file from the crate.")?; + + readme = Some(data); + } + } + + let manifest = manifest.context("Can't find a Cargo.toml file in the crate.")?; + + Ok(PackagedCrate { + contents, + checksum, + manifest, + readme, + files, + }) + } + + /// Format the metadata into a valid index entry structure. + /// + /// Also returns the contents of the crate file, so it can be written to disk along with the index. + pub fn into_index_entry(mut self, config: &LatestConfig) -> Result<(IndexEntry, Vec)> { + // Collect all regular and build dependencies, but not dev dependencies since those are + // irrelevant when installing a crate from a registry. + let dependencies = [self.manifest.build_dependencies, self.manifest.dependencies] + .into_iter() + .flatten() + .collect::>(); + + // Remove features that only refer to dev dependencies + // Find all dev-only dependency names + let dev_deps = self + .manifest + .dev_dependencies + .into_keys() + .filter(|name| !dependencies.contains_key(name)) + // We don't care about the official package name here as the feature syntax + // has to match the user-specified dependency name. + .map(|name| format!("{name}/")); + + for prefix in dev_deps { + for val in self.manifest.features.values_mut() { + val.retain(|v| !v.starts_with(&prefix)); + } + } + + // Map the dependency information into the expected index format. + let deps = dependencies + .into_iter() + .map(|(name, dep)| Dependency { + name: name.clone(), + req: dep.version, + features: dep.features, + optional: dep.optional, + default_features: dep.default_features, + target: None, + kind: DependencyKind::Normal, + registry: match dep.registry_index { + // If the dependency lists no registry, it refers to crates.io. Since this + // is a custom registry, we need to make that reference explicit. + None => Some(CRATES_IO_INDEX_URL.clone()), + // If the dependency lists a custom registry that matches this one, + // we can set it to None so cargo knows it's a local reference. + Some(url) if url == config.base_url => None, + // Otherwise we just pass the registry URL through. + Some(url) => Some(url), + }, + package: Some(self.manifest.package.name.clone()), + }) + .collect(); + + let entry = IndexEntry { + name: self.manifest.package.name, + vers: self.manifest.package.version, + deps, + cksum: self.checksum, + features: self.manifest.features, + yanked: false, + links: None, + v: 2, + features2: Default::default(), + rust_version: self.manifest.package.rust_version, + }; + + Ok((entry, self.contents)) + } +} diff --git a/src/registry/registry.rs b/src/registry/registry.rs new file mode 100644 index 0000000..85ccac2 --- /dev/null +++ b/src/registry/registry.rs @@ -0,0 +1,549 @@ +use crate::config_json::ConfigJson; +use crate::margo_config::{LatestConfig, MargoConfig}; +use crate::prelude::*; +use crate::registry::{Index, PackagedCrate}; +use crate::util::PathExt; +use crate::Result; + +use cargo_util_schemas::manifest::PackageName; +use colored::Colorize; +use rayon::prelude::*; +use semver::Version; +use std::collections::BTreeSet; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::{fs, str}; +use walkdir::WalkDir; + +pub const MARGO_CONFIG_FILE_NAME: &str = "margo-config.toml"; +pub const MARGO_INDEX_FILE_NAME: &str = "margo-index.json"; +const CONFIG_JSON_FILE_NAME: &str = "config.json"; + +const CRATES_DIR_NAME: &str = "crates"; + +#[derive(Debug)] +pub struct Registry { + path: PathBuf, + config: LatestConfig, + index: BTreeSet, +} + +impl Registry { + /// Get the root directory containing the registry files. + pub fn path(&self) -> &Path { + &self.path + } + + /// Get the loaded registry configuration. + pub fn config(&self) -> &LatestConfig { + &self.config + } + + /// Initialise a new registry and create the basic file structure. + pub fn initialise(path: impl AsRef, config: LatestConfig) -> Result { + println!("Initialising new registry"); + + // Create the registry directory + let path = path.as_ref(); + fs::create_dir_all(path).context("Can't create registry directory.")?; + + // Initialise Registry struct + let registry = Registry { + path: path.to_path_buf(), + index: Default::default(), + config, + }; + + // Create registry config.json + let config_json_path = path.join(CONFIG_JSON_FILE_NAME); + let config_json = ConfigJson::new(registry.config())?; + let config_json = serde_json::to_string(&config_json)?; + fs::write(&config_json_path, config_json) + .context("Can't write config.json to registry directory.")?; + + // Create the Margo config file + registry.save_config()?; + + // Render the initial index.html + registry.maybe_generate_html()?; + + Ok(registry) + } + + /// Open an existing registry in a directory. + pub fn open(path: impl AsRef) -> Result { + println!("Opening registry"); + + let path = path.as_ref().to_path_buf(); + + let config_path = path.join(MARGO_CONFIG_FILE_NAME); + let config = fs::read_to_string(&config_path).context(format!( + "Can't read margo-config.toml in {path}.", + path = path.display() + ))?; + let config = + toml::from_str::(&config).context("Can't parse margo-config.toml.")?; + + let index_path = path.join(MARGO_INDEX_FILE_NAME); + let index = match fs::read_to_string(&index_path) { + Ok(index) => Some(index), + Err(e) if e.kind() == ErrorKind::NotFound => None, + Err(e) => Err(e).context("Can't read margo-index.json.")?, + }; + // If the index file doesn't exist, refresh it from disk. + let should_refresh_index = index.is_none(); + let index = match index { + Some(json) => serde_json::from_str::>(&json) + .context("Can't parse margo-index.json.")?, + None => Default::default(), + }; + + // If the loaded config is not the latest version, we should resave it in the latest format + // once it's been converted below. + let should_resave = !config.is_latest(); + + let mut registry = Registry { + path: path.to_path_buf(), + config: config.into_latest(), + index, + }; + + if should_refresh_index { + registry.refresh_index_from_disk()?; + } + + if should_resave || should_refresh_index { + registry.save_config()?; + } + + Ok(registry) + } + + /// Add a crate to the registry. + /// + /// The crate path should point to the `target/package/*.crate` file in the project you want to + /// publish. Run `cargo package` to create this file. + pub fn add(&self, crate_path: impl AsRef) -> Result<()> { + let crate_path = crate_path.as_ref(); + let crate_path = crate_path + .canonicalize() + .context(format!("Can't resolve path {}", crate_path.display()))?; + + println!("Adding crate to registry"); + println!(" from {}", crate_path.display().to_string().cyan()); + + let packaged_crate = PackagedCrate::open(&crate_path)?; + + self.add_from_packaged_crate(packaged_crate) + } + + /// Add a crate to the registry from a loaded crate package. + fn add_from_packaged_crate(&self, packaged_crate: PackagedCrate) -> Result<()> { + let (entry, crate_contents) = packaged_crate.into_index_entry(&self.config)?; + + // Make sure the version doesn't exist yet. + let mut index = Index::open_or_new(entry.name.clone(), self)?; + if index.contains_version(&entry.vers) { + return Err(MargoError::DuplicateVersion(entry.name, entry.vers).into()); + } + + // Create directories + fs::create_dir_all(self.index_dir_for(&entry.name))?; + fs::create_dir_all(self.crate_dir_for(&entry.name))?; + + // Get the crate path first because we can't access `entry` any more after adding it to the index. + let crate_path = self.crate_file_path_for(&entry.name, &entry.vers); + + index.add(entry); + index.save()?; + + // FUTURE: Stronger file system consistency (atomic file overwrites, rollbacks on error) + // FUTURE: "transactional" adding of multiple crates + + fs::write(&crate_path, crate_contents) + .context("Can't write crate to registry crates directory.")?; + println!("Wrote crate to `{}`", crate_path.display()); + + Ok(()) + } + + /// Delete a crate version from the registry. + /// + /// This deletes the entry from the index and the corresponding .crate file from + /// the registry. You should only do this in cases of abuse or when testing. Prefer + /// [Self::yank] instead, for regular production usage. + pub fn remove(&self, name: &PackageName, version: &Version) -> Result<()> { + let mut index = Index::open_or_new(name.clone(), self)?; + index.remove(version); + index.save()?; + + let crate_file_path = self.crate_file_path_for(name, version); + + match fs::remove_file(&crate_file_path) { + Ok(()) => Ok(()), + Err(e) if e.kind() == ErrorKind::NotFound => Ok(()), + Err(e) => Err(e).context("Can't delete removed crate file."), + }?; + + crate_file_path.remove_dirs_if_empty()?; + + Ok(()) + } + + /// Yank a crate version from the registry. + pub fn yank(&self, name: &PackageName, version: &Version, yanked: bool) -> Result<()> { + let mut index = Index::open_or_new(name.clone(), self)?; + index.set_yanked(version, yanked)?; + index.save()?; + + Ok(()) + } + + /// List all indexes in the registry. + pub fn list_indexes(&self) -> Result> { + self.index + .par_iter() + .map(|n| Index::open_or_new(n.clone(), self)) + .collect() + } + + /// Get the .crate package information for the given version. + pub fn read_crate_info(&self, name: &PackageName, version: &Version) -> Result { + PackagedCrate::open(&self.crate_file_path_for(name, version)) + } + + /// Refresh the Margo package index by scanning the .crate files on disk. + pub fn refresh_index_from_disk(&mut self) -> Result<()> { + println!("Building registry index from disk"); + + self.index = WalkDir::new(self.crate_dir()) + .into_iter() + .filter_map(|e| e.ok()) + // Ignore anything that isn't a .crate file. + .filter_map(|e| match e.path().extension() { + Some(ext) if ext == "crate" => Some(e), + _ => None, + }) + .filter_map(|e| Some(e.path().parent()?.file_name()?.to_str()?.to_string())) + .map(|n| { + PackageName::from_str(&n).context(format!("Invalid package name: {name}", name = n)) + }) + .collect::>>()?; + + println!("Index updated"); + Ok(()) + } + + /// Generate HTML if the registry is configured to output HTML contents. + /// + /// This method simply calls [Registry::generate_html] if the html config is set. + pub fn maybe_generate_html(&self) -> Result<()> { + match &self.config.html { + Some(_) => self.generate_html(), + _ => Ok(()), + } + } + + /// Generate the HTML contents using the configured template. + #[cfg(feature = "html")] + pub fn generate_html(&self) -> Result<()> { + use crate::template::Template; + + let config = &self + .config + .html + .as_ref() + .context("Missing HTML configuration in registry config.")?; + + let template = Template::load_from_reference(&config.template)?; + template.render(self, config)?; + + println!("Done"); + Ok(()) + } + + #[cfg(not(feature = "html"))] + pub fn generate_html(&self) -> Result<()> { + Err(MargoError::NoHtml) + } + + /// Save the Margo config file. + fn save_config(&self) -> Result<()> { + // Save Margo config. + println!( + "Writing registry configuration to {name}", + name = MARGO_CONFIG_FILE_NAME + ); + let config_toml_path = self.path.join(MARGO_CONFIG_FILE_NAME); + let config_toml = toml::to_string(&MargoConfig::from(self.config.clone()))?; + + fs::write(&config_toml_path, config_toml) + .context("Can't write margo-config.toml to registry directory.")?; + + // Save Margo index. + println!( + "Writing registry index to {name}", + name = MARGO_INDEX_FILE_NAME + ); + let index_json_path = self.path.join(MARGO_INDEX_FILE_NAME); + let index_json = serde_json::to_string_pretty(&self.index)?; + + fs::write(&index_json_path, &index_json) + .context("Can't write margo-index.json to registry directory.")?; + + Ok(()) + } + + /// Calculate the containing directory of the index file for a given package name. + pub(super) fn index_dir_for(&self, name: &PackageName) -> PathBuf { + self.path.join_prefix_directories(name) + } + + /// Calculate the full path of the index file for a given package name. + pub(super) fn index_file_path_for(&self, name: &PackageName) -> PathBuf { + self.index_dir_for(name).join(name.as_str()) + } + + /// The directory containing the crates in this registry. + fn crate_dir(&self) -> PathBuf { + self.path.join(CRATES_DIR_NAME) + } + + /// Calculate the containing directory for the .crate file for a given package name. + fn crate_dir_for(&self, name: &PackageName) -> PathBuf { + self.crate_dir() + .join_prefix_directories(name) + .join(name.as_str()) + } + + /// Calculate the full path for the .crate file for a given package name and version. + fn crate_file_path_for(&self, name: &PackageName, version: &Version) -> PathBuf { + self.crate_dir_for(name).join(format!("{}.crate", version)) + } +} + +#[cfg(test)] +mod test { + use super::{Registry, CRATES_DIR_NAME}; + use crate::margo_config::{LatestConfig, MargoConfig}; + use assert_fs::prelude::*; + use assert_fs::TempDir; + use cargo_util_schemas::manifest::PackageName; + use predicates::prelude::*; + use registry_conformance::{Crate, ScratchSpace}; + use semver::Version; + use std::str::FromStr; + use tokio::fs; + + #[test] + fn paths() { + let temp_dir = TempDir::with_prefix("margo-tests-").unwrap(); + let config = MargoConfig::default().into_latest(); + let registry = Registry::initialise(&temp_dir, config).unwrap(); + + let crates_dir = temp_dir.join(CRATES_DIR_NAME); + + assert_eq!( + registry.path(), + temp_dir.path(), + "Registry path should be the given directory" + ); + assert_eq!( + registry.crate_dir(), + crates_dir, + "Crates dir should be inside the given directory" + ); + + let name = PackageName::from_str("margo-test-package").unwrap(); + let version = Version::new(1, 0, 0); + + assert_eq!( + registry.index_dir_for(&name), + temp_dir.join("ma/rg"), + "Index dir should be ma/rg" + ); + assert_eq!( + registry.index_file_path_for(&name), + temp_dir.join("ma/rg/margo-test-package"), + "Index file path should be ma/rg/margo-test-package" + ); + + assert_eq!( + registry.crate_dir_for(&name), + crates_dir.join("ma/rg/margo-test-package"), + "Crate dir should be ma/rg/margo-test-package" + ); + assert_eq!( + registry.crate_file_path_for(&name, &version), + crates_dir.join("ma/rg/margo-test-package/1.0.0.crate"), + "Crate file path should be ma/rg/margo-test-package/1.0.0.crate" + ); + } + + /// [Registry::initialise] should create the given directory and basic configuration files. + #[test] + fn initialise() { + use predicate::path::{exists, is_dir, is_file}; + use predicate::str::is_empty; + + let temp_dir = TempDir::with_prefix("margo-tests-").unwrap(); + + let registry_dir = temp_dir.child("registry"); + let config_json = registry_dir.child("config.json"); + let margo_config_toml = registry_dir.child("margo-config.toml"); + let index_html = registry_dir.child("index.html"); + + let config = MargoConfig::default().into_latest(); + Registry::initialise(®istry_dir, config).unwrap(); + + registry_dir.assert(exists().name("registry path should exist")); + registry_dir.assert(is_dir().name("registry path should be a directory")); + + config_json.assert(exists().name("config.json should exist")); + config_json.assert(is_file().name("config.json should be a file")); + config_json.assert(is_empty().not().name("config.json should not be empty")); + + margo_config_toml.assert(exists().name("margo-config.toml should exist")); + margo_config_toml.assert(is_file().name("margo-config.toml should be a file")); + margo_config_toml.assert( + is_empty() + .not() + .name("margo-config.toml should not be empty"), + ); + + index_html.assert(exists().name("index.html should exist")); + index_html.assert(is_file().name("index.html should be a file")); + index_html.assert(is_empty().not().name("index.html should not be empty")); + } + + /// [Registry::initialise] should not create an index.html if the index option is set to None. + #[test] + fn initialise_index_none() { + use predicate::path::exists; + + let temp_dir = TempDir::with_prefix("margo-tests-").unwrap(); + + let registry_dir = temp_dir.child("registry"); + let index_html = registry_dir.child("index.html"); + + let config = LatestConfig { + html: None, + ..MargoConfig::default().into_latest() + }; + Registry::initialise(®istry_dir, config).unwrap(); + + index_html.assert(exists().not().name("index.html should not exist")); + } + + /// [Registry::add] should add a crate to the registry from a file. + #[tokio::test] + async fn add() { + use predicate::path::{exists, is_file}; + use predicate::str::is_empty; + + let temp_dir = TempDir::with_prefix("margo-tests-").unwrap(); + + let config = MargoConfig::default().into_latest(); + let registry = Registry::initialise(&temp_dir, config).unwrap(); + + let name = PackageName::from_str("margo-test-package").unwrap(); + let version = Version::new(1, 0, 0); + + let scratch = ScratchSpace::new().await.unwrap(); + let test_crate = Crate::new(name.as_str(), version.to_string()) + .lib_rs(r#"pub const ID: u8 = 1;"#) + .create_in(&scratch) + .await + .unwrap(); + + let crate_path = test_crate.package().await.unwrap(); + + assert!( + registry.add(crate_path).is_ok(), + "Should be able to add the crate" + ); + + let index_file = temp_dir.child(registry.index_file_path_for(&name)); + let crate_file = temp_dir.child(registry.crate_file_path_for(&name, &version)); + + index_file.assert(exists().name("Index should exist")); + index_file.assert(is_file().name("Index should be a file")); + index_file.assert(is_empty().not().name("Index should not be empty")); + + crate_file.assert(exists().name("Crate should exist")); + crate_file.assert(is_file().name("Crate should be a file")); + } + + #[tokio::test] + async fn ading_duplicate_crate() { + let scratch = ScratchSpace::new().await.unwrap(); + + let config = MargoConfig::default().into_latest(); + let registry = Registry::initialise(scratch.registry(), config).unwrap(); + + let test_crate = Crate::new("duplicated", "1.0.0") + .lib_rs(r#"pub const ID: u8 = 1;"#) + .create_in(&scratch) + .await + .unwrap(); + + let crate_path = test_crate.package().await.unwrap(); + + assert!( + registry.add(&crate_path).is_ok(), + "Should be able to add the crate" + ); + assert!( + registry.add(&crate_path).is_err(), + "Should not be able to add the crate a second time" + ); + + let name = PackageName::from_str(test_crate.name()).unwrap(); + + let index_file_path = registry.index_file_path_for(&name); + let index_contents = fs::read_to_string(index_file_path).await.unwrap(); + + assert_eq!(1, index_contents.lines().count()); + } + + #[tokio::test] + async fn removing_a_crate_deletes_from_disk() { + let scratch = ScratchSpace::new().await.unwrap(); + + let config = MargoConfig::default().into_latest(); + + let registry = Registry::initialise(scratch.registry(), config).unwrap(); + + let name = "to-go-away"; + let version = "1.0.0"; + + let test_crate = Crate::new(name, version) + .lib_rs(r#"pub const ID: u8 = 1;"#) + .create_in(&scratch) + .await + .unwrap(); + + let original_crate_path = test_crate.package().await.unwrap(); + + let name = name.parse().unwrap(); + let version = version.parse().unwrap(); + let target_crate_path = registry.crate_file_path_for(&name, &version); + + registry.add(original_crate_path).unwrap(); + + assert!( + target_crate_path.exists(), + "The crate file should be in the registry at {}", + target_crate_path.display(), + ); + + registry.remove(&name, &version).unwrap(); + + assert!( + !target_crate_path.exists(), + "The crate file should not be in the registry at {}", + target_crate_path.display(), + ); + } +} diff --git a/src/template.rs b/src/template.rs new file mode 100644 index 0000000..a560816 --- /dev/null +++ b/src/template.rs @@ -0,0 +1,446 @@ +//! This module contains all required logic to load and render templates from a .tar file. +//! +//! # Built-in templates +//! Margo ships with a few templates built-in, to help you get up and running quickly. +//! +//! - `Classic` (default): the original Margo registry template. +//! +//! # Custom templates +//! A template is a simple uncompressed tarball archive (`.tar`, not `.tar.gz`) containing all +//! necessary files required for the template to work. All files inside the template archive will +//! be copied as-is to the registry output directory, except for the index template. +//! +//! ## Index +//! The index template must be a file called `index.hbs` or `index.html`. This file must contain +//! a [handlebars](https://handlebarsjs.com/guide/) template to render the HTML for the index page. +//! +//! ### Variables +//! See [TemplateData] for all variables provided to the Handlebars template while rendering. +//! +//! ### Helpers +//! Margo adds a few Rust-specific helpers to the Handlebars renderer: +//! +//! - `len [arr]`: Return the length of an array. +//! - `eq [lhs] [rhs]`: Check if two values are equal. +//! - `ne [lhs] [rhs]`: Check if two values are not equal. +//! - `gt [lhs] [rhs]`: Check if `lhs` is greater than `rhs`. +//! - `lt [lhs] [rhs]`: Check if `lhs` is less than `rhs`. +//! +//! ### Minimal example +//! ```hbs +//!

{{title}}

+//! +//!

Installation

+//!

Add this registry to .cargo/config.toml:

+//!
+//! [registries]
+//! {{registry.suggested_name}} = { index = "sparse+{{registry.base_url}}" }
+//! 
+//! +//!

Install a crate from this registry:

+//!
+//! cargo add --registry={{registry.suggested_name}} [crate name]
+//! 
+//! +//!

Crates

+//! {{#each crates}} +//!

{{name}}

+//! {{#each versions}} +//!

{{this}}

+//! {{/each}} +//! {{/each}} +//! ``` + +use crate::margo_config::LatestConfigIndex; +use crate::prelude::*; +use crate::registry::{PackagedCargoToml, Registry}; +use crate::template_reference::{BuiltInTemplate, TemplateReference}; +use crate::Result; + +use cargo_util_schemas::manifest::{FeatureName, PackageName}; +use colored::Colorize; +use comrak::nodes::{NodeLink, NodeValue}; +use comrak::{format_html, parse_document, Arena, Options}; +use handlebars::{handlebars_helper, Handlebars, JsonValue}; +use serde::Serialize; +use std::cmp::Ordering; +use std::collections::BTreeMap; +use std::fs; +use std::fs::File; +use std::io::{BufReader, Cursor, Read}; +use std::path::{Path, PathBuf}; +use tar::Archive; +use url::Url; + +#[derive(Debug, Default)] +pub struct Template { + index_hbs: String, + assets: BTreeMap>, +} + +impl Template { + /// Load a template from a reference. + pub fn load_from_reference(reference: &TemplateReference) -> Result