├── .github ├── ISSUE_TEMPLATE │ ├── ---bug-report.md │ ├── ---feature-request.md │ └── --question.md ├── cross-linux-aarch64 │ └── Dockerfile ├── pull_request_template.md └── workflows │ └── main.yaml ├── .gitignore ├── .graphqlconfig.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── assets ├── largewasmfile.wapm.toml ├── largewasmfile.wasm └── logo.png ├── end-to-end-tests ├── chunked_upload.sh ├── chunked_upload.txt ├── ci │ ├── README.md │ ├── chunked-upload.sh │ ├── direct-execution.sh │ ├── init-and-add.sh │ ├── install.sh │ ├── manifest-validation.sh │ ├── package-fs-mapping.sh │ ├── validate-global.sh │ └── verification.sh ├── direct_execute.sh ├── direct_execute.txt ├── init-and-add.sh ├── init-and-add.txt ├── install.sh ├── install.txt ├── manifest-validation.sh ├── manifest-validation.txt ├── package-fs-mapping.sh ├── package-fs-mapping.txt ├── validate-global.sh ├── validate-global.txt ├── verification.sh └── verification.txt ├── graphql ├── queries │ ├── get_bindings.graphql │ ├── get_interface_version.graphql │ ├── get_package.graphql │ ├── get_package_by_command.graphql │ ├── get_package_version.graphql │ ├── get_packages.graphql │ ├── get_signed_url.graphql │ ├── login.graphql │ ├── publish_package.graphql │ ├── publish_package_chunked.graphql │ ├── publish_public_key.graphql │ ├── search.graphql │ ├── test_if_registry_present.graphql │ ├── wax_get_command.graphql │ ├── wax_get_package_command.graphql │ └── whoami.graphql └── schema.graphql ├── lib └── wasm-interface │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── interface.rs │ ├── interface_matcher.rs │ ├── lib.rs │ ├── parser.rs │ └── validate.rs └── src ├── abi.rs ├── bin └── wapm.rs ├── commands ├── add.rs ├── bin.rs ├── completions.rs ├── config.rs ├── execute.rs ├── init.rs ├── install.rs ├── keys.rs ├── list.rs ├── login.rs ├── logout.rs ├── mod.rs ├── package │ ├── assets.rs │ ├── compress.rs │ ├── header.rs │ ├── mod.rs │ └── options.rs ├── publish.rs ├── remove.rs ├── run.rs ├── search.rs ├── uninstall.rs ├── validate.rs └── whoami.rs ├── config.rs ├── constants.rs ├── data ├── lock │ ├── lockfile.rs │ ├── lockfile_command.rs │ ├── lockfile_module.rs │ ├── migrate.rs │ └── mod.rs ├── manifest.rs ├── mod.rs └── wax_index.rs ├── database.rs ├── dataflow ├── added_packages.rs ├── bin_script.rs ├── bindings.rs ├── changed_manifest_packages.rs ├── find_command_result.rs ├── installed_packages.rs ├── interfaces.rs ├── local_package.rs ├── lockfile_packages.rs ├── manifest_packages.rs ├── merged_lockfile_packages.rs ├── mod.rs ├── removed_lockfile_packages.rs ├── removed_packages.rs ├── resolved_packages.rs └── retained_lockfile_packages.rs ├── graphql.rs ├── init.rs ├── integration_tests ├── add_remove_init.rs ├── data.rs ├── mod.rs └── prelude.rs ├── interfaces.rs ├── keys.rs ├── lib.rs ├── logging.rs ├── proxy.rs ├── sql ├── migrations │ ├── 0000.sql │ ├── 0001.sql │ └── 0002.sql ├── mod.rs └── queries │ ├── delete_personal_key_pair.sql │ ├── get_interface.sql │ ├── get_latest_public_key_for_user.sql │ ├── get_personal_keys.sql │ ├── get_wapm_public_keys.sql │ ├── insert_and_activate_personal_key_pair.sql │ ├── insert_interface.sql │ ├── insert_user.sql │ ├── insert_wapm_public_key.sql │ ├── personal_private_key_value_existence_check.sql │ ├── personal_public_key_value_existence_check.sql │ ├── private_key_location_exists.sql │ ├── wapm_public_key_existence_check.sql │ └── wasm_interface_existence_check.sql ├── update_notifier.rs ├── util.rs └── validate.rs /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: "\U0001F41E bug" 6 | assignees: '' 7 | 8 | --- 9 | 10 | Thanks for the bug report! 11 | 12 | ### Describe the bug 13 | 14 | A clear and concise description of what the bug is. 15 | 16 | Copy and paste the result of executing the following in your shell, so we can know the version of `wapm-cli`, Rust (if available) and architecture of your environment. 17 | 18 | ```bash 19 | wapm -V 20 | rustc -V 21 | ``` 22 | 23 | Please include the `wapm.log` file located in your `WASMER_DIR` if applicable. 24 | 25 | ### Steps to reproduce 26 | 27 | 1. Go to '…' 28 | 2. Compile with '…' 29 | 3. Run '…' 30 | 4. See error 31 | 32 | If applicable, add a link to a test case (as a zip file or link to a repository we can clone). 33 | 34 | ### Expected behavior 35 | 36 | A clear and concise description of what you expected to happen. 37 | 38 | ### Actual behavior 39 | 40 | A clear and concise description of what actually happened. 41 | 42 | If applicable, add screenshots to help explain your problem. 43 | 44 | ### Additional context 45 | 46 | Add any other context about the problem here. 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F389 Feature request" 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: "\U0001F389 enhancement" 6 | assignees: '' 7 | 8 | --- 9 | 10 | Thanks for proposing a new feature! 11 | 12 | ### Motivation 13 | 14 | A clear and concise description of what the motivation for the new feature is, and what problem it is solving. 15 | 16 | ### Proposed solution 17 | 18 | A clear and concise description of the feature you would like to add, and how it solves the motivating problem. 19 | 20 | ### Alternatives 21 | 22 | A clear and concise description of any alternative solutions or features you've considered, and why you're proposed solution is better. 23 | 24 | ### Additional context 25 | 26 | Add any other context or screenshots about the feature request here. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "❓ Question" 3 | about: Ask a question about this project 4 | title: '' 5 | labels: "❓ question" 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Summary 11 | 12 | A clear and concise summary of your question. 13 | 14 | ### Additional details 15 | 16 | Provide any additional details here. 17 | -------------------------------------------------------------------------------- /.github/cross-linux-aarch64/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1 2 | #FROM ghcr.io/cross-rs/aarch64-unknown-linux-gnu:edge 3 | 4 | # set CROSS_DOCKER_IN_DOCKER to inform `cross` that it is executed from within a container 5 | ENV CROSS_DOCKER_IN_DOCKER=true 6 | 7 | RUN cargo install cross 8 | RUN dpkg --add-architecture arm64 && \ 9 | apt-get update && \ 10 | apt-get install -qy curl && \ 11 | curl -sSL https://get.docker.com/ | sh 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | While drafting pull requests, please use the draft feature of GitHub, or insert "WIP" into the title. 2 | This helps us know when to review. 3 | 4 | When a PR is ready for review, please do the following: 5 | - run `cargo fmt` 6 | - mention the issue number(s) in PR 7 | - remove these instructions from the PR 8 | 9 | See the contributing guide for clarification on these requests. 10 | 11 | Thank you for contributing to `wapm-cli`! -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.env 3 | /wapm_packages 4 | package 5 | dist 6 | wax 7 | \.idea/ 8 | \.vscode/ 9 | .DS_Store -------------------------------------------------------------------------------- /.graphqlconfig.yaml: -------------------------------------------------------------------------------- 1 | schemaPath: graphql/schema.graphql 2 | extensions: 3 | endpoints: 4 | prod: "https://registry.wapm.io/graphql" 5 | dev: "https://registry.wapm.dev/graphql" 6 | local: "http://localhost:8000/graphql" 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All PRs to the `wapm-cli` repository must add to this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## **[Unreleased]** 9 | ### Changed 10 | - Updated dependency `whoami` to 1.1.5 11 | - Support reusing login tokens when switching registries: old login tokens are now not cleared automatically 12 | - Add `wapm login --user --password ` 13 | - Add `wapm init ` to make initializing projects quicker 14 | 15 | ## [0.5.1] - 2021-03-30 16 | ### Added 17 | - Added support for SOCKS proxies 18 | 19 | ### Changed 20 | - Minor updates and dependency updates 21 | 22 | ## [0.5.0] - 2020-03-10 23 | ### Added 24 | - Added support for any WebAssembly runtime via `WAPM_RUNTIME` (so you can use it like `WAPM_RUNTIME=wasmer-js wapm run python`) 25 | - Add ergonomic `wax` command for directly executing Wasm from the wapm registry. TODO: write a brief intro to wax here (can also put it in docs) 26 | ### Changed 27 | - Use [`boxx`](https://crates.io/crates/boxx) to show the update notification message. 28 | - Make module pre-hashing optional and non-dependent on the [wasmer runtime](https://github.com/wasmerio/wasmer) 29 | - Remove `run` extra runtime argument passed to `WAPM_RUNTIME` 30 | - The `--comand-name` flag passed to Wasm runtimes is now of the form `command-name` instead of `wapm run command-name`. 31 | 32 | ## [0.4.3] - 2020-01-16 33 | ### Changed 34 | - Fixed a bug related to the use of `package.wasmer-extra-flags` 35 | 36 | ## [0.4.2] - 2020-01-06 37 | ### Changed 38 | - Changed the way manifests are found in the filesystem fixing bugs related to global install and commands in the manifest 39 | 40 | ## [0.4.1] - 2019-11-20 41 | ### Changed 42 | - The automatic updater will now attempt to parse the version numbers to intelligently suggest upgrades. Falls back to the old logic if version parsing fails. 43 | 44 | ## [0.4.0] - 2019-11-11 45 | ### Added 46 | - `wapm add` and `wapm remove` command to add and remove dependencies from the wapm manifest 47 | 48 | ### Changed 49 | - `wapm init` is now interactive: run `wapm init -y` to accept all defaults 50 | - `wapm.lock` now uses relative paths; it can safely be shared and used on different systems 51 | 52 | ## [0.3.7] - 2019-07-31 53 | ### Added 54 | - Remove unwrap in validate subcommand 55 | - Make update notifications async; add config option 56 | 57 | ## [0.3.6] - 2019-07-23 58 | ### Added 59 | - Hash of Wasm modules to lockfile for faster startup times 60 | - Update notification and check that runs daily 61 | 62 | ## [0.3.5] - 2019-07-16 63 | ### Added 64 | - Proxy support using standard env vars (`ALL_PROXY`, `HTTPS_PROXY`, `http_proxy`) and add override in config, for example: `wapm config set proxy.url "https://username:password@example.com"` 65 | - Updated Wasm interface syntax 66 | 67 | ## [0.3.4] - 2019-07-05 68 | ### Added 69 | - Improved Wasm Interface manifest format 70 | - fixes and improvements for Wasm Interface 71 | 72 | ## [0.3.3] - 2019-07-02 73 | ### Changed 74 | - use the author the registry sends over to look for a saved public key to validate the package/new public keys with with 75 | 76 | ## [0.3.2] - 2019-07-02 77 | ### Added 78 | - `disable-command-rename` field in `package` which prevents the first argument (usually the name of the program) from being edited by wapm (it's edited to provide better feedback when things like the `--help` command run). Programs that rely on the first argument, like python, need renaming disabled. 79 | - `--force-yes` flag to `wapm install` which accepts all prompts 80 | - `--dry-run` flag to `wapm publish` which runs the publish logic without sending anything to the registry 81 | - validation of the manifest on publish, all commands must reference valid modules 82 | - wapm will now suggest a package to install that contains the desired command if the command is not found 83 | 84 | ### Changed 85 | - Files in the wapm module are now relative to their locations in the manifest. This means that going into the directory of an installed global package lets you run it as if it were local. This improves consistency and usability and allows programs interfacing with packages to be simpler. 86 | - Renamed Wasm Contracts to Wasm Interfaces 87 | - Lockfile version 3 with package root directory added 88 | - Changes to the pkg-fs api 89 | 90 | ## [0.3.1] - 2019-06-19 91 | ### Added 92 | - Bug fix to stop wapm from entirely blocking consuming unsigned packages from producers for whom the consumer has a public key 93 | - `keys generate` convenience subcommand 94 | - Package filesystem support allowing filesystems to be bundled with wapm packages 95 | 96 | ## [0.3.0] - 2019-06-17 97 | ### Added 98 | - Wasm contracts (experimental way of validating imports and exports) 99 | - Package signing 100 | - Packages can be signed and verified with Minisign keys 101 | - `wapm keys` for relevant subcommands 102 | - SQLite database as backing store for data like keys and contracts 103 | 104 | ## [0.2.0] - 2019-05-06 105 | ### Added 106 | - Install packages with name and version e.g. `wapm install lua@0.1.2`. 107 | - Fall back to default for `WASMER_DIR` env var if it doesn't exist 108 | - Global install packages from the registry with `-g`/`--global` flag. 109 | - Packages are installed into `WASMER_DIR/globals`. 110 | - Packages are runnable from any directory that does not already have that package installed. 111 | - Packages are runnable from any directory that does not already have that package installed. 112 | - List subcommand (`wapm list`) to show packages and commands installed 113 | - Enforce semantic version numbers on the package and dependencies. 114 | - Allow ranges of semantic versions when declaring dependencies e.g. `"_/sqlite": "^0.1"` 115 | - Uninstall a package with `wapm uninstall` and use the `-g` flag for global uninstall. 116 | - Get the bin directory for wapm-run-scripts using `wapm bin` command. 117 | - Publish wapm package with license file 118 | - Add CI job for Windows 119 | - Add CI integration tests 120 | ### Changed 121 | - Refactored process for generating updates to manifest, regenerating the lockfile, and installing packages. 122 | - Changed OpenSSL to statically link for Linux builds (because version 1.1 is not widely deployed yet) 123 | - Statically link LibSSL 1.1 on Linux 124 | ### Fixed 125 | - Fixed installing packages with http responses that are missing the gzip content encoding header. 126 | 127 | ## [0.1.0] - 2019-04-22 128 | ☄ First release of `wapm-cli` 🌌 129 | 130 | [Unreleased]: https://github.com/wasmerio/wapm-cli/compare/v0.3.1...HEAD 131 | [0.3.1]: https://github.com/wasmerio/wapm-cli/releases/tag/v0.3.1 132 | [0.3.0]: https://github.com/wasmerio/wapm-cli/releases/tag/v0.3.0 133 | [0.2.0]: https://github.com/wasmerio/wapm-cli/releases/tag/v0.2.0 134 | [0.1.0]: https://github.com/wasmerio/wapm-cli/releases/tag/v0.1.0 135 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Get Help 4 | 5 | Need help using the tool? Join the [spectrum chat][spectrum]. Find the `wapm-cli` channel! 6 | 7 | Found a problem? Please report an issue or open a PR! Use this guide for instruction on opening issues and pull requests. 8 | 9 | ## Open an Issue 10 | 11 | Reporting issues is incredibly helpful for identifying problems with`wapm-cli` and we appreciate anyone taking the time to write them up! 12 | 13 | ## Open a Pull Request 14 | 15 | Please create an issue before submitting pull requests and follow the PR template instructions. 16 | 17 | A few other clarifications: 18 | 19 | 1. Please format code with `rustfmt`/`cargo fmt` on the `stable` chain. 20 | See the [`rustfmt` installing instructions][rustfmt] if you do not have this setup. 21 | 2. Please update the [changelog file][changelog]. This helps with managing `wapm-cli` releases. 22 | 23 | [changelog]: CHANGELOG.md 24 | [rustfmt]: https://github.com/rust-lang/rustfmt 25 | [spectrum]: https://spectrum.chat/wasmer 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wapm-cli" 3 | version = "0.5.9" 4 | authors = ["The Wasmer Engineering Team "] 5 | edition = "2018" 6 | repository = "https://github.com/wasmerio/wapm-cli" 7 | description = "WebAssembly Package Manager CLI" 8 | license = "MIT" 9 | 10 | [dependencies] 11 | billboard = { version = "0.1.0", optional = true } 12 | chrono = { version = "0.4", features = ["serde"] } 13 | colored = { version = "1.8", optional = true } 14 | dirs = { version = "4", optional = true } 15 | anyhow = "1" 16 | thiserror = "1.0" 17 | fern = {version = "0.6", features = ["colored"]} 18 | flate2 = "1.0.7" 19 | graphql_client = "0.9" 20 | lazy_static = "1.3" 21 | license-exprs = "1.4.0" 22 | log = "0.4" 23 | maplit = { version = "1", optional = true } 24 | minisign = "0.5" 25 | prettytable-rs = { version = "0.8.0", optional = true } 26 | regex = "1" 27 | rpassword-wasi = "5" 28 | rusqlite = { version = "0.24", optional = true } 29 | semver = { version = "1.0", features = ["serde"] } 30 | sentry = { version = "0.22.0", optional = true, features = ["anyhow", "panic", "backtrace"] } 31 | serde = "1.0" 32 | serde_derive = "1.0" 33 | serde_json = "1.0" 34 | structopt = { version = "0.3", features = ["color"] } 35 | tempfile = "3" 36 | time = "0.1" 37 | toml = "0.5.6" 38 | url = "2" 39 | wapm-toml = { version = "0.3.0" } 40 | wasmer-wasm-interface = { version = "0.1.0", path = "lib/wasm-interface" } 41 | wasmparser = "0.51.4" 42 | dialoguer = "0.4.0" 43 | hex = { version = "0.4", optional = true } 44 | blake3 = { version = "0.3.1", optional = true } 45 | sublime_fuzzy = "0.7.0" 46 | indicatif = { version = "0.17.1", features = ["improved_unicode"] } 47 | console = "0.15.2" 48 | 49 | [target.'cfg(not(target_os = "wasi"))'.dependencies] 50 | whoami = "1.1.5" 51 | atty = "0.2" 52 | reqwest = { version = "0.11.0", features = ["native-tls-vendored", "blocking", "json", "gzip","socks","multipart"], optional = true } 53 | tar = { version = "0.4" } 54 | 55 | [target.'cfg(target_os = "wasi")'.dependencies] 56 | whoami = "0.5" 57 | wasm-bus-reqwest = "1.0" 58 | wasm-bus-process = "1.0" 59 | getrandom = "0.2.3" 60 | tar = { package = "tar-wasi", version = "0.4" } 61 | serde_yaml = { version = "^0.8" } 62 | 63 | [dev-dependencies] 64 | tempfile = "3" 65 | 66 | [workspace] 67 | members = [ 68 | ".", 69 | "lib/wasm-interface" 70 | ] 71 | 72 | [features] 73 | default = ["full","packagesigning", "sqlite-bundled"] 74 | sqlite-bundled = ["rusqlite/bundled"] 75 | telemetry = ["sentry"] 76 | update-notifications= ["billboard", "colored"] 77 | prehash-module = ["hex", "blake3"] 78 | packagesigning = []#[cfg(feature = "full")] 79 | integration_tests = ["maplit", "wapm-toml/integration_tests"] 80 | full = [ "dirs", "rusqlite", "prettytable-rs", "reqwest" ] 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Wasmer, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IS_DARWIN := 0 2 | IS_LINUX := 0 3 | IS_WINDOWS := 0 4 | IS_AMD64 := 0 5 | IS_AARCH64 := 0 6 | 7 | CARGO_BINARY ?= cargo 8 | CARGO_TARGET ?= 9 | 10 | # Test Windows apart because it doesn't support `uname -s`. 11 | ifeq ($(OS), Windows_NT) 12 | # We can assume it will likely be in amd64. 13 | IS_AMD64 := 1 14 | IS_WINDOWS := 1 15 | else 16 | # Platform 17 | uname := $(shell uname -s) 18 | 19 | ifeq ($(uname), Darwin) 20 | IS_DARWIN := 1 21 | else ifeq ($(uname), Linux) 22 | IS_LINUX := 1 23 | else 24 | # We use spaces instead of tabs to indent `$(error)` 25 | # otherwise it's considered as a command outside a 26 | # target and it will fail. 27 | $(error Unrecognized platform, expect `Darwin`, `Linux` or `Windows_NT`) 28 | endif 29 | 30 | # Architecture 31 | uname := $(shell uname -m) 32 | 33 | ifeq ($(uname), x86_64) 34 | IS_AMD64 := 1 35 | else ifneq (, $(filter $(uname), aarch64 arm64)) 36 | IS_AARCH64 := 1 37 | else 38 | # We use spaces instead of tabs to indent `$(error)` 39 | # otherwise it's considered as a command outside a 40 | # target and it will fail. 41 | $(error Unrecognized architecture, expect `x86_64`, `aarch64` or `arm64`) 42 | endif 43 | endif 44 | 45 | ifeq ($(IS_DARWIN), 1) 46 | TARGET_DIR ?= target/*/release 47 | else 48 | TARGET_DIR ?= target/release 49 | endif 50 | 51 | build-release: 52 | ifeq ($(IS_DARWIN), 1) 53 | # We build it without bundling sqlite, as is included by default in macos 54 | $(CARGO_BINARY) build $(CARGO_TARGET) --release --no-default-features --features "full packagesigning telemetry update-notifications" 55 | else 56 | $(CARGO_BINARY) build $(CARGO_TARGET) --release --features "telemetry update-notifications" 57 | endif 58 | 59 | release: build-release 60 | mkdir -p "package/bin" 61 | ifeq ($(IS_WINDOWS), 1) 62 | cp $(TARGET_DIR)/wapm.exe package/bin/ &&\ 63 | printf '@echo off\nwapm.exe execute %%*' > package/bin/wax.cmd &&\ 64 | chmod +x package/bin/wax.cmd ; 65 | else 66 | ifneq (, $(filter 1, $(IS_DARWIN) $(IS_LINUX))) 67 | cp $(TARGET_DIR)/wapm package/bin/ &&\ 68 | printf "#!/bin/bash\nwapm execute \"\$$@\"" > package/bin/wax &&\ 69 | chmod +x package/bin/wax ; 70 | else 71 | cp $(TARGET_DIR)/wapm package/bin/ 72 | ifeq ($(IS_DARWIN), 1) 73 | codesign -s - package/bin/wapm || true 74 | endif 75 | endif 76 | endif 77 | cp LICENSE package/LICENSE 78 | tar -C package -zcvf wapm-cli.tar.gz LICENSE bin 79 | mkdir -p dist 80 | mv wapm-cli.tar.gz dist/ 81 | 82 | integration-tests: 83 | $(CARGO_BINARY) test --features "integration_tests" integration_tests:: 84 | 85 | regression-tests: 86 | chmod +x end-to-end-tests/ci/chunked-upload.sh 87 | ./end-to-end-tests/ci/chunked-upload.sh 88 | chmod +x end-to-end-tests/ci/direct-execution.sh 89 | ./end-to-end-tests/ci/direct-execution.sh 90 | chmod -x end-to-end-tests/ci/direct-execution.sh 91 | echo "\n - name: 'Regression test: Install, Uninstall, Run, and List'" 92 | chmod +x end-to-end-tests/ci/install.sh 93 | ./end-to-end-tests/ci/install.sh 94 | chmod -x end-to-end-tests/ci/install.sh 95 | echo "\nname: 'Regression test: verification and public key management'" 96 | chmod +x end-to-end-tests/ci/verification.sh 97 | ./end-to-end-tests/ci/verification.sh 98 | chmod -x end-to-end-tests/ci/verification.sh 99 | echo "\n name: 'Regression test: manifest validation rejects invalid manifests'" 100 | chmod +x end-to-end-tests/ci/manifest-validation.sh 101 | ./end-to-end-tests/ci/manifest-validation.sh 102 | chmod -x end-to-end-tests/ci/manifest-validation.sh 103 | echo "\n: 'Regression test: package fs and command rename'" 104 | chmod +x end-to-end-tests/ci/validate-global.sh 105 | ./end-to-end-tests/ci/validate-global.sh 106 | chmod -x end-to-end-tests/ci/validate-global.sh 107 | echo "\n: 'Regression test: Init a Manifest and Add some dependencies'" 108 | chmod +x end-to-end-tests/ci/init-and-add.sh 109 | ./end-to-end-tests/ci/init-and-add.sh 110 | chmod -x end-to-end-tests/ci/init-and-add.sh 111 | 112 | test: integration-tests 113 | cargo test --features "update-notifications" -- --test-threads=1 114 | cargo test --manifest-path lib/wasm-interface/Cargo.toml 115 | cargo check --features "telemetry update-notifications" 116 | 117 | update-schema: 118 | curl --fail "$(shell wapm config get registry.url)/schema.graphql" > graphql/schema.graphql 119 | 120 | clean: 121 | rm -rf ./dist 122 | rm -rf ./package 123 | rm -rf ./target 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Wapm logo 4 | 5 |

6 | 7 |

8 | 9 | tests 10 | 11 | 12 | 13 | License 14 | 15 | 16 | Join the Wasmer Community 17 | 18 |

