From d9a8fb5d9c97a560a5e2c29358f4bb969b963af7 Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Wed, 17 Apr 2024 13:32:59 +0200 Subject: [PATCH 1/4] feat: wasmer-config Add a new wasmer-config create, which: * Supersedes the wasmer-toml crate * Includes the app.yaml configuration schema Compared to the original upstream wasmer-toml, this PR includes the following additions: * new types for packages (PackageSource, PackageId, PackageIdent, Sha256Hash, PackageHash, ...) * `AppConfigV1`, which describes the format of `app.yaml` files The motivation for inclusion is to: * Simplify management so we don't have to maintain another repository * Expand the scope of the crate to include additional Wasmer related configuration - we would oterhwise need to maintain additional crates The PR also adds a new Github workflow for checking the crate. --- .github/workflows/wasmer-config.yaml | 54 + Cargo.lock | 27 +- Cargo.toml | 2 + lib/config/.gitignore | 1 + lib/config/CHANGELOG.md | 92 + lib/config/Cargo.lock | 511 ++++++ lib/config/Cargo.toml | 34 + lib/config/LICENSE | 25 + lib/config/README.md | 48 + lib/config/rust-toolchain.toml | 3 + lib/config/src/app/healthcheck.rs | 50 + lib/config/src/app/mod.rs | 287 ++++ lib/config/src/cargo_annotations.rs | 26 + lib/config/src/hash.rs | 105 ++ lib/config/src/lib.rs | 6 + lib/config/src/package/error.rs | 27 + lib/config/src/package/mod.rs | 1490 +++++++++++++++++ lib/config/src/package/named_package_ident.rs | 412 +++++ lib/config/src/package/package_hash.rs | 121 ++ lib/config/src/package/package_id.rs | 123 ++ lib/config/src/package/package_ident.rs | 94 ++ lib/config/src/package/package_source.rs | 367 ++++ 22 files changed, 3903 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/wasmer-config.yaml create mode 100644 lib/config/.gitignore create mode 100644 lib/config/CHANGELOG.md create mode 100644 lib/config/Cargo.lock create mode 100644 lib/config/Cargo.toml create mode 100644 lib/config/LICENSE create mode 100644 lib/config/README.md create mode 100644 lib/config/rust-toolchain.toml create mode 100644 lib/config/src/app/healthcheck.rs create mode 100644 lib/config/src/app/mod.rs create mode 100644 lib/config/src/cargo_annotations.rs create mode 100644 lib/config/src/hash.rs create mode 100644 lib/config/src/lib.rs create mode 100644 lib/config/src/package/error.rs create mode 100644 lib/config/src/package/mod.rs create mode 100644 lib/config/src/package/named_package_ident.rs create mode 100644 lib/config/src/package/package_hash.rs create mode 100644 lib/config/src/package/package_id.rs create mode 100644 lib/config/src/package/package_ident.rs create mode 100644 lib/config/src/package/package_source.rs diff --git a/.github/workflows/wasmer-config.yaml b/.github/workflows/wasmer-config.yaml new file mode 100644 index 00000000000..6d1b30fc953 --- /dev/null +++ b/.github/workflows/wasmer-config.yaml @@ -0,0 +1,54 @@ +# Dedicated workflow just for the wasmer-config crate +name: wasmer-config + +on: + push: + branches: + - master + pull_request: + +# Automatically cancel previous workflow runs when a new commit is pushed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + DEFAULT_CRATE_NAME: wasmer_toml + +jobs: + check: + name: Compile and Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + - name: Setup Rust + uses: dsherret/rust-toolchain-file@v1 + - name: Install Nextest + uses: taiki-e/install-action@nextest + - name: Type Checking + run: | + cd lib/config && cargo check --verbose --locked + - name: Build + run: | + cd lib/config && cargo build --verbose --locked + - name: Test + run: | + cd lib/config && cargo nextest run --verbose --locked + + lints: + name: Linting and Formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + - name: Setup Rust + uses: dsherret/rust-toolchain-file@v1 + - name: Check Formatting + run: | + cd lib/config && cargo fmt --verbose --check + - name: Clippy + run: | + cd lib/config && cargo clippy --verbose diff --git a/Cargo.lock b/Cargo.lock index 9d67007052e..66453461c51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4376,9 +4376,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", @@ -6521,6 +6521,28 @@ dependencies = [ "wasmer-types", ] +[[package]] +name = "wasmer-config" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytesize", + "derive_builder", + "hex", + "indexmap 2.2.6", + "pretty_assertions", + "schemars", + "semver 1.0.22", + "serde", + "serde_cbor", + "serde_json", + "serde_yaml 0.9.34+deprecated", + "tempfile", + "thiserror", + "toml 0.8.12", + "url", +] + [[package]] name = "wasmer-derive" version = "4.2.8" @@ -6940,6 +6962,7 @@ dependencies = [ "wasmer-compiler-cranelift", "wasmer-compiler-llvm", "wasmer-compiler-singlepass", + "wasmer-config", "wasmer-emscripten", "wasmer-middlewares", "wasmer-types", diff --git a/Cargo.toml b/Cargo.toml index c5edd62f9a5..235b9ee9c66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ rust-version.workspace = true version.workspace = true [dependencies] +wasmer-config = { path = "./lib/config" } wasmer = { version = "=4.2.8", path = "lib/api", default-features = false } wasmer-compiler = { version = "=4.2.8", path = "lib/compiler", features = [ "compiler", @@ -50,6 +51,7 @@ members = [ "lib/compiler-llvm", "lib/compiler-singlepass", "lib/compiler", + "lib/config", "lib/derive", "lib/emscripten", "lib/object", diff --git a/lib/config/.gitignore b/lib/config/.gitignore new file mode 100644 index 00000000000..2f7896d1d13 --- /dev/null +++ b/lib/config/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/lib/config/CHANGELOG.md b/lib/config/CHANGELOG.md new file mode 100644 index 00000000000..1c6758ff704 --- /dev/null +++ b/lib/config/CHANGELOG.md @@ -0,0 +1,92 @@ +# Changelog + +## [0.10.0](https://github.com/wasmerio/wasmer-toml/compare/wasmer-toml-v0.9.3...wasmer-toml-v0.10.0) (2024-04-09) + + +### Features + +* Add support for unnamed packages ([#40](https://github.com/wasmerio/wasmer-toml/issues/40)) ([#41](https://github.com/wasmerio/wasmer-toml/issues/41)) ([7d1fd97](https://github.com/wasmerio/wasmer-toml/commit/7d1fd978852736afab5de36ff9b3066d7a2a6108)) + +## [0.9.3](https://github.com/wasmerio/wasmer-toml/compare/wasmer-toml-v0.9.2...wasmer-toml-v0.9.3) (2024-04-09) + + +### Miscellaneous Chores + +* release 0.9.3 ([#36](https://github.com/wasmerio/wasmer-toml/issues/36)) ([d2e0003](https://github.com/wasmerio/wasmer-toml/commit/d2e0003a7b014ac01e4db94d66b757c6e3a5b409)) + +## [0.9.2](https://github.com/wasmerio/wasmer-toml/compare/wasmer-toml-v0.9.1...wasmer-toml-v0.9.2) (2023-10-10) + + +### Features + +* Deprecated CommandV1 and several unused Package fields ([331831e](https://github.com/wasmerio/wasmer-toml/commit/331831e1064f5f49d3fc134ba76297cb777fcdcb)) + + +### Bug Fixes + +* Serializing a `wasmer_toml::Package` won't include the `private` flag unless it is `true` ([1791623](https://github.com/wasmerio/wasmer-toml/commit/1791623d0c8ff4d03429b78053d93561ff62da70)) + +## [0.9.1](https://github.com/wasmerio/wasmer-toml/compare/wasmer-toml-v0.9.0...wasmer-toml-v0.9.1) (2023-10-09) + + +### Features + +* Packages can be marked as private by setting `private = true` under `[package]` ([6eb00dc](https://github.com/wasmerio/wasmer-toml/commit/6eb00dc55d72ec04ab04dda96d169a01cf56bef0)) + +## [0.9.0](https://github.com/wasmerio/wasmer-toml/compare/wasmer-toml-v0.8.1...wasmer-toml-v0.9.0) (2023-09-29) + + +### ⚠ BREAKING CHANGES + +* Upgraded public dependencies + +### Miscellaneous Chores + +* Upgraded public dependencies ([2749624](https://github.com/wasmerio/wasmer-toml/commit/2749624bb63bb8fe614eb26d0d871828cce49b14)) + +## [0.8.1](https://github.com/wasmerio/wasmer-toml/compare/wasmer-toml-v0.8.0...wasmer-toml-v0.8.1) (2023-09-29) + + +### Bug Fixes + +* Public dependencies that aren't 1.0 yet are now re-exported using `pub extern crate` ([f320204](https://github.com/wasmerio/wasmer-toml/commit/f320204adc8cff1fa635b59e651adcdffff11702)) + +## [0.8.0](https://github.com/wasmerio/wasmer-toml/compare/wasmer-toml-v0.7.0...wasmer-toml-v0.8.0) (2023-09-19) + + +### ⚠ BREAKING CHANGES + +* Removed some unnecessary command getters and switched others from returning owned copies to returning references + +### Features + +* Commands can now use modules from other dependencies with the `module = "my/dependency:module"` syntax ([88b784d](https://github.com/wasmerio/wasmer-toml/commit/88b784dc6ed5ddae6c2edc69c82c416be62cef35)) +* Removed some unnecessary command getters and switched others from returning owned copies to returning references ([88b784d](https://github.com/wasmerio/wasmer-toml/commit/88b784dc6ed5ddae6c2edc69c82c416be62cef35)) + + +### Miscellaneous Chores + +* Release 0.8.0 ([c885839](https://github.com/wasmerio/wasmer-toml/commit/c8858399767cec116f8560a5e913bdfdf3e00771)) + +## [0.7.0](https://github.com/wasmerio/wasmer-toml/compare/wasmer-toml-v0.6.0...wasmer-toml-v0.7.0) (2023-07-20) + + +### ⚠ BREAKING CHANGES + +* Manifest and Package are now #[non_exhaustive] and configurable via a builder API +* made ManifestError and ValidationError more strongly typed and descriptive +* Removed unnecessary Option wrappers from the Manifest type + +### Features + +* Added an "entrypoint" field to the "[package]" table (fixes [#15](https://github.com/wasmerio/wasmer-toml/issues/15)) ([d6bce6b](https://github.com/wasmerio/wasmer-toml/commit/d6bce6b620000dd156e3cc5a6aefa9c316c7c8ac)) +* Added validation for duplicate commands and modules ([26f8f84](https://github.com/wasmerio/wasmer-toml/commit/26f8f84e168c01e30d5838b10b2eea10b457f57c)) +* Added validation to check that the entrypoint is valid ([b9b677c](https://github.com/wasmerio/wasmer-toml/commit/b9b677cc461896cdc26246d32add2043b26ffd1e)) +* made ManifestError and ValidationError more strongly typed and descriptive ([75040b8](https://github.com/wasmerio/wasmer-toml/commit/75040b8bb73a267024ae2f11aeda88387a56795e)) +* Manifest and Package are now #[non_exhaustive] and configurable via a builder API ([2b99e5c](https://github.com/wasmerio/wasmer-toml/commit/2b99e5cc8a1f9c1e6aa1a9e6d9da05ca6a5cd998)) +* Removed unnecessary Option wrappers from the Manifest type ([5307784](https://github.com/wasmerio/wasmer-toml/commit/53077842114d39b0d1ce8277c4158f669e641545)) + + +### Miscellaneous Chores + +* release 0.7.0 ([e855934](https://github.com/wasmerio/wasmer-toml/commit/e85593437f3d862b06659b105528199fbfcb1cbf)) diff --git a/lib/config/Cargo.lock b/lib/config/Cargo.lock new file mode 100644 index 00000000000..39ef9fc10bd --- /dev/null +++ b/lib/config/Cargo.lock @@ -0,0 +1,511 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad227c3af19d4914570ad36d30409928b75967c298feb9ea1969db3a610bb14e" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "libc" +version = "0.2.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + +[[package]] +name = "memchr" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rustix" +version = "0.38.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "semver" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "toml" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc1433177506450fe920e46a4f9812d0c211f5dd556da10e731a0a3dfa151f0" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca676d9ba1a322c1b64eb8045a5ec5c0cfb0c9d08e15e9ff622589ad5221c8fe" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + +[[package]] +name = "wasmer-toml" +version = "0.10.0" +dependencies = [ + "anyhow", + "derive_builder", + "indexmap", + "semver", + "serde", + "serde_cbor", + "serde_json", + "serde_yaml", + "tempfile", + "thiserror", + "toml", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +dependencies = [ + "memchr", +] diff --git a/lib/config/Cargo.toml b/lib/config/Cargo.toml new file mode 100644 index 00000000000..0e1ed36d6f3 --- /dev/null +++ b/lib/config/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "wasmer-config" +version = "0.1.0" +description = "Configuration types for Wasmer." +edition.workspace = true +license.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +keywords = ["wasm", "wasmer", "toml"] +categories = ["parser-implementations", "wasm"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +anyhow = "1" +toml = "0.8" +thiserror = "1" +semver = { version = "1", features = ["serde"] } +serde_json = "1" +serde_yaml = "0.9.0" +serde_cbor = "0.11.2" +indexmap = { version = "2", features = ["serde"] } +derive_builder = "0.12.0" +bytesize = { version = "1.3.0", features = ["serde"] } +schemars = { version = "0.8.16", features = ["url"] } +url = { version = "2.5.0", features = ["serde"] } +hex = "0.4.3" + +[dev-dependencies] +pretty_assertions = "1.4.0" +serde_json = "1.0.116" +tempfile = "3.3.0" diff --git a/lib/config/LICENSE b/lib/config/LICENSE new file mode 100644 index 00000000000..593f89fc7c6 --- /dev/null +++ b/lib/config/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2022 The Wasmer Engineering Team + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/lib/config/README.md b/lib/config/README.md new file mode 100644 index 00000000000..91b28f62f27 --- /dev/null +++ b/lib/config/README.md @@ -0,0 +1,48 @@ +# wasmer-config + +[![Continuous Integration](https://github.com/wasmerio/wasmer-toml/actions/workflows/ci.yml/badge.svg)](https://github.com/wasmerio/wasmer-toml/actions/workflows/ci.yml) + +([API Docs](https://wasmerio.github.io/wasmer-toml)) + +Provides configuration types for Wasmer. + +## For Developers + +### Releasing + +This repository uses [Release Please][release-please] to automate a lot of the +work around creating releases. + +Every time a commit following the [Conventional Commit Style][conv] is merged +into `main`, the [`release-please.yml`](.github/workflows/release-please.yml) +workflow will run and update the "Release PR" to reflect the new changes. + +For commits that just fix bugs (i.e. the message starts with `"fix: "`), the +associated crate will receive a changelog entry and a patch version bump. +Similarly, adding a new feature (i.e. `"feat:"`) does a minor version bump and +adding breaking changes (i.e. `"fix!:"` or `"feat!:"`) will result in a major +version bump. + +When the release PR is merged, the updated changelogs and bumped version numbers +will be merged into the `main` branch, the `release-please.yml` workflow will +automatically generate GitHub Releases, and CI will publish the crate if +necessary. + +TL;DR: + +1. Use [Conventional Commit Messages][conv] whenever you make a noteworthy change +2. Merge the release PR when ready to release +3. Let the automation do everything else + +## License + +This project is licensed under the MIT license ([LICENSE](./LICENSE) or +). + +It is recommended to always use [`cargo crev`][crev] to verify the +trustworthiness of each of your dependencies, including this one. + +[conv]: https://www.conventionalcommits.org/en/v1.0.0/ +[crev]: https://github.com/crev-dev/cargo-crev +[release-please]: https://github.com/googleapis/release-please +[wasmer]: https://wasmer.io/ diff --git a/lib/config/rust-toolchain.toml b/lib/config/rust-toolchain.toml new file mode 100644 index 00000000000..0538cafabc5 --- /dev/null +++ b/lib/config/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.70" +components = ["rustfmt", "clippy"] diff --git a/lib/config/src/app/healthcheck.rs b/lib/config/src/app/healthcheck.rs new file mode 100644 index 00000000000..5866b88af2b --- /dev/null +++ b/lib/config/src/app/healthcheck.rs @@ -0,0 +1,50 @@ +#[derive( + schemars::JsonSchema, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone, Debug, +)] +pub enum HealthCheckV1 { + #[serde(rename = "http")] + Http(HealthCheckHttpV1), +} + +/// Health check configuration for http endpoints. +#[derive( + schemars::JsonSchema, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone, Debug, +)] +pub struct HealthCheckHttpV1 { + /// Path to the health check endpoint. + pub path: String, + /// HTTP method to use for the health check. + /// + /// Defaults to GET. + #[serde(skip_serializing_if = "Option::is_none")] + pub method: Option, + /// Interval for the health check. + /// + /// Format: 1s, 5m, 11h, ... + /// + /// Defaults to 60s. + #[serde(skip_serializing_if = "Option::is_none")] + pub interval: Option, + /// Timeout for the health check. + /// + /// Deaults to 120s. + /// + /// Format: 1s, 5m, 11h, ... + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option, + /// Number of retries before the health check is considered unhealthy. + #[serde(skip_serializing_if = "Option::is_none")] + pub unhealthy_threshold: Option, + /// Number of retries before the health check is considered healthy again. + #[serde(skip_serializing_if = "Option::is_none")] + pub healthy_threshold: Option, + /// Expected status codes that are considered a pass for the health check. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub expected_status_codes: Vec, + /// Optional text that is in the body of the response that is considered a pass for the health check. + #[serde(skip_serializing_if = "Option::is_none")] + pub expected_body_includes: Option, + /// Regular expression tested against the body that is considered a pass for the health check + #[serde(skip_serializing_if = "Option::is_none")] + pub expected_body_regex: Option, +} diff --git a/lib/config/src/app/mod.rs b/lib/config/src/app/mod.rs new file mode 100644 index 00000000000..8934f7da516 --- /dev/null +++ b/lib/config/src/app/mod.rs @@ -0,0 +1,287 @@ +//! User-facing app.yaml file config: [`AppConfigV1`]. + +mod healthcheck; + +pub use self::healthcheck::{HealthCheckHttpV1, HealthCheckV1}; + +use std::collections::HashMap; + +use anyhow::{bail, Context}; +use bytesize::ByteSize; + +use crate::package::PackageSource; + +/// User-facing app.yaml config file for apps. +/// +/// NOTE: only used by the backend, Edge itself does not use this format, and +/// uses [`super::AppVersionV1Spec`] instead. +#[derive( + serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq, +)] +pub struct AppConfigV1 { + /// Name of the app. + pub name: String, + + /// App id assigned by the backend. + /// + /// This will get populated once the app has been deployed. + /// + /// This id is also used to map to the existing app during deployments. + // #[serde(skip_serializing_if = "Option::is_none")] + // pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub app_id: Option, + + /// Owner of the app. + /// + /// This is either a username or a namespace. + #[serde(skip_serializing_if = "Option::is_none")] + pub owner: Option, + + /// The package to execute. + pub package: PackageSource, + + /// Domains for the app. + /// + /// This can include both provider-supplied + /// alias domains and custom domains. + #[serde(skip_serializing_if = "Option::is_none")] + pub domains: Option>, + + /// Environment variables. + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub env: HashMap, + + // CLI arguments passed to the runner. + /// Only applicable for runners that accept CLI arguments. + #[serde(skip_serializing_if = "Option::is_none")] + pub cli_args: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub capabilities: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub scheduled_tasks: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub volumes: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub health_checks: Option>, + + /// Enable debug mode, which will show detailed error pages in the web gateway. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub debug: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub scaling: Option, + + /// Capture extra fields for forwards compatibility. + #[serde(flatten)] + pub extra: HashMap, +} + +#[derive( + serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq, +)] +pub struct AppScalingConfigV1 { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mode: Option, +} + +#[derive( + serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq, +)] +pub enum AppScalingModeV1 { + #[serde(rename = "single_concurrency")] + SingleConcurrency, +} + +#[derive( + serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq, +)] +pub struct AppVolume { + pub name: String, + pub mounts: Vec, +} + +#[derive( + serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq, +)] +pub struct AppVolumeMount { + /// Path to mount the volume at. + pub mount_path: String, + /// Sub-path within the volume to mount. + pub sub_path: Option, +} + +#[derive( + serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq, +)] +pub struct AppScheduledTask { + pub name: String, + // #[serde(flatten)] + // pub spec: CronJobSpecV1, +} + +impl AppConfigV1 { + pub const KIND: &'static str = "wasmer.io/App.v0"; + pub const CANONICAL_FILE_NAME: &'static str = "app.yaml"; + + pub fn to_yaml_value(self) -> Result { + // Need to do an annoying type dance to both insert the kind field + // and also insert kind at the top. + let obj = match serde_yaml::to_value(self)? { + serde_yaml::Value::Mapping(m) => m, + _ => unreachable!(), + }; + let mut m = serde_yaml::Mapping::new(); + m.insert("kind".into(), Self::KIND.into()); + for (k, v) in obj.into_iter() { + m.insert(k, v); + } + Ok(m.into()) + } + + pub fn to_yaml(self) -> Result { + serde_yaml::to_string(&self.to_yaml_value()?) + } + + pub fn parse_yaml(value: &str) -> Result { + let raw = serde_yaml::from_str::(value).context("invalid yaml")?; + let kind = raw + .get("kind") + .context("invalid app config: no 'kind' field found")? + .as_str() + .context("invalid app config: 'kind' field is not a string")?; + match kind { + Self::KIND => {} + other => { + bail!( + "invalid app config: unspported kind '{}', expected {}", + other, + Self::KIND + ); + } + } + + let data = serde_yaml::from_value(raw).context("could not deserialize app config")?; + Ok(data) + } +} + +/// Restricted version of [`super::CapabilityMapV1`], with only a select subset +/// of settings. +#[derive( + serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq, +)] +pub struct AppConfigCapabilityMapV1 { + /// Instance memory settings. + #[serde(skip_serializing_if = "Option::is_none")] + pub memory: Option, +} + +/// Memory capability settings. +/// +/// NOTE: this is kept separate from the [`super::CapabilityMemoryV1`] struct +/// to have separation between the high-level app.yaml and the more internal +/// App entity. +#[derive( + serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq, +)] +pub struct AppConfigCapabilityMemoryV1 { + /// Memory limit for an instance. + /// + /// Format: [digit][unit], where unit is Mb/Gb/MiB/GiB,... + #[schemars(with = "Option")] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_app_config_v1_deser() { + let config = r#" +kind: wasmer.io/App.v0 +name: test +package: ns/name@0.1.0 +debug: true +env: + e1: v1 + E2: V2 +cli_args: + - arg1 + - arg2 +scheduled_tasks: + - name: backup + schedule: 1day + max_retries: 3 + timeout: 10m + invoke: + fetch: + url: /api/do-backup + headers: + h1: v1 + success_status_codes: [200, 201] + "#; + + let parsed = AppConfigV1::parse_yaml(config).unwrap(); + + assert_eq!( + parsed, + AppConfigV1 { + name: "test".to_string(), + app_id: None, + package: "ns/name@0.1.0".parse().unwrap(), + owner: None, + domains: None, + env: [ + ("e1".to_string(), "v1".to_string()), + ("E2".to_string(), "V2".to_string()) + ] + .into_iter() + .collect(), + volumes: None, + cli_args: Some(vec!["arg1".to_string(), "arg2".to_string()]), + capabilities: None, + scaling: None, + scheduled_tasks: Some(vec![AppScheduledTask { + name: "backup".to_string(), + // spec: CronJobSpecV1 { + // schedule: "1day".to_string(), + // max_schedule_drift: None, + // job: crate::schema::JobDefinition { + // max_retries: Some(3), + // timeout: Some(std::time::Duration::from_secs(10 * 60).into()), + // invoke: crate::schema::JobInvoke::Fetch( + // crate::schema::JobInvokeFetch { + // url: "/api/do-backup".parse().unwrap(), + // headers: Some( + // [("h1".to_string(), "v1".to_string())] + // .into_iter() + // .collect() + // ), + // success_status_codes: Some(vec![200, 201]), + // method: None, + // } + // ) + // }, + // } + }]), + health_checks: None, + extra: [( + "kind".to_string(), + serde_json::Value::from("wasmer.io/App.v0") + ),] + .into_iter() + .collect(), + debug: Some(true), + } + ); + } +} diff --git a/lib/config/src/cargo_annotations.rs b/lib/config/src/cargo_annotations.rs new file mode 100644 index 00000000000..33b4b3e59be --- /dev/null +++ b/lib/config/src/cargo_annotations.rs @@ -0,0 +1,26 @@ +//! Rust and cargo specific annotations used to interoperate with external tools. + +use std::{collections::HashMap, path::PathBuf}; + +use crate::package::{Abi, Bindings}; + +/// The annotation used by `cargo wapm` when it parses the +/// `[package.metadata.wapm]` table in your `Cargo.toml`. +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct CargoWasmerPackageAnnotation { + /// The namespace this package should be published under. + pub namespace: String, + /// The name the package should be published under, if it differs from the + /// crate name. + pub package: Option, + /// Extra flags that should be passed to the `wasmer` CLI. + pub wasmer_extra_flags: Option, + /// The ABI to use when adding the compiled crate to the package. + pub abi: Abi, + /// Filesystem mappings. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fs: Option>, + /// Binding declarations for the crate. + pub bindings: Option, +} diff --git a/lib/config/src/hash.rs b/lib/config/src/hash.rs new file mode 100644 index 00000000000..79cf00de31b --- /dev/null +++ b/lib/config/src/hash.rs @@ -0,0 +1,105 @@ +/// Sha256 hash, represented as bytes. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Sha256Hash(pub [u8; 32]); + +impl Sha256Hash { + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +impl std::str::FromStr for Sha256Hash { + type Err = Sha256HashParseError; + + fn from_str(s: &str) -> Result { + if s.len() != 64 { + return Err(Sha256HashParseError { + value: s.to_string(), + message: "invalid hash length - hash must have 64 hex-encoded characters " + .to_string(), + }); + } + + let bytes = hex::decode(s).map_err(|e| Sha256HashParseError { + value: s.to_string(), + message: e.to_string(), + })?; + + Ok(Sha256Hash(bytes.try_into().unwrap())) + } +} + +impl std::fmt::Display for Sha256Hash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + +impl schemars::JsonSchema for Sha256Hash { + fn schema_name() -> String { + "Sha256Hash".to_string() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + String::json_schema(gen) + } +} + +#[derive(Clone, Debug)] +pub struct Sha256HashParseError { + value: String, + message: String, +} + +impl std::fmt::Display for Sha256HashParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "could not parse value as sha256 hash: {} (value: '{}')", + self.message, self.value + ) + } +} + +impl std::error::Error for Sha256HashParseError {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hash_sha256_parse_roundtrip() { + let input = "c355cd53795b9b481f7eb2b5f4f6c8cf73631bdc343723a579d671e32db70b3c"; + let h1 = input + .parse::() + .expect("string should parse to hash"); + + assert_eq!( + h1.0, + [ + 195, 85, 205, 83, 121, 91, 155, 72, 31, 126, 178, 181, 244, 246, 200, 207, 115, 99, + 27, 220, 52, 55, 35, 165, 121, 214, 113, 227, 45, 183, 11, 60 + ], + ); + + assert_eq!(h1.to_string(), input); + } + + #[test] + fn hash_sha256_parse_fails() { + let res1 = + "c355cd53795b9b481f7eb2b5f4f6c8cf73631bdc343723a579d671e32db70b3".parse::(); + assert!(matches!(res1, Err(_))); + + let res2 = "".parse::(); + assert!(matches!(res2, Err(_))); + + let res3 = "öööööööööööööööööööööööööööööööööööööööööööööööööööööööööööööööö" + .parse::(); + assert!(matches!(res3, Err(_))); + } +} diff --git a/lib/config/src/lib.rs b/lib/config/src/lib.rs new file mode 100644 index 00000000000..3a1f7a97898 --- /dev/null +++ b/lib/config/src/lib.rs @@ -0,0 +1,6 @@ +//! Provides configuration types for Wasmer. + +pub mod app; +pub mod cargo_annotations; +pub mod hash; +pub mod package; diff --git a/lib/config/src/package/error.rs b/lib/config/src/package/error.rs new file mode 100644 index 00000000000..98d0c02193e --- /dev/null +++ b/lib/config/src/package/error.rs @@ -0,0 +1,27 @@ +/// Error that occurs during package ident/source parsing. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PackageParseError { + value: String, + message: String, +} + +impl PackageParseError { + pub(crate) fn new(value: impl Into, message: impl Into) -> Self { + Self { + value: value.into(), + message: message.into(), + } + } +} + +impl std::fmt::Display for PackageParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "could not parse value as package identifier: {} (value: '{}')", + self.message, self.value + ) + } +} + +impl std::error::Error for PackageParseError {} diff --git a/lib/config/src/package/mod.rs b/lib/config/src/package/mod.rs new file mode 100644 index 00000000000..1a3ca32a746 --- /dev/null +++ b/lib/config/src/package/mod.rs @@ -0,0 +1,1490 @@ +//! Wasmer package definitions. +//! +//! Describes the contents of a `wasmer.toml` file. + +#![allow(deprecated)] + +mod error; +mod named_package_ident; +mod package_hash; +mod package_id; +mod package_ident; +mod package_source; + +pub use self::{ + error::PackageParseError, + named_package_ident::{NamedPackageIdent, Tag}, + package_hash::PackageHash, + package_id::{NamedPackageId, PackageId}, + package_ident::PackageIdent, + package_source::PackageSource, +}; + +use std::{ + borrow::Cow, + collections::{hash_map::HashMap, BTreeMap, BTreeSet}, + fmt::{self, Display}, + path::{Path, PathBuf}, + str::FromStr, +}; + +use indexmap::IndexMap; +use semver::{Version, VersionReq}; +use serde::{de::Error as _, Deserialize, Serialize}; +use thiserror::Error; + +/// The ABI is a hint to WebAssembly runtimes about what additional imports to +/// insert and how a module may be run. +/// +/// If not specified, [`Abi::None`] is the default. +#[derive(Clone, Copy, Default, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[non_exhaustive] +pub enum Abi { + #[serde(rename = "emscripten")] + Emscripten, + #[default] + #[serde(rename = "none")] + None, + #[serde(rename = "wasi")] + Wasi, + #[serde(rename = "wasm4")] + WASM4, +} + +impl Abi { + /// Get the ABI's human-friendly name. + pub fn to_str(&self) -> &str { + match self { + Abi::Emscripten => "emscripten", + Abi::Wasi => "wasi", + Abi::WASM4 => "wasm4", + Abi::None => "generic", + } + } + + /// Is this a [`Abi::None`]? + pub fn is_none(&self) -> bool { + matches!(self, Abi::None) + } + + /// Create an [`Abi`] from its human-friendly name. + pub fn from_name(name: &str) -> Self { + name.parse().unwrap_or(Abi::None) + } +} + +impl fmt::Display for Abi { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_str()) + } +} + +impl FromStr for Abi { + type Err = Box; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "emscripten" => Ok(Abi::Emscripten), + "wasi" => Ok(Abi::Wasi), + "wasm4" => Ok(Abi::WASM4), + "generic" => Ok(Abi::None), + _ => Err(format!("Unknown ABI, \"{s}\"").into()), + } + } +} + +/// The default name for the manifest file. +pub static MANIFEST_FILE_NAME: &str = "wasmer.toml"; + +const README_PATHS: &[&str; 5] = &[ + "README", + "README.md", + "README.markdown", + "README.mdown", + "README.mkdn", +]; + +const LICENSE_PATHS: &[&str; 3] = &["LICENSE", "LICENSE.md", "COPYING"]; + +/// Package definition for a Wasmer package. +/// +/// Usually stored in a `wasmer.toml` file. +#[derive(Clone, Debug, Deserialize, Serialize, derive_builder::Builder)] +#[non_exhaustive] +pub struct Package { + /// The package's name in the form `namespace/name`. + #[builder(setter(into))] + pub name: String, + /// The package's version number. + pub version: Version, + /// A brief description of the package. + #[builder(setter(into))] + pub description: String, + /// A SPDX license specifier for this package. + #[builder(setter(into, strip_option), default)] + pub license: Option, + /// The location of the license file, useful for non-standard licenses + #[serde(rename = "license-file")] + #[builder(setter(into, strip_option), default)] + pub license_file: Option, + /// The package's README file. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(setter(into, strip_option), default)] + pub readme: Option, + /// A URL pointing to the package's source code. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(setter(into, strip_option), default)] + pub repository: Option, + /// The website used as the package's homepage. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(setter(into, strip_option), default)] + pub homepage: Option, + #[serde(rename = "wasmer-extra-flags")] + #[builder(setter(into, strip_option), default)] + #[deprecated( + since = "0.9.2", + note = "Use runner-specific command attributes instead" + )] + pub wasmer_extra_flags: Option, + #[serde( + rename = "disable-command-rename", + default, + skip_serializing_if = "std::ops::Not::not" + )] + #[builder(default)] + #[deprecated( + since = "0.9.2", + note = "Does nothing. Prefer a runner-specific command attribute instead" + )] + pub disable_command_rename: bool, + /// Unlike, `disable-command-rename` which prevents `wasmer run `, + /// this flag enables the command rename of `wasmer run ` into + /// just ``. This is useful for programs that need to inspect + /// their `argv[0]` names and when the command name matches their executable + /// name. + #[serde( + rename = "rename-commands-to-raw-command-name", + default, + skip_serializing_if = "std::ops::Not::not" + )] + #[builder(default)] + #[deprecated( + since = "0.9.2", + note = "Does nothing. Prefer a runner-specific command attribute instead" + )] + pub rename_commands_to_raw_command_name: bool, + /// The name of the command that should be used by `wasmer run` by default. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(setter(into, strip_option), default)] + pub entrypoint: Option, + /// Mark this as a private package + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + #[builder(default)] + pub private: bool, +} + +impl Package { + /// Create a [`PackageBuilder`] populated with all mandatory fields. + pub fn builder( + name: impl Into, + version: Version, + description: impl Into, + ) -> PackageBuilder { + PackageBuilder::new(name, version, description) + } +} + +impl PackageBuilder { + pub fn new(name: impl Into, version: Version, description: impl Into) -> Self { + let mut builder = PackageBuilder::default(); + builder.name(name).version(version).description(description); + builder + } +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum Command { + V1(CommandV1), + V2(CommandV2), +} + +impl Command { + /// Get the command's name. + pub fn get_name(&self) -> &str { + match self { + Self::V1(c) => &c.name, + Self::V2(c) => &c.name, + } + } + + /// Get the module this [`Command`] refers to. + pub fn get_module(&self) -> &ModuleReference { + match self { + Self::V1(c) => &c.module, + Self::V2(c) => &c.module, + } + } +} + +/// Describes a command for a wasmer module. +/// +/// When a command is deserialized using [`CommandV1`], the runner is inferred +/// by looking at the [`Abi`] from the [`Module`] it refers to. +/// +/// If possible, prefer to use the [`CommandV2`] format. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] // Note: needed to prevent accidentally parsing +// a CommandV2 as a CommandV1 +#[deprecated(since = "0.9.2", note = "Prefer the CommandV2 syntax")] +pub struct CommandV1 { + pub name: String, + pub module: ModuleReference, + pub main_args: Option, + pub package: Option, +} + +/// An executable command. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct CommandV2 { + /// The name of the command. + pub name: String, + /// The module containing this command's executable. + pub module: ModuleReference, + /// The runner to use when running this command. + /// + /// This may be a URL, or the well-known runners `wasi`, `wcgi`, or + /// `emscripten`. + pub runner: String, + /// Extra annotations that will be consumed by the runner. + pub annotations: Option, +} + +impl CommandV2 { + /// Get annotations, automatically loading them from a file relative to the + /// `wasmer.toml`'s directory, if necessary. + pub fn get_annotations(&self, basepath: &Path) -> Result, String> { + match self.annotations.as_ref() { + Some(CommandAnnotations::Raw(v)) => Ok(Some(toml_to_cbor_value(v))), + Some(CommandAnnotations::File(FileCommandAnnotations { file, kind })) => { + let path = basepath.join(file.clone()); + let file = std::fs::read_to_string(&path).map_err(|e| { + format!( + "Error reading {:?}.annotation ({:?}): {e}", + self.name, + path.display() + ) + })?; + match kind { + FileKind::Json => { + let value: serde_json::Value = + serde_json::from_str(&file).map_err(|e| { + format!( + "Error reading {:?}.annotation ({:?}): {e}", + self.name, + path.display() + ) + })?; + Ok(Some(json_to_cbor_value(&value))) + } + FileKind::Yaml => { + let value: serde_yaml::Value = + serde_yaml::from_str(&file).map_err(|e| { + format!( + "Error reading {:?}.annotation ({:?}): {e}", + self.name, + path.display() + ) + })?; + Ok(Some(yaml_to_cbor_value(&value))) + } + } + } + None => Ok(None), + } + } +} + +/// A reference to a module which may or may not come from another package. +/// +/// # Serialization +/// +/// A [`ModuleReference`] is serialized via its [`String`] representation. +#[derive(Clone, Debug, PartialEq)] +pub enum ModuleReference { + /// A module in the current package. + CurrentPackage { + /// The name of the module. + module: String, + }, + /// A module that will be provided by a dependency, in `dependency:module` + /// form. + Dependency { + /// The name of the dependency the module comes from. + dependency: String, + /// The name of the module. + module: String, + }, +} + +impl Serialize for ModuleReference { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for ModuleReference { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let repr: Cow<'de, str> = Cow::deserialize(deserializer)?; + repr.parse().map_err(D::Error::custom) + } +} + +impl FromStr for ModuleReference { + type Err = Box; + + fn from_str(s: &str) -> Result { + match s.split_once(':') { + Some((dependency, module)) => { + if module.contains(':') { + return Err("Invalid format".into()); + } + + Ok(ModuleReference::Dependency { + dependency: dependency.to_string(), + module: module.to_string(), + }) + } + None => Ok(ModuleReference::CurrentPackage { + module: s.to_string(), + }), + } + } +} + +impl Display for ModuleReference { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ModuleReference::CurrentPackage { module } => Display::fmt(module, f), + ModuleReference::Dependency { dependency, module } => { + write!(f, "{dependency}:{module}") + } + } + } +} + +fn toml_to_cbor_value(val: &toml::Value) -> serde_cbor::Value { + match val { + toml::Value::String(s) => serde_cbor::Value::Text(s.clone()), + toml::Value::Integer(i) => serde_cbor::Value::Integer(*i as i128), + toml::Value::Float(f) => serde_cbor::Value::Float(*f), + toml::Value::Boolean(b) => serde_cbor::Value::Bool(*b), + toml::Value::Datetime(d) => serde_cbor::Value::Text(format!("{}", d)), + toml::Value::Array(sq) => { + serde_cbor::Value::Array(sq.iter().map(toml_to_cbor_value).collect()) + } + toml::Value::Table(m) => serde_cbor::Value::Map( + m.iter() + .map(|(k, v)| (serde_cbor::Value::Text(k.clone()), toml_to_cbor_value(v))) + .collect(), + ), + } +} + +fn json_to_cbor_value(val: &serde_json::Value) -> serde_cbor::Value { + match val { + serde_json::Value::Null => serde_cbor::Value::Null, + serde_json::Value::Bool(b) => serde_cbor::Value::Bool(*b), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + serde_cbor::Value::Integer(i as i128) + } else if let Some(u) = n.as_u64() { + serde_cbor::Value::Integer(u as i128) + } else if let Some(f) = n.as_f64() { + serde_cbor::Value::Float(f) + } else { + serde_cbor::Value::Null + } + } + serde_json::Value::String(s) => serde_cbor::Value::Text(s.clone()), + serde_json::Value::Array(sq) => { + serde_cbor::Value::Array(sq.iter().map(json_to_cbor_value).collect()) + } + serde_json::Value::Object(m) => serde_cbor::Value::Map( + m.iter() + .map(|(k, v)| (serde_cbor::Value::Text(k.clone()), json_to_cbor_value(v))) + .collect(), + ), + } +} + +fn yaml_to_cbor_value(val: &serde_yaml::Value) -> serde_cbor::Value { + match val { + serde_yaml::Value::Null => serde_cbor::Value::Null, + serde_yaml::Value::Bool(b) => serde_cbor::Value::Bool(*b), + serde_yaml::Value::Number(n) => { + if let Some(i) = n.as_i64() { + serde_cbor::Value::Integer(i as i128) + } else if let Some(u) = n.as_u64() { + serde_cbor::Value::Integer(u as i128) + } else if let Some(f) = n.as_f64() { + serde_cbor::Value::Float(f) + } else { + serde_cbor::Value::Null + } + } + serde_yaml::Value::String(s) => serde_cbor::Value::Text(s.clone()), + serde_yaml::Value::Sequence(sq) => { + serde_cbor::Value::Array(sq.iter().map(yaml_to_cbor_value).collect()) + } + serde_yaml::Value::Mapping(m) => serde_cbor::Value::Map( + m.iter() + .map(|(k, v)| (yaml_to_cbor_value(k), yaml_to_cbor_value(v))) + .collect(), + ), + serde_yaml::Value::Tagged(tag) => yaml_to_cbor_value(&tag.value), + } +} + +/// Annotations for a command. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +#[repr(C)] +pub enum CommandAnnotations { + /// Annotations that will be read from a file on disk. + File(FileCommandAnnotations), + /// Annotations that are specified inline. + Raw(toml::Value), +} + +/// Annotations on disk. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct FileCommandAnnotations { + /// The path to the annotations file. + pub file: PathBuf, + /// Which format are the annotations saved in? + pub kind: FileKind, +} + +/// The different formats that [`FileCommandAnnotations`] can be saved in. +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Deserialize, Serialize)] +pub enum FileKind { + /// A `*.yaml` file that will be deserialized using [`serde_yaml`]. + #[serde(rename = "yaml")] + Yaml, + /// A `*.json` file that will be deserialized using [`serde_json`]. + #[serde(rename = "json")] + Json, +} + +/// A file which may be executed by a [`Command`]. Sometimes also referred to as +/// an "atom". +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct Module { + /// The name used to refer to this module. + pub name: String, + /// The location of the module file on disk, relative to the manifest + /// directory. + pub source: PathBuf, + /// The ABI this module satisfies. + #[serde(default = "Abi::default", skip_serializing_if = "Abi::is_none")] + pub abi: Abi, + #[serde(default)] + pub kind: Option, + /// WebAssembly interfaces this module requires. + #[serde(skip_serializing_if = "Option::is_none")] + pub interfaces: Option>, + /// Interface definitions that can be used to generate bindings to this + /// module. + pub bindings: Option, +} + +/// The interface exposed by a [`Module`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Bindings { + Wit(WitBindings), + Wai(WaiBindings), +} + +impl Bindings { + /// Get all files that make up this interface. + /// + /// For all binding types except [`WitBindings`], this will recursively + /// look for any files that are imported. + /// + /// The caller can assume that any path that was referenced exists. + pub fn referenced_files(&self, base_directory: &Path) -> Result, ImportsError> { + match self { + Bindings::Wit(WitBindings { wit_exports, .. }) => { + // Note: we explicitly don't support imported files with WIT + // because wit-bindgen's wit-parser crate isn't on crates.io. + + let path = base_directory.join(wit_exports); + + if path.exists() { + Ok(vec![path]) + } else { + Err(ImportsError::FileNotFound(path)) + } + } + Bindings::Wai(wai) => wai.referenced_files(base_directory), + } + } +} + +impl Serialize for Bindings { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Bindings::Wit(w) => w.serialize(serializer), + Bindings::Wai(w) => w.serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for Bindings { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = toml::Value::deserialize(deserializer)?; + + let keys = ["wit-bindgen", "wai-version"]; + let [wit_bindgen, wai_version] = keys.map(|key| value.get(key).is_some()); + + match (wit_bindgen, wai_version) { + (true, false) => WitBindings::deserialize(value) + .map(Bindings::Wit) + .map_err(D::Error::custom), + (false, true) => WaiBindings::deserialize(value) + .map(Bindings::Wai) + .map_err(D::Error::custom), + (true, true) | (false, false) => { + let msg = format!( + "expected one of \"{}\" to be provided, but not both", + keys.join("\" or \""), + ); + Err(D::Error::custom(msg)) + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct WitBindings { + /// The version of the WIT format being used. + pub wit_bindgen: Version, + /// The `*.wit` file's location on disk. + pub wit_exports: PathBuf, +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct WaiBindings { + /// The version of the WAI format being used. + pub wai_version: Version, + /// The `*.wai` file defining the interface this package exposes. + pub exports: Option, + /// The `*.wai` files for any functionality this package imports from the + /// host. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub imports: Vec, +} + +impl WaiBindings { + fn referenced_files(&self, base_directory: &Path) -> Result, ImportsError> { + let WaiBindings { + exports, imports, .. + } = self; + + // Note: WAI files may import other WAI files, so we start with all + // WAI files mentioned in the wasmer.toml then recursively add their + // imports. + + let initial_paths = exports + .iter() + .chain(imports) + .map(|relative_path| base_directory.join(relative_path)); + + let mut to_check: Vec = Vec::new(); + + for path in initial_paths { + if !path.exists() { + return Err(ImportsError::FileNotFound(path)); + } + to_check.push(path); + } + + let mut files = BTreeSet::new(); + + while let Some(path) = to_check.pop() { + if files.contains(&path) { + continue; + } + + to_check.extend(get_imported_wai_files(&path)?); + files.insert(path); + } + + Ok(files.into_iter().collect()) + } +} + +/// Parse a `*.wai` file to find the absolute path for any other `*.wai` files +/// it may import, relative to the original `*.wai` file. +/// +/// This function makes sure any imported files exist. +fn get_imported_wai_files(path: &Path) -> Result, ImportsError> { + let _wai_src = std::fs::read_to_string(path).map_err(|error| ImportsError::Read { + path: path.to_path_buf(), + error, + })?; + + let parent_dir = path.parent() + .expect("All paths should have a parent directory because we joined them relative to the base directory"); + + // TODO(Michael-F-Bryan): update the wai-parser crate to give you access to + // the imported interfaces. For now, we just pretend there are no import + // statements in the *.wai file. + let raw_imports: Vec = Vec::new(); + + // Note: imported paths in a *.wai file are all relative, so we need to + // resolve their absolute path relative to the original *.wai file. + let mut resolved_paths = Vec::new(); + + for imported in raw_imports { + let absolute_path = parent_dir.join(imported); + + if !absolute_path.exists() { + return Err(ImportsError::ImportedFileNotFound { + path: absolute_path, + referenced_by: path.to_path_buf(), + }); + } + + resolved_paths.push(absolute_path); + } + + Ok(resolved_paths) +} + +/// Errors that may occur when resolving [`Bindings`] imports. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum ImportsError { + #[error( + "The \"{}\" mentioned in the manifest doesn't exist", + _0.display(), + )] + FileNotFound(PathBuf), + #[error( + "The \"{}\" imported by \"{}\" doesn't exist", + path.display(), + referenced_by.display(), + )] + ImportedFileNotFound { + path: PathBuf, + referenced_by: PathBuf, + }, + #[error("Unable to parse \"{}\" as a WAI file", path.display())] + WaiParse { path: PathBuf }, + #[error("Unable to read \"{}\"", path.display())] + Read { + path: PathBuf, + #[source] + error: std::io::Error, + }, +} + +/// The manifest represents the file used to describe a Wasm package. +#[derive(Clone, Debug, Deserialize, Serialize, derive_builder::Builder)] +#[non_exhaustive] +pub struct Manifest { + /// Metadata about the package itself. + pub package: Option, + /// The package's dependencies. + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + #[builder(default)] + pub dependencies: HashMap, + /// The mappings used when making bundled assets available to WebAssembly + /// instances, in the form guest -> host. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + #[builder(default)] + pub fs: IndexMap, + /// WebAssembly modules to be published. + #[serde(default, rename = "module", skip_serializing_if = "Vec::is_empty")] + #[builder(default)] + pub modules: Vec, + /// Commands the package makes available to users. + #[serde(default, rename = "command", skip_serializing_if = "Vec::is_empty")] + #[builder(default)] + pub commands: Vec, +} + +impl Manifest { + /// Create a [`ManifestBuilder`] populated with all mandatory fields. + pub fn builder(package: Package) -> ManifestBuilder { + ManifestBuilder::new(package) + } + + /// Parse a [`Manifest`] from its TOML representation. + pub fn parse(s: &str) -> Result { + toml::from_str(s) + } + + /// Construct a manifest by searching in the specified directory for a + /// manifest file. + pub fn find_in_directory>(path: T) -> Result { + let path = path.as_ref(); + + if !path.is_dir() { + return Err(ManifestError::MissingManifest(path.to_path_buf())); + } + let manifest_path_buf = path.join(MANIFEST_FILE_NAME); + let contents = std::fs::read_to_string(&manifest_path_buf) + .map_err(|_e| ManifestError::MissingManifest(manifest_path_buf))?; + let mut manifest: Self = toml::from_str(contents.as_str())?; + + if let Some(package) = manifest.package.as_mut() { + if package.readme.is_none() { + package.readme = locate_file(path, README_PATHS); + } + + if package.license_file.is_none() { + package.license_file = locate_file(path, LICENSE_PATHS); + } + } + manifest.validate()?; + + Ok(manifest) + } + + /// Validate this [`Manifest`] to check for common semantic errors. + /// + /// Some common error cases are: + /// + /// - Having multiple modules with the same name + /// - Having multiple commands with the same name + /// - A [`Command`] that references a non-existent [`Module`] in the current + /// package + /// - A [`Package::entrypoint`] which points to a non-existent [`Command`] + pub fn validate(&self) -> Result<(), ValidationError> { + let mut modules = BTreeMap::new(); + + for module in &self.modules { + let is_duplicate = modules.insert(&module.name, module).is_some(); + + if is_duplicate { + return Err(ValidationError::DuplicateModule { + name: module.name.clone(), + }); + } + } + + let mut commands = BTreeMap::new(); + + for command in &self.commands { + let is_duplicate = commands.insert(command.get_name(), command).is_some(); + + if is_duplicate { + return Err(ValidationError::DuplicateCommand { + name: command.get_name().to_string(), + }); + } + + let module_reference = command.get_module(); + match &module_reference { + ModuleReference::CurrentPackage { module } => { + if let Some(module) = modules.get(&module) { + if module.abi == Abi::None && module.interfaces.is_none() { + return Err(ValidationError::MissingABI { + command: command.get_name().to_string(), + module: module.name.clone(), + }); + } + } else { + return Err(ValidationError::MissingModuleForCommand { + command: command.get_name().to_string(), + module: command.get_module().clone(), + }); + } + } + ModuleReference::Dependency { dependency, .. } => { + // We don't have access to the dependency so just assume + // the module is correct. + if !self.dependencies.contains_key(dependency) { + return Err(ValidationError::MissingDependency { + command: command.get_name().to_string(), + dependency: dependency.clone(), + module_ref: module_reference.clone(), + }); + } + } + } + } + + if let Some(package) = &self.package { + if let Some(entrypoint) = package.entrypoint.as_deref() { + if !commands.contains_key(entrypoint) { + return Err(ValidationError::InvalidEntrypoint { + entrypoint: entrypoint.to_string(), + available_commands: commands.keys().map(ToString::to_string).collect(), + }); + } + } + } + + Ok(()) + } + + /// add a dependency + pub fn add_dependency(&mut self, dependency_name: String, dependency_version: VersionReq) { + self.dependencies + .insert(dependency_name, dependency_version); + } + + /// remove dependency by package name + pub fn remove_dependency(&mut self, dependency_name: &str) -> Option { + self.dependencies.remove(dependency_name) + } + + /// Convert a [`Manifest`] to its TOML representation. + pub fn to_string(&self) -> anyhow::Result { + let repr = toml::to_string_pretty(&self)?; + Ok(repr) + } + + /// Write the manifest to permanent storage + pub fn save(&self, path: impl AsRef) -> anyhow::Result<()> { + let manifest = toml::to_string_pretty(self)?; + std::fs::write(path, manifest).map_err(ManifestError::CannotSaveManifest)?; + Ok(()) + } +} + +fn locate_file(path: &Path, candidates: &[&str]) -> Option { + for filename in candidates { + let path_buf = path.join(filename); + if path_buf.exists() { + return Some(filename.into()); + } + } + None +} + +impl ManifestBuilder { + pub fn new(package: Package) -> Self { + let mut builder = ManifestBuilder::default(); + builder.package(Some(package)); + builder + } + + /// Include a directory on the host in the package and make it available to + /// a WebAssembly guest at the `guest` path. + pub fn map_fs(&mut self, guest: impl Into, host: impl Into) -> &mut Self { + self.fs + .get_or_insert_with(IndexMap::new) + .insert(guest.into(), host.into()); + self + } + + /// Add a dependency to the [`Manifest`]. + pub fn with_dependency(&mut self, name: impl Into, version: VersionReq) -> &mut Self { + self.dependencies + .get_or_insert_with(HashMap::new) + .insert(name.into(), version); + self + } + + /// Add a [`Module`] to the [`Manifest`]. + pub fn with_module(&mut self, module: Module) -> &mut Self { + self.modules.get_or_insert_with(Vec::new).push(module); + self + } + + /// Add a [`Command`] to the [`Manifest`]. + pub fn with_command(&mut self, command: Command) -> &mut Self { + self.commands.get_or_insert_with(Vec::new).push(command); + self + } +} + +/// Errors that may occur while working with a [`Manifest`]. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum ManifestError { + #[error("Manifest file not found at \"{}\"", _0.display())] + MissingManifest(PathBuf), + #[error("Could not save manifest file: {0}.")] + CannotSaveManifest(#[source] std::io::Error), + #[error("Could not parse manifest because {0}.")] + TomlParseError(#[from] toml::de::Error), + #[error("There was an error validating the manifest")] + ValidationError(#[from] ValidationError), +} + +/// Errors that may be returned by [`Manifest::validate()`]. +#[derive(Debug, PartialEq, Error)] +#[non_exhaustive] +pub enum ValidationError { + #[error( + "missing ABI field on module, \"{module}\", used by command, \"{command}\"; an ABI of `wasi` or `emscripten` is required", + )] + MissingABI { command: String, module: String }, + #[error("missing module, \"{module}\", in manifest used by command, \"{command}\"")] + MissingModuleForCommand { + command: String, + module: ModuleReference, + }, + #[error("The \"{command}\" command refers to a nonexistent dependency, \"{dependency}\" in \"{module_ref}\"")] + MissingDependency { + command: String, + dependency: String, + module_ref: ModuleReference, + }, + #[error("The entrypoint, \"{entrypoint}\", isn't a valid command (commands: {})", available_commands.join(", "))] + InvalidEntrypoint { + entrypoint: String, + available_commands: Vec, + }, + #[error("Duplicate module, \"{name}\"")] + DuplicateModule { name: String }, + #[error("Duplicate command, \"{name}\"")] + DuplicateCommand { name: String }, +} + +#[cfg(test)] +mod tests { + use std::fmt::Debug; + + use serde::{de::DeserializeOwned, Deserialize}; + use toml::toml; + + use super::*; + + #[test] + fn test_to_string() { + Manifest { + package: Some(Package { + name: "package/name".to_string(), + version: Version::parse("1.0.0").unwrap(), + description: "test".to_string(), + license: None, + license_file: None, + readme: None, + repository: None, + homepage: None, + wasmer_extra_flags: None, + disable_command_rename: false, + rename_commands_to_raw_command_name: false, + entrypoint: None, + private: false, + }), + dependencies: HashMap::new(), + modules: vec![Module { + name: "test".to_string(), + abi: Abi::Wasi, + bindings: None, + interfaces: None, + kind: Some("https://webc.org/kind/wasi".to_string()), + source: Path::new("test.wasm").to_path_buf(), + }], + commands: Vec::new(), + fs: vec![ + ("a".to_string(), Path::new("/a").to_path_buf()), + ("b".to_string(), Path::new("/b").to_path_buf()), + ] + .into_iter() + .collect(), + } + .to_string() + .unwrap(); + } + + #[test] + fn interface_test() { + let manifest_str = r#" +[package] +name = "test" +version = "0.0.0" +description = "This is a test package" +license = "MIT" + +[[module]] +name = "mod" +source = "target/wasm32-wasi/release/mod.wasm" +interfaces = {"wasi" = "0.0.0-unstable"} + +[[module]] +name = "mod-with-exports" +source = "target/wasm32-wasi/release/mod-with-exports.wasm" +bindings = { wit-exports = "exports.wit", wit-bindgen = "0.0.0" } + +[[command]] +name = "command" +module = "mod" +"#; + let manifest: Manifest = Manifest::parse(manifest_str).unwrap(); + let modules = &manifest.modules; + assert_eq!( + modules[0].interfaces.as_ref().unwrap().get("wasi"), + Some(&"0.0.0-unstable".to_string()) + ); + + assert_eq!( + modules[1], + Module { + name: "mod-with-exports".to_string(), + source: PathBuf::from("target/wasm32-wasi/release/mod-with-exports.wasm"), + abi: Abi::None, + kind: None, + interfaces: None, + bindings: Some(Bindings::Wit(WitBindings { + wit_exports: PathBuf::from("exports.wit"), + wit_bindgen: "0.0.0".parse().unwrap() + })), + }, + ); + } + + #[test] + fn parse_wit_bindings() { + let table = toml! { + name = "..." + source = "..." + bindings = { wit-bindgen = "0.1.0", wit-exports = "./file.wit" } + }; + + let module = Module::deserialize(table).unwrap(); + + assert_eq!( + module.bindings.as_ref().unwrap(), + &Bindings::Wit(WitBindings { + wit_bindgen: "0.1.0".parse().unwrap(), + wit_exports: PathBuf::from("./file.wit"), + }), + ); + assert_round_trippable(&module); + } + + #[test] + fn parse_wai_bindings() { + let table = toml! { + name = "..." + source = "..." + bindings = { wai-version = "0.1.0", exports = "./file.wai", imports = ["a.wai", "../b.wai"] } + }; + + let module = Module::deserialize(table).unwrap(); + + assert_eq!( + module.bindings.as_ref().unwrap(), + &Bindings::Wai(WaiBindings { + wai_version: "0.1.0".parse().unwrap(), + exports: Some(PathBuf::from("./file.wai")), + imports: vec![PathBuf::from("a.wai"), PathBuf::from("../b.wai")], + }), + ); + assert_round_trippable(&module); + } + + #[track_caller] + fn assert_round_trippable(value: &T) + where + T: Serialize + DeserializeOwned + PartialEq + Debug, + { + let repr = toml::to_string(value).unwrap(); + let round_tripped: T = toml::from_str(&repr).unwrap(); + assert_eq!( + round_tripped, *value, + "The value should convert to/from TOML losslessly" + ); + } + + #[test] + fn imports_and_exports_are_optional_with_wai() { + let table = toml! { + name = "..." + source = "..." + bindings = { wai-version = "0.1.0" } + }; + + let module = Module::deserialize(table).unwrap(); + + assert_eq!( + module.bindings.as_ref().unwrap(), + &Bindings::Wai(WaiBindings { + wai_version: "0.1.0".parse().unwrap(), + exports: None, + imports: Vec::new(), + }), + ); + assert_round_trippable(&module); + } + + #[test] + fn ambiguous_bindings_table() { + let table = toml! { + wai-version = "0.2.0" + wit-bindgen = "0.1.0" + }; + + let err = Bindings::deserialize(table).unwrap_err(); + + assert_eq!( + err.to_string(), + "expected one of \"wit-bindgen\" or \"wai-version\" to be provided, but not both\n" + ); + } + + #[test] + fn bindings_table_that_is_neither_wit_nor_wai() { + let table = toml! { + wai-bindgen = "lol, this should have been wai-version" + exports = "./file.wai" + }; + + let err = Bindings::deserialize(table).unwrap_err(); + + assert_eq!( + err.to_string(), + "expected one of \"wit-bindgen\" or \"wai-version\" to be provided, but not both\n" + ); + } + + #[test] + fn command_v2_isnt_ambiguous_with_command_v1() { + let src = r#" +[package] +name = "hotg-ai/sine" +version = "0.12.0" +description = "sine" + +[dependencies] +"hotg-ai/train_test_split" = "0.12.1" +"hotg-ai/elastic_net" = "0.12.1" + +[[module]] # This is the same as atoms +name = "sine" +kind = "tensorflow-SavedModel" # It can also be "wasm" (default) +source = "models/sine" + +[[command]] +name = "run" +runner = "rune" +module = "sine" +annotations = { file = "Runefile.yml", kind = "yaml" } +"#; + + let manifest: Manifest = toml::from_str(src).unwrap(); + + let commands = &manifest.commands; + assert_eq!(commands.len(), 1); + assert_eq!( + commands[0], + Command::V2(CommandV2 { + name: "run".into(), + module: "sine".parse().unwrap(), + runner: "rune".into(), + annotations: Some(CommandAnnotations::File(FileCommandAnnotations { + file: "Runefile.yml".into(), + kind: FileKind::Yaml, + })) + }) + ); + } + + #[test] + fn get_manifest() { + let wasmer_toml = toml! { + [package] + name = "test" + version = "1.0.0" + repository = "test.git" + homepage = "test.com" + description = "The best package." + }; + let manifest: Manifest = wasmer_toml.try_into().unwrap(); + if let Some(package) = manifest.package { + assert!(!package.disable_command_rename); + } + } + + #[test] + fn parse_manifest_without_package_section() { + let wasmer_toml = toml! { + [[module]] + name = "test-module" + source = "data.wasm" + abi = "wasi" + }; + let manifest: Manifest = wasmer_toml.try_into().unwrap(); + assert!(manifest.package.is_none()); + } + + #[test] + fn get_commands() { + let wasmer_toml = toml! { + [package] + name = "test" + version = "1.0.0" + repository = "test.git" + homepage = "test.com" + description = "The best package." + [[module]] + name = "test-pkg" + module = "target.wasm" + source = "source.wasm" + description = "description" + interfaces = {"wasi" = "0.0.0-unstable"} + [[command]] + name = "foo" + module = "test" + [[command]] + name = "baz" + module = "test" + main_args = "$@" + }; + let manifest: Manifest = wasmer_toml.try_into().unwrap(); + let commands = &manifest.commands; + assert_eq!(2, commands.len()); + } + + #[test] + fn add_new_dependency() { + let tmp_dir = tempfile::tempdir().unwrap(); + let tmp_dir_path: &std::path::Path = tmp_dir.as_ref(); + let manifest_path = tmp_dir_path.join(MANIFEST_FILE_NAME); + let wasmer_toml = toml! { + [package] + name = "_/test" + version = "1.0.0" + description = "description" + [[module]] + name = "test" + source = "test.wasm" + interfaces = {} + }; + let toml_string = toml::to_string(&wasmer_toml).unwrap(); + std::fs::write(manifest_path, toml_string).unwrap(); + let mut manifest = Manifest::find_in_directory(tmp_dir).unwrap(); + + let dependency_name = "dep_pkg"; + let dependency_version: VersionReq = "0.1.0".parse().unwrap(); + + manifest.add_dependency(dependency_name.to_string(), dependency_version.clone()); + assert_eq!(1, manifest.dependencies.len()); + + // adding the same dependency twice changes nothing + manifest.add_dependency(dependency_name.to_string(), dependency_version); + assert_eq!(1, manifest.dependencies.len()); + + // adding a second different dependency will increase the count + let dependency_name_2 = "dep_pkg_2"; + let dependency_version_2: VersionReq = "0.2.0".parse().unwrap(); + manifest.add_dependency(dependency_name_2.to_string(), dependency_version_2); + assert_eq!(2, manifest.dependencies.len()); + } + + #[test] + fn duplicate_modules_are_invalid() { + let wasmer_toml = toml! { + [package] + name = "some/package" + version = "0.0.0" + description = "" + [[module]] + name = "test" + source = "test.wasm" + [[module]] + name = "test" + source = "test.wasm" + }; + let manifest = Manifest::deserialize(wasmer_toml).unwrap(); + + let error = manifest.validate().unwrap_err(); + + assert_eq!( + error, + ValidationError::DuplicateModule { + name: "test".to_string() + } + ); + } + + #[test] + fn duplicate_commands_are_invalid() { + let wasmer_toml = toml! { + [package] + name = "some/package" + version = "0.0.0" + description = "" + [[module]] + name = "test" + source = "test.wasm" + abi = "wasi" + [[command]] + name = "cmd" + module = "test" + [[command]] + name = "cmd" + module = "test" + }; + let manifest = Manifest::deserialize(wasmer_toml).unwrap(); + + let error = manifest.validate().unwrap_err(); + + assert_eq!( + error, + ValidationError::DuplicateCommand { + name: "cmd".to_string() + } + ); + } + + #[test] + fn nonexistent_entrypoint() { + let wasmer_toml = toml! { + [package] + name = "some/package" + version = "0.0.0" + description = "" + entrypoint = "this-doesnt-exist" + [[module]] + name = "test" + source = "test.wasm" + abi = "wasi" + [[command]] + name = "cmd" + module = "test" + }; + let manifest = Manifest::deserialize(wasmer_toml).unwrap(); + + let error = manifest.validate().unwrap_err(); + + assert_eq!( + error, + ValidationError::InvalidEntrypoint { + entrypoint: "this-doesnt-exist".to_string(), + available_commands: vec!["cmd".to_string()] + } + ); + } + + #[test] + fn command_with_nonexistent_module() { + let wasmer_toml = toml! { + [package] + name = "some/package" + version = "0.0.0" + description = "" + [[command]] + name = "cmd" + module = "this-doesnt-exist" + }; + let manifest = Manifest::deserialize(wasmer_toml).unwrap(); + + let error = manifest.validate().unwrap_err(); + + assert_eq!( + error, + ValidationError::MissingModuleForCommand { + command: "cmd".to_string(), + module: "this-doesnt-exist".parse().unwrap() + } + ); + } + + #[test] + fn use_builder_api_to_create_simplest_manifest() { + let package = + Package::builder("my/package", "1.0.0".parse().unwrap(), "My awesome package") + .build() + .unwrap(); + let manifest = Manifest::builder(package).build().unwrap(); + + manifest.validate().unwrap(); + } + + #[test] + fn deserialize_command_referring_to_module_from_dependency() { + let wasmer_toml = toml! { + [package] + name = "some/package" + version = "0.0.0" + description = "" + + [dependencies] + dep = "1.2.3" + + [[command]] + name = "cmd" + module = "dep:module" + }; + let manifest = Manifest::deserialize(wasmer_toml).unwrap(); + + let command = manifest + .commands + .iter() + .find(|cmd| cmd.get_name() == "cmd") + .unwrap(); + + assert_eq!( + command.get_module(), + &ModuleReference::Dependency { + dependency: "dep".to_string(), + module: "module".to_string() + } + ); + } + + #[test] + fn command_with_module_from_nonexistent_dependency() { + let wasmer_toml = toml! { + [package] + name = "some/package" + version = "0.0.0" + description = "" + [[command]] + name = "cmd" + module = "dep:module" + }; + let manifest = Manifest::deserialize(wasmer_toml).unwrap(); + + let error = manifest.validate().unwrap_err(); + + assert_eq!( + error, + ValidationError::MissingDependency { + command: "cmd".to_string(), + dependency: "dep".to_string(), + module_ref: ModuleReference::Dependency { + dependency: "dep".to_string(), + module: "module".to_string() + } + } + ); + } + + #[test] + fn round_trip_dependency_module_ref() { + let original = ModuleReference::Dependency { + dependency: "my/dep".to_string(), + module: "module".to_string(), + }; + + let repr = original.to_string(); + let round_tripped: ModuleReference = repr.parse().unwrap(); + + assert_eq!(round_tripped, original); + } +} diff --git a/lib/config/src/package/named_package_ident.rs b/lib/config/src/package/named_package_ident.rs new file mode 100644 index 00000000000..1f8384a8c82 --- /dev/null +++ b/lib/config/src/package/named_package_ident.rs @@ -0,0 +1,412 @@ +use std::{fmt::Write, str::FromStr}; + +use semver::VersionReq; + +use super::{NamedPackageId, PackageParseError}; + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub enum Tag { + Named(String), + VersionReq(semver::VersionReq), +} + +impl std::fmt::Display for Tag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Tag::Named(n) => n.fmt(f), + Tag::VersionReq(v) => v.fmt(f), + } + } +} + +impl std::str::FromStr for Tag { + type Err = PackageParseError; + + fn from_str(s: &str) -> Result { + if s == "latest" { + Ok(Self::VersionReq(semver::VersionReq::STAR)) + } else { + match semver::VersionReq::from_str(s) { + Ok(v) => Ok(Self::VersionReq(v)), + Err(_) => Ok(Self::Named(s.to_string())), + } + } + } +} + +/// Parsed representation of a package identifier. +/// +/// Format: +/// [https?:///][namespace/]name[@version] +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub struct NamedPackageIdent { + pub registry: Option, + pub namespace: Option, + pub name: String, + pub tag: Option, +} + +impl NamedPackageIdent { + pub fn try_from_full_name_and_version( + full_name: &str, + version: &str, + ) -> Result { + let (namespace, name) = match full_name.split_once('/') { + Some((ns, name)) => (Some(ns.to_owned()), name.to_owned()), + None => (None, full_name.to_owned()), + }; + + let version = version + .parse::() + .map_err(|e| PackageParseError::new(version, e.to_string()))?; + + Ok(Self { + registry: None, + namespace, + name, + tag: Some(Tag::VersionReq(version)), + }) + } + + pub fn tag_str(&self) -> Option { + self.tag.as_ref().map(|x| x.to_string()) + } + + /// Namespaced name. + /// + /// Eg: "namespace/name" + pub fn full_name(&self) -> String { + if let Some(ns) = &self.namespace { + format!("{}/{}", ns, self.name) + } else { + self.name.clone() + } + } + + pub fn version_opt(&self) -> Option<&VersionReq> { + match &self.tag { + Some(Tag::VersionReq(v)) => Some(v), + Some(Tag::Named(_)) | None => None, + } + } + + pub fn version_or_default(&self) -> VersionReq { + match &self.tag { + Some(Tag::VersionReq(v)) => v.clone(), + Some(Tag::Named(_)) | None => semver::VersionReq::STAR, + } + } + + /// Build the ident for a package. + /// + /// Format: [NAMESPACE/]NAME[@tag] + pub fn build_identifier(&self) -> String { + let mut ident = if let Some(ns) = &self.namespace { + format!("{}/{}", ns, self.name) + } else { + self.name.to_string() + }; + + if let Some(tag) = &self.tag { + ident.push('@'); + // Writing to a string only fails on memory allocation errors. + write!(&mut ident, "{}", tag).unwrap(); + } + ident + } + + pub fn build(&self) -> String { + let mut out = String::new(); + if let Some(url) = &self.registry { + // NOTE: writing to a String can only fail on allocation errors. + write!(&mut out, "{}", url).unwrap(); + + if !out.ends_with('/') { + out.push(':'); + } + } + if let Some(ns) = &self.namespace { + out.push_str(ns); + out.push('/'); + } + out.push_str(&self.name); + if let Some(tag) = &self.tag { + out.push('@'); + // Writing to a string only fails on memory allocation errors. + write!(&mut out, "{}", tag).unwrap(); + } + + out + } +} + +impl From for NamedPackageIdent { + fn from(value: NamedPackageId) -> Self { + let (namespace, name) = match value.full_name.split_once('/') { + Some((ns, name)) => (Some(ns.to_owned()), name.to_owned()), + None => (None, value.full_name), + }; + + Self { + registry: None, + namespace, + name, + tag: Some(Tag::VersionReq(semver::VersionReq { + comparators: vec![semver::Comparator { + op: semver::Op::Exact, + major: value.version.major, + minor: Some(value.version.minor), + patch: Some(value.version.patch), + pre: value.version.pre, + }], + })), + } + } +} + +impl std::str::FromStr for NamedPackageIdent { + type Err = PackageParseError; + + fn from_str(value: &str) -> Result { + let (rest, tag_opt) = value + .trim() + .rsplit_once('@') + .map(|(x, y)| (x, if y.is_empty() { None } else { Some(y) })) + .unwrap_or((value, None)); + + let tag = if let Some(v) = tag_opt.filter(|x| !x.is_empty()) { + Some(Tag::from_str(v)?) + } else { + None + }; + + let (rest, name) = if let Some((r, n)) = rest.rsplit_once('/') { + (r, n) + } else { + ("", rest) + }; + + let name = name.trim(); + if name.is_empty() { + return Err(PackageParseError::new(value, "package name is required")); + } + + let (rest, namespace) = if rest.is_empty() { + ("", None) + } else { + let (rest, ns) = rest.rsplit_once(':').unwrap_or(("", rest)); + + let ns = ns.trim(); + + if ns.is_empty() { + return Err(PackageParseError::new(value, "namespace can not be empty")); + } + (rest, Some(ns.to_string())) + }; + + let rest = rest.trim(); + let registry = if rest.is_empty() { + None + } else { + Some(rest.to_string()) + }; + + Ok(Self { + registry, + namespace, + name: name.to_string(), + tag, + }) + } +} + +impl std::fmt::Display for NamedPackageIdent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.build()) + } +} + +impl serde::Serialize for NamedPackageIdent { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + self.to_string().serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for NamedPackageIdent { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(serde::de::Error::custom) + } +} + +impl schemars::JsonSchema for NamedPackageIdent { + fn schema_name() -> String { + "NamedPackageIdent".to_string() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + String::json_schema(gen) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::package::PackageParseError; + + use super::*; + + #[test] + fn test_parse_webc_ident() { + // Success cases. + + assert_eq!( + NamedPackageIdent::from_str("ns/name").unwrap(), + NamedPackageIdent { + registry: None, + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: None, + } + ); + + assert_eq!( + NamedPackageIdent::from_str("ns/name@").unwrap(), + NamedPackageIdent { + registry: None, + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: None, + }, + "empty tag should be parsed as None" + ); + + assert_eq!( + NamedPackageIdent::from_str("ns/name@tag").unwrap(), + NamedPackageIdent { + registry: None, + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: Some(Tag::Named("tag".to_string())), + } + ); + + assert_eq!( + NamedPackageIdent::from_str("reg.com:ns/name").unwrap(), + NamedPackageIdent { + registry: Some("reg.com".to_string()), + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: None, + } + ); + + assert_eq!( + NamedPackageIdent::from_str("reg.com:ns/name@tag").unwrap(), + NamedPackageIdent { + registry: Some("reg.com".to_string()), + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: Some(Tag::Named("tag".to_string())), + } + ); + + assert_eq!( + NamedPackageIdent::from_str("reg.com:ns/name").unwrap(), + NamedPackageIdent { + registry: Some("reg.com".to_string()), + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: None, + } + ); + + assert_eq!( + NamedPackageIdent::from_str("reg.com:ns/name@tag").unwrap(), + NamedPackageIdent { + registry: Some("reg.com".to_string()), + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: Some(Tag::Named("tag".to_string())), + } + ); + + assert_eq!( + NamedPackageIdent::from_str("reg.com:ns/name").unwrap(), + NamedPackageIdent { + registry: Some("reg.com".to_string()), + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: None, + } + ); + + assert_eq!( + NamedPackageIdent::from_str("reg.com:ns/name@tag").unwrap(), + NamedPackageIdent { + registry: Some("reg.com".to_string()), + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: Some(Tag::Named("tag".to_string())), + } + ); + + // Failure cases. + + assert_eq!( + NamedPackageIdent::from_str("alpha").unwrap(), + NamedPackageIdent { + registry: None, + namespace: None, + name: "alpha".to_string(), + tag: None, + }, + ); + + assert_eq!( + NamedPackageIdent::from_str(""), + Err(PackageParseError::new("", "package name is required")) + ); + } + + #[test] + fn test_serde_serialize_package_ident_with_repo() { + // Serialize + let ident = NamedPackageIdent { + registry: Some("wapm.io".to_string()), + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: None, + }; + + let raw = serde_json::to_string(&ident).unwrap(); + assert_eq!(raw, "\"wapm.io:ns/name\""); + + let ident2 = serde_json::from_str::(&raw).unwrap(); + assert_eq!(ident, ident2); + } + + #[test] + fn test_serde_serialize_webc_str_ident_without_repo() { + // Serialize + let ident = NamedPackageIdent { + registry: None, + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: None, + }; + + let raw = serde_json::to_string(&ident).unwrap(); + assert_eq!(raw, "\"ns/name\""); + + let ident2 = serde_json::from_str::(&raw).unwrap(); + assert_eq!(ident, ident2); + } +} diff --git a/lib/config/src/package/package_hash.rs b/lib/config/src/package/package_hash.rs new file mode 100644 index 00000000000..d14da04ddef --- /dev/null +++ b/lib/config/src/package/package_hash.rs @@ -0,0 +1,121 @@ +use crate::{hash::Sha256Hash, package::PackageParseError}; + +/// Hash for a package. +/// +/// Currently only supports the format: `sha256:`. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PackageHash(Sha256Hash); + +impl PackageHash { + const STR_PREFIX: &'static str = "sha256:"; + + pub fn as_sha256(&self) -> Option<&Sha256Hash> { + Some(&self.0) + } + + pub fn from_sha256_bytes(bytes: [u8; 32]) -> Self { + Self(Sha256Hash(bytes)) + } +} + +impl From for PackageHash { + fn from(value: Sha256Hash) -> Self { + Self(value) + } +} + +impl std::fmt::Display for PackageHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "sha256:{}", self.0) + } +} + +impl std::str::FromStr for PackageHash { + type Err = PackageParseError; + + fn from_str(s: &str) -> Result { + if !s.starts_with(Self::STR_PREFIX) { + return Err(PackageParseError::new( + s, + "package hashes must start with 'sha256:'", + )); + } + let hash = Sha256Hash::from_str(&s[Self::STR_PREFIX.len()..]) + .map_err(|e| PackageParseError::new(s, e.to_string()))?; + + Ok(PackageHash(hash)) + } +} + +impl serde::Serialize for PackageHash { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> serde::Deserialize<'de> for PackageHash { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse::() + .map_err(|e| serde::de::Error::custom(e.to_string())) + } +} + +impl schemars::JsonSchema for PackageHash { + fn schema_name() -> String { + "PackageHash".to_string() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + String::json_schema(gen) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_package_hash_roundtrip() { + let input = "sha256:c355cd53795b9b481f7eb2b5f4f6c8cf73631bdc343723a579d671e32db70b3c"; + let h1 = input + .parse::() + .expect("string should parse to hash"); + + assert_eq!( + h1.as_sha256().unwrap().as_bytes(), + &[ + 195, 85, 205, 83, 121, 91, 155, 72, 31, 126, 178, 181, 244, 246, 200, 207, 115, 99, + 27, 220, 52, 55, 35, 165, 121, 214, 113, 227, 45, 183, 11, 60 + ], + ); + + assert_eq!(h1.to_string(), input); + } + + #[test] + fn package_hash_serde_roundtrip() { + let input = "sha256:c355cd53795b9b481f7eb2b5f4f6c8cf73631bdc343723a579d671e32db70b3c"; + let h1 = input + .parse::() + .expect("string should parse to hash"); + + // Test serialization. + assert_eq!( + serde_json::to_value(&h1).unwrap(), + serde_json::Value::String(input.to_owned()), + ); + + // Test deserialize. + let v = serde_json::to_string(&h1).unwrap(); + let h2 = serde_json::from_str::(&v).unwrap(); + + assert_eq!(h1, h2); + } +} diff --git a/lib/config/src/package/package_id.rs b/lib/config/src/package/package_id.rs new file mode 100644 index 00000000000..0f91fc369fa --- /dev/null +++ b/lib/config/src/package/package_id.rs @@ -0,0 +1,123 @@ +use super::PackageHash; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct NamedPackageId { + pub full_name: String, + pub version: semver::Version, +} + +impl NamedPackageId { + pub fn try_new( + name: impl Into, + version: impl AsRef, + ) -> Result { + Ok(Self { + full_name: name.into(), + version: version.as_ref().parse()?, + }) + } +} + +impl std::fmt::Display for NamedPackageId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}@{}", self.full_name, self.version) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PackageId { + Hash(PackageHash), + Named(NamedPackageId), +} + +impl PackageId { + pub fn new_named(name: impl Into, version: semver::Version) -> Self { + Self::Named(NamedPackageId { + full_name: name.into(), + version, + }) + } + + pub fn as_named(&self) -> Option<&NamedPackageId> { + if let Self::Named(v) = self { + Some(v) + } else { + None + } + } + + pub fn as_hash(&self) -> Option<&PackageHash> { + if let Self::Hash(v) = self { + Some(v) + } else { + None + } + } +} + +impl From for PackageId { + fn from(value: NamedPackageId) -> Self { + Self::Named(value) + } +} + +impl From for PackageId { + fn from(value: PackageHash) -> Self { + Self::Hash(value) + } +} + +// impl std::str::FromStr for PackageId { +// type Err = PackageParseError; +// +// fn from_str(s: &str) -> Result { +// if let Ok(hash) = PackageHash::from_str(s) { +// Ok(Self::Hash(hash)) +// } else if let Ok(named) = NamedPackageId::from_str(s) { +// Ok(Self::Named(named)) +// } else { +// Err(PackageParseError::new( +// s, +// "invalid package ident: expected a hash or a valid named package identifier", +// )) +// } +// } +// } + +impl std::fmt::Display for PackageId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Named(n) => n.fmt(f), + Self::Hash(h) => h.fmt(f), + } + } +} + +// impl serde::Serialize for PackageId { +// fn serialize(&self, serializer: S) -> Result +// where +// S: serde::ser::Serializer, +// { +// self.to_string().serialize(serializer) +// } +// } +// +// impl<'de> serde::Deserialize<'de> for PackageId { +// fn deserialize(deserializer: D) -> Result +// where +// D: serde::de::Deserializer<'de>, +// { +// let s = String::deserialize(deserializer)?; +// Self::from_str(&s).map_err(serde::de::Error::custom) +// } +// } + +impl schemars::JsonSchema for PackageId { + fn schema_name() -> String { + "PackageIdent".to_string() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + String::json_schema(gen) + } +} diff --git a/lib/config/src/package/package_ident.rs b/lib/config/src/package/package_ident.rs new file mode 100644 index 00000000000..5b03d6ef812 --- /dev/null +++ b/lib/config/src/package/package_ident.rs @@ -0,0 +1,94 @@ +use std::str::FromStr; + +use super::{NamedPackageIdent, PackageHash, PackageParseError}; + +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub enum PackageIdent { + Named(NamedPackageIdent), + Hash(PackageHash), +} + +impl PackageIdent { + pub fn as_named(&self) -> Option<&NamedPackageIdent> { + if let Self::Named(v) = self { + Some(v) + } else { + None + } + } + + pub fn as_hash(&self) -> Option<&PackageHash> { + if let Self::Hash(v) = self { + Some(v) + } else { + None + } + } +} + +impl From for PackageIdent { + fn from(value: NamedPackageIdent) -> Self { + Self::Named(value) + } +} + +impl From for PackageIdent { + fn from(value: PackageHash) -> Self { + Self::Hash(value) + } +} + +impl std::str::FromStr for PackageIdent { + type Err = PackageParseError; + + fn from_str(s: &str) -> Result { + if let Ok(hash) = PackageHash::from_str(s) { + Ok(Self::Hash(hash)) + } else if let Ok(named) = NamedPackageIdent::from_str(s) { + Ok(Self::Named(named)) + } else { + Err(PackageParseError::new( + s, + "invalid package ident: expected a hash or a valid named package identifier", + )) + } + } +} + +impl std::fmt::Display for PackageIdent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Named(n) => n.fmt(f), + Self::Hash(h) => h.fmt(f), + } + } +} + +impl serde::Serialize for PackageIdent { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + self.to_string().serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for PackageIdent { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(serde::de::Error::custom) + } +} + +impl schemars::JsonSchema for PackageIdent { + fn schema_name() -> String { + "PackageIdent".to_string() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + String::json_schema(gen) + } +} diff --git a/lib/config/src/package/package_source.rs b/lib/config/src/package/package_source.rs new file mode 100644 index 00000000000..ed286773dc4 --- /dev/null +++ b/lib/config/src/package/package_source.rs @@ -0,0 +1,367 @@ +use std::str::FromStr; + +use super::{ + NamedPackageId, NamedPackageIdent, PackageHash, PackageId, PackageIdent, PackageParseError, +}; + +/// Source location of a package. +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum PackageSource { + /// An identifier in the format prescribed by [`WebcIdent`]. + Ident(PackageIdent), + /// An absolute or relative (dot-leading) path. + Path(String), + Url(url::Url), +} + +impl PackageSource { + pub fn as_ident(&self) -> Option<&PackageIdent> { + if let Self::Ident(v) = self { + Some(v) + } else { + None + } + } + + pub fn as_hash(&self) -> Option<&PackageHash> { + self.as_ident().and_then(|x| x.as_hash()) + } + + pub fn as_named(&self) -> Option<&NamedPackageIdent> { + self.as_ident().and_then(|x| x.as_named()) + } + + pub fn as_path(&self) -> Option<&String> { + if let Self::Path(v) = self { + Some(v) + } else { + None + } + } + + pub fn as_url(&self) -> Option<&url::Url> { + if let Self::Url(v) = self { + Some(v) + } else { + None + } + } +} + +impl From for PackageSource { + fn from(id: PackageIdent) -> Self { + Self::Ident(id) + } +} + +impl From for PackageSource { + fn from(value: NamedPackageIdent) -> Self { + Self::Ident(PackageIdent::Named(value)) + } +} + +impl From for PackageSource { + fn from(value: NamedPackageId) -> Self { + Self::Ident(PackageIdent::Named(NamedPackageIdent::from(value))) + } +} + +impl From for PackageSource { + fn from(value: PackageHash) -> Self { + Self::Ident(PackageIdent::Hash(value)) + } +} + +impl From for PackageSource { + fn from(value: PackageId) -> Self { + match value { + PackageId::Hash(hash) => Self::from(hash), + PackageId::Named(named) => Self::Ident(PackageIdent::Named(named.into())), + } + } +} + +impl std::fmt::Display for PackageSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Ident(id) => id.fmt(f), + Self::Path(path) => path.fmt(f), + Self::Url(url) => url.fmt(f), + } + } +} + +impl std::str::FromStr for PackageSource { + type Err = PackageParseError; + + fn from_str(value: &str) -> Result { + let Some(first_char) = value.chars().next() else { + return Err(PackageParseError::new( + value, + "An empty string is not a valid package source", + )); + }; + + if value.contains("://") { + let url = value + .parse::() + .map_err(|e| PackageParseError::new(value, e.to_string()))?; + return Ok(Self::Url(url)); + } + + match first_char { + '.' | '/' => Ok(Self::Path(value.to_string())), + _ => PackageIdent::from_str(value).map(Self::Ident), + } + } +} + +impl serde::Serialize for PackageSource { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Ident(id) => id.serialize(serializer), + Self::Path(path) => path.serialize(serializer), + Self::Url(url) => url.serialize(serializer), + } + } +} + +impl<'de> serde::Deserialize<'de> for PackageSource { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + PackageSource::from_str(&s).map_err(|e| serde::de::Error::custom(e.to_string())) + } +} + +impl schemars::JsonSchema for PackageSource { + fn schema_name() -> String { + "PackageSource".to_string() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + String::json_schema(gen) + } +} + +#[cfg(test)] +mod tests { + use crate::package::Tag; + + use super::*; + + #[test] + fn test_parse_package_specifier() { + // Parse as WebcIdent + assert_eq!( + PackageSource::from_str("ns/name").unwrap(), + PackageSource::from(NamedPackageIdent { + registry: None, + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: None, + }) + ); + + assert_eq!( + PackageSource::from_str("ns/name@").unwrap(), + PackageSource::from(NamedPackageIdent { + registry: None, + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: None, + }), + "empty tag should be parsed as None" + ); + + assert_eq!( + PackageSource::from_str("ns/name@tag").unwrap(), + PackageSource::from(NamedPackageIdent { + registry: None, + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: Some(Tag::Named("tag".to_string())), + }) + ); + + assert_eq!( + PackageSource::from_str("reg.com:ns/name").unwrap(), + PackageSource::from(NamedPackageIdent { + registry: Some("reg.com".to_string()), + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: None, + }) + ); + + assert_eq!( + PackageSource::from_str("reg.com:ns/name@tag").unwrap(), + PackageSource::from(NamedPackageIdent { + registry: Some("reg.com".to_string()), + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: Some(Tag::Named("tag".to_string())), + }) + ); + + assert_eq!( + PackageSource::from_str("reg.com:ns/name").unwrap(), + PackageSource::from(NamedPackageIdent { + registry: Some("reg.com".to_string()), + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: None, + }) + ); + + assert_eq!( + PackageSource::from_str("reg.com:ns/name@tag").unwrap(), + PackageSource::from(NamedPackageIdent { + registry: Some("reg.com".to_string()), + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: Some(Tag::Named("tag".to_string())), + }) + ); + + assert_eq!( + PackageSource::from_str("reg.com:ns/name").unwrap(), + PackageSource::from(NamedPackageIdent { + registry: Some("reg.com".to_string()), + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: None, + }) + ); + + assert_eq!( + PackageSource::from_str("reg.com:ns/name@tag").unwrap(), + PackageSource::from(NamedPackageIdent { + registry: Some("reg.com".to_string()), + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: Some(Tag::Named("tag".to_string())), + }) + ); + + // Failure cases. + assert_eq!( + PackageSource::from_str("alpha"), + Ok(PackageSource::from(NamedPackageIdent { + registry: None, + namespace: None, + name: "alpha".to_string(), + tag: None, + })) + ); + + assert_eq!( + PackageSource::from_str(""), + Err(PackageParseError::new( + "", + "An empty string is not a valid package source" + )) + ); + assert_eq!( + PackageSource::from_str("ns/name").unwrap(), + PackageSource::from(NamedPackageIdent { + registry: None, + namespace: Some("ns".to_string()), + name: "name".to_string(), + tag: None, + }) + ); + + assert_eq!( + PackageSource::from_str( + "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03" + ) + .unwrap(), + PackageSource::from( + PackageHash::from_str( + "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03" + ) + .unwrap() + ) + ); + + let wants = vec![ + "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03", + "./dir", + "ns/name", + "ns/name@", + "ns/name@tag", + ]; + for want in wants { + let spec = PackageSource::from_str(want).unwrap(); + assert_eq!(spec, PackageSource::from_str(&spec.to_string()).unwrap()); + } + } + + #[test] + fn parse_package_sources() { + let inputs = [ + ( + "first", + PackageSource::from(NamedPackageIdent { + registry: None, + namespace: None, + name: "first".to_string(), + tag: None, + }), + ), + ( + "namespace/package", + PackageSource::from(NamedPackageIdent { + registry: None, + namespace: Some("namespace".to_string()), + name: "package".to_string(), + tag: None, + }), + ), + ( + "namespace/package@1.0.0", + PackageSource::from(NamedPackageIdent { + registry: None, + namespace: Some("namespace".to_string()), + name: "package".to_string(), + tag: Some(Tag::VersionReq("1.0.0".parse().unwrap())), + }), + ), + ( + "namespace/package@latest", + PackageSource::from(NamedPackageIdent { + registry: None, + namespace: Some("namespace".to_string()), + name: "package".to_string(), + tag: Some(Tag::VersionReq(semver::VersionReq::STAR)), + }), + ), + ( + "https://wapm/io/namespace/package@1.0.0", + PackageSource::Url("https://wapm/io/namespace/package@1.0.0".parse().unwrap()), + ), + ( + "/path/to/some/file.webc", + PackageSource::Path("/path/to/some/file.webc".into()), + ), + ("./file.webc", PackageSource::Path("./file.webc".into())), + #[cfg(windows)] + ( + r"C:\Path\to\some\file.webc", + PackageSource::Path(r"C:\Path\to\some\file.webc".into()), + ), + ]; + + for (index, (src, expected)) in inputs.into_iter().enumerate() { + eprintln!("testing pattern {}", index + 1); + let parsed = PackageSource::from_str(src).unwrap(); + assert_eq!(parsed, expected); + } + } +} From 8f0aee08f0cb70ed7179908162f761bfe4454022 Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Wed, 17 Apr 2024 14:22:48 +0200 Subject: [PATCH 2/4] feat(wasmer-config): Add HEADER_APP_VERSION_ID constant --- lib/config/src/app/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/config/src/app/mod.rs b/lib/config/src/app/mod.rs index 8934f7da516..49db3f41eff 100644 --- a/lib/config/src/app/mod.rs +++ b/lib/config/src/app/mod.rs @@ -11,6 +11,14 @@ use bytesize::ByteSize; use crate::package::PackageSource; +/// Header added to Edge app HTTP responses. +/// The value contains the app version ID that generated the response. +/// +// This is used by the CLI to determine when a new version was successfully +// released. +#[allow(clippy::declare_interior_mutable_const)] +pub const HEADER_APP_VERSION_ID: &str = "x-edge-app-version-id"; + /// User-facing app.yaml config file for apps. /// /// NOTE: only used by the backend, Edge itself does not use this format, and From 263035503c722036aefdd84a94ad12ef1422bbd9 Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Wed, 17 Apr 2024 15:37:41 +0200 Subject: [PATCH 3/4] deps: Lift indexmap to be a workspace dependency --- Cargo.lock | 2 +- Cargo.toml | 1 + lib/config/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66453461c51..2f9fe967b90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6529,7 +6529,7 @@ dependencies = [ "bytesize", "derive_builder", "hex", - "indexmap 2.2.6", + "indexmap 1.9.3", "pretty_assertions", "schemars", "semver 1.0.22", diff --git a/Cargo.toml b/Cargo.toml index 235b9ee9c66..7b1ed5c7d67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,6 +95,7 @@ shared-buffer = "0.1.4" rkyv = { version = "0.7.40", features = ["indexmap", "validation", "strict"] } memmap2 = { version = "0.6.2" } edge-schema = { version = "=0.1.0" } +indexmap = "1" [build-dependencies] test-generator = { path = "tests/lib/test-generator" } diff --git a/lib/config/Cargo.toml b/lib/config/Cargo.toml index 0e1ed36d6f3..0e7790f1515 100644 --- a/lib/config/Cargo.toml +++ b/lib/config/Cargo.toml @@ -21,7 +21,7 @@ semver = { version = "1", features = ["serde"] } serde_json = "1" serde_yaml = "0.9.0" serde_cbor = "0.11.2" -indexmap = { version = "2", features = ["serde"] } +indexmap = { workspace = true, features = ["serde"] } derive_builder = "0.12.0" bytesize = { version = "1.3.0", features = ["serde"] } schemars = { version = "0.8.16", features = ["url"] } From f5b182fce677570abf4af9b982969d29db7777e5 Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Wed, 17 Apr 2024 15:37:57 +0200 Subject: [PATCH 4/4] chore: Remove dead code --- lib/config/src/package/package_id.rs | 36 ---------------------------- 1 file changed, 36 deletions(-) diff --git a/lib/config/src/package/package_id.rs b/lib/config/src/package/package_id.rs index 0f91fc369fa..2a292a6774f 100644 --- a/lib/config/src/package/package_id.rs +++ b/lib/config/src/package/package_id.rs @@ -67,23 +67,6 @@ impl From for PackageId { } } -// impl std::str::FromStr for PackageId { -// type Err = PackageParseError; -// -// fn from_str(s: &str) -> Result { -// if let Ok(hash) = PackageHash::from_str(s) { -// Ok(Self::Hash(hash)) -// } else if let Ok(named) = NamedPackageId::from_str(s) { -// Ok(Self::Named(named)) -// } else { -// Err(PackageParseError::new( -// s, -// "invalid package ident: expected a hash or a valid named package identifier", -// )) -// } -// } -// } - impl std::fmt::Display for PackageId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -93,25 +76,6 @@ impl std::fmt::Display for PackageId { } } -// impl serde::Serialize for PackageId { -// fn serialize(&self, serializer: S) -> Result -// where -// S: serde::ser::Serializer, -// { -// self.to_string().serialize(serializer) -// } -// } -// -// impl<'de> serde::Deserialize<'de> for PackageId { -// fn deserialize(deserializer: D) -> Result -// where -// D: serde::de::Deserializer<'de>, -// { -// let s = String::deserialize(deserializer)?; -// Self::from_str(&s).map_err(serde::de::Error::custom) -// } -// } - impl schemars::JsonSchema for PackageId { fn schema_name() -> String { "PackageIdent".to_string()