diff --git a/Cargo.lock b/Cargo.lock index cd04c087..e78a99d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -159,7 +159,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.1", + "hyper 1.5.2", "hyper-util", "itoa", "matchit", @@ -249,9 +249,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" dependencies = [ "memchr", "regex-automata", @@ -260,7 +260,7 @@ dependencies = [ [[package]] name = "buffrs" -version = "0.9.0" +version = "0.10.1" dependencies = [ "anyhow", "assert_cmd", @@ -280,6 +280,7 @@ dependencies = [ "paste", "predicates", "pretty_assertions", + "pretty_yaml", "protobuf", "protobuf-parse", "reqwest", @@ -287,6 +288,7 @@ dependencies = [ "serde", "serde_json", "serde_test", + "serde_yml", "sha2", "similar-asserts", "strum", @@ -314,9 +316,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" +checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" dependencies = [ "shlex", ] @@ -381,14 +383,14 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "console" -version = "0.15.8" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] @@ -407,6 +409,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + [[package]] name = "cpufeatures" version = "0.2.16" @@ -427,9 +435,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -446,9 +454,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" @@ -460,15 +468,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - [[package]] name = "diff" version = "0.1.13" @@ -544,9 +543,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" @@ -578,6 +577,9 @@ name = "faster-hex" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" +dependencies = [ + "serde", +] [[package]] name = "fastrand" @@ -705,9 +707,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gix" -version = "0.63.0" +version = "0.68.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "984c5018adfa7a4536ade67990b3ebc6e11ab57b3d6cd9968de0947ca99b4b06" +checksum = "b04c66359b5e17f92395abc433861df0edf48f39f3f590818d1d7217327dd6a1" dependencies = [ "gix-actor", "gix-commitgraph", @@ -721,7 +723,6 @@ dependencies = [ "gix-hash", "gix-hashtable", "gix-lock", - "gix-macros", "gix-object", "gix-odb", "gix-pack", @@ -738,22 +739,21 @@ dependencies = [ "gix-utils", "gix-validate", "once_cell", - "parking_lot", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.8", ] [[package]] name = "gix-actor" -version = "0.31.5" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0e454357e34b833cc3a00b6efbbd3dd4d18b24b9fb0c023876ec2645e8aa3f2" +checksum = "32b24171f514cef7bb4dfb72a0b06dacf609b33ba8ad2489d4c4559a03b7afb3" dependencies = [ "bstr", "gix-date", "gix-utils", "itoa", - "thiserror 1.0.69", + "thiserror 2.0.8", "winnow", ] @@ -763,28 +763,28 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6ffbeb3a5c0b8b84c3fe4133a6f8c82fa962f4caefe8d0762eced025d3eb4f7" dependencies = [ - "thiserror 2.0.6", + "thiserror 2.0.8", ] [[package]] name = "gix-commitgraph" -version = "0.24.3" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133b06f67f565836ec0c473e2116a60fb74f80b6435e21d88013ac0e3c60fc78" +checksum = "a8da6591a7868fb2b6dabddea6b09988b0b05e0213f938dbaa11a03dd7a48d85" dependencies = [ "bstr", "gix-chunk", "gix-features", "gix-hash", "memmap2", - "thiserror 1.0.69", + "thiserror 2.0.8", ] [[package]] name = "gix-config" -version = "0.37.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fafe42957e11d98e354a66b6bd70aeea00faf2f62dd11164188224a507c840" +checksum = "6649b406ca1f99cb148959cf00468b231f07950f8ec438cc0903cda563606f19" dependencies = [ "bstr", "gix-config-value", @@ -796,7 +796,7 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.8", "unicode-bom", "winnow", ] @@ -811,38 +811,38 @@ dependencies = [ "bstr", "gix-path", "libc", - "thiserror 2.0.6", + "thiserror 2.0.8", ] [[package]] name = "gix-date" -version = "0.8.7" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eed6931f21491ee0aeb922751bd7ec97b4b2fe8fbfedcb678e2a2dce5f3b8c0" +checksum = "691142b1a34d18e8ed6e6114bc1a2736516c5ad60ef3aa9bd1b694886e3ca92d" dependencies = [ "bstr", "itoa", - "thiserror 1.0.69", - "time", + "jiff", + "thiserror 2.0.8", ] [[package]] name = "gix-diff" -version = "0.44.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1996d5c8a305b59709467d80617c9fde48d9d75fd1f4179ea970912630886c9d" +checksum = "a327be31a392144b60ab0b1c863362c32a1c8f7effdfa2141d5d5b6b916ef3bf" dependencies = [ "bstr", "gix-hash", "gix-object", - "thiserror 1.0.69", + "thiserror 2.0.8", ] [[package]] name = "gix-discover" -version = "0.32.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc27c699b63da66b50d50c00668bc0b7e90c3a382ef302865e891559935f3dbf" +checksum = "83bf6dfa4e266a4a9becb4d18fc801f92c3f7cc6c433dd86fdadbcf315ffb6ef" dependencies = [ "bstr", "dunce", @@ -851,14 +851,14 @@ dependencies = [ "gix-path", "gix-ref", "gix-sec", - "thiserror 1.0.69", + "thiserror 2.0.8", ] [[package]] name = "gix-features" -version = "0.38.2" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac7045ac9fe5f9c727f38799d002a7ed3583cd777e3322a7c4b43e3cf437dc69" +checksum = "7d85d673f2e022a340dba4713bed77ef2cf4cd737d2f3e0f159d45e0935fd81f" dependencies = [ "crc32fast", "flate2", @@ -869,15 +869,15 @@ dependencies = [ "once_cell", "prodash", "sha1_smol", - "thiserror 1.0.69", + "thiserror 2.0.8", "walkdir", ] [[package]] name = "gix-fs" -version = "0.11.3" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2bfe6249cfea6d0c0e0990d5226a4cb36f030444ba9e35e0639275db8f98575" +checksum = "34740384d8d763975858fa2c176b68652a6fcc09f616e24e3ce967b0d370e4d8" dependencies = [ "fastrand", "gix-features", @@ -886,9 +886,9 @@ dependencies = [ [[package]] name = "gix-glob" -version = "0.16.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74908b4bbc0a0a40852737e5d7889f676f081e340d5451a16e5b4c50d592f111" +checksum = "aaf69a6bec0a3581567484bf99a4003afcaf6c469fd4214352517ea355cf3435" dependencies = [ "bitflags 2.6.0", "bstr", @@ -898,19 +898,19 @@ dependencies = [ [[package]] name = "gix-hash" -version = "0.14.2" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d7df7366121b5018f947a04d37f034717e113dcf9ccd85c34b58e57a74d5e" +checksum = "0b5eccc17194ed0e67d49285e4853307e4147e95407f91c1c3e4a13ba9f4e4ce" dependencies = [ "faster-hex", - "thiserror 1.0.69", + "thiserror 2.0.8", ] [[package]] name = "gix-hashtable" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddf80e16f3c19ac06ce415a38b8591993d3f73aede049cb561becb5b3a8e242" +checksum = "0ef65b256631078ef733bc5530c4e6b1c2e7d5c2830b75d4e9034ab3997d18fe" dependencies = [ "gix-hash", "hashbrown 0.14.5", @@ -919,70 +919,61 @@ dependencies = [ [[package]] name = "gix-lock" -version = "14.0.0" +version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bc7fe297f1f4614774989c00ec8b1add59571dc9b024b4c00acb7dedd4e19d" +checksum = "1cd3ab68a452db63d9f3ebdacb10f30dba1fa0d31ac64f4203d395ed1102d940" dependencies = [ "gix-tempfile", "gix-utils", - "thiserror 1.0.69", -] - -[[package]] -name = "gix-macros" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999ce923619f88194171a67fb3e6d613653b8d4d6078b529b15a765da0edcc17" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", + "thiserror 2.0.8", ] [[package]] name = "gix-object" -version = "0.42.3" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25da2f46b4e7c2fa7b413ce4dffb87f69eaf89c2057e386491f4c55cadbfe386" +checksum = "65d93e2bbfa83a307e47f45e45de7b6c04d7375a8bd5907b215f4bf45237d879" dependencies = [ "bstr", "gix-actor", "gix-date", "gix-features", "gix-hash", + "gix-hashtable", "gix-utils", "gix-validate", "itoa", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.8", "winnow", ] [[package]] name = "gix-odb" -version = "0.61.1" +version = "0.65.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20d384fe541d93d8a3bb7d5d5ef210780d6df4f50c4e684ccba32665a5e3bc9b" +checksum = "93bed6e1b577c25a6bb8e6ecbf4df525f29a671ddf5f2221821a56a8dbeec4e3" dependencies = [ "arc-swap", "gix-date", "gix-features", "gix-fs", "gix-hash", + "gix-hashtable", "gix-object", "gix-pack", "gix-path", "gix-quote", "parking_lot", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.8", ] [[package]] name = "gix-pack" -version = "0.51.1" +version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0594491fffe55df94ba1c111a6566b7f56b3f8d2e1efc750e77d572f5f5229" +checksum = "9b91fec04d359544fecbb8e85117ec746fbaa9046ebafcefb58cb74f20dc76d4" dependencies = [ "clru", "gix-chunk", @@ -993,7 +984,7 @@ dependencies = [ "gix-path", "memmap2", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.8", ] [[package]] @@ -1006,7 +997,7 @@ dependencies = [ "gix-trace", "home", "once_cell", - "thiserror 2.0.6", + "thiserror 2.0.8", ] [[package]] @@ -1017,17 +1008,16 @@ checksum = "64a1e282216ec2ab2816cd57e6ed88f8009e634aec47562883c05ac8a7009a63" dependencies = [ "bstr", "gix-utils", - "thiserror 2.0.6", + "thiserror 2.0.8", ] [[package]] name = "gix-ref" -version = "0.44.1" +version = "0.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3394a2997e5bc6b22ebc1e1a87b41eeefbcfcff3dbfa7c4bd73cb0ac8f1f3e2e" +checksum = "1eae462723686272a58f49501015ef7c0d67c3e042c20049d8dd9c7eff92efde" dependencies = [ "gix-actor", - "gix-date", "gix-features", "gix-fs", "gix-hash", @@ -1038,43 +1028,44 @@ dependencies = [ "gix-utils", "gix-validate", "memmap2", - "thiserror 1.0.69", + "thiserror 2.0.8", "winnow", ] [[package]] name = "gix-refspec" -version = "0.23.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6868f8cd2e62555d1f7c78b784bece43ace40dd2a462daf3b588d5416e603f37" +checksum = "00c056bb747868c7eb0aeb352c9f9181ab8ca3d0a2550f16470803500c6c413d" dependencies = [ "bstr", "gix-hash", "gix-revision", "gix-validate", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.8", ] [[package]] name = "gix-revision" -version = "0.27.2" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b13e43c2118c4b0537ddac7d0821ae0dfa90b7b8dbf20c711e153fb749adce" +checksum = "44488e0380847967bc3e3cacd8b22652e02ea1eb58afb60edd91847695cd2d8d" dependencies = [ "bstr", + "gix-commitgraph", "gix-date", "gix-hash", "gix-object", "gix-revwalk", - "thiserror 1.0.69", + "thiserror 2.0.8", ] [[package]] name = "gix-revwalk" -version = "0.13.2" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b030ccaab71af141f537e0225f19b9e74f25fefdba0372246b844491cab43e0" +checksum = "510026fc32f456f8f067d8f37c34088b97a36b2229d88a6a5023ef179fcb109d" dependencies = [ "gix-commitgraph", "gix-date", @@ -1082,7 +1073,7 @@ dependencies = [ "gix-hashtable", "gix-object", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.8", ] [[package]] @@ -1099,9 +1090,9 @@ dependencies = [ [[package]] name = "gix-tempfile" -version = "14.0.2" +version = "15.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046b4927969fa816a150a0cda2e62c80016fe11fb3c3184e4dddf4e542f108aa" +checksum = "2feb86ef094cc77a4a9a5afbfe5de626897351bbbd0de3cb9314baf3049adb82" dependencies = [ "gix-fs", "libc", @@ -1118,9 +1109,9 @@ checksum = "04bdde120c29f1fc23a24d3e115aeeea3d60d8e65bab92cc5f9d90d9302eb952" [[package]] name = "gix-traverse" -version = "0.39.2" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e499a18c511e71cf4a20413b743b9f5bcf64b3d9e81e9c3c6cd399eae55a8840" +checksum = "3ff2ec9f779680f795363db1c563168b32b8d6728ec58564c628e85c92d29faf" dependencies = [ "bitflags 2.6.0", "gix-commitgraph", @@ -1130,20 +1121,19 @@ dependencies = [ "gix-object", "gix-revwalk", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.8", ] [[package]] name = "gix-url" -version = "0.27.5" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd280c5e84fb22e128ed2a053a0daeacb6379469be6a85e3d518a0636e160c89" +checksum = "e09f97db3618fb8e473d7d97e77296b50aaee0ddcd6a867f07443e3e87391099" dependencies = [ "bstr", "gix-features", "gix-path", - "home", - "thiserror 1.0.69", + "thiserror 2.0.8", "url", ] @@ -1159,12 +1149,12 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.8.5" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c27dd34a49b1addf193c92070bcbf3beaf6e10f16a78544de6372e146a0acf" +checksum = "cd520d09f9f585b34b32aba1d0b36ada89ab7fefb54a8ca3fe37fc482a750937" dependencies = [ "bstr", - "thiserror 1.0.69", + "thiserror 2.0.8", ] [[package]] @@ -1228,12 +1218,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - [[package]] name = "hex" version = "0.4.3" @@ -1242,11 +1226,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1335,9 +1319,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.31" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -1359,9 +1343,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", @@ -1384,7 +1368,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.31", + "hyper 0.14.32", "rustls", "tokio", "tokio-rustls", @@ -1400,7 +1384,7 @@ dependencies = [ "futures-util", "http 1.2.0", "http-body 1.0.1", - "hyper 1.5.1", + "hyper 1.5.2", "pin-project-lite", "tokio", "tower-service", @@ -1577,17 +1561,6 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" -[[package]] -name = "is-terminal" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "is_ci" version = "1.2.0" @@ -1606,6 +1579,31 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "jiff" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db69f08d4fb10524cacdb074c10b296299d71274ddbc830a8ee65666867002e9" +dependencies = [ + "jiff-tzdb-platform", + "windows-sys 0.59.0", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "js-sys" version = "0.3.76" @@ -1639,6 +1637,16 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libyml" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1690,15 +1698,14 @@ dependencies = [ [[package]] name = "miette" -version = "5.10.0" +version = "7.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +checksum = "317f146e2eb7021892722af37cf1b971f0a70c8406f487e24952667616192c64" dependencies = [ "backtrace", "backtrace-ext", - "is-terminal", + "cfg-if", "miette-derive", - "once_cell", "owo-colors", "supports-color", "supports-hyperlinks", @@ -1711,9 +1718,9 @@ dependencies = [ [[package]] name = "miette-derive" -version = "5.10.0" +version = "7.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +checksum = "23c9b935fbe1d6cbd1dac857b54a688145e2d93f48db36010514d0f612d0ad67" dependencies = [ "proc-macro2", "quote", @@ -1728,9 +1735,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", ] @@ -1795,12 +1802,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-integer" version = "0.1.46" @@ -1841,15 +1842,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - [[package]] name = "object" version = "0.36.5" @@ -1890,9 +1882,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owo-colors" -version = "3.5.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" [[package]] name = "parking_lot" @@ -1941,12 +1933,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[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.2" @@ -1987,6 +1973,17 @@ dependencies = [ "yansi", ] +[[package]] +name = "pretty_yaml" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda9a64ee7296e82d1e0f4389383e6a7d8e6e2487d8391f7d028c131395fd376" +dependencies = [ + "rowan", + "tiny_pretty", + "yaml_parser", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -1998,9 +1995,13 @@ dependencies = [ [[package]] name = "prodash" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" +checksum = "a266d8d6020c61a437be704c5e618037588e1985c7dbb7bf8d265db84cffe325" +dependencies = [ + "log", + "parking_lot", +] [[package]] name = "protobuf" @@ -2049,9 +2050,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.6.0", ] @@ -2099,7 +2100,7 @@ dependencies = [ "h2", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.31", + "hyper 0.14.32", "hyper-rustls", "ipnet", "js-sys", @@ -2141,12 +2142,30 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rowan" +version = "0.15.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a542b0253fa46e632d27a1dc5cf7b930de4df8659dc6e720b647fc72147ae3d" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash", + "text-size", +] + [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.38.42" @@ -2264,9 +2283,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" dependencies = [ "core-foundation-sys", "libc", @@ -2274,27 +2293,27 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", @@ -2343,6 +2362,21 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yml" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" +dependencies = [ + "indexmap", + "itoa", + "libyml", + "memchr", + "ryu", + "serde", + "version_check", +] + [[package]] name = "sha1_smol" version = "1.0.1" @@ -2419,12 +2453,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - [[package]] name = "socket2" version = "0.5.8" @@ -2477,31 +2505,24 @@ dependencies = [ [[package]] name = "supports-color" -version = "2.1.0" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" dependencies = [ - "is-terminal", "is_ci", ] [[package]] name = "supports-hyperlinks" -version = "2.1.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d" -dependencies = [ - "is-terminal", -] +checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" [[package]] name = "supports-unicode" -version = "2.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f850c19edd184a205e883199a261ed44471c81e39bd95b1357f5febbef00e77a" -dependencies = [ - "is-terminal", -] +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" @@ -2595,12 +2616,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.1.17" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ - "libc", - "winapi", + "rustix", + "windows-sys 0.59.0", ] [[package]] @@ -2609,13 +2630,18 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + [[package]] name = "textwrap" -version = "0.15.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ - "smawk", "unicode-linebreak", "unicode-width", ] @@ -2631,11 +2657,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.6" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +checksum = "08f5383f3e0071702bf93ab5ee99b52d26936be9dedd9413067cbdcddcb6141a" dependencies = [ - "thiserror-impl 2.0.6", + "thiserror-impl 2.0.8", ] [[package]] @@ -2651,9 +2677,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.6" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +checksum = "f2f357fcec90b3caef6623a099691be676d033b40a058ac95d2a6ade6fa0c943" dependencies = [ "proc-macro2", "quote", @@ -2671,37 +2697,10 @@ dependencies = [ ] [[package]] -name = "time" -version = "0.3.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" -dependencies = [ - "deranged", - "itoa", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.19" +name = "tiny_pretty" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" -dependencies = [ - "num-conv", - "time-core", -] +checksum = "4b3f46f0549180b9c6f7f76270903f1a06867c43a03998b99dce81aa1760c3b2" [[package]] name = "tinystr" @@ -2816,14 +2815,14 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.2", "tokio", "tower-layer", "tower-service", @@ -3350,6 +3349,16 @@ dependencies = [ "rustix", ] +[[package]] +name = "yaml_parser" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c551c67700672ec050a94d5d917487c0ecd644a12735133df65564779c5b7b" +dependencies = [ + "rowan", + "winnow", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index a2de8964..0027afab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "buffrs" -version = "0.9.0" +version = "0.10.1" edition = "2021" description = "Modern protobuf package management" authors = [ @@ -12,6 +12,7 @@ authors = [ "Robert Fink ", "Thomas Pellissier-Tanon ", "Tom Karwowski ", + "Daniel Gehriger " ] repository = "https://github.com/helsing-ai/buffrs" documentation = "https://docs.rs/buffrs" @@ -46,13 +47,15 @@ flate2 = "1" hex = "0.4.3" home = "0.5.5" human-panic = "2" -miette = { version = "5.10.0", features = ["fancy"] } +miette = { version = "7", features = ["fancy"] } +pretty_yaml = { version = "0.5.0" } protobuf = { version = "3.3.0", optional = true } protobuf-parse = { version = "3.3.0", optional = true } reqwest = { version = "0.11", features = ["rustls-tls-native-roots"], default-features = false } semver = { version = "1", features = ["serde"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_yml = { version = "0.0.12" } tar = "0.4" thiserror = "1.0.49" tokio = { version = "^1.26", features = ["fs", "rt", "macros", "process", "io-std", "tracing"] } @@ -69,7 +72,7 @@ assert_cmd = "2.0" assert_fs = "1.0" axum = { version = "0.7.2", default-features = false, features = ["tokio", "http1"] } fs_extra = "1.3" -gix = { version = "0.63", default-features = false } +gix = { version = "0.68.0", default-features = false } hex = "0.4.3" paste = "1.0.14" predicates = "3.0" diff --git a/deny.toml b/deny.toml index 6fcf9b25..18300cf4 100644 --- a/deny.toml +++ b/deny.toml @@ -1,6 +1,6 @@ [licenses] version = 2 -allow = ["Apache-2.0", "BSD-3-Clause", "MIT", "Unicode-3.0", "ISC"] +allow = ["Apache-2.0", "BSD-3-Clause", "MIT", "Unicode-3.0", "Unicode-DFS-2016", "ISC", "MPL-2.0"] [[licenses.clarify]] name = "ring" diff --git a/docs/src/guide/local-dependencies.md b/docs/src/guide/local-dependencies.md index f23214a8..a654f5a2 100644 --- a/docs/src/guide/local-dependencies.md +++ b/docs/src/guide/local-dependencies.md @@ -64,7 +64,7 @@ mono Where `mono/mono-api/Proto.toml` has this content: ``` -edition = "0.9" +edition = "0.10" [package] type = "api" @@ -75,7 +75,7 @@ version = "0.1.0" And `mono/mono-server/Proto.toml` has this content: ``` -edition = "0.9" +edition = "0.10" [dependencies] mono-api = { path = "../api" } diff --git a/src/command.rs b/src/command.rs index 564e65aa..7187def9 100644 --- a/src/command.rs +++ b/src/command.rs @@ -14,12 +14,14 @@ use crate::{ cache::Cache, + config::Config, credentials::Credentials, + integration::{buf_yaml, path_util::PathUtil}, lock::{LockedPackage, Lockfile}, manifest::{Dependency, Manifest, PackageManifest, MANIFEST_FILE}, - package::{PackageName, PackageStore, PackageType}, - registry::{Artifactory, RegistryUri}, - resolver::{DependencyGraph, ResolvedDependency}, + package::{Package, PackageName, PackageStore, PackageType}, + registry::{Artifactory, CertValidationPolicy, RegistryRef, RegistryUri}, + resolver::{DependencyGraph, DependencyGraphBuilder, ResolvedDependency}, }; use async_recursion::async_recursion; @@ -134,7 +136,7 @@ impl FromStr for DependencyLocator { let (repository, dependency) = dependency .trim() .split_once('/') - .ok_or_else(|| miette!("locator {dependency} is missing a repository delimiter"))?; + .ok_or_else(|| miette!("locator \"{dependency}\" is missing a repository delimiter (use /@)"))?; ensure!( repository.chars().all(lower_kebab), @@ -173,8 +175,18 @@ impl FromStr for DependencyLocator { } /// Adds a dependency to this project -pub async fn add(registry: RegistryUri, dependency: &str) -> miette::Result<()> { - let mut manifest = Manifest::read().await?; +/// +/// # Arguments +/// * `registry` - The registry URI +/// * `dependency` - The dependency to add (e.g. `my-repo/my-package@1.0`) +pub async fn add( + registry: &RegistryRef, + dependency: &str, + config: &Config, + policy: CertValidationPolicy, +) -> miette::Result<()> { + let mut manifest = Manifest::read(config).await?; + let registry = registry.with_alias_resolved(Some(config))?; let DependencyLocator { repository, @@ -187,7 +199,7 @@ pub async fn add(registry: RegistryUri, dependency: &str) -> miette::Result<()> DependencyLocatorVersion::Latest => { // query artifactory to retrieve the actual latest version let credentials = Credentials::load().await?; - let artifactory = Artifactory::new(registry.clone(), &credentials)?; + let artifactory = Artifactory::new(registry.clone().try_into()?, &credentials, policy)?; let latest_version = artifactory .get_latest_version(repository.clone(), package.clone()) @@ -201,7 +213,7 @@ pub async fn add(registry: RegistryUri, dependency: &str) -> miette::Result<()> manifest .dependencies - .push(Dependency::new(registry, repository, package, version)); + .push(Dependency::new(®istry, repository, package, version)); manifest .write() @@ -210,8 +222,8 @@ pub async fn add(registry: RegistryUri, dependency: &str) -> miette::Result<()> } /// Removes a dependency from this project -pub async fn remove(package: PackageName) -> miette::Result<()> { - let mut manifest = Manifest::read().await?; +pub async fn remove(package: PackageName, config: &Config) -> miette::Result<()> { + let mut manifest = Manifest::read(config).await?; let store = PackageStore::current().await?; let dependency = manifest @@ -227,20 +239,22 @@ pub async fn remove(package: PackageName) -> miette::Result<()> { manifest.write().await } -/// Packages the api and writes it to the filesystem -pub async fn package( - directory: impl AsRef, - dry_run: bool, - version: Option, +/// Prepare package for local packaging or remote publishing +/// +/// # Arguments +/// * `set_version` - Desired manifest version +/// * `config` - Configuration data used for registry alias resolution +async fn prepare_package( + set_version: Option, preserve_mtime: bool, -) -> miette::Result<()> { - let mut manifest = Manifest::read().await?; + config: &Config, +) -> miette::Result { + let mut manifest = Manifest::read(config).await?; let store = PackageStore::current().await?; - if let Some(version) = version { + if let Some(version) = set_version { if let Some(ref mut package) = manifest.package { - tracing::info!(":: modified version in published manifest to {version}"); - + tracing::info!(":: modified version in manifest to {version}"); package.version = version; } } @@ -249,7 +263,26 @@ pub async fn package( store.populate(pkg).await?; } - let package = store.release(&manifest, preserve_mtime).await?; + let package = store + .release(&manifest, preserve_mtime, config, None) + .await?; + + // Ensure package was fully resolved + package.manifest.assert_fully_resolved()?; + + Ok(package) +} + +/// Packages the api and writes it to the filesystem +pub async fn package( + directory: impl AsRef, + dry_run: bool, + version: Option, + preserve_mtime: bool, + config: &Config, +) -> miette::Result<()> { + // Prepare the package + let package = prepare_package(version, preserve_mtime, config).await?; if dry_run { return Ok(()); @@ -270,14 +303,19 @@ pub async fn package( } /// Publishes the api package to the registry +#[allow(clippy::too_many_arguments)] pub async fn publish( - registry: RegistryUri, + registry: &RegistryRef, repository: String, #[cfg(feature = "git")] allow_dirty: bool, dry_run: bool, version: Option, preserve_mtime: bool, + config: &Config, + policy: CertValidationPolicy, ) -> miette::Result<()> { + let registry = registry.with_alias_resolved(Some(config))?; + #[cfg(feature = "git")] async fn git_statuses() -> miette::Result> { use std::process::Stdio; @@ -330,24 +368,9 @@ pub async fn publish( } } - let mut manifest = Manifest::read().await?; + let package = prepare_package(version, preserve_mtime, config).await?; let credentials = Credentials::load().await?; - let store = PackageStore::current().await?; - let artifactory = Artifactory::new(registry, &credentials)?; - - if let Some(version) = version { - if let Some(ref mut package) = manifest.package { - tracing::info!(":: modified version in published manifest to {version}"); - - package.version = version; - } - } - - if let Some(ref pkg) = manifest.package { - store.populate(pkg).await?; - } - - let package = store.release(&manifest, preserve_mtime).await?; + let artifactory = Artifactory::new(registry.try_into()?, &credentials, policy)?; if dry_run { tracing::warn!(":: aborting upload due to dry run"); @@ -357,11 +380,37 @@ pub async fn publish( artifactory.publish(package, repository).await } +/// Install mode for dependencies +pub enum InstallMode { + /// Only install dependencies, not the package itself + DependenciesOnly, + + /// Install the package and its dependencies + All, +} + +/// Options for optional generation of files +#[derive(Debug)] +pub enum GenerationOption { + /// Generate a buf.yaml file + BufYaml, +} + /// Installs dependencies /// -/// if [preserve_mtime] is true, local dependencies will keep their modification time -pub async fn install(preserve_mtime: bool) -> miette::Result<()> { - let manifest = Manifest::read().await?; +/// # Arguments +/// * `preserve_mtime` - If `true`, local dependencies will keep their modification time +/// * `mode` - The install mode (dependencies only or all) +/// * `generation` - Flags for generation of files +/// * `config` - The configuration +pub async fn install( + preserve_mtime: bool, + mode: InstallMode, + generation: &[GenerationOption], + config: &Config, + policy: CertValidationPolicy, +) -> miette::Result<()> { + let manifest = Manifest::read(config).await?; let lockfile = Lockfile::read_or_default().await?; let store = PackageStore::current().await?; let credentials = Credentials::load().await?; @@ -369,19 +418,24 @@ pub async fn install(preserve_mtime: bool) -> miette::Result<()> { store.clear().await?; - if let Some(ref pkg) = manifest.package { - store.populate(pkg).await?; + if let InstallMode::All = mode { + if let Some(ref pkg) = manifest.package { + store.populate(pkg).await?; - tracing::info!(":: installed {}@{}", pkg.name, pkg.version); + tracing::info!(":: installed {}@{}", pkg.name, pkg.version); + } } - let dependency_graph = DependencyGraph::from_manifest( + let dependency_graph = DependencyGraphBuilder::new( &manifest, &lockfile, - &credentials.into(), + &credentials, &cache, preserve_mtime, + config, + policy, ) + .build() .await .wrap_err(miette!("dependency resolution failed"))?; @@ -440,7 +494,7 @@ pub async fn install(preserve_mtime: bool) -> miette::Result<()> { Ok(()) } - for dependency in manifest.dependencies { + for dependency in &manifest.dependencies { traverse_and_install( &dependency.package, &dependency_graph, @@ -451,6 +505,14 @@ pub async fn install(preserve_mtime: bool) -> miette::Result<()> { .await?; } + for option in generation { + match option { + GenerationOption::BufYaml => { + buf_yaml::generate_buf_yaml_file(&dependency_graph, &manifest, &store)?; + } + } + } + Lockfile::from_iter(locked.into_iter()).write().await } @@ -460,9 +522,9 @@ pub async fn uninstall() -> miette::Result<()> { } /// Lists all protobuf files managed by Buffrs to stdout -pub async fn list() -> miette::Result<()> { +pub async fn list(config: &Config) -> miette::Result<()> { let store = PackageStore::current().await?; - let manifest = Manifest::read().await?; + let manifest = Manifest::read(config).await?; if let Some(ref pkg) = manifest.package { store.populate(pkg).await?; @@ -470,6 +532,17 @@ pub async fn list() -> miette::Result<()> { let protos = store.collect(&store.proto_vendor_path(), true).await; + // Canonicalize the protos + let protos = protos + .into_iter() + .map(|proto| { + proto + .canonicalize() + .into_diagnostic() + .wrap_err(miette!("failed to canonicalize proto path")) + }) + .collect::>>()?; + let cwd = { let cwd = std::env::current_dir() .into_diagnostic() @@ -485,9 +558,10 @@ pub async fn list() -> miette::Result<()> { let rel = proto .strip_prefix(&cwd) .into_diagnostic() - .wrap_err(miette!("failed to transform protobuf path"))?; + .wrap_err(miette!("failed to transform protobuf path"))? + .to_posix_string(); - print!("{} ", rel.display()) + print!("{} ", rel) } Ok(()) @@ -495,8 +569,8 @@ pub async fn list() -> miette::Result<()> { /// Parses current package and validates rules. #[cfg(feature = "validation")] -pub async fn lint() -> miette::Result<()> { - let manifest = Manifest::read().await?; +pub async fn lint(config: &Config) -> miette::Result<()> { + let manifest = Manifest::read(config).await?; let store = PackageStore::current().await?; let pkg = manifest.package.ok_or(miette!( @@ -516,28 +590,41 @@ pub async fn lint() -> miette::Result<()> { } /// Logs you in for a registry -pub async fn login(registry: RegistryUri) -> miette::Result<()> { +/// +/// # Arguments +/// * `registry` - The registry to log in to +/// * `token` - An optional token to use, if not provided, the user will be prompted for one +pub async fn login( + registry: &RegistryRef, + token: Option, + policy: CertValidationPolicy, + config: &Config, +) -> miette::Result<()> { let mut credentials = Credentials::load().await?; + let registry: RegistryUri = registry.with_alias_resolved(Some(config))?.try_into()?; - tracing::info!(":: please enter your artifactory token:"); + let token = match token { + Some(token) => token, + None => { + tracing::info!(":: please enter your artifactory token:"); - let token = { - let mut raw = String::new(); - let mut reader = BufReader::new(io::stdin()); + let mut raw = String::new(); + let mut reader = BufReader::new(io::stdin()); - reader - .read_line(&mut raw) - .await - .into_diagnostic() - .wrap_err(miette!("failed to read the token from the user"))?; + reader + .read_line(&mut raw) + .await + .into_diagnostic() + .wrap_err(miette!("failed to read the token from the user"))?; - raw.trim().into() + raw.trim().into() + } }; credentials.registry_tokens.insert(registry.clone(), token); if env::var(BUFFRS_TESTSUITE_VAR).is_err() { - Artifactory::new(registry, &credentials)? + Artifactory::new(registry, &credentials, policy)? .ping() .await .wrap_err(miette!("failed to validate token"))?; @@ -547,7 +634,8 @@ pub async fn login(registry: RegistryUri) -> miette::Result<()> { } /// Logs you out from a registry -pub async fn logout(registry: RegistryUri) -> miette::Result<()> { +pub async fn logout(registry: &RegistryRef, config: &Config) -> miette::Result<()> { + let registry: RegistryUri = registry.with_alias_resolved(Some(config))?.try_into()?; let mut credentials = Credentials::load().await?; credentials.registry_tokens.remove(®istry); credentials.write().await @@ -562,7 +650,7 @@ pub mod lock { pub async fn print_files() -> miette::Result<()> { let lock = Lockfile::read().await?; - let requirements: Vec = lock.into(); + let requirements: Vec = lock.try_into()?; // hint: always ok, as per serde_json doc if let Ok(json) = serde_json::to_string_pretty(&requirements) { diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 00000000..b7f98386 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,368 @@ +// Copyright 2024 Globus Medical, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use miette::{bail, miette, Context, IntoDiagnostic}; +use serde::{de, Deserialize, Deserializer, Serialize}; + +use crate::{ + manifest::{Edition, CANARY_EDITION}, + registry::{RegistryAlias, RegistryRef, RegistryUri}, +}; + +/// Location of the configuration file +const CONFIG_FILE: &str = ".buffrs/config.toml"; + +/// Representation of the configuration file +/// +/// # Example +/// +/// ```toml +/// edition = "0.10.0" +/// +/// [registries] +/// some_org = "https://artifactory.example.com/artifactory/some-org" +/// +/// [registry] +/// default = "some_org" +/// +/// [commands] +/// default_args = ["--insecure"] +/// +/// [commands.install] +/// default_args = ["--generate-buf-yaml"] +/// ``` +/// +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Config { + /// Edition of this configuration file (in sync with the Proto.toml edition) + #[serde(deserialize_with = "validate_edition")] + edition: Edition, + + /// Path to the configuration file + #[serde(skip)] + config_path: Option, + + /// Default registry alias to use if none is specified + #[serde(default)] + pub registry: RegistryConfig, + + /// List of registries + #[serde(default)] + registries: HashMap, + + /// Default arguments for commands + #[serde(rename = "commands", default)] + command_defaults: Commands, +} + +/// Registry configuration +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct RegistryConfig { + /// Default registry alias to use if none is specified + pub default: Option, +} + +/// Commands configuration, including global and per-command default arguments +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct Commands { + /// Default arguments for all commands + #[serde(rename = "default_args", default)] + pub common: Option>, + + /// Specific command configurations + #[serde(flatten)] + pub specific: HashMap, +} + +/// Per-command configuration +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CommandConfig { + /// Default arguments for this command + #[serde(rename = "default_args", default)] + pub default_args: Option>, +} + +impl Config { + /// Create a new configuration with default values + pub fn new() -> Self { + Self { + edition: Edition::latest(), + config_path: None, + registry: RegistryConfig::default(), + registries: HashMap::new(), + command_defaults: Commands::default(), + } + } + + /// Create configuration from the workspace directory + /// by locating the configuration file in the workspace. + /// + /// # Arguments + /// * `workspace` - Path to the workspace directory + /// + /// # Returns + /// Configuration loaded from file if found; or default configuration otherwise. + pub fn new_from_workspace(workspace: &Path) -> miette::Result { + let config_path = Self::locate_config(Some(workspace)); + match config_path { + Some(config_path) => Self::new_from_config_file(&config_path), + None => Ok(Default::default()), + } + } + + /// Create configuration from a TOML file + /// + /// # Arguments + /// * `config_path` - Path to the configuration file + pub fn new_from_config_file(config_path: &Path) -> miette::Result { + let config_str = std::fs::read_to_string(config_path) + .into_diagnostic() + .wrap_err(miette!( + "failed to read config file: {}", + config_path.display() + ))?; + + let raw_config: toml::Value = + toml::from_str(&config_str) + .into_diagnostic() + .wrap_err(miette!( + "failed to parse config file: {}", + config_path.display() + ))?; + + // Validate and parse the edition + let edition = raw_config + .get("edition") + .and_then(|edition| edition.as_str()) + .ok_or_else(|| miette!("missing or invalid 'edition' field in config file"))? + .into(); + + match edition { + Edition::Canary => (), + _ => bail!( + "unsupported config file edition '{}', supported editions: {}", + Into::<&str>::into(edition), + CANARY_EDITION + ), + } + + // Deserialize the remaining fields into the `Config` struct + let mut config: Config = + toml::from_str(&config_str) + .into_diagnostic() + .wrap_err(miette!( + "failed to parse configuration fields in file: {}", + config_path.display() + ))?; + + // Set the edition and config path manually + config.edition = edition; + config.config_path = Some(config_path.to_path_buf()); + + Ok(config) + } + + /// Parse a registry argument + /// + /// # Arguments + /// * `registry` - The registry argument to parse + /// + /// # Returns + /// URI with either alias scheme or actual URI: + /// - -> alias:// + /// - -> + /// - None -> alias:// + pub fn parse_registry_arg(&self, registry: &Option) -> miette::Result { + match registry { + Some(registry) => registry.parse(), + None => match &self.registry.default { + Some(default_registry) => default_registry.parse(), + None => bail!("no registry provided and no default registry found"), + }, + } + } + + /// Lookup a registry by name + /// + /// # Arguments + /// * `name` - Name of the registry to lookup + /// + /// # Returns + /// The registry URI + pub(crate) fn lookup_registry(&self, name: &RegistryAlias) -> miette::Result { + self.registries.get(name).cloned().ok_or_else(|| { + miette!( + "registry '{}' not found in {}", + name, + self.config_path + .clone() + .unwrap_or("config file".into()) + .display() + ) + }) + } + + /// Get the default arguments for a specific command + /// + /// # Arguments + /// * `command` - The command name to get default arguments for, or None for global defaults + /// + /// # Returns + /// A vector of default arguments for the specified command + pub fn get_default_args(&self, command: Option<&str>) -> Vec { + self.command_defaults + .get(command) + .cloned() + .unwrap_or_default() + } + + /// Locate the configuration file in the current directory or any parent directories + /// + /// # Arguments + /// * `cwd` - Starting directory to search for the configuration file + /// + /// # Returns + /// Some(PathBuf) if the configuration file is found, None otherwise + pub fn locate_config(cwd: Option<&Path>) -> Option { + if let Some(cwd) = cwd { + let mut current_dir = cwd.to_owned(); + + loop { + let config_path = current_dir.join(CONFIG_FILE); + if config_path.exists() { + return Some(config_path); + } + + if !current_dir.pop() { + break; + } + } + } + + None + } +} + +impl Default for Config { + fn default() -> Self { + Self::new() + } +} + +fn validate_edition<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let edition: String = Deserialize::deserialize(deserializer)?; + match edition.as_str() { + "0.10" | "canary" => Ok(Edition::from(edition.as_str())), + _ => Err(de::Error::custom(format!( + "unsupported config file edition '{}', supported editions: 0.10, canary", + edition + ))), + } +} + +impl Commands { + /// Get the default arguments for a specific command + /// + /// # Arguments + /// * `command` - The command name to get default arguments for, or None for global defaults + /// + /// # Returns + /// A vector of default arguments for the specified command + pub fn get(&self, command: Option<&str>) -> Option<&Vec> { + match command { + Some(command) => self + .specific + .get(command) + .and_then(|config| config.default_args.as_ref()), + None => self.common.as_ref(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_fs::TempDir; + use std::{fs::File, io::Write}; + + #[test] + fn test_new_from_config_file() { + let tmp_dir = TempDir::new().unwrap(); + let config_path = tmp_dir.path().join(CONFIG_FILE); + std::fs::create_dir_all(config_path.parent().unwrap()).unwrap(); + let mut file = File::create(&config_path).unwrap(); + file.write_all( + br#" +edition = "0.10" + +[registry] +default = "acme" + +[registries] +acme = "https://conan.acme.com/artifactory" + +[commands] +default_args = ["--insecure"] + +[commands.install] +default_args = ["--generate-buf-yaml", "--generate-tonic-proto-module", "src/proto.rs"] +"#, + ) + .unwrap(); + + let alias: RegistryAlias = "acme".parse().unwrap(); + let config = Config::new_from_config_file(&config_path).unwrap(); + assert_eq!(config.edition, Edition::latest()); + assert_eq!(config.registry.default, Some("acme".to_string())); + assert_eq!( + config.registries.get(&alias).unwrap(), + &"https://conan.acme.com/artifactory".parse().unwrap() + ); + assert_eq!( + config.command_defaults.get(Some("install")).unwrap(), + &vec![ + "--generate-buf-yaml".to_string(), + "--generate-tonic-proto-module".to_string(), + "src/proto.rs".to_string() + ] + ); + assert_eq!( + config.command_defaults.get(None).unwrap(), + &vec!["--insecure".to_string()] + ); + } + + #[test] + fn test_new_from_empty_config_file() { + let tmp_dir = TempDir::new().unwrap(); + let config_path = tmp_dir.path().join(CONFIG_FILE); + std::fs::create_dir_all(config_path.parent().unwrap()).unwrap(); + let mut file = File::create(&config_path).unwrap(); + file.write_all(br#"edition = "0.10""#).unwrap(); + + let config = Config::new_from_config_file(&config_path).unwrap(); + assert_eq!(config.edition, Edition::latest()); + assert_eq!(config.registry.default, None); + assert!(config.registries.is_empty()); + assert!(config.command_defaults.common.is_none()); + assert!(config.command_defaults.specific.is_empty()); + } +} diff --git a/src/errors.rs b/src/errors.rs index 86a6b7eb..7efc1254 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -25,3 +25,8 @@ pub(crate) struct SerializationError(pub ManagedFile); #[derive(thiserror::Error, Diagnostic, Debug)] #[error("file `{0}` is missing")] pub(crate) struct FileNotFound(pub String); + +/// Error for when a registry name is invalid. +#[derive(thiserror::Error, Diagnostic, Debug)] +#[error("Invalid registry name format")] +pub struct RegistryNameError; diff --git a/src/integration/buf_yaml.rs b/src/integration/buf_yaml.rs new file mode 100644 index 00000000..81f8f42e --- /dev/null +++ b/src/integration/buf_yaml.rs @@ -0,0 +1,350 @@ +use miette::{miette, Context, IntoDiagnostic}; +use pretty_yaml::config::FormatOptions; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fs, io::Write, path::PathBuf}; +use walkdir::WalkDir; + +use crate::{manifest::Manifest, package::PackageStore, resolver::DependencyGraph}; + +use super::path_util::PathUtil; + +const BUF_YAML_FILE: &str = "buf.yaml"; + +/// Representation of a Buf YAML file +pub struct BufYamlFile { + config: Config, + buf_yaml_path: PathBuf, + proto_rel_path: String, + vendor_rel_path: String, +} + +impl BufYamlFile { + /// Create default `BufYamlFile` with default values + pub fn new(store: &PackageStore) -> miette::Result { + Self::new_from_str(DEFAULT_YAML, store) + } + + /// Create a new `BufYamlFile` from a string + pub fn new_from_str(s: &str, store: &PackageStore) -> miette::Result { + let config: Config = serde_yml::from_str(s).into_diagnostic()?; + + // Version needs to be v2 + if config.version != "v2" { + return Err(miette!("Only v2 is supported for buf.yaml files")); + } + + let proto_path = store.proto_path(); + let proto_vendor_path = store.proto_vendor_path(); + let buf_yaml_path = proto_path.join(BUF_YAML_FILE); + let proto_rel_path = ".".to_owned(); + let vendor_rel_path = proto_vendor_path + .relative_to(&proto_path) + .unwrap_or(proto_vendor_path.clone()) + .to_posix_string(); + + Ok(Self { + config, + buf_yaml_path, + proto_rel_path, + vendor_rel_path, + }) + } + + /// Load `BufYamlFile` from buf.yaml file + pub fn from_file(store: &PackageStore) -> miette::Result { + let proto_path = store.proto_path(); + let buf_yaml_path = proto_path.join(BUF_YAML_FILE); + let yaml_content = fs::read_to_string(buf_yaml_path).into_diagnostic()?; + Self::new_from_str(&yaml_content, store) + } + + /// Serialize the `BufYamlFile` to a string + pub fn to_string(&self) -> miette::Result { + let yaml = serde_yml::to_string(&self.config).into_diagnostic()?; + + // prettyfy the output + let options = FormatOptions::default(); + pretty_yaml::format_text(&yaml, &options).into_diagnostic() + } + + /// Write `BufYamlFile` to a YAML file + pub fn to_file(&self) -> miette::Result<()> { + let yaml_content = self.to_string()?; + let mut file = fs::File::create(&self.buf_yaml_path).into_diagnostic()?; + file.write_all(yaml_content.as_bytes()).into_diagnostic()?; + Ok(()) + } + + /// Clear all modules from the Buf YAML file + pub fn clear_modules(&mut self) { + self.config.modules.clear(); + } + + /// Add non-vendor module to the Buf YAML file + pub fn add_module(&mut self) { + self.config.modules.push(Module { + path: self.proto_rel_path.clone(), + excludes: vec![self.vendor_rel_path.clone()], + ..Default::default() + }); + } + + /// Add vendor modules to the Buf YAML file + pub fn set_vendor_modules(&mut self, vendor_modules: Vec) { + // Add vendor modules + for module in vendor_modules { + self.config.modules.push(Module { + path: format!("{}/{}", &self.vendor_rel_path, module), + ..Default::default() + }); + } + } +} + +/// Generates a buf.yaml file matching the current dependency graph +pub fn generate_buf_yaml_file( + dependency_graph: &DependencyGraph, + manifest: &Manifest, + store: &PackageStore, +) -> miette::Result<()> { + // The file will be created in the "store" directory + let store_dir = store.proto_path(); + let buf_yaml_file = store_dir.join(BUF_YAML_FILE); + + let mut buf_yaml = if buf_yaml_file.exists() { + BufYamlFile::from_file(store).wrap_err(miette!( + "failed to read buf.yaml file at {}.", + store_dir.display() + ))? + } else { + BufYamlFile::new(store)? + }; + + let mut vendor_modules: Vec = dependency_graph + .get_package_names() + .iter() + .map(|p| p.to_string()) + .collect(); + + vendor_modules.sort(); + buf_yaml.clear_modules(); + + if manifest.package.is_some() { + // double-check that the package really contains proto files + // under proto/** (but not under proto/vendor/**) + let vendor_path = store.proto_vendor_path(); + let mut has_protos = false; + for entry in WalkDir::new(store.proto_path()).into_iter().flatten() { + if entry.path().is_file() { + let path = entry.path(); + if path.starts_with(&vendor_path) { + continue; + } + + if path.extension().map_or(false, |ext| ext == "proto") { + has_protos = true; + break; + } + } + } + + if has_protos { + buf_yaml.add_module(); + } + } + buf_yaml.set_vendor_modules(vendor_modules); + buf_yaml + .to_file() + .wrap_err(miette!("failed to write buf.yaml file"))?; + Ok(()) +} + +/// Default buf.yaml file +const DEFAULT_YAML: &str = r#" +version: v2 + +modules: +lint: + except: + - PACKAGE_VERSION_SUFFIX +breaking: + use: + - FILE +deps: + - buf.build/googleapis/googleapis + - buf.build/grpc/grpc +"#; + +#[derive(Debug, Serialize, Deserialize)] +struct Config { + version: String, + modules: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + deps: Vec, + lint: Option, + breaking: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + plugins: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +struct Module { + path: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + name: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + excludes: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + includes: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + lint: Option, + #[serde(default, skip_serializing_if = "is_false")] + disallow_comment_ignores: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + enum_zero_value_suffix: Option, + #[serde(default, skip_serializing_if = "is_false")] + rpc_allow_same_request_response: bool, + #[serde(default, skip_serializing_if = "is_false")] + rpc_allow_google_protobuf_empty_requests: bool, + #[serde(default, skip_serializing_if = "is_false")] + rpc_allow_google_protobuf_empty_responses: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + service_suffix: Option, + #[serde(default, skip_serializing_if = "is_false")] + disable_builtin: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + breaking: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct LintConfig { + #[serde(rename = "use", default, skip_serializing_if = "Vec::is_empty")] + use_rules: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + except: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + ignore: Vec, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + ignore_only: HashMap>, +} + +#[derive(Debug, Serialize, Deserialize)] +struct BreakingConfig { + #[serde(rename = "use", default, skip_serializing_if = "Vec::is_empty")] + use_rules: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + except: Vec, + #[serde(default, skip_serializing_if = "is_false")] + ignore_unstable_packages: bool, + #[serde(default, skip_serializing_if = "is_false")] + disable_builtin: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Plugin { + plugin: String, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + options: HashMap, +} + +#[derive(Debug, Serialize, Deserialize)] +struct PluginOptions { + timestamp_suffix: Option, +} + +fn is_false(b: impl std::borrow::Borrow) -> bool { + !b.borrow() +} + +#[cfg(test)] +mod tests { + use assert_fs::TempDir; + use std::io::Write; + + use super::*; + + #[test] + fn test_gen_default_buf_yaml() {} + + #[test] + fn test_new_from_str() { + let tmp_dir = TempDir::new().unwrap(); + let store = PackageStore::new(tmp_dir.path().to_owned()); + let buf_yaml = BufYamlFile::new_from_str(SAMPLE_YAML, &store).unwrap(); + + // serialize to file + let serialized = buf_yaml.to_string().unwrap(); + + let yaml_path = tmp_dir.join(BUF_YAML_FILE); + let writer = std::fs::File::create(yaml_path).unwrap(); + let mut writer = std::io::BufWriter::new(writer); + writer.write_all(serialized.as_bytes()).unwrap(); + } + + const SAMPLE_YAML: &str = r#" +version: v2 +modules: + - path: proto/foo + name: buf.build/acme/foo + - path: proto/bar + name: buf.build/acme/bar + excludes: + - proto/bar/a + - proto/bar/b/e.proto + lint: + use: + - STANDARD + except: + - IMPORT_USED + ignore: + - proto/bar/c + ignore_only: + ENUM_ZERO_VALUE_SUFFIX: + - proto/bar/a + - proto/bar/b/f.proto + disallow_comment_ignores: false + enum_zero_value_suffix: _UNSPECIFIED + rpc_allow_same_request_response: false + rpc_allow_google_protobuf_empty_requests: false + rpc_allow_google_protobuf_empty_responses: false + service_suffix: Service + disable_builtin: false + breaking: + use: + - FILE + except: + - EXTENSION_MESSAGE_NO_DELETE + ignore_unstable_packages: true + disable_builtin: false + - path: proto/common + module: buf.build/acme/weather + includes: + - proto/common/weather + - path: proto/common + module: buf.build/acme/location + includes: + - proto/common/location + excludes: + - proto/common/location/test + - path: proto/common + module: buf.build/acme/other + excludes: + - proto/common/location + - proto/common/weather +deps: + - buf.build/acme/paymentapis + - buf.build/acme/pkg:47b927cbb41c4fdea1292bafadb8976f + - buf.build/googleapis/googleapis:v1beta1.1.0 +lint: + use: + - STANDARD + - TIMESTAMP_SUFFIX +breaking: + use: + - FILE +plugins: + - plugin: plugin-timestamp-suffix + options: + timestamp_suffix: _time +"#; +} diff --git a/src/integration/mod.rs b/src/integration/mod.rs new file mode 100644 index 00000000..3cbcf045 --- /dev/null +++ b/src/integration/mod.rs @@ -0,0 +1,4 @@ +/// buf.yaml generation +pub mod buf_yaml; +/// Path utilities +pub mod path_util; diff --git a/src/integration/path_util.rs b/src/integration/path_util.rs new file mode 100644 index 00000000..b1d19401 --- /dev/null +++ b/src/integration/path_util.rs @@ -0,0 +1,97 @@ +use std::path::{Component, Path, PathBuf}; + +/// Extension trait for `Path`. +pub trait PathUtil { + /// Convert a path to a string, replacing backslashes with forward slashes on Windows. + fn to_posix_string(&self) -> String; + + /// Get the relative path of `self` from `base`. + /// + /// If `self` is not a subpath of `base`, the relative path + /// uses `..` to traverse up to `base`. If `this` isn't reachable + /// from `base`, `None` is returned. + /// + /// # Warning + /// + /// - This function does not check if the paths are valid or if they exist. + /// - Hence, this function does not consider symbolic links. + /// + /// # Arguments + /// * `base` - The base path + fn relative_to(&self, base: &Path) -> Option; +} + +impl PathUtil for Path { + fn to_posix_string(&self) -> String { + #[cfg(windows)] + let path = self.to_string_lossy().replace('\\', "/"); + #[cfg(not(windows))] + let path = self.to_string_lossy().to_string(); + path + } + + fn relative_to(&self, base: &Path) -> Option { + let mut ita = self.components(); + let mut itb = base.components(); + let mut comps: Vec = vec![]; + + loop { + match (ita.next(), itb.next()) { + (None, None) => break, + (Some(a), None) => { + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + (None, _) => comps.push(Component::ParentDir), + (Some(a), Some(b)) if comps.is_empty() && a == b => (), + (Some(a), Some(Component::CurDir)) => comps.push(a), + (Some(_), Some(Component::ParentDir)) => return None, + (Some(a), Some(_)) => { + comps.push(Component::ParentDir); + for _ in itb { + comps.push(Component::ParentDir); + } + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + } + } + + Some(comps.iter().map(|c| c.as_os_str()).collect()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_posix_string() { + #[cfg(windows)] + let path = Path::new("foo\\bar\\baz"); + #[cfg(not(windows))] + let path = Path::new("foo/bar/baz"); + assert_eq!(path.to_posix_string(), "foo/bar/baz"); + } + + #[test] + fn test_relative_from() { + let path = Path::new("foo/bar/baz"); + let base = Path::new("foo/bar"); + assert_eq!(path.relative_to(base), Some(PathBuf::from("baz"))); + + let path = Path::new("foo/bar/baz"); + let base = Path::new("foo/bar/baz"); + assert_eq!(path.relative_to(base), Some(PathBuf::new())); + + let path = Path::new("foo/bar/baz"); + let base = Path::new("foo/bar/baz/qux"); + assert_eq!(path.relative_to(base), Some(PathBuf::from(".."))); + + let path = Path::new("foo/bar/baz"); + let base = Path::new("foo/bar/qux"); + assert_eq!(path.relative_to(base), Some(PathBuf::from("../baz"))); + } +} diff --git a/src/lib.rs b/src/lib.rs index 9db2fa12..512ddf25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,10 +23,14 @@ use thiserror::Error; pub mod cache; /// CLI command implementations pub mod command; +/// Configuration file (.buffrs/config.toml) handling +pub mod config; /// Credential management pub mod credentials; /// Common error types pub mod errors; +/// Integration with external tools +pub mod integration; /// Lockfile implementation pub mod lock; /// Manifest format and IO diff --git a/src/lock.rs b/src/lock.rs index 8a56d034..b3dc760f 100644 --- a/src/lock.rs +++ b/src/lock.rs @@ -24,7 +24,7 @@ use url::Url; use crate::{ errors::{DeserializationError, FileExistsError, FileNotFound, SerializationError, WriteError}, package::{Package, PackageName}, - registry::RegistryUri, + registry::{RegistryRef, RegistryUri}, ManagedFile, }; @@ -44,7 +44,8 @@ pub struct LockedPackage { /// The cryptographic digest of the package contents pub digest: Digest, /// The URI of the registry that contains the package - pub registry: RegistryUri, + #[serde(serialize_with = "RegistryRef::serialize_resolved")] + pub registry: RegistryRef, /// The identifier of the repository where the package was published pub repository: String, /// The exact version of the package @@ -61,7 +62,7 @@ impl LockedPackage { /// Captures the source, version and checksum of a Package for use in reproducible installs pub fn lock( package: &Package, - registry: RegistryUri, + registry: RegistryRef, repository: String, dependants: usize, ) -> Self { @@ -220,9 +221,14 @@ impl FromIterator for Lockfile { } } -impl From for Vec { - fn from(lock: Lockfile) -> Self { - lock.packages.values().map(FileRequirement::from).collect() +impl TryFrom for Vec { + type Error = miette::Report; + + fn try_from(lock: Lockfile) -> miette::Result { + lock.packages + .values() + .map(FileRequirement::try_from) + .collect() } } @@ -244,13 +250,15 @@ impl FileRequirement { /// Construct new file requirement. pub fn new( - url: &RegistryUri, + url: &RegistryRef, repository: &String, name: &PackageName, version: &Version, digest: &Digest, - ) -> Self { - let mut url = url.clone(); + ) -> miette::Result { + let url: RegistryUri = url.try_into()?; + let mut url: url::Url = url.into(); + let new_path = format!( "{}/{}/{}/{}-{}.tgz", url.path(), @@ -262,16 +270,18 @@ impl FileRequirement { url.set_path(&new_path); - Self { + Ok(Self { package: name.to_owned(), - url: url.into(), + url, digest: digest.clone(), - } + }) } } -impl From for FileRequirement { - fn from(package: LockedPackage) -> Self { +impl TryFrom for FileRequirement { + type Error = miette::Report; + + fn try_from(package: LockedPackage) -> miette::Result { Self::new( &package.registry, &package.repository, @@ -282,8 +292,10 @@ impl From for FileRequirement { } } -impl From<&LockedPackage> for FileRequirement { - fn from(package: &LockedPackage) -> Self { +impl TryFrom<&LockedPackage> for FileRequirement { + type Error = miette::Report; + + fn try_from(package: &LockedPackage) -> miette::Result { Self::new( &package.registry, &package.repository, diff --git a/src/main.rs b/src/main.rs index 8efcdbbd..1fcfa50b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,19 +12,33 @@ // See the License for the specific language governing permissions and // limitations under the License. -use buffrs::command; +use std::env; + +use buffrs::command::{self, GenerationOption, InstallMode}; +use buffrs::config::Config; use buffrs::manifest::Manifest; use buffrs::package::{PackageName, PackageStore}; -use buffrs::registry::RegistryUri; +use buffrs::registry::CertValidationPolicy; use buffrs::{manifest::MANIFEST_FILE, package::PackageType}; -use clap::{Parser, Subcommand}; -use miette::{miette, WrapErr}; +use clap::{CommandFactory, Parser, Subcommand}; +use miette::{miette, IntoDiagnostic, WrapErr}; use semver::Version; #[derive(Parser)] #[command(author, version, about, long_about)] #[command(propagate_version = true)] struct Cli { + /// Opt out of applying default arguments from config + #[clap(long)] + ignore_defaults: bool, + + /// Disable certificate validation + /// + /// By default, every secure connection buffrs makes will validate the certificate chain. + /// This option makes buffrs skip the verification step and proceed without checking. + #[clap(long, long = "insecure", short = 'k')] + disable_cert_validation: bool, + #[command(subcommand)] command: Command, } @@ -68,7 +82,7 @@ enum Command { Add { /// Artifactory url (e.g. https:///artifactory) #[clap(long)] - registry: RegistryUri, + registry: Option, /// Dependency to add (Format /@ dependency: String, }, @@ -104,7 +118,7 @@ enum Command { Publish { /// Artifactory url (e.g. https:///artifactory) #[clap(long)] - registry: RegistryUri, + registry: Option, /// Destination repository for the release #[clap(long)] repository: String, @@ -131,7 +145,16 @@ enum Command { /// Default is 'true' #[clap(long)] preserve_local_mtime: Option, + + /// Only install dependencies + #[clap(long, default_value = "false")] + only_dependencies: bool, + + /// Generate buf.yaml file matching the installed dependencies + #[clap(long, default_value = "false")] + generate_buf_yaml: bool, }, + /// Uninstalls dependencies Uninstall, @@ -143,13 +166,17 @@ enum Command { Login { /// Artifactory url (e.g. https:///artifactory) #[clap(long)] - registry: RegistryUri, + registry: Option, + + /// Token to use for login (if not provided, will prompt for input) + #[clap(long)] + token: Option, }, /// Logs you out from a registry Logout { /// Artifactory url (e.g. https:///artifactory) #[clap(long)] - registry: RegistryUri, + registry: Option, }, /// Lockfile related commands @@ -181,17 +208,31 @@ async fn main() -> miette::Result<()> { .try_init() .unwrap(); - let cli = Cli::parse(); + // The CLI handling is part of the library crate. + // This allows build.rs scripts to simply declare buffrs as + // a dependency and use the CLI without any additional setup. + let args = env::args().collect::>(); + run(&args).await +} + +/// Main entry point for the CLI +async fn run(args: &[String]) -> miette::Result<()> { + let cwd = std::env::current_dir().into_diagnostic()?; + let config = Config::new_from_workspace(&cwd)?; + + // Merge default arguments with user-specified arguments + let merged_args = merge_args_with_defaults(&config, args); + + // Parse CLI with merged arguments + let cli = Cli::parse_from(merged_args); let manifest = if Manifest::exists().await? { - Some(Manifest::read().await?) + Some(Manifest::read(&config).await?) } else { None }; let package = { - let cwd = std::env::current_dir().unwrap(); - let name = cwd .file_name() .ok_or_else(|| miette!("failed to locate current directory"))? @@ -203,6 +244,12 @@ async fn main() -> miette::Result<()> { .unwrap_or_else(|| name.to_string()) }; + let policy = if cli.disable_cert_validation { + CertValidationPolicy::NoValidation + } else { + CertValidationPolicy::Validate + }; + match cli.command { Command::Init { lib, api, package } => { let kind = infer_package_type(lib, api); @@ -221,23 +268,36 @@ async fn main() -> miette::Result<()> { .await .wrap_err(miette!("failed to initialize {}", format!("`{package}`"))) } - Command::Login { registry } => command::login(registry.to_owned()) - .await - .wrap_err(miette!("failed to login to `{registry}`")), - Command::Logout { registry } => command::logout(registry.to_owned()) - .await - .wrap_err(miette!("failed to logout from `{registry}`")), + Command::Login { registry, token } => { + let registry = config.parse_registry_arg(®istry)?; + command::login(®istry, token, policy, &config) + .await + .wrap_err(miette!("failed to login to `{registry}`")) + } + Command::Logout { registry } => { + let registry = config.parse_registry_arg(®istry)?; + command::logout(®istry, &config) + .await + .wrap_err(miette!("failed to logout from `{registry}`")) + } Command::Add { registry, dependency, - } => command::add(registry.to_owned(), &dependency) - .await - .wrap_err(miette!( - "failed to add `{dependency}` from `{registry}` to `{MANIFEST_FILE}`" - )), - Command::Remove { package } => command::remove(package.to_owned()).await.wrap_err(miette!( - "failed to remove `{package}` from `{MANIFEST_FILE}`" - )), + } => { + let registry = config.parse_registry_arg(®istry)?; + command::add(®istry, &dependency, &config, policy) + .await + .wrap_err(miette!( + "failed to add `{dependency}` from `{registry}` to `{MANIFEST_FILE}`" + )) + } + Command::Remove { package } => { + command::remove(package.to_owned(), &config) + .await + .wrap_err(miette!( + "failed to remove `{package}` from `{MANIFEST_FILE}`" + )) + } Command::Package { output_directory, dry_run, @@ -248,6 +308,7 @@ async fn main() -> miette::Result<()> { dry_run, set_version, preserve_mtime.unwrap_or(true), + &config, ) .await .wrap_err(miette!( @@ -260,31 +321,59 @@ async fn main() -> miette::Result<()> { dry_run, set_version, preserve_mtime, - } => command::publish( - registry.to_owned(), - repository.to_owned(), - allow_dirty, - dry_run, - set_version, - preserve_mtime.unwrap_or(true), - ) - .await - .wrap_err(miette!( - "failed to publish `{package}` to `{registry}:{repository}`", - )), - Command::Lint => command::lint().await.wrap_err(miette!( + } => { + let registry = config.parse_registry_arg(®istry)?; + command::publish( + ®istry, + repository.to_owned(), + #[cfg(feature = "git")] + allow_dirty, + dry_run, + set_version, + preserve_mtime.unwrap_or(true), + &config, + policy, + ) + .await + .wrap_err(miette!( + "failed to publish `{package}` to `{registry}:{repository}`", + )) + } + Command::Lint => command::lint(&config).await.wrap_err(miette!( "failed to lint protocol buffers in `{}`", PackageStore::PROTO_PATH )), Command::Install { preserve_local_mtime, - } => command::install(preserve_local_mtime.unwrap_or(true)) + only_dependencies, + generate_buf_yaml, + } => { + let mut generation_options = Vec::new(); + + if generate_buf_yaml { + generation_options.push(GenerationOption::BufYaml); + } + + let install_mode = if only_dependencies { + InstallMode::DependenciesOnly + } else { + InstallMode::All + }; + + command::install( + preserve_local_mtime.unwrap_or(true), + install_mode, + &generation_options, + &config, + policy, + ) .await - .wrap_err(miette!("failed to install dependencies for `{package}`")), + .wrap_err(miette!("failed to install dependencies for `{package}`")) + } Command::Uninstall => command::uninstall() .await .wrap_err(miette!("failed to uninstall dependencies for `{package}`")), - Command::List => command::list().await.wrap_err(miette!( + Command::List => command::list(&config).await.wrap_err(miette!( "failed to list installed protobuf files for `{package}`" )), Command::Lock { command } => match command { @@ -304,3 +393,51 @@ fn infer_package_type(lib: bool, api: bool) -> Option { None } } + +/// Retrieve and merge default arguments with user-provided arguments +/// +/// # Arguments +/// * `config` - The configuration object +/// * `args` - The user-provided arguments +/// +/// # Returns +/// A vector of arguments with default arguments merged in +pub fn merge_args_with_defaults(config: &Config, args: &[String]) -> Vec { + // Check if --ignore-defaults is in the arguments + let initial_cli = Cli::try_parse_from(args); + if let Ok(cli) = initial_cli { + if cli.ignore_defaults { + return args.to_vec(); // Return original arguments if --ignore-defaults is set + } + } + + // Parse the CLI matches to find the subcommand + let cli_matches = Cli::command().get_matches_from(args); + let mut args = args.to_vec(); + + // Fetch sub-command-specific defaults if a subcommand is present + if let Some(subcommand) = cli_matches.subcommand_name() { + let command_specific_args = config.get_default_args(Some(subcommand)); + + // Find the position of the subcommand in the arguments + if let Some(position) = args.iter().position(|arg| arg == subcommand) { + // Insert command-specific defaults after the subcommand + let user_args: std::collections::HashSet<_> = args.iter().collect(); + let filtered_defaults: Vec = command_specific_args + .into_iter() + .filter(|arg| !user_args.contains(arg)) + .collect(); + args.splice(position + 1..position + 1, filtered_defaults); + } + } + + // Always fetch common defaults + let mut default_args = config.get_default_args(None); + + // Prepend common defaults before all other arguments, filtering duplicates + let user_args: std::collections::HashSet<_> = args.iter().collect(); + default_args.retain(|arg| !user_args.contains(arg)); + args.splice(1..1, default_args); + + args +} diff --git a/src/manifest.rs b/src/manifest.rs index b6db64c6..72508d12 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -24,9 +24,10 @@ use std::{ use tokio::fs; use crate::{ + config::{self}, errors::{DeserializationError, FileExistsError, SerializationError, WriteError}, package::{PackageName, PackageType}, - registry::RegistryUri, + registry::RegistryRef, ManagedFile, }; @@ -46,6 +47,8 @@ pub enum Edition { /// at any time. Users are responsible for consulting documentation and /// help channels if errors occur. Canary, + /// The canary edition used by buffrs 0.9.x + Canary09, /// The canary edition used by buffrs 0.8.x Canary08, /// The canary edition used by buffrs 0.7.x @@ -68,6 +71,7 @@ impl From<&str> for Edition { fn from(value: &str) -> Self { match value { self::CANARY_EDITION => Self::Canary, + "0.9" => Self::Canary09, "0.8" => Self::Canary08, "0.7" => Self::Canary07, _ => Self::Unknown, @@ -79,6 +83,7 @@ impl From for &'static str { fn from(value: Edition) -> Self { match value { Edition::Canary => CANARY_EDITION, + Edition::Canary09 => "0.9", Edition::Canary08 => "0.8", Edition::Canary07 => "0.7", Edition::Unknown => "unknown", @@ -194,7 +199,9 @@ mod deserializer { while let Some(key) = map.next_key::()? { match key.as_str() { "package" => package = Some(map.next_value()?), - "dependencies" => dependencies = Some(map.next_value()?), + "dependencies" => { + dependencies = Some(map.next_value()?); + } "edition" => edition = Some(map.next_value()?), _ => return Err(de::Error::unknown_field(&key, FIELDS)), } @@ -210,7 +217,7 @@ mod deserializer { }; match Edition::from(edition.as_str()) { - Edition::Canary | Edition::Canary08 | Edition::Canary07 => Ok(RawManifest::Canary { + Edition::Canary | Edition::Canary09 | Edition::Canary08 | Edition::Canary07 => Ok(RawManifest::Canary { package, dependencies, }), @@ -235,10 +242,12 @@ impl From for RawManifest { .collect(); match manifest.edition { - Edition::Canary | Edition::Canary08 | Edition::Canary07 => RawManifest::Canary { - package: manifest.package, - dependencies, - }, + Edition::Canary | Edition::Canary09 | Edition::Canary08 | Edition::Canary07 => { + RawManifest::Canary { + package: manifest.package, + dependencies, + } + } Edition::Unknown => RawManifest::Unknown { package: manifest.package, dependencies, @@ -270,6 +279,9 @@ pub struct Manifest { pub dependencies: Vec, } +/// A resolved version of the manifest with registry aliases and local dependencies resolved +pub struct ResolvedManifest(pub Manifest); + impl Manifest { /// Create a new manifest of the current edition pub fn new(package: Option, dependencies: Vec) -> Self { @@ -289,14 +301,76 @@ impl Manifest { } /// Loads the manifest from the current directory - pub async fn read() -> miette::Result { - Self::try_read_from(MANIFEST_FILE) + pub async fn read(config: &config::Config) -> miette::Result { + Self::try_read_from(MANIFEST_FILE, Some(config)) .await? .ok_or(miette!("`{MANIFEST_FILE}` does not exist")) } + /// Parses the manifest from the given string + /// + /// # Arguments + /// * `contents` - The contents of the manifest file + /// * `config` - The configuration to use for resolving registry aliases + pub fn try_parse(contents: &str, config: Option<&config::Config>) -> miette::Result { + let raw: RawManifest = toml::from_str(contents) + .into_diagnostic() + .wrap_err(DeserializationError(ManagedFile::Manifest))?; + + let dependencies = raw + .dependencies() + .iter() + .map(|(package, manifest)| { + let package = package.clone(); + let manifest = match manifest { + DependencyManifest::Remote(remote_manifest) => { + // For remote manifest dependencies, resolve the registry alias + DependencyManifest::Remote(RemoteDependencyManifest { + version: remote_manifest.version.clone(), + repository: remote_manifest.repository.clone(), + registry: remote_manifest.registry.with_alias_resolved(config)?, + }) + } + DependencyManifest::Local(local_manifest) => { + // For local dependencies, check if a remote manifest is present + // and resolve its registry alias + if let Some(ref remote_manifest) = local_manifest.publish { + DependencyManifest::Local(LocalDependencyManifest { + path: local_manifest.path.clone(), + publish: Some(RemoteDependencyManifest { + version: remote_manifest.version.clone(), + repository: remote_manifest.repository.clone(), + registry: remote_manifest + .registry + .with_alias_resolved(config)?, + }), + }) + } else { + manifest.clone() + } + } + }; + + Ok(Dependency { package, manifest }) + }) + .collect::>>()?; + + Ok(Self { + edition: raw.edition(), + package: raw.package().cloned(), + dependencies, + }) + } + /// Loads the manifest from the given path - pub async fn try_read_from(path: impl AsRef) -> miette::Result> { + /// + /// # Arguments + /// * `path` - The path to the manifest file + /// * `config` - The configuration to use for resolving registry aliases + pub async fn try_read_from( + path: impl AsRef, + config: Option<&config::Config>, + ) -> miette::Result> { let contents = match fs::read_to_string(path.as_ref()).await { Ok(c) => c, Err(e) if e.kind() == std::io::ErrorKind::NotFound => { @@ -310,11 +384,7 @@ impl Manifest { } }; - let raw: RawManifest = toml::from_str(&contents) - .into_diagnostic() - .wrap_err(DeserializationError(ManagedFile::Manifest))?; - - Ok(Some(raw.into())) + Ok(Some(Self::try_parse(&contents, config)?)) } /// Persists the manifest into the current directory @@ -342,32 +412,103 @@ impl Manifest { .into_diagnostic() .wrap_err(WriteError(MANIFEST_FILE)) } -} -impl From for Manifest { - fn from(raw: RawManifest) -> Self { - let dependencies = raw - .dependencies() - .iter() - .map(|(package, manifest)| Dependency { - package: package.to_owned(), - manifest: manifest.to_owned(), - }) - .collect(); + /// Tests if the manifest is fully resolved (only contains remote dependencies) + pub fn assert_fully_resolved(&self) -> miette::Result<()> { + for dependency in &self.dependencies { + if dependency.manifest.is_local() { + return Err(miette!( + "dependency {} of {} does not specify version/registry/repository", + dependency.package, + self.package + .as_ref() + .map(|p| p.name.clone()) + .map(|n| n.to_string()) + .unwrap_or("package".to_string()) + )); + } + } - Self { - edition: raw.edition(), - package: raw.package().cloned(), - dependencies, + Ok(()) + } +} + +impl ResolvedManifest { + /// Returns a clone of this manifest suitable for publishing + /// + /// - All local manifest dependencies are replaced with their remote counterparts + pub fn new_from_manifest(mut manifest: Manifest) -> miette::Result { + // Resolve aliases in dependencies prior to packaging + for dependency in &mut manifest.dependencies { + if let DependencyManifest::Local(ref local_manifest) = dependency.manifest { + match local_manifest.publish { + Some(ref remote_manifest) => { + dependency.manifest = DependencyManifest::Remote(remote_manifest.clone()); + } + None => { + return Err(miette!( + "local dependency {} of {} does not specify version/registry/repository", + dependency.package, + manifest.package + .as_ref() + .map(|p| p.name.clone()) + .map(|n| n.to_string()) + .unwrap_or("package".to_string()) + )); + } + } + } } + + Ok(Self(manifest)) } } -impl FromStr for Manifest { - type Err = toml::de::Error; +/// Trait for serializable manifest types +pub trait PublishableManifest { + /// Returns the header for the manifest file + fn header() -> Option<&'static str>; + /// Returns the name of the manifest file + fn file_name() -> String; +} - fn from_str(input: &str) -> Result { - input.parse::().map(Self::from) +impl PublishableManifest for Manifest { + fn header() -> Option<&'static str> { + None + } + + fn file_name() -> String { + format!("{}.orig", MANIFEST_FILE) + } +} + +impl PublishableManifest for ResolvedManifest { + fn header() -> Option<&'static str> { + const MANIFEST_PREFIX: &str = r#"# THIS FILE IS AUTOMATICALLY GENERATED BY BUFFRS +# +# When uploading packages to the registry buffrs will automatically +# "normalize" Proto.toml files for maximal compatibility +# with all versions of buffrs and also rewrite `path` dependencies +# to registry dependencies. +# +# If you are reading this file be aware that the original Proto.toml +# will likely look very different (and much more reasonable). +# See Proto.toml.orig for the original contents. +"#; + + Some(MANIFEST_PREFIX) + } + + fn file_name() -> String { + MANIFEST_FILE.to_owned() + } +} + +impl TryInto for Manifest { + type Error = miette::Report; + + fn try_into(self) -> Result { + ResolvedManifest::new_from_manifest(self) } } @@ -379,6 +520,14 @@ impl TryInto for Manifest { } } +impl TryInto for ResolvedManifest { + type Error = toml::ser::Error; + + fn try_into(self) -> Result { + self.0.try_into() + } +} + /// Manifest format for api packages #[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct PackageManifest { @@ -405,7 +554,7 @@ pub struct Dependency { impl Dependency { /// Creates a new dependency pub fn new( - registry: RegistryUri, + registry: &RegistryRef, repository: String, package: PackageName, version: VersionReq, @@ -415,7 +564,7 @@ impl Dependency { manifest: RemoteDependencyManifest { repository, version, - registry, + registry: registry.to_owned(), } .into(), } @@ -457,7 +606,7 @@ impl Display for Dependency { } /// Manifest format for dependencies -#[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Hash, Serialize, PartialEq, Eq)] #[serde(untagged)] pub enum DependencyManifest { /// A remote dependency from artifactory @@ -480,7 +629,7 @@ pub struct RemoteDependencyManifest { /// Artifactory repository to pull dependency from pub repository: String, /// Artifactory registry to pull from - pub registry: RegistryUri, + pub registry: RegistryRef, } impl From for DependencyManifest { @@ -494,6 +643,9 @@ impl From for DependencyManifest { pub struct LocalDependencyManifest { /// Path to local buffrs package pub path: PathBuf, + /// Optional remote manifest for publishing + #[serde(flatten)] + pub publish: Option, } impl From for DependencyManifest { @@ -501,3 +653,54 @@ impl From for DependencyManifest { Self::Local(value) } } + +// Custom deserialization logic for `DependencyManifest` +mod dependency_manifest_deserializer { + use super::*; + use serde::{de::Error, Deserialize, Deserializer}; + + impl<'de> Deserialize<'de> for DependencyManifest { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct TempManifest { + path: Option, + version: Option, + repository: Option, + registry: Option, + } + + let temp: TempManifest = TempManifest::deserialize(deserializer)?; + + if let Some(path) = temp.path { + // Deserialize as a local dependency with optional remote attributes + Ok(DependencyManifest::Local(LocalDependencyManifest { + path, + publish: match (temp.version, temp.repository, temp.registry) { + (Some(version), Some(repository), Some(registry)) => { + Some(RemoteDependencyManifest { + version, + repository, + registry, + }) + } + _ => None, + }, + })) + } else if let (Some(version), Some(repository), Some(registry)) = + (temp.version, temp.repository, temp.registry) + { + // Deserialize as a remote dependency + Ok(DependencyManifest::Remote(RemoteDependencyManifest { + version, + repository, + registry, + })) + } else { + Err(D::Error::custom("Invalid dependency manifest")) + } + } + } +} diff --git a/src/package/compressed.rs b/src/package/compressed.rs index fc17932f..4679cb6d 100644 --- a/src/package/compressed.rs +++ b/src/package/compressed.rs @@ -27,9 +27,9 @@ use tokio::fs; use crate::{ errors::{DeserializationError, SerializationError}, lock::{Digest, DigestAlgorithm, LockedPackage}, - manifest::{self, Edition, Manifest, MANIFEST_FILE}, + manifest::{self, Edition, Manifest, PublishableManifest, ResolvedManifest}, package::PackageName, - registry::RegistryUri, + registry::RegistryRef, ManagedFile, }; @@ -44,6 +44,8 @@ pub struct Package { pub tgz: Bytes, } +pub struct Tarball(Vec); + impl Package { /// Create new [`Package`] from [`Manifest`] and list of files. /// @@ -54,47 +56,26 @@ impl Package { files: BTreeMap, preserve_mtime: bool, ) -> miette::Result { + // Create a new conforming manifest if the edition is unknown if manifest.edition == Edition::Unknown { manifest = Manifest::new(manifest.package, manifest.dependencies); } + // Ensure the manifest has a package declaration if manifest.package.is_none() { return Err(miette!( - "failed to create package, manifest doesnt contain a package declaration" + "failed to create package, manifest doesn't contain a package declaration" )); } let mut archive = tar::Builder::new(Vec::new()); - let manifest_bytes = { - let as_str: String = manifest - .clone() - .try_into() - .into_diagnostic() - .wrap_err(SerializationError(ManagedFile::Manifest))?; - - as_str.into_bytes() - }; - - let mut header = tar::Header::new_gnu(); - - header.set_size( - manifest_bytes - .len() - .try_into() - .into_diagnostic() - .wrap_err(miette!( - "serialized manifest was too large to fit in a tarball" - ))?, - ); - - header.set_mode(0o444); - - archive - .append_data(&mut header, MANIFEST_FILE, Cursor::new(manifest_bytes)) - .into_diagnostic() - .wrap_err(miette!("failed to add manifest to release"))?; + // Add original and resolved manifests + let resolve_manifest: ResolvedManifest = manifest.clone().try_into()?; + Self::add_manifest_to_archive(&mut archive, resolve_manifest)?; + Self::add_manifest_to_archive(&mut archive, manifest.clone())?; + // Add files to the archive for (name, entry) in &files { let mut header = tar::Header::new_gnu(); @@ -120,25 +101,64 @@ impl Package { .wrap_err(miette!("failed to add proto {name:?} to release tar"))?; } - let tar = archive - .into_inner() - .into_diagnostic() - .wrap_err(miette!("failed to assemble tar package"))?; + // Finalize tarball + let tar = Tarball( + archive + .into_inner() + .into_diagnostic() + .wrap_err(miette!("failed to assemble tar package"))?, + ); - let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + Ok(Self { + manifest, + tgz: tar.compress()?, + }) + } - encoder - .write_all(&tar) - .into_diagnostic() - .wrap_err(miette!("failed to compress release"))?; + /// Helper to add a manifest (original or resolved) to the tarball. + fn add_manifest_to_archive( + archive: &mut tar::Builder>, + manifest: M, + ) -> miette::Result<()> + where + M: PublishableManifest + TryInto, + { + let manifest_bytes = { + let as_str: String = manifest + .try_into() + .into_diagnostic() + .wrap_err(SerializationError(ManagedFile::Manifest))?; - let tgz: Bytes = encoder - .finish() - .into_diagnostic() - .wrap_err(miette!("failed to finalize package"))? - .into(); + // Prepend the prefix if provided + let mut result = String::new(); + if let Some(prefix_text) = M::header() { + result.push_str(prefix_text); + } + result.push_str(&as_str); + result.into_bytes() + }; - Ok(Self { manifest, tgz }) + let mut header = tar::Header::new_gnu(); + header.set_size( + manifest_bytes + .len() + .try_into() + .into_diagnostic() + .wrap_err(miette!( + "serialized manifest `{}` was too large to fit in a tarball", + M::file_name() + ))?, + ); + header.set_mode(0o444); + + archive + .append_data(&mut header, M::file_name(), Cursor::new(manifest_bytes)) + .into_diagnostic() + .wrap_err(miette!( + "failed to add manifest `{}` to release", + M::file_name() + ))?; + Ok(()) } /// Unpack a package to a specific path. @@ -210,9 +230,9 @@ impl Package { let manifest = String::from_utf8(manifest) .into_diagnostic() - .wrap_err(miette!("manifest has invalid character encoding"))? - .parse() - .into_diagnostic()?; + .wrap_err(miette!("manifest has invalid character encoding"))?; + + let manifest = Manifest::try_parse(manifest.as_str(), None)?; Ok(Self { manifest, tgz }) } @@ -251,7 +271,7 @@ impl Package { /// Lock this package pub fn lock( &self, - registry: RegistryUri, + registry: RegistryRef, repository: String, dependants: usize, ) -> LockedPackage { @@ -266,3 +286,19 @@ impl TryFrom for Package { Package::parse(tgz) } } + +impl Tarball { + /// Compress the tarball into a `.tgz` file. + pub fn compress(&self) -> miette::Result { + let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + encoder + .write_all(&self.0) + .into_diagnostic() + .wrap_err(miette!("failed to compress tarball"))?; + let tgz = encoder + .finish() + .into_diagnostic() + .wrap_err(miette!("failed to finalize compressed tarball"))?; + Ok(tgz.into()) + } +} diff --git a/src/package/store.rs b/src/package/store.rs index a948cbd6..e1cd94e5 100644 --- a/src/package/store.rs +++ b/src/package/store.rs @@ -24,8 +24,10 @@ use tokio::fs; use walkdir::WalkDir; use crate::{ + config::Config, manifest::{Manifest, PackageManifest, MANIFEST_FILE}, package::{Package, PackageName, PackageType}, + resolver::DependencyGraph, }; /// IO abstraction layer over local `buffrs` package store @@ -40,7 +42,10 @@ impl PackageStore { /// Path to the dependency store pub const PROTO_VENDOR_PATH: &'static str = "proto/vendor"; - fn new(root: PathBuf) -> Self { + /// Create a new package store from a given path + /// + /// Note: pub(crate) for use by unit tests + pub(crate) fn new(root: PathBuf) -> Self { Self { root } } @@ -122,13 +127,19 @@ impl PackageStore { } /// Resolves a package in the local file system - pub async fn resolve(&self, package: &PackageName) -> miette::Result { + pub async fn resolve( + &self, + package: &PackageName, + config: &Config, + ) -> miette::Result { let manifest = self.locate(package).join(MANIFEST_FILE); - let manifest = Manifest::try_read_from(&manifest).await?.ok_or(miette!( - "the package store is corrupted: `{}` is not present", - manifest.display() - ))?; + let manifest = Manifest::try_read_from(&manifest, Some(config)) + .await? + .ok_or(miette!( + "the package store is corrupted: `{}` is not present", + manifest.display() + ))?; Ok(manifest) } @@ -151,14 +162,39 @@ impl PackageStore { parser.validate() } - /// Packages a release from the local file system state + /// Packages a release from the local file system state or from a dependency graph. + /// + /// This method will package the contents of the local file system into a `Package` instance. + /// If the `deps` argument is provided, it will fetch the dependencies from the graph + /// instead of the local file system. + /// + /// # Arguments + /// - `manifest` - Package manifest to package + /// - `config` - Configuration to use (for alias resolution) + /// - `deps` - Optional dependency graph to fetch dependencies from + /// + /// # Returns + /// A `Package` instance representing the packaged release pub async fn release( &self, manifest: &Manifest, preserve_mtime: bool, + config: &Config, + deps: Option<&DependencyGraph>, ) -> miette::Result { for dependency in manifest.dependencies.iter() { - let resolved = self.resolve(&dependency.package).await?; + let resolved = if let Some(deps) = deps { + deps.get(&dependency.package) + .map(|dep| dep.package().manifest.clone()) + } else { + None + }; + + let resolved = if let Some(resolved) = resolved { + resolved + } else { + self.resolve(&dependency.package, config).await? + }; let Some(ref resolved_pkg) = resolved.package else { bail!("upstream package is invalid, [package] section is missing in manifest"); diff --git a/src/registry/artifactory.rs b/src/registry/artifactory.rs index 7a825097..c04eb4e5 100644 --- a/src/registry/artifactory.rs +++ b/src/registry/artifactory.rs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::RegistryUri; use crate::{ credentials::Credentials, manifest::{Dependency, DependencyManifest}, package::{Package, PackageName}, + registry::RegistryUri, }; use miette::{ensure, miette, Context, IntoDiagnostic}; use reqwest::{Body, Method, Response}; @@ -24,6 +24,17 @@ use semver::Version; use serde::Deserialize; use url::Url; +/// The policy for validating artifactory server certificates +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum CertValidationPolicy { + /// Validate the certificate + #[default] + Validate, + + /// Do not validate the certificate + NoValidation, +} + /// The registry implementation for artifactory #[derive(Debug, Clone)] pub struct Artifactory { @@ -34,14 +45,27 @@ pub struct Artifactory { impl Artifactory { /// Creates a new instance of an Artifactory registry client - pub fn new(registry: RegistryUri, credentials: &Credentials) -> miette::Result { + /// + /// # Arguments + /// * `registry` - The registry URI + /// * `credentials` - The credentials to use for the registry + /// * `policy` - The policy for validating artifactory server certificates + pub fn new( + registry: RegistryUri, + credentials: &Credentials, + policy: CertValidationPolicy, + ) -> miette::Result { + let token = credentials.registry_tokens.get(®istry).cloned(); + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .danger_accept_invalid_certs(policy == CertValidationPolicy::NoValidation) + .build() + .into_diagnostic()?; + Ok(Self { - registry: registry.clone(), - token: credentials.registry_tokens.get(®istry).cloned(), - client: reqwest::Client::builder() - .redirect(reqwest::redirect::Policy::none()) - .build() - .into_diagnostic()?, + registry, + token, + client, }) } @@ -58,10 +82,10 @@ impl Artifactory { /// Pings artifactory to ensure registry access is working pub async fn ping(&self) -> miette::Result<()> { let repositories_url: Url = { - let mut uri = self.registry.to_owned(); + let mut uri: url::Url = self.registry.to_owned().into(); let path = &format!("{}/api/repositories", uri.path()); uri.set_path(path); - uri.into() + uri }; self.new_request(Method::GET, repositories_url) @@ -79,10 +103,10 @@ impl Artifactory { ) -> miette::Result { // First retrieve all packages matching the given name let search_query_url: Url = { - let mut url = self.registry.clone(); - url.set_path("artifactory/api/search/artifact"); - url.set_query(Some(&format!("name={}&repos={}", name, repository))); - url.into() + let mut uri: url::Url = self.registry.to_owned().into(); + uri.set_path("artifactory/api/search/artifact"); + uri.set_query(Some(&format!("name={}&repos={}", name, repository))); + uri }; let response = self @@ -162,24 +186,22 @@ impl Artifactory { let artifact_url = { let version = super::dependency_version_string(&dependency)?; + let url: RegistryUri = self.registry.clone(); + let mut url: url::Url = url.into(); + let path = url.path(); - let path = manifest.registry.path().to_owned(); - - let mut url = manifest.registry.clone(); url.set_path(&format!( "{}/{}/{}/{}-{}.tgz", path, manifest.repository, dependency.package, dependency.package, version )); - url.into() + url }; tracing::debug!("Hitting download URL: {artifact_url}"); let response = self.new_request(Method::GET, artifact_url).send().await?; - let response: reqwest::Response = response.0; - let headers = response.headers(); let content_type = headers .get(&reqwest::header::CONTENT_TYPE) diff --git a/src/registry/cache.rs b/src/registry/cache.rs index 7a83f095..436610f4 100644 --- a/src/registry/cache.rs +++ b/src/registry/cache.rs @@ -157,7 +157,7 @@ mod tests { // had published. let fetched = registry .download(Dependency::new( - registry_uri, + ®istry_uri, "test-repo".into(), "test-api".parse().unwrap(), "=0.1.0".parse().unwrap(), diff --git a/src/registry/mod.rs b/src/registry/mod.rs index d19bd177..f3289b76 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -14,7 +14,6 @@ use std::{ fmt::{self, Display}, - ops::{Deref, DerefMut}, str::FromStr, }; @@ -22,36 +21,178 @@ mod artifactory; #[cfg(test)] mod cache; -pub use artifactory::Artifactory; +use crate::{config, manifest::Dependency}; +use crate::{errors::RegistryNameError, manifest::DependencyManifest}; +pub use artifactory::{Artifactory, CertValidationPolicy}; use miette::{ensure, miette, Context, IntoDiagnostic}; use semver::VersionReq; use serde::{Deserialize, Serialize}; use thiserror::Error; use url::Url; -use crate::manifest::{Dependency, DependencyManifest}; +/// Representation of a registry name +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub struct RegistryAlias(String); + +impl FromStr for RegistryAlias { + type Err = RegistryNameError; + + fn from_str(s: &str) -> Result { + if s.chars() + .all(|c| c.is_alphanumeric() || c == '-' || c == '_') + { + Ok(RegistryAlias(s.to_string())) + } else { + Err(RegistryNameError) + } + } +} + +impl Display for RegistryAlias { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} /// A representation of a registry URI #[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct RegistryUri(Url); +impl RegistryUri { + /// Get the host component of the registry URI + pub fn host(&self) -> Option<&str> { + self.0.host_str() + } + + /// Get the path component of the registry URI + pub fn path(&self) -> &str { + self.0.path() + } +} + +/// A reference to a registry +#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum RegistryRef { + /// A URL to a registry + Url(RegistryUri), + /// An alias to a registry + Alias(RegistryAlias), + /// A resolved alias to a registry + ResolvedAlias { + /// The alias + alias: RegistryAlias, + /// The resolved URL + url: RegistryUri, + }, +} + +impl RegistryRef { + /// Get the raw URL of the registry with any alias resolved + /// + /// # Arguments + /// * `config` - The configuration to use to resolve the alias + pub fn with_alias_resolved(&self, config: Option<&config::Config>) -> miette::Result { + match self { + RegistryRef::Alias(alias) => match config { + Some(config) => { + let url = config.lookup_registry(alias)?; + Ok(RegistryRef::ResolvedAlias { + alias: alias.clone(), + url: url.clone(), + }) + } + None => Err(miette!( + "no configuration provided to resolve alias \"{}\"", + alias.0 + )), + }, + _ => Ok(self.clone()), + } + } + + /// Serializer for resolved RegistryUris + pub fn serialize_resolved(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + RegistryRef::ResolvedAlias { url, .. } => url.serialize(serializer), + RegistryRef::Url(url) => url.serialize(serializer), + _ => Err(serde::ser::Error::custom( + "cannot serialize unresolved alias", + )), + } + } +} + +impl<'de> Deserialize<'de> for RegistryRef { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let url = String::deserialize(deserializer)?; + RegistryRef::from_str(&url).map_err(serde::de::Error::custom) + } +} + +impl Serialize for RegistryRef { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_string().serialize(serializer) + } +} + impl From for Url { fn from(value: RegistryUri) -> Self { value.0 } } -impl Deref for RegistryUri { - type Target = Url; +impl TryFrom for RegistryUri { + type Error = miette::Report; + + fn try_from(value: RegistryRef) -> Result { + match value { + RegistryRef::Url(url) => Ok(url), + RegistryRef::ResolvedAlias { url, .. } => Ok(url), + _ => Err(miette!( + "cannot convert unresolved alias \"{value}\" to URL" + )), + } + } +} + +impl TryFrom<&RegistryRef> for RegistryUri { + type Error = miette::Report; - fn deref(&self) -> &Self::Target { - &self.0 + fn try_from(value: &RegistryRef) -> Result { + // Delegate to the implementation for the owned type + TryFrom::::try_from(value.clone()) } } -impl DerefMut for RegistryUri { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 +impl Display for RegistryRef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RegistryRef::Url(url) => write!(f, "{}", url), + RegistryRef::Alias(alias) => write!(f, "{}", alias), + RegistryRef::ResolvedAlias { alias, url } => write!(f, "{} ({})", alias, url), + } + } +} + +impl FromStr for RegistryRef { + type Err = miette::Report; + + fn from_str(value: &str) -> Result { + // Attempt to parse the value as a URL + match RegistryUri::from_str(value) { + Ok(uri) => Ok(Self::Url(uri)), + // If the value is not a valid URL, treat it as an alias + Err(_) => Ok(Self::Alias(value.parse()?)), + } } } @@ -75,6 +216,15 @@ impl FromStr for RegistryUri { } } +/// Ensure that the URL is valid for a registry +/// +/// A valid registry URL must: +/// - Have a scheme of either "http" or "https" +/// - End with "/artifactory" if the host is a JFrog Artifactory instance +/// - Have a host component +/// +/// # Arguments +/// * `url` - The URL to check fn sanity_check_url(url: &Url) -> miette::Result<()> { let scheme = url.scheme(); @@ -155,14 +305,14 @@ mod tests { registry::{dependency_version_string, VersionNotPinned}, }; - use super::RegistryUri; + use super::RegistryRef; fn get_dependency(version: &str) -> Dependency { - let registry = RegistryUri::from_str("https://my-registry.com").unwrap(); + let registry = RegistryRef::from_str("https://my-registry.com").unwrap(); let repository = String::from("my-repo"); let package = PackageName::from_str("package").unwrap(); let version = VersionReq::from_str(version).unwrap(); - Dependency::new(registry, repository, package, version) + Dependency::new(®istry, repository, package, version) } #[test] diff --git a/src/resolver.rs b/src/resolver.rs index 80ee10a4..88d5cd10 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -1,12 +1,18 @@ -use std::{collections::HashMap, path::PathBuf, sync::Arc}; +use std::{ + collections::HashMap, + env, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, +}; use async_recursion::async_recursion; -use miette::{bail, ensure, Context, Diagnostic}; +use miette::{bail, ensure, Context, Diagnostic, IntoDiagnostic}; use semver::VersionReq; use thiserror::Error; use crate::{ cache::{Cache, Entry}, + config::Config, credentials::Credentials, lock::{FileRequirement, Lockfile}, manifest::{ @@ -14,17 +20,18 @@ use crate::{ RemoteDependencyManifest, MANIFEST_FILE, }, package::{Package, PackageName, PackageStore}, - registry::{Artifactory, RegistryUri}, + registry::{Artifactory, CertValidationPolicy, RegistryRef}, }; /// Represents a dependency contextualized by the current dependency graph +#[derive(Debug, Clone)] pub enum ResolvedDependency { /// A resolved dependency that is located on a remote registry Remote { /// The materialized package as downloaded from the registry package: Package, /// The registry the package was downloaded from - registry: RegistryUri, + registry: RegistryRef, /// The repository in the registry where the package can be found repository: String, /// Packages that requested this dependency (and what versions they accept) @@ -32,7 +39,7 @@ pub enum ResolvedDependency { /// Transitive dependencies depends_on: Vec, }, - /// A resolved depnedency that is located on the filesystem + /// A resolved dependency that is located on the filesystem Local { /// The materialized package that was created from the buffrs package at the given path package: Package, @@ -62,6 +69,7 @@ impl ResolvedDependency { } /// Represents a requester of the associated dependency +#[derive(Debug, Clone)] pub struct Dependant { /// Package that requested the dependency pub name: PackageName, @@ -70,8 +78,20 @@ pub struct Dependant { } /// Represents direct and transitive dependencies of the root package +#[derive(Debug, Clone, Default)] pub struct DependencyGraph { - entries: HashMap, + pub(crate) entries: HashMap, +} + +/// A builder for constructing a dependency graph +pub struct DependencyGraphBuilder<'a> { + manifest: &'a Manifest, + lockfile: &'a Lockfile, + credentials: &'a Credentials, + cache: &'a Cache, + preserve_mtime: bool, + config: &'a Config, + policy: CertValidationPolicy, } #[derive(Debug, Clone, Eq, PartialEq)] @@ -89,6 +109,41 @@ impl From for Dependency { } } +impl DependencyGraph { + /// Locates and returns a reference to a resolved dependency package by its name + pub fn get(&self, name: &PackageName) -> Option<&ResolvedDependency> { + self.entries.get(name) + } + + /// Returns a list of all package names in the dependency graph + pub fn get_package_names(&self) -> Vec { + self.entries.keys().cloned().collect() + } +} + +impl IntoIterator for DependencyGraph { + type Item = ResolvedDependency; + type IntoIter = std::collections::hash_map::IntoValues; + + fn into_iter(self) -> Self::IntoIter { + self.entries.into_values() + } +} + +impl Deref for DependencyGraph { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.entries + } +} + +impl DerefMut for DependencyGraph { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.entries + } +} + #[derive(Debug, Clone, Eq, PartialEq)] struct LocalDependency { package: PackageName, @@ -102,120 +157,100 @@ struct DownloadError { version: VersionReq, } -struct ProcessDependency<'a> { - name: PackageName, - dependency: Dependency, - is_root: bool, - lockfile: &'a Lockfile, - credentials: &'a Arc, - cache: &'a Cache, - preserve_mtime: bool, -} - -struct ProcessLocalDependency<'a> { - name: PackageName, - dependency: LocalDependency, - #[allow(dead_code)] - is_root: bool, - lockfile: &'a Lockfile, - credentials: &'a Arc, - cache: &'a Cache, - preserve_mtime: bool, -} - -struct ProcessRemoteDependency<'a> { - name: PackageName, - dependency: RemoteDependency, - is_root: bool, - lockfile: &'a Lockfile, - credentials: &'a Arc, - cache: &'a Cache, - preserve_mtime: bool, -} - -impl DependencyGraph { - /// Recursively resolves dependencies from the manifest to build a dependency graph - pub async fn from_manifest( - manifest: &Manifest, - lockfile: &Lockfile, - credentials: &Arc, - cache: &Cache, +impl<'a> DependencyGraphBuilder<'a> { + /// Creates a new dependency graph builder + /// + /// # Parameters + /// - `manifest`: Manifest of the root package + /// - `lockfile`: Lockfile of the root package + /// - `credentials`: Credentials used to authenticate with remote registries + /// - `cache`: Cache used to store downloaded packages + /// - `preserve_mtime`: Whether to preserve modification times during package release + /// - `config`: Configuration settings + /// - `policy`: Policy used to validate certificates + /// + /// # Returns + /// A new dependency graph builder + pub fn new( + manifest: &'a Manifest, + lockfile: &'a Lockfile, + credentials: &'a Credentials, + cache: &'a Cache, preserve_mtime: bool, - ) -> miette::Result { - let name = manifest + config: &'a Config, + policy: CertValidationPolicy, + ) -> Self { + Self { + manifest, + lockfile, + credentials, + cache, + config, + policy, + preserve_mtime, + } + } + + /// Builds the dependency graph + pub async fn build(self) -> miette::Result { + let name = self + .manifest .package .as_ref() .map(|p| p.name.clone()) .unwrap_or_else(|| PackageName::unchecked(".")); - let mut entries = HashMap::new(); - - for dependency in &manifest.dependencies { - Self::process_dependency( - &mut entries, - ProcessDependency { - name: name.clone(), - dependency: dependency.clone(), - is_root: true, - lockfile, - credentials, - cache, - preserve_mtime, - }, + let parent_dir = env::current_dir().into_diagnostic()?; + + // Prepare the dependency graph + let mut deps = DependencyGraph::default(); + + for dependency in &self.manifest.dependencies { + self.process_dependency( + name.clone(), + dependency.clone(), + true, // is_root + &parent_dir, + &mut deps, ) .await?; } - Ok(Self { entries }) + Ok(deps) } async fn process_dependency( - entries: &mut HashMap, - params: ProcessDependency<'_>, + &self, + name: PackageName, + dependency: Dependency, + is_root: bool, + parent_dir: &Path, + deps: &mut DependencyGraph, ) -> miette::Result<()> { - let ProcessDependency { - name, - dependency, - is_root, - lockfile, - credentials, - cache, - preserve_mtime, - } = params; match dependency.manifest { DependencyManifest::Remote(manifest) => { - Self::process_remote_dependency( - entries, - ProcessRemoteDependency { - name: name.clone(), - dependency: RemoteDependency { - package: dependency.package, - manifest, - }, - is_root, - lockfile, - credentials, - cache, - preserve_mtime, + self.process_remote_dependency( + name.clone(), + RemoteDependency { + package: dependency.package, + manifest, }, + is_root, + parent_dir, + deps, ) .await?; } DependencyManifest::Local(manifest) => { - Self::process_local_dependency( - entries, - ProcessLocalDependency { - name: name.clone(), - dependency: LocalDependency { - package: dependency.package, - manifest, - }, - is_root, - lockfile, - credentials, - cache, - preserve_mtime, + self.process_local_dependency( + name.clone(), + LocalDependency { + package: dependency.package, + manifest, }, + is_root, + parent_dir, + deps, ) .await?; } @@ -225,32 +260,108 @@ impl DependencyGraph { } #[async_recursion] - async fn process_local_dependency<'a>( - entries: &'a mut HashMap, - params: ProcessLocalDependency<'a>, + async fn process_local_dependency( + &self, + name: PackageName, + dependency: LocalDependency, + is_root: bool, + parent_dir: &Path, + deps: &mut DependencyGraph, ) -> miette::Result<()> { - let ProcessLocalDependency { - name, - dependency, - is_root: _, - lockfile, - credentials, - cache, - preserve_mtime, - } = params; - let manifest = Manifest::try_read_from(&dependency.manifest.path.join(MANIFEST_FILE)) - .await? - .ok_or_else(|| { - miette::miette!( - "no `{}` for package {} found at path {}", + // If the dependency.manifest_path is relative, it's relative to the parent manifest. + // We therefore need to resolve it to an absolute path. + let abs_manifest_dir = if dependency.manifest.path.is_relative() { + // combine the parent manifest path with the relative path + parent_dir + .join(&dependency.manifest.path) + .canonicalize() + .into_diagnostic() + .wrap_err(miette::miette!( + "no `{}` for package {} found at path {} referenced by {} as \"{}\"", MANIFEST_FILE, dependency.package, - dependency.manifest.path.join(MANIFEST_FILE).display() - ) - })?; + parent_dir.join(&dependency.manifest.path).display(), + name, + dependency.manifest.path.display() + ))? + } else { + dependency.manifest.path.clone() + }; + + let manifest = + Manifest::try_read_from(&abs_manifest_dir.join(MANIFEST_FILE), Some(self.config)) + .await? + .ok_or_else(|| { + miette::miette!( + "no `{}` for package {} found at path {} referenced by {} as \"{}\"", + MANIFEST_FILE, + dependency.package, + abs_manifest_dir.join(MANIFEST_FILE).display(), + name, + dependency.manifest.path.display() + ) + })?; - let store = PackageStore::open(&dependency.manifest.path).await?; - let package = store.release(&manifest, preserve_mtime).await?; + // Process sub-dependencies first + for sub_dependency in &manifest.dependencies { + self.process_dependency( + dependency.package.clone(), + sub_dependency.clone(), + true, // is_root + &abs_manifest_dir, + deps, + ) + .await?; + } + + let package = if is_root { + let store = PackageStore::open(&abs_manifest_dir).await?; + let package = store + .release(&manifest, self.preserve_mtime, self.config, Some(deps)) + .await?; + + // Ensure that the package version doesn't clash with an existing entry, + // and that it matches the version requirement in the manifest + if let Some(version_req) = dependency.manifest.publish.map(|p| p.version) { + let found_version = package.version(); + + if let Some(entry) = deps.get_mut(package.name()) { + let existing_package = entry.package(); + ensure!( + version_req.matches(existing_package.version()), + "a dependency of your project requires {}@{} which collides with {}@{} required by {:?}", + package.name(), + found_version, + existing_package.name(), + existing_package.version(), + name, + ); + } else { + // Package not yet in the dependency graph, so we verify the version requirement + ensure!( + version_req.matches(found_version), + "a dependency of your project requires {}@{} but the resolved version is {}", + package.name(), + version_req, + found_version, + ); + } + } + + package + } else { + // Non-root packages may not be physically present on disk. + // Take it from the collected entries instead. + deps.get(&dependency.package) + .ok_or_else(|| { + miette::miette!( + "no resolved package found for local dependency {}", + dependency.package + ) + })? + .package() + .clone() + }; let dependency_name = package.name().clone(); let sub_dependencies = package.manifest.dependencies.clone(); @@ -259,11 +370,12 @@ impl DependencyGraph { .map(|sub_dependency| sub_dependency.package.clone()) .collect(); - entries.insert( + // Add the local package to the dependency graph + deps.insert( dependency_name.clone(), ResolvedDependency::Local { package, - path: dependency.manifest.path, + path: abs_manifest_dir.clone(), dependants: vec![Dependant { name, version_req: VersionReq::STAR, @@ -272,18 +384,14 @@ impl DependencyGraph { }, ); + // Process the sub-dependencies of the local package for sub_dependency in sub_dependencies { - Self::process_dependency( - entries, - ProcessDependency { - name: dependency_name.clone(), - dependency: sub_dependency, - is_root: false, - lockfile, - credentials, - cache, - preserve_mtime, - }, + self.process_dependency( + dependency_name.clone(), + sub_dependency, + false, + &abs_manifest_dir, + deps, ) .await?; } @@ -292,22 +400,18 @@ impl DependencyGraph { } #[async_recursion] - async fn process_remote_dependency<'a>( - entries: &'a mut HashMap, - params: ProcessRemoteDependency<'a>, + async fn process_remote_dependency( + &self, + name: PackageName, + dependency: RemoteDependency, + is_root: bool, + parent_dir: &Path, + deps: &mut DependencyGraph, ) -> miette::Result<()> { - let ProcessRemoteDependency { - name, - dependency, - is_root, - lockfile, - credentials, - cache, - preserve_mtime, - } = params; let version_req = dependency.manifest.version.clone(); - if let Some(entry) = entries.get_mut(&dependency.package) { + // Check if the dependency is already resolved + if let Some(entry) = deps.get_mut(&dependency.package) { match entry { ResolvedDependency::Local { path, dependants, .. @@ -340,8 +444,8 @@ impl DependencyGraph { } } } else { - let dependency_pkg = - Self::resolve(dependency.clone(), is_root, lockfile, credentials, cache).await?; + // Resolve the dependency + let dependency_pkg = self.resolve(dependency.clone(), is_root).await?; let dependency_name = dependency_pkg.name().clone(); let sub_dependencies = dependency_pkg.manifest.dependencies.clone(); @@ -350,7 +454,7 @@ impl DependencyGraph { .map(|sub_dependency| sub_dependency.package.clone()) .collect(); - entries.insert( + deps.insert( dependency_name.clone(), ResolvedDependency::Remote { package: dependency_pkg, @@ -362,17 +466,12 @@ impl DependencyGraph { ); for sub_dependency in sub_dependencies { - Self::process_dependency( - entries, - ProcessDependency { - name: dependency_name.clone(), - dependency: sub_dependency, - is_root: false, - lockfile, - credentials, - cache, - preserve_mtime, - }, + self.process_dependency( + dependency_name.clone(), + sub_dependency, + false, + parent_dir, + deps, ) .await?; } @@ -382,13 +481,11 @@ impl DependencyGraph { } async fn resolve( + &self, dependency: RemoteDependency, is_root: bool, - lockfile: &Lockfile, - credentials: &Arc, - cache: &Cache, ) -> miette::Result { - if let Some(local_locked) = lockfile.get(&dependency.package) { + if let Some(local_locked) = self.lockfile.get(&dependency.package) { ensure!( is_root || dependency.manifest.registry == local_locked.registry, "mismatched registry detected for dependency {} - requested {} but lockfile requires {}", @@ -401,17 +498,21 @@ impl DependencyGraph { // but theoretically we should be able to still look into cache when freshly installing // a dependency. if dependency.manifest.version.matches(&local_locked.version) { - if let Some(cached) = cache.get(local_locked.into()).await? { + if let Some(cached) = self.cache.get(local_locked.try_into()?).await? { local_locked.validate(&cached)?; return Ok(cached); } } - let registry = Artifactory::new(dependency.manifest.registry.clone(), credentials) - .wrap_err(DownloadError { - name: dependency.package.clone(), - version: dependency.manifest.version.clone(), - })?; + let registry = Artifactory::new( + dependency.manifest.registry.clone().try_into()?, + self.credentials, + self.policy, + ) + .wrap_err(DownloadError { + name: dependency.package.clone(), + version: dependency.manifest.version.clone(), + })?; let package = registry // TODO(#205): This works now because buffrs only supports pinned versions. @@ -423,19 +524,25 @@ impl DependencyGraph { version: dependency.manifest.version, })?; - let file_requirement = FileRequirement::from(local_locked); - cache + let file_requirement = FileRequirement::try_from(local_locked)?; + self.cache .put(file_requirement.into(), package.tgz.clone()) .await .ok(); Ok(package) } else { - let registry = Artifactory::new(dependency.manifest.registry.clone(), credentials) - .wrap_err(DownloadError { - name: dependency.package.clone(), - version: dependency.manifest.version.clone(), - })?; + // Package not present in lockfile (and thus not in cache) + // => download it from the registry + let registry = Artifactory::new( + dependency.manifest.registry.clone().try_into()?, + self.credentials, + self.policy, + ) + .wrap_err(DownloadError { + name: dependency.package.clone(), + version: dependency.manifest.version.clone(), + })?; let package = registry .download(dependency.clone().into()) @@ -447,23 +554,9 @@ impl DependencyGraph { let key = Entry::from(&package); let content = package.tgz.clone(); - cache.put(key, content).await.ok(); + self.cache.put(key, content).await.ok(); Ok(package) } } - - /// Locates and returns a reference to a resolved dependency package by its name - pub fn get(&self, name: &PackageName) -> Option<&ResolvedDependency> { - self.entries.get(name) - } -} - -impl IntoIterator for DependencyGraph { - type Item = ResolvedDependency; - type IntoIter = std::collections::hash_map::IntoValues; - - fn into_iter(self) -> Self::IntoIter { - self.entries.into_values() - } } diff --git a/tests/cmd/add/out/Proto.toml b/tests/cmd/add/out/Proto.toml index 97ff489d..c1916639 100644 --- a/tests/cmd/add/out/Proto.toml +++ b/tests/cmd/add/out/Proto.toml @@ -1,4 +1,4 @@ -edition = "0.9" +edition = "0.10" [package] type = "lib" diff --git a/tests/cmd/init/api/out/Proto.toml b/tests/cmd/init/api/out/Proto.toml index 1ccf1e14..e3622ca1 100644 --- a/tests/cmd/init/api/out/Proto.toml +++ b/tests/cmd/init/api/out/Proto.toml @@ -1,4 +1,4 @@ -edition = "0.9" +edition = "0.10" [package] type = "api" diff --git a/tests/cmd/init/default/out/Proto.toml b/tests/cmd/init/default/out/Proto.toml index e6af6237..84de9fab 100644 --- a/tests/cmd/init/default/out/Proto.toml +++ b/tests/cmd/init/default/out/Proto.toml @@ -1,3 +1,3 @@ -edition = "0.9" +edition = "0.10" [dependencies] diff --git a/tests/cmd/init/lib/out/Proto.toml b/tests/cmd/init/lib/out/Proto.toml index 5c680da2..a17c4fa8 100644 --- a/tests/cmd/init/lib/out/Proto.toml +++ b/tests/cmd/init/lib/out/Proto.toml @@ -1,4 +1,4 @@ -edition = "0.9" +edition = "0.10" [package] type = "lib" diff --git a/tests/cmd/install/local/out/proto/vendor/some-local-api/Proto.toml b/tests/cmd/install/local/out/proto/vendor/some-local-api/Proto.toml index 32e08e84..6f564b2c 100644 --- a/tests/cmd/install/local/out/proto/vendor/some-local-api/Proto.toml +++ b/tests/cmd/install/local/out/proto/vendor/some-local-api/Proto.toml @@ -1,4 +1,14 @@ -edition = "0.9" +# THIS FILE IS AUTOMATICALLY GENERATED BY BUFFRS +# +# When uploading packages to the registry buffrs will automatically +# "normalize" Proto.toml files for maximal compatibility +# with all versions of buffrs and also rewrite `path` dependencies +# to registry dependencies. +# +# If you are reading this file be aware that the original Proto.toml +# will likely look very different (and much more reasonable). +# See Proto.toml.orig for the original contents. +edition = "0.10" [package] type = "api" diff --git a/tests/cmd/package/out/lib-0.0.1.tgz b/tests/cmd/package/out/lib-0.0.1.tgz index 4de76e25..a9cd8559 100644 Binary files a/tests/cmd/package/out/lib-0.0.1.tgz and b/tests/cmd/package/out/lib-0.0.1.tgz differ diff --git a/tests/cmd/publish/local/in/proto/vendor/some-local-lib/Proto.toml b/tests/cmd/publish/local/in/proto/vendor/some-local-lib/Proto.toml index 222a7559..b0bafa03 100644 --- a/tests/cmd/publish/local/in/proto/vendor/some-local-lib/Proto.toml +++ b/tests/cmd/publish/local/in/proto/vendor/some-local-lib/Proto.toml @@ -1,4 +1,4 @@ -edition = "0.9" +edition = "0.10" [package] type = "lib" diff --git a/tests/cmd/publish/local/out/proto/vendor/some-local-lib/Proto.toml b/tests/cmd/publish/local/out/proto/vendor/some-local-lib/Proto.toml index 222a7559..b0bafa03 100644 --- a/tests/cmd/publish/local/out/proto/vendor/some-local-lib/Proto.toml +++ b/tests/cmd/publish/local/out/proto/vendor/some-local-lib/Proto.toml @@ -1,4 +1,4 @@ -edition = "0.9" +edition = "0.10" [package] type = "lib" diff --git a/tests/cmd/publish/local/stderr.log b/tests/cmd/publish/local/stderr.log index 01cb41b6..a11777d3 100644 --- a/tests/cmd/publish/local/stderr.log +++ b/tests/cmd/publish/local/stderr.log @@ -1,5 +1,6 @@ -Error: × failed to publish `my-api` to `https://localhost:54321/fake-uri:my- +Error: + × failed to publish `my-api` to `https://localhost:54321/fake-uri:my- │ repository` - ╰─▶ unable to publish my-api to artifactory due having the following local - dependencies: some-local-lib + ╰─▶ local dependency some-local-lib of my-api does not specify version/ + registry/repository diff --git a/tests/cmd/publish/local/stdout.log b/tests/cmd/publish/local/stdout.log index b36d641e..e69de29b 100644 --- a/tests/cmd/publish/local/stdout.log +++ b/tests/cmd/publish/local/stdout.log @@ -1 +0,0 @@ -:: packaged my-api@0.1.0 diff --git a/tests/data/projects/lib/Proto.toml b/tests/data/projects/lib/Proto.toml index c7030f56..3b61f7d7 100644 --- a/tests/data/projects/lib/Proto.toml +++ b/tests/data/projects/lib/Proto.toml @@ -1,4 +1,4 @@ -edition = "0.9" +edition = "0.10" [package] type = "lib" diff --git a/tests/lib.rs b/tests/lib.rs index 2cc054ac..6e7f9888 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -109,6 +109,8 @@ impl VirtualFileSystem { let filter_gitkeep = |f: &PathBuf| !f.ends_with(".gitkeep"); + let filter_proto_toml_orig = |f: &PathBuf| !f.ends_with("Proto.toml.orig"); + let mut actual_files: Vec = vfs .files .iter() @@ -116,6 +118,7 @@ impl VirtualFileSystem { .map(|f| f.strip_prefix(self.root()).unwrap().to_path_buf()) .filter(filter_vhome) .filter(filter_gitkeep) + .filter(filter_proto_toml_orig) .collect(); actual_files.sort(); @@ -127,6 +130,7 @@ impl VirtualFileSystem { .map(|f| f.strip_prefix(expected.as_ref()).unwrap().to_path_buf()) .filter(filter_vhome) .filter(filter_gitkeep) + .filter(filter_proto_toml_orig) .collect(); expected_files.sort();