19 | 20 | # Introduction 21 | 22 | The WebAssembly Package Manager CLI. This tool enables installing, managing, and publishing wasm packages on the [wapm.io][wapmio] registry. 23 | 24 | ## Installation 25 | 26 | ### With [`Wasmer`](https://wapm.io/help/install): 27 | 28 | As described in [README.md of Wasmer](https://github.com/wasmerio/wasmer#install), you can install `wapm-cli` together by the following command: 29 | 30 | ```bash 31 | curl https://get.wasmer.io -sSfL | sh 32 | ``` 33 | 34 | ### With [`Cargo`](https://crates.io/crates/wapm-cli/): 35 | 36 | ```bash 37 | cargo install wapm-cli 38 | ``` 39 | 40 | ### With [`Homebrew`](https://formulae.brew.sh/formula/wapm): 41 | 42 | ```bash 43 | brew install wapm 44 | ``` 45 | 46 | ## Get Started 47 | 48 | Read the [`wapm-cli` user guide on `wapm.io`][guide] to get started using the tool and use the [`wapm-cli` reference][reference] for information about the CLI commands. 49 | 50 | ## Get Help 51 | 52 | Feel free to take a look at the [WAPM documentation](https://docs.wasmer.io/ecosystem/wapm). You can also join the discussion on [spectrum chat][spectrum] in the `wapm-cli` channel, or create a GitHub issue. We love to help! 53 | 54 | ## Contributing 55 | 56 | See the [contributing guide][contributing] for instruction on contributing to `wapm-cli`. 57 | 58 | ## Development 59 | 60 | ### Update GraphQL Schema 61 | 62 | If the WAPM GraphQL server has been updated, update the GraphQL schema with: 63 | 64 | ``` 65 | graphql get-schema -e prod 66 | ``` 67 | 68 | _Note: You will need `graphql-cli` installed for this: `npm install -g graphql-cli`._ 69 | 70 | [contributing]: CONTRIBUTING.md 71 | [guide]: https://wapm.io/help/guide 72 | [reference]: https://wapm.io/help/reference 73 | [spectrum]: https://spectrum.chat/wasmer 74 | [wasmer]: https://wasmer.io 75 | [wapmio]: https://wapm.io 76 | -------------------------------------------------------------------------------- /assets/largewasmfile.wapm.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "WAPMUSERNAME/largewasmfile" 3 | version = "RANDOMVERSION1.RANDOMVERSION2.RANDOMVERSION3" 4 | description = "large wasm file" 5 | license = "Apache-2.0" 6 | repository = "" 7 | 8 | [[module]] 9 | name = "largewasmfile" 10 | source = "largewasmfile.wasm" 11 | abi = "wasi" 12 | 13 | [module.interfaces] 14 | wasi = "0.1.0-unstable" 15 | 16 | [[command]] 17 | name = "largewasmfile" 18 | module = "largewasmfile" 19 | -------------------------------------------------------------------------------- /assets/largewasmfile.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmerio/wapm-cli/6865b801e184b697277a96ca8ef0c5f53102bf6e/assets/largewasmfile.wasm -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmerio/wapm-cli/6865b801e184b697277a96ca8ef0c5f53102bf6e/assets/logo.png -------------------------------------------------------------------------------- /end-to-end-tests/chunked_upload.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export RUST_BACKTRACE=1 4 | ln -sf `which wapm` wax 5 | 6 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 7 | PWD=$(pwd -P) 8 | RANDOMVERSION1=$RANDOM 9 | RANDOMVERSION2=$RANDOM 10 | RANDOMVERSION3=$RANDOM 11 | WAPMUSERNAME=$WAPM_DEV_USERNAME 12 | 13 | mkdir -p /tmp/largewasmfile 14 | cp -rf $SCRIPT_DIR/../assets/largewasmfile.wasm /tmp/largewasmfile/largewasmfile.wasm 15 | cp -rf $SCRIPT_DIR/../assets/largewasmfile.wapm.toml /tmp/largewasmfile/wapm.toml 16 | cp -rf $SCRIPT_DIR/chunked_upload.txt /tmp/chunked_upload_reference.txt 17 | 18 | if [ "$(uname)" == "Darwin" ]; then 19 | sed -i '' "s/RANDOMVERSION3/$RANDOMVERSION3/g" /tmp/largewasmfile/wapm.toml 20 | sed -i '' "s/RANDOMVERSION2/$RANDOMVERSION2/g" /tmp/largewasmfile/wapm.toml 21 | sed -i '' "s/RANDOMVERSION1/$RANDOMVERSION1/g" /tmp/largewasmfile/wapm.toml 22 | sed -i '' "s/WAPMUSERNAME/$WAPMUSERNAME/g" /tmp/largewasmfile/wapm.toml 23 | 24 | sed -i '' "s/RANDOMVERSION3/$RANDOMVERSION3/g" /tmp/chunked_upload_reference.txt 25 | sed -i '' "s/RANDOMVERSION2/$RANDOMVERSION2/g" /tmp/chunked_upload_reference.txt 26 | sed -i '' "s/RANDOMVERSION1/$RANDOMVERSION1/g" /tmp/chunked_upload_reference.txt 27 | sed -i '' "s/WAPMUSERNAME/$WAPMUSERNAME/g" /tmp/chunked_upload_reference.txt 28 | else 29 | sed -i "s/RANDOMVERSION3/$RANDOMVERSION3/g" /tmp/largewasmfile/wapm.toml 30 | sed -i "s/RANDOMVERSION2/$RANDOMVERSION2/g" /tmp/largewasmfile/wapm.toml 31 | sed -i "s/RANDOMVERSION1/$RANDOMVERSION1/g" /tmp/largewasmfile/wapm.toml 32 | sed -i "s/WAPMUSERNAME/$WAPMUSERNAME/g" /tmp/largewasmfile/wapm.toml 33 | 34 | sed -i "s/RANDOMVERSION3/$RANDOMVERSION3/g" /tmp/chunked_upload_reference.txt 35 | sed -i "s/RANDOMVERSION2/$RANDOMVERSION2/g" /tmp/chunked_upload_reference.txt 36 | sed -i "s/RANDOMVERSION1/$RANDOMVERSION1/g" /tmp/chunked_upload_reference.txt 37 | sed -i "s/WAPMUSERNAME/$WAPMUSERNAME/g" /tmp/chunked_upload_reference.txt 38 | fi 39 | 40 | cd /tmp/largewasmfile 41 | wapm publish --quiet 42 | cd $PWD 43 | -------------------------------------------------------------------------------- /end-to-end-tests/chunked_upload.txt: -------------------------------------------------------------------------------- 1 | Successfully published package `WAPMUSERNAME/largewasmfile@RANDOMVERSION1.RANDOMVERSION2.RANDOMVERSION3` 2 | -------------------------------------------------------------------------------- /end-to-end-tests/ci/README.md: -------------------------------------------------------------------------------- 1 | This folder is for scripts to run the legacy integration tests in CI. 2 | -------------------------------------------------------------------------------- /end-to-end-tests/ci/chunked-upload.sh: -------------------------------------------------------------------------------- 1 | export PATH=$PATH:$HOME/.cargo/bin 2 | export PATH=$PATH:$HOME/.wasmer/bin 3 | export WAPM_DISABLE_COLOR=true 4 | export WAPM_DEV_USERNAME="${WAPM_DEV_USERNAME:-felix}" 5 | rm -f $WASMER_DIR/.wax_index.toml 6 | # TODO force clear cache 7 | rm -f wapm.toml 8 | rm -f wapm.lock 9 | wapm config set registry.url "https://registry.wapm.dev/graphql" 10 | chmod +x end-to-end-tests/chunked_upload.sh 11 | if ! [[ -z "${WAPM_DEV_TOKEN}" ]]; then 12 | wapm login "${WAPM_DEV_TOKEN}" 13 | fi 14 | echo "RUNNING SCRIPT..." 15 | ./end-to-end-tests/chunked_upload.sh &> /tmp/chunked_upload.txt 16 | echo "GENERATED OUTPUT:" 17 | cat /tmp/chunked_upload.txt 18 | echo "COMPARING..." 19 | diff -Bba /tmp/chunked_upload_reference.txt /tmp/chunked_upload.txt 20 | export OUT=$? 21 | if ( [ -d globals ] || [ -f wapm.log ] ) then 22 | { echo "globals or wapm.log found; these files should not be in the working directory"; exit 1; } 23 | else { true; } 24 | fi 25 | 26 | rm -f wapm.lock 27 | rm -f wapm.toml 28 | rm -rf wapm_packages 29 | rm -f /tmp/chunked_upload.txt 30 | if ( [ $OUT -ne 0 ] ) then { cat $HOME/.wasmer/wapm.log; } fi 31 | exit $OUT -------------------------------------------------------------------------------- /end-to-end-tests/ci/direct-execution.sh: -------------------------------------------------------------------------------- 1 | export PATH=$PATH:$HOME/.cargo/bin 2 | export PATH=$PATH:$HOME/.wasmer/bin 3 | export WAPM_DISABLE_COLOR=true 4 | rm -f $WASMER_DIR/.wax_index.toml 5 | # TODO force clear cache 6 | rm -f wapm.toml 7 | rm -f wapm.lock 8 | chmod +x end-to-end-tests/direct_execute.sh 9 | wapm config set registry.url "https://registry.wapm.dev" 10 | echo "RUNNING SCRIPT..." 11 | ./end-to-end-tests/direct_execute.sh &> /tmp/direct_execute.txt 12 | echo "GENERATED OUTPUT:" 13 | cat /tmp/direct_execute.txt 14 | echo "COMPARING..." 15 | diff -Bba end-to-end-tests/direct_execute.txt /tmp/direct_execute.txt 16 | export OUT=$? 17 | if ( [ -d globals ] || [ -f wapm.log ] ) then 18 | { echo "globals or wapm.log found; these files should not be in the working directory"; exit 1; } 19 | else { true; } 20 | fi 21 | 22 | rm -f wapm.lock 23 | rm -f wapm.toml 24 | rm -rf wapm_packages 25 | rm -f /tmp/direct_execute.txt 26 | if ( [ $OUT -ne 0 ] ) then { cat $HOME/.wasmer/wapm.log; } fi 27 | exit $OUT 28 | -------------------------------------------------------------------------------- /end-to-end-tests/ci/init-and-add.sh: -------------------------------------------------------------------------------- 1 | export PATH=$PATH:$HOME/.cargo/bin 2 | export PATH=$PATH:$HOME/.wasmer/bin 3 | export WAPM_DISABLE_COLOR=true 4 | export WAPM_DEV_USERNAME="${WAPM_DEV_USERNAME:-felix}" 5 | rm -f $WASMER_DIR/wapm.sqlite 6 | rm -f $WASMER_DIR/globals/wapm.lock 7 | rm -rf wapm_packages 8 | rm -f wapm.toml 9 | rm -f wapm.lock 10 | chmod +x end-to-end-tests/init-and-add.sh 11 | wapm config set registry.url "https://registry.wapm.dev" 12 | echo "RUNNING SCRIPT..." 13 | ./end-to-end-tests/init-and-add.sh &> /tmp/init-and-add-out.txt 14 | echo "GENERATED OUTPUT:" 15 | cat /tmp/init-and-add-out.txt 16 | echo "ADJUSTING OUTPUT" 17 | # removes the absolute path 18 | tail -n +3 /tmp/init-and-add-out.txt > /tmp/init-and-add-out2.txt 19 | cat /tmp/init-and-add-out2.txt 20 | mv /tmp/init-and-add-out2.txt /tmp/init-and-add-out.txt 21 | cat end-to-end-tests/init-and-add.txt > /tmp/init-and-add-original.txt 22 | WAPMUSERNAME=$WAPM_DEV_USERNAME 23 | if [ "$(uname)" == "Darwin" ]; then 24 | sed -i '' "s/WAPMUSERNAME/$WAPMUSERNAME/g" /tmp/init-and-add-original.txt 25 | else 26 | sed -i "s/WAPMUSERNAME/$WAPMUSERNAME/g" /tmp/init-and-add-original.txt 27 | fi 28 | echo "COMPARING..." 29 | diff -Bba /tmp/init-and-add-original.txt /tmp/init-and-add-out.txt 30 | export OUT=$? 31 | if ( [ -d globals ] || [ -f wapm.log ] ) then { echo "globals or wapm.log found; these files should not be in the working directory"; exit 1; } else { true; } fi 32 | rm -f wapm.lock 33 | rm -f wapm.toml 34 | rm -rf wapm_packages 35 | rm -f /tmp/init-and-add-out.txt 36 | if ( [ $OUT -ne 0 ] ) then { cat $HOME/.wasmer/wapm.log; } fi 37 | exit $OUT 38 | -------------------------------------------------------------------------------- /end-to-end-tests/ci/install.sh: -------------------------------------------------------------------------------- 1 | export PATH=$PATH:$HOME/.cargo/bin 2 | export PATH=$PATH:$HOME/.wasmer/bin 3 | export WAPM_DISABLE_COLOR=true 4 | rm -f $WASMER_DIR/wapm.sqlite 5 | rm -f $WASMER_DIR/globals/wapm.lock 6 | rm -rf wapm_packages 7 | rm -f wapm.toml 8 | rm -f wapm.lock 9 | chmod +x end-to-end-tests/install.sh 10 | wapm config set registry.url "https://registry.wapm.dev" 11 | echo "RUNNING SCRIPT..." 12 | ./end-to-end-tests/install.sh &> /tmp/install-out.txt 13 | echo "GENERATED OUTPUT:" 14 | cat /tmp/install-out.txt 15 | echo "COMPARING..." 16 | diff -Bba end-to-end-tests/install.txt /tmp/install-out.txt 17 | export OUT=$? 18 | if ( [ -d globals ] || [ -f wapm.log ] ) then { echo "globals or wapm.log found; these files should not be in the working directory"; exit 1; } else { true; } fi 19 | rm -f wapm.lock 20 | rm -f wapm.toml 21 | rm -rf wapm_packages 22 | rm -f /tmp/install-out.txt 23 | if ( [ $OUT -ne 0 ] ) then { cat $HOME/.wasmer/wapm.log; } fi 24 | exit $OUT 25 | -------------------------------------------------------------------------------- /end-to-end-tests/ci/manifest-validation.sh: -------------------------------------------------------------------------------- 1 | export PATH=$PATH:$HOME/.cargo/bin 2 | export PATH=$PATH:$HOME/.wasmer/bin 3 | export WAPM_DISABLE_COLOR=true 4 | rm -f $HOME/.wasmer/wapm.sqlite 5 | rm -f $HOME/.wasmer/globals/wapm.lock 6 | rm -f wapm.lock 7 | rm -f wapm.toml 8 | rm -rf wapm_packages 9 | chmod +x end-to-end-tests/manifest-validation.sh 10 | wapm config set registry.url "https://registry.wapm.dev" 11 | echo "RUNNING SCRIPT..." 12 | ./end-to-end-tests/manifest-validation.sh &> /tmp/manifest-validation-out.txt 13 | echo "GENERATED OUTPUT:" 14 | cat /tmp/manifest-validation-out.txt 15 | echo "COMPARING..." 16 | diff -Bba end-to-end-tests/manifest-validation.txt /tmp/manifest-validation-out.txt 17 | export OUT=$? 18 | if ( [ -d globals ] || [ -f wapm.log ] ) then { echo "globals or wapm.log found; these files should not be in the working directory"; exit 1; } else { true; } fi 19 | rm -f wapm.lock 20 | rm -f wapm.toml 21 | rm -rf wapm_packages 22 | rm -f /tmp/manifest-validation-out.txt 23 | rm -f $HOME/.wasmer/wapm.sqlite 24 | if ( [ $OUT -ne 0 ] ) then { cat $HOME/.wasmer/wapm.log; } fi 25 | exit $OUT 26 | -------------------------------------------------------------------------------- /end-to-end-tests/ci/package-fs-mapping.sh: -------------------------------------------------------------------------------- 1 | export PATH=$PATH:$HOME/.cargo/bin 2 | export PATH=$PATH:$HOME/.wasmer/bin 3 | export WAPM_DISABLE_COLOR=true 4 | rm -f $HOME/.wasmer/wapm.sqlite 5 | rm -f $HOME/.wasmer/globals/wapm.lock 6 | rm -rf wapm_packages 7 | rm -f wapm.toml 8 | rm -f wapm.lock 9 | chmod +x end-to-end-tests/package-fs-mapping.sh 10 | wapm config set registry.url "https://registry.wapm.dev" 11 | echo "RUNNING SCRIPT..." 12 | ./end-to-end-tests/package-fs-mapping.sh &> /tmp/package-fs-mapping-out.txt 13 | echo "GENERATED OUTPUT:" 14 | cat /tmp/package-fs-mapping-out.txt 15 | echo "COMPARING..." 16 | ## hack to get the current directory in the expected output 17 | #sed -i.bak "s/{{CURRENT_DIR}}/$(pwd | sed 's/\//\\\//g')/g" end-to-end-tests/package-fs-mapping.txt 18 | diff -Bba end-to-end-tests/package-fs-mapping.txt /tmp/package-fs-mapping-out.txt 19 | export OUT=$? 20 | if ( [ -d globals ] || [ -f wapm.log ] ) then { echo "globals or wapm.log found; these files should not be in the working directory"; exit 1; } else { true; } fi 21 | rm -f wapm.lock 22 | rm -f wapm.toml 23 | rm -rf wapm_packages 24 | rm -f /tmp/package-fs-mapping-out.txt 25 | rm -f $HOME/.wasmer/wapm.sqlite 26 | if ( [ $OUT -ne 0 ] ) then { cat $HOME/.wasmer/wapm.log; } fi 27 | exit $OUT 28 | -------------------------------------------------------------------------------- /end-to-end-tests/ci/validate-global.sh: -------------------------------------------------------------------------------- 1 | export PATH=$PATH:$HOME/.cargo/bin 2 | export PATH=$PATH:$HOME/.wasmer/bin 3 | export WAPM_DISABLE_COLOR=true 4 | rm -f $HOME/.wasmer/wapm.sqlite 5 | rm -f $HOME/.wasmer/globals/wapm.lock 6 | rm -f wapm.lock 7 | rm -f wapm.toml 8 | rm -rf wapm_packages 9 | chmod +x end-to-end-tests/validate-global.sh 10 | wapm config set registry.url "https://registry.wapm.dev" 11 | echo "RUNNING SCRIPT..." 12 | ./end-to-end-tests/validate-global.sh &> /tmp/validate-global-out.txt 13 | echo "GENERATED OUTPUT:" 14 | cat /tmp/validate-global-out.txt 15 | echo "COMPARING..." 16 | diff -Bba end-to-end-tests/validate-global.txt /tmp/validate-global-out.txt 17 | export OUT=$? 18 | if ( [ -d globals ] || [ -f wapm.log ] ) then { echo "globals or wapm.log found; these files should not be in the working directory"; exit 1; } else { true; } fi 19 | rm -f wapm.lock 20 | rm -f wapm.toml 21 | rm -rf wapm_packages 22 | rm -f /tmp/validate-global-out.txt 23 | rm -f $HOME/.wasmer/wapm.sqlite 24 | if ( [ $OUT -ne 0 ] ) then { cat $HOME/.wasmer/wapm.log; } fi 25 | exit $OUT 26 | -------------------------------------------------------------------------------- /end-to-end-tests/ci/verification.sh: -------------------------------------------------------------------------------- 1 | export PATH=$PATH:$HOME/.cargo/bin 2 | export PATH=$PATH:$HOME/.wasmer/bin 3 | export WAPM_DISABLE_COLOR=true 4 | rm -f $WASMER_DIR/wapm.sqlite 5 | rm -f $WASMER_DIR/globals/wapm.lock 6 | rm -rf wapm_packages 7 | rm -f wapm.toml 8 | rm -f wapm.lock 9 | chmod +x end-to-end-tests/verification.sh 10 | wapm config set registry.url "https://registry.wapm.dev" 11 | echo "RUNNING SCRIPT..." 12 | ./end-to-end-tests/verification.sh &> /tmp/verification-out.txt 13 | echo "GENERATED OUTPUT:" 14 | cat /tmp/verification-out.txt 15 | echo "COMPARING..." 16 | diff -Bba end-to-end-tests/verification.txt /tmp/verification-out.txt 17 | export OUT=$? 18 | if ( [ -d globals ] || [ -f wapm.log ] ) then { echo "globals or wapm.log found; these files should not be in the working directory"; exit 1; } else { true; } fi 19 | rm -f wapm.lock 20 | rm -f wapm.toml 21 | rm -rf wapm_packages 22 | rm -f /tmp/verification-out.txt 23 | if ( [ $OUT -ne 0 ] ) then { cat $HOME/.wasmer/wapm.log; } fi 24 | exit $OUT 25 | -------------------------------------------------------------------------------- /end-to-end-tests/direct_execute.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export RUST_BACKTRACE=1 4 | ln -sf `which wapm` wax 5 | 6 | # echo "hello" | wapm execute base64 7 | # ./wax echo "hello" 8 | wapm install namespace-example/cowsay 9 | ./wax --emscripten cowsay "hello" 10 | wapm uninstall namespace-example/cowsay 11 | wapm install lolcat 12 | wapm run lolcat -V 13 | ./wax lolcat -V 14 | wapm uninstall lolcat 15 | ./wax lolcat -V 16 | wapm list -a 17 | rm -rf $(./wax --which lolcat)/wapm_packages/_/lolcat@0.1.1/* 18 | ./wax lolcat -V 19 | ./wax --offline lolcat -V 20 | # WAPM_RUNTIME=echo ./wax ls | grep "\-\-command-name" || echo "Success: command-name not found" 21 | -------------------------------------------------------------------------------- /end-to-end-tests/direct_execute.txt: -------------------------------------------------------------------------------- 1 | [INFO] Installing namespace-example/cowsay@0.2.0 2 | Package installed successfully to wapm_packages! 3 | _______ 4 | < hello > 5 | ------- 6 | \ ^__^ 7 | \ (oo)\_______ 8 | (__)\ )\/\ 9 | ||----w | 10 | || || 11 | [INFO] Package "namespace-example/cowsay" uninstalled. 12 | [INFO] Installing syrusakbary/lolcat@0.1.1 13 | Package installed successfully to wapm_packages! 14 | lolcat 1.0.1 15 | lolcat 1.0.1 16 | [INFO] Package "lolcat" is not installed. 17 | lolcat 1.0.1 18 | LOCAL PACKAGES: 19 | PACKAGE | VERSION | MODULE | ABI 20 | syrusakbary/lolcat | 0.1.1 | lolcat | wasi 21 | 22 | LOCAL COMMANDS: 23 | COMMAND | PACKAGE | VERSION 24 | lolcat | syrusakbary/lolcat | 0.1.1 25 | 26 | lolcat 1.0.1 27 | lolcat 1.0.1 28 | -------------------------------------------------------------------------------- /end-to-end-tests/init-and-add.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir test-package 4 | cd test-package 5 | wapm init -y 6 | wapm add this-package-does-not-exist 7 | wapm add mark2/python@0.0.4 mark2/dog2 8 | wapm add lolcat@0.1.1 9 | wapm remove lolcat 10 | cd .. 11 | rm -rf test-package 12 | -------------------------------------------------------------------------------- /end-to-end-tests/init-and-add.txt: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "WAPMUSERNAME/test-package" 4 | version = "1.0.0" 5 | description = "Package description for WAPMUSERNAME/test-package" 6 | license = "ISC" 7 | 8 | [[module]] 9 | name = "entry" 10 | source = "entry.wasm" 11 | 12 | 13 | Successfully initialized project "WAPMUSERNAME/test-package" 14 | [ERROR] Package "this-package-does-not-exist" was not found 15 | Error: There were problems adding packages 16 | [INFO] Adding mark2/python@0.0.4 17 | [INFO] Adding mark2/dog2@0.0.13 18 | Packages successfully added! 19 | [INFO] Adding lolcat@0.1.1 20 | Packages successfully added! 21 | [INFO] Removing "lolcat" 22 | Packages successfully removed! 23 | -------------------------------------------------------------------------------- /end-to-end-tests/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | wapm install namespace-example/cowsay@0.1.2 4 | wapm install namespace-example/cowsay@0.1.2 5 | wapm run cowsay "hello, world" 6 | wapm list 7 | wapm uninstall namespace-example/cowsay 8 | wapm install namespace-example/cowsay@0.1.2 9 | wapm uninstall namespace-example/cowsay 10 | wapm uninstall namespace-example/cowsay 11 | wapm install -g mark/rust-example@0.1.11 12 | wapm run hq9+ -e "H" 13 | wapm uninstall -g mark/rust-example 14 | wapm install -g mark/wapm-override-test@0.1.0 15 | wapm list -a 16 | wapm run wapm-override-test 17 | wapm install mark/wapm-override-test@0.2.0 18 | wapm run wapm-override-test 19 | wapm uninstall mark/wapm-override-test 20 | wapm run wapm-override-test 21 | wapm uninstall -g mark/wapm-override-test 22 | wapm install namespace-example/cowsay@0.1.1 namespace-example/cowsay@0.1.2 23 | -------------------------------------------------------------------------------- /end-to-end-tests/install.txt: -------------------------------------------------------------------------------- 1 | [INFO] Installing namespace-example/cowsay@0.1.2 2 | Package installed successfully to wapm_packages! 3 | No packages to install (package already installed?) 4 | ______________ 5 | < hello, world > 6 | -------------- 7 | \ ^__^ 8 | \ (oo)\_______ 9 | (__)\ )\/\ 10 | ||----w | 11 | || || 12 | LOCAL PACKAGES: 13 | PACKAGE | VERSION | MODULE | ABI 14 | namespace-example/cowsay | 0.1.2 | cowsay | generic 15 | 16 | LOCAL COMMANDS: 17 | COMMAND | PACKAGE | VERSION 18 | cowsay | namespace-example/cowsay | 0.1.2 19 | cowthink | namespace-example/cowsay | 0.1.2 20 | [INFO] Package "namespace-example/cowsay" uninstalled. 21 | [INFO] Installing namespace-example/cowsay@0.1.2 22 | Package installed successfully to wapm_packages! 23 | [INFO] Package "namespace-example/cowsay" uninstalled. 24 | [INFO] Package "namespace-example/cowsay" is not installed. 25 | [INFO] Installing mark/rust-example@0.1.11 26 | Global package installed successfully! 27 | Hello, world! 28 | 29 | [INFO] Package "mark/rust-example" uninstalled. 30 | [INFO] Installing mark/wapm-override-test@0.1.0 31 | Global package installed successfully! 32 | GLOBAL PACKAGES: 33 | PACKAGE | VERSION | MODULE | ABI 34 | mark/wapm-override-test | 0.1.0 | wapm-override-test | wasi 35 | 36 | GLOBAL COMMANDS: 37 | COMMAND | PACKAGE | VERSION 38 | wapm-override-test | mark/wapm-override-test | 0.1.0 39 | Version 0.1.0 40 | [INFO] Installing mark/wapm-override-test@0.2.0 41 | Package installed successfully to wapm_packages! 42 | Version 0.2.0 43 | [INFO] Package "mark/wapm-override-test" uninstalled. 44 | Version 0.1.0 45 | [INFO] Package "mark/wapm-override-test" uninstalled. 46 | Error: Failed to install packages. Attempting to install multiple versions of package namespace-example/cowsay (0.1.1 and 0.1.2) 47 | -------------------------------------------------------------------------------- /end-to-end-tests/manifest-validation.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export RUST_BACKTRACE=1 4 | echo '[package]\nname="test"\nversion="0.0.0"\ndescription="this is a test"\n[[command]]\nname="test"\nmodule="test-module"\n[fs]\n"wapm_file"="src/bin"' > wapm.toml 5 | wapm publish --dry-run 6 | # get a wasm module so we forget the abi field 7 | wapm install mark2/dog2@0.0.13 --force-yes 8 | cp wapm_packages/mark2/dog2@0.0.13/dog.wasm . 9 | echo '[package]\nname="test"\nversion="0.0.0"\ndescription="this is a test"\n[[module]]\nname="test-module"\nsource="dog.wasm"\n[[command]]\nname="test"\nmodule="test-module"\n[fs]\n"wapm_file"="src/bin"' > wapm.toml 10 | wapm publish --dry-run 11 | echo '[package]\nname="test"\nversion="0.0.0"\ndescription="this is a test"\n[[module]]\nname="test-module"\nsource="dog.wasm"\nabi="wasi"\n[[command]]\nname="test"\nmodule="test-module"\n[fs]\n"wapm_file"="src/bin"' > wapm.toml 12 | wapm publish --dry-run 13 | rm dog.wasm 14 | -------------------------------------------------------------------------------- /end-to-end-tests/manifest-validation.txt: -------------------------------------------------------------------------------- 1 | Error: There was an error validating the manifest: missing module test in manifest used by command test-module 2 | [INFO] Installing mark2/dog2@0.0.13 3 | New public key encountered for user mark2: BB702CB61EC2F2F6 RWT28sIetixwu5p3VkrhG8kyV60RxwQ1ycfHjep8UroIdP8DF4JLmGvt while installing dog2@0.0.13. 4 | Would you like to trust this key? 5 | [y/n] [INFO] Importing key "BB702CB61EC2F2F6" for user "mark2" 6 | [INFO] Signature of package dog2@0.0.13 verified! 7 | Package installed successfully to wapm_packages! 8 | Error: There was an error validating the manifest: missing ABI field on module test used by command test-module; an ABI of `wasi` or `emscripten` is required 9 | Successfully published package `test@0.0.0` 10 | [INFO] Publish succeeded, but package was not published because it was run in dry-run mode 11 | -------------------------------------------------------------------------------- /end-to-end-tests/package-fs-mapping.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export RUST_BACKTRACE=1 4 | wapm install -g mark2/dog2@0.0.13 --force-yes 5 | wapm run dog -- data 6 | wapm uninstall -g mark2/dog2 7 | wapm install mark2/dog2@0.0.13 8 | wapm run dog -- data 9 | wapm uninstall mark2/dog2 10 | cp wapm_packages/mark2/dog2@0.0.13/dog.wasm . 11 | echo '[package]\nname="test"\nversion="0.0.0"\ndescription="this is a test"\n[[module]]\nname="test-module"\nsource="dog.wasm"\n[[command]]\nname="test"\nmodule="test-module"\n[fs]\n"wapm_file"="src/bin"' > wapm.toml 12 | wapm run test -- wapm_file 13 | rm dog.wasm 14 | -------------------------------------------------------------------------------- /end-to-end-tests/package-fs-mapping.txt: -------------------------------------------------------------------------------- 1 | [INFO] Installing mark2/dog2@0.0.13 2 | New public key encountered for user mark2: BB702CB61EC2F2F6 RWT28sIetixwu5p3VkrhG8kyV60RxwQ1ycfHjep8UroIdP8DF4JLmGvt while installing dog2@0.0.13. 3 | Would you like to trust this key? 4 | [y/n] [INFO] Importing key "BB702CB61EC2F2F6" for user "mark2" 5 | [INFO] Signature of package dog2@0.0.13 verified! 6 | Global package installed successfully! 7 | "data/gl.txt" 8 | [INFO] Package "mark2/dog2" uninstalled. 9 | [INFO] Installing mark2/dog2@0.0.13 10 | [INFO] Signature of package dog2@0.0.13 verified! 11 | Package installed successfully to wapm_packages! 12 | "data/gl.txt" 13 | [INFO] Package "mark2/dog2" uninstalled. 14 | "wapm_file/wapm.rs" 15 | -------------------------------------------------------------------------------- /end-to-end-tests/validate-global.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # test that the command name is overriden by default 4 | wapm install -g mark2/binary-name-matters@0.0.3 -y 5 | wapm run binary-name-matters 6 | wapm uninstall -g mark2/binary-name-matters 7 | wapm install mark2/binary-name-matters@0.0.3 -y 8 | wapm run binary-name-matters 9 | wapm uninstall mark2/binary-name-matters 10 | 11 | # disable command rename and manually reenable it with `wasmer-extra-flags` 12 | wapm install -g mark2/binary-name-matters-2 -y 13 | wapm run binary-name-matters-2 14 | wapm uninstall -g mark2/binary-name-matters-2 15 | wapm install mark2/binary-name-matters-2 -y 16 | wapm run binary-name-matters-2 17 | wapm uninstall mark2/binary-name-matters-2 18 | -------------------------------------------------------------------------------- /end-to-end-tests/validate-global.txt: -------------------------------------------------------------------------------- 1 | [INFO] Installing mark2/binary-name-matters@0.0.3 2 | Global package installed successfully! 3 | Success: command name correct 4 | Success: fs found 5 | [INFO] Package "mark2/binary-name-matters" uninstalled. 6 | [INFO] Installing mark2/binary-name-matters@0.0.3 7 | Package installed successfully to wapm_packages! 8 | Success: command name correct 9 | Success: fs found 10 | [INFO] Package "mark2/binary-name-matters" uninstalled. 11 | [INFO] Installing Fawadkhan/binary-name-matters-2@0.0.0 12 | Global package installed successfully! 13 | Success: manual override worked 14 | [INFO] Package "mark2/binary-name-matters-2" is not installed. 15 | [INFO] Installing Fawadkhan/binary-name-matters-2@0.0.0 16 | Package installed successfully to wapm_packages! 17 | Success: manual override worked 18 | [INFO] Package "mark2/binary-name-matters-2" is not installed. 19 | -------------------------------------------------------------------------------- /end-to-end-tests/verification.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export RUST_BACKTRACE=1 4 | # redirect stderr to /dev/null so we can capture important stderr 5 | yes no 2> /dev/null | wapm install mark2/dog2@0.0.0 6 | # wc because the date changes 7 | wapm keys list -a 8 | yes 2> /dev/null | wapm install mark2/dog@0.0.4 9 | wapm keys list -a | wc -l | xargs 10 | wapm uninstall mark2/dog 11 | wapm install mark2/dog@0.0.4 12 | wapm install mark2/dog2@0.0.0 13 | rm $HOME/.wasmer/wapm.sqlite &> /dev/null 14 | wapm install syrusakbary/dog3@0.0.0 --force-yes 15 | wapm uninstall syrusakbary/dog3 16 | wapm install syrusakbary/dog3@0.0.0 --force-yes 17 | -------------------------------------------------------------------------------- /end-to-end-tests/verification.txt: -------------------------------------------------------------------------------- 1 | [INFO] Installing mark2/dog2@0.0.0 2 | New public key encountered for user mark2: 3F6D278A36843FFE RWT+P4Q2iidtP7bkcLP4fBTYc9YPpuTKNVIquvPPnsFXrGdecaMKpQ+t while installing dog2@0.0.0. 3 | Would you like to trust this key? 4 | [y/n] Error: Failed to install packages. Could not install package(s). Install aborted: User did not trust key from registry for package dog2@0.0.0 5 | No keys found 6 | [INFO] Installing mark2/dog@0.0.4 7 | New public key encountered for user mark2: 3F6D278A36843FFE RWT+P4Q2iidtP7bkcLP4fBTYc9YPpuTKNVIquvPPnsFXrGdecaMKpQ+t while installing dog@0.0.4. 8 | Would you like to trust this key? 9 | [y/n] [INFO] Importing key "3F6D278A36843FFE" for user "mark2" 10 | [INFO] Signature of package dog@0.0.4 verified! 11 | Package installed successfully to wapm_packages! 12 | 3 13 | [INFO] Package "mark2/dog" uninstalled. 14 | [INFO] Installing mark2/dog@0.0.4 15 | [INFO] Signature of package dog@0.0.4 verified! 16 | Package installed successfully to wapm_packages! 17 | [INFO] Installing mark2/dog2@0.0.0 18 | [INFO] Signature of package dog2@0.0.0 verified! 19 | Package installed successfully to wapm_packages! 20 | [INFO] Installing syrusakbary/dog3@0.0.0 21 | New public key encountered for user mark2: BB702CB61EC2F2F6 RWT28sIetixwu5p3VkrhG8kyV60RxwQ1ycfHjep8UroIdP8DF4JLmGvt while installing dog3@0.0.0. 22 | Would you like to trust this key? 23 | [y/n] [INFO] Importing key "BB702CB61EC2F2F6" for user "mark2" 24 | [INFO] Signature of package dog3@0.0.0 verified! 25 | Package installed successfully to wapm_packages! 26 | [INFO] Package "syrusakbary/dog3" uninstalled. 27 | [INFO] Installing syrusakbary/dog3@0.0.0 28 | [INFO] Signature of package dog3@0.0.0 verified! 29 | Package installed successfully to wapm_packages! 30 | -------------------------------------------------------------------------------- /graphql/queries/get_bindings.graphql: -------------------------------------------------------------------------------- 1 | query GetBindingsQuery ($name: String!, $version: String = "latest") { 2 | packageVersion: getPackageVersion(name:$name, version:$version) { 3 | version 4 | bindings { 5 | id 6 | language 7 | url 8 | __typename 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /graphql/queries/get_interface_version.graphql: -------------------------------------------------------------------------------- 1 | query GetInterfaceVersionQuery ($name: String!, $version: String!) { 2 | interface: getInterfaceVersion(name: $name, version: $version) { 3 | version, 4 | content, 5 | interface { 6 | name, 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /graphql/queries/get_package.graphql: -------------------------------------------------------------------------------- 1 | query GetPackageQuery ($name: String!) { 2 | package: getPackage(name:$name) { 3 | name 4 | private 5 | lastVersion { 6 | version 7 | distribution { 8 | downloadUrl 9 | } 10 | manifest 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /graphql/queries/get_package_by_command.graphql: -------------------------------------------------------------------------------- 1 | query GetPackageByCommandQuery ($commandName: String!) { 2 | getCommand(name: $commandName) { 3 | command 4 | packageVersion { 5 | version 6 | package { 7 | displayName 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /graphql/queries/get_package_version.graphql: -------------------------------------------------------------------------------- 1 | query GetPackageVersionQuery ($name: String!, $version: String) { 2 | packageVersion: getPackageVersion(name:$name, version:$version) { 3 | package { 4 | name 5 | } 6 | version 7 | distribution { 8 | downloadUrl 9 | } 10 | manifest 11 | } 12 | } -------------------------------------------------------------------------------- /graphql/queries/get_packages.graphql: -------------------------------------------------------------------------------- 1 | query GetPackagesQuery ($names: [String!]!) { 2 | package: getPackages(names:$names) { 3 | name 4 | versions { 5 | version 6 | distribution { 7 | downloadUrl 8 | } 9 | signature { 10 | publicKey { 11 | keyId 12 | owner { 13 | username 14 | } 15 | key 16 | uploadedAt 17 | revoked 18 | } 19 | data 20 | createdAt 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /graphql/queries/get_signed_url.graphql: -------------------------------------------------------------------------------- 1 | query GetSignedUrl ($name:String!, $version:String!,$expiresAfterSeconds:Int) { 2 | url: getSignedUrlForPackageUpload(name:$name, version:$version,expiresAfterSeconds:$expiresAfterSeconds) { 3 | url 4 | } 5 | } -------------------------------------------------------------------------------- /graphql/queries/login.graphql: -------------------------------------------------------------------------------- 1 | mutation LoginMutation($username: String!, $password: String!) { 2 | tokenAuth(input: {username: $username, password: $password}) { 3 | refreshToken 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /graphql/queries/publish_package.graphql: -------------------------------------------------------------------------------- 1 | mutation PublishPackageMutation($name: String!, $version: String!, $description: String!, $manifest: String!, $license: String, $licenseFile: String, $readme: String, $fileName:String, $repository:String, $homepage:String, $signature: InputSignature) { 2 | publishPackage(input: { 3 | name: $name, 4 | version: $version, 5 | description: $description, 6 | manifest: $manifest, 7 | license: $license, 8 | licenseFile: $licenseFile, 9 | readme: $readme, 10 | file: $fileName, 11 | repository: $repository, 12 | homepage: $homepage, 13 | signature: $signature, 14 | clientMutationId: "" 15 | }) { 16 | success 17 | packageVersion { 18 | version 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /graphql/queries/publish_package_chunked.graphql: -------------------------------------------------------------------------------- 1 | mutation PublishPackageMutationChunked($name: String!, $version: String!, $description: String!, $manifest: String!, $license: String, $licenseFile: String, $readme: String, $fileName:String, $repository:String, $homepage:String, $signature: InputSignature, $signedUrl:String) { 2 | publishPackage(input: { 3 | name: $name, 4 | version: $version, 5 | description: $description, 6 | manifest: $manifest, 7 | license: $license, 8 | licenseFile: $licenseFile, 9 | readme: $readme, 10 | file: $fileName, 11 | signedUrl: $signedUrl, 12 | repository: $repository, 13 | homepage: $homepage, 14 | signature: $signature, 15 | clientMutationId: "" 16 | }) { 17 | success 18 | packageVersion { 19 | version 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /graphql/queries/publish_public_key.graphql: -------------------------------------------------------------------------------- 1 | mutation PublishPublicKeyMutation($keyId: String!, $key: String!, $verifyingSignatureId: String) { 2 | publishPublicKey(input: { 3 | keyId: $keyId, 4 | key: $key, 5 | verifyingSignatureId: $verifyingSignatureId, 6 | clientMutationId: "" 7 | }) { 8 | success 9 | publicKey { 10 | keyId 11 | key 12 | revokedAt 13 | uploadedAt 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /graphql/queries/search.graphql: -------------------------------------------------------------------------------- 1 | query SearchQuery($query: String!) { 2 | search(query: $query) { 3 | edges { 4 | node { 5 | __typename 6 | ... on PackageVersion { 7 | package { 8 | displayName 9 | } 10 | createdAt 11 | version 12 | description 13 | } 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /graphql/queries/test_if_registry_present.graphql: -------------------------------------------------------------------------------- 1 | query TestIfRegistryPresent { 2 | __typename 3 | } -------------------------------------------------------------------------------- /graphql/queries/wax_get_command.graphql: -------------------------------------------------------------------------------- 1 | query WaxGetCommandQuery($command: String!) { 2 | command: getCommand(name: $command) { 3 | command 4 | module { 5 | abi 6 | source 7 | } 8 | packageVersion { 9 | version 10 | package { 11 | name 12 | displayName 13 | } 14 | signature { 15 | publicKey { 16 | keyId 17 | owner { 18 | username 19 | } 20 | key 21 | uploadedAt 22 | revoked 23 | } 24 | data 25 | } 26 | distribution { 27 | downloadUrl 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /graphql/queries/wax_get_package_command.graphql: -------------------------------------------------------------------------------- 1 | query WaxGetPackageCommandQuery($name: String!) { 2 | package: getPackage(name:$name) { 3 | name 4 | private 5 | lastVersion { 6 | version 7 | distribution { 8 | downloadUrl 9 | } 10 | manifest 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /graphql/queries/whoami.graphql: -------------------------------------------------------------------------------- 1 | query WhoAmIQuery { 2 | viewer { 3 | username 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/wasm-interface/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasmer-wasm-interface" 3 | version = "0.1.0" 4 | authors = ["The Wasmer Engineering Team "] 5 | edition = "2018" 6 | repository = "https://github.com/wasmerio/wapm-cli" 7 | description = "WASM Interface definition and parser" 8 | readme = "README.md" 9 | license = "MIT" 10 | 11 | [dependencies] 12 | bincode = { version = "1", optional = true } 13 | either = "1.5" 14 | nom = "5" 15 | serde = { version = "1", features = ["derive"] } 16 | wasmparser = { version = "0.51.4", optional = true } 17 | 18 | [dev-dependencies] 19 | wat = "1.0" 20 | 21 | [features] 22 | validation = ["wasmparser"] 23 | binary_encode = ["bincode"] 24 | default = ["validation"] 25 | -------------------------------------------------------------------------------- /lib/wasm-interface/README.md: -------------------------------------------------------------------------------- 1 | # Wasm Interface 2 | 3 | This is an experimental crate for validating the imports and exports of a WebAssembly module. 4 | 5 | For the time being, Wasm Interface provides: 6 | 7 | - a convenient text format for specifying the requirements of Wasm modules 8 | - a convenient way to compose interfaces safely (it ensures no conflicts (duplicates are allowed but they must agree)) 9 | - validation that the modules meet the requirements 10 | 11 | ## Syntax example 12 | 13 | Here's the interface for the current version of [WASI](https://github.com/WebAssembly/WASI): 14 | 15 | ```lisp 16 | (interface "wasi_unstable" 17 | ;; Here's a bunch of function imports! 18 | (func (import "wasi_unstable" "args_get") (param i32 i32) (result i32)) 19 | (func (import "wasi_unstable" "args_sizes_get") (param i32 i32) (result i32)) 20 | (func (import "wasi_unstable" "clock_res_get") (param i32 i32) (result i32)) 21 | (func (import "wasi_unstable" "clock_time_get") (param i32 i32 i32) (result i32)) 22 | (func (import "wasi_unstable" "environ_get") (param i32 i32) (result i32)) 23 | (func (import "wasi_unstable" "environ_sizes_get") (param i32 i32) (result i32)) 24 | (func (import "wasi_unstable" "fd_advise") (param i32 i64 i64 i32) (result i32)) 25 | (func (import "wasi_unstable" "fd_allocate") (param i32 i64 i64) (result i32)) 26 | (func (import "wasi_unstable" "fd_close") (param i32) (result i32)) 27 | (func (import "wasi_unstable" "fd_datasync") (param i32) (result i32)) 28 | (func (import "wasi_unstable" "fd_fdstat_get") (param i32 i32) (result i32)) 29 | (func (import "wasi_unstable" "fd_fdstat_set_flags") (param i32 i32) (result i32)) 30 | (func (import "wasi_unstable" "fd_fdstat_set_rights") (param i32 i64 i64) (result i32)) 31 | (func (import "wasi_unstable" "fd_filestat_get") (param i32 i32) (result i32)) 32 | (func (import "wasi_unstable" "fd_filestat_set_size") (param i32 i64) (result i32)) 33 | (func (import "wasi_unstable" "fd_filestat_set_times") (param i32 i64 i64 i32) (result i32)) 34 | (func (import "wasi_unstable" "fd_pread") (param i32 i32 i32 i64 i32) (result i32)) 35 | (func (import "wasi_unstable" "fd_prestat_get") (param i32 i32) (result i32)) 36 | (func (import "wasi_unstable" "fd_prestat_dir_name") (param i32 i32 i32) (result i32)) 37 | (func (import "wasi_unstable" "fd_pwrite") (param i32 i32 i32 i64 i32) (result i32)) 38 | (func (import "wasi_unstable" "fd_read") (param i32 i32 i32 i32) (result i32)) 39 | (func (import "wasi_unstable" "fd_readdir") (param i32 i32 i32 i64 i32) (result i32)) 40 | (func (import "wasi_unstable" "fd_renumber") (param i32 i32) (result i32)) 41 | (func (import "wasi_unstable" "fd_seek") (param i32 i64 i32 i32) (result i32)) 42 | (func (import "wasi_unstable" "fd_sync") (param i32) (result i32)) 43 | (func (import "wasi_unstable" "fd_tell") (param i32 i32) (result i32)) 44 | (func (import "wasi_unstable" "fd_write") (param i32 i32 i32 i32) (result i32)) 45 | (func (import "wasi_unstable" "path_create_directory") (param i32 i32 i32) (result i32)) 46 | (func (import "wasi_unstable" "path_filestat_get") (param i32 i32 i32 i32 i32) (result i32)) 47 | (func (import "wasi_unstable" "path_filestat_set_times") (param i32 i32 i32 i32 i64 i64 i32) (result i32)) 48 | (func (import "wasi_unstable" "path_link") (param i32 i32 i32 i32 i32 i32 i32) (result i32)) 49 | (func (import "wasi_unstable" "path_open") (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32)) 50 | (func (import "wasi_unstable" "path_readlink") (param i32 i32 i32 i32 i32 i32) (result i32)) 51 | (func (import "wasi_unstable" "path_remove_directory") (param i32 i32 i32) (result i32)) 52 | (func (import "wasi_unstable" "path_rename") (param i32 i32 i32 i32 i32 i32) (result i32)) 53 | (func (import "wasi_unstable" "path_symlink") (param i32 i32 i32 i32 i32) (result i32)) 54 | (func (import "wasi_unstable" "path_unlink_file") (param i32 i32 i32) (result i32)) 55 | (func (import "wasi_unstable" "poll_oneoff") (param i32 i32 i32 i32) (result i32)) 56 | (func (import "wasi_unstable" "proc_exit") (param i32)) 57 | (func (import "wasi_unstable" "proc_raise") (param i32) (result i32)) 58 | (func (import "wasi_unstable" "random_get") (param i32 i32) (result i32)) 59 | (func (import "wasi_unstable" "sched_yield") (result i32)) 60 | (func (import "wasi_unstable" "sock_recv") (param i32 i32 i32 i32 i32 i32) (result i32)) 61 | (func (import "wasi_unstable" "sock_send") (param i32 i32 i32 i32 i32) (result i32)) 62 | (func (import "wasi_unstable" "sock_shutdown") (param i32 i32) (result i32)) 63 | ) 64 | ``` 65 | 66 | 67 | Notes: 68 | - multiple `assert-import` and `assert-export` declarations are allowed. 69 | - comments (starts with `;` and ends with a newline) and whitespace are valid between any tokens 70 | 71 | ## Semantics 72 | 73 | All imports used by the module must be specified in the interface. 74 | 75 | All exports in the interface must be exported by the module. 76 | 77 | Thus the module may have additional exports than the interface or fewer imports than the interface specifies and be considered valid. 78 | 79 | 80 | ## Misc 81 | 82 | Wasm Interface serves a slightly different purpose than the proposed WebIDL for Wasm standard, but may be replaced by it in the future if things change. 83 | 84 | Due to an issue with nested closures in Rust, `wasm-interface` can't both compile on stable and have good error reporting. This is being fixed and `wasm-interface` will be updated to have better error handling. 85 | 86 | See the `parser.rs` file for a comment containing the grammar in a BNF style. 87 | 88 | Suggestions, contributions, and thoughts welcome! This is an experiment in the early stages, but we hope to work with the wider community and develop this in cooperation with all interested parties. 89 | -------------------------------------------------------------------------------- /lib/wasm-interface/src/interface.rs: -------------------------------------------------------------------------------- 1 | //! The definition of a WASM interface 2 | 3 | use crate::interface_matcher::InterfaceMatcher; 4 | use serde::{Deserialize, Serialize}; 5 | use std::collections::{hash_map::Entry, HashMap, HashSet}; 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] 8 | pub struct Interface { 9 | /// The name the interface gave itself 10 | pub name: Option, 11 | /// Things that the module can import 12 | pub imports: HashMap<(String, String), Import>, 13 | /// Things that the module must export 14 | pub exports: HashMap, 15 | } 16 | 17 | impl Interface { 18 | pub fn merge(&self, other: Interface) -> Result { 19 | let mut base = self.clone(); 20 | 21 | for (key, val) in other.imports { 22 | match base.imports.entry(key) { 23 | Entry::Occupied(e) if *e.get() != val => { 24 | let (namespace, name) = e.key(); 25 | let original_value = e.get(); 26 | return Err(format!("Conflict detected: the import \"{namespace}\" \"{name}\" was found but the definitions were different: {original_value:?} {val:?}")); 27 | } 28 | Entry::Occupied(_) => { 29 | // it's okay for the imported items to be the same. 30 | } 31 | Entry::Vacant(e) => { 32 | e.insert(val); 33 | } 34 | }; 35 | } 36 | 37 | for (key, val) in other.exports { 38 | match base.exports.entry(key) { 39 | Entry::Occupied(e) if *e.get() != val => { 40 | let name = e.key(); 41 | let original_value = e.get(); 42 | return Err(format!("Conflict detected: the key \"{name}\" was found in exports but the definitions were different: {original_value:?} {val:?}")); 43 | } 44 | Entry::Occupied(_) => { 45 | // it's okay for the exported items to be the same. 46 | } 47 | Entry::Vacant(e) => { 48 | e.insert(val); 49 | } 50 | }; 51 | } 52 | 53 | Ok(base) 54 | } 55 | 56 | pub fn create_interface_matcher(&self) -> InterfaceMatcher { 57 | let mut namespaces = HashSet::new(); 58 | let mut namespace_imports: HashMap> = 59 | HashMap::with_capacity(self.imports.len()); 60 | let mut exports = HashSet::with_capacity(self.exports.len()); 61 | 62 | for (_, import) in self.imports.iter() { 63 | match import { 64 | Import::Func { namespace, .. } | Import::Global { namespace, .. } => { 65 | if !namespaces.contains(namespace) { 66 | namespaces.insert(namespace.clone()); 67 | } 68 | let ni = namespace_imports.entry(namespace.clone()).or_default(); 69 | ni.insert(import.clone()); 70 | } 71 | } 72 | } 73 | for (_, export) in self.exports.iter() { 74 | exports.insert(export.clone()); 75 | } 76 | InterfaceMatcher { 77 | namespaces, 78 | namespace_imports, 79 | exports, 80 | } 81 | } 82 | } 83 | 84 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 85 | pub enum Import { 86 | Func { 87 | namespace: String, 88 | name: String, 89 | params: Vec, 90 | result: Vec, 91 | }, 92 | Global { 93 | namespace: String, 94 | name: String, 95 | var_type: WasmType, 96 | }, 97 | } 98 | 99 | impl Import { 100 | pub fn format_key(ns: &str, name: &str) -> (String, String) { 101 | (ns.to_string(), name.to_string()) 102 | } 103 | 104 | /// Get the key used to look this import up in the Interface's import hashmap 105 | pub fn get_key(&self) -> (String, String) { 106 | match self { 107 | Import::Func { 108 | namespace, name, .. 109 | } 110 | | Import::Global { 111 | namespace, name, .. 112 | } => Self::format_key(namespace, name), 113 | } 114 | } 115 | } 116 | 117 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 118 | pub enum Export { 119 | Func { 120 | name: String, 121 | params: Vec, 122 | result: Vec, 123 | }, 124 | Global { 125 | name: String, 126 | var_type: WasmType, 127 | }, 128 | } 129 | 130 | impl Export { 131 | pub fn format_key(name: &str) -> String { 132 | name.to_string() 133 | } 134 | 135 | /// Get the key used to look this export up in the Interface's export hashmap 136 | pub fn get_key(&self) -> String { 137 | match self { 138 | Export::Func { name, .. } | Export::Global { name, .. } => Self::format_key(name), 139 | } 140 | } 141 | } 142 | 143 | /// Primitive wasm type 144 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 145 | pub enum WasmType { 146 | I32, 147 | I64, 148 | F32, 149 | F64, 150 | } 151 | 152 | impl std::fmt::Display for WasmType { 153 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 154 | write!( 155 | f, 156 | "{}", 157 | match self { 158 | WasmType::I32 => "i32", 159 | WasmType::I64 => "i64", 160 | WasmType::F32 => "f32", 161 | WasmType::F64 => "f64", 162 | } 163 | ) 164 | } 165 | } 166 | 167 | #[cfg(test)] 168 | mod test { 169 | use crate::parser; 170 | 171 | #[test] 172 | fn merging_works() { 173 | let interface1_src = 174 | r#"(interface (func (import "env" "plus_one") (param i32) (result i32)))"#; 175 | let interface2_src = 176 | r#"(interface (func (import "env" "plus_one") (param i64) (result i64)))"#; 177 | let interface3_src = 178 | r#"(interface (func (import "env" "times_two") (param i64) (result i64)))"#; 179 | let interface4_src = 180 | r#"(interface (func (import "env" "times_two") (param i64 i64) (result i64)))"#; 181 | let interface5_src = r#"(interface (func (export "empty_bank_account") (param) (result)))"#; 182 | let interface6_src = 183 | r#"(interface (func (export "empty_bank_account") (param) (result i64)))"#; 184 | 185 | let interface1 = parser::parse_interface(interface1_src).unwrap(); 186 | let interface2 = parser::parse_interface(interface2_src).unwrap(); 187 | let interface3 = parser::parse_interface(interface3_src).unwrap(); 188 | let interface4 = parser::parse_interface(interface4_src).unwrap(); 189 | let interface5 = parser::parse_interface(interface5_src).unwrap(); 190 | let interface6 = parser::parse_interface(interface6_src).unwrap(); 191 | 192 | assert!(interface1.merge(interface2.clone()).is_err()); 193 | assert!(interface2.merge(interface1.clone()).is_err()); 194 | assert!(interface1.merge(interface3.clone()).is_ok()); 195 | assert!(interface2.merge(interface3.clone()).is_ok()); 196 | assert!(interface3.merge(interface2).is_ok()); 197 | assert!( 198 | interface1.merge(interface1.clone()).is_ok(), 199 | "exact matches are accepted" 200 | ); 201 | assert!(interface3.merge(interface4).is_err()); 202 | assert!(interface5.merge(interface5.clone()).is_ok()); 203 | assert!(interface5.merge(interface6).is_err()); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /lib/wasm-interface/src/interface_matcher.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::{HashMap, HashSet}; 3 | 4 | use crate::interface::{Export, Import}; 5 | 6 | /// A struct containing data for more efficient matching. 7 | /// 8 | /// An ideal use case for this is to parse [`Interface`]s at compile time, 9 | /// create [`InterfaceMatcher`]s, and store them as bytes so that they 10 | /// can be efficiently loaded at runtime for matching. 11 | #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] 12 | pub struct InterfaceMatcher { 13 | pub namespaces: HashSet, 14 | pub namespace_imports: HashMap>, 15 | pub exports: HashSet, 16 | } 17 | 18 | #[cfg(feature = "binary_encode")] 19 | impl InterfaceMatcher { 20 | /// Store the matcher as bytes to avoid reparsing 21 | fn into_bytes(&self) -> Vec { 22 | bincode::serialize(self).expect("Could not serialize InterfaceMatcher") 23 | } 24 | 25 | /// Load the matcher from bytes to avoid reparsing 26 | fn from_bytes(bytes: &[u8]) -> Option { 27 | bincode::deserialize(bytes).ok() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/wasm-interface/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![type_length_limit = "5795522"] 2 | //! Definition and parsing of wasm interfaces 3 | //! 4 | //! wasm interfaces ensure wasm modules conform to a specific shape 5 | //! they do this by asserting on the imports and exports of the module. 6 | 7 | pub mod interface; 8 | pub mod interface_matcher; 9 | pub mod parser; 10 | #[cfg(feature = "validation")] 11 | pub mod validate; 12 | 13 | pub use interface::*; 14 | -------------------------------------------------------------------------------- /src/abi.rs: -------------------------------------------------------------------------------- 1 | pub use wapm_toml::Abi; 2 | -------------------------------------------------------------------------------- /src/commands/add.rs: -------------------------------------------------------------------------------- 1 | //! Code pertaining to the `add` subcommand: it adds dependencies to 2 | //! the manifest without installing 3 | 4 | use crate::graphql::execute_query; 5 | use graphql_client::*; 6 | use thiserror::Error; 7 | 8 | use crate::data::manifest::Manifest; 9 | use structopt::StructOpt; 10 | 11 | /// Options for the `add` subcommand 12 | #[derive(StructOpt, Debug)] 13 | pub struct AddOpt { 14 | packages: Vec, 15 | } 16 | 17 | #[derive(GraphQLQuery)] 18 | #[graphql( 19 | schema_path = "graphql/schema.graphql", 20 | query_path = "graphql/queries/get_package_version.graphql", 21 | response_derives = "Debug" 22 | )] 23 | pub(crate) struct GetPackageVersionQuery; 24 | 25 | #[derive(Debug, Error)] 26 | enum AddError { 27 | #[error("There were problems adding packages")] 28 | GenericError, 29 | #[error("Could not find a manifest in the current directory, try running `wapm init`")] 30 | NoManifest, 31 | #[error("No packages listed to add")] 32 | ArgumentsRequired, 33 | } 34 | 35 | /// Run the add command 36 | pub fn add(options: AddOpt) -> anyhow::Result<()> { 37 | let mut error = false; 38 | let mut manifest: Manifest = { 39 | let cur_dir = crate::config::Config::get_current_dir()?; 40 | Manifest::find_in_directory(cur_dir).map_err(|_| AddError::NoManifest)? 41 | }; 42 | 43 | if options.packages.is_empty() { 44 | return Err(AddError::ArgumentsRequired.into()); 45 | } 46 | 47 | for (package_name, maybe_version) in options.packages.into_iter().map(|package_str| { 48 | if package_str.contains('@') { 49 | let mut p = package_str.split('@'); 50 | let package_name = p.next().unwrap(); 51 | let package_version = p.next().unwrap(); 52 | (package_name.to_string(), Some(package_version.to_string())) 53 | } else { 54 | (package_str, None) 55 | } 56 | }) { 57 | let q = GetPackageVersionQuery::build_query(get_package_version_query::Variables { 58 | name: package_name.clone(), 59 | version: maybe_version.clone(), 60 | }); 61 | let response: get_package_version_query::ResponseData = execute_query(&q)?; 62 | 63 | if let Some(pv) = response.package_version { 64 | info!("Adding {}@{}", &package_name, &pv.version); 65 | manifest.add_dependency(package_name, pv.version); 66 | } else { 67 | error = true; 68 | if let Some(ver) = maybe_version { 69 | error!("Package \"{}@{}\" was not found", &package_name, &ver); 70 | } else { 71 | error!("Package \"{}\" was not found", &package_name); 72 | } 73 | } 74 | } 75 | 76 | manifest.save()?; 77 | 78 | if error { 79 | Err(AddError::GenericError.into()) 80 | } else { 81 | println!("Packages successfully added!"); 82 | Ok(()) 83 | } 84 | } 85 | 86 | #[cfg(feature = "integration_tests")] 87 | impl AddOpt { 88 | pub fn new(packages: Vec) -> Self { 89 | AddOpt { packages } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/commands/bin.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::data::manifest::PACKAGES_DIR_NAME; 3 | use crate::dataflow::bin_script::BIN_DIR_NAME; 4 | use structopt::StructOpt; 5 | use thiserror::Error; 6 | 7 | #[derive(StructOpt, Debug)] 8 | pub struct BinOpt { 9 | /// Get the global .bin dir 10 | #[structopt(short = "g", long = "global")] 11 | pub global: bool, 12 | } 13 | 14 | #[derive(Clone, Debug, Error)] 15 | pub enum BinError { 16 | #[error("The directory \"{0}\" does not contain wapm packages.")] 17 | NotWapmProjectDir(String), 18 | } 19 | 20 | pub fn bin(options: BinOpt) -> anyhow::Result<()> { 21 | let mut root_dir = match options.global { 22 | true => Config::get_globals_directory()?, 23 | false => Config::get_current_dir()?, 24 | }; 25 | root_dir.push(PACKAGES_DIR_NAME); 26 | 27 | // for wapm bin -g, display the global path even if it does not exist 28 | // otherwise error if the bin directory does not exist in the local directory 29 | if !root_dir.exists() && !options.global { 30 | return Err(BinError::NotWapmProjectDir(root_dir.to_string_lossy().to_string()).into()); 31 | } 32 | 33 | root_dir.push(BIN_DIR_NAME); 34 | let bin_dir = root_dir; 35 | println!("{}", bin_dir.display()); 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/completions.rs: -------------------------------------------------------------------------------- 1 | use structopt::{clap::AppSettings, clap::Shell, StructOpt}; 2 | 3 | #[derive(StructOpt, Debug)] 4 | #[structopt(setting = AppSettings::Hidden)] 5 | pub struct CompletionOpt { 6 | /// The shell to generate the completions script for 7 | #[structopt(name = "SHELL", hidden = true, parse(try_from_str))] 8 | pub shell: Shell, 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/config.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{get, set, Config}; 2 | use structopt::StructOpt; 3 | 4 | #[derive(StructOpt, Debug)] 5 | pub enum ConfigOpt { 6 | #[structopt(name = "set")] 7 | /// Sets a key 8 | Set(ConfigKeyValue), 9 | 10 | #[structopt(name = "get")] 11 | /// Gets a key 12 | Get(ConfigKey), 13 | } 14 | 15 | #[derive(StructOpt, Debug)] 16 | pub struct ConfigKeyValue { 17 | #[structopt(parse(from_str))] 18 | key: String, 19 | 20 | #[structopt(parse(from_str))] 21 | value: String, 22 | } 23 | 24 | #[derive(StructOpt, Debug)] 25 | pub struct ConfigKey { 26 | #[structopt(parse(from_str))] 27 | key: String, 28 | } 29 | 30 | pub fn config(config_opt: ConfigOpt) -> anyhow::Result<()> { 31 | let mut config = Config::from_file()?; 32 | match config_opt { 33 | ConfigOpt::Set(ConfigKeyValue { key, value }) => set(&mut config, key, value), 34 | ConfigOpt::Get(ConfigKey { key }) => { 35 | let value = get(&mut config, key)?; 36 | println!("{}", value); 37 | Ok(()) 38 | } 39 | } 40 | } 41 | 42 | /// Extras for testing 43 | #[cfg(feature = "integration_tests")] 44 | impl ConfigOpt { 45 | pub fn set(key: String, value: String) -> Self { 46 | ConfigOpt::Set(ConfigKeyValue { key, value }) 47 | } 48 | 49 | pub fn get(key: String) -> Self { 50 | ConfigOpt::Get(ConfigKey { key }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/commands/init.rs: -------------------------------------------------------------------------------- 1 | use crate::init; 2 | use structopt::StructOpt; 3 | 4 | #[derive(StructOpt, Debug)] 5 | pub struct InitOpt { 6 | /// Agree to all prompts. Useful for non-interactive uses 7 | #[structopt(long = "force-yes", short = "y")] 8 | force_yes: bool, 9 | /// Initial project name to specify (`wapm init myproject`) 10 | project_name: Option, 11 | } 12 | 13 | pub fn init(opt: InitOpt) -> anyhow::Result<()> { 14 | let current_directory = crate::config::Config::get_current_dir()?; 15 | init::init(current_directory, opt.force_yes, opt.project_name) 16 | } 17 | 18 | #[cfg(feature = "integration_tests")] 19 | impl InitOpt { 20 | pub fn new(force_yes: bool) -> Self { 21 | InitOpt { 22 | force_yes, 23 | project_name: None, 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/list.rs: -------------------------------------------------------------------------------- 1 | //! Subcommand for inspecting installed packages and commands 2 | 3 | use crate::config; 4 | use crate::data::lock::lockfile::{CommandMap, ModuleMap}; 5 | use crate::dataflow::lockfile_packages::LockfileResult; 6 | use prettytable::{format, Table}; 7 | use std::fmt::Write as _; 8 | use structopt::StructOpt; 9 | 10 | #[derive(StructOpt, Debug)] 11 | pub struct ListOpt { 12 | /// List just the globally installed packages 13 | #[structopt(short = "g", long = "global")] 14 | global: bool, 15 | 16 | /// List both locally and globally installed packages 17 | #[structopt(short = "a", long = "all")] 18 | all: bool, 19 | } 20 | 21 | pub fn list(options: ListOpt) -> anyhow::Result<()> { 22 | let mut local = false; 23 | let mut global = false; 24 | match (options.global, options.all) { 25 | (_, true) => { 26 | local = true; 27 | global = true; 28 | } 29 | (true, false) => { 30 | global = true; 31 | } 32 | (false, false) => { 33 | local = true; 34 | } 35 | } 36 | let local_start_value = local; 37 | 38 | let mut handle = String::new(); 39 | if local { 40 | let cwd = crate::config::Config::get_current_dir()?; 41 | match LockfileResult::find_in_directory(cwd) { 42 | LockfileResult::Lockfile(lockfile) => { 43 | let has_modules = !lockfile.modules.is_empty(); 44 | let has_commands = !lockfile.commands.is_empty(); 45 | // unset local if there's nothing to list 46 | local = has_modules | has_commands; 47 | if has_modules { 48 | writeln!(handle, "LOCAL PACKAGES:")?; 49 | write!(handle, "{}", create_module_ascii_table(&lockfile.modules))?; 50 | } 51 | if has_modules && has_commands { 52 | writeln!(handle)?; 53 | } 54 | if has_commands { 55 | writeln!(handle, "LOCAL COMMANDS:")?; 56 | write!(handle, "{}", create_command_ascii_table(&lockfile.commands))?; 57 | } 58 | } 59 | LockfileResult::NoLockfile => { 60 | if !global { 61 | writeln!(handle, "No packages in current directory")?; 62 | return Ok(()); 63 | } 64 | local = false; 65 | } 66 | LockfileResult::LockfileError(e) => { 67 | return Err(anyhow!( 68 | "Failed to read lock file in current directory: {}", 69 | e 70 | )); 71 | } 72 | } 73 | } 74 | 75 | if local && global { 76 | writeln!(handle)?; 77 | } 78 | 79 | if global { 80 | let global_path = config::Config::get_globals_directory()?; 81 | match LockfileResult::find_in_directory(global_path) { 82 | LockfileResult::Lockfile(lockfile) => { 83 | let has_modules = !lockfile.modules.is_empty(); 84 | let has_commands = !lockfile.commands.is_empty(); 85 | // unset global if there's nothing to list 86 | global = has_modules || has_commands; 87 | if has_modules { 88 | writeln!(handle, "GLOBAL PACKAGES:")?; 89 | write!(handle, "{}", create_module_ascii_table(&lockfile.modules))?; 90 | } 91 | if has_modules && has_commands { 92 | writeln!(handle)?; 93 | } 94 | if has_commands { 95 | writeln!(handle, "GLOBAL COMMANDS:")?; 96 | write!(handle, "{}", create_command_ascii_table(&lockfile.commands))?; 97 | } 98 | } 99 | LockfileResult::NoLockfile => { 100 | if !local_start_value { 101 | writeln!(handle, "No global packages")?; 102 | return Ok(()); 103 | } 104 | global = false; 105 | } 106 | LockfileResult::LockfileError(e) => { 107 | return Err(anyhow!( 108 | "Failed to read lock file in current directory: {}", 109 | e 110 | )); 111 | } 112 | } 113 | } 114 | 115 | if handle.is_empty() { 116 | // we should only have an empty string if we unset both global and local 117 | debug_assert!(!global); 118 | debug_assert!(!local); 119 | println!("No packages found"); 120 | } else { 121 | print!("{}", handle); 122 | } 123 | Ok(()) 124 | } 125 | 126 | fn create_module_ascii_table(modules: &ModuleMap) -> String { 127 | let mut table = Table::new(); 128 | table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 129 | table.add_row(row!["PACKAGE", "VERSION", "MODULE", "ABI"]); 130 | for (package_name, version_info) in modules.iter() { 131 | for (version_number, module_info) in version_info.iter() { 132 | for (module_name, module) in module_info.iter() { 133 | table.add_row(row![package_name, version_number, module_name, module.abi,]); 134 | } 135 | } 136 | } 137 | format!("{}", table) 138 | } 139 | 140 | fn create_command_ascii_table(commands: &CommandMap) -> String { 141 | let mut table = Table::new(); 142 | table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 143 | table.add_row(row!["COMMAND", "PACKAGE", "VERSION"]); 144 | for (command_name, command) in commands.iter() { 145 | table.add_row(row![ 146 | command_name, 147 | command.package_name, 148 | command.package_version, 149 | ]); 150 | } 151 | format!("{}", table) 152 | } 153 | -------------------------------------------------------------------------------- /src/commands/login.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{Config, UpdateRegistry}; 2 | use crate::graphql::execute_query; 3 | use rpassword_wasi as rpassword; 4 | use std::io::prelude::*; 5 | use std::io::{stdin, stdout}; 6 | use structopt::StructOpt; 7 | 8 | use graphql_client::*; 9 | 10 | #[derive(StructOpt, Debug)] 11 | pub struct LoginOpt { 12 | /// Provide the token 13 | token: Option, 14 | /// Whether the CLI should print the username and registry 15 | #[structopt(long = "quiet")] 16 | quiet: bool, 17 | /// Username 18 | #[structopt(long)] 19 | user: Option, 20 | /// Password 21 | #[structopt(long)] 22 | password: Option, 23 | } 24 | 25 | #[derive(GraphQLQuery)] 26 | #[graphql( 27 | schema_path = "graphql/schema.graphql", 28 | query_path = "graphql/queries/login.graphql", 29 | response_derives = "Debug" 30 | )] 31 | struct LoginMutation; 32 | 33 | pub fn login(login_options: LoginOpt) -> anyhow::Result<()> { 34 | if let Some(token) = login_options.token { 35 | let mut config = Config::from_file()?; 36 | config.registry.set_login_token_for_registry( 37 | &config.registry.get_current_registry(), 38 | &token, 39 | UpdateRegistry::Update, 40 | ); 41 | config.save()?; 42 | if !login_options.quiet { 43 | if let Some(s) = crate::util::get_username().ok().and_then(|o| o) { 44 | println!("Login for WAPM user {:?} saved", s); 45 | } else { 46 | println!("Login for WAPM user saved"); 47 | } 48 | } 49 | return Ok(()); 50 | } 51 | 52 | let username = if let Some(u) = login_options.user.as_ref() { 53 | u.to_string() 54 | } else { 55 | print!("Username: "); 56 | stdout().flush().expect("Could not flush stdout"); 57 | 58 | let buffer = &mut String::new(); 59 | stdin().read_line(buffer)?; 60 | buffer.trim_end().to_string() 61 | }; 62 | 63 | let password = if let Some(p) = login_options.password.as_ref() { 64 | p.to_string() 65 | } else { 66 | rpassword::prompt_password("Password: ").expect("Can't get password") 67 | }; 68 | 69 | let q = LoginMutation::build_query(login_mutation::Variables { username, password }); 70 | let response: login_mutation::ResponseData = execute_query(&q)?; 71 | let token = match response.token_auth { 72 | Some(token_auth) => Some(token_auth.refresh_token), 73 | None => None, 74 | }; 75 | if let Some(token) = token { 76 | // Save the token 77 | let mut config = Config::from_file()?; 78 | config.registry.set_login_token_for_registry( 79 | &config.registry.get_current_registry(), 80 | &token, 81 | UpdateRegistry::Update, 82 | ); 83 | config.save()?; 84 | 85 | if !login_options.quiet { 86 | if let Some(u) = crate::util::get_username().ok().and_then(|o| o) { 87 | println!( 88 | "Successfully logged into registry {:?} as user {:?}", 89 | config.registry.get_current_registry(), 90 | u 91 | ); 92 | } 93 | } 94 | } 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /src/commands/logout.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | 3 | pub fn logout() -> anyhow::Result<()> { 4 | let mut config = Config::from_file()?; 5 | config.registry.clear_current_registry_token(); 6 | config.save()?; 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | //! List of exported subcommands for use by wapm 2 | 3 | mod add; 4 | mod bin; 5 | mod completions; 6 | mod config; 7 | mod execute; 8 | mod init; 9 | mod install; 10 | #[cfg(feature = "full")] 11 | mod keys; 12 | #[cfg(feature = "full")] 13 | mod list; 14 | mod login; 15 | mod logout; 16 | #[cfg(feature = "full")] 17 | mod publish; 18 | mod remove; 19 | mod run; 20 | #[cfg(feature = "full")] 21 | mod search; 22 | mod uninstall; 23 | mod validate; 24 | mod whoami; 25 | 26 | pub use self::add::{add, AddOpt}; 27 | pub use self::bin::{bin, BinOpt}; 28 | pub use self::completions::CompletionOpt; 29 | pub use self::config::{config, ConfigOpt}; 30 | pub use self::execute::{execute, ExecuteOpt}; 31 | pub use self::init::{init, InitOpt}; 32 | pub use self::install::{install, InstallOpt}; 33 | #[cfg(feature = "full")] 34 | pub use self::keys::{keys, KeyOpt}; 35 | #[cfg(feature = "full")] 36 | pub use self::list::{list, ListOpt}; 37 | pub use self::login::{login, LoginOpt}; 38 | pub use self::logout::logout; 39 | #[cfg(feature = "full")] 40 | pub use self::publish::{publish, PublishOpt}; 41 | pub use self::remove::{remove, RemoveOpt}; 42 | pub use self::run::{run, RunOpt}; 43 | #[cfg(feature = "full")] 44 | pub use self::search::{search, SearchOpt}; 45 | pub use self::uninstall::{uninstall, UninstallOpt}; 46 | pub use self::validate::{validate, ValidateOpt}; 47 | pub use self::whoami::whoami; 48 | -------------------------------------------------------------------------------- /src/commands/package/compress.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::package::header::CompressionType; 2 | 3 | /// A general way to talk about compression algorithms. This allows wapm package to use different 4 | /// kinds of compression when storing assets in the wasm. 5 | pub trait Compress { 6 | fn compress(uncompressed_data: Vec) -> anyhow::Result>; 7 | fn compression_type() -> CompressionType; 8 | } 9 | 10 | static ZSTD_COMPRESSION_LEVEL: i32 = 3; 11 | 12 | /// [zstd compression](https://facebook.github.io/zstd/) 13 | /// Construction is disallowed. 14 | pub struct ZStdCompression { 15 | _private: (), 16 | } 17 | 18 | impl Compress for ZStdCompression { 19 | fn compress(uncompressed_data: Vec) -> anyhow::Result> { 20 | zstd::stream::encode_all(&uncompressed_data[..], ZSTD_COMPRESSION_LEVEL) 21 | .map_err(|e| e.into()) 22 | } 23 | 24 | fn compression_type() -> CompressionType { 25 | CompressionType::ZSTD 26 | } 27 | } 28 | 29 | /// A non-compression Compression! Useful for unit tests. 30 | /// Construction is disallowed. 31 | #[cfg(test)] 32 | pub struct NoCompression { 33 | _private: (), 34 | } 35 | 36 | #[cfg(test)] 37 | impl Compress for NoCompression { 38 | fn compress(uncompressed_data: Vec) -> anyhow::Result> { 39 | Ok(uncompressed_data) 40 | } 41 | 42 | fn compression_type() -> CompressionType { 43 | CompressionType::NONE 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/commands/package/header.rs: -------------------------------------------------------------------------------- 1 | /// Represents the version of this header schema. 2 | #[repr(u8)] 3 | #[derive(Debug, PartialEq)] 4 | pub enum HeaderVersion { 5 | Version1 = 1, 6 | } 7 | 8 | /// Represents the compression type of the file data. Only Zstd or no-compression is supported. 9 | #[repr(u8)] 10 | #[derive(Debug, PartialEq)] 11 | pub enum CompressionType { 12 | #[allow(dead_code)] 13 | NONE = 0, 14 | ZSTD = 1, 15 | } 16 | 17 | /// Represents the type of archive. The only supported archive is the Tar format. 18 | #[repr(u8)] 19 | #[derive(Debug, PartialEq)] 20 | pub enum ArchiveType { 21 | TAR = 0, 22 | } 23 | 24 | /// A serializing function of creating the header buffer from parts. 25 | pub fn header_to_bytes( 26 | version: HeaderVersion, 27 | compression_type: CompressionType, 28 | archive_type: ArchiveType, 29 | ) -> [u8; 4] { 30 | [version as _, compression_type as _, archive_type as _, 0] 31 | } 32 | 33 | #[cfg(test)] 34 | mod test { 35 | use crate::commands::package::header::{ 36 | header_to_bytes, ArchiveType, CompressionType, HeaderVersion, 37 | }; 38 | 39 | #[test] 40 | fn does_it_work() { 41 | let bytes = header_to_bytes( 42 | HeaderVersion::Version1, 43 | CompressionType::NONE, 44 | ArchiveType::TAR, 45 | ); 46 | assert_eq!(bytes, [1, 0, 0, 0]); 47 | let bytes = header_to_bytes( 48 | HeaderVersion::Version1, 49 | CompressionType::ZSTD, 50 | ArchiveType::TAR, 51 | ); 52 | assert_eq!(bytes, [1, 1, 0, 0]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/commands/package/mod.rs: -------------------------------------------------------------------------------- 1 | mod assets; 2 | mod compress; 3 | mod header; 4 | mod options; 5 | 6 | use crate::commands::package::assets::Assets; 7 | use crate::commands::package::compress::ZStdCompression; 8 | pub use crate::commands::package::options::PackageOpt; 9 | use crate::manifest::Manifest; 10 | use std::env; 11 | use std::path::PathBuf; 12 | use thiserror::Error; 13 | 14 | pub fn package(package_options: PackageOpt) -> anyhow::Result<()> { 15 | let (manifest, base_path) = match package_options.manifest_file_path { 16 | Some(manifest_path) => { 17 | let manifest = Manifest::open(&manifest_path)?; 18 | let base_path = manifest_path.parent().unwrap().to_path_buf(); 19 | (manifest, base_path) 20 | } 21 | None => { 22 | let manifest = Manifest::find_in_current_directory()?; 23 | let base_path = crate::config::Config::get_current_dir()?; 24 | (manifest, base_path) 25 | } 26 | }; 27 | 28 | let wapm_module = manifest.module.as_ref().ok_or(PackageError::NoModule)?; 29 | 30 | // fail early if missing required source 31 | let source = manifest 32 | .source_path() 33 | .map_err(|_| PackageError::MissingSource)?; 34 | 35 | // add assets from CLI pattern 36 | // let base_manifest_path = manifest_path_buf.parent().unwrap(); 37 | let mut assets = Assets::new(); 38 | assets.add_asset_from_pattern(&base_path, package_options.assets)?; 39 | // add assets from manifest if they exist 40 | if let Some(table) = &wapm_module.fs { 41 | for pair in table.iter() { 42 | let local_path = PathBuf::from(pair.0.as_str()); 43 | // assume there is a virtual path_string for now 44 | let virtual_path_string = pair.1.as_str().unwrap(); 45 | let local_path = base_path.join(local_path); 46 | assets.add_asset(&local_path, virtual_path_string)?; 47 | } 48 | } 49 | 50 | // create a walrus module from the source file 51 | let mut module = walrus::Module::from_file(source)?; 52 | 53 | // insert a custom section with assets if we have one using zstd compression 54 | if let Some(custom_section) = assets.into_custom_section::() { 55 | module.custom.push(custom_section); 56 | } 57 | 58 | // because this possibly does not exist yet, simply join to the base path if it is relative 59 | let module_path = manifest.module_path()?; 60 | let module_path = if module_path.is_relative() { 61 | base_path.join(module_path) 62 | } else { 63 | module_path 64 | }; 65 | 66 | // publish the wasm module 67 | module.emit_wasm_file(module_path) 68 | } 69 | 70 | #[derive(Debug, Error)] 71 | pub enum PackageError { 72 | #[error("Missing source.")] 73 | MissingSource, 74 | #[error("Cannot package without a module.")] 75 | NoModule, 76 | } 77 | -------------------------------------------------------------------------------- /src/commands/package/options.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use structopt::StructOpt; 3 | 4 | /// This command produces a wasm package from a manifest file (wapm.toml). By default, this command 5 | /// looks in the current directory for the manifest file. One may also pass the path to the file 6 | /// with a flag. 7 | #[derive(Debug, StructOpt)] 8 | #[structopt(name = "package", about = "Bundle a package with assets.")] 9 | pub struct PackageOpt { 10 | /// Path to the manifest file (wasmer.toml) for the wasm package. 11 | #[structopt(short = "m", long = "manifest-path", parse(from_os_str))] 12 | pub manifest_file_path: Option, 13 | /// Assets to be bundled in the wasm package. This is a comma delimited list of patterns 14 | /// e.g. `foo.txt:foo.txt,bar.txt:other/place/bar.txt`. 15 | #[structopt(short = "a", long = "assets", raw(multiple = "true"))] 16 | pub assets: Vec, 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/remove.rs: -------------------------------------------------------------------------------- 1 | //! Code pertaining to the `remove` subcommand: it removes dependencies 2 | //! from the manifest. 3 | 4 | use crate::data::manifest::Manifest; 5 | use structopt::StructOpt; 6 | use thiserror::Error; 7 | 8 | /// Options for the `remove` subcommand 9 | #[derive(StructOpt, Debug)] 10 | pub struct RemoveOpt { 11 | packages: Vec, 12 | } 13 | 14 | #[derive(Debug, Error)] 15 | enum RemoveError { 16 | #[error("There were problems removing packages")] 17 | GenericError, 18 | #[error("No packages to remove; could not find a manifest in the current directory")] 19 | NoManifest, 20 | #[error("No packages listed to remove")] 21 | ArgumentsRequired, 22 | } 23 | 24 | /// Run the remove command 25 | pub fn remove(options: RemoveOpt) -> anyhow::Result<()> { 26 | let mut error = false; 27 | let mut manifest: Manifest = { 28 | let cur_dir = crate::config::Config::get_current_dir()?; 29 | Manifest::find_in_directory(cur_dir).map_err(|_| RemoveError::NoManifest)? 30 | }; 31 | 32 | if options.packages.is_empty() { 33 | return Err(RemoveError::ArgumentsRequired.into()); 34 | } 35 | 36 | for package_name in options.packages { 37 | if package_name.contains('@') { 38 | error = true; 39 | error!( 40 | "`wapm remove` can not remove specific versions. Try to remove \"{}\" again without the version", 41 | package_name 42 | ); 43 | continue; 44 | } 45 | 46 | if manifest.remove_dependency(&package_name).is_some() { 47 | info!("Removing \"{}\"", &package_name); 48 | } 49 | } 50 | 51 | manifest.save()?; 52 | 53 | if error { 54 | Err(RemoveError::GenericError.into()) 55 | } else { 56 | println!("Packages successfully removed!"); 57 | Ok(()) 58 | } 59 | } 60 | 61 | #[cfg(feature = "integration_tests")] 62 | impl RemoveOpt { 63 | pub fn new(packages: Vec) -> Self { 64 | RemoveOpt { packages } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/commands/search.rs: -------------------------------------------------------------------------------- 1 | //! Code pertaining to the `search` subcommand, which queries the server about 2 | //! the specified package. 3 | 4 | use crate::graphql::execute_query; 5 | 6 | use graphql_client::*; 7 | 8 | use prettytable::format; 9 | use prettytable::Table; 10 | use structopt::StructOpt; 11 | 12 | /// Options for the `search` subcommand 13 | #[derive(StructOpt, Debug)] 14 | pub struct SearchOpt { 15 | #[structopt(parse(from_str))] 16 | query: String, 17 | } 18 | 19 | type DateTime = String; 20 | 21 | #[derive(GraphQLQuery)] 22 | #[graphql( 23 | schema_path = "graphql/schema.graphql", 24 | query_path = "graphql/queries/search.graphql", 25 | response_derives = "Debug" 26 | )] 27 | struct SearchQuery; 28 | 29 | /// Run the search command 30 | pub fn search(options: SearchOpt) -> anyhow::Result<()> { 31 | let query = options.query; 32 | let q = SearchQuery::build_query(search_query::Variables { 33 | query: query.to_string(), 34 | }); 35 | let response: search_query::ResponseData = execute_query(&q)?; 36 | 37 | if response.search.edges.is_empty() { 38 | println!("No packages found for \"{}\"", query); 39 | return Ok(()); 40 | } 41 | let mut table = Table::new(); 42 | table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); 43 | 44 | // Add a row per time 45 | table.add_row(row!["NAME", "DESCRIPTION", "DATE", "VERSION"]); 46 | for edge in response.search.edges.into_iter() { 47 | let node = edge.unwrap().node; 48 | 49 | if let Some(search_query::SearchQuerySearchEdgesNode::PackageVersion(version)) = node { 50 | table.add_row(row![ 51 | version.package.display_name, 52 | version.description, 53 | version.created_at[..10], 54 | version.version 55 | ]); 56 | } 57 | } 58 | table.printstd(); 59 | 60 | Ok(()) 61 | } 62 | -------------------------------------------------------------------------------- /src/commands/uninstall.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::dataflow; 3 | use structopt::StructOpt; 4 | use thiserror::Error; 5 | 6 | #[derive(Clone, Debug, Error)] 7 | pub enum Error { 8 | #[error("Packages may only be uninstalled by the package name.")] 9 | NoAtSignAllowed, 10 | } 11 | 12 | #[derive(StructOpt, Debug)] 13 | pub struct UninstallOpt { 14 | pub package: String, 15 | /// Uninstall the package(s) globally 16 | #[structopt(short = "g", long = "global")] 17 | pub global: bool, 18 | } 19 | 20 | pub fn uninstall(options: UninstallOpt) -> anyhow::Result<()> { 21 | let dir = match options.global { 22 | true => Config::get_globals_directory()?, 23 | false => Config::get_current_dir()?, 24 | }; 25 | let uninstalled_package_names = vec![options.package.as_str()]; 26 | 27 | // do not allow the "@" symbol to prevent mis-use of this command 28 | if options.package.contains('@') { 29 | return Err(Error::NoAtSignAllowed.into()); 30 | } 31 | 32 | // returned bool indicates if there was any to the lockfile. If this pacakge is uninstalled, 33 | // there will be a diff created, which causes update to return true. Because no other change 34 | // is made, we can assume any change resulted in successfully uninstalled package. 35 | let result = dataflow::update(vec![], uninstalled_package_names, dir)?; 36 | 37 | if !result { 38 | info!("Package \"{}\" is not installed.", options.package); 39 | } else { 40 | info!("Package \"{}\" uninstalled.", options.package); 41 | } 42 | 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/validate.rs: -------------------------------------------------------------------------------- 1 | use crate::util::create_temp_dir; 2 | use crate::validate::*; 3 | use flate2::read::GzDecoder; 4 | use std::{fs, io::Read, path::PathBuf}; 5 | use structopt::StructOpt; 6 | use tar::Archive; 7 | 8 | #[derive(StructOpt, Debug)] 9 | pub struct ValidateOpt { 10 | /// Directory or tar file to validate 11 | package: String, 12 | } 13 | 14 | pub fn validate(validate_opts: ValidateOpt) -> anyhow::Result<()> { 15 | let pkg_path = PathBuf::from(&validate_opts.package); 16 | validate_manifest_and_modules(pkg_path) 17 | } 18 | 19 | pub fn validate_manifest_and_modules(pkg_path: PathBuf) -> anyhow::Result<()> { 20 | if pkg_path.is_dir() { 21 | validate_directory(pkg_path) 22 | } else { 23 | //unzip then validate as dir 24 | let mut compressed_archive_data = Vec::new(); 25 | let mut compressed_archive = 26 | fs::File::open(&pkg_path).map_err(|_| ValidationError::MissingFile { 27 | file: pkg_path.to_string_lossy().to_string(), 28 | })?; 29 | compressed_archive 30 | .read_to_end(&mut compressed_archive_data) 31 | .map_err(|err| ValidationError::MiscCannotRead { 32 | file: pkg_path.to_string_lossy().to_string(), 33 | error: format!("{}", err), 34 | })?; 35 | 36 | let mut gz = GzDecoder::new(&compressed_archive_data[..]); 37 | let mut archive_data = Vec::new(); 38 | gz.read_to_end(&mut archive_data) 39 | .map_err(|e| anyhow!("Failed to read archive data: {}", e.to_string()))?; 40 | 41 | let temp_out_dir = create_temp_dir() 42 | .map_err(|e| anyhow!("Could not create temporary directory: {}", e.to_string()))?; 43 | let out_dir = temp_out_dir.path(); 44 | let mut archive = Archive::new(archive_data.as_slice()); 45 | // TODO: consider doing this entirely in memory with multiple passes 46 | archive 47 | .unpack(out_dir) 48 | .map_err(|err| ValidationError::CannotUnpackArchive { 49 | file: pkg_path.to_string_lossy().to_string(), 50 | error: format!("{}", err), 51 | })?; 52 | 53 | let archive_path = { 54 | let mut ar_path = out_dir.to_path_buf(); 55 | let archive_folder_name = pkg_path 56 | .file_name() 57 | .and_then(|file_name| file_name.to_str()) 58 | .ok_or_else(|| anyhow!("Failed to get archive folder name"))? 59 | .replace(".tar.gz", ""); 60 | ar_path.push(archive_folder_name); 61 | ar_path 62 | }; 63 | 64 | validate_directory(archive_path) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/commands/whoami.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | 3 | use crate::util; 4 | 5 | pub fn whoami() -> anyhow::Result<()> { 6 | let username = util::get_username()?.context("(not logged in)")?; 7 | println!("{username}"); 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | pub const DEFAULT_RUNTIME: &str = "wasmer"; 2 | pub const WAPM_RUNTIME_ENV_KEY: &str = "WAPM_RUNTIME"; 3 | 4 | pub const RFC3339_FORMAT_STRING: &str = "%Y-%m-%dT%H:%M:%S-%f"; 5 | pub const RFC3339_FORMAT_STRING_WITH_TIMEZONE: &str = "%Y-%m-%dT%H:%M:%S.%f+%Z"; 6 | -------------------------------------------------------------------------------- /src/data/lock/lockfile.rs: -------------------------------------------------------------------------------- 1 | use crate::data::lock::lockfile_command::LockfileCommand; 2 | use crate::data::lock::lockfile_module::{ 3 | LockfileModule, LockfileModuleV2, LockfileModuleV3, LockfileModuleV4, 4 | }; 5 | use crate::data::lock::{LOCKFILE_HEADER, LOCKFILE_NAME}; 6 | use semver::Version; 7 | use std::collections::BTreeMap; 8 | use std::fs::File; 9 | use std::io; 10 | use std::io::Write; 11 | use std::path::Path; 12 | use thiserror::Error; 13 | 14 | pub type ModuleMapV2 = BTreeMap>>; 15 | pub type CommandMapV2 = BTreeMap; 16 | 17 | /// The lockfile for versions 2 and below (no changes to the fields happened until version 3, 18 | /// so these can be a singel struct) 19 | #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] 20 | pub struct LockfileV2 { 21 | pub modules: ModuleMapV2, // PackageName -> VersionNumber -> ModuleName -> Module 22 | pub commands: CommandMapV2, // CommandName -> Command 23 | } 24 | 25 | pub type ModuleMapV3 = BTreeMap>>; 26 | pub type CommandMapV3 = BTreeMap; 27 | 28 | /// The latest Lockfile version 29 | #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] 30 | pub struct LockfileV3 { 31 | pub modules: ModuleMapV3, // PackageName -> VersionNumber -> ModuleName -> Module 32 | pub commands: CommandMapV3, // CommandName -> Command 33 | } 34 | 35 | pub type ModuleMap = BTreeMap>>; 36 | pub type CommandMap = BTreeMap; 37 | 38 | /// The latest Lockfile version 39 | #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] 40 | pub struct Lockfile { 41 | pub modules: ModuleMap, // PackageName -> VersionNumber -> ModuleName -> Module 42 | pub commands: CommandMap, // CommandName -> Command 43 | } 44 | 45 | pub type LockfileV4 = Lockfile; 46 | pub type ModuleMapV4 = ModuleMap; 47 | pub type CommandMapV4 = CommandMap; 48 | 49 | impl Lockfile { 50 | /// Save the lockfile to the directory. 51 | pub fn save>(&self, directory: P) -> anyhow::Result<()> { 52 | let lockfile_string = toml::to_string(self)?; 53 | let lockfile_string = format!("{}\n{}", LOCKFILE_HEADER, lockfile_string); 54 | let lockfile_path = directory.as_ref().join(LOCKFILE_NAME); 55 | let mut file = File::create(lockfile_path)?; 56 | file.write_all(lockfile_string.as_bytes())?; 57 | Ok(()) 58 | } 59 | 60 | /// Looks up the prehashed cache key based on data in the Command 61 | pub fn get_prehashed_cache_key_from_command( 62 | &self, 63 | command: &LockfileCommand, 64 | ) -> Option { 65 | self.modules 66 | .get(&command.package_name) 67 | .and_then(|version_map| version_map.get(&command.package_version)) 68 | .and_then(|module_map| module_map.get(&command.module)) 69 | .and_then(|module| module.prehashed_module_key.clone()) 70 | } 71 | 72 | pub fn get_command(&self, command_name: &str) -> Result<&LockfileCommand, LockfileError> { 73 | self.commands 74 | .get(command_name) 75 | .ok_or_else(|| LockfileError::CommandNotFound(command_name.to_string())) 76 | } 77 | 78 | pub fn get_module( 79 | &self, 80 | package_name: &str, 81 | package_version: &Version, 82 | module_name: &str, 83 | ) -> anyhow::Result<&LockfileModule> { 84 | let version_map = self.modules.get(package_name).ok_or_else(|| { 85 | LockfileError::PackageWithVersionNotFoundWhenFindingModule( 86 | package_name.to_string(), 87 | package_version.to_string(), 88 | module_name.to_string(), 89 | ) 90 | })?; 91 | let module_map = version_map.get(package_version).ok_or_else(|| { 92 | LockfileError::VersionNotFoundForPackageWhenFindingModule( 93 | package_name.to_string(), 94 | package_version.to_string(), 95 | module_name.to_string(), 96 | ) 97 | })?; 98 | let module = module_map.get(module_name).ok_or_else(|| { 99 | LockfileError::ModuleForPackageVersionNotFound( 100 | package_name.to_string(), 101 | package_version.to_string(), 102 | module_name.to_string(), 103 | ) 104 | })?; 105 | Ok(module) 106 | } 107 | } 108 | 109 | #[derive(Debug, Error)] 110 | pub enum LockfileError { 111 | #[error("Command not found: {0}")] 112 | CommandNotFound(String), 113 | #[error("module {2} in package \"{0} {1}\" was not found")] 114 | ModuleForPackageVersionNotFound(String, String, String), 115 | #[error("Module \"{2}\" with package name \"{0}\" and version \"{1}\" was not found.")] 116 | PackageWithVersionNotFoundWhenFindingModule(String, String, String), 117 | #[error( 118 | "version \"{1}\" for package \"{0}\" was not found when searching for module \"{2}\"." 119 | )] 120 | VersionNotFoundForPackageWhenFindingModule(String, String, String), 121 | #[error("Lockfile file not found.")] 122 | MissingLockfile, 123 | #[error("File I/O error reading lockfile. I/O error: {0:?}")] 124 | FileIoErrorReadingLockfile(io::Error), 125 | #[error( 126 | "Failed to parse lockfile toml. Did you modify the generated lockfile? Toml error: {0:?}" 127 | )] 128 | TomlParseError(toml::de::Error), 129 | } 130 | -------------------------------------------------------------------------------- /src/data/lock/lockfile_command.rs: -------------------------------------------------------------------------------- 1 | use crate::data::manifest::Command; 2 | use semver::Version; 3 | use thiserror::Error; 4 | 5 | /// Describes a command for a wapm module 6 | #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] 7 | pub struct LockfileCommand { 8 | pub name: String, 9 | pub package_name: String, 10 | pub package_version: Version, 11 | pub module: String, 12 | pub is_top_level_dependency: bool, 13 | pub main_args: Option, 14 | } 15 | 16 | #[test] 17 | fn test_lockfile_command_ok() { 18 | use wapm_toml::CommandV1; 19 | assert_eq!( 20 | LockfileCommand::from_command( 21 | "Micheal-F-Bryan/wit-pack", 22 | Version::new(1, 0, 0), 23 | &Command::V1(CommandV1 { 24 | name: "wit-pack".to_string(), 25 | module: "wit-pack".to_string(), 26 | main_args: None, 27 | package: None, 28 | }) 29 | ), 30 | Ok(LockfileCommand { 31 | name: "wit-pack".to_string(), 32 | package_name: "Micheal-F-Bryan/wit-pack".to_string(), 33 | package_version: Version::new(1, 0, 0), 34 | module: "wit-pack".to_string(), 35 | is_top_level_dependency: true, 36 | main_args: None, 37 | }) 38 | ) 39 | } 40 | 41 | impl<'a> LockfileCommand { 42 | pub fn from_command( 43 | local_package_name: &str, 44 | local_package_version: Version, 45 | command: &'a Command, 46 | ) -> Result { 47 | // split the "package" field of the command if it exists 48 | // otherwise assume that this is a command for a local module 49 | // extract the package name and version for this command and insert into the lockfile command 50 | let package = command.get_package(); 51 | let (package_name, package_version): (&str, Version) = match &package { 52 | Some(package_string) => { 53 | let split = package_string.as_str().split(' ').collect::>(); 54 | match &split[..] { 55 | [package_name, package_version] => { 56 | // this string must be parsed again because the package field on a command is a concatenated string 57 | // e.g. "_/pkg 1.0.0" 58 | let package_version = Version::parse(package_version).unwrap(); 59 | (package_name, package_version) 60 | } 61 | [package_name] => (package_name, local_package_version), 62 | _ => { 63 | return Err(Error::CouldNotParsePackageVersionForCommand( 64 | package_string.clone(), 65 | command.get_name(), 66 | )); 67 | } 68 | } 69 | } 70 | None => (local_package_name, local_package_version), 71 | }; 72 | 73 | let lockfile_command = LockfileCommand { 74 | name: command.get_name(), 75 | package_name: package_name.to_string(), 76 | package_version, 77 | module: command.get_module(), 78 | main_args: command.get_main_args(), 79 | is_top_level_dependency: true, 80 | }; 81 | Ok(lockfile_command) 82 | } 83 | } 84 | 85 | #[derive(Clone, PartialEq, Eq, Debug, Error)] 86 | pub enum Error { 87 | #[error("The module for this command does not exist. Did you modify the wapm.lock?")] 88 | ModuleForCommandDoesNotExist, 89 | #[error("Could not parse the package name and version \"{0}\" for the command \"{1}\".")] 90 | CouldNotParsePackageVersionForCommand(String, String), 91 | } 92 | -------------------------------------------------------------------------------- /src/data/lock/lockfile_module.rs: -------------------------------------------------------------------------------- 1 | use crate::abi::Abi; 2 | use crate::data::manifest::{Module, PACKAGES_DIR_NAME}; 3 | use crate::util; 4 | use semver::Version; 5 | use std::path::{Path, PathBuf}; 6 | 7 | /// legacy Lockfile module struct; which is only used to parse legacy lockfiles which get 8 | /// transformed into up to date ones (V1, V2) 9 | #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] 10 | pub struct LockfileModuleV2 { 11 | pub name: String, 12 | pub package_version: String, 13 | pub package_name: String, 14 | pub source: String, 15 | pub resolved: String, 16 | pub abi: Abi, 17 | pub entry: String, 18 | } 19 | 20 | /// legacy Lockfile module struct; which is only used to parse legacy lockfiles which get 21 | /// transformed into up to date ones (V3) 22 | #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] 23 | pub struct LockfileModuleV3 { 24 | pub name: String, 25 | pub package_version: String, 26 | pub package_name: String, 27 | pub source: String, 28 | pub resolved: String, 29 | pub abi: Abi, 30 | /// The entry is where the wasm module lives 31 | pub entry: String, 32 | /// The root is where the manifest lives 33 | pub root: String, 34 | /// The hash of the wasm module cached here for faster startup time 35 | pub prehashed_module_key: Option, 36 | } 37 | 38 | /// The latest Lockfile module struct (V4) 39 | /// It contains data relating to the Wasm module itself 40 | #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] 41 | pub struct LockfileModule { 42 | pub name: String, 43 | pub package_version: String, 44 | pub package_name: String, 45 | pub package_path: String, 46 | pub resolved: String, 47 | pub resolved_source: String, 48 | pub abi: Abi, 49 | /// The source path is where the wasm module lives 50 | pub source: String, 51 | /// The hash of the wasm module cached here for faster startup time 52 | pub prehashed_module_key: Option, 53 | } 54 | 55 | pub type LockfileModuleV4 = LockfileModule; 56 | 57 | impl LockfileModule { 58 | pub fn from_module( 59 | manifest_base_dir_path: &Path, 60 | name: &str, 61 | version: &Version, 62 | module: &Module, 63 | download_url: &str, 64 | ) -> Self { 65 | // build the entry path 66 | // this is path like /wapm_packages/_/lua@0.1.3/path/to/module/lua.wasm 67 | let path = PathBuf::from(manifest_base_dir_path); 68 | 69 | let source = { 70 | let mut new_style = path.clone(); 71 | new_style.push(&module.source); 72 | if new_style.exists() { 73 | module.source.to_string_lossy().to_string() 74 | } else { 75 | // to prevent breaking packages published before this change (~2019/06/25) 76 | module 77 | .source 78 | .file_name() 79 | .unwrap() 80 | .to_string_lossy() 81 | .to_string() 82 | } 83 | }; 84 | 85 | LockfileModule { 86 | name: module.name.clone(), 87 | package_version: version.to_string(), 88 | package_name: name.to_string(), 89 | package_path: format!("{}@{}", name, version), 90 | resolved: download_url.to_string(), 91 | resolved_source: format!("registry+{}", module.name), 92 | abi: module.abi, 93 | prehashed_module_key: util::get_hashed_module_key(&path.join(&source)), 94 | source, 95 | } 96 | } 97 | 98 | pub fn from_local_module( 99 | manifest_base_dir_path: &Path, 100 | name: &str, 101 | version: &Version, 102 | module: &Module, 103 | ) -> Self { 104 | let mut wasm_module_full_path = PathBuf::from(manifest_base_dir_path); 105 | wasm_module_full_path.push(&module.source); 106 | 107 | LockfileModule { 108 | name: module.name.clone(), 109 | package_version: version.to_string(), 110 | package_name: name.to_string(), 111 | package_path: format!("{}@{}", name, version), 112 | resolved: "local".to_string(), 113 | resolved_source: "local".to_string(), 114 | abi: module.abi, 115 | source: module.source.to_string_lossy().to_string(), 116 | prehashed_module_key: util::get_hashed_module_key(&wasm_module_full_path), 117 | } 118 | } 119 | 120 | /// Returns the full, absolute path to the WASM module 121 | pub fn get_canonical_source_path_from_lockfile_dir( 122 | &self, 123 | mut lockfile_dir: PathBuf, 124 | ) -> PathBuf { 125 | lockfile_dir.push(PACKAGES_DIR_NAME); 126 | lockfile_dir.push(&self.package_path); 127 | lockfile_dir.push(&self.source); 128 | 129 | lockfile_dir 130 | } 131 | 132 | /// Returns the Manifest path from the lockfile 133 | /// 134 | /// This method does extra logic to detect if the lockfile is global and adjusts accordingly. 135 | /// 136 | /// The `local_dep` flag should be passed when its known that the manifest we want to access 137 | /// is not in the current directory and that we need to add `wapm_packages/...` to it. 138 | pub fn get_canonical_manifest_path_from_lockfile_dir( 139 | &self, 140 | mut lockfile_dir: PathBuf, 141 | local_dep: bool, 142 | ) -> PathBuf { 143 | if crate::config::Config::get_globals_directory().expect("Could not get globals direcotry") 144 | == lockfile_dir 145 | || local_dep 146 | { 147 | lockfile_dir.push(PACKAGES_DIR_NAME); 148 | lockfile_dir.push(&self.package_path); 149 | 150 | lockfile_dir 151 | } else { 152 | lockfile_dir 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/data/lock/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod lockfile; 2 | pub mod lockfile_command; 3 | pub mod lockfile_module; 4 | pub mod migrate; 5 | 6 | pub static LOCKFILE_NAME: &str = "wapm.lock"; 7 | 8 | static LOCKFILE_HEADER: &str = r#"# Lockfile v4 9 | # This file is automatically generated by Wapm. 10 | # It is not intended for manual editing. The schema of this file may change."#; 11 | 12 | use crate::data::manifest::MANIFEST_FILE_NAME; 13 | use std::path::Path; 14 | 15 | pub fn is_lockfile_out_of_date>(directory: P) -> anyhow::Result { 16 | use std::fs; 17 | let wapm_lock_metadata = fs::metadata(directory.as_ref().join(LOCKFILE_NAME))?; 18 | let wapm_toml_metadata = fs::metadata(directory.as_ref().join(MANIFEST_FILE_NAME))?; 19 | let wapm_lock_last_modified = wapm_lock_metadata.modified()?; 20 | let wapm_toml_last_modified = wapm_toml_metadata.modified()?; 21 | Ok(wapm_lock_last_modified < wapm_toml_last_modified) 22 | } 23 | -------------------------------------------------------------------------------- /src/data/manifest.rs: -------------------------------------------------------------------------------- 1 | //! The Manifest file is where the core metadata of a wapm package lives 2 | pub use wapm_toml::{ 3 | Command, CommandV1, CommandV2, Manifest, ManifestError, Module, Package, ValidationError, 4 | MANIFEST_FILE_NAME, PACKAGES_DIR_NAME, 5 | }; 6 | -------------------------------------------------------------------------------- /src/data/mod.rs: -------------------------------------------------------------------------------- 1 | //! The definitions of data types that wapm uses. The Manifest and the Lockfile are 2 | //! collections of metadata, used for specifying a package and indexing dependencies 3 | //! respectively. 4 | pub mod lock; 5 | pub mod manifest; 6 | pub mod wax_index; 7 | -------------------------------------------------------------------------------- /src/data/wax_index.rs: -------------------------------------------------------------------------------- 1 | //! The Wax index is where temporary commands are tracked for use with the `wax` 2 | //! command. 3 | 4 | use crate::config; 5 | use crate::constants::RFC3339_FORMAT_STRING; 6 | use semver::Version; 7 | use std::convert::From; 8 | use std::env; 9 | use std::fs; 10 | use std::io::{self, Read, Write}; 11 | use std::path::{Path, PathBuf}; 12 | use thiserror::Error; 13 | 14 | use std::collections::HashMap; 15 | 16 | #[derive(Debug, Deserialize, Serialize)] 17 | pub struct WaxIndex { 18 | base_dir: PathBuf, 19 | index: HashMap, 20 | } 21 | 22 | #[derive(Debug, Deserialize, Serialize)] 23 | struct WaxEntry { 24 | /// Fully qualified package name `namespace/name@version` 25 | package_name: String, 26 | /// Timestamp when wax was last updated 27 | last_updated: String, 28 | } 29 | 30 | impl WaxEntry { 31 | fn new(name: String, version: Version, time: String) -> Self { 32 | WaxEntry { 33 | package_name: format!("{}@{}", name, version), 34 | last_updated: time, 35 | } 36 | } 37 | } 38 | 39 | impl WaxIndex { 40 | /// Read the `WaxIndex` from disk 41 | pub fn open() -> Result { 42 | trace!("Loading WaxIndex!"); 43 | let wax_path = config::Config::get_wax_file_path()?; 44 | let wax_index = if wax_path.exists() { 45 | let mut f = fs::OpenOptions::new().read(true).open(wax_path)?; 46 | 47 | let index_str = { 48 | let mut s = String::new(); 49 | f.read_to_string(&mut s)?; 50 | s 51 | }; 52 | 53 | if index_str.is_empty() { 54 | WaxIndex { 55 | index: Default::default(), 56 | base_dir: env::temp_dir().join("wax"), 57 | } 58 | } else { 59 | serde_json::from_str(&index_str)? 60 | } 61 | } else { 62 | WaxIndex { 63 | index: Default::default(), 64 | base_dir: env::temp_dir().join("wax"), 65 | } 66 | }; 67 | 68 | // ensure the directory exists 69 | fs::create_dir_all(&wax_index.base_dir)?; 70 | trace!("WaxIndex created!"); 71 | 72 | Ok(wax_index) 73 | } 74 | 75 | /// Save the `WaxIndex` to disk 76 | pub fn save(&self) -> Result<(), WaxIndexError> { 77 | trace!("Saving WaxIndex!"); 78 | let wax_path = config::Config::get_wax_file_path()?; 79 | let mut f = fs::OpenOptions::new() 80 | .write(true) 81 | .create(true) 82 | .truncate(true) 83 | .open(wax_path)?; 84 | 85 | let json_str = serde_json::to_string(self)?; 86 | 87 | f.write_all(json_str.as_bytes())?; 88 | trace!("WaxIndex saved!"); 89 | Ok(()) 90 | } 91 | 92 | /// This function takes a `&mut` because it will update itself with the 93 | /// information that it finds. 94 | pub fn search_for_entry( 95 | &mut self, 96 | entry: String, 97 | ) -> Result<(String, Version, time::Timespec), WaxIndexError> { 98 | if let Some(WaxEntry { 99 | package_name, 100 | last_updated, 101 | }) = self.index.get(&entry) 102 | { 103 | let location = self.base_path().join(package_name); 104 | // check if entry still exists and if not remove it 105 | if location.exists() { 106 | trace!("Wax entry found and it still exists!"); 107 | let mut splitter = package_name.split('@'); 108 | let package_name = splitter 109 | .next() 110 | .ok_or_else(|| WaxIndexError::EntryCorrupt { 111 | entry: entry.clone(), 112 | })? 113 | .to_string(); 114 | let version = splitter 115 | .next() 116 | .and_then(|v| Version::parse(v).ok()) 117 | .ok_or_else(|| WaxIndexError::EntryCorrupt { 118 | entry: entry.clone(), 119 | })?; 120 | let last_seen = 121 | time::strptime(last_updated, RFC3339_FORMAT_STRING).map_err(|e| { 122 | WaxIndexError::EntryCorrupt { 123 | entry: e.to_string(), 124 | } 125 | })?; 126 | return Ok((package_name, version, last_seen.to_timespec())); 127 | } 128 | trace!("Wax entry found but it no longer exists, removing from registry!"); 129 | self.index.remove(&entry); 130 | } 131 | Err(WaxIndexError::EntryNotFound { 132 | entry: entry.clone(), 133 | }) 134 | } 135 | 136 | /// Package installed, add it to the index. 137 | /// 138 | /// Returns true if an existing entry was updated. 139 | pub fn insert_entry(&mut self, entry: String, version: Version, package_name: String) -> bool { 140 | let now = time::now_utc(); 141 | let now_str = time::strftime(RFC3339_FORMAT_STRING, &now).expect("Format current time!"); 142 | self.index 143 | .insert(entry, WaxEntry::new(package_name, version, now_str)) 144 | .is_some() 145 | } 146 | 147 | /// Get path at which packages should be installed. 148 | pub fn base_path(&self) -> &Path { 149 | &self.base_dir 150 | } 151 | } 152 | 153 | #[derive(Debug, Error)] 154 | pub enum WaxIndexError { 155 | #[error("Error finding Wax Index: {0}")] 156 | ConfigError(config::GlobalConfigError), 157 | #[error("Failed to operate on Wax index file: `{0}`")] 158 | IoError(io::Error), 159 | #[error("Failed to parse WaxIndex from JSON or convert WaxIndex to JSON: `{0}`")] 160 | SerdeError(serde_json::error::Error), 161 | #[error("Entry `{entry}` not found")] 162 | EntryNotFound { entry: String }, 163 | #[error("Entry `{entry}` found but was corrupt")] 164 | EntryCorrupt { entry: String }, 165 | } 166 | 167 | impl From for WaxIndexError { 168 | fn from(other: config::GlobalConfigError) -> Self { 169 | WaxIndexError::ConfigError(other) 170 | } 171 | } 172 | 173 | impl From for WaxIndexError { 174 | fn from(other: io::Error) -> Self { 175 | WaxIndexError::IoError(other) 176 | } 177 | } 178 | 179 | impl From for WaxIndexError { 180 | fn from(other: serde_json::error::Error) -> Self { 181 | WaxIndexError::SerdeError(other) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/database.rs: -------------------------------------------------------------------------------- 1 | //! Functions and data for dealing with the local (sqlite) database 2 | //! 3 | //! Internal information: 4 | //! Schema updates are handled by applying migrations based on the `pragma user_version` 5 | //! in sqlite. 6 | //! 7 | //! To create a new migration: 8 | //! - add an entry applying the desired changes for the CURRENT_DATA_VERSION in `apply_migration` 9 | //! - increment `CURRENT_DATA_VERSION` 10 | //! 11 | //! Every data version must leave the database in a valid state. 12 | //! Prefer additive changes over destructive ones 13 | 14 | use crate::config::Config; 15 | use crate::constants::*; 16 | use rusqlite::{Connection, OpenFlags, TransactionBehavior}; 17 | use thiserror::Error; 18 | 19 | /// The current version of the database. Update this to perform a migration 20 | pub const CURRENT_DATA_VERSION: i32 = 3; 21 | 22 | /// Gets the current time in our standard format 23 | pub fn get_current_time_in_format() -> Option { 24 | let cur_time = time::now(); 25 | time::strftime(RFC3339_FORMAT_STRING, &cur_time).ok() 26 | } 27 | 28 | /// Opens an exclusive read/write connection to the database, creating it if it does not exist 29 | pub fn open_db() -> anyhow::Result { 30 | let db_path = Config::get_database_file_path()?; 31 | let mut conn = Connection::open_with_flags( 32 | db_path, 33 | OpenFlags::SQLITE_OPEN_CREATE 34 | | OpenFlags::SQLITE_OPEN_READ_WRITE 35 | | OpenFlags::SQLITE_OPEN_FULL_MUTEX, 36 | )?; 37 | 38 | apply_migrations(&mut conn)?; 39 | Ok(conn) 40 | } 41 | 42 | /// Applies migrations to the database 43 | pub fn apply_migrations(conn: &mut Connection) -> anyhow::Result<()> { 44 | let user_version = conn.pragma_query_value(None, "user_version", |val| val.get(0))?; 45 | for data_version in user_version..CURRENT_DATA_VERSION { 46 | debug!("Applying migration {}", data_version); 47 | apply_migration(conn, data_version)?; 48 | } 49 | Ok(()) 50 | } 51 | 52 | #[derive(Debug, Error)] 53 | pub enum MigrationError { 54 | #[error( 55 | "Critical internal error: the data version {0} is not handleded; current data version: {1}" 56 | )] 57 | MigrationNumberDoesNotExist(i32, i32), 58 | #[error("Critical internal error: failed to commit trasaction migrating to data version {0}")] 59 | CommitFailed(i32), 60 | #[error("Critical internal error: transaction failed on migration number {0}: {1}")] 61 | TransactionFailed(i32, String), 62 | } 63 | 64 | /// Applies migrations to the database and updates the `user_version` pragma. 65 | /// Every migration must leave the database in a valid state. 66 | fn apply_migration(conn: &mut Connection, migration_number: i32) -> Result<(), MigrationError> { 67 | let tx = conn 68 | .transaction_with_behavior(TransactionBehavior::Immediate) 69 | .map_err(|e| MigrationError::TransactionFailed(migration_number, format!("{}", e)))?; 70 | match migration_number { 71 | 0 => { 72 | tx.execute_batch(include_str!("sql/migrations/0000.sql")) 73 | .map_err(|e| { 74 | MigrationError::TransactionFailed(migration_number, format!("{}", e)) 75 | })?; 76 | } 77 | 1 => { 78 | tx.execute_batch(include_str!("sql/migrations/0001.sql")) 79 | .map_err(|e| { 80 | MigrationError::TransactionFailed(migration_number, format!("{}", e)) 81 | })?; 82 | } 83 | 2 => { 84 | tx.execute_batch(include_str!("sql/migrations/0002.sql")) 85 | .map_err(|e| { 86 | MigrationError::TransactionFailed(migration_number, format!("{}", e)) 87 | })?; 88 | } 89 | _ => { 90 | return Err(MigrationError::MigrationNumberDoesNotExist( 91 | migration_number, 92 | CURRENT_DATA_VERSION, 93 | )); 94 | } 95 | } 96 | tx.pragma_update(None, "user_version", &(migration_number + 1)) 97 | .map_err(|e| MigrationError::TransactionFailed(migration_number, format!("{}", e)))?; 98 | tx.commit() 99 | .map_err(|_| MigrationError::CommitFailed(migration_number)) 100 | } 101 | 102 | #[cfg(test)] 103 | mod test { 104 | use super::*; 105 | use crate::util::create_temp_dir; 106 | 107 | #[test] 108 | fn migrations_are_valid() { 109 | let mut conn = rusqlite::Connection::open_in_memory().unwrap(); 110 | for data_version in 0..CURRENT_DATA_VERSION { 111 | apply_migration(&mut conn, data_version).unwrap(); 112 | } 113 | let user_version: i32 = conn 114 | .pragma_query_value(None, "user_version", |val| val.get(0)) 115 | .unwrap(); 116 | assert_eq!(user_version, CURRENT_DATA_VERSION); 117 | } 118 | 119 | #[test] 120 | fn data_version_was_updated() { 121 | let tmp_dir = create_temp_dir().unwrap().path().to_owned(); 122 | let mut conn = Connection::open(tmp_dir).unwrap(); 123 | if let Err(MigrationError::MigrationNumberDoesNotExist { .. }) = 124 | apply_migration(&mut conn, CURRENT_DATA_VERSION) 125 | { 126 | // failed for the correct reason 127 | } else { 128 | panic!("Migration for CURRENT_DATA_VERSION ({}) found! Did you forget to increment CURRENT_DATA_VERSION?", CURRENT_DATA_VERSION); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/dataflow/added_packages.rs: -------------------------------------------------------------------------------- 1 | use crate::dataflow::{normalize_global_namespace, PackageKey}; 2 | use semver::Version; 3 | use std::collections::HashSet; 4 | use thiserror::Error; 5 | 6 | #[derive(Clone, Debug, Error)] 7 | pub enum Error { 8 | #[error("Package must have version that follows semantic versioning. {0}")] 9 | SemVerError(String), 10 | } 11 | 12 | /// Holds packages that are added via the command line 13 | #[derive(Debug, Default)] 14 | pub struct AddedPackages<'a> { 15 | pub packages: HashSet>, 16 | } 17 | 18 | impl<'a> AddedPackages<'a> { 19 | /// Extract name and version, parse version as semver, construct registry key, and finally 20 | /// normalize the global namespace if using the shorthand e.g. "_/pkg" == pkg 21 | pub fn new_from_str_pairs(added_packages: Vec<(&'a str, &'a str)>) -> Result { 22 | let added_packages = added_packages 23 | .into_iter() 24 | .map(Self::extract_name_and_version) 25 | .collect::, Error>>()?; 26 | let packages = added_packages 27 | .into_iter() 28 | .map(|(n, v)| PackageKey::new_registry_package(n, v)) 29 | .map(normalize_global_namespace) 30 | .collect(); 31 | 32 | Ok(Self { packages }) 33 | } 34 | 35 | pub fn prune_already_installed_packages( 36 | self, 37 | lockfile_packages_keys: HashSet>, 38 | ) -> Self { 39 | let added_packages = self.packages; 40 | let packages = added_packages 41 | .difference(&lockfile_packages_keys) 42 | .cloned() 43 | .collect(); 44 | Self { packages } 45 | } 46 | 47 | pub fn add_missing_packages(self, missing_package_keys: HashSet>) -> Self { 48 | let added_packages = self.packages; 49 | let packages = added_packages 50 | .union(&missing_package_keys) 51 | .cloned() 52 | .collect(); 53 | Self { packages } 54 | } 55 | 56 | /// parse the version as semver, or error 57 | fn extract_name_and_version(pair: (&'a str, &'a str)) -> Result<(&'a str, Version), Error> { 58 | Version::parse(pair.1) 59 | .map(|version| (pair.0, version)) 60 | .map_err(|e| Error::SemVerError(e.to_string())) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod test { 66 | use crate::dataflow::added_packages::AddedPackages; 67 | use crate::dataflow::lockfile_packages::{LockfilePackage, LockfilePackages}; 68 | use crate::dataflow::PackageKey; 69 | use std::collections::hash_set::HashSet; 70 | use std::collections::HashMap; 71 | 72 | #[test] 73 | fn prune_test() { 74 | let mut packages = HashSet::new(); 75 | packages.insert(PackageKey::new_registry_package( 76 | "_/foo", 77 | semver::Version::new(1, 1, 0), 78 | )); 79 | packages.insert(PackageKey::new_registry_package( 80 | "_/bar", 81 | semver::Version::new(2, 0, 0), 82 | )); 83 | let added_packages = AddedPackages { packages }; 84 | 85 | let mut packages = HashMap::new(); 86 | packages.insert( 87 | PackageKey::new_registry_package("_/foo", semver::Version::new(1, 0, 0)), 88 | LockfilePackage::default(), 89 | ); 90 | packages.insert( 91 | PackageKey::new_registry_package("_/bar", semver::Version::new(2, 0, 0)), 92 | LockfilePackage::default(), 93 | ); 94 | let existing_lockfile_packages = LockfilePackages { packages }; 95 | 96 | let existing_lockfile_keys = existing_lockfile_packages.package_keys(); 97 | 98 | let pruned_packages = 99 | added_packages.prune_already_installed_packages(existing_lockfile_keys); 100 | assert!(pruned_packages 101 | .packages 102 | .contains(&PackageKey::new_registry_package( 103 | "_/foo", 104 | semver::Version::new(1, 1, 0) 105 | ))); 106 | assert_eq!(1, pruned_packages.packages.len()) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/dataflow/bin_script.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(target_os = "wasi", allow(dead_code))] 2 | use crate::data::manifest::PACKAGES_DIR_NAME; 3 | use std::fs; 4 | use std::io::Write; 5 | use std::path::Path; 6 | use thiserror::Error; 7 | 8 | pub const BIN_DIR_NAME: &str = ".bin"; 9 | 10 | #[derive(Clone, Debug, Error)] 11 | pub enum Error { 12 | #[error("Could not save script file for command \"{0}\". {1}")] 13 | SaveError(String, String), 14 | #[error("Could not create file at \"{0}\". {1}")] 15 | FileCreationError(String, String), 16 | } 17 | 18 | #[cfg(target_os = "wasi")] 19 | #[derive(Debug, Clone, Serialize, Deserialize)] 20 | pub struct AliasConfig { 21 | pub run: String, 22 | #[serde(default)] 23 | pub chroot: bool, 24 | #[serde(default)] 25 | pub base: Option, 26 | #[serde(default)] 27 | pub mappings: Vec, 28 | } 29 | 30 | #[cfg(target_os = "wasi")] 31 | pub fn save_bin_script>( 32 | _directory: P, 33 | command_name: String, 34 | package_path: String, 35 | module_path: String, 36 | ) -> Result<(), Error> { 37 | let current_dir = crate::config::Config::get_current_dir() 38 | .ok() 39 | .unwrap_or_else(|| std::path::PathBuf::from("/".to_string())); 40 | let command_path = format!("/bin/{}.alias", command_name); 41 | let package_path = current_dir 42 | .clone() 43 | .join("wapm_packages") 44 | .join(package_path) 45 | .to_string_lossy() 46 | .to_string(); 47 | let module_path = current_dir 48 | .clone() 49 | .join("wapm_packages") 50 | .join(module_path) 51 | .to_string_lossy() 52 | .to_string(); 53 | let mut file = fs::OpenOptions::new() 54 | .create(true) 55 | .truncate(true) 56 | .write(true) 57 | .open(Path::new(command_path.as_str())) 58 | .map_err(|e| Error::FileCreationError(command_path, e.to_string()))?; 59 | 60 | let mut mappings = Vec::new(); 61 | match crate::dataflow::ManifestResult::find_in_directory(&package_path) { 62 | crate::dataflow::ManifestResult::Manifest(manifest) => { 63 | if let Some(ref fs) = manifest.fs { 64 | for (guest_path, host_path) in fs.iter() { 65 | mappings.push(format!( 66 | "{}:{}/{}", 67 | guest_path, 68 | package_path, 69 | host_path.to_string_lossy() 70 | )); 71 | } 72 | } 73 | } 74 | _ => (), 75 | } 76 | 77 | let alias = AliasConfig { 78 | run: module_path, 79 | chroot: false, 80 | base: Some(package_path), 81 | mappings, 82 | }; 83 | 84 | let data = serde_yaml::to_vec(&alias) 85 | .map_err(|e| Error::SaveError(command_name.clone(), e.to_string()))?; 86 | file.write_all(&data[..]) 87 | .map_err(|e| Error::SaveError(command_name.clone(), e.to_string()))?; 88 | Ok(()) 89 | } 90 | 91 | #[cfg(all(not(target_os = "windows"), not(target_os = "wasi")))] 92 | pub fn save_bin_script>( 93 | directory: P, 94 | command_name: String, 95 | _package_path: String, 96 | _module_path: String, 97 | ) -> Result<(), Error> { 98 | let data = format!("#!/bin/bash\nwapm run {} \"$@\"\n", command_name); 99 | save(data, directory, command_name) 100 | } 101 | 102 | #[cfg(target_os = "windows")] 103 | pub fn save_bin_script>( 104 | directory: P, 105 | command_name: String, 106 | _package_path: String, 107 | _module_path: String, 108 | ) -> Result<(), Error> { 109 | let data = format!("@\"wapm\" run {} %*\n", command_name); 110 | let file_name = format!("{}.cmd", command_name); 111 | save(data, directory, file_name) 112 | } 113 | 114 | #[cfg(target_os = "wasi")] 115 | pub fn delete_bin_script>(_directory: P, command_name: String) -> Result<(), Error> { 116 | let command_path = format!("/bin/{}", command_name); 117 | if Path::new(command_path.as_str()).exists() { 118 | fs::remove_file(command_path) 119 | .map_err(|e| Error::SaveError(command_name.clone(), e.to_string()))?; 120 | } 121 | Ok(()) 122 | } 123 | 124 | #[cfg(all(not(target_os = "windows"), not(target_os = "wasi")))] 125 | pub fn delete_bin_script>(directory: P, command_name: String) -> Result<(), Error> { 126 | delete(directory, command_name) 127 | } 128 | 129 | #[cfg(target_os = "windows")] 130 | pub fn delete_bin_script>(directory: P, command_name: String) -> Result<(), Error> { 131 | let file_name = format!("{}.cmd", command_name); 132 | delete(directory, file_name) 133 | } 134 | 135 | /// save the bin script for a command into the .bin directory 136 | fn save>(data: String, directory: P, command_name: String) -> Result<(), Error> { 137 | let mut dir = directory.as_ref().join(PACKAGES_DIR_NAME); 138 | dir.push(BIN_DIR_NAME); 139 | if !dir.exists() { 140 | fs::create_dir_all(&dir) 141 | .map_err(|e| Error::SaveError(command_name.clone(), e.to_string()))?; 142 | } 143 | let script_path = dir.join(command_name.clone()); 144 | #[cfg(unix)] 145 | let maybe_unix_mode = { 146 | use std::os::unix::fs::PermissionsExt; 147 | script_path.metadata().map(|md| md.permissions().mode()) 148 | }; 149 | let mut script_file = { 150 | let mut oo = fs::OpenOptions::new(); 151 | oo.create(true).truncate(true).write(true); 152 | #[cfg(unix)] 153 | let oo = { 154 | use std::os::unix::fs::OpenOptionsExt; 155 | if let Ok(unix_mode) = maybe_unix_mode { 156 | oo.mode(unix_mode | 0o110) 157 | } else { 158 | oo.mode(0o754) 159 | } 160 | }; 161 | oo.open(&script_path).map_err(|e| { 162 | Error::FileCreationError(script_path.to_string_lossy().to_string(), e.to_string()) 163 | })? 164 | }; 165 | script_file 166 | .write(data.as_bytes()) 167 | .map_err(|e| Error::SaveError(command_name.clone(), e.to_string()))?; 168 | Ok(()) 169 | } 170 | 171 | /// delete the bin script for a command - for cleanup during uninstall 172 | fn delete>(directory: P, command_name: String) -> Result<(), Error> { 173 | let mut dir = directory.as_ref().join(PACKAGES_DIR_NAME); 174 | dir.push(BIN_DIR_NAME); 175 | if !dir.exists() { 176 | Ok(()) 177 | } else { 178 | let script_path = dir.join(command_name.clone()); 179 | if script_path.exists() { 180 | fs::remove_file(script_path) 181 | .map_err(|e| Error::SaveError(command_name.clone(), e.to_string()))?; 182 | Ok(()) 183 | } else { 184 | Ok(()) 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/dataflow/bindings.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display, Formatter}; 2 | 3 | use graphql_client::GraphQLQuery; 4 | 5 | #[derive(Debug, thiserror::Error)] 6 | pub enum Error { 7 | #[error(transparent)] 8 | Query(anyhow::Error), 9 | #[error("Package not found in the registry {registry:?}: {name}")] 10 | UnknownPackage { name: String, registry: String }, 11 | #[error("{package_name} v{version} doesn't have any {language} bindings")] 12 | MissingBindings { 13 | package_name: String, 14 | version: String, 15 | language: Language, 16 | }, 17 | #[error("The {package_name} package doesn't contain any bindings")] 18 | NoBindings { package_name: String }, 19 | } 20 | 21 | #[derive(graphql_client::GraphQLQuery)] 22 | #[graphql( 23 | schema_path = "graphql/schema.graphql", 24 | query_path = "graphql/queries/get_bindings.graphql", 25 | response_derives = "Debug" 26 | )] 27 | struct GetBindingsQuery; 28 | 29 | /// Get the link to the bindings for a package. 30 | /// 31 | /// If the package contains multiple modules with bindings, the `module` 32 | /// argument is used to pick the correct one. 33 | pub fn link_to_package_bindings( 34 | package_name: &str, 35 | version: Option<&str>, 36 | language: Language, 37 | ) -> Result { 38 | let q = GetBindingsQuery::build_query(get_bindings_query::Variables { 39 | name: package_name.to_string(), 40 | version: version.map(|s| s.to_string()), 41 | }); 42 | 43 | let config = 44 | crate::config::Config::from_file().map_err(|e| Error::Query(anyhow::anyhow!("{e}")))?; 45 | 46 | let get_bindings_query::ResponseData { package_version } = 47 | crate::graphql::execute_query(&q).map_err(Error::Query)?; 48 | let get_bindings_query::GetBindingsQueryPackageVersion { bindings, version } = package_version 49 | .ok_or_else(|| Error::UnknownPackage { 50 | name: package_name.to_string(), 51 | registry: config.registry.get_current_registry(), 52 | })?; 53 | 54 | if bindings.is_empty() { 55 | return Err(Error::NoBindings { 56 | package_name: package_name.to_string(), 57 | }); 58 | } 59 | 60 | let chosen_language = match language { 61 | Language::JavaScript => get_bindings_query::ProgrammingLanguage::JAVASCRIPT, 62 | Language::Python => get_bindings_query::ProgrammingLanguage::PYTHON, 63 | }; 64 | 65 | let bindings = bindings 66 | .into_iter() 67 | .flatten() 68 | .find(|b| b.language == chosen_language) 69 | .ok_or_else(|| Error::MissingBindings { 70 | package_name: package_name.to_string(), 71 | version, 72 | language, 73 | })?; 74 | 75 | Ok(bindings.url) 76 | } 77 | 78 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 79 | pub enum Language { 80 | JavaScript, 81 | Python, 82 | } 83 | 84 | impl Display for Language { 85 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 86 | match self { 87 | Language::JavaScript => "JavaScript".fmt(f), 88 | Language::Python => "Python".fmt(f), 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/dataflow/interfaces.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | not(feature = "full"), 3 | allow(dead_code, unused_imports, unused_variables) 4 | )] 5 | use crate::graphql::execute_query; 6 | use graphql_client::*; 7 | 8 | #[derive(GraphQLQuery)] 9 | #[graphql( 10 | schema_path = "graphql/schema.graphql", 11 | query_path = "graphql/queries/get_interface_version.graphql", 12 | response_derives = "Debug" 13 | )] 14 | struct GetInterfaceVersionQuery; 15 | 16 | #[derive(Debug)] 17 | pub struct InterfaceFromServer { 18 | pub name: String, 19 | pub version: String, 20 | pub content: String, 21 | } 22 | 23 | impl InterfaceFromServer { 24 | fn get_response( 25 | name: String, 26 | version: String, 27 | ) -> anyhow::Result { 28 | let q = GetInterfaceVersionQuery::build_query(get_interface_version_query::Variables { 29 | name, 30 | version, 31 | }); 32 | execute_query(&q) 33 | } 34 | 35 | pub fn get(name: String, version: String) -> anyhow::Result { 36 | let response = Self::get_response(name, version)?; 37 | let response_val = response 38 | .interface 39 | .ok_or_else(|| anyhow!("Error downloading Interface from the server"))?; 40 | Ok(Self { 41 | name: response_val.interface.name, 42 | version: response_val.version, 43 | content: response_val.content, 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/dataflow/local_package.rs: -------------------------------------------------------------------------------- 1 | use crate::data::lock::lockfile_command; 2 | use crate::data::lock::lockfile_command::LockfileCommand; 3 | use crate::data::lock::lockfile_module::LockfileModule; 4 | use crate::data::manifest::Manifest; 5 | use crate::dataflow::lockfile_packages::{LockfilePackage, LockfilePackages}; 6 | use crate::dataflow::PackageKey; 7 | use std::collections::hash_map::HashMap; 8 | use thiserror::Error; 9 | 10 | #[derive(Clone, Debug, Error)] 11 | pub enum Error { 12 | #[error("Could not extract commands from manifest. {0}")] 13 | CouldNotExtractCommandsFromManifest(lockfile_command::Error), 14 | } 15 | 16 | pub struct LocalPackage<'a> { 17 | pub key: PackageKey<'a>, 18 | pub data: LockfilePackage, 19 | } 20 | 21 | impl<'a> LocalPackage<'a> { 22 | pub fn new_from_local_package_in_manifest(manifest: &'a Manifest) -> Result { 23 | let package_name = manifest.package.name.as_str(); 24 | let package_version = &manifest.package.version; 25 | let modules = manifest 26 | .module 27 | .as_ref() 28 | .cloned() 29 | .unwrap_or_default() 30 | .into_iter() 31 | .map(|m| { 32 | LockfileModule::from_local_module( 33 | &manifest.base_directory_path, 34 | package_name, 35 | package_version, 36 | &m, 37 | ) 38 | }) 39 | .collect(); 40 | let commands = manifest 41 | .command 42 | .as_ref() 43 | .cloned() 44 | .unwrap_or_default() 45 | .into_iter() 46 | .map(|c| LockfileCommand::from_command(package_name, package_version.clone(), &c)) 47 | .collect::, lockfile_command::Error>>() 48 | .map_err(Error::CouldNotExtractCommandsFromManifest)?; 49 | let key = PackageKey::new_registry_package(package_name, package_version.clone()); 50 | let data = LockfilePackage { modules, commands }; 51 | Ok(LocalPackage { key, data }) 52 | } 53 | } 54 | 55 | impl<'a> From> for LockfilePackages<'a> { 56 | fn from(local: LocalPackage<'a>) -> LockfilePackages<'a> { 57 | let mut packages = HashMap::new(); 58 | packages.insert(local.key, local.data); 59 | LockfilePackages { packages } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/dataflow/manifest_packages.rs: -------------------------------------------------------------------------------- 1 | use crate::data::manifest::{Manifest, MANIFEST_FILE_NAME}; 2 | use crate::dataflow::added_packages::AddedPackages; 3 | use crate::dataflow::removed_packages::RemovedPackages; 4 | use crate::dataflow::{normalize_global_namespace, PackageKey, WapmPackageKey}; 5 | use semver::{Version, VersionReq}; 6 | use std::collections::hash_set::HashSet; 7 | use std::fs; 8 | use std::path::Path; 9 | use thiserror::Error; 10 | 11 | #[derive(Clone, Debug, Error)] 12 | pub enum Error { 13 | #[error("Could not parse manifest because {0}.")] 14 | ManifestTomlParse(String), 15 | #[error( 16 | "Version {0} for package {1} must be a semantic version or a semantic version requirement." 17 | )] 18 | SemVer(String, String), 19 | } 20 | 21 | /// A ternary for a manifest: Some, None, Error. 22 | #[derive(Debug)] 23 | #[allow(clippy::large_enum_variant)] // The happy path returns the largest variant (Manifest) 24 | pub enum ManifestResult { 25 | Manifest(Manifest), 26 | NoManifest, 27 | ManifestError(Error), 28 | } 29 | 30 | impl ManifestResult { 31 | pub fn find_in_directory>(directory: P) -> Self { 32 | let directory = directory.as_ref(); 33 | if !directory.is_dir() { 34 | return ManifestResult::NoManifest; 35 | } 36 | let manifest_path_buf = directory.join(MANIFEST_FILE_NAME); 37 | if !manifest_path_buf.is_file() { 38 | return ManifestResult::NoManifest; 39 | } 40 | let source = match fs::read_to_string(&manifest_path_buf) { 41 | Ok(s) => s, 42 | Err(_) => return ManifestResult::NoManifest, 43 | }; 44 | match toml::from_str::(&source) { 45 | Ok(mut m) => { 46 | m.base_directory_path = directory.to_owned(); 47 | ManifestResult::Manifest(m) 48 | } 49 | Err(e) => ManifestResult::ManifestError(Error::ManifestTomlParse(e.to_string())), 50 | } 51 | } 52 | } 53 | 54 | /// A convenient structure containing all modules and commands for a package stored in manifest. 55 | #[derive(Clone, Debug, Default)] 56 | pub struct ManifestPackages<'a> { 57 | pub packages: HashSet>, 58 | } 59 | 60 | impl<'a> ManifestPackages<'a> { 61 | /// Construct package keys from the manifest and any other additional packages. 62 | /// Short-hand package names are transformed. 63 | pub fn new_from_manifest_and_added_packages( 64 | manifest: &'a Manifest, 65 | added_packages: &AddedPackages<'a>, 66 | ) -> Result { 67 | let packages = ManifestPackages::extract_package_keys(manifest)?; 68 | let mut packages: HashSet = packages 69 | .into_iter() 70 | .map(normalize_global_namespace) 71 | .collect(); 72 | 73 | packages.extend(added_packages.packages.iter().cloned()); 74 | Ok(Self { packages }) 75 | } 76 | 77 | pub fn keys(&self) -> HashSet> { 78 | self.packages.iter().cloned().collect() 79 | } 80 | 81 | pub fn remove_packages(&mut self, removed_packages: &'a RemovedPackages<'a>) { 82 | let removed_package_keys = removed_packages 83 | .packages 84 | .iter() 85 | .cloned() 86 | .flat_map(|pkg_name| { 87 | self.packages 88 | .iter() 89 | .cloned() 90 | .filter(|package_key| match package_key { 91 | PackageKey::WapmPackage(WapmPackageKey { name, .. }) => name == &pkg_name, 92 | _ => false, 93 | }) 94 | .collect::>() 95 | }) 96 | .collect::>(); 97 | 98 | for removed_package_key in removed_package_keys { 99 | self.packages.remove(&removed_package_key); 100 | } 101 | } 102 | 103 | /// Extract package keys from the manifest 104 | fn extract_package_keys(manifest: &'a Manifest) -> Result>, Error> { 105 | match manifest.dependencies { 106 | Some(ref dependencies) => { 107 | let result = dependencies 108 | .iter() 109 | .map(|(name, value)| (name.as_str(), value.as_str())) 110 | .map(Self::parse_wapm_package_key) 111 | .collect::, Error>>()?; 112 | Ok(result) 113 | } 114 | None => Ok(vec![]), 115 | } 116 | } 117 | 118 | /// Parse a raw pair of strings as an exact wapm package or a range. May fail with a semver 119 | /// error. 120 | fn parse_wapm_package_key( 121 | (name, version): (&'a str, &'a str), 122 | ) -> Result, Error> { 123 | if let Ok(version) = Version::parse(version) { 124 | Ok(PackageKey::new_registry_package(name, version)) 125 | } else if let Ok(version_req) = VersionReq::parse(version) { 126 | Ok(PackageKey::new_registry_package_range(name, version_req)) 127 | } else { 128 | Err(Error::SemVer(name.to_string(), version.to_string())) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/dataflow/merged_lockfile_packages.rs: -------------------------------------------------------------------------------- 1 | use crate::data::lock::lockfile::{CommandMap, Lockfile, ModuleMap}; 2 | use crate::dataflow::bin_script::save_bin_script; 3 | use crate::dataflow::lockfile_packages::{LockfilePackage, LockfilePackages}; 4 | use crate::dataflow::retained_lockfile_packages::RetainedLockfilePackages; 5 | use crate::dataflow::{PackageKey, WapmPackageKey}; 6 | use std::collections::btree_map::BTreeMap; 7 | use std::collections::hash_map::HashMap; 8 | use std::path::Path; 9 | use thiserror::Error; 10 | 11 | #[derive(Clone, Debug, Error)] 12 | pub enum Error { 13 | #[error("Could not save generated lockfile because {0}.")] 14 | FailedToSaveLockfile(String), 15 | } 16 | 17 | /// Merge two sets, and keep upgraded packages and all other unchanged packages. 18 | /// Remove changed packages e.g. upgraded versions. 19 | #[derive(Clone, Debug)] 20 | pub struct MergedLockfilePackages<'a> { 21 | pub packages: HashMap, LockfilePackage>, 22 | } 23 | 24 | impl<'a> MergedLockfilePackages<'a> { 25 | pub fn merge( 26 | new_packages: LockfilePackages<'a>, 27 | old_packages: RetainedLockfilePackages<'a>, 28 | ) -> Self { 29 | let mut unique_packages = HashMap::new(); 30 | for (key, data) in old_packages.packages { 31 | let name = match key { 32 | PackageKey::WapmPackage(ref k) => k.name.clone(), 33 | _ => panic!("Non wapm registry keys are unsupported."), 34 | }; 35 | unique_packages.insert(name, (key, data)); 36 | } 37 | for (key, data) in new_packages.packages { 38 | let name = match key { 39 | PackageKey::WapmPackage(ref k) => k.name.clone(), 40 | _ => panic!("Non wapm registry keys are unsupported."), 41 | }; 42 | unique_packages.insert(name, (key, data)); 43 | } 44 | let packages: HashMap<_, _> = unique_packages 45 | .into_iter() 46 | .map(|(_, (key, data))| (key, data)) 47 | .collect(); 48 | 49 | Self { packages } 50 | } 51 | 52 | pub fn generate_lockfile(self, directory: &'a Path) -> Result<(), Error> { 53 | let mut modules: ModuleMap = BTreeMap::new(); 54 | let mut commands: CommandMap = BTreeMap::new(); 55 | for (key, package) in self.packages { 56 | match key { 57 | PackageKey::WapmPackage(WapmPackageKey { 58 | name: package_name, 59 | version: package_version, 60 | }) => { 61 | let versions = modules.entry(package_name.into_owned()).or_default(); 62 | let modules = versions.entry(package_version).or_default(); 63 | for module in package.modules { 64 | let name = module.name.clone(); 65 | modules.insert(name, module); 66 | } 67 | for command in package.commands { 68 | if let Some(module) = modules.get(&command.module) { 69 | let name = command.name.clone(); 70 | let script_name = command.name.clone(); 71 | let module_path = format!("{}/{}", module.package_path, module.source); 72 | commands.insert(name, command); 73 | // save the bin script to execute this command from the terminal 74 | save_bin_script( 75 | directory, 76 | script_name, 77 | module.package_path.clone(), 78 | module_path, 79 | ) 80 | .map_err(|e| Error::FailedToSaveLockfile(e.to_string()))?; 81 | } 82 | } 83 | } 84 | PackageKey::WapmPackageRange(_) => { 85 | unreachable!("Attempting to generate with lockfile with package version range.") 86 | } 87 | } 88 | } 89 | 90 | let lockfile = Lockfile { modules, commands }; 91 | 92 | lockfile 93 | .save(directory) 94 | .map_err(|e| Error::FailedToSaveLockfile(e.to_string()))?; 95 | Ok(()) 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod test { 101 | use crate::dataflow::lockfile_packages::{LockfilePackage, LockfilePackages}; 102 | use crate::dataflow::merged_lockfile_packages::MergedLockfilePackages; 103 | use crate::dataflow::retained_lockfile_packages::RetainedLockfilePackages; 104 | use crate::dataflow::PackageKey; 105 | use std::collections::HashMap; 106 | 107 | #[test] 108 | fn test_merge() { 109 | let mut new_lockfile_packages_map = HashMap::new(); 110 | let pkg_1 = PackageKey::new_registry_package("_/foo", semver::Version::new(1, 1, 0)); 111 | let pkg_2 = PackageKey::new_registry_package("_/bar", semver::Version::new(2, 0, 0)); 112 | new_lockfile_packages_map.insert( 113 | pkg_1, 114 | LockfilePackage { 115 | modules: vec![], 116 | commands: vec![], 117 | }, 118 | ); 119 | new_lockfile_packages_map.insert( 120 | pkg_2, 121 | LockfilePackage { 122 | modules: vec![], 123 | commands: vec![], 124 | }, 125 | ); 126 | let new_lockfile_packages = LockfilePackages { 127 | packages: new_lockfile_packages_map, 128 | }; 129 | 130 | let mut old_lockfile_packages_map = HashMap::new(); 131 | let pkg_1_old = PackageKey::new_registry_package("_/foo", semver::Version::new(1, 0, 0)); 132 | let pkg_2_old = PackageKey::new_registry_package("_/qux", semver::Version::new(3, 0, 0)); 133 | old_lockfile_packages_map.insert( 134 | pkg_1_old, 135 | LockfilePackage { 136 | modules: vec![], 137 | commands: vec![], 138 | }, 139 | ); 140 | old_lockfile_packages_map.insert( 141 | pkg_2_old, 142 | LockfilePackage { 143 | modules: vec![], 144 | commands: vec![], 145 | }, 146 | ); 147 | 148 | let old_lockfile_packages = RetainedLockfilePackages { 149 | packages: old_lockfile_packages_map, 150 | }; 151 | 152 | let result = MergedLockfilePackages::merge(new_lockfile_packages, old_lockfile_packages); 153 | 154 | // should now contain all old packages, and upgraded new ones 155 | assert!(result 156 | .packages 157 | .contains_key(&PackageKey::new_registry_package( 158 | "_/foo", 159 | semver::Version::new(1, 1, 0) 160 | ))); 161 | assert!(result 162 | .packages 163 | .contains_key(&PackageKey::new_registry_package( 164 | "_/bar", 165 | semver::Version::new(2, 0, 0) 166 | ))); 167 | assert!(result 168 | .packages 169 | .contains_key(&PackageKey::new_registry_package( 170 | "_/qux", 171 | semver::Version::new(3, 0, 0) 172 | ))); 173 | 174 | // should no longer contain the old foo package 175 | assert!(!result 176 | .packages 177 | .contains_key(&PackageKey::new_registry_package( 178 | "_/foo", 179 | semver::Version::new(1, 0, 0) 180 | ))); 181 | 182 | assert_eq!(3, result.packages.len()); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/dataflow/removed_lockfile_packages.rs: -------------------------------------------------------------------------------- 1 | use crate::dataflow::bin_script::delete_bin_script; 2 | use crate::dataflow::lockfile_packages::{LockfilePackage, LockfilePackages}; 3 | use crate::dataflow::manifest_packages::ManifestPackages; 4 | use crate::dataflow::removed_packages::RemovedPackages; 5 | use crate::dataflow::{bin_script, PackageKey, WapmPackageKey}; 6 | use std::collections::hash_map::HashMap; 7 | use std::collections::hash_set::HashSet; 8 | use std::path::Path; 9 | use thiserror::Error; 10 | 11 | #[derive(Clone, Debug, Error)] 12 | pub enum Error { 13 | #[error("Could not cleanup uninstalled command \"{0}\". {1}")] 14 | CommandCleanupError(String, bin_script::Error), 15 | } 16 | 17 | #[derive(Clone, Debug)] 18 | pub struct RemovedLockfilePackages<'a> { 19 | pub packages: HashMap, LockfilePackage>, 20 | } 21 | 22 | impl<'a> RemovedLockfilePackages<'a> { 23 | pub fn from_manifest_and_lockfile( 24 | manifest_packages: &'a ManifestPackages<'a>, 25 | lockfile_packages: &'a LockfilePackages<'a>, 26 | ) -> Self { 27 | // collect all removed packages 28 | let old_package_keys: HashSet<_> = lockfile_packages.packages.keys().cloned().collect(); 29 | let packages = old_package_keys 30 | .difference(&manifest_packages.packages) 31 | .map(|key| { 32 | ( 33 | key.clone(), 34 | lockfile_packages.packages.get(key).cloned().unwrap(), 35 | ) 36 | }) 37 | .collect(); 38 | Self { packages } 39 | } 40 | 41 | pub fn from_removed_packages_and_lockfile( 42 | removed_packages: &'a RemovedPackages<'a>, 43 | lockfile_packages: &'a LockfilePackages<'a>, 44 | ) -> Self { 45 | let packages = removed_packages 46 | .packages 47 | .iter() 48 | .cloned() 49 | .filter_map(|removed_package_name| { 50 | lockfile_packages 51 | .packages 52 | .iter() 53 | .find(|(key, _)| match key { 54 | PackageKey::WapmPackage(WapmPackageKey { name, .. }) => { 55 | name == &removed_package_name 56 | } 57 | _ => unreachable!( 58 | "Lockfile should only contain exact wapm package versions." 59 | ), 60 | }) 61 | .map(|(key, data)| (key.clone(), data.clone())) 62 | }) 63 | .collect(); 64 | Self { packages } 65 | } 66 | 67 | /// This will do the required cleanup of old artifacts like bin scripts and wapm packages 68 | pub fn cleanup_old_packages>(self, directory: P) -> Result<(), Error> { 69 | let directory = directory.as_ref(); 70 | for (_key, data) in self.packages { 71 | for command in data.commands { 72 | delete_bin_script(directory, command.name.clone()) 73 | .map_err(|e| Error::CommandCleanupError(command.name.clone(), e))?; 74 | } 75 | // TODO cleanup wapm_packages 76 | } 77 | Ok(()) 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod test { 83 | use crate::dataflow::lockfile_packages::{LockfilePackage, LockfilePackages}; 84 | use crate::dataflow::manifest_packages::ManifestPackages; 85 | use crate::dataflow::removed_lockfile_packages::RemovedLockfilePackages; 86 | use crate::dataflow::removed_packages::RemovedPackages; 87 | use crate::dataflow::PackageKey; 88 | use std::collections::hash_set::HashSet; 89 | use std::collections::HashMap; 90 | 91 | #[test] 92 | fn get_removed_lockfile_packages_from_manifest_and_lockfile() { 93 | let mut manifest_packages = ManifestPackages::default(); 94 | let mut packages = HashSet::default(); 95 | packages.insert(PackageKey::new_registry_package( 96 | "_/foo", 97 | semver::Version::parse("1.0.0").unwrap(), 98 | )); 99 | packages.insert(PackageKey::new_registry_package( 100 | "_/bar", 101 | semver::Version::parse("2.0.0").unwrap(), 102 | )); 103 | manifest_packages.packages = packages; 104 | 105 | let mut lockfile_packages = LockfilePackages::default(); 106 | let mut packages = HashMap::default(); 107 | packages.insert( 108 | PackageKey::new_registry_package("_/foo", semver::Version::parse("1.0.0").unwrap()), 109 | LockfilePackage::default(), 110 | ); 111 | packages.insert( 112 | PackageKey::new_registry_package("_/bar", semver::Version::parse("2.0.0").unwrap()), 113 | LockfilePackage::default(), 114 | ); 115 | packages.insert( 116 | PackageKey::new_registry_package("_/baz", semver::Version::parse("3.0.0").unwrap()), 117 | LockfilePackage::default(), 118 | ); 119 | lockfile_packages.packages = packages; 120 | 121 | let removed_lockfile_packages = RemovedLockfilePackages::from_manifest_and_lockfile( 122 | &manifest_packages, 123 | &lockfile_packages, 124 | ); 125 | assert_eq!(1, removed_lockfile_packages.packages.len()); 126 | removed_lockfile_packages 127 | .packages 128 | .get(&PackageKey::new_registry_package( 129 | "_/baz", 130 | semver::Version::parse("3.0.0").unwrap(), 131 | )) 132 | .unwrap(); 133 | } 134 | 135 | #[test] 136 | fn get_removed_lockfile_packages_from_removed_packages_and_lockfile() { 137 | let mut removed_packages = RemovedPackages::default(); 138 | let mut packages = HashSet::default(); 139 | packages.insert("_/foo".into()); 140 | packages.insert("_/bar".into()); 141 | removed_packages.packages = packages; 142 | 143 | let mut lockfile_packages = LockfilePackages::default(); 144 | let mut packages = HashMap::default(); 145 | packages.insert( 146 | PackageKey::new_registry_package("_/foo", semver::Version::parse("1.0.0").unwrap()), 147 | LockfilePackage::default(), 148 | ); 149 | packages.insert( 150 | PackageKey::new_registry_package("_/bar", semver::Version::parse("2.0.0").unwrap()), 151 | LockfilePackage::default(), 152 | ); 153 | packages.insert( 154 | PackageKey::new_registry_package("_/baz", semver::Version::parse("3.0.0").unwrap()), 155 | LockfilePackage::default(), 156 | ); 157 | lockfile_packages.packages = packages; 158 | 159 | let removed_lockfile_packages = RemovedLockfilePackages::from_removed_packages_and_lockfile( 160 | &removed_packages, 161 | &lockfile_packages, 162 | ); 163 | assert_eq!(2, removed_lockfile_packages.packages.len()); 164 | removed_lockfile_packages 165 | .packages 166 | .get(&PackageKey::new_registry_package( 167 | "_/bar", 168 | semver::Version::parse("2.0.0").unwrap(), 169 | )) 170 | .unwrap(); 171 | removed_lockfile_packages 172 | .packages 173 | .get(&PackageKey::new_registry_package( 174 | "_/foo", 175 | semver::Version::parse("1.0.0").unwrap(), 176 | )) 177 | .unwrap(); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/dataflow/removed_packages.rs: -------------------------------------------------------------------------------- 1 | use crate::dataflow::normalize_global_namespace_package_name; 2 | use std::borrow::Cow; 3 | use std::collections::hash_set::HashSet; 4 | 5 | /// Holds packages that are removed on the cli 6 | #[derive(Debug, Default)] 7 | pub struct RemovedPackages<'a> { 8 | pub packages: HashSet>, 9 | } 10 | 11 | impl<'a> RemovedPackages<'a> { 12 | pub fn new_from_package_names(removed_packages: Vec<&'a str>) -> Self { 13 | let packages = removed_packages 14 | .into_iter() 15 | .map(|package_name| normalize_global_namespace_package_name(package_name.into())) 16 | .collect(); 17 | Self { packages } 18 | } 19 | } 20 | 21 | #[cfg(test)] 22 | mod test { 23 | use crate::dataflow::lockfile_packages::{LockfilePackage, LockfilePackages}; 24 | use crate::dataflow::removed_packages::RemovedPackages; 25 | use crate::dataflow::PackageKey; 26 | use std::collections::HashMap; 27 | 28 | #[test] 29 | fn remove_from_lockfile_packages() { 30 | let mut packages = HashMap::default(); 31 | packages.insert( 32 | PackageKey::new_registry_package("_/foo", semver::Version::parse("1.0.0").unwrap()), 33 | LockfilePackage::default(), 34 | ); 35 | packages.insert( 36 | PackageKey::new_registry_package("_/bar", semver::Version::parse("2.0.0").unwrap()), 37 | LockfilePackage::default(), 38 | ); 39 | let mut lockfile_packages = LockfilePackages { packages }; 40 | 41 | let removed_packages = RemovedPackages::new_from_package_names(vec!["_/foo"]); 42 | 43 | lockfile_packages.remove_packages(removed_packages); 44 | 45 | assert_eq!(1, lockfile_packages.packages.len()); 46 | lockfile_packages 47 | .packages 48 | .get(&PackageKey::new_registry_package( 49 | "_/bar", 50 | semver::Version::parse("2.0.0").unwrap(), 51 | )) 52 | .unwrap(); 53 | } 54 | 55 | #[test] 56 | fn remove_from_lockfile_packages_using_global_namespace_shorthand() { 57 | let mut packages = HashMap::default(); 58 | packages.insert( 59 | PackageKey::new_registry_package("_/foo", semver::Version::parse("1.0.0").unwrap()), 60 | LockfilePackage::default(), 61 | ); 62 | packages.insert( 63 | PackageKey::new_registry_package("_/bar", semver::Version::parse("2.0.0").unwrap()), 64 | LockfilePackage::default(), 65 | ); 66 | let mut lockfile_packages = LockfilePackages { packages }; 67 | 68 | let removed_packages = RemovedPackages::new_from_package_names(vec!["foo"]); 69 | 70 | lockfile_packages.remove_packages(removed_packages); 71 | 72 | assert_eq!(1, lockfile_packages.packages.len()); 73 | lockfile_packages 74 | .packages 75 | .get(&PackageKey::new_registry_package( 76 | "_/bar", 77 | semver::Version::parse("2.0.0").unwrap(), 78 | )) 79 | .unwrap(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/dataflow/retained_lockfile_packages.rs: -------------------------------------------------------------------------------- 1 | use crate::dataflow::lockfile_packages::{LockfilePackage, LockfilePackages}; 2 | use crate::dataflow::manifest_packages::ManifestPackages; 3 | use crate::dataflow::PackageKey; 4 | use std::collections::hash_set::HashSet; 5 | use std::collections::HashMap; 6 | 7 | pub struct RetainedLockfilePackages<'a> { 8 | pub packages: HashMap, LockfilePackage>, 9 | } 10 | 11 | impl<'a> RetainedLockfilePackages<'a> { 12 | pub fn from_manifest_and_lockfile( 13 | manifest_packages: &'a ManifestPackages<'a>, 14 | lockfile_packages: LockfilePackages<'a>, 15 | ) -> Self { 16 | let manifest_keys = manifest_packages.keys(); 17 | let lockfile_keys: HashSet<_> = lockfile_packages.packages.keys().cloned().collect(); 18 | let keys: HashSet<_> = manifest_keys 19 | .intersection(&lockfile_keys) 20 | .cloned() 21 | .collect(); 22 | 23 | let packages: HashMap<_, _> = lockfile_packages 24 | .packages 25 | .into_iter() 26 | .filter(|(k, _)| keys.contains(k)) 27 | .collect(); 28 | 29 | RetainedLockfilePackages { packages } 30 | } 31 | 32 | pub fn from_lockfile_packages(lockfile_packages: LockfilePackages<'a>) -> Self { 33 | Self { 34 | packages: lockfile_packages.packages, 35 | } 36 | } 37 | } 38 | 39 | #[cfg(test)] 40 | mod retained_lockfile_packages_tests { 41 | use crate::dataflow::lockfile_packages::{LockfilePackage, LockfilePackages}; 42 | use crate::dataflow::manifest_packages::ManifestPackages; 43 | use crate::dataflow::retained_lockfile_packages::RetainedLockfilePackages; 44 | use crate::dataflow::PackageKey; 45 | use std::collections::hash_set::HashSet; 46 | use std::collections::HashMap; 47 | 48 | #[test] 49 | fn retain_lockfile_packages() { 50 | let mut manifest_package_keys = HashSet::new(); 51 | 52 | // one upgrade package, one new package 53 | manifest_package_keys.insert(PackageKey::new_registry_package( 54 | "_/foo", 55 | semver::Version::new(1, 1, 0), 56 | )); 57 | manifest_package_keys.insert(PackageKey::new_registry_package( 58 | "_/bar", 59 | semver::Version::new(2, 2, 0), 60 | )); 61 | manifest_package_keys.insert(PackageKey::new_registry_package( 62 | "_/baz", 63 | semver::Version::new(11, 11, 11), 64 | )); 65 | 66 | let manifest_packages = ManifestPackages { 67 | packages: manifest_package_keys, 68 | }; 69 | 70 | let mut lockfile_package_map = HashMap::new(); 71 | // one existing package that is upgraded, and one old package, and one unchanged 72 | lockfile_package_map.insert( 73 | PackageKey::new_registry_package("_/foo", semver::Version::new(1, 0, 0)), 74 | LockfilePackage { 75 | modules: vec![], 76 | commands: vec![], 77 | }, 78 | ); 79 | lockfile_package_map.insert( 80 | PackageKey::new_registry_package("_/baz", semver::Version::new(11, 11, 11)), 81 | LockfilePackage { 82 | modules: vec![], 83 | commands: vec![], 84 | }, 85 | ); 86 | lockfile_package_map.insert( 87 | PackageKey::new_registry_package("_/qux", semver::Version::new(3, 0, 0)), 88 | LockfilePackage { 89 | modules: vec![], 90 | commands: vec![], 91 | }, 92 | ); 93 | 94 | let lockfile_packages = LockfilePackages { 95 | packages: lockfile_package_map, 96 | }; 97 | 98 | let retained_lockfile_packages = RetainedLockfilePackages::from_manifest_and_lockfile( 99 | &manifest_packages, 100 | lockfile_packages, 101 | ); 102 | 103 | assert_eq!(1, retained_lockfile_packages.packages.len()); 104 | // should only contain this one: 105 | assert!(retained_lockfile_packages.packages.contains_key( 106 | &PackageKey::new_registry_package("_/baz", semver::Version::new(11, 11, 11)) 107 | )); 108 | // should not contain these: 109 | assert!(!retained_lockfile_packages.packages.contains_key( 110 | &PackageKey::new_registry_package("_/qux", semver::Version::new(3, 0, 0)) 111 | )); 112 | assert!(!retained_lockfile_packages.packages.contains_key( 113 | &PackageKey::new_registry_package("_/foo", semver::Version::new(1, 1, 0)) 114 | )); 115 | assert!(!retained_lockfile_packages.packages.contains_key( 116 | &PackageKey::new_registry_package("_/foo", semver::Version::new(1, 0, 0)) 117 | )); 118 | assert!(!retained_lockfile_packages.packages.contains_key( 119 | &PackageKey::new_registry_package("_/bar", semver::Version::new(2, 2, 0)) 120 | )); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/graphql.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::{QueryBody, Response}; 2 | use std::env; 3 | use std::string::ToString; 4 | use thiserror::Error; 5 | #[cfg(not(target_os = "wasi"))] 6 | use { 7 | crate::proxy, 8 | reqwest::{ 9 | blocking::{multipart::Form, Client}, 10 | header::USER_AGENT, 11 | }, 12 | }; 13 | #[cfg(target_os = "wasi")] 14 | use {wasm_bus_reqwest::prelude::header::*, wasm_bus_reqwest::prelude::*}; 15 | 16 | use crate::util::whoami_distro; 17 | 18 | use super::config::Config; 19 | 20 | #[derive(Debug, Error)] 21 | enum GraphQLError { 22 | #[error("{message}")] 23 | Error { message: String }, 24 | } 25 | 26 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 27 | pub type DateTime = String; 28 | 29 | pub fn execute_query_modifier(query: &QueryBody, form_modifier: F) -> anyhow::Result 30 | where 31 | for<'de> R: serde::Deserialize<'de>, 32 | V: serde::Serialize, 33 | F: FnOnce(Form) -> Form, 34 | { 35 | let config = Config::from_file()?; 36 | let registry_url = &config.registry.get_graphql_url(); 37 | execute_query_modifier_inner(registry_url, query, form_modifier) 38 | } 39 | 40 | pub fn execute_query_modifier_inner( 41 | registry_url: &str, 42 | query: &QueryBody, 43 | form_modifier: F, 44 | ) -> anyhow::Result 45 | where 46 | for<'de> R: serde::Deserialize<'de>, 47 | V: serde::Serialize, 48 | F: FnOnce(Form) -> Form, 49 | { 50 | let client = { 51 | let builder = Client::builder(); 52 | 53 | #[cfg(not(target_os = "wasi"))] 54 | let builder = if let Some(proxy) = proxy::maybe_set_up_proxy()? { 55 | builder.proxy(proxy) 56 | } else { 57 | builder 58 | }; 59 | builder.build()? 60 | }; 61 | let config = Config::from_file()?; 62 | 63 | let vars = serde_json::to_string(&query.variables).unwrap(); 64 | 65 | let form = Form::new() 66 | .text("query", query.query.to_string()) 67 | .text("operationName", query.operation_name.to_string()) 68 | .text("variables", vars); 69 | 70 | let form = form_modifier(form); 71 | 72 | let user_agent = format!( 73 | "wapm/{} {} {}", 74 | VERSION, 75 | whoami::platform(), 76 | whoami_distro(), 77 | ); 78 | 79 | let res = client 80 | .post(registry_url) 81 | .multipart(form) 82 | .bearer_auth(env::var("WAPM_REGISTRY_TOKEN").unwrap_or_else(|_| { 83 | config 84 | .registry 85 | .get_login_token_for_registry(&config.registry.get_current_registry()) 86 | .unwrap_or_default() 87 | })) 88 | .header(USER_AGENT, user_agent) 89 | .send()?; 90 | 91 | let response_body: Response = res.json()?; 92 | if let Some(errors) = response_body.errors { 93 | let error_messages: Vec = errors.into_iter().map(|err| err.message).collect(); 94 | return Err(GraphQLError::Error { 95 | message: error_messages.join(", "), 96 | } 97 | .into()); 98 | } 99 | Ok(response_body.data.expect("missing response data")) 100 | } 101 | 102 | pub fn execute_query(query: &QueryBody) -> anyhow::Result 103 | where 104 | for<'de> R: serde::Deserialize<'de>, 105 | V: serde::Serialize, 106 | { 107 | execute_query_modifier(query, |f| f) 108 | } 109 | 110 | pub fn execute_query_custom_registry( 111 | registry_url: &str, 112 | query: &QueryBody, 113 | ) -> anyhow::Result 114 | where 115 | for<'de> R: serde::Deserialize<'de>, 116 | V: serde::Serialize, 117 | { 118 | execute_query_modifier_inner(registry_url, query, |f| f) 119 | } 120 | -------------------------------------------------------------------------------- /src/integration_tests/add_remove_init.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | 3 | use maplit::hashmap; 4 | 5 | use super::prelude::*; 6 | 7 | #[test] 8 | fn it_works() { 9 | let _t = set_test_dir_to_new_temp_dir(); 10 | set_registry_to_dev().unwrap(); 11 | init_manifest().unwrap(); 12 | let manifest = get_manifest().unwrap(); 13 | assert!(manifest.dependencies.is_none()); 14 | 15 | { 16 | let result = add_dependencies(&["this-package-does-not-exist"]); 17 | assert!(result.is_err()); 18 | let manifest = get_manifest().unwrap(); 19 | assert!(manifest.dependencies.is_none()); 20 | } 21 | { 22 | add_dependencies(&["mark2/python@0.0.4", "mark2/dog2"]).unwrap(); 23 | let manifest = get_manifest().unwrap(); 24 | assert_eq!( 25 | manifest.dependencies, 26 | Some(hashmap! { 27 | "mark2/python".to_string() => "0.0.4".to_string(), 28 | "mark2/dog2".to_string() => "0.0.13".to_string(), 29 | }) 30 | ); 31 | } 32 | { 33 | add_dependencies(&["lolcat@0.1.1"]).unwrap(); 34 | let manifest_before = get_manifest().unwrap(); 35 | remove_dependencies(&["lolcat"]).unwrap(); 36 | let manifest_after = get_manifest().unwrap(); 37 | 38 | assert_eq!( 39 | manifest_before.dependencies, 40 | Some(hashmap! { 41 | "mark2/python".to_string() => "0.0.4".to_string(), 42 | "mark2/dog2".to_string() => "0.0.13".to_string(), 43 | "lolcat".to_string() => "0.1.1".to_string(), 44 | }) 45 | ); 46 | assert_eq!( 47 | manifest_after.dependencies, 48 | Some(hashmap! { 49 | "mark2/python".to_string() => "0.0.4".to_string(), 50 | "mark2/dog2".to_string() => "0.0.13".to_string(), 51 | }) 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/integration_tests/data.rs: -------------------------------------------------------------------------------- 1 | pub use wapm_toml::integration_tests::data::*; 2 | -------------------------------------------------------------------------------- /src/integration_tests/mod.rs: -------------------------------------------------------------------------------- 1 | //! An experimental way to write integration tests 2 | 3 | mod add_remove_init; 4 | pub mod data; 5 | pub mod prelude; 6 | -------------------------------------------------------------------------------- /src/integration_tests/prelude.rs: -------------------------------------------------------------------------------- 1 | //! The functions needed to write integration tests 2 | 3 | use crate::commands::*; 4 | use crate::data::manifest::{Manifest, ManifestError}; 5 | use crate::util::create_temp_dir; 6 | 7 | /// Runs `wapm config set registry.url https://registry.wapm.dev` 8 | pub fn set_registry_to_dev() -> anyhow::Result<()> { 9 | config(ConfigOpt::set( 10 | "registry.url".to_string(), 11 | "https://registry.wapm.dev".to_string(), 12 | )) 13 | } 14 | 15 | pub fn set_test_dir_to_new_temp_dir() -> tempfile::TempDir { 16 | let new_dir = create_temp_dir().expect("Could not create temp dir"); 17 | let new_dir_path: &std::path::Path = new_dir.as_ref(); 18 | let new_cur_dir = new_dir_path.join("integration_test"); 19 | std::fs::create_dir(&new_cur_dir).expect("Could not create subdir"); 20 | std::env::set_current_dir(new_cur_dir) 21 | .expect("Could not set current directory to temporary directory"); 22 | new_dir 23 | } 24 | 25 | /// Runs `wapm init` 26 | pub fn init_manifest() -> anyhow::Result<()> { 27 | init(InitOpt::new(true)) 28 | } 29 | 30 | /// Runs `wapm add` 31 | pub fn add_dependencies(deps: &[&str]) -> anyhow::Result<()> { 32 | add(AddOpt::new(deps.iter().map(|s| s.to_string()).collect())) 33 | } 34 | 35 | /// Runs `wapm remove` 36 | pub fn remove_dependencies(deps: &[&str]) -> anyhow::Result<()> { 37 | remove(RemoveOpt::new(deps.iter().map(|s| s.to_string()).collect())) 38 | } 39 | 40 | /// Gets the thread local Manifest 41 | pub fn get_manifest() -> Result { 42 | Manifest::find_in_directory("this isn't used in the test impl right now") 43 | } 44 | -------------------------------------------------------------------------------- /src/interfaces.rs: -------------------------------------------------------------------------------- 1 | use crate::database::*; 2 | use crate::sql; 3 | 4 | use rusqlite::{params, Connection, TransactionBehavior}; 5 | 6 | pub fn interface_exists( 7 | conn: &mut Connection, 8 | interface_name: &str, 9 | version: &str, 10 | ) -> anyhow::Result { 11 | let mut stmt = conn.prepare(sql::WASM_INTERFACE_EXISTENCE_CHECK)?; 12 | Ok(stmt.exists(params![interface_name, version])?) 13 | } 14 | 15 | pub fn load_interface_from_db( 16 | conn: &mut Connection, 17 | interface_name: &str, 18 | version: &str, 19 | ) -> anyhow::Result { 20 | let mut stmt = conn.prepare(sql::GET_WASM_INTERFACE)?; 21 | let interface_string: String = 22 | stmt.query_row(params![interface_name, version], |row| row.get(0))?; 23 | 24 | wasmer_wasm_interface::parser::parse_interface(&interface_string).map_err(|e| { 25 | anyhow!( 26 | "Failed to parse interface {} version {} in database: {}", 27 | interface_name, 28 | version, 29 | e 30 | ) 31 | }) 32 | } 33 | 34 | pub fn import_interface( 35 | conn: &mut Connection, 36 | interface_name: &str, 37 | version: &str, 38 | content: &str, 39 | ) -> anyhow::Result<()> { 40 | // fail if we already have this interface 41 | { 42 | let mut key_check = conn.prepare(sql::WASM_INTERFACE_EXISTENCE_CHECK)?; 43 | let result = key_check.exists(params![interface_name, version])?; 44 | 45 | if result { 46 | return Err(anyhow!( 47 | "Interface {}, version {} already exists", 48 | interface_name, 49 | version 50 | )); 51 | } 52 | } 53 | 54 | let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; 55 | let time_string = get_current_time_in_format().expect("Could not get current time"); 56 | 57 | debug!("Adding interface {:?} {:?}", interface_name, version); 58 | tx.execute( 59 | sql::INSERT_WASM_INTERFACE, 60 | params![interface_name, version, time_string, content], 61 | )?; 62 | 63 | tx.commit()?; 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | #[cfg(feature = "package")] 4 | #[macro_use] 5 | extern crate lazy_static; 6 | #[macro_use] 7 | extern crate anyhow; 8 | #[macro_use] 9 | extern crate serde_derive; 10 | #[cfg(feature = "prettytable-rs")] 11 | #[macro_use] 12 | extern crate prettytable; 13 | 14 | #[cfg(test)] 15 | #[macro_use] 16 | extern crate toml; 17 | 18 | #[cfg(feature = "integration_tests")] 19 | pub mod integration_tests; 20 | 21 | mod abi; 22 | pub mod commands; 23 | mod config; 24 | mod constants; 25 | pub mod data; 26 | #[cfg(feature = "full")] 27 | mod database; 28 | mod dataflow; 29 | mod graphql; 30 | mod init; 31 | #[cfg(feature = "full")] 32 | mod interfaces; 33 | mod keys; 34 | pub mod logging; 35 | #[cfg(not(target_os = "wasi"))] 36 | mod proxy; 37 | mod sql; 38 | #[cfg(feature = "update-notifications")] 39 | pub mod update_notifier; 40 | pub mod util; 41 | mod validate; 42 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for setting up logging 2 | 3 | use crate::config::Config; 4 | use crate::util; 5 | use fern::colors::{Color, ColoredLevelConfig}; 6 | use std::fs; 7 | use std::io; 8 | use std::sync::atomic::{AtomicUsize, Ordering}; 9 | use thiserror::Error; 10 | 11 | static STDOUT_LINE_COUNTER: AtomicUsize = AtomicUsize::new(0); 12 | 13 | fn get_num_stdout_lines_logged() -> usize { 14 | STDOUT_LINE_COUNTER.load(Ordering::Acquire) 15 | } 16 | 17 | /// Updates counter with the lines printed to stdout 18 | pub(crate) fn add_lines_printed_to_stdout(num_lines: usize) { 19 | STDOUT_LINE_COUNTER.fetch_add(num_lines, Ordering::AcqRel); 20 | } 21 | 22 | /// Note: this function doesn't lock the atomic, so using it from 23 | /// multiple threads may not work. 24 | pub(crate) fn clear_stdout() -> io::Result<()> { 25 | use std::io::Write; 26 | 27 | let stdout = io::stdout(); 28 | let mut f = stdout.lock(); 29 | let num_lines_to_clear = get_num_stdout_lines_logged(); 30 | for _ in 0..num_lines_to_clear { 31 | // ANSI escape codes for: 32 | // - go up one line: \x1B[A 33 | // - clear the line: \x1B[2K 34 | write!(f, "\x1B[1A\x1B[2K")?; 35 | } 36 | f.flush()?; 37 | Ok(()) 38 | } 39 | 40 | /// Subroutine to instantiate the loggers 41 | pub fn set_up_logging(count_lines: bool) -> anyhow::Result<()> { 42 | let colors_line = ColoredLevelConfig::new() 43 | .error(Color::Red) 44 | .warn(Color::Yellow) 45 | .trace(Color::BrightBlack); 46 | let should_color = util::wapm_should_print_color(); 47 | 48 | let colors_level = colors_line.info(Color::Green); 49 | let dispatch = fern::Dispatch::new() 50 | // stdout and stderr logging 51 | .chain({ 52 | let base = if should_color { 53 | fern::Dispatch::new() 54 | .level(log::LevelFilter::Info) 55 | .filter(|metadata| metadata.target().starts_with("wapm_cli")) 56 | .format(move |out, message, record| { 57 | if count_lines && record.level() == log::Level::Info { 58 | let num_lines = message.to_string().lines().count(); 59 | add_lines_printed_to_stdout(num_lines); 60 | } 61 | out.finish(format_args!( 62 | "{color_line}[{level}{color_line}]{ansi_close} {message}", 63 | color_line = format_args!( 64 | "\x1B[{}m", 65 | colors_line.get_color(&record.level()).to_fg_str() 66 | ), 67 | level = colors_level.color(record.level()), 68 | ansi_close = "\x1B[0m", 69 | message = message, 70 | )); 71 | }) 72 | } else { 73 | // default formatter without color 74 | fern::Dispatch::new() 75 | .level(log::LevelFilter::Info) 76 | .filter(|metadata| metadata.target().starts_with("wapm_cli")) 77 | .format(move |out, message, record| { 78 | if count_lines && record.level() == log::Level::Info { 79 | let num_lines = message.to_string().lines().count(); 80 | add_lines_printed_to_stdout(num_lines); 81 | } 82 | out.finish(format_args!( 83 | "[{level}] {message}", 84 | level = record.level(), 85 | message = message, 86 | )); 87 | }) 88 | }; 89 | base 90 | // stdout 91 | .chain( 92 | fern::Dispatch::new() 93 | .filter(|metadata| metadata.level() == log::LevelFilter::Info) 94 | .chain(std::io::stdout()), 95 | ) 96 | // stderr 97 | .chain( 98 | fern::Dispatch::new() 99 | .filter(|metadata| { 100 | // lower is higher priority 101 | metadata.level() <= log::LevelFilter::Warn 102 | && metadata.target().starts_with("wapm_cli") 103 | }) 104 | .chain(std::io::stderr()), 105 | ) 106 | }); 107 | 108 | // verbose logging to file 109 | let dispatch = if let Ok(wasmer_dir) = Config::get_folder() { 110 | let log_out = wasmer_dir.join("wapm.log"); 111 | dispatch.chain( 112 | fern::Dispatch::new() 113 | .level(log::LevelFilter::Debug) 114 | .level_for("hyper", log::LevelFilter::Info) 115 | .level_for("tokio_reactor", log::LevelFilter::Info) 116 | .format(move |out, message, record| { 117 | out.finish(format_args!( 118 | "[{date}][{level}][{target}][{file}:{line}] {message}", 119 | date = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), 120 | target = record.target(), 121 | level = record.level(), 122 | message = message, 123 | file = record.file().unwrap_or(""), 124 | line = record 125 | .line() 126 | .map(|line| format!("{}", line)) 127 | .unwrap_or_else(|| "".to_string()), 128 | )); 129 | }) 130 | .chain( 131 | fs::OpenOptions::new() 132 | .write(true) 133 | .create(true) 134 | .truncate(true) 135 | .open(log_out.clone()) 136 | .map_err(|e| { 137 | LoggingError::FailedToOpenLoggingFile(format!( 138 | "log_out: {:?}, error type: {:?}", 139 | log_out, 140 | e.kind() 141 | )) 142 | })?, 143 | ), 144 | ) 145 | } else { 146 | dispatch 147 | }; 148 | 149 | dispatch 150 | .apply() 151 | .map_err(|e| LoggingError::FailedToInstantiateLogger(format!("{}", e)))?; 152 | 153 | trace!("Logging set up"); 154 | Ok(()) 155 | } 156 | 157 | #[derive(Debug, Error)] 158 | pub enum LoggingError { 159 | #[error("Failed to open logging file in WASMER_DIR: {0}")] 160 | FailedToOpenLoggingFile(String), 161 | #[error("Something went wrong setting up logging: {0}")] 162 | FailedToInstantiateLogger(String), 163 | } 164 | -------------------------------------------------------------------------------- /src/proxy.rs: -------------------------------------------------------------------------------- 1 | //! Code for dealing with setting things up to proxy network requests 2 | use thiserror::Error; 3 | 4 | #[derive(Debug, Error)] 5 | pub enum ProxyError { 6 | #[error("Failed to parse URL from {}: {}", url_location, error_message)] 7 | UrlParseError { 8 | url_location: String, 9 | error_message: String, 10 | }, 11 | 12 | #[error("Could not connect to proxy: {0}")] 13 | ConnectionError(String), 14 | } 15 | 16 | /// Tries to set up a proxy 17 | /// 18 | /// This function reads from wapm config's `proxy.url` first, then checks 19 | /// `ALL_PROXY`, `HTTPS_PROXY`, and `HTTP_PROXY` environment variables, in both 20 | /// upper case and lower case, in that order. 21 | /// 22 | /// If a proxy is specified in wapm config's `proxy.url`, it is assumed 23 | /// to be a general proxy 24 | /// 25 | /// A return value of `Ok(None)` means that there was no attempt to set up a proxy, 26 | /// `Ok(Some(proxy))` means that the proxy was set up successfully, and `Err(e)` that 27 | /// there was a failure while attempting to set up the proxy. 28 | pub fn maybe_set_up_proxy() -> anyhow::Result> { 29 | use std::env; 30 | let maybe_proxy_url = crate::config::Config::from_file() 31 | .ok() 32 | .and_then(|config| config.proxy.url); 33 | let proxy = if let Some(proxy_url) = maybe_proxy_url { 34 | reqwest::Proxy::all(&proxy_url).map(|proxy| (proxy_url, proxy, "`proxy.url` config key")) 35 | } else if let Ok(proxy_url) = env::var("ALL_PROXY").or_else(|_| env::var("all_proxy")) { 36 | reqwest::Proxy::all(&proxy_url).map(|proxy| (proxy_url, proxy, "ALL_PROXY")) 37 | } else if let Ok(https_proxy_url) = env::var("HTTPS_PROXY").or_else(|_| env::var("https_proxy")) 38 | { 39 | reqwest::Proxy::https(&https_proxy_url).map(|proxy| (https_proxy_url, proxy, "HTTPS_PROXY")) 40 | } else if let Ok(http_proxy_url) = env::var("HTTP_PROXY").or_else(|_| env::var("http_proxy")) { 41 | reqwest::Proxy::http(&http_proxy_url).map(|proxy| (http_proxy_url, proxy, "http_proxy")) 42 | } else { 43 | return Ok(None); 44 | } 45 | .map_err(|e| ProxyError::ConnectionError(e.to_string())) 46 | .and_then( 47 | |(proxy_url_str, proxy, url_location): (String, _, &'static str)| { 48 | url::Url::parse(&proxy_url_str) 49 | .map_err(|e| ProxyError::UrlParseError { 50 | url_location: url_location.to_string(), 51 | error_message: e.to_string(), 52 | }) 53 | .map(|url| { 54 | if !(url.username().is_empty()) && url.password().is_some() { 55 | proxy.basic_auth(url.username(), url.password().unwrap_or_default()) 56 | } else { 57 | proxy 58 | } 59 | }) 60 | }, 61 | )?; 62 | 63 | Ok(Some(proxy)) 64 | } 65 | -------------------------------------------------------------------------------- /src/sql/migrations/0000.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE personal_keys 2 | ( 3 | id integer primary key, 4 | active integer not null, 5 | public_key_id text not null UNIQUE, 6 | public_key_value text not null UNIQUE, 7 | private_key_location text UNIQUE, 8 | key_type_identifier text not null, 9 | date_added text not null 10 | ); 11 | 12 | CREATE TABLE wapm_users 13 | ( 14 | id integer primary key, 15 | name text not null UNIQUE 16 | ); 17 | 18 | CREATE TABLE wapm_public_keys 19 | ( 20 | id integer primary key, 21 | public_key_id text not null UNIQUE, 22 | user_key integer not null, 23 | public_key_value text not null UNIQUE, 24 | key_type_identifier text not null, 25 | date_added text not null, 26 | FOREIGN KEY(user_key) REFERENCES wapm_users(id) 27 | ); 28 | -------------------------------------------------------------------------------- /src/sql/migrations/0001.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE wasm_contracts 2 | ( 3 | id integer primary key, 4 | contract_name text not null, 5 | version text not null, 6 | date_added text not null, 7 | content text not null, 8 | CONSTRAINT name_version_unique UNIQUE (contract_name, version) 9 | ); 10 | -------------------------------------------------------------------------------- /src/sql/migrations/0002.sql: -------------------------------------------------------------------------------- 1 | PRAGMA foreign_keys=off; 2 | 3 | CREATE TABLE wasm_interfaces 4 | ( 5 | id integer primary key, 6 | interface_name text not null, 7 | version text not null, 8 | date_added text not null, 9 | content text not null, 10 | CONSTRAINT name_version_unique UNIQUE (interface_name, version) 11 | ); 12 | 13 | INSERT INTO wasm_interfaces 14 | ( 15 | id, 16 | interface_name, 17 | version, 18 | date_added, 19 | content 20 | ) 21 | SELECT id, contract_name, version, date_added, content 22 | FROM wasm_contracts; 23 | 24 | DROP TABLE wasm_contracts; 25 | 26 | PRAGMA foreign_keys=on; 27 | -------------------------------------------------------------------------------- /src/sql/mod.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | not(feature = "full"), 3 | allow(dead_code, unused_imports, unused_variables) 4 | )] 5 | pub const GET_PERSONAL_KEYS: &str = include_str!("queries/get_personal_keys.sql"); 6 | pub const GET_WAPM_PUBLIC_KEYS: &str = include_str!("queries/get_wapm_public_keys.sql"); 7 | pub const DELETE_PERSONAL_KEY_PAIR: &str = include_str!("queries/delete_personal_key_pair.sql"); 8 | pub const PERSONAL_PUBLIC_KEY_VALUE_EXISTENCE_CHECK: &str = 9 | include_str!("queries/personal_public_key_value_existence_check.sql"); 10 | pub const PERSONAL_PRIVATE_KEY_VALUE_EXISTENCE_CHECK: &str = 11 | include_str!("queries/personal_private_key_value_existence_check.sql"); 12 | pub const WAPM_PUBLIC_KEY_EXISTENCE_CHECK: &str = 13 | include_str!("queries/wapm_public_key_existence_check.sql"); 14 | pub const INSERT_AND_ACTIVATE_PERSONAL_KEY_PAIR: &str = 15 | include_str!("queries/insert_and_activate_personal_key_pair.sql"); 16 | pub const INSERT_WAPM_PUBLIC_KEY: &str = include_str!("queries/insert_wapm_public_key.sql"); 17 | pub const INSERT_USER: &str = include_str!("queries/insert_user.sql"); 18 | pub const GET_LATEST_PUBLIC_KEY_FOR_USER: &str = 19 | include_str!("queries/get_latest_public_key_for_user.sql"); 20 | pub const WASM_INTERFACE_EXISTENCE_CHECK: &str = 21 | include_str!("queries/wasm_interface_existence_check.sql"); 22 | pub const INSERT_WASM_INTERFACE: &str = include_str!("queries/insert_interface.sql"); 23 | pub const GET_WASM_INTERFACE: &str = include_str!("queries/get_interface.sql"); 24 | 25 | #[cfg(feature = "full")] 26 | #[cfg(test)] 27 | mod test { 28 | use super::*; 29 | use rusqlite::params; 30 | 31 | fn open_db() -> rusqlite::Connection { 32 | let mut conn = rusqlite::Connection::open_in_memory().unwrap(); 33 | crate::database::apply_migrations(&mut conn).unwrap(); 34 | conn 35 | } 36 | 37 | const DATE_STR: &str = "2019-05-10T16:12:34-00.000"; 38 | 39 | #[test] 40 | fn sql_tests() { 41 | let conn = open_db(); 42 | let public_key_id = "79EC1A7316BFD5A"; 43 | let public_key_value = "RWRa/Wsxp8GeB4bcA7v0HAdbYYR00QKwAb5kN8yN+uuyugf51XGuYqWD"; 44 | conn.execute( 45 | INSERT_AND_ACTIVATE_PERSONAL_KEY_PAIR, 46 | params![ 47 | public_key_id, 48 | public_key_value, 49 | "1", 50 | "/dog/face", 51 | DATE_STR, 52 | "minisign", 53 | ], 54 | ) 55 | .unwrap(); 56 | 57 | let mut get_pk = conn.prepare(GET_PERSONAL_KEYS).unwrap(); 58 | let pks = get_pk 59 | .query_map(params![], |row| row.get(5)) 60 | .unwrap() 61 | .collect::, _>>() 62 | .unwrap(); 63 | assert_eq!(pks, vec![public_key_id.to_string()]); 64 | 65 | let mut key_check = conn 66 | .prepare(PERSONAL_PUBLIC_KEY_VALUE_EXISTENCE_CHECK) 67 | .unwrap(); 68 | let result = key_check 69 | .query_map(params![public_key_id, public_key_value], |row| row.get(0)) 70 | .unwrap() 71 | .collect::, _>>() 72 | .unwrap(); 73 | assert_eq!(result, vec![public_key_id.to_string()]); 74 | 75 | conn.execute(DELETE_PERSONAL_KEY_PAIR, params![public_key_value]) 76 | .unwrap(); 77 | 78 | let result = key_check 79 | .query_map(params![public_key_id, public_key_value], |row| row.get(0)) 80 | .unwrap() 81 | .collect::, _>>() 82 | .unwrap(); 83 | assert!(result.is_empty()); 84 | 85 | conn.execute(INSERT_USER, params!["ZinedineZidane"]) 86 | .unwrap(); 87 | conn.execute( 88 | INSERT_WAPM_PUBLIC_KEY, 89 | params![ 90 | "ZinedineZidane", 91 | public_key_id, 92 | public_key_value, 93 | "minisign", 94 | DATE_STR, 95 | ], 96 | ) 97 | .unwrap(); 98 | 99 | let mut key_check = conn.prepare(GET_LATEST_PUBLIC_KEY_FOR_USER).unwrap(); 100 | let result = key_check 101 | .query_map(params!["ZinedineZidane"], |row| row.get(0)) 102 | .unwrap() 103 | .collect::, _>>() 104 | .unwrap(); 105 | assert_eq!(result, vec![public_key_id.to_string()]); 106 | 107 | let mut stmt = conn.prepare(WAPM_PUBLIC_KEY_EXISTENCE_CHECK).unwrap(); 108 | let result = stmt 109 | .query_map(params![public_key_id, public_key_value], |row| { 110 | Ok((row.get(0)?, row.get(1)?)) 111 | }) 112 | .unwrap() 113 | .collect::, _>>() 114 | .unwrap(); 115 | assert_eq!( 116 | result, 117 | vec![("ZinedineZidane".to_string(), public_key_id.to_string())] 118 | ); 119 | 120 | let mut stmt = conn.prepare(GET_WAPM_PUBLIC_KEYS).unwrap(); 121 | let result = stmt 122 | .query_map(params![], |row| Ok((row.get(0)?, row.get(1)?))) 123 | .unwrap() 124 | .collect::, _>>() 125 | .unwrap(); 126 | assert_eq!( 127 | result, 128 | vec![("ZinedineZidane".to_string(), public_key_value.to_string())] 129 | ); 130 | 131 | conn.execute( 132 | INSERT_WASM_INTERFACE, 133 | params![ 134 | "test_interface", 135 | "0.0.0", 136 | DATE_STR, 137 | "this is where the interface data goes!" 138 | ], 139 | ) 140 | .unwrap(); 141 | 142 | let mut stmt = conn.prepare(WASM_INTERFACE_EXISTENCE_CHECK).unwrap(); 143 | let result = stmt.exists(params!["test_interface", "0.0.0"]).unwrap(); 144 | assert!(result); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/sql/queries/delete_personal_key_pair.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM personal_keys 2 | WHERE public_key_value = (?1) 3 | -------------------------------------------------------------------------------- /src/sql/queries/get_interface.sql: -------------------------------------------------------------------------------- 1 | SELECT content 2 | FROM wasm_interfaces 3 | WHERE interface_name = (?1) 4 | AND version = (?2) 5 | -------------------------------------------------------------------------------- /src/sql/queries/get_latest_public_key_for_user.sql: -------------------------------------------------------------------------------- 1 | SELECT public_key_id, public_key_value, date_added, key_type_identifier 2 | FROM wapm_public_keys 3 | JOIN wapm_users wu ON user_key = wu.id 4 | WHERE wu.name = (?1) 5 | ORDER BY date_added DESC 6 | LIMIT 1 7 | -------------------------------------------------------------------------------- /src/sql/queries/get_personal_keys.sql: -------------------------------------------------------------------------------- 1 | SELECT active, public_key_value, private_key_location, date_added, key_type_identifier, public_key_id 2 | FROM personal_keys 3 | ORDER BY date_added; 4 | -------------------------------------------------------------------------------- /src/sql/queries/get_wapm_public_keys.sql: -------------------------------------------------------------------------------- 1 | SELECT wu.name, public_key_value, date_added, key_type_identifier, public_key_id 2 | FROM wapm_public_keys 3 | JOIN wapm_users wu ON user_key = wu.id 4 | ORDER BY date_added; 5 | -------------------------------------------------------------------------------- /src/sql/queries/insert_and_activate_personal_key_pair.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO personal_keys 2 | (public_key_id, 3 | public_key_value, 4 | active, 5 | private_key_location, 6 | date_added, 7 | key_type_identifier) 8 | VALUES (?1, ?2, ?3, ?4, ?5, ?6) 9 | -------------------------------------------------------------------------------- /src/sql/queries/insert_interface.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO wasm_interfaces 2 | (interface_name, version, date_added, content) 3 | VALUES (?1, ?2, ?3, ?4) 4 | -------------------------------------------------------------------------------- /src/sql/queries/insert_user.sql: -------------------------------------------------------------------------------- 1 | INSERT OR IGNORE INTO wapm_users 2 | (name) 3 | VALUES (?1) 4 | -------------------------------------------------------------------------------- /src/sql/queries/insert_wapm_public_key.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO wapm_public_keys 2 | (user_key, 3 | public_key_id, 4 | public_key_value, 5 | key_type_identifier, 6 | date_added) 7 | VALUES 8 | ((SELECT id 9 | FROM wapm_users 10 | WHERE name = (?1)), 11 | (?2), 12 | (?3), 13 | (?4), 14 | (?5)) 15 | -------------------------------------------------------------------------------- /src/sql/queries/personal_private_key_value_existence_check.sql: -------------------------------------------------------------------------------- 1 | SELECT private_key_location, public_key_value 2 | FROM personal_keys 3 | WHERE private_key_location = (?1) 4 | -------------------------------------------------------------------------------- /src/sql/queries/personal_public_key_value_existence_check.sql: -------------------------------------------------------------------------------- 1 | SELECT public_key_id 2 | FROM personal_keys 3 | WHERE public_key_id = (?1) 4 | OR public_key_value = (?2) 5 | -------------------------------------------------------------------------------- /src/sql/queries/private_key_location_exists.sql: -------------------------------------------------------------------------------- 1 | SELECT private_key_location, public_key_value 2 | FROM personal_keys 3 | WHERE private_key_location = (?1) 4 | -------------------------------------------------------------------------------- /src/sql/queries/wapm_public_key_existence_check.sql: -------------------------------------------------------------------------------- 1 | SELECT wu.name, public_key_id 2 | FROM wapm_public_keys 3 | JOIN wapm_users wu ON user_key = wu.id 4 | WHERE public_key_id = (?1) 5 | OR public_key_value = (?2) 6 | -------------------------------------------------------------------------------- /src/sql/queries/wasm_interface_existence_check.sql: -------------------------------------------------------------------------------- 1 | SELECT 1 2 | FROM wasm_interfaces 3 | WHERE interface_name = (?1) 4 | AND version = (?2) 5 | LIMIT 1 6 | -------------------------------------------------------------------------------- /src/validate.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | not(feature = "full"), 3 | allow(dead_code, unused_imports, unused_variables) 4 | )] 5 | #[cfg(feature = "full")] 6 | use crate::database; 7 | use crate::dataflow::{interfaces::InterfaceFromServer, manifest_packages::ManifestResult}; 8 | #[cfg(feature = "full")] 9 | use crate::interfaces; 10 | use std::{ 11 | fs, 12 | io::Read, 13 | path::{Path, PathBuf}, 14 | }; 15 | use thiserror::Error; 16 | use wasmer_wasm_interface::{validate, Interface}; 17 | 18 | #[cfg(feature = "full")] 19 | pub fn validate_directory(pkg_path: PathBuf) -> anyhow::Result<()> { 20 | // validate as dir 21 | let manifest = match ManifestResult::find_in_directory(&pkg_path) { 22 | ManifestResult::NoManifest => return Ok(()), 23 | ManifestResult::ManifestError(e) => return Err(e.into()), 24 | ManifestResult::Manifest(manifest) => manifest, 25 | }; 26 | if let Some(modules) = manifest.module { 27 | for module in modules.into_iter() { 28 | let source_path = if module.source.is_relative() { 29 | manifest.base_directory_path.join(&module.source) 30 | } else { 31 | module.source.clone() 32 | }; 33 | let source_path_string = source_path.to_string_lossy().to_string(); 34 | let mut wasm_file = 35 | fs::File::open(&source_path).map_err(|_| ValidationError::MissingFile { 36 | file: source_path_string.clone(), 37 | })?; 38 | let mut wasm_buffer = Vec::new(); 39 | wasm_file.read_to_end(&mut wasm_buffer).map_err(|err| { 40 | ValidationError::MiscCannotRead { 41 | file: source_path_string.clone(), 42 | error: format!("{}", err), 43 | } 44 | })?; 45 | 46 | if let Some(bindings) = &module.bindings { 47 | validate_bindings(bindings, &manifest.base_directory_path)?; 48 | } 49 | 50 | // hack, short circuit if no interface for now 51 | if module.interfaces.is_none() { 52 | return validate_wasm_and_report_errors_old(&wasm_buffer[..], source_path_string); 53 | } 54 | 55 | let mut conn = database::open_db()?; 56 | let mut interface: Interface = Default::default(); 57 | for (interface_name, interface_version) in 58 | module.interfaces.unwrap_or_default().into_iter() 59 | { 60 | if !interfaces::interface_exists(&mut conn, &interface_name, &interface_version)? { 61 | // download interface and store it if we don't have it locally 62 | let interface_data_from_server = InterfaceFromServer::get( 63 | interface_name.clone(), 64 | interface_version.clone(), 65 | )?; 66 | interfaces::import_interface( 67 | &mut conn, 68 | &interface_name, 69 | &interface_version, 70 | &interface_data_from_server.content, 71 | )?; 72 | } 73 | let sub_interface = interfaces::load_interface_from_db( 74 | &mut conn, 75 | &interface_name, 76 | &interface_version, 77 | )?; 78 | interface = interface 79 | .merge(sub_interface) 80 | .map_err(|e| anyhow!("Failed to merge interface {}: {}", &interface_name, e))?; 81 | } 82 | validate::validate_wasm_and_report_errors(&wasm_buffer, &interface).map_err(|e| { 83 | ValidationError::InvalidWasm { 84 | file: source_path_string, 85 | error: format!("{:?}", e), 86 | } 87 | })?; 88 | } 89 | } 90 | debug!("package at path {:#?} validated", &pkg_path); 91 | 92 | Ok(()) 93 | } 94 | 95 | fn validate_bindings( 96 | bindings: &wapm_toml::Bindings, 97 | base_directory_path: &Path, 98 | ) -> Result<(), ValidationError> { 99 | // Note: checking for referenced files will make sure they all exist. 100 | let _ = bindings.referenced_files(base_directory_path)?; 101 | 102 | Ok(()) 103 | } 104 | 105 | #[cfg(not(feature = "full"))] 106 | pub fn validate_directory(pkg_path: PathBuf) -> anyhow::Result<()> { 107 | Ok(()) 108 | } 109 | 110 | #[derive(Debug, Error)] 111 | pub enum ValidationError { 112 | #[error("WASM file \"{file}\" detected as invalid because {error}")] 113 | InvalidWasm { file: String, error: String }, 114 | #[error("Could not find file {file}")] 115 | MissingFile { file: String }, 116 | #[error("Failed to read file {file}; {error}")] 117 | MiscCannotRead { file: String, error: String }, 118 | #[error("Failed to unpack archive \"{file}\"! {error}")] 119 | CannotUnpackArchive { file: String, error: String }, 120 | #[error(transparent)] 121 | Imports(#[from] wapm_toml::ImportsError), 122 | } 123 | 124 | // legacy function, validates wasm. TODO: clean up 125 | pub fn validate_wasm_and_report_errors_old(wasm: &[u8], file_name: String) -> anyhow::Result<()> { 126 | use wasmparser::WasmDecoder; 127 | let mut parser = wasmparser::ValidatingParser::new( 128 | wasm, 129 | Some(wasmparser::ValidatingParserConfig { 130 | operator_config: wasmparser::OperatorValidatorConfig { 131 | enable_threads: true, 132 | enable_reference_types: true, 133 | enable_simd: true, 134 | enable_bulk_memory: true, 135 | enable_multi_value: true, 136 | }, 137 | }), 138 | ); 139 | loop { 140 | let state = parser.read(); 141 | match state { 142 | wasmparser::ParserState::EndWasm => return Ok(()), 143 | wasmparser::ParserState::Error(e) => { 144 | return Err(ValidationError::InvalidWasm { 145 | file: file_name, 146 | error: format!("{}", e), 147 | } 148 | .into()); 149 | } 150 | _ => {} 151 | } 152 | } 153 | } 154 | --------------------------------------------------------------------------------