├── .cargo └── config.toml ├── .github ├── dependabot.yml └── workflows │ ├── api-docs.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CODE_OF_CONDUCT.md ├── COMPATIBILITY.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── RELEASES.md ├── ci ├── build-linux.sh ├── build-macos.sh ├── docker │ └── Dockerfile └── volta.manifest ├── crates ├── archive │ ├── Cargo.toml │ ├── fixtures │ │ ├── tarballs │ │ │ └── test-file.tar.gz │ │ └── zips │ │ │ └── test-file.zip │ └── src │ │ ├── lib.rs │ │ ├── tarball.rs │ │ └── zip.rs ├── fs-utils │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── progress-read │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── test-support │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── matchers.rs │ │ ├── paths.rs │ │ └── process.rs ├── validate-npm-package-name │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── volta-core │ ├── Cargo.toml │ ├── fixtures │ │ ├── basic │ │ │ ├── node_modules │ │ │ │ ├── .bin │ │ │ │ │ └── rsvp │ │ │ │ ├── @namespace │ │ │ │ │ └── some-dep │ │ │ │ │ │ └── package.json │ │ │ │ ├── @namespaced │ │ │ │ │ └── something-else │ │ │ │ │ │ └── package.json │ │ │ │ ├── eslint │ │ │ │ │ └── package.json │ │ │ │ ├── rsvp │ │ │ │ │ └── package.json │ │ │ │ └── typescript │ │ │ │ │ └── package.json │ │ │ ├── package.json │ │ │ ├── rsvp │ │ │ └── subdir │ │ │ │ └── .gitkeep │ │ ├── cycle-1 │ │ │ ├── package.json │ │ │ └── volta.json │ │ ├── cycle-2 │ │ │ ├── package.json │ │ │ ├── workspace-1.json │ │ │ └── workspace-2.json │ │ ├── hooks │ │ │ ├── bins.json │ │ │ ├── event_url.json │ │ │ ├── format_github.json │ │ │ ├── format_npm.json │ │ │ ├── prefixes.json │ │ │ ├── project │ │ │ │ ├── .volta │ │ │ │ │ └── hooks.json │ │ │ │ └── package.json │ │ │ └── templates.json │ │ ├── nested │ │ │ ├── node_modules │ │ │ │ └── .bin │ │ │ │ │ └── eslint │ │ │ ├── package.json │ │ │ └── subproject │ │ │ │ ├── inner_project │ │ │ │ ├── node_modules │ │ │ │ │ └── .bin │ │ │ │ │ │ └── tsc │ │ │ │ └── package.json │ │ │ │ ├── node_modules │ │ │ │ └── .bin │ │ │ │ │ └── rsvp │ │ │ │ └── package.json │ │ ├── no_toolchain │ │ │ └── package.json │ │ └── yarn │ │ │ ├── pnp-cjs │ │ │ ├── .pnp.cjs │ │ │ └── package.json │ │ │ ├── pnp-js │ │ │ ├── .pnp.js │ │ │ └── package.json │ │ │ └── yarnrc-yml │ │ │ ├── .yarnrc.yml │ │ │ └── package.json │ └── src │ │ ├── command.rs │ │ ├── error │ │ ├── kind.rs │ │ ├── mod.rs │ │ └── reporter.rs │ │ ├── event.rs │ │ ├── fs.rs │ │ ├── hook │ │ ├── mod.rs │ │ ├── serial.rs │ │ └── tool.rs │ │ ├── inventory.rs │ │ ├── layout │ │ ├── mod.rs │ │ ├── unix.rs │ │ └── windows.rs │ │ ├── lib.rs │ │ ├── log.rs │ │ ├── monitor.rs │ │ ├── platform │ │ ├── image.rs │ │ ├── mod.rs │ │ ├── system.rs │ │ └── tests.rs │ │ ├── project │ │ ├── mod.rs │ │ ├── serial.rs │ │ └── tests.rs │ │ ├── run │ │ ├── binary.rs │ │ ├── executor.rs │ │ ├── mod.rs │ │ ├── node.rs │ │ ├── npm.rs │ │ ├── npx.rs │ │ ├── parser.rs │ │ ├── pnpm.rs │ │ └── yarn.rs │ │ ├── session.rs │ │ ├── shim.rs │ │ ├── signal.rs │ │ ├── style.rs │ │ ├── sync.rs │ │ ├── tool │ │ ├── mod.rs │ │ ├── node │ │ │ ├── fetch.rs │ │ │ ├── metadata.rs │ │ │ ├── mod.rs │ │ │ └── resolve.rs │ │ ├── npm │ │ │ ├── fetch.rs │ │ │ ├── mod.rs │ │ │ └── resolve.rs │ │ ├── package │ │ │ ├── configure.rs │ │ │ ├── install.rs │ │ │ ├── manager.rs │ │ │ ├── metadata.rs │ │ │ ├── mod.rs │ │ │ └── uninstall.rs │ │ ├── pnpm │ │ │ ├── fetch.rs │ │ │ ├── mod.rs │ │ │ └── resolve.rs │ │ ├── registry.rs │ │ ├── serial.rs │ │ └── yarn │ │ │ ├── fetch.rs │ │ │ ├── metadata.rs │ │ │ ├── mod.rs │ │ │ └── resolve.rs │ │ ├── toolchain │ │ ├── mod.rs │ │ └── serial.rs │ │ └── version │ │ ├── mod.rs │ │ └── serial.rs ├── volta-layout-macro │ ├── Cargo.toml │ └── src │ │ ├── ast.rs │ │ ├── ir.rs │ │ └── lib.rs ├── volta-layout │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── macros.rs │ │ ├── v0.rs │ │ ├── v1.rs │ │ ├── v2.rs │ │ ├── v3.rs │ │ └── v4.rs └── volta-migrate │ ├── Cargo.toml │ └── src │ ├── empty.rs │ ├── lib.rs │ ├── v0.rs │ ├── v1.rs │ ├── v2.rs │ ├── v3.rs │ ├── v3 │ └── config.rs │ └── v4.rs ├── dev ├── package.json ├── rpm │ ├── build-rpm.sh │ └── volta.spec └── unix │ ├── SHASUMS256.txt │ ├── boot-install.sh │ ├── build.sh │ ├── install.sh.in │ ├── release.sh │ ├── test-events │ ├── tests │ └── install-script.bats │ ├── volta-install-legacy.sh │ └── volta-install.sh ├── rust-toolchain.toml ├── src ├── cli.rs ├── command │ ├── completions.rs │ ├── fetch.rs │ ├── install.rs │ ├── list │ │ ├── human.rs │ │ ├── mod.rs │ │ ├── plain.rs │ │ └── toolchain.rs │ ├── mod.rs │ ├── pin.rs │ ├── run.rs │ ├── setup.rs │ ├── uninstall.rs │ ├── use.rs │ └── which.rs ├── common.rs ├── main.rs ├── volta-migrate.rs └── volta-shim.rs ├── tests ├── acceptance │ ├── corrupted_download.rs │ ├── direct_install.rs │ ├── direct_uninstall.rs │ ├── execute_binary.rs │ ├── hooks.rs │ ├── main.rs │ ├── merged_platform.rs │ ├── migrations.rs │ ├── run_shim_directly.rs │ ├── support │ │ ├── events_helpers.rs │ │ ├── mod.rs │ │ └── sandbox.rs │ ├── verbose_errors.rs │ ├── volta_bypass.rs │ ├── volta_install.rs │ ├── volta_pin.rs │ ├── volta_run.rs │ └── volta_uninstall.rs ├── fixtures │ ├── cli-dist-2.4.159.tgz │ ├── cli-dist-3.12.99.tgz │ ├── cli-dist-3.2.42.tgz │ ├── cli-dist-3.7.71.tgz │ ├── node-v0.0.1-darwin-x64.tar.gz │ ├── node-v0.0.1-linux-arm64.tar.gz │ ├── node-v0.0.1-linux-x64.tar.gz │ ├── node-v0.0.1-win-x64.zip │ ├── node-v0.0.1-win-x86.zip │ ├── node-v10.99.1040-darwin-x64.tar.gz │ ├── node-v10.99.1040-linux-arm64.tar.gz │ ├── node-v10.99.1040-linux-x64.tar.gz │ ├── node-v10.99.1040-win-x64.zip │ ├── node-v10.99.1040-win-x86.zip │ ├── node-v6.19.62-darwin-x64.tar.gz │ ├── node-v6.19.62-linux-arm64.tar.gz │ ├── node-v6.19.62-linux-x64.tar.gz │ ├── node-v6.19.62-win-x64.zip │ ├── node-v6.19.62-win-x86.zip │ ├── node-v8.9.10-darwin-x64.tar.gz │ ├── node-v8.9.10-linux-arm64.tar.gz │ ├── node-v8.9.10-linux-x64.tar.gz │ ├── node-v8.9.10-win-x64.zip │ ├── node-v8.9.10-win-x86.zip │ ├── node-v9.27.6-darwin-x64.tar.gz │ ├── node-v9.27.6-linux-arm64.tar.gz │ ├── node-v9.27.6-linux-x64.tar.gz │ ├── node-v9.27.6-win-x64.zip │ ├── node-v9.27.6-win-x86.zip │ ├── npm-1.2.3.tgz │ ├── npm-4.5.6.tgz │ ├── npm-8.1.5.tgz │ ├── pnpm-0.0.1.tgz │ ├── pnpm-6.34.0.tgz │ ├── pnpm-7.7.1.tgz │ ├── volta-test-1.0.0.tgz │ ├── yarn-0.0.1.tgz │ ├── yarn-1.12.99.tgz │ ├── yarn-1.2.42.tgz │ ├── yarn-1.4.159.tgz │ └── yarn-1.7.71.tgz └── smoke │ ├── autodownload.rs │ ├── direct_install.rs │ ├── direct_upgrade.rs │ ├── main.rs │ ├── npm_link.rs │ ├── package_migration.rs │ ├── support │ ├── mod.rs │ └── temp_project.rs │ ├── volta_fetch.rs │ ├── volta_install.rs │ └── volta_run.rs ├── volta.iml ├── volta.png └── wix ├── License.rtf ├── main.wxs ├── shim.cmd └── volta.ico /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | rustflags = ["-C", "target-feature=+crt-static"] 3 | [target.aarch64-pc-windows-msvc] 4 | rustflags = ["-C", "target-feature=+crt-static"] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | open-pull-requests-limit: 5 5 | schedule: 6 | interval: "daily" 7 | directories: 8 | - "/" 9 | - "/crates/*" 10 | -------------------------------------------------------------------------------- /.github/workflows/api-docs.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | name: API Docs 7 | 8 | jobs: 9 | publish: 10 | name: Build and publish 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v4 15 | with: 16 | persist-credentials: false 17 | - name: Set up cargo 18 | uses: actions-rust-lang/setup-rust-toolchain@v1 19 | - name: Cargo Cache 20 | uses: Swatinem/rust-cache@v2 21 | - name: Build docs 22 | run: | 23 | cargo doc --all --features cross-platform-docs --no-deps --document-private-items 24 | - name: Prepare docs for publication 25 | run: | 26 | mkdir -p publish 27 | mv target/doc publish/main 28 | echo '<!doctype html><a href="volta">volta</a>' > publish/main/index.html 29 | echo '<!doctype html><a href="main">main</a>' > publish/index.html 30 | - name: Publish docs to GitHub pages 31 | uses: JamesIves/github-pages-deploy-action@releases/v3 32 | with: 33 | COMMIT_MESSAGE: Publishing GitHub Pages 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | BRANCH: gh-pages 36 | FOLDER: publish 37 | SINGLE_COMMIT: true 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | name: Test 10 | 11 | jobs: 12 | tests: 13 | strategy: 14 | matrix: 15 | os: 16 | - ubuntu 17 | - macos 18 | - windows 19 | name: Acceptance Tests (${{ matrix.os }}) 20 | runs-on: ${{ matrix.os }}-latest 21 | env: 22 | RUST_BACKTRACE: full 23 | steps: 24 | - name: Check out code 25 | uses: actions/checkout@v4 26 | - name: Set up cargo 27 | uses: actions-rust-lang/setup-rust-toolchain@v1 28 | - name: Run tests 29 | run: | 30 | cargo test --all --features mock-network 31 | - name: Lint with clippy 32 | run: cargo clippy 33 | - name: Lint tests with clippy 34 | run: | 35 | cargo clippy --tests --features mock-network 36 | 37 | smoke-tests: 38 | name: Smoke Tests 39 | runs-on: macos-latest 40 | env: 41 | RUST_BACKTRACE: full 42 | steps: 43 | - name: Check out code 44 | uses: actions/checkout@v4 45 | - name: Set up cargo 46 | uses: actions-rust-lang/setup-rust-toolchain@v1 47 | - name: Run tests 48 | run: | 49 | cargo test --test smoke --features smoke-tests -- --test-threads 1 50 | 51 | shell-tests: 52 | name: Shell Script Tests 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Setup BATS 56 | run: sudo npm install -g bats 57 | - name: Check out code 58 | uses: actions/checkout@v4 59 | - name: Run tests 60 | run: bats dev/unix/tests/ 61 | 62 | check-formatting: 63 | name: Check code formatting 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Check out code 67 | uses: actions/checkout@v4 68 | - name: Set up cargo 69 | uses: actions-rust-lang/setup-rust-toolchain@v1 70 | - name: Run check 71 | run: | 72 | cargo fmt --all --quiet -- --check 73 | 74 | validate-installer-checksum: 75 | name: Validate installer checksum 76 | runs-on: ubuntu-latest 77 | steps: 78 | - name: Check out code 79 | uses: actions/checkout@v4 80 | - name: Run check 81 | run: | 82 | cd dev/unix 83 | sha256sum --check SHASUMS256.txt 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Notion.msi 4 | Volta.msi 5 | dev/windows/*.log 6 | dev/windows/Notion.wixobj 7 | dev/windows/Volta.wixobj 8 | dev/windows/Notion.wixpdb 9 | dev/windows/Volta.wixpdb 10 | dev/unix/install.sh 11 | /.idea/ 12 | /rls/ 13 | /rls* 14 | 15 | 16 | # Created by https://www.gitignore.io/api/intellij (and then modified heavily) 17 | 18 | ### Intellij ### 19 | # Ignore all IDEA files. This means you may have to rebuild the project on your 20 | # new machines at times, but avoids checking in a bunch of files which are not 21 | # generally relevant to other developers. 22 | .idea 23 | 24 | # CMake 25 | cmake-build-*/ 26 | 27 | # File-based project format 28 | *.iws 29 | 30 | # IntelliJ 31 | out/ 32 | 33 | # mpeltonen/sbt-idea plugin 34 | .idea_modules/ 35 | 36 | # JIRA plugin 37 | atlassian-ide-plugin.xml 38 | 39 | # Crashlytics plugin (for Android Studio and IntelliJ) 40 | com_crashlytics_export_strings.xml 41 | crashlytics.properties 42 | crashlytics-build.properties 43 | fabric.properties 44 | 45 | # End of https://www.gitignore.io/api/intellij 46 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "lldb", 6 | "request": "launch", 7 | "name": "Cargo run volta core", 8 | "cargo": { 9 | "args": ["run", "--bin", "volta"], 10 | "filter": { 11 | "kind": "bin", 12 | "name": "volta" 13 | } 14 | }, 15 | "program": "${cargo:program}", 16 | "args": [], 17 | "sourceLanguages": ["rust"] 18 | }, 19 | { 20 | "type": "lldb", 21 | "request": "launch", 22 | "name": "Cargo test volta core", 23 | "cargo": { 24 | "args": [ 25 | "test", 26 | "--lib", 27 | "--no-run", 28 | "--package", 29 | "volta-core", 30 | "--", 31 | "--test-threads", 32 | "1" 33 | ], 34 | "filter": { 35 | "kind": "lib", 36 | "name": "volta-core" 37 | } 38 | }, 39 | "program": "${cargo:program}", 40 | "args": [], 41 | "sourceLanguages": ["rust"] 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at david.herman@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /COMPATIBILITY.md: -------------------------------------------------------------------------------- 1 | # Compatibility 2 | 3 | Volta currently tests against the following platforms, and will treat it as a breaking change to drop support for them: 4 | 5 | - macOS 6 | - x86-64 7 | - Apple Silicon 8 | - Linux x86-64 9 | - Windows x86-64 10 | 11 | We compile release artifacts compatible with the following, and likewise will treat it as a breaking change to drop support for them: 12 | 13 | - macOS v11 14 | - RHEL and CentOS v7 15 | - Windows 10 16 | 17 | In general, Volta should build and run against any other modern hardware and operating system supported by stable Rust, and we will make a best effort not to break them. However, we do *not* include them in our SemVer guarantees or test against them. 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please see https://docs.volta.sh/contributing/ 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "volta" 3 | version = "2.0.2" 4 | authors = ["David Herman <david.herman@gmail.com>", "Charles Pierce <cpierce.grad@gmail.com>"] 5 | license = "BSD-2-Clause" 6 | repository = "https://github.com/volta-cli/volta" 7 | edition = "2021" 8 | 9 | [features] 10 | cross-platform-docs = ["volta-core/cross-platform-docs"] 11 | mock-network = ["mockito", "volta-core/mock-network"] 12 | volta-dev = [] 13 | smoke-tests = [] 14 | 15 | [[bin]] 16 | name = "volta-shim" 17 | path = "src/volta-shim.rs" 18 | 19 | [[bin]] 20 | name = "volta-migrate" 21 | path = "src/volta-migrate.rs" 22 | 23 | [profile.release] 24 | lto = "fat" 25 | codegen-units = 1 26 | 27 | [dependencies] 28 | volta-core = { path = "crates/volta-core" } 29 | serde = { version = "1.0", features = ["derive"] } 30 | serde_json = "1.0.135" 31 | once_cell = "1.19.0" 32 | log = { version = "0.4", features = ["std"] } 33 | node-semver = "2" 34 | clap = { version = "4.5.24", features = ["color", "derive", "wrap_help"] } 35 | clap_complete = "4.5.46" 36 | mockito = { version = "0.31.1", optional = true } 37 | textwrap = "0.16.1" 38 | which = "7.0.1" 39 | dirs = "6.0.0" 40 | volta-migrate = { path = "crates/volta-migrate" } 41 | 42 | [target.'cfg(windows)'.dependencies] 43 | winreg = "0.53.0" 44 | 45 | [dev-dependencies] 46 | hamcrest2 = "0.3.0" 47 | envoy = "0.1.3" 48 | ci_info = "0.14.14" 49 | headers = "0.4" 50 | cfg-if = "1.0" 51 | test-support = { path = "crates/test-support" } 52 | 53 | [workspace] 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-CLAUSE LICENSE 2 | 3 | Copyright (c) 2017, The Volta Contributors. 4 | All rights reserved. 5 | 6 | This product includes: 7 | 8 | Contributions from LinkedIn Corporation 9 | Copyright (c) 2017, LinkedIn Corporation. 10 | 11 | Redistribution and use in source and binary forms, with or without 12 | modification, are permitted provided that the following conditions are met: 13 | 14 | 1. Redistributions of source code must retain the above copyright notice, this 15 | list of conditions and the following disclaimer. 16 | 2. Redistributions in binary form must reproduce the above copyright notice, 17 | this list of conditions and the following disclaimer in the documentation 18 | and/or other materials provided with the distribution. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | The views and conclusions contained in the software and documentation are those 32 | of the authors and should not be interpreted as representing official policies, 33 | either expressed or implied, of the FreeBSD Project. 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | <p align="center"> 2 | <a href="https://www.volta.sh/"> 3 | <img alt="Volta" src="./volta.png?raw=true" width="360"> 4 | </a> 5 | </p> 6 | 7 | <p align="center"> 8 | The Hassle-Free JavaScript Tool Manager 9 | </p> 10 | 11 | <p align="center"> 12 | <img alt="Production Build Status" src="https://github.com/volta-cli/volta/workflows/Production/badge.svg" /> 13 | <a href="https://github.com/volta-cli/volta/actions?query=workflow%3ATest"> 14 | <img alt="Test Status" src="https://github.com/volta-cli/volta/workflows/Test/badge.svg" /> 15 | </a> 16 | </p> 17 | 18 | **Fast:** Install and run any JS tool quickly and seamlessly! Volta is built in Rust and ships as a snappy static binary. 19 | 20 | **Reliable:** Ensure everyone in your project has the same tools—without interfering with their workflow. 21 | 22 | **Universal:** No matter the package manager, Node runtime, or OS, one command is all you need: `volta install`. 23 | 24 | ## Features 25 | 26 | - Speed 🚀 27 | - Seamless, per-project version switching 28 | - Cross-platform support, including Windows and all Unix shells 29 | - Support for multiple package managers 30 | - Stable tool installation—no reinstalling on every Node upgrade! 31 | - Extensibility hooks for site-specific customization 32 | 33 | ## Installing Volta 34 | 35 | Read the [Getting Started Guide](https://docs.volta.sh/guide/getting-started) on our website for detailed instructions on how to install Volta. 36 | 37 | ## Using Volta 38 | 39 | Read the [Understanding Volta Guide](https://docs.volta.sh/guide/understanding) on our website for detailed instructions on how to use Volta. 40 | 41 | ## Contributing to Volta 42 | 43 | Contributions are always welcome, no matter how large or small. Substantial feature ideas should be proposed as an [RFC](https://github.com/volta-cli/rfcs). Before contributing, please read the [code of conduct](CODE_OF_CONDUCT.md). 44 | 45 | See the [Contributing Guide](https://docs.volta.sh/contributing/) on our website for detailed instructions on how to contribute to Volta. 46 | 47 | ## Who is using Volta? 48 | 49 | <table> 50 | <tbody> 51 | <tr> 52 | <td align="center"> 53 | <a href="https://github.com/microsoft/TypeScript" target="_blank"> 54 | <img src="https://raw.githubusercontent.com/microsoft/TypeScript-Website/v2/packages/typescriptlang-org/static/branding/ts-logo-512.svg" alt="TypeScript" width="100" height="100"> 55 | </a> 56 | </td> 57 | <td align="center"> 58 | <a href="https://github.com/getsentry/sentry-javascript" target="_blank"> 59 | <img src="https://avatars.githubusercontent.com/u/1396951?s=100" alt="Sentry" width="100" height="100"> 60 | </a> 61 | </td> 62 | </tr> 63 | </tbody> 64 | </table> 65 | 66 | See [here](https://sourcegraph.com/search?q=context:global+%22volta%22+file:package.json&patternType=literal) for more Volta users. 67 | -------------------------------------------------------------------------------- /ci/build-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Activate the upgraded versions of GCC and binutils 6 | # See https://linux.web.cern.ch/centos7/docs/softwarecollections/#inst 7 | source /opt/rh/devtoolset-8/enable 8 | 9 | echo "Building Volta" 10 | 11 | cargo build --release 12 | 13 | echo "Packaging Binaries" 14 | 15 | cd target/release 16 | tar -zcvf "$1.tar.gz" volta volta-shim volta-migrate 17 | -------------------------------------------------------------------------------- /ci/build-macos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "Building Volta" 6 | 7 | MACOSX_DEPLOYMENT_TARGET=11.0 cargo build --release --target=aarch64-apple-darwin 8 | MACOSX_DEPLOYMENT_TARGET=11.0 cargo build --release --target=x86_64-apple-darwin 9 | 10 | echo "Packaging Binaries" 11 | 12 | mkdir -p target/universal-apple-darwin/release 13 | 14 | for exe in volta volta-shim volta-migrate 15 | do 16 | lipo -create -output target/universal-apple-darwin/release/$exe target/x86_64-apple-darwin/release/$exe target/aarch64-apple-darwin/release/$exe 17 | done 18 | 19 | cd target/universal-apple-darwin/release 20 | 21 | tar -zcvf "$1.tar.gz" volta volta-shim volta-migrate 22 | -------------------------------------------------------------------------------- /ci/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cern/cc7-base 2 | 3 | # This repo file references a URL that is no longer valid. It also isn't used by the build 4 | # toolchain, so we can safely remove it entirely 5 | RUN rm /etc/yum.repos.d/epel.repo 6 | 7 | # https://linux.web.cern.ch/centos7/docs/softwarecollections/#inst 8 | # Tools needed for the build and setup process 9 | RUN yum -y install wget tar 10 | # Fetch the repo information for the devtoolset repo 11 | RUN yum install -y centos-release-scl 12 | # Install more recent GCC and binutils, to allow us to compile 13 | RUN yum install -y devtoolset-8 14 | 15 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y 16 | ENV PATH="/root/.cargo/bin:${PATH}" 17 | -------------------------------------------------------------------------------- /ci/volta.manifest: -------------------------------------------------------------------------------- 1 | volta 2 | volta-shim 3 | volta-migrate 4 | -------------------------------------------------------------------------------- /crates/archive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "archive" 3 | version = "0.1.0" 4 | authors = ["David Herman <david.herman@gmail.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | flate2 = "1.0" 9 | tar = "0.4.13" 10 | # Set features manually to drop usage of `time` crate: we do not rely on that 11 | # set of capabilities, and it has a vulnerability. We also don't need to use 12 | # every single compression algorithm feature since we are only downloading 13 | # Node as a zip file 14 | zip_rs = { version = "=2.1.6", package = "zip", default-features = false, features = ["deflate", "bzip2"] } 15 | tee = "0.1.0" 16 | fs-utils = { path = "../fs-utils" } 17 | progress-read = { path = "../progress-read" } 18 | verbatim = "0.1" 19 | cfg-if = "1.0" 20 | headers = "0.4" 21 | thiserror = "2.0.0" 22 | attohttpc = { version = "0.28", default-features = false, features = ["json", "compress", "tls-rustls-native-roots"] } 23 | log = { version = "0.4", features = ["std"] } 24 | -------------------------------------------------------------------------------- /crates/archive/fixtures/tarballs/test-file.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/crates/archive/fixtures/tarballs/test-file.tar.gz -------------------------------------------------------------------------------- /crates/archive/fixtures/zips/test-file.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/crates/archive/fixtures/zips/test-file.zip -------------------------------------------------------------------------------- /crates/archive/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides types for fetching and unpacking compressed 2 | //! archives in tarball or zip format. 3 | use std::fs::File; 4 | use std::path::Path; 5 | 6 | use attohttpc::header::HeaderMap; 7 | use headers::{ContentLength, Header, HeaderMapExt}; 8 | use thiserror::Error; 9 | 10 | mod tarball; 11 | mod zip; 12 | 13 | pub use crate::tarball::Tarball; 14 | pub use crate::zip::Zip; 15 | 16 | /// Error type for this crate 17 | #[derive(Error, Debug)] 18 | pub enum ArchiveError { 19 | #[error("HTTP failure ({0})")] 20 | HttpError(attohttpc::StatusCode), 21 | 22 | #[error("HTTP header '{0}' not found")] 23 | MissingHeaderError(&'static attohttpc::header::HeaderName), 24 | 25 | #[error("unexpected content length in HTTP response: {0}")] 26 | UnexpectedContentLengthError(u64), 27 | 28 | #[error("{0}")] 29 | IoError(#[from] std::io::Error), 30 | 31 | #[error("{0}")] 32 | AttohttpcError(#[from] attohttpc::Error), 33 | 34 | #[error("{0}")] 35 | ZipError(#[from] zip_rs::result::ZipError), 36 | } 37 | 38 | /// Metadata describing whether an archive comes from a local or remote origin. 39 | #[derive(Copy, Clone)] 40 | pub enum Origin { 41 | Local, 42 | Remote, 43 | } 44 | 45 | pub trait Archive { 46 | fn compressed_size(&self) -> u64; 47 | 48 | /// Unpacks the zip archive to the specified destination folder. 49 | fn unpack( 50 | self: Box<Self>, 51 | dest: &Path, 52 | progress: &mut dyn FnMut(&(), usize), 53 | ) -> Result<(), ArchiveError>; 54 | 55 | fn origin(&self) -> Origin; 56 | } 57 | 58 | cfg_if::cfg_if! { 59 | if #[cfg(unix)] { 60 | /// Load an archive in the native OS-preferred format from the specified file. 61 | /// 62 | /// On Windows, the preferred format is zip. On Unixes, the preferred format 63 | /// is tarball. 64 | pub fn load_native(source: File) -> Result<Box<dyn Archive>, ArchiveError> { 65 | Tarball::load(source) 66 | } 67 | 68 | /// Fetch a remote archive in the native OS-preferred format from the specified 69 | /// URL and store its results at the specified file path. 70 | /// 71 | /// On Windows, the preferred format is zip. On Unixes, the preferred format 72 | /// is tarball. 73 | pub fn fetch_native(url: &str, cache_file: &Path) -> Result<Box<dyn Archive>, ArchiveError> { 74 | Tarball::fetch(url, cache_file) 75 | } 76 | } else if #[cfg(windows)] { 77 | /// Load an archive in the native OS-preferred format from the specified file. 78 | /// 79 | /// On Windows, the preferred format is zip. On Unixes, the preferred format 80 | /// is tarball. 81 | pub fn load_native(source: File) -> Result<Box<dyn Archive>, ArchiveError> { 82 | Zip::load(source) 83 | } 84 | 85 | /// Fetch a remote archive in the native OS-preferred format from the specified 86 | /// URL and store its results at the specified file path. 87 | /// 88 | /// On Windows, the preferred format is zip. On Unixes, the preferred format 89 | /// is tarball. 90 | pub fn fetch_native(url: &str, cache_file: &Path) -> Result<Box<dyn Archive>, ArchiveError> { 91 | Zip::fetch(url, cache_file) 92 | } 93 | } else { 94 | compile_error!("Unsupported OS (expected 'unix' or 'windows')."); 95 | } 96 | } 97 | 98 | /// Determines the length of an HTTP response's content in bytes, using 99 | /// the HTTP `"Content-Length"` header. 100 | fn content_length(headers: &HeaderMap) -> Result<u64, ArchiveError> { 101 | headers 102 | .typed_get() 103 | .map(|ContentLength(v)| v) 104 | .ok_or_else(|| ArchiveError::MissingHeaderError(ContentLength::name())) 105 | } 106 | -------------------------------------------------------------------------------- /crates/archive/src/tarball.rs: -------------------------------------------------------------------------------- 1 | //! Provides types and functions for fetching and unpacking a Node installation 2 | //! tarball in Unix operating systems. 3 | 4 | use std::fs::File; 5 | use std::io::Read; 6 | use std::path::Path; 7 | 8 | use super::{content_length, Archive, ArchiveError, Origin}; 9 | use flate2::read::GzDecoder; 10 | use fs_utils::ensure_containing_dir_exists; 11 | use progress_read::ProgressRead; 12 | use tee::TeeReader; 13 | 14 | /// A Node installation tarball. 15 | pub struct Tarball { 16 | compressed_size: u64, 17 | data: Box<dyn Read>, 18 | origin: Origin, 19 | } 20 | 21 | impl Tarball { 22 | /// Loads a tarball from the specified file. 23 | pub fn load(source: File) -> Result<Box<dyn Archive>, ArchiveError> { 24 | let compressed_size = source.metadata()?.len(); 25 | Ok(Box::new(Tarball { 26 | compressed_size, 27 | data: Box::new(source), 28 | origin: Origin::Local, 29 | })) 30 | } 31 | 32 | /// Initiate fetching of a tarball from the given URL, returning a 33 | /// tarball that can be streamed (and that tees its data to a local 34 | /// file as it streams). 35 | pub fn fetch(url: &str, cache_file: &Path) -> Result<Box<dyn Archive>, ArchiveError> { 36 | let (status, headers, response) = attohttpc::get(url).send()?.split(); 37 | 38 | if !status.is_success() { 39 | return Err(ArchiveError::HttpError(status)); 40 | } 41 | 42 | let compressed_size = content_length(&headers)?; 43 | 44 | ensure_containing_dir_exists(&cache_file)?; 45 | let file = File::create(cache_file)?; 46 | let data = Box::new(TeeReader::new(response, file)); 47 | 48 | Ok(Box::new(Tarball { 49 | compressed_size, 50 | data, 51 | origin: Origin::Remote, 52 | })) 53 | } 54 | } 55 | 56 | impl Archive for Tarball { 57 | fn compressed_size(&self) -> u64 { 58 | self.compressed_size 59 | } 60 | fn unpack( 61 | self: Box<Self>, 62 | dest: &Path, 63 | progress: &mut dyn FnMut(&(), usize), 64 | ) -> Result<(), ArchiveError> { 65 | let decoded = GzDecoder::new(ProgressRead::new(self.data, (), progress)); 66 | let mut tarball = tar::Archive::new(decoded); 67 | tarball.unpack(dest)?; 68 | Ok(()) 69 | } 70 | fn origin(&self) -> Origin { 71 | self.origin 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | pub mod tests { 77 | 78 | use crate::tarball::Tarball; 79 | use std::fs::File; 80 | use std::path::PathBuf; 81 | 82 | fn fixture_path(fixture_dir: &str) -> PathBuf { 83 | let mut cargo_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 84 | cargo_manifest_dir.push("fixtures"); 85 | cargo_manifest_dir.push(fixture_dir); 86 | cargo_manifest_dir 87 | } 88 | 89 | #[test] 90 | fn test_load() { 91 | let mut test_file_path = fixture_path("tarballs"); 92 | test_file_path.push("test-file.tar.gz"); 93 | let test_file = File::open(test_file_path).expect("Couldn't open test file"); 94 | let tarball = Tarball::load(test_file).expect("Failed to load tarball"); 95 | 96 | assert_eq!(tarball.compressed_size(), 402); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crates/archive/src/zip.rs: -------------------------------------------------------------------------------- 1 | //! Provides types and functions for fetching and unpacking a Node installation 2 | //! zip file in Windows operating systems. 3 | 4 | use std::fs::File; 5 | use std::io::Read; 6 | use std::path::Path; 7 | 8 | use super::{content_length, ArchiveError}; 9 | use fs_utils::ensure_containing_dir_exists; 10 | use progress_read::ProgressRead; 11 | use tee::TeeReader; 12 | use verbatim::PathExt; 13 | use zip_rs::unstable::stream::ZipStreamReader; 14 | 15 | use super::Archive; 16 | use super::Origin; 17 | 18 | pub struct Zip { 19 | compressed_size: u64, 20 | data: Box<dyn Read>, 21 | origin: Origin, 22 | } 23 | 24 | impl Zip { 25 | /// Loads a cached Node zip archive from the specified file. 26 | pub fn load(source: File) -> Result<Box<dyn Archive>, ArchiveError> { 27 | let compressed_size = source.metadata()?.len(); 28 | 29 | Ok(Box::new(Zip { 30 | compressed_size, 31 | data: Box::new(source), 32 | origin: Origin::Local, 33 | })) 34 | } 35 | 36 | /// Initiate fetching of a Node zip archive from the given URL, returning 37 | /// a `Remote` data source. 38 | pub fn fetch(url: &str, cache_file: &Path) -> Result<Box<dyn Archive>, ArchiveError> { 39 | let (status, headers, response) = attohttpc::get(url).send()?.split(); 40 | 41 | if !status.is_success() { 42 | return Err(ArchiveError::HttpError(status)); 43 | } 44 | 45 | let compressed_size = content_length(&headers)?; 46 | 47 | ensure_containing_dir_exists(&cache_file)?; 48 | let file = File::create(cache_file)?; 49 | let data = Box::new(TeeReader::new(response, file)); 50 | 51 | Ok(Box::new(Zip { 52 | compressed_size, 53 | data, 54 | origin: Origin::Remote, 55 | })) 56 | } 57 | } 58 | 59 | impl Archive for Zip { 60 | fn compressed_size(&self) -> u64 { 61 | self.compressed_size 62 | } 63 | fn unpack( 64 | self: Box<Self>, 65 | dest: &Path, 66 | progress: &mut dyn FnMut(&(), usize), 67 | ) -> Result<(), ArchiveError> { 68 | // Use a verbatim path to avoid the legacy Windows 260 byte path limit. 69 | let dest: &Path = &dest.to_verbatim(); 70 | let zip = ZipStreamReader::new(ProgressRead::new(self.data, (), progress)); 71 | zip.extract(dest)?; 72 | Ok(()) 73 | } 74 | fn origin(&self) -> Origin { 75 | self.origin 76 | } 77 | } 78 | 79 | #[cfg(test)] 80 | pub mod tests { 81 | 82 | use crate::zip::Zip; 83 | use std::fs::File; 84 | use std::path::PathBuf; 85 | 86 | fn fixture_path(fixture_dir: &str) -> PathBuf { 87 | let mut cargo_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 88 | cargo_manifest_dir.push("fixtures"); 89 | cargo_manifest_dir.push(fixture_dir); 90 | cargo_manifest_dir 91 | } 92 | 93 | #[test] 94 | fn test_load() { 95 | let mut test_file_path = fixture_path("zips"); 96 | test_file_path.push("test-file.zip"); 97 | let test_file = File::open(test_file_path).expect("Couldn't open test file"); 98 | let zip = Zip::load(test_file).expect("Failed to load zip file"); 99 | 100 | assert_eq!(zip.compressed_size(), 214); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/fs-utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fs-utils" 3 | version = "0.1.0" 4 | authors = ["Michael Stewart <mikrostew@gmail.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /crates/fs-utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides utilities for operating on the filesystem. 2 | 3 | use std::fs; 4 | use std::io; 5 | use std::path::Path; 6 | 7 | /// This creates the parent directory of the input path, assuming the input path is a file. 8 | pub fn ensure_containing_dir_exists<P: AsRef<Path>>(path: &P) -> io::Result<()> { 9 | path.as_ref() 10 | .parent() 11 | .ok_or_else(|| { 12 | io::Error::new( 13 | io::ErrorKind::NotFound, 14 | format!( 15 | "Could not determine directory information for {}", 16 | path.as_ref().display() 17 | ), 18 | ) 19 | }) 20 | .and_then(fs::create_dir_all) 21 | } 22 | -------------------------------------------------------------------------------- /crates/progress-read/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "progress-read" 3 | version = "0.1.0" 4 | authors = ["David Herman <david.herman@gmail.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /crates/progress-read/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides an adapter for the `std::io::Read` trait to 2 | //! allow reporting incremental progress to a callback function. 3 | 4 | use std::io::{self, Read, Seek, SeekFrom}; 5 | 6 | /// A reader that reports incremental progress while reading. 7 | pub struct ProgressRead<R: Read, T, F: FnMut(&T, usize) -> T> { 8 | source: R, 9 | accumulator: T, 10 | progress: F, 11 | } 12 | 13 | impl<R: Read, T, F: FnMut(&T, usize) -> T> Read for ProgressRead<R, T, F> { 14 | /// Read some bytes from the underlying reader into the specified buffer, 15 | /// and report progress to the progress callback. The progress callback is 16 | /// passed the current value of the accumulator as its first argument and 17 | /// the number of bytes read as its second argument. The result of the 18 | /// progress callback is stored as the updated value of the accumulator, 19 | /// to be passed to the next invocation of the callback. 20 | fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { 21 | let len = self.source.read(buf)?; 22 | let new_accumulator = { 23 | let progress = &mut self.progress; 24 | progress(&self.accumulator, len) 25 | }; 26 | self.accumulator = new_accumulator; 27 | Ok(len) 28 | } 29 | } 30 | 31 | impl<R: Read, T, F: FnMut(&T, usize) -> T> ProgressRead<R, T, F> { 32 | /// Construct a new progress reader with the specified underlying reader, 33 | /// initial value for an accumulator, and progress callback. 34 | pub fn new(source: R, init: T, progress: F) -> ProgressRead<R, T, F> { 35 | ProgressRead { 36 | source, 37 | accumulator: init, 38 | progress, 39 | } 40 | } 41 | } 42 | 43 | impl<R: Read + Seek, T, F: FnMut(&T, usize) -> T> Seek for ProgressRead<R, T, F> { 44 | fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> { 45 | self.source.seek(pos) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/test-support/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-support" 3 | version = "0.1.0" 4 | authors = ["David Herman <david.herman@gmail.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | hamcrest2 = "0.3.0" 9 | serde_json = { version = "1.0.135" } 10 | thiserror = "2.0.9" 11 | -------------------------------------------------------------------------------- /crates/test-support/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Utilities to use with acceptance tests in Volta. 2 | 3 | #[macro_export] 4 | macro_rules! ok_or_panic { 5 | { $e:expr } => { 6 | match $e { 7 | Ok(x) => x, 8 | Err(err) => panic!("{} failed with {}", stringify!($e), err), 9 | } 10 | }; 11 | } 12 | 13 | pub mod matchers; 14 | pub mod paths; 15 | pub mod process; 16 | -------------------------------------------------------------------------------- /crates/validate-npm-package-name/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "validate-npm-package-name" 3 | version = "0.1.0" 4 | authors = ["Chris Krycho <hello@chriskrycho.com>"] 5 | edition = "2021" 6 | 7 | [lib] 8 | 9 | [dependencies] 10 | once_cell = "1.19.0" 11 | percent-encoding = "2.1.0" 12 | regex = "1.1.6" 13 | -------------------------------------------------------------------------------- /crates/volta-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "volta-core" 3 | version = "0.1.0" 4 | authors = ["David Herman <david.herman@gmail.com>"] 5 | edition = "2021" 6 | 7 | [features] 8 | mock-network = ["mockito"] 9 | # The `cross-platform-docs` feature flag is used for generating API docs for 10 | # multiple platforms in one build. 11 | # See ci/publish-docs.yml for an example of how it's enabled. 12 | # See volta-core::path for an example of where it's used. 13 | cross-platform-docs = [] 14 | 15 | [dependencies] 16 | terminal_size = "0.4.1" 17 | indicatif = "0.17.9" 18 | console = ">=0.11.3, <1.0.0" 19 | readext = "0.1.0" 20 | serde_json = { version = "1.0.135", features = ["preserve_order"] } 21 | serde = { version = "1.0.217", features = ["derive"] } 22 | archive = { path = "../archive" } 23 | node-semver = "2" 24 | cmdline_words_parser = "0.2.1" 25 | fs-utils = { path = "../fs-utils" } 26 | cfg-if = "1.0" 27 | tempfile = "3.14.0" 28 | os_info = "3.9.2" 29 | detect-indent = "0.1" 30 | envoy = "0.1.3" 31 | mockito = { version = "0.31.1", optional = true } 32 | regex = "1.11.1" 33 | dirs = "6.0.0" 34 | # We manually configure the feature list here because 0.4.16 includes the 35 | # `oldtime` feature by default to avoid a breaking change. Additionally, using 36 | # the feature list explicitly lets us drop the `wasmbind` feature, which we do 37 | # not need. 38 | chrono = { version = "0.4.39", default-features = false, features = ["alloc", "std", "clock"] } 39 | validate-npm-package-name = { path = "../validate-npm-package-name" } 40 | textwrap = "0.16.1" 41 | log = { version = "0.4", features = ["std"] } 42 | ctrlc = "3.4.5" 43 | walkdir = "2.5.0" 44 | volta-layout = { path = "../volta-layout" } 45 | once_cell = "1.19.0" 46 | dunce = "1.0.5" 47 | ci_info = "0.14.14" 48 | httpdate = "1" 49 | headers = "0.4" 50 | attohttpc = { version = "0.28", default-features = false, features = ["json", "compress", "tls-rustls-native-roots"] } 51 | chain-map = "0.1.0" 52 | indexmap = "2.7.0" 53 | retry = "2" 54 | fs2 = "0.4.3" 55 | which = "7.0.1" 56 | 57 | [target.'cfg(windows)'.dependencies] 58 | winreg = "0.55.0" 59 | junction = "1.2.0" 60 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/basic/node_modules/.bin/rsvp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // for testing 4 | console.log("running the local rsvp binary..."); 5 | 6 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/basic/node_modules/@namespace/some-dep/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "some-dep", 3 | "version": "1.1.1", 4 | "description": "Dependency that doesn't have the 'bin' field", 5 | "license": "BSD", 6 | "dependencies": {}, 7 | "devDependencies": {} 8 | } 9 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/basic/node_modules/@namespaced/something-else/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-project", 3 | "version": "0.0.7", 4 | "description": "Testing that manifest pulls things out of this correctly", 5 | "license": "To Kill", 6 | "dependencies": { 7 | "rsvp": "^3.5.0" 8 | }, 9 | "devDependencies": { 10 | "eslint": "~4.8.0" 11 | }, 12 | "bin": { 13 | "bin-1": "./lib/cli.js", 14 | "bin-2": "./lib/cli.js" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/basic/node_modules/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-project", 3 | "version": "4.8.0", 4 | "description": "Mock eslint", 5 | "license": "To Ill", 6 | "bin": { 7 | "eslint": "./bin/eslint.js" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": {} 11 | } 12 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/basic/node_modules/rsvp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsvp", 3 | "version": "3.5.0", 4 | "description": "Mock rsvp", 5 | "license": "MIT", 6 | "bin": "./bin/rsvp.js", 7 | "dependencies": {} 8 | } 9 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/basic/node_modules/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "author": "Microsoft Corp.", 4 | "homepage": "http://typescriptlang.org/", 5 | "version": "2.8.3", 6 | "license": "Apache-2.0", 7 | "description": "Mock Typescript package", 8 | "bin": { 9 | "tsc": "./bin/tsc", 10 | "tsserver": "./bin/tsserver" 11 | }, 12 | "dependencies": {}, 13 | "devDependencies": {} 14 | } 15 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-project", 3 | "version": "0.0.7", 4 | "description": "Testing that manifest pulls things out of this correctly", 5 | "license": "To Kill", 6 | "dependencies": { 7 | "@namespace/some-dep": "0.2.4", 8 | "rsvp": "^3.5.0" 9 | }, 10 | "devDependencies": { 11 | "@namespaced/something-else": "^6.3.7", 12 | "eslint": "~4.8.0" 13 | }, 14 | "volta": { 15 | "node": "6.11.1", 16 | "npm": "3.10.10", 17 | "yarn": "1.2.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/basic/rsvp: -------------------------------------------------------------------------------- 1 | ../../../../target/debug/launchbin -------------------------------------------------------------------------------- /crates/volta-core/fixtures/basic/subdir/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/crates/volta-core/fixtures/basic/subdir/.gitkeep -------------------------------------------------------------------------------- /crates/volta-core/fixtures/cycle-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cycle-1-project", 3 | "version": "0.0.1", 4 | "description": "Testing that project correctly detects a cycle between this and volta.json", 5 | "license": "To Kill", 6 | "volta": { 7 | "extends": "./volta.json" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/cycle-1/volta.json: -------------------------------------------------------------------------------- 1 | { 2 | "volta": { 3 | "extends": "./package.json" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/cycle-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cycle-2-project", 3 | "version": "0.0.1", 4 | "description": "Testing that project correctly detects a cycle between workspace-1.json and workspace-2.json", 5 | "license": "To Kill", 6 | "volta": { 7 | "extends": "./workspace-1.json" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/cycle-2/workspace-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "volta": { 3 | "extends": "./workspace-2.json" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/cycle-2/workspace-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "volta": { 3 | "extends": "./workspace-1.json" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/hooks/bins.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": { 3 | "distro": { 4 | "bin": "/some/bin/for/node/distro" 5 | }, 6 | "latest": { 7 | "bin": "/some/bin/for/node/latest" 8 | }, 9 | "index": { 10 | "bin": "/some/bin/for/node/index" 11 | } 12 | }, 13 | "pnpm": { 14 | "distro": { 15 | "bin": "/bin/to/pnpm/distro" 16 | }, 17 | "latest": { 18 | "bin": "/bin/to/pnpm/latest" 19 | }, 20 | "index": { 21 | "bin": "/bin/to/pnpm/index" 22 | } 23 | }, 24 | "yarn": { 25 | "distro": { 26 | "bin": "/bin/to/yarn/distro" 27 | }, 28 | "latest": { 29 | "bin": "/bin/to/yarn/latest" 30 | }, 31 | "index": { 32 | "bin": "/bin/to/yarn/index" 33 | } 34 | }, 35 | "events": { 36 | "publish": { 37 | "bin": "/events/bin" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/hooks/event_url.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": { 3 | "publish": { 4 | "url": "https://google.com" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/hooks/format_github.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": { 3 | "index": { 4 | "prefix": "http://localhost/node/index/", 5 | "format": "github" 6 | } 7 | }, 8 | "npm": { 9 | "index": { 10 | "prefix": "http://localhost/npm/index/", 11 | "format": "github" 12 | } 13 | }, 14 | "pnpm": { 15 | "index": { 16 | "prefix": "http://localhost/pnpm/index/", 17 | "format": "github" 18 | } 19 | }, 20 | "yarn": { 21 | "index": { 22 | "prefix": "http://localhost/yarn/index/", 23 | "format": "github" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/hooks/format_npm.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": { 3 | "index": { 4 | "prefix": "http://localhost/node/index/", 5 | "format": "npm" 6 | } 7 | }, 8 | "npm": { 9 | "index": { 10 | "prefix": "http://localhost/npm/index/", 11 | "format": "npm" 12 | } 13 | }, 14 | "pnpm": { 15 | "index": { 16 | "prefix": "http://localhost/pnpm/index/", 17 | "format": "npm" 18 | } 19 | }, 20 | "yarn": { 21 | "index": { 22 | "prefix": "http://localhost/yarn/index/", 23 | "format": "npm" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/hooks/prefixes.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": { 3 | "distro": { 4 | "prefix": "http://localhost/node/distro/" 5 | }, 6 | "latest": { 7 | "prefix": "http://localhost/node/latest/" 8 | }, 9 | "index": { 10 | "prefix": "http://localhost/node/index/" 11 | } 12 | }, 13 | "pnpm": { 14 | "distro": { 15 | "prefix": "http://localhost/pnpm/distro/" 16 | }, 17 | "latest": { 18 | "prefix": "http://localhost/pnpm/latest/" 19 | }, 20 | "index": { 21 | "prefix": "http://localhost/pnpm/index/" 22 | } 23 | }, 24 | "yarn": { 25 | "distro": { 26 | "prefix": "http://localhost/yarn/distro/" 27 | }, 28 | "latest": { 29 | "prefix": "http://localhost/yarn/latest/" 30 | }, 31 | "index": { 32 | "prefix": "http://localhost/yarn/index/" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/hooks/project/.volta/hooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": { 3 | "distro": { 4 | "bin": "/some/bin/for/node/distro" 5 | }, 6 | "latest": { 7 | "bin": "/some/bin/for/node/latest" 8 | }, 9 | "index": { 10 | "bin": "/some/bin/for/node/index" 11 | } 12 | }, 13 | "events": { 14 | "publish": { 15 | "bin": "/events/bin" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/hooks/project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "this file": "causes this directory to be recognized as a project" 3 | } 4 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/hooks/templates.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": { 3 | "distro": { 4 | "template": "http://localhost/node/distro/{{version}}/" 5 | }, 6 | "latest": { 7 | "template": "http://localhost/node/latest/{{version}}/" 8 | }, 9 | "index": { 10 | "template": "http://localhost/node/index/{{version}}/" 11 | } 12 | }, 13 | "pnpm": { 14 | "distro": { 15 | "template": "http://localhost/pnpm/distro/{{version}}/" 16 | }, 17 | "latest": { 18 | "template": "http://localhost/pnpm/latest/{{version}}/" 19 | }, 20 | "index": { 21 | "template": "http://localhost/pnpm/index/{{version}}/" 22 | } 23 | }, 24 | "yarn": { 25 | "distro": { 26 | "template": "http://localhost/yarn/distro/{{version}}/" 27 | }, 28 | "latest": { 29 | "template": "http://localhost/yarn/latest/{{version}}/" 30 | }, 31 | "index": { 32 | "template": "http://localhost/yarn/index/{{version}}/" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/nested/node_modules/.bin/eslint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | console.log("Running eslint"); 4 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/nested/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nested-project", 3 | "version": "0.0.1", 4 | "description": "Testing that project correctly detects a nested workspace", 5 | "license": "To Kill", 6 | "dependencies": { 7 | "lodash": "*" 8 | }, 9 | "devDependencies": { 10 | "eslint": "*" 11 | }, 12 | "volta": { 13 | "yarn": "1.11.0", 14 | "npm": "6.12.1", 15 | "node": "12.14.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/nested/subproject/inner_project/node_modules/.bin/tsc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | console.log("Running Typescript"); 4 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/nested/subproject/inner_project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inner-project", 3 | "version": "0.0.1", 4 | "description": "Testing that project correctly detects a nested workspace", 5 | "license": "To Kill", 6 | "dependencies": { 7 | "express": "*" 8 | }, 9 | "devDependencies": { 10 | "typescript": "*" 11 | }, 12 | "volta": { 13 | "yarn": "1.22.4", 14 | "extends": "../package.json" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/nested/subproject/node_modules/.bin/rsvp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | console.log("Running rsvp"); 4 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/nested/subproject/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subproject", 3 | "version": "0.0.1", 4 | "description": "Testing that project correctly detects a nested workspace", 5 | "license": "To Kill", 6 | "dependencies": { 7 | "rsvp": "*" 8 | }, 9 | "devDependencies": { 10 | "glob": "*" 11 | }, 12 | "volta": { 13 | "yarn": "1.17.0", 14 | "npm": "6.9.0", 15 | "extends": "../package.json" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/no_toolchain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-project", 3 | "version": "0.0.7", 4 | "description": "Testing that manifest pulls things out of this correctly", 5 | "license": "To Kill", 6 | "dependencies": { 7 | "@namespace/some-dep": "0.2.4", 8 | "rsvp": "^3.5.0" 9 | }, 10 | "devDependencies": { 11 | "@namespaced/something-else": "^6.3.7", 12 | "eslint": "~4.8.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/yarn/pnp-cjs/.pnp.cjs: -------------------------------------------------------------------------------- 1 | // plug and play 2 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/yarn/pnp-cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plug-n-play-cjs", 3 | "version": "2.0.0", 4 | "description": "Testing that Plug-n-Play things work", 5 | "license": "To Ill", 6 | "volta": { 7 | "node": "6.11.1", 8 | "npm": "3.10.10", 9 | "yarn": "3.2.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/yarn/pnp-js/.pnp.js: -------------------------------------------------------------------------------- 1 | // plug and play 2 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/yarn/pnp-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plug-n-play-js", 3 | "version": "2.0.0", 4 | "description": "Testing that Plug-n-Play things work", 5 | "license": "To Ill", 6 | "volta": { 7 | "node": "6.11.1", 8 | "npm": "3.10.10", 9 | "yarn": "2.4.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /crates/volta-core/fixtures/yarn/yarnrc-yml/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-3.3.0.cjs -------------------------------------------------------------------------------- /crates/volta-core/fixtures/yarn/yarnrc-yml/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yarnrc-yml", 3 | "version": "2.0.0", 4 | "description": "Testing that Yarn berry things work", 5 | "license": "To Ill", 6 | "volta": { 7 | "node": "6.11.1", 8 | "npm": "3.10.10", 9 | "yarn": "3.3.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /crates/volta-core/src/command.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::process::Command; 3 | 4 | use cfg_if::cfg_if; 5 | 6 | cfg_if! { 7 | if #[cfg(windows)] { 8 | pub fn create_command<E>(exe: E) -> Command 9 | where 10 | E: AsRef<OsStr> 11 | { 12 | // Several of the node utilities are implemented as `.bat` or `.cmd` files 13 | // When executing those files with `Command`, we need to call them with: 14 | // cmd.exe /C <COMMAND> <ARGUMENTS> 15 | // Instead of: <COMMAND> <ARGUMENTS> 16 | // See: https://github.com/rust-lang/rust/issues/42791 For a longer discussion 17 | let mut command = Command::new("cmd.exe"); 18 | command.arg("/C"); 19 | command.arg(exe); 20 | command 21 | } 22 | } else { 23 | pub fn create_command<E>(exe: E) -> Command 24 | where 25 | E: AsRef<OsStr> 26 | { 27 | Command::new(exe) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/volta-core/src/error/mod.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | use std::process::exit; 4 | 5 | mod kind; 6 | mod reporter; 7 | 8 | pub use kind::ErrorKind; 9 | pub use reporter::report_error; 10 | 11 | pub type Fallible<T> = Result<T, VoltaError>; 12 | 13 | /// Error type for Volta 14 | #[derive(Debug)] 15 | pub struct VoltaError { 16 | inner: Box<Inner>, 17 | } 18 | 19 | #[derive(Debug)] 20 | struct Inner { 21 | kind: ErrorKind, 22 | source: Option<Box<dyn Error>>, 23 | } 24 | 25 | impl VoltaError { 26 | /// The exit code Volta should use when this error stops execution 27 | pub fn exit_code(&self) -> ExitCode { 28 | self.inner.kind.exit_code() 29 | } 30 | 31 | /// Create a new VoltaError instance including a source error 32 | pub fn from_source<E>(source: E, kind: ErrorKind) -> Self 33 | where 34 | E: Into<Box<dyn Error>>, 35 | { 36 | VoltaError { 37 | inner: Box::new(Inner { 38 | kind, 39 | source: Some(source.into()), 40 | }), 41 | } 42 | } 43 | 44 | /// Get a reference to the ErrorKind for this error 45 | pub fn kind(&self) -> &ErrorKind { 46 | &self.inner.kind 47 | } 48 | } 49 | 50 | impl fmt::Display for VoltaError { 51 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 52 | self.inner.kind.fmt(f) 53 | } 54 | } 55 | 56 | impl Error for VoltaError { 57 | fn source(&self) -> Option<&(dyn Error + 'static)> { 58 | self.inner.source.as_ref().map(|b| b.as_ref()) 59 | } 60 | } 61 | 62 | impl From<ErrorKind> for VoltaError { 63 | fn from(kind: ErrorKind) -> Self { 64 | VoltaError { 65 | inner: Box::new(Inner { kind, source: None }), 66 | } 67 | } 68 | } 69 | 70 | /// Trait providing the with_context method to easily convert any Result error into a VoltaError 71 | pub trait Context<T> { 72 | fn with_context<F>(self, f: F) -> Fallible<T> 73 | where 74 | F: FnOnce() -> ErrorKind; 75 | } 76 | 77 | impl<T, E> Context<T> for Result<T, E> 78 | where 79 | E: Error + 'static, 80 | { 81 | fn with_context<F>(self, f: F) -> Fallible<T> 82 | where 83 | F: FnOnce() -> ErrorKind, 84 | { 85 | self.map_err(|e| VoltaError::from_source(e, f())) 86 | } 87 | } 88 | 89 | /// Exit codes supported by Volta Errors 90 | #[derive(Copy, Clone, Debug)] 91 | pub enum ExitCode { 92 | /// No error occurred. 93 | Success = 0, 94 | 95 | /// An unknown error occurred. 96 | UnknownError = 1, 97 | 98 | /// An invalid combination of command-line arguments was supplied. 99 | InvalidArguments = 3, 100 | 101 | /// No match could be found for the requested version string. 102 | NoVersionMatch = 4, 103 | 104 | /// A network error occurred. 105 | NetworkError = 5, 106 | 107 | /// A required environment variable was unset or invalid. 108 | EnvironmentError = 6, 109 | 110 | /// A file could not be read or written. 111 | FileSystemError = 7, 112 | 113 | /// Package configuration is missing or incorrect. 114 | ConfigurationError = 8, 115 | 116 | /// The command or feature is not yet implemented. 117 | NotYetImplemented = 9, 118 | 119 | /// The requested executable could not be run. 120 | ExecutionFailure = 126, 121 | 122 | /// The requested executable is not available. 123 | ExecutableNotFound = 127, 124 | } 125 | 126 | impl ExitCode { 127 | pub fn exit(self) -> ! { 128 | exit(self as i32); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /crates/volta-core/src/error/reporter.rs: -------------------------------------------------------------------------------- 1 | use std::env::args_os; 2 | use std::error::Error; 3 | use std::fs::File; 4 | use std::io::Write; 5 | use std::path::PathBuf; 6 | 7 | use super::VoltaError; 8 | use crate::layout::volta_home; 9 | use crate::style::format_error_cause; 10 | use chrono::Local; 11 | use ci_info::is_ci; 12 | use console::strip_ansi_codes; 13 | use fs_utils::ensure_containing_dir_exists; 14 | use log::{debug, error}; 15 | 16 | /// Report an error, both to the console and to error logs 17 | pub fn report_error(volta_version: &str, err: &VoltaError) { 18 | let message = err.to_string(); 19 | error!("{}", message); 20 | 21 | if let Some(details) = compose_error_details(err) { 22 | if is_ci() { 23 | // In CI, we write the error details to the log so that they are available in the CI logs 24 | // A log file may not even exist by the time the user is reviewing a failure 25 | error!("{}", details); 26 | } else { 27 | // Outside of CI, we write the error details as Debug (Verbose) information 28 | // And we write an actual error log that the user can review 29 | debug!("{}", details); 30 | 31 | // Note: Writing the error log info directly to stderr as it is a message for the user 32 | // Any custom logs will have all of the details already, so showing a message about writing 33 | // the error log would be redundant 34 | match write_error_log(volta_version, message, details) { 35 | Ok(log_file) => { 36 | eprintln!("Error details written to {}", log_file.to_string_lossy()); 37 | } 38 | Err(_) => { 39 | eprintln!("Unable to write error log!"); 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | /// Write an error log with all details about the error 47 | fn write_error_log( 48 | volta_version: &str, 49 | message: String, 50 | details: String, 51 | ) -> Result<PathBuf, Box<dyn Error>> { 52 | let file_name = Local::now() 53 | .format("volta-error-%Y-%m-%d_%H_%M_%S%.3f.log") 54 | .to_string(); 55 | let log_file_path = volta_home()?.log_dir().join(file_name); 56 | 57 | ensure_containing_dir_exists(&log_file_path)?; 58 | let mut log_file = File::create(&log_file_path)?; 59 | 60 | writeln!(log_file, "{}", collect_arguments())?; 61 | writeln!(log_file, "Volta v{}", volta_version)?; 62 | writeln!(log_file)?; 63 | writeln!(log_file, "{}", strip_ansi_codes(&message))?; 64 | writeln!(log_file)?; 65 | writeln!(log_file, "{}", strip_ansi_codes(&details))?; 66 | 67 | Ok(log_file_path) 68 | } 69 | 70 | fn compose_error_details(err: &VoltaError) -> Option<String> { 71 | // Only compose details if there is an underlying cause for the error 72 | let mut current = err.source()?; 73 | let mut details = String::new(); 74 | 75 | // Walk up the tree of causes and include all of them 76 | loop { 77 | details.push_str(&format_error_cause(current)); 78 | 79 | match current.source() { 80 | Some(cause) => { 81 | details.push_str("\n\n"); 82 | current = cause; 83 | } 84 | None => { 85 | break; 86 | } 87 | }; 88 | } 89 | 90 | Some(details) 91 | } 92 | 93 | /// Combines all the arguments into a single String 94 | fn collect_arguments() -> String { 95 | // The Debug formatter for OsString properly quotes and escapes each value 96 | args_os() 97 | .map(|arg| format!("{:?}", arg)) 98 | .collect::<Vec<String>>() 99 | .join(" ") 100 | } 101 | -------------------------------------------------------------------------------- /crates/volta-core/src/inventory.rs: -------------------------------------------------------------------------------- 1 | //! Provides types for working with Volta's _inventory_, the local repository 2 | //! of available tool versions. 3 | 4 | use std::collections::BTreeSet; 5 | use std::ffi::OsStr; 6 | use std::path::Path; 7 | 8 | use crate::error::{Context, ErrorKind, Fallible}; 9 | use crate::fs::read_dir_eager; 10 | use crate::layout::volta_home; 11 | use crate::tool::PackageConfig; 12 | use crate::version::parse_version; 13 | use log::debug; 14 | use node_semver::Version; 15 | use walkdir::WalkDir; 16 | 17 | /// Checks if a given Node version image is available on the local machine 18 | pub fn node_available(version: &Version) -> Fallible<bool> { 19 | volta_home().map(|home| { 20 | home.node_image_root_dir() 21 | .join(version.to_string()) 22 | .exists() 23 | }) 24 | } 25 | 26 | /// Collects a set of all Node versions fetched on the local machine 27 | pub fn node_versions() -> Fallible<BTreeSet<Version>> { 28 | volta_home().and_then(|home| read_versions(home.node_image_root_dir())) 29 | } 30 | 31 | /// Checks if a given npm version image is available on the local machine 32 | pub fn npm_available(version: &Version) -> Fallible<bool> { 33 | volta_home().map(|home| home.npm_image_dir(&version.to_string()).exists()) 34 | } 35 | 36 | /// Collects a set of all npm versions fetched on the local machine 37 | pub fn npm_versions() -> Fallible<BTreeSet<Version>> { 38 | volta_home().and_then(|home| read_versions(home.npm_image_root_dir())) 39 | } 40 | 41 | /// Checks if a given pnpm version image is available on the local machine 42 | pub fn pnpm_available(version: &Version) -> Fallible<bool> { 43 | volta_home().map(|home| home.pnpm_image_dir(&version.to_string()).exists()) 44 | } 45 | 46 | /// Collects a set of all pnpm versions fetched on the local machine 47 | pub fn pnpm_versions() -> Fallible<BTreeSet<Version>> { 48 | volta_home().and_then(|home| read_versions(home.pnpm_image_root_dir())) 49 | } 50 | 51 | /// Checks if a given Yarn version image is available on the local machine 52 | pub fn yarn_available(version: &Version) -> Fallible<bool> { 53 | volta_home().map(|home| home.yarn_image_dir(&version.to_string()).exists()) 54 | } 55 | 56 | /// Collects a set of all Yarn versions fetched on the local machine 57 | pub fn yarn_versions() -> Fallible<BTreeSet<Version>> { 58 | volta_home().and_then(|home| read_versions(home.yarn_image_root_dir())) 59 | } 60 | 61 | /// Collects a set of all Package Configs on the local machine 62 | pub fn package_configs() -> Fallible<BTreeSet<PackageConfig>> { 63 | let package_dir = volta_home()?.default_package_dir(); 64 | 65 | WalkDir::new(package_dir) 66 | .max_depth(2) 67 | .into_iter() 68 | // Ignore any items which didn't resolve as `DirEntry` correctly. 69 | // There is no point trying to do anything with those, and no error 70 | // we can report to the user in any case. Log the failure in the 71 | // debug output, though 72 | .filter_map(|entry| match entry { 73 | Ok(dir_entry) => { 74 | // Ignore directory entries and any files that don't have a .json extension. 75 | // This will prevent us from trying to parse OS-generated files as package 76 | // configs (e.g. `.DS_Store` on macOS) 77 | let extension = dir_entry.path().extension().and_then(OsStr::to_str); 78 | match (dir_entry.file_type().is_file(), extension) { 79 | (true, Some(ext)) if ext.eq_ignore_ascii_case("json") => { 80 | Some(dir_entry.into_path()) 81 | } 82 | _ => None, 83 | } 84 | } 85 | Err(e) => { 86 | debug!("{}", e); 87 | None 88 | } 89 | }) 90 | .map(PackageConfig::from_file) 91 | .collect() 92 | } 93 | 94 | /// Reads the contents of a directory and returns the set of all versions found 95 | /// in the directory's listing by parsing the directory names as semantic versions 96 | fn read_versions(dir: &Path) -> Fallible<BTreeSet<Version>> { 97 | let contents = read_dir_eager(dir).with_context(|| ErrorKind::ReadDirError { 98 | dir: dir.to_owned(), 99 | })?; 100 | 101 | Ok(contents 102 | .filter(|(_, metadata)| metadata.is_dir()) 103 | .filter_map(|(entry, _)| parse_version(entry.file_name().to_string_lossy()).ok()) 104 | .collect()) 105 | } 106 | -------------------------------------------------------------------------------- /crates/volta-core/src/layout/mod.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | 4 | use crate::error::{Context, ErrorKind, Fallible}; 5 | use cfg_if::cfg_if; 6 | use dunce::canonicalize; 7 | use once_cell::sync::OnceCell; 8 | use volta_layout::v4::{VoltaHome, VoltaInstall}; 9 | 10 | cfg_if! { 11 | if #[cfg(unix)] { 12 | mod unix; 13 | pub use unix::*; 14 | } else if #[cfg(windows)] { 15 | mod windows; 16 | pub use windows::*; 17 | } 18 | } 19 | 20 | static VOLTA_HOME: OnceCell<VoltaHome> = OnceCell::new(); 21 | static VOLTA_INSTALL: OnceCell<VoltaInstall> = OnceCell::new(); 22 | 23 | pub fn volta_home<'a>() -> Fallible<&'a VoltaHome> { 24 | VOLTA_HOME.get_or_try_init(|| { 25 | let home_dir = match env::var_os("VOLTA_HOME") { 26 | Some(home) => PathBuf::from(home), 27 | None => default_home_dir()?, 28 | }; 29 | 30 | Ok(VoltaHome::new(home_dir)) 31 | }) 32 | } 33 | 34 | pub fn volta_install<'a>() -> Fallible<&'a VoltaInstall> { 35 | VOLTA_INSTALL.get_or_try_init(|| { 36 | let install_dir = match env::var_os("VOLTA_INSTALL_DIR") { 37 | Some(install) => PathBuf::from(install), 38 | None => default_install_dir()?, 39 | }; 40 | 41 | Ok(VoltaInstall::new(install_dir)) 42 | }) 43 | } 44 | 45 | /// Determine the binary install directory from the currently running executable 46 | /// 47 | /// The volta-shim and volta binaries will be installed in the same location, so we can use the 48 | /// currently running executable to find the binary install directory. Note that we need to 49 | /// canonicalize the path we get from current_exe to make sure we resolve symlinks and find the 50 | /// actual binary files 51 | fn default_install_dir() -> Fallible<PathBuf> { 52 | env::current_exe() 53 | .and_then(canonicalize) 54 | .map(|mut path| { 55 | path.pop(); // Remove the executable name from the path 56 | path 57 | }) 58 | .with_context(|| ErrorKind::NoInstallDir) 59 | } 60 | -------------------------------------------------------------------------------- /crates/volta-core/src/layout/unix.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use super::volta_home; 4 | use crate::error::{ErrorKind, Fallible}; 5 | 6 | pub(super) fn default_home_dir() -> Fallible<PathBuf> { 7 | let mut home = dirs::home_dir().ok_or(ErrorKind::NoHomeEnvironmentVar)?; 8 | home.push(".volta"); 9 | Ok(home) 10 | } 11 | 12 | pub fn env_paths() -> Fallible<Vec<PathBuf>> { 13 | let home = volta_home()?; 14 | Ok(vec![home.shim_dir().to_owned()]) 15 | } 16 | -------------------------------------------------------------------------------- /crates/volta-core/src/layout/windows.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use super::{volta_home, volta_install}; 4 | use crate::error::{ErrorKind, Fallible}; 5 | 6 | pub(super) fn default_home_dir() -> Fallible<PathBuf> { 7 | let mut home = dirs::data_local_dir().ok_or(ErrorKind::NoLocalDataDir)?; 8 | home.push("Volta"); 9 | Ok(home) 10 | } 11 | 12 | pub fn env_paths() -> Fallible<Vec<PathBuf>> { 13 | let home = volta_home()?; 14 | let install = volta_install()?; 15 | 16 | Ok(vec![home.shim_dir().to_owned(), install.root().to_owned()]) 17 | } 18 | -------------------------------------------------------------------------------- /crates/volta-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The main implementation crate for the core of Volta. 2 | 3 | mod command; 4 | pub mod error; 5 | pub mod event; 6 | pub mod fs; 7 | mod hook; 8 | pub mod inventory; 9 | pub mod layout; 10 | pub mod log; 11 | pub mod monitor; 12 | pub mod platform; 13 | pub mod project; 14 | pub mod run; 15 | pub mod session; 16 | pub mod shim; 17 | pub mod signal; 18 | pub mod style; 19 | pub mod sync; 20 | pub mod tool; 21 | pub mod toolchain; 22 | pub mod version; 23 | 24 | const VOLTA_FEATURE_PNPM: &str = "VOLTA_FEATURE_PNPM"; 25 | -------------------------------------------------------------------------------- /crates/volta-core/src/monitor.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::io::Write; 3 | use std::path::PathBuf; 4 | use std::process::{Child, Stdio}; 5 | 6 | use log::debug; 7 | use tempfile::NamedTempFile; 8 | 9 | use crate::command::create_command; 10 | use crate::event::Event; 11 | 12 | /// Send event to the spawned command process 13 | // if hook command is not configured, this is not called 14 | pub fn send_events(command: &str, events: &[Event]) { 15 | match serde_json::to_string_pretty(&events) { 16 | Ok(events_json) => { 17 | let tempfile_path = env::var_os("VOLTA_WRITE_EVENTS_FILE") 18 | .and_then(|_| write_events_file(events_json.clone())); 19 | if let Some(ref mut child_process) = spawn_process(command, tempfile_path) { 20 | if let Some(ref mut p_stdin) = child_process.stdin.as_mut() { 21 | if let Err(error) = writeln!(p_stdin, "{}", events_json) { 22 | debug!("Could not write events to executable stdin: {:?}", error); 23 | } 24 | } 25 | } 26 | } 27 | Err(error) => { 28 | debug!("Could not serialize events data to JSON: {:?}", error); 29 | } 30 | } 31 | } 32 | 33 | // Write the events JSON to a file in the temporary directory 34 | fn write_events_file(events_json: String) -> Option<PathBuf> { 35 | match NamedTempFile::new() { 36 | Ok(mut events_file) => { 37 | match events_file.write_all(events_json.as_bytes()) { 38 | Ok(()) => { 39 | let path = events_file.into_temp_path(); 40 | // if it's not persisted, the temp file will be automatically deleted 41 | // (and the executable won't be able to read it) 42 | match path.keep() { 43 | Ok(tempfile_path) => Some(tempfile_path), 44 | Err(error) => { 45 | debug!("Failed to persist temp file for events data: {:?}", error); 46 | None 47 | } 48 | } 49 | } 50 | Err(error) => { 51 | debug!("Failed to write events to the temp file: {:?}", error); 52 | None 53 | } 54 | } 55 | } 56 | Err(error) => { 57 | debug!("Failed to create a temp file for events data: {:?}", error); 58 | None 59 | } 60 | } 61 | } 62 | 63 | // Spawn a child process to receive the events data, setting the path to the events file as an env var 64 | fn spawn_process(command: &str, tempfile_path: Option<PathBuf>) -> Option<Child> { 65 | command.split(' ').take(1).next().and_then(|executable| { 66 | let mut child = create_command(executable); 67 | child.args(command.split(' ').skip(1)); 68 | child.stdin(Stdio::piped()); 69 | if let Some(events_file) = tempfile_path { 70 | child.env("EVENTS_FILE", events_file); 71 | } 72 | 73 | #[cfg(not(debug_assertions))] 74 | // Hide stdout and stderr of spawned process in release mode 75 | child.stdout(Stdio::null()).stderr(Stdio::null()); 76 | 77 | match child.spawn() { 78 | Err(err) => { 79 | debug!("Unable to run executable command: '{}'\n{}", command, err); 80 | None 81 | } 82 | Ok(c) => Some(c), 83 | } 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /crates/volta-core/src/platform/image.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | use std::path::PathBuf; 3 | 4 | use super::{build_path_error, Sourced}; 5 | use crate::error::{Context, Fallible}; 6 | use crate::layout::volta_home; 7 | use crate::tool::load_default_npm_version; 8 | use node_semver::Version; 9 | 10 | /// A platform image. 11 | pub struct Image { 12 | /// The pinned version of Node. 13 | pub node: Sourced<Version>, 14 | /// The custom version of npm, if any. `None` represents using the npm that is bundled with Node 15 | pub npm: Option<Sourced<Version>>, 16 | /// The pinned version of pnpm, if any. 17 | pub pnpm: Option<Sourced<Version>>, 18 | /// The pinned version of Yarn, if any. 19 | pub yarn: Option<Sourced<Version>>, 20 | } 21 | 22 | impl Image { 23 | fn bins(&self) -> Fallible<Vec<PathBuf>> { 24 | let home = volta_home()?; 25 | let mut bins = Vec::with_capacity(3); 26 | 27 | if let Some(npm) = &self.npm { 28 | let npm_str = npm.value.to_string(); 29 | bins.push(home.npm_image_bin_dir(&npm_str)); 30 | } 31 | 32 | if let Some(pnpm) = &self.pnpm { 33 | let pnpm_str = pnpm.value.to_string(); 34 | bins.push(home.pnpm_image_bin_dir(&pnpm_str)); 35 | } 36 | 37 | if let Some(yarn) = &self.yarn { 38 | let yarn_str = yarn.value.to_string(); 39 | bins.push(home.yarn_image_bin_dir(&yarn_str)); 40 | } 41 | 42 | // Add Node path to the bins last, so that any custom version of npm will be earlier in the PATH 43 | let node_str = self.node.value.to_string(); 44 | bins.push(home.node_image_bin_dir(&node_str)); 45 | Ok(bins) 46 | } 47 | 48 | /// Produces a modified version of the current `PATH` environment variable that 49 | /// will find toolchain executables (Node, npm, pnpm, Yarn) in the installation directories 50 | /// for the given versions instead of in the Volta shim directory. 51 | pub fn path(&self) -> Fallible<OsString> { 52 | let old_path = envoy::path().unwrap_or_else(|| envoy::Var::from("")); 53 | 54 | old_path 55 | .split() 56 | .prefix(self.bins()?) 57 | .join() 58 | .with_context(build_path_error) 59 | } 60 | 61 | /// Determines the sourced version of npm that will be available, resolving the version bundled with Node, if needed 62 | pub fn resolve_npm(&self) -> Fallible<Sourced<Version>> { 63 | match &self.npm { 64 | Some(npm) => Ok(npm.clone()), 65 | None => load_default_npm_version(&self.node.value).map(|npm| Sourced { 66 | value: npm, 67 | source: self.node.source, 68 | }), 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/volta-core/src/platform/system.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | 3 | use super::build_path_error; 4 | use crate::error::{Context, Fallible}; 5 | use crate::layout::env_paths; 6 | 7 | /// A lightweight namespace type representing the system environment, i.e. the environment 8 | /// with Volta removed. 9 | pub struct System; 10 | 11 | impl System { 12 | /// Produces a modified version of the current `PATH` environment variable that 13 | /// removes the Volta shims and binaries, to use for running system node and 14 | /// executables. 15 | pub fn path() -> Fallible<OsString> { 16 | let old_path = envoy::path().unwrap_or_else(|| envoy::Var::from("")); 17 | let mut new_path = old_path.split(); 18 | 19 | for remove_path in env_paths()? { 20 | new_path = new_path.remove(remove_path); 21 | } 22 | 23 | new_path.join().with_context(build_path_error) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/volta-core/src/run/node.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::ffi::OsString; 3 | 4 | use super::executor::{Executor, ToolCommand, ToolKind}; 5 | use super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR}; 6 | use crate::error::{ErrorKind, Fallible}; 7 | use crate::platform::{Platform, System}; 8 | use crate::session::{ActivityKind, Session}; 9 | 10 | /// Build a `ToolCommand` for Node 11 | pub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> { 12 | session.add_event_start(ActivityKind::Node); 13 | // Don't re-evaluate the platform if this is a recursive call 14 | let platform = match env::var_os(RECURSION_ENV_VAR) { 15 | Some(_) => None, 16 | None => Platform::current(session)?, 17 | }; 18 | 19 | Ok(ToolCommand::new("node", args, platform, ToolKind::Node).into()) 20 | } 21 | 22 | /// Determine the execution context (PATH and failure error message) for Node 23 | pub(super) fn execution_context( 24 | platform: Option<Platform>, 25 | session: &mut Session, 26 | ) -> Fallible<(OsString, ErrorKind)> { 27 | match platform { 28 | Some(plat) => { 29 | let image = plat.checkout(session)?; 30 | let path = image.path()?; 31 | debug_active_image(&image); 32 | 33 | Ok((path, ErrorKind::BinaryExecError)) 34 | } 35 | None => { 36 | let path = System::path()?; 37 | debug_no_platform(); 38 | Ok((path, ErrorKind::NoPlatform)) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/volta-core/src/run/npm.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::ffi::OsString; 3 | use std::fs::File; 4 | 5 | use super::executor::{Executor, ToolCommand, ToolKind, UninstallCommand}; 6 | use super::parser::{CommandArg, InterceptedCommand}; 7 | use super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR}; 8 | use crate::error::{ErrorKind, Fallible}; 9 | use crate::platform::{Platform, System}; 10 | use crate::session::{ActivityKind, Session}; 11 | use crate::tool::{PackageManifest, Spec}; 12 | use crate::version::VersionSpec; 13 | 14 | /// Build an `Executor` for npm 15 | /// 16 | /// If the command is a global install or uninstall and we have a default platform available, then 17 | /// we will use custom logic to ensure that the package is correctly installed / uninstalled in the 18 | /// Volta directory. 19 | /// 20 | /// If the command is _not_ a global install / uninstall or we don't have a default platform, then 21 | /// we will allow npm to execute the command as usual. 22 | pub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> { 23 | session.add_event_start(ActivityKind::Npm); 24 | // Don't re-evaluate the context or global install interception if this is a recursive call 25 | let platform = match env::var_os(RECURSION_ENV_VAR) { 26 | Some(_) => None, 27 | None => { 28 | match CommandArg::for_npm(args) { 29 | CommandArg::Global(cmd) => { 30 | // For globals, only intercept if the default platform exists 31 | if let Some(default_platform) = session.default_platform()? { 32 | return cmd.executor(default_platform); 33 | } 34 | } 35 | CommandArg::Intercepted(InterceptedCommand::Link(link)) => { 36 | // For link commands, only intercept if a platform exists 37 | if let Some(platform) = Platform::current(session)? { 38 | return link.executor(platform, current_project_name(session)); 39 | } 40 | } 41 | CommandArg::Intercepted(InterceptedCommand::Unlink) => { 42 | // For unlink, attempt to find the current project name. If successful, treat 43 | // this as a global uninstall of the current project. 44 | if let Some(name) = current_project_name(session) { 45 | // Same as for link, only intercept if a platform exists 46 | if Platform::current(session)?.is_some() { 47 | return Ok(UninstallCommand::new(Spec::Package( 48 | name, 49 | VersionSpec::None, 50 | )) 51 | .into()); 52 | } 53 | } 54 | } 55 | _ => {} 56 | } 57 | 58 | Platform::current(session)? 59 | } 60 | }; 61 | 62 | Ok(ToolCommand::new("npm", args, platform, ToolKind::Npm).into()) 63 | } 64 | 65 | /// Determine the execution context (PATH and failure error message) for npm 66 | pub(super) fn execution_context( 67 | platform: Option<Platform>, 68 | session: &mut Session, 69 | ) -> Fallible<(OsString, ErrorKind)> { 70 | match platform { 71 | Some(plat) => { 72 | let image = plat.checkout(session)?; 73 | let path = image.path()?; 74 | debug_active_image(&image); 75 | 76 | Ok((path, ErrorKind::BinaryExecError)) 77 | } 78 | None => { 79 | let path = System::path()?; 80 | debug_no_platform(); 81 | Ok((path, ErrorKind::NoPlatform)) 82 | } 83 | } 84 | } 85 | 86 | /// Determine the name of the current project, if possible 87 | fn current_project_name(session: &mut Session) -> Option<String> { 88 | let project = session.project().ok()??; 89 | let manifest_file = File::open(project.manifest_file()).ok()?; 90 | let manifest: PackageManifest = serde_json::de::from_reader(manifest_file).ok()?; 91 | 92 | Some(manifest.name) 93 | } 94 | -------------------------------------------------------------------------------- /crates/volta-core/src/run/npx.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::ffi::OsString; 3 | 4 | use super::executor::{Executor, ToolCommand, ToolKind}; 5 | use super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR}; 6 | use crate::error::{ErrorKind, Fallible}; 7 | use crate::platform::{Platform, System}; 8 | use crate::session::{ActivityKind, Session}; 9 | use node_semver::Version; 10 | use once_cell::sync::Lazy; 11 | 12 | static REQUIRED_NPM_VERSION: Lazy<Version> = Lazy::new(|| Version { 13 | major: 5, 14 | minor: 2, 15 | patch: 0, 16 | build: vec![], 17 | pre_release: vec![], 18 | }); 19 | 20 | /// Build a `ToolCommand` for npx 21 | pub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> { 22 | session.add_event_start(ActivityKind::Npx); 23 | // Don't re-evaluate the context if this is a recursive call 24 | let platform = match env::var_os(RECURSION_ENV_VAR) { 25 | Some(_) => None, 26 | None => Platform::current(session)?, 27 | }; 28 | 29 | Ok(ToolCommand::new("npx", args, platform, ToolKind::Npx).into()) 30 | } 31 | 32 | /// Determine the execution context (PATH and failure error message) for npx 33 | pub(super) fn execution_context( 34 | platform: Option<Platform>, 35 | session: &mut Session, 36 | ) -> Fallible<(OsString, ErrorKind)> { 37 | match platform { 38 | Some(plat) => { 39 | let image = plat.checkout(session)?; 40 | 41 | // If the npm version is lower than the minimum required, we can show a helpful error 42 | // message instead of a 'command not found' error. 43 | let active_npm = image.resolve_npm()?; 44 | if active_npm.value < *REQUIRED_NPM_VERSION { 45 | return Err(ErrorKind::NpxNotAvailable { 46 | version: active_npm.value.to_string(), 47 | } 48 | .into()); 49 | } 50 | 51 | let path = image.path()?; 52 | debug_active_image(&image); 53 | 54 | Ok((path, ErrorKind::BinaryExecError)) 55 | } 56 | None => { 57 | let path = System::path()?; 58 | debug_no_platform(); 59 | Ok((path, ErrorKind::NoPlatform)) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/volta-core/src/run/pnpm.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::ffi::OsString; 3 | 4 | use super::executor::{Executor, ToolCommand, ToolKind}; 5 | use super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR}; 6 | use crate::error::{ErrorKind, Fallible}; 7 | use crate::platform::{Platform, Source, System}; 8 | use crate::session::{ActivityKind, Session}; 9 | 10 | pub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> { 11 | session.add_event_start(ActivityKind::Pnpm); 12 | // Don't re-evaluate the context or global install interception if this is a recursive call 13 | let platform = match env::var_os(RECURSION_ENV_VAR) { 14 | Some(_) => None, 15 | None => { 16 | // FIXME: Figure out how to intercept pnpm global commands properly. 17 | // This guard prevents all global commands from running, it should 18 | // be removed when we fully implement global command interception. 19 | let is_global = args.iter().any(|f| f == "--global" || f == "-g"); 20 | if is_global { 21 | return Err(ErrorKind::Unimplemented { 22 | feature: "pnpm global commands".into(), 23 | } 24 | .into()); 25 | } 26 | 27 | Platform::current(session)? 28 | } 29 | }; 30 | 31 | Ok(ToolCommand::new("pnpm", args, platform, ToolKind::Pnpm).into()) 32 | } 33 | 34 | /// Determine the execution context (PATH and failure error message) for pnpm 35 | pub(super) fn execution_context( 36 | platform: Option<Platform>, 37 | session: &mut Session, 38 | ) -> Fallible<(OsString, ErrorKind)> { 39 | match platform { 40 | Some(plat) => { 41 | validate_platform_pnpm(&plat)?; 42 | 43 | let image = plat.checkout(session)?; 44 | let path = image.path()?; 45 | debug_active_image(&image); 46 | 47 | Ok((path, ErrorKind::BinaryExecError)) 48 | } 49 | None => { 50 | let path = System::path()?; 51 | debug_no_platform(); 52 | Ok((path, ErrorKind::NoPlatform)) 53 | } 54 | } 55 | } 56 | 57 | fn validate_platform_pnpm(platform: &Platform) -> Fallible<()> { 58 | match &platform.pnpm { 59 | Some(_) => Ok(()), 60 | None => match platform.node.source { 61 | Source::Project => Err(ErrorKind::NoProjectPnpm.into()), 62 | Source::Default | Source::Binary => Err(ErrorKind::NoDefaultPnpm.into()), 63 | Source::CommandLine => Err(ErrorKind::NoCommandLinePnpm.into()), 64 | }, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/volta-core/src/run/yarn.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::ffi::OsString; 3 | 4 | use super::executor::{Executor, ToolCommand, ToolKind}; 5 | use super::parser::CommandArg; 6 | use super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR}; 7 | use crate::error::{ErrorKind, Fallible}; 8 | use crate::platform::{Platform, Source, System}; 9 | use crate::session::{ActivityKind, Session}; 10 | 11 | /// Build an `Executor` for Yarn 12 | /// 13 | /// If the command is a global add or remove and we have a default platform available, then we will 14 | /// use custom logic to ensure that the package is correctly installed / uninstalled in the Volta 15 | /// directory. 16 | /// 17 | /// If the command is _not_ a global add / remove or we don't have a default platform, then 18 | /// we will allow Yarn to execute the command as usual. 19 | pub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible<Executor> { 20 | session.add_event_start(ActivityKind::Yarn); 21 | // Don't re-evaluate the context or global install interception if this is a recursive call 22 | let platform = match env::var_os(RECURSION_ENV_VAR) { 23 | Some(_) => None, 24 | None => { 25 | if let CommandArg::Global(cmd) = CommandArg::for_yarn(args) { 26 | // For globals, only intercept if the default platform exists 27 | if let Some(default_platform) = session.default_platform()? { 28 | return cmd.executor(default_platform); 29 | } 30 | } 31 | 32 | Platform::current(session)? 33 | } 34 | }; 35 | 36 | Ok(ToolCommand::new("yarn", args, platform, ToolKind::Yarn).into()) 37 | } 38 | 39 | /// Determine the execution context (PATH and failure error message) for Yarn 40 | pub(super) fn execution_context( 41 | platform: Option<Platform>, 42 | session: &mut Session, 43 | ) -> Fallible<(OsString, ErrorKind)> { 44 | match platform { 45 | Some(plat) => { 46 | validate_platform_yarn(&plat)?; 47 | 48 | let image = plat.checkout(session)?; 49 | let path = image.path()?; 50 | debug_active_image(&image); 51 | 52 | Ok((path, ErrorKind::BinaryExecError)) 53 | } 54 | None => { 55 | let path = System::path()?; 56 | debug_no_platform(); 57 | Ok((path, ErrorKind::NoPlatform)) 58 | } 59 | } 60 | } 61 | 62 | fn validate_platform_yarn(platform: &Platform) -> Fallible<()> { 63 | match &platform.yarn { 64 | Some(_) => Ok(()), 65 | None => match platform.node.source { 66 | Source::Project => Err(ErrorKind::NoProjectYarn.into()), 67 | Source::Default | Source::Binary => Err(ErrorKind::NoDefaultYarn.into()), 68 | Source::CommandLine => Err(ErrorKind::NoCommandLineYarn.into()), 69 | }, 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/volta-core/src/signal.rs: -------------------------------------------------------------------------------- 1 | use std::process::exit; 2 | use std::sync::atomic::{AtomicBool, Ordering}; 3 | 4 | use log::debug; 5 | 6 | static SHIM_HAS_CONTROL: AtomicBool = AtomicBool::new(false); 7 | const INTERRUPTED_EXIT_CODE: i32 = 130; 8 | 9 | pub fn pass_control_to_shim() { 10 | SHIM_HAS_CONTROL.store(true, Ordering::SeqCst); 11 | } 12 | 13 | pub fn setup_signal_handler() { 14 | let result = ctrlc::set_handler(|| { 15 | if !SHIM_HAS_CONTROL.load(Ordering::SeqCst) { 16 | exit(INTERRUPTED_EXIT_CODE); 17 | } 18 | }); 19 | 20 | if result.is_err() { 21 | debug!("Unable to set Ctrl+C handler, SIGINT will not be handled correctly"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /crates/volta-core/src/tool/node/metadata.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use super::NODE_DISTRO_IDENTIFIER; 4 | #[cfg(any( 5 | all(target_os = "macos", target_arch = "aarch64"), 6 | all(target_os = "windows", target_arch = "aarch64") 7 | ))] 8 | use super::NODE_DISTRO_IDENTIFIER_FALLBACK; 9 | use crate::version::{option_version_serde, version_serde}; 10 | use node_semver::Version; 11 | use serde::{Deserialize, Deserializer}; 12 | 13 | /// The index of the public Node server. 14 | pub struct NodeIndex { 15 | pub(super) entries: Vec<NodeEntry>, 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct NodeEntry { 20 | pub version: Version, 21 | pub lts: bool, 22 | } 23 | 24 | #[derive(Deserialize)] 25 | pub struct RawNodeIndex(Vec<RawNodeEntry>); 26 | 27 | #[derive(Deserialize)] 28 | pub struct RawNodeEntry { 29 | #[serde(with = "version_serde")] 30 | version: Version, 31 | #[serde(default)] // handles Option 32 | #[serde(with = "option_version_serde")] 33 | npm: Option<Version>, 34 | files: HashSet<String>, 35 | #[serde(deserialize_with = "lts_version_serde")] 36 | lts: bool, 37 | } 38 | 39 | impl From<RawNodeIndex> for NodeIndex { 40 | fn from(raw: RawNodeIndex) -> NodeIndex { 41 | let entries = raw 42 | .0 43 | .into_iter() 44 | .filter_map(|entry| { 45 | #[cfg(not(any( 46 | all(target_os = "macos", target_arch = "aarch64"), 47 | all(target_os = "windows", target_arch = "aarch64") 48 | )))] 49 | if entry.npm.is_some() && entry.files.contains(NODE_DISTRO_IDENTIFIER) { 50 | Some(NodeEntry { 51 | version: entry.version, 52 | lts: entry.lts, 53 | }) 54 | } else { 55 | None 56 | } 57 | 58 | #[cfg(any( 59 | all(target_os = "macos", target_arch = "aarch64"), 60 | all(target_os = "windows", target_arch = "aarch64") 61 | ))] 62 | if entry.npm.is_some() 63 | && (entry.files.contains(NODE_DISTRO_IDENTIFIER) 64 | || entry.files.contains(NODE_DISTRO_IDENTIFIER_FALLBACK)) 65 | { 66 | Some(NodeEntry { 67 | version: entry.version, 68 | lts: entry.lts, 69 | }) 70 | } else { 71 | None 72 | } 73 | }) 74 | .collect(); 75 | 76 | NodeIndex { entries } 77 | } 78 | } 79 | 80 | #[allow(clippy::unnecessary_wraps)] // Needs to match the API expected by Serde 81 | fn lts_version_serde<'de, D>(deserializer: D) -> Result<bool, D::Error> 82 | where 83 | D: Deserializer<'de>, 84 | { 85 | match String::deserialize(deserializer) { 86 | Ok(_) => Ok(true), 87 | Err(_) => Ok(false), 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /crates/volta-core/src/tool/npm/resolve.rs: -------------------------------------------------------------------------------- 1 | //! Provides resolution of npm Version requirements into specific versions 2 | 3 | use super::super::registry::{ 4 | fetch_npm_registry, public_registry_index, PackageDetails, PackageIndex, 5 | }; 6 | use crate::error::{ErrorKind, Fallible}; 7 | use crate::hook::ToolHooks; 8 | use crate::session::Session; 9 | use crate::tool::Npm; 10 | use crate::version::{VersionSpec, VersionTag}; 11 | use log::debug; 12 | use node_semver::{Range, Version}; 13 | 14 | pub fn resolve(matching: VersionSpec, session: &mut Session) -> Fallible<Option<Version>> { 15 | let hooks = session.hooks()?.npm(); 16 | match matching { 17 | VersionSpec::Semver(requirement) => resolve_semver(requirement, hooks).map(Some), 18 | VersionSpec::Exact(version) => Ok(Some(version)), 19 | VersionSpec::None | VersionSpec::Tag(VersionTag::Latest) => { 20 | resolve_tag("latest", hooks).map(Some) 21 | } 22 | VersionSpec::Tag(VersionTag::Custom(tag)) if tag == "bundled" => Ok(None), 23 | VersionSpec::Tag(tag) => resolve_tag(&tag.to_string(), hooks).map(Some), 24 | } 25 | } 26 | 27 | fn fetch_npm_index(hooks: Option<&ToolHooks<Npm>>) -> Fallible<(String, PackageIndex)> { 28 | let url = match hooks { 29 | Some(&ToolHooks { 30 | index: Some(ref hook), 31 | .. 32 | }) => { 33 | debug!("Using npm.index hook to determine npm index URL"); 34 | hook.resolve("npm")? 35 | } 36 | _ => public_registry_index("npm"), 37 | }; 38 | 39 | fetch_npm_registry(url, "npm") 40 | } 41 | 42 | fn resolve_tag(tag: &str, hooks: Option<&ToolHooks<Npm>>) -> Fallible<Version> { 43 | let (url, mut index) = fetch_npm_index(hooks)?; 44 | 45 | match index.tags.remove(tag) { 46 | Some(version) => { 47 | debug!("Found npm@{} matching tag '{}' from {}", version, tag, url); 48 | Ok(version) 49 | } 50 | None => Err(ErrorKind::NpmVersionNotFound { 51 | matching: tag.into(), 52 | } 53 | .into()), 54 | } 55 | } 56 | 57 | fn resolve_semver(matching: Range, hooks: Option<&ToolHooks<Npm>>) -> Fallible<Version> { 58 | let (url, index) = fetch_npm_index(hooks)?; 59 | 60 | let details_opt = index 61 | .entries 62 | .into_iter() 63 | .find(|PackageDetails { version, .. }| matching.satisfies(version)); 64 | 65 | match details_opt { 66 | Some(details) => { 67 | debug!( 68 | "Found npm@{} matching requirement '{}' from {}", 69 | details.version, matching, url 70 | ); 71 | Ok(details.version) 72 | } 73 | None => Err(ErrorKind::NpmVersionNotFound { 74 | matching: matching.to_string(), 75 | } 76 | .into()), 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/volta-core/src/tool/package/configure.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use super::manager::PackageManager; 4 | use super::metadata::{BinConfig, PackageConfig, PackageManifest}; 5 | use crate::error::{ErrorKind, Fallible}; 6 | use crate::layout::volta_home; 7 | use crate::platform::{Image, PlatformSpec}; 8 | use crate::shim; 9 | use crate::tool::check_shim_reachable; 10 | 11 | /// Read the manifest for the package being installed 12 | pub(super) fn parse_manifest( 13 | package_name: &str, 14 | staging_dir: PathBuf, 15 | manager: PackageManager, 16 | ) -> Fallible<PackageManifest> { 17 | let mut package_dir = manager.source_dir(staging_dir); 18 | package_dir.push(package_name); 19 | 20 | PackageManifest::for_dir(package_name, &package_dir) 21 | } 22 | 23 | /// Generate configuration files and shims for the package and each of its bins 24 | pub(super) fn write_config_and_shims( 25 | name: &str, 26 | manifest: &PackageManifest, 27 | image: &Image, 28 | manager: PackageManager, 29 | ) -> Fallible<()> { 30 | validate_bins(name, manifest)?; 31 | 32 | let platform = PlatformSpec { 33 | node: image.node.value.clone(), 34 | npm: image.npm.clone().map(|s| s.value), 35 | pnpm: image.pnpm.clone().map(|s| s.value), 36 | yarn: image.yarn.clone().map(|s| s.value), 37 | }; 38 | 39 | // Generate the shims and bin configs for each bin provided by the package 40 | for bin_name in &manifest.bin { 41 | shim::create(bin_name)?; 42 | check_shim_reachable(bin_name); 43 | 44 | BinConfig { 45 | name: bin_name.clone(), 46 | package: name.into(), 47 | version: manifest.version.clone(), 48 | platform: platform.clone(), 49 | manager, 50 | } 51 | .write()?; 52 | } 53 | 54 | // Write the config for the package 55 | PackageConfig { 56 | name: name.into(), 57 | version: manifest.version.clone(), 58 | platform, 59 | bins: manifest.bin.clone(), 60 | manager, 61 | } 62 | .write()?; 63 | 64 | Ok(()) 65 | } 66 | 67 | /// Validate that we aren't attempting to install a bin that is already installed by 68 | /// another package. 69 | fn validate_bins(package_name: &str, manifest: &PackageManifest) -> Fallible<()> { 70 | let home = volta_home()?; 71 | for bin_name in &manifest.bin { 72 | // Check for name conflicts with already-installed bins 73 | // Some packages may install bins with the same name 74 | if let Ok(config) = BinConfig::from_file(home.default_tool_bin_config(bin_name)) { 75 | // The file exists, so there is a bin with this name 76 | // That is okay iff it came from the package that is currently being installed 77 | if package_name != config.package { 78 | return Err(ErrorKind::BinaryAlreadyInstalled { 79 | bin_name: bin_name.into(), 80 | existing_package: config.package, 81 | new_package: package_name.into(), 82 | } 83 | .into()); 84 | } 85 | } 86 | } 87 | 88 | Ok(()) 89 | } 90 | -------------------------------------------------------------------------------- /crates/volta-core/src/tool/package/install.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use super::manager::PackageManager; 4 | use crate::command::create_command; 5 | use crate::error::{Context, ErrorKind, Fallible}; 6 | use crate::platform::Image; 7 | use crate::style::progress_spinner; 8 | use log::debug; 9 | 10 | /// Use `npm install --global` to install the package 11 | /// 12 | /// Sets the environment variable `npm_config_prefix` to redirect the install to the Volta 13 | /// data directory, taking advantage of the standard global install behavior with a custom 14 | /// location 15 | pub(super) fn run_global_install( 16 | package: String, 17 | staging_dir: PathBuf, 18 | platform_image: &Image, 19 | ) -> Fallible<()> { 20 | let mut command = create_command("npm"); 21 | command.args([ 22 | "install", 23 | "--global", 24 | "--loglevel=warn", 25 | "--no-update-notifier", 26 | "--no-audit", 27 | ]); 28 | command.arg(&package); 29 | command.env("PATH", platform_image.path()?); 30 | PackageManager::Npm.setup_global_command(&mut command, staging_dir); 31 | 32 | debug!("Installing {} with command: {:?}", package, command); 33 | let spinner = progress_spinner(format!("Installing {}", package)); 34 | let output_result = command 35 | .output() 36 | .with_context(|| ErrorKind::PackageInstallFailed { 37 | package: package.clone(), 38 | }); 39 | spinner.finish_and_clear(); 40 | let output = output_result?; 41 | 42 | let stderr = String::from_utf8_lossy(&output.stderr); 43 | debug!("[install stderr]\n{}", stderr); 44 | debug!( 45 | "[install stdout]\n{}", 46 | String::from_utf8_lossy(&output.stdout) 47 | ); 48 | 49 | if output.status.success() { 50 | Ok(()) 51 | } else if stderr.contains("code E404") { 52 | // npm outputs "code E404" as part of the error output when a package couldn't be found 53 | // Detect that and show a nicer error message (since we likely know the problem in that case) 54 | Err(ErrorKind::PackageNotFound { package }.into()) 55 | } else { 56 | Err(ErrorKind::PackageInstallFailed { package }.into()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /crates/volta-core/src/tool/package/uninstall.rs: -------------------------------------------------------------------------------- 1 | use super::metadata::{BinConfig, PackageConfig}; 2 | use crate::error::{Context, ErrorKind, Fallible}; 3 | use crate::fs::{ 4 | dir_entry_match, ok_if_not_found, read_dir_eager, remove_dir_if_exists, remove_file_if_exists, 5 | }; 6 | use crate::layout::volta_home; 7 | use crate::shim; 8 | use crate::style::success_prefix; 9 | use crate::sync::VoltaLock; 10 | use log::{info, warn}; 11 | 12 | /// Uninstalls the specified package. 13 | /// 14 | /// This removes: 15 | /// 16 | /// - The JSON configuration files for both the package and its bins 17 | /// - The shims for the package bins 18 | /// - The package directory itself 19 | pub fn uninstall(name: &str) -> Fallible<()> { 20 | let home = volta_home()?; 21 | // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes 22 | let _lock = VoltaLock::acquire(); 23 | 24 | // If the package config file exists, use that to remove any installed bins and shims 25 | let package_config_file = home.default_package_config_file(name); 26 | 27 | let package_found = match PackageConfig::from_file_if_exists(&package_config_file)? { 28 | None => { 29 | // there is no package config - check for orphaned binaries 30 | let package_binary_list = binaries_from_package(name)?; 31 | if !package_binary_list.is_empty() { 32 | for bin_name in package_binary_list { 33 | remove_config_and_shim(&bin_name, name)?; 34 | } 35 | true 36 | } else { 37 | false 38 | } 39 | } 40 | Some(package_config) => { 41 | for bin_name in package_config.bins { 42 | remove_config_and_shim(&bin_name, name)?; 43 | } 44 | 45 | remove_file_if_exists(package_config_file)?; 46 | true 47 | } 48 | }; 49 | 50 | remove_shared_link_dir(name)?; 51 | 52 | // Remove the package directory itself 53 | let package_image_dir = home.package_image_dir(name); 54 | remove_dir_if_exists(package_image_dir)?; 55 | 56 | if package_found { 57 | info!("{} package '{}' uninstalled", success_prefix(), name); 58 | } else { 59 | warn!("No package '{}' found to uninstall", name); 60 | } 61 | 62 | Ok(()) 63 | } 64 | 65 | /// Remove a shim and its associated configuration file 66 | fn remove_config_and_shim(bin_name: &str, pkg_name: &str) -> Fallible<()> { 67 | shim::delete(bin_name)?; 68 | let config_file = volta_home()?.default_tool_bin_config(bin_name); 69 | remove_file_if_exists(config_file)?; 70 | info!( 71 | "Removed executable '{}' installed by '{}'", 72 | bin_name, pkg_name 73 | ); 74 | Ok(()) 75 | } 76 | 77 | /// Reads the contents of a directory and returns a Vec containing the names of 78 | /// all the binaries installed by the given package. 79 | fn binaries_from_package(package: &str) -> Fallible<Vec<String>> { 80 | let bin_config_dir = volta_home()?.default_bin_dir(); 81 | 82 | dir_entry_match(bin_config_dir, |entry| { 83 | let path = entry.path(); 84 | if let Ok(config) = BinConfig::from_file(path) { 85 | if config.package == package { 86 | return Some(config.name); 87 | } 88 | } 89 | None 90 | }) 91 | .or_else(ok_if_not_found) 92 | .with_context(|| ErrorKind::ReadBinConfigDirError { 93 | dir: bin_config_dir.to_owned(), 94 | }) 95 | } 96 | 97 | /// Remove the link to the package in the shared lib directory 98 | /// 99 | /// For scoped packages, if the scope directory is now empty, it will also be removed 100 | fn remove_shared_link_dir(name: &str) -> Fallible<()> { 101 | // Remove the link in the shared package directory, if it exists 102 | let mut shared_lib_dir = volta_home()?.shared_lib_dir(name); 103 | remove_dir_if_exists(&shared_lib_dir)?; 104 | 105 | // For scoped packages, clean up the scope directory if it is now empty 106 | if name.starts_with('@') { 107 | shared_lib_dir.pop(); 108 | 109 | if let Ok(mut entries) = read_dir_eager(&shared_lib_dir) { 110 | if entries.next().is_none() { 111 | remove_dir_if_exists(&shared_lib_dir)?; 112 | } 113 | } 114 | } 115 | 116 | Ok(()) 117 | } 118 | -------------------------------------------------------------------------------- /crates/volta-core/src/tool/pnpm/mod.rs: -------------------------------------------------------------------------------- 1 | use node_semver::Version; 2 | use std::fmt::{self, Display}; 3 | 4 | use crate::error::{ErrorKind, Fallible}; 5 | use crate::inventory::pnpm_available; 6 | use crate::session::Session; 7 | use crate::style::tool_version; 8 | use crate::sync::VoltaLock; 9 | 10 | use super::{ 11 | check_fetched, check_shim_reachable, debug_already_fetched, info_fetched, info_installed, 12 | info_pinned, info_project_version, FetchStatus, Tool, 13 | }; 14 | 15 | mod fetch; 16 | mod resolve; 17 | 18 | pub use resolve::resolve; 19 | 20 | /// The Tool implementation for fetching and installing pnpm 21 | pub struct Pnpm { 22 | pub(super) version: Version, 23 | } 24 | 25 | impl Pnpm { 26 | pub fn new(version: Version) -> Self { 27 | Pnpm { version } 28 | } 29 | 30 | pub fn archive_basename(version: &str) -> String { 31 | format!("pnpm-{}", version) 32 | } 33 | 34 | pub fn archive_filename(version: &str) -> String { 35 | format!("{}.tgz", Pnpm::archive_basename(version)) 36 | } 37 | 38 | pub(crate) fn ensure_fetched(&self, session: &mut Session) -> Fallible<()> { 39 | match check_fetched(|| pnpm_available(&self.version))? { 40 | FetchStatus::AlreadyFetched => { 41 | debug_already_fetched(self); 42 | Ok(()) 43 | } 44 | FetchStatus::FetchNeeded(_lock) => fetch::fetch(&self.version, session.hooks()?.pnpm()), 45 | } 46 | } 47 | } 48 | 49 | impl Tool for Pnpm { 50 | fn fetch(self: Box<Self>, session: &mut Session) -> Fallible<()> { 51 | self.ensure_fetched(session)?; 52 | 53 | info_fetched(self); 54 | Ok(()) 55 | } 56 | 57 | fn install(self: Box<Self>, session: &mut Session) -> Fallible<()> { 58 | // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes 59 | let _lock = VoltaLock::acquire(); 60 | self.ensure_fetched(session)?; 61 | 62 | session 63 | .toolchain_mut()? 64 | .set_active_pnpm(Some(self.version.clone()))?; 65 | 66 | info_installed(&self); 67 | check_shim_reachable("pnpm"); 68 | 69 | if let Ok(Some(project)) = session.project_platform() { 70 | if let Some(pnpm) = &project.pnpm { 71 | info_project_version(tool_version("pnpm", pnpm), &self); 72 | } 73 | } 74 | Ok(()) 75 | } 76 | 77 | fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()> { 78 | if session.project()?.is_some() { 79 | self.ensure_fetched(session)?; 80 | 81 | // Note: We know this will succeed, since we checked above 82 | let project = session.project_mut()?.unwrap(); 83 | project.pin_pnpm(Some(self.version.clone()))?; 84 | 85 | info_pinned(self); 86 | Ok(()) 87 | } else { 88 | Err(ErrorKind::NotInPackage.into()) 89 | } 90 | } 91 | } 92 | 93 | impl Display for Pnpm { 94 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 95 | f.write_str(&tool_version("pnpm", &self.version)) 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod tests { 101 | use super::*; 102 | 103 | #[test] 104 | fn test_pnpm_archive_basename() { 105 | assert_eq!(Pnpm::archive_basename("1.2.3"), "pnpm-1.2.3"); 106 | } 107 | 108 | #[test] 109 | fn test_pnpm_archive_filename() { 110 | assert_eq!(Pnpm::archive_filename("1.2.3"), "pnpm-1.2.3.tgz"); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /crates/volta-core/src/tool/pnpm/resolve.rs: -------------------------------------------------------------------------------- 1 | use log::debug; 2 | use node_semver::{Range, Version}; 3 | 4 | use crate::error::{ErrorKind, Fallible}; 5 | use crate::hook::ToolHooks; 6 | use crate::session::Session; 7 | use crate::tool::registry::{fetch_npm_registry, public_registry_index, PackageIndex}; 8 | use crate::tool::{PackageDetails, Pnpm}; 9 | use crate::version::{VersionSpec, VersionTag}; 10 | 11 | pub fn resolve(matching: VersionSpec, session: &mut Session) -> Fallible<Version> { 12 | let hooks = session.hooks()?.pnpm(); 13 | match matching { 14 | VersionSpec::Semver(requirement) => resolve_semver(requirement, hooks), 15 | VersionSpec::Exact(version) => Ok(version), 16 | VersionSpec::None | VersionSpec::Tag(VersionTag::Latest) => resolve_tag("latest", hooks), 17 | VersionSpec::Tag(tag) => resolve_tag(&tag.to_string(), hooks), 18 | } 19 | } 20 | 21 | fn resolve_tag(tag: &str, hooks: Option<&ToolHooks<Pnpm>>) -> Fallible<Version> { 22 | let (url, mut index) = fetch_pnpm_index(hooks)?; 23 | 24 | match index.tags.remove(tag) { 25 | Some(version) => { 26 | debug!("Found pnpm@{} matching tag '{}' from {}", version, tag, url); 27 | Ok(version) 28 | } 29 | None => Err(ErrorKind::PnpmVersionNotFound { 30 | matching: tag.into(), 31 | } 32 | .into()), 33 | } 34 | } 35 | 36 | fn resolve_semver(matching: Range, hooks: Option<&ToolHooks<Pnpm>>) -> Fallible<Version> { 37 | let (url, index) = fetch_pnpm_index(hooks)?; 38 | 39 | let details_opt = index 40 | .entries 41 | .into_iter() 42 | .find(|PackageDetails { version, .. }| matching.satisfies(version)); 43 | 44 | match details_opt { 45 | Some(details) => { 46 | debug!( 47 | "Found pnpm@{} matching requirement '{}' from {}", 48 | details.version, matching, url 49 | ); 50 | Ok(details.version) 51 | } 52 | None => Err(ErrorKind::PnpmVersionNotFound { 53 | matching: matching.to_string(), 54 | } 55 | .into()), 56 | } 57 | } 58 | 59 | /// Fetch the index of available pnpm versions from the npm registry 60 | fn fetch_pnpm_index(hooks: Option<&ToolHooks<Pnpm>>) -> Fallible<(String, PackageIndex)> { 61 | let url = match hooks { 62 | Some(&ToolHooks { 63 | index: Some(ref hook), 64 | .. 65 | }) => { 66 | debug!("Using pnpm.index hook to determine pnpm index URL"); 67 | hook.resolve("pnpm")? 68 | } 69 | _ => public_registry_index("pnpm"), 70 | }; 71 | 72 | fetch_npm_registry(url, "pnpm") 73 | } 74 | -------------------------------------------------------------------------------- /crates/volta-core/src/tool/yarn/metadata.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | 3 | use crate::version::version_serde; 4 | use node_semver::Version; 5 | use serde::Deserialize; 6 | 7 | /// The public Yarn index. 8 | pub struct YarnIndex { 9 | pub(super) entries: BTreeSet<Version>, 10 | } 11 | 12 | #[derive(Deserialize)] 13 | pub struct RawYarnIndex(Vec<RawYarnEntry>); 14 | 15 | #[derive(Deserialize)] 16 | pub struct RawYarnEntry { 17 | /// Yarn releases are given a tag name of the form "v$version" where $version 18 | /// is the release's version string. 19 | #[serde(with = "version_serde")] 20 | pub tag_name: Version, 21 | 22 | /// The GitHub API provides a list of assets. Some Yarn releases don't include 23 | /// a tarball, so we don't support them and remove them from the set of available 24 | /// Yarn versions. 25 | pub assets: Vec<RawYarnAsset>, 26 | } 27 | 28 | impl RawYarnEntry { 29 | /// Is this entry a full release, i.e., does this entry's asset list include a 30 | /// proper release tarball? 31 | fn is_full_release(&self) -> bool { 32 | let release_filename = &format!("yarn-v{}.tar.gz", self.tag_name)[..]; 33 | self.assets 34 | .iter() 35 | .any(|raw_asset| raw_asset.name == release_filename) 36 | } 37 | } 38 | 39 | #[derive(Deserialize)] 40 | pub struct RawYarnAsset { 41 | /// The filename of an asset included in a Yarn GitHub release. 42 | pub name: String, 43 | } 44 | 45 | impl From<RawYarnIndex> for YarnIndex { 46 | fn from(raw: RawYarnIndex) -> YarnIndex { 47 | let mut entries = BTreeSet::new(); 48 | for entry in raw.0 { 49 | if entry.is_full_release() { 50 | entries.insert(entry.tag_name); 51 | } 52 | } 53 | YarnIndex { entries } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /crates/volta-core/src/tool/yarn/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | 3 | use super::{ 4 | check_fetched, check_shim_reachable, debug_already_fetched, info_fetched, info_installed, 5 | info_pinned, info_project_version, FetchStatus, Tool, 6 | }; 7 | use crate::error::{ErrorKind, Fallible}; 8 | use crate::inventory::yarn_available; 9 | use crate::session::Session; 10 | use crate::style::tool_version; 11 | use crate::sync::VoltaLock; 12 | use node_semver::Version; 13 | 14 | mod fetch; 15 | mod metadata; 16 | mod resolve; 17 | 18 | pub use resolve::resolve; 19 | 20 | /// The Tool implementation for fetching and installing Yarn 21 | pub struct Yarn { 22 | pub(super) version: Version, 23 | } 24 | 25 | impl Yarn { 26 | pub fn new(version: Version) -> Self { 27 | Yarn { version } 28 | } 29 | 30 | pub fn archive_basename(version: &str) -> String { 31 | format!("yarn-v{}", version) 32 | } 33 | 34 | pub fn archive_filename(version: &str) -> String { 35 | format!("{}.tar.gz", Yarn::archive_basename(version)) 36 | } 37 | 38 | pub(crate) fn ensure_fetched(&self, session: &mut Session) -> Fallible<()> { 39 | match check_fetched(|| yarn_available(&self.version))? { 40 | FetchStatus::AlreadyFetched => { 41 | debug_already_fetched(self); 42 | Ok(()) 43 | } 44 | FetchStatus::FetchNeeded(_lock) => fetch::fetch(&self.version, session.hooks()?.yarn()), 45 | } 46 | } 47 | } 48 | 49 | impl Tool for Yarn { 50 | fn fetch(self: Box<Self>, session: &mut Session) -> Fallible<()> { 51 | self.ensure_fetched(session)?; 52 | 53 | info_fetched(self); 54 | Ok(()) 55 | } 56 | fn install(self: Box<Self>, session: &mut Session) -> Fallible<()> { 57 | // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes 58 | let _lock = VoltaLock::acquire(); 59 | self.ensure_fetched(session)?; 60 | 61 | session 62 | .toolchain_mut()? 63 | .set_active_yarn(Some(self.version.clone()))?; 64 | 65 | info_installed(&self); 66 | check_shim_reachable("yarn"); 67 | 68 | if let Ok(Some(project)) = session.project_platform() { 69 | if let Some(yarn) = &project.yarn { 70 | info_project_version(tool_version("yarn", yarn), &self); 71 | } 72 | } 73 | Ok(()) 74 | } 75 | fn pin(self: Box<Self>, session: &mut Session) -> Fallible<()> { 76 | if session.project()?.is_some() { 77 | self.ensure_fetched(session)?; 78 | 79 | // Note: We know this will succeed, since we checked above 80 | let project = session.project_mut()?.unwrap(); 81 | project.pin_yarn(Some(self.version.clone()))?; 82 | 83 | info_pinned(self); 84 | Ok(()) 85 | } else { 86 | Err(ErrorKind::NotInPackage.into()) 87 | } 88 | } 89 | } 90 | 91 | impl Display for Yarn { 92 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 93 | f.write_str(&tool_version("yarn", &self.version)) 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::*; 100 | 101 | #[test] 102 | fn test_yarn_archive_basename() { 103 | assert_eq!(Yarn::archive_basename("1.2.3"), "yarn-v1.2.3"); 104 | } 105 | 106 | #[test] 107 | fn test_yarn_archive_filename() { 108 | assert_eq!(Yarn::archive_filename("1.2.3"), "yarn-v1.2.3.tar.gz"); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /crates/volta-core/src/version/serial.rs: -------------------------------------------------------------------------------- 1 | use node_semver::{Range, SemverError}; 2 | 3 | // NOTE: using `parse_compat` here because the semver crate defaults to 4 | // parsing in a cargo-compatible way. This is normally fine, except for 5 | // 2 cases (that I know about): 6 | // * "1.2.3" parses as `^1.2.3` for cargo, but `=1.2.3` for Node 7 | // * `>1.2.3 <2.0.0` serializes to ">1.2.3, <2.0.0" for cargo (with the 8 | // comma), but ">1.2.3 <2.0.0" for Node (no comma, because Node parses 9 | // commas differently) 10 | // 11 | // Because we are parsing the version requirements from the command line, 12 | // then serializing them to pass to `npm view`, they need to be handled in 13 | // a Node-compatible way (or we get the wrong version info returned). 14 | pub fn parse_requirements(src: &str) -> Result<Range, SemverError> { 15 | let src = src.trim().trim_start_matches('v'); 16 | 17 | Range::parse(src) 18 | } 19 | 20 | #[cfg(test)] 21 | pub mod tests { 22 | 23 | use crate::version::serial::parse_requirements; 24 | use node_semver::Range; 25 | 26 | #[test] 27 | fn test_parse_requirements() { 28 | assert_eq!( 29 | parse_requirements("1.2.3").unwrap(), 30 | Range::parse("=1.2.3").unwrap() 31 | ); 32 | assert_eq!( 33 | parse_requirements("v1.5").unwrap(), 34 | Range::parse("=1.5").unwrap() 35 | ); 36 | assert_eq!( 37 | parse_requirements("=1.2.3").unwrap(), 38 | Range::parse("=1.2.3").unwrap() 39 | ); 40 | assert_eq!( 41 | parse_requirements("^1.2").unwrap(), 42 | Range::parse("^1.2").unwrap() 43 | ); 44 | assert_eq!( 45 | parse_requirements(">=1.4").unwrap(), 46 | Range::parse(">=1.4").unwrap() 47 | ); 48 | assert_eq!( 49 | parse_requirements("8.11 - 8.17 || 10.* || >= 12").unwrap(), 50 | Range::parse("8.11 - 8.17 || 10.* || >= 12").unwrap() 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/volta-layout-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "volta-layout-macro" 3 | version = "0.1.0" 4 | authors = ["David Herman <david.herman@gmail.com>"] 5 | edition = "2021" 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | syn = "1.0.5" 12 | quote = "1.0.2" 13 | proc-macro2 = "1.0.2" 14 | -------------------------------------------------------------------------------- /crates/volta-layout-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "128"] 2 | 3 | extern crate proc_macro; 4 | 5 | mod ast; 6 | mod ir; 7 | 8 | use crate::ast::Ast; 9 | use proc_macro::TokenStream; 10 | use syn::parse_macro_input; 11 | 12 | /// A macro for defining Volta directory layout hierarchies. 13 | /// 14 | /// The syntax of `layout!` takes the form: 15 | /// 16 | /// ```text,no_run 17 | /// layout! { 18 | /// LayoutStruct* 19 | /// } 20 | /// ``` 21 | /// 22 | /// The syntax of a `LayoutStruct` takes the form: 23 | /// 24 | /// ```text,no_run 25 | /// Attribute* Visibility "struct" Ident Directory 26 | /// ``` 27 | /// 28 | /// The syntax of a `Directory` takes the form: 29 | /// 30 | /// ```text,no_run 31 | /// { 32 | /// (FieldPrefix)FieldContents* 33 | /// } 34 | /// ``` 35 | /// 36 | /// The syntax of a `FieldPrefix` takes the form: 37 | /// 38 | /// ```text,no_run 39 | /// LitStr ":" Ident 40 | /// ``` 41 | /// 42 | /// The syntax of a `FieldContents` is either: 43 | /// 44 | /// ```text,no_run 45 | /// ";" 46 | /// ``` 47 | /// 48 | /// or: 49 | /// 50 | /// ```text,no_run 51 | /// Directory 52 | /// ``` 53 | #[proc_macro] 54 | pub fn layout(input: TokenStream) -> TokenStream { 55 | let ast = parse_macro_input!(input as Ast); 56 | let expanded = ast.compile(); 57 | TokenStream::from(expanded) 58 | } 59 | -------------------------------------------------------------------------------- /crates/volta-layout/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "volta-layout" 3 | version = "0.1.1" 4 | authors = ["Chuck Pierce <cpierce.grad@gmail.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | volta-layout-macro = { path = "../volta-layout-macro" } 9 | -------------------------------------------------------------------------------- /crates/volta-layout/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod macros; 3 | 4 | pub mod v0; 5 | pub mod v1; 6 | pub mod v2; 7 | pub mod v3; 8 | pub mod v4; 9 | 10 | fn executable(name: &str) -> String { 11 | format!("{}{}", name, std::env::consts::EXE_SUFFIX) 12 | } 13 | -------------------------------------------------------------------------------- /crates/volta-layout/src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! path_buf { 2 | ($base:expr, $( $x:expr ), *) => { 3 | { 4 | let mut temp = $base; 5 | $( 6 | temp.push($x); 7 | )* 8 | temp 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /crates/volta-layout/src/v0.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use super::executable; 4 | use volta_layout_macro::layout; 5 | 6 | layout! { 7 | pub struct VoltaInstall { 8 | "shim[.exe]": shim_executable; 9 | } 10 | 11 | pub struct VoltaHome { 12 | "cache": cache_dir { 13 | "node": node_cache_dir { 14 | "index.json": node_index_file; 15 | "index.json.expires": node_index_expiry_file; 16 | } 17 | } 18 | "bin": shim_dir {} 19 | "log": log_dir {} 20 | "tools": tools_dir { 21 | "inventory": inventory_dir { 22 | "node": node_inventory_dir {} 23 | "packages": package_inventory_dir {} 24 | "yarn": yarn_inventory_dir {} 25 | } 26 | "image": image_dir { 27 | "node": node_image_root_dir {} 28 | "yarn": yarn_image_root_dir {} 29 | "packages": package_image_root_dir {} 30 | } 31 | "user": default_toolchain_dir { 32 | "bins": default_bin_dir {} 33 | "packages": default_package_dir {} 34 | "platform.json": default_platform_file; 35 | } 36 | } 37 | "tmp": tmp_dir {} 38 | "hooks.json": default_hooks_file; 39 | } 40 | } 41 | 42 | impl VoltaHome { 43 | pub fn package_distro_file(&self, name: &str, version: &str) -> PathBuf { 44 | path_buf!( 45 | self.package_inventory_dir.clone(), 46 | format!("{}-{}.tgz", name, version) 47 | ) 48 | } 49 | 50 | pub fn package_distro_shasum(&self, name: &str, version: &str) -> PathBuf { 51 | path_buf!( 52 | self.package_inventory_dir.clone(), 53 | format!("{}-{}.shasum", name, version) 54 | ) 55 | } 56 | 57 | pub fn node_image_dir(&self, node: &str, npm: &str) -> PathBuf { 58 | path_buf!(self.node_image_root_dir.clone(), node, npm) 59 | } 60 | 61 | pub fn yarn_image_dir(&self, version: &str) -> PathBuf { 62 | path_buf!(self.yarn_image_root_dir.clone(), version) 63 | } 64 | 65 | pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf { 66 | path_buf!(self.yarn_image_dir(version), "bin") 67 | } 68 | 69 | pub fn package_image_dir(&self, name: &str, version: &str) -> PathBuf { 70 | path_buf!(self.package_image_root_dir.clone(), name, version) 71 | } 72 | 73 | pub fn default_package_config_file(&self, package_name: &str) -> PathBuf { 74 | path_buf!( 75 | self.default_package_dir.clone(), 76 | format!("{}.json", package_name) 77 | ) 78 | } 79 | 80 | pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf { 81 | path_buf!(self.default_bin_dir.clone(), format!("{}.json", bin_name)) 82 | } 83 | 84 | pub fn node_npm_version_file(&self, version: &str) -> PathBuf { 85 | path_buf!( 86 | self.node_inventory_dir.clone(), 87 | format!("node-v{}-npm", version) 88 | ) 89 | } 90 | 91 | pub fn shim_file(&self, toolname: &str) -> PathBuf { 92 | path_buf!(self.shim_dir.clone(), executable(toolname)) 93 | } 94 | } 95 | 96 | #[cfg(windows)] 97 | impl VoltaHome { 98 | pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf { 99 | path_buf!(self.shim_dir.clone(), toolname) 100 | } 101 | 102 | pub fn node_image_bin_dir(&self, node: &str, npm: &str) -> PathBuf { 103 | self.node_image_dir(node, npm) 104 | } 105 | } 106 | 107 | #[cfg(windows)] 108 | impl VoltaInstall { 109 | pub fn bin_dir(&self) -> PathBuf { 110 | path_buf!(self.root.clone(), "bin") 111 | } 112 | } 113 | 114 | #[cfg(unix)] 115 | impl VoltaHome { 116 | pub fn node_image_bin_dir(&self, node: &str, npm: &str) -> PathBuf { 117 | path_buf!(self.node_image_dir(node, npm), "bin") 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /crates/volta-layout/src/v1.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use super::executable; 4 | use volta_layout_macro::layout; 5 | 6 | layout! { 7 | pub struct VoltaInstall { 8 | "volta-shim[.exe]": shim_executable; 9 | "volta[.exe]": main_executable; 10 | "volta-migrate[.exe]": migrate_executable; 11 | } 12 | 13 | pub struct VoltaHome { 14 | "cache": cache_dir { 15 | "node": node_cache_dir { 16 | "index.json": node_index_file; 17 | "index.json.expires": node_index_expiry_file; 18 | } 19 | } 20 | "bin": shim_dir {} 21 | "log": log_dir {} 22 | "tools": tools_dir { 23 | "inventory": inventory_dir { 24 | "node": node_inventory_dir {} 25 | "packages": package_inventory_dir {} 26 | "yarn": yarn_inventory_dir {} 27 | } 28 | "image": image_dir { 29 | "node": node_image_root_dir {} 30 | "yarn": yarn_image_root_dir {} 31 | "packages": package_image_root_dir {} 32 | } 33 | "user": default_toolchain_dir { 34 | "bins": default_bin_dir {} 35 | "packages": default_package_dir {} 36 | "platform.json": default_platform_file; 37 | } 38 | } 39 | "tmp": tmp_dir {} 40 | "hooks.json": default_hooks_file; 41 | "layout.v1": layout_file; 42 | } 43 | } 44 | 45 | impl VoltaHome { 46 | pub fn package_distro_file(&self, name: &str, version: &str) -> PathBuf { 47 | path_buf!( 48 | self.package_inventory_dir.clone(), 49 | format!("{}-{}.tgz", name, version) 50 | ) 51 | } 52 | 53 | pub fn package_distro_shasum(&self, name: &str, version: &str) -> PathBuf { 54 | path_buf!( 55 | self.package_inventory_dir.clone(), 56 | format!("{}-{}.shasum", name, version) 57 | ) 58 | } 59 | 60 | pub fn node_image_dir(&self, node: &str, npm: &str) -> PathBuf { 61 | path_buf!(self.node_image_root_dir.clone(), node, npm) 62 | } 63 | 64 | pub fn yarn_image_dir(&self, version: &str) -> PathBuf { 65 | path_buf!(self.yarn_image_root_dir.clone(), version) 66 | } 67 | 68 | pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf { 69 | path_buf!(self.yarn_image_dir(version), "bin") 70 | } 71 | 72 | pub fn package_image_dir(&self, name: &str, version: &str) -> PathBuf { 73 | path_buf!(self.package_image_root_dir.clone(), name, version) 74 | } 75 | 76 | pub fn default_package_config_file(&self, package_name: &str) -> PathBuf { 77 | path_buf!( 78 | self.default_package_dir.clone(), 79 | format!("{}.json", package_name) 80 | ) 81 | } 82 | 83 | pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf { 84 | path_buf!(self.default_bin_dir.clone(), format!("{}.json", bin_name)) 85 | } 86 | 87 | pub fn node_npm_version_file(&self, version: &str) -> PathBuf { 88 | path_buf!( 89 | self.node_inventory_dir.clone(), 90 | format!("node-v{}-npm", version) 91 | ) 92 | } 93 | 94 | pub fn shim_file(&self, toolname: &str) -> PathBuf { 95 | path_buf!(self.shim_dir.clone(), executable(toolname)) 96 | } 97 | } 98 | 99 | #[cfg(windows)] 100 | impl VoltaHome { 101 | pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf { 102 | path_buf!(self.shim_dir.clone(), toolname) 103 | } 104 | 105 | pub fn node_image_bin_dir(&self, node: &str, npm: &str) -> PathBuf { 106 | self.node_image_dir(node, npm) 107 | } 108 | } 109 | 110 | #[cfg(unix)] 111 | impl VoltaHome { 112 | pub fn node_image_bin_dir(&self, node: &str, npm: &str) -> PathBuf { 113 | path_buf!(self.node_image_dir(node, npm), "bin") 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /crates/volta-layout/src/v2.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use super::executable; 4 | use volta_layout_macro::layout; 5 | 6 | pub use crate::v1::VoltaInstall; 7 | 8 | layout! { 9 | pub struct VoltaHome { 10 | "cache": cache_dir { 11 | "node": node_cache_dir { 12 | "index.json": node_index_file; 13 | "index.json.expires": node_index_expiry_file; 14 | } 15 | } 16 | "bin": shim_dir {} 17 | "log": log_dir {} 18 | "tools": tools_dir { 19 | "inventory": inventory_dir { 20 | "node": node_inventory_dir {} 21 | "npm": npm_inventory_dir {} 22 | "packages": package_inventory_dir {} 23 | "yarn": yarn_inventory_dir {} 24 | } 25 | "image": image_dir { 26 | "node": node_image_root_dir {} 27 | "npm": npm_image_root_dir {} 28 | "yarn": yarn_image_root_dir {} 29 | "packages": package_image_root_dir {} 30 | } 31 | "user": default_toolchain_dir { 32 | "bins": default_bin_dir {} 33 | "packages": default_package_dir {} 34 | "platform.json": default_platform_file; 35 | } 36 | } 37 | "tmp": tmp_dir {} 38 | "hooks.json": default_hooks_file; 39 | "layout.v2": layout_file; 40 | } 41 | } 42 | 43 | impl VoltaHome { 44 | pub fn package_distro_file(&self, name: &str, version: &str) -> PathBuf { 45 | path_buf!( 46 | self.package_inventory_dir.clone(), 47 | format!("{}-{}.tgz", name, version) 48 | ) 49 | } 50 | 51 | pub fn package_distro_shasum(&self, name: &str, version: &str) -> PathBuf { 52 | path_buf!( 53 | self.package_inventory_dir.clone(), 54 | format!("{}-{}.shasum", name, version) 55 | ) 56 | } 57 | 58 | pub fn node_image_dir(&self, node: &str) -> PathBuf { 59 | path_buf!(self.node_image_root_dir.clone(), node) 60 | } 61 | 62 | pub fn npm_image_dir(&self, npm: &str) -> PathBuf { 63 | path_buf!(self.npm_image_root_dir.clone(), npm) 64 | } 65 | 66 | pub fn npm_image_bin_dir(&self, npm: &str) -> PathBuf { 67 | path_buf!(self.npm_image_dir(npm), "bin") 68 | } 69 | 70 | pub fn yarn_image_dir(&self, version: &str) -> PathBuf { 71 | path_buf!(self.yarn_image_root_dir.clone(), version) 72 | } 73 | 74 | pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf { 75 | path_buf!(self.yarn_image_dir(version), "bin") 76 | } 77 | 78 | pub fn package_image_dir(&self, name: &str, version: &str) -> PathBuf { 79 | path_buf!(self.package_image_root_dir.clone(), name, version) 80 | } 81 | 82 | pub fn default_package_config_file(&self, package_name: &str) -> PathBuf { 83 | path_buf!( 84 | self.default_package_dir.clone(), 85 | format!("{}.json", package_name) 86 | ) 87 | } 88 | 89 | pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf { 90 | path_buf!(self.default_bin_dir.clone(), format!("{}.json", bin_name)) 91 | } 92 | 93 | pub fn node_npm_version_file(&self, version: &str) -> PathBuf { 94 | path_buf!( 95 | self.node_inventory_dir.clone(), 96 | format!("node-v{}-npm", version) 97 | ) 98 | } 99 | 100 | pub fn shim_file(&self, toolname: &str) -> PathBuf { 101 | path_buf!(self.shim_dir.clone(), executable(toolname)) 102 | } 103 | } 104 | 105 | #[cfg(windows)] 106 | impl VoltaHome { 107 | pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf { 108 | path_buf!(self.shim_dir.clone(), toolname) 109 | } 110 | 111 | pub fn node_image_bin_dir(&self, node: &str) -> PathBuf { 112 | self.node_image_dir(node) 113 | } 114 | } 115 | 116 | #[cfg(unix)] 117 | impl VoltaHome { 118 | pub fn node_image_bin_dir(&self, node: &str) -> PathBuf { 119 | path_buf!(self.node_image_dir(node), "bin") 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /crates/volta-layout/src/v3.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use super::executable; 4 | use volta_layout_macro::layout; 5 | 6 | pub use crate::v1::VoltaInstall; 7 | 8 | layout! { 9 | pub struct VoltaHome { 10 | "cache": cache_dir { 11 | "node": node_cache_dir { 12 | "index.json": node_index_file; 13 | "index.json.expires": node_index_expiry_file; 14 | } 15 | } 16 | "bin": shim_dir {} 17 | "log": log_dir {} 18 | "tools": tools_dir { 19 | "inventory": inventory_dir { 20 | "node": node_inventory_dir {} 21 | "npm": npm_inventory_dir {} 22 | "pnpm": pnpm_inventory_dir {} 23 | "yarn": yarn_inventory_dir {} 24 | } 25 | "image": image_dir { 26 | "node": node_image_root_dir {} 27 | "npm": npm_image_root_dir {} 28 | "pnpm": pnpm_image_root_dir {} 29 | "yarn": yarn_image_root_dir {} 30 | "packages": package_image_root_dir {} 31 | } 32 | "shared": shared_lib_root {} 33 | "user": default_toolchain_dir { 34 | "bins": default_bin_dir {} 35 | "packages": default_package_dir {} 36 | "platform.json": default_platform_file; 37 | } 38 | } 39 | "tmp": tmp_dir {} 40 | "hooks.json": default_hooks_file; 41 | "layout.v3": layout_file; 42 | } 43 | } 44 | 45 | impl VoltaHome { 46 | pub fn node_image_dir(&self, node: &str) -> PathBuf { 47 | path_buf!(self.node_image_root_dir.clone(), node) 48 | } 49 | 50 | pub fn npm_image_dir(&self, npm: &str) -> PathBuf { 51 | path_buf!(self.npm_image_root_dir.clone(), npm) 52 | } 53 | 54 | pub fn npm_image_bin_dir(&self, npm: &str) -> PathBuf { 55 | path_buf!(self.npm_image_dir(npm), "bin") 56 | } 57 | 58 | pub fn pnpm_image_dir(&self, version: &str) -> PathBuf { 59 | path_buf!(self.pnpm_image_root_dir.clone(), version) 60 | } 61 | 62 | pub fn pnpm_image_bin_dir(&self, version: &str) -> PathBuf { 63 | path_buf!(self.pnpm_image_dir(version), "bin") 64 | } 65 | 66 | pub fn yarn_image_dir(&self, version: &str) -> PathBuf { 67 | path_buf!(self.yarn_image_root_dir.clone(), version) 68 | } 69 | 70 | pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf { 71 | path_buf!(self.yarn_image_dir(version), "bin") 72 | } 73 | 74 | pub fn package_image_dir(&self, name: &str) -> PathBuf { 75 | path_buf!(self.package_image_root_dir.clone(), name) 76 | } 77 | 78 | pub fn default_package_config_file(&self, package_name: &str) -> PathBuf { 79 | path_buf!( 80 | self.default_package_dir.clone(), 81 | format!("{}.json", package_name) 82 | ) 83 | } 84 | 85 | pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf { 86 | path_buf!(self.default_bin_dir.clone(), format!("{}.json", bin_name)) 87 | } 88 | 89 | pub fn node_npm_version_file(&self, version: &str) -> PathBuf { 90 | path_buf!( 91 | self.node_inventory_dir.clone(), 92 | format!("node-v{}-npm", version) 93 | ) 94 | } 95 | 96 | pub fn shim_file(&self, toolname: &str) -> PathBuf { 97 | path_buf!(self.shim_dir.clone(), executable(toolname)) 98 | } 99 | 100 | pub fn shared_lib_dir(&self, library: &str) -> PathBuf { 101 | path_buf!(self.shared_lib_root.clone(), library) 102 | } 103 | } 104 | 105 | #[cfg(windows)] 106 | impl VoltaHome { 107 | pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf { 108 | path_buf!(self.shim_dir.clone(), toolname) 109 | } 110 | 111 | pub fn node_image_bin_dir(&self, node: &str) -> PathBuf { 112 | self.node_image_dir(node) 113 | } 114 | } 115 | 116 | #[cfg(unix)] 117 | impl VoltaHome { 118 | pub fn node_image_bin_dir(&self, node: &str) -> PathBuf { 119 | path_buf!(self.node_image_dir(node), "bin") 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /crates/volta-layout/src/v4.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use volta_layout_macro::layout; 4 | 5 | pub use crate::v1::VoltaInstall; 6 | 7 | layout! { 8 | pub struct VoltaHome { 9 | "cache": cache_dir { 10 | "node": node_cache_dir { 11 | "index.json": node_index_file; 12 | "index.json.expires": node_index_expiry_file; 13 | } 14 | } 15 | "bin": shim_dir {} 16 | "log": log_dir {} 17 | "tools": tools_dir { 18 | "inventory": inventory_dir { 19 | "node": node_inventory_dir {} 20 | "npm": npm_inventory_dir {} 21 | "pnpm": pnpm_inventory_dir {} 22 | "yarn": yarn_inventory_dir {} 23 | } 24 | "image": image_dir { 25 | "node": node_image_root_dir {} 26 | "npm": npm_image_root_dir {} 27 | "pnpm": pnpm_image_root_dir {} 28 | "yarn": yarn_image_root_dir {} 29 | "packages": package_image_root_dir {} 30 | } 31 | "shared": shared_lib_root {} 32 | "user": default_toolchain_dir { 33 | "bins": default_bin_dir {} 34 | "packages": default_package_dir {} 35 | "platform.json": default_platform_file; 36 | } 37 | } 38 | "tmp": tmp_dir {} 39 | "hooks.json": default_hooks_file; 40 | "layout.v4": layout_file; 41 | } 42 | } 43 | 44 | impl VoltaHome { 45 | pub fn node_image_dir(&self, node: &str) -> PathBuf { 46 | path_buf!(self.node_image_root_dir.clone(), node) 47 | } 48 | 49 | pub fn npm_image_dir(&self, npm: &str) -> PathBuf { 50 | path_buf!(self.npm_image_root_dir.clone(), npm) 51 | } 52 | 53 | pub fn npm_image_bin_dir(&self, npm: &str) -> PathBuf { 54 | path_buf!(self.npm_image_dir(npm), "bin") 55 | } 56 | 57 | pub fn pnpm_image_dir(&self, version: &str) -> PathBuf { 58 | path_buf!(self.pnpm_image_root_dir.clone(), version) 59 | } 60 | 61 | pub fn pnpm_image_bin_dir(&self, version: &str) -> PathBuf { 62 | path_buf!(self.pnpm_image_dir(version), "bin") 63 | } 64 | 65 | pub fn yarn_image_dir(&self, version: &str) -> PathBuf { 66 | path_buf!(self.yarn_image_root_dir.clone(), version) 67 | } 68 | 69 | pub fn yarn_image_bin_dir(&self, version: &str) -> PathBuf { 70 | path_buf!(self.yarn_image_dir(version), "bin") 71 | } 72 | 73 | pub fn package_image_dir(&self, name: &str) -> PathBuf { 74 | path_buf!(self.package_image_root_dir.clone(), name) 75 | } 76 | 77 | pub fn default_package_config_file(&self, package_name: &str) -> PathBuf { 78 | path_buf!( 79 | self.default_package_dir.clone(), 80 | format!("{}.json", package_name) 81 | ) 82 | } 83 | 84 | pub fn default_tool_bin_config(&self, bin_name: &str) -> PathBuf { 85 | path_buf!(self.default_bin_dir.clone(), format!("{}.json", bin_name)) 86 | } 87 | 88 | pub fn node_npm_version_file(&self, version: &str) -> PathBuf { 89 | path_buf!( 90 | self.node_inventory_dir.clone(), 91 | format!("node-v{}-npm", version) 92 | ) 93 | } 94 | 95 | pub fn shim_file(&self, toolname: &str) -> PathBuf { 96 | // On Windows, shims are created as `<name>.cmd` since they 97 | // are thin scripts that use `volta run` to execute the command 98 | #[cfg(windows)] 99 | let toolname = format!("{}{}", toolname, ".cmd"); 100 | 101 | path_buf!(self.shim_dir.clone(), toolname) 102 | } 103 | 104 | pub fn shared_lib_dir(&self, library: &str) -> PathBuf { 105 | path_buf!(self.shared_lib_root.clone(), library) 106 | } 107 | } 108 | 109 | #[cfg(windows)] 110 | impl VoltaHome { 111 | pub fn shim_git_bash_script_file(&self, toolname: &str) -> PathBuf { 112 | path_buf!(self.shim_dir.clone(), toolname) 113 | } 114 | 115 | pub fn node_image_bin_dir(&self, node: &str) -> PathBuf { 116 | self.node_image_dir(node) 117 | } 118 | } 119 | 120 | #[cfg(unix)] 121 | impl VoltaHome { 122 | pub fn node_image_bin_dir(&self, node: &str) -> PathBuf { 123 | path_buf!(self.node_image_dir(node), "bin") 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /crates/volta-migrate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "volta-migrate" 3 | version = "0.1.0" 4 | authors = ["Charles Pierce <cpierce.grad@gmail.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | volta-core = { path = "../volta-core" } 9 | volta-layout = { path = "../volta-layout" } 10 | log = { version = "0.4", features = ["std"] } 11 | tempfile = "3.14.0" 12 | node-semver = "2" 13 | serde_json = { version = "1.0.135", features = ["preserve_order"] } 14 | serde = { version = "1.0.217", features = ["derive"] } 15 | walkdir = "2.5.0" 16 | -------------------------------------------------------------------------------- /crates/volta-migrate/src/empty.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | /// Represents an Empty (or uninitialized) Volta layout, one that has never been used by any prior version 4 | /// 5 | /// This is the easiest to migrate from, as we simply need to create the current layout within the .volta 6 | /// directory 7 | pub struct Empty { 8 | pub home: PathBuf, 9 | } 10 | 11 | impl Empty { 12 | pub fn new(home: PathBuf) -> Self { 13 | Empty { home } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /crates/volta-migrate/src/v0.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use volta_layout::v0::VoltaHome; 4 | 5 | /// Represents a V0 Volta layout (from before v0.7.0) 6 | /// 7 | /// This needs some migration work to move up to V1, so we keep a reference to the V0 layout 8 | /// struct to allow for easy comparison between versions 9 | pub struct V0 { 10 | pub home: VoltaHome, 11 | } 12 | 13 | impl V0 { 14 | pub fn new(home: PathBuf) -> Self { 15 | V0 { 16 | home: VoltaHome::new(home), 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/volta-migrate/src/v1.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | use std::fs::remove_file; 3 | use std::fs::File; 4 | use std::path::PathBuf; 5 | 6 | use super::empty::Empty; 7 | use super::v0::V0; 8 | use log::debug; 9 | use volta_core::error::{Context, ErrorKind, Fallible, VoltaError}; 10 | #[cfg(unix)] 11 | use volta_core::fs::{read_dir_eager, remove_file_if_exists}; 12 | use volta_layout::v1; 13 | 14 | /// Represents a V1 Volta Layout (used by Volta v0.7.0 - v0.7.2) 15 | /// 16 | /// Holds a reference to the V1 layout struct to support potential future migrations 17 | pub struct V1 { 18 | pub home: v1::VoltaHome, 19 | } 20 | 21 | impl V1 { 22 | pub fn new(home: PathBuf) -> Self { 23 | V1 { 24 | home: v1::VoltaHome::new(home), 25 | } 26 | } 27 | 28 | /// Write the layout file to mark migration to V1 as complete 29 | /// 30 | /// Should only be called once all other migration steps are finished, so that we don't 31 | /// accidentally mark an incomplete migration as completed 32 | fn complete_migration(home: v1::VoltaHome) -> Fallible<Self> { 33 | debug!("Writing layout marker file"); 34 | File::create(home.layout_file()).with_context(|| ErrorKind::CreateLayoutFileError { 35 | file: home.layout_file().to_owned(), 36 | })?; 37 | 38 | Ok(V1 { home }) 39 | } 40 | } 41 | 42 | impl TryFrom<Empty> for V1 { 43 | type Error = VoltaError; 44 | 45 | fn try_from(old: Empty) -> Fallible<V1> { 46 | debug!("New Volta installation detected, creating fresh layout"); 47 | 48 | let home = v1::VoltaHome::new(old.home); 49 | home.create().with_context(|| ErrorKind::CreateDirError { 50 | dir: home.root().to_owned(), 51 | })?; 52 | 53 | V1::complete_migration(home) 54 | } 55 | } 56 | 57 | impl TryFrom<V0> for V1 { 58 | type Error = VoltaError; 59 | 60 | fn try_from(old: V0) -> Fallible<V1> { 61 | debug!("Existing Volta installation detected, migrating from V0 layout"); 62 | 63 | let new_home = v1::VoltaHome::new(old.home.root().to_owned()); 64 | new_home 65 | .create() 66 | .with_context(|| ErrorKind::CreateDirError { 67 | dir: new_home.root().to_owned(), 68 | })?; 69 | 70 | #[cfg(unix)] 71 | { 72 | debug!("Removing unnecessary 'load.*' files"); 73 | let root_contents = 74 | read_dir_eager(new_home.root()).with_context(|| ErrorKind::ReadDirError { 75 | dir: new_home.root().to_owned(), 76 | })?; 77 | for (entry, _) in root_contents { 78 | let path = entry.path(); 79 | if let Some(stem) = path.file_stem() { 80 | if stem == "load" && path.is_file() { 81 | remove_file(&path) 82 | .with_context(|| ErrorKind::DeleteFileError { file: path })?; 83 | } 84 | } 85 | } 86 | 87 | debug!("Removing old Volta binaries"); 88 | 89 | let old_volta_bin = new_home.root().join("volta"); 90 | remove_file_if_exists(old_volta_bin)?; 91 | 92 | let old_shim_bin = new_home.root().join("shim"); 93 | remove_file_if_exists(old_shim_bin)?; 94 | } 95 | 96 | V1::complete_migration(new_home) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crates/volta-migrate/src/v3/config.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::path::Path; 3 | 4 | use node_semver::Version; 5 | use volta_core::platform::PlatformSpec; 6 | use volta_core::version::{option_version_serde, version_serde}; 7 | 8 | #[derive(serde::Deserialize)] 9 | pub struct LegacyPackageConfig { 10 | pub name: String, 11 | #[serde(with = "version_serde")] 12 | pub version: Version, 13 | pub platform: LegacyPlatform, 14 | pub bins: Vec<String>, 15 | } 16 | 17 | #[derive(serde::Deserialize)] 18 | pub struct LegacyPlatform { 19 | pub node: NodeVersion, 20 | #[serde(with = "option_version_serde")] 21 | pub yarn: Option<Version>, 22 | } 23 | 24 | #[derive(serde::Deserialize)] 25 | pub struct NodeVersion { 26 | #[serde(with = "version_serde")] 27 | pub runtime: Version, 28 | #[serde(with = "option_version_serde")] 29 | pub npm: Option<Version>, 30 | } 31 | 32 | impl LegacyPackageConfig { 33 | pub fn from_file(config_file: &Path) -> Option<Self> { 34 | let file = File::open(config_file).ok()?; 35 | 36 | serde_json::from_reader(file).ok() 37 | } 38 | } 39 | 40 | impl From<LegacyPlatform> for PlatformSpec { 41 | fn from(config_platform: LegacyPlatform) -> Self { 42 | PlatformSpec { 43 | node: config_platform.node.runtime, 44 | npm: config_platform.node.npm, 45 | // LegacyPlatform (layout.v2) doesn't have a pnpm field 46 | pnpm: None, 47 | yarn: config_platform.yarn, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0", 4 | "description": "an example Node project using volta", 5 | "author": "Dave Herman <david.herman@gmail.com>", 6 | "main": "lib/index.js", 7 | "devDependencies": { 8 | "ember-cli": "2.18.1" 9 | }, 10 | "volta": { 11 | "node": "6.11.1", 12 | "yarn": "1.7.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /dev/rpm/build-rpm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Build an RPM package for Volta 3 | 4 | # using the directions from https://rpm-packaging-guide.github.io/ 5 | 6 | # exit on error 7 | set -e 8 | 9 | # only argument is the version number 10 | release_version="${1:?Must specify the release version, like \`build-rpm 1.2.3\`}" 11 | archive_filename="v${release_version}.tar.gz" 12 | 13 | # make sure these packages are installed 14 | # (https://rpm-packaging-guide.github.io/#prerequisites) 15 | sudo yum install gcc rpm-build rpm-devel rpmlint make python bash coreutils diffutils patch rpmdevtools 16 | 17 | # set up the directory layout for the RPM packaging workspace 18 | # (https://rpm-packaging-guide.github.io/#rpm-packaging-workspace) 19 | rpmdev-setuptree 20 | 21 | # create a tarball of the repo for the specified version 22 | # using prefix because the rpmbuild process expects a 'volta-<version>' directory 23 | # (https://rpm-packaging-guide.github.io/#putting-source-code-into-tarball) 24 | git archive --format=tar.gz --output=$archive_filename --prefix="volta-${release_version}/" HEAD 25 | 26 | # move the archive to the SOURCES dir, after cleaning it up 27 | # (https://rpm-packaging-guide.github.io/#working-with-spec-files) 28 | rm -rf "$HOME/rmpbuild/SOURCES/"* 29 | mv "$archive_filename" "$HOME/rpmbuild/SOURCES/" 30 | 31 | # copy the .spec file to SPECS dir 32 | cp dev/rpm/volta.spec "$HOME/rpmbuild/SPECS/" 33 | 34 | # build it! 35 | # (https://rpm-packaging-guide.github.io/#binary-rpms) 36 | rpmbuild -bb "$HOME/rpmbuild/SPECS/volta.spec" 37 | # (there will be a lot of output) 38 | 39 | # then install it and verify everything worked... 40 | echo "" 41 | echo "Build finished!" 42 | echo "" 43 | echo "Run this to install:" 44 | echo " \`sudo yum install ~/rpmbuild/RPMS/x86_64/volta-${release_version}-1.el7.x86_64.rpm\`" 45 | echo "" 46 | echo "Then run this to uninstall after verifying:" 47 | echo " \`sudo yum erase volta-${release_version}-1.el7.x86_64\`" 48 | -------------------------------------------------------------------------------- /dev/rpm/volta.spec: -------------------------------------------------------------------------------- 1 | Name: volta 2 | Version: 0.8.2 3 | Release: 1%{?dist} 4 | Summary: The JavaScript Launcher ⚡ 5 | 6 | License: BSD 2-CLAUSE 7 | URL: https://%{name}.sh 8 | Source0: https://github.com/volta-cli/volta/archive/v%{version}.tar.gz 9 | 10 | # cargo is required, but installing from RPM is failing with libcrypto dep error 11 | # so you will have to install cargo manually to build this 12 | #BuildRequires: cargo 13 | 14 | # because these are built with openssl 15 | Requires: openssl 16 | 17 | 18 | %description 19 | Volta’s job is to manage your JavaScript command-line tools, such as node, npm, yarn, or executables shipped as part of JavaScript packages. Similar to package managers, Volta keeps track of which project (if any) you’re working on based on your current directory. The tools in your Volta toolchain automatically detect when you’re in a project that’s using a particular version of the tools, and take care of routing to the right version of the tools for you. 20 | 21 | 22 | %prep 23 | # this unpacks the tarball to the build root 24 | %setup -q 25 | 26 | 27 | %build 28 | # build the release binaries 29 | # NOTE: build expects to `cd` into a volta-<version> directory 30 | cargo build --release 31 | 32 | 33 | # this installs into a chroot directory resembling the user's root directory 34 | %install 35 | # BUILDROOT/usr/bin 36 | %define volta_install_dir %{buildroot}/%{_bindir} 37 | # setup the /usr/bin/volta-lib/ directory 38 | rm -rf %{buildroot} 39 | mkdir -p %{volta_install_dir} 40 | # install everything into into /usr/bin/, so it's on the PATH 41 | install -m 0755 target/release/%{name} %{volta_install_dir}/%{name} 42 | install -m 0755 target/release/volta-shim %{volta_install_dir}/volta-shim 43 | install -m 0755 target/release/volta-migrate %{volta_install_dir}/volta-migrate 44 | 45 | 46 | # files installed by this package 47 | %files 48 | %license LICENSE 49 | %{_bindir}/%{name} 50 | %{_bindir}/volta-shim 51 | %{_bindir}/volta-migrate 52 | 53 | 54 | # this runs before install 55 | %pre 56 | # make sure the /usr/bin/volta/ dir does not exist, from prev RPM installs (or this will fail) 57 | printf '\033[1;32m%12s\033[0m %s\n' "Running" "Volta pre-install..." 1>&2 58 | rm -rf %{_bindir}/%{name} 59 | 60 | 61 | # this runs after install, and sets up VOLTA_HOME and the shell integration 62 | %post 63 | printf '\033[1;32m%12s\033[0m %s\n' "Running" "Volta post-install setup..." 1>&2 64 | # run this as the user who invoked sudo (not as root, because we're writing to $HOME) 65 | /bin/su -c "%{_bindir}/volta setup" - $SUDO_USER 66 | 67 | 68 | %changelog 69 | * Tue Oct 22 2019 Charles Pierce <cpierce.grad@gmail.com> - 0.6.5-1 70 | - Update to use 'volta setup' as the postinstall script 71 | * Mon Jun 03 2019 Michael Stewart <mikrostew@gmail.com> - 0.5.3-1 72 | - First volta package 73 | -------------------------------------------------------------------------------- /dev/unix/SHASUMS256.txt: -------------------------------------------------------------------------------- 1 | fbdc4b8cb33fb6d19e5f07b22423265943d34e7e5c3d5a1efcecc9621854f9cb volta-install.sh 2 | -------------------------------------------------------------------------------- /dev/unix/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | script_dir="$(dirname "$0")" 4 | 5 | usage() { 6 | cat <<END_USAGE 7 | build.sh: generate volta's generic unix installation script 8 | 9 | usage: build.sh [target] 10 | [target] build artifacts to use ('release' or 'debug', defaults to 'release') 11 | 12 | The output file is saved as $script_dir/install.sh. 13 | END_USAGE 14 | } 15 | 16 | if [ -z "$1" ]; then 17 | target_dir='release' 18 | elif [[ "$1" =~ (debug|release) ]]; then 19 | target_dir="$1" 20 | else 21 | usage 22 | exit 1 23 | fi 24 | 25 | encode_base64_sed_command() { 26 | command printf "s|<PLACEHOLDER_$2_PAYLOAD>|" > $1.base64.txt 27 | cat $3 | base64 - | tr -d '\n' >> $1.base64.txt 28 | command printf "|\n" >> $1.base64.txt 29 | } 30 | 31 | encode_expand_sed_command() { 32 | # This atrocity is a combination of: 33 | # - https://unix.stackexchange.com/questions/141387/sed-replace-string-with-file-contents 34 | # - https://serverfault.com/questions/391360/remove-line-break-using-awk 35 | # - https://stackoverflow.com/questions/1421478/how-do-i-use-a-new-line-replacement-in-a-bsd-sed 36 | command printf "s|<PLACEHOLDER_$2_PAYLOAD>|$(sed 's/|/\\|/g' $3 | awk '{printf "%s\\\n",$0} END {print ""}' )\\\n|\n" > $1.expand.txt 37 | } 38 | 39 | build_dir="$script_dir/../../target/$target_dir" 40 | shell_dir="$script_dir/../../shell" 41 | 42 | encode_base64_sed_command volta VOLTA "$build_dir/volta" 43 | encode_base64_sed_command shim SHIM "$build_dir/shim" 44 | encode_expand_sed_command bash_launcher BASH_LAUNCHER "$shell_dir/unix/load.sh" 45 | encode_expand_sed_command fish_launcher FISH_LAUNCHER "$shell_dir/unix/load.fish" 46 | 47 | sed -f volta.base64.txt \ 48 | -f shim.base64.txt \ 49 | -f bash_launcher.expand.txt \ 50 | -f fish_launcher.expand.txt \ 51 | < "$script_dir/install.sh.in" > "$script_dir/install.sh" 52 | 53 | chmod 755 "$script_dir/install.sh" 54 | 55 | rm volta.base64.txt \ 56 | shim.base64.txt \ 57 | bash_launcher.expand.txt \ 58 | fish_launcher.expand.txt 59 | -------------------------------------------------------------------------------- /dev/unix/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Script to build the binaries and package them up for release. 4 | # This should be run from the top-level directory. 5 | 6 | # get the directory of this script 7 | # (from https://stackoverflow.com/a/246128) 8 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 9 | 10 | # get shared functions from the volta-install.sh file 11 | source "$DIR/volta-install.sh" 12 | 13 | usage() { 14 | cat >&2 <<END_OF_USAGE 15 | release.sh 16 | 17 | Compile and package a release for Volta 18 | 19 | USAGE: 20 | ./dev/unix/release.sh [FLAGS] [OPTIONS] 21 | 22 | FLAGS: 23 | -h, --help Prints this help info 24 | 25 | OPTIONS: 26 | --release Build artifacts in release mode, with optimizations (default) 27 | --dev Build artifacts in dev mode, without optimizations 28 | END_OF_USAGE 29 | } 30 | 31 | 32 | # default to compiling with '--release' 33 | build_with_release="true" 34 | 35 | # parse input arguments 36 | case "$1" in 37 | -h|--help) 38 | usage 39 | exit 0 40 | ;; 41 | --dev) 42 | build_with_release="false" 43 | ;; 44 | ''|--release) 45 | # not really necessary to set this again 46 | build_with_release="true" 47 | ;; 48 | *) 49 | error "Unknown argument '$1'" 50 | usage 51 | exit1 52 | ;; 53 | esac 54 | 55 | # read the current version from Cargo.toml 56 | cargo_toml_contents="$(<Cargo.toml)" 57 | VOLTA_VERSION="$(parse_cargo_version "$cargo_toml_contents")" || exit 1 58 | 59 | # figure out the OS details 60 | os="$(uname -s)" 61 | openssl_version="$(openssl version)" || exit 1 62 | VOLTA_OS="$(parse_os_info "$os" "$openssl_version")" 63 | if [ "$?" != 0 ]; then 64 | error "Releases for '$os' are not yet supported." 65 | request "To support '$os', add another case to parse_os_info() in volta-install.sh." 66 | exit 1 67 | fi 68 | 69 | release_filename="volta-$VOLTA_VERSION-$VOLTA_OS" 70 | 71 | # first make sure the release binaries have been built 72 | info 'Building' "Volta for $(bold "$release_filename")" 73 | if [ "$build_with_release" == "true" ] 74 | then 75 | target_dir="target/release" 76 | cargo build --release 77 | else 78 | target_dir="target/debug" 79 | cargo build 80 | fi || exit 1 81 | 82 | # then package the binaries and shell scripts together 83 | info 'Packaging' "the compiled binaries" 84 | cd "$target_dir" 85 | # using COPYFILE_DISABLE to avoid storing extended attribute files when run on OSX 86 | # (see https://superuser.com/q/61185) 87 | COPYFILE_DISABLE=1 tar -czvf "$release_filename.tar.gz" volta volta-shim volta-migrate 88 | 89 | info 'Completed' "release in file $target_dir/$release_filename.tar.gz" 90 | -------------------------------------------------------------------------------- /dev/unix/test-events: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # long-running script to show events and test spawning processes 4 | 5 | my_pid="$" 6 | 7 | echo "$my_pid called with $# args: $@" 8 | 9 | # get JSON data from stdin 10 | read some_data 11 | 12 | echo "$my_pid got:" 13 | # display nicely with jq if it is installed 14 | command -v jq >/dev/null 2>&1 && echo "$some_data" | jq '.' || echo "$some_data" 15 | 16 | i=0 17 | while [ $i -lt 3 ] 18 | do 19 | sleep 2s 20 | echo "$my_pid still running!" 21 | let i=i+1 22 | done 23 | 24 | echo "$my_pid done!!" 25 | 26 | -------------------------------------------------------------------------------- /dev/unix/tests/install-script.bats: -------------------------------------------------------------------------------- 1 | # test the volta-install.sh script 2 | 3 | # load the functions from the script 4 | source dev/unix/volta-install.sh 5 | 6 | 7 | # happy path test to parse the version from Cargo.toml 8 | @test "parse_cargo_version - normal Cargo.toml" { 9 | input=$(cat <<'END_CARGO_TOML' 10 | [package] 11 | name = "volta" 12 | version = "0.7.38" 13 | authors = ["David Herman <david.herman@gmail.com>"] 14 | license = "BSD-2-Clause" 15 | END_CARGO_TOML 16 | ) 17 | 18 | expected_output="0.7.38" 19 | 20 | run parse_cargo_version "$input" 21 | [ "$status" -eq 0 ] 22 | diff <(echo "$output") <(echo "$expected_output") 23 | } 24 | 25 | # it doesn't parse the version from other dependencies 26 | @test "parse_cargo_version - error" { 27 | input=$(cat <<'END_CARGO_TOML' 28 | [dependencies] 29 | volta-core = { path = "crates/volta-core" } 30 | serde = { version = "1.0", features = ["derive"] } 31 | serde_json = "1.0.37" 32 | console = "0.6.1" 33 | END_CARGO_TOML 34 | ) 35 | 36 | expected_output=$(echo -e "\033[1;31mError\033[0m: Could not determine the current version from Cargo.toml") 37 | 38 | run parse_cargo_version "$input" 39 | [ "$status" -eq 1 ] 40 | diff <(echo "$output") <(echo "$expected_output") 41 | } 42 | 43 | # linux 44 | @test "parse_os_info - linux" { 45 | expected_output="linux" 46 | 47 | run parse_os_info "Linux" 48 | [ "$status" -eq 0 ] 49 | diff <(echo "$output") <(echo "$expected_output") 50 | } 51 | 52 | # macos 53 | @test "parse_os_info - macos" { 54 | expected_output="macos" 55 | 56 | run parse_os_info "Darwin" 57 | [ "$status" -eq 0 ] 58 | diff <(echo "$output") <(echo "$expected_output") 59 | } 60 | 61 | # unsupported OS 62 | @test "parse_os_info - unsupported OS" { 63 | expected_output="" 64 | 65 | run parse_os_info "DOS" 66 | [ "$status" -eq 1 ] 67 | diff <(echo "$output") <(echo "$expected_output") 68 | } 69 | 70 | # test element_in helper function 71 | @test "element_in works correctly" { 72 | run element_in "foo" "foo" "bar" "baz" 73 | [ "$status" -eq 0 ] 74 | 75 | array=( "foo" "bar" "baz" ) 76 | run element_in "foo" "${array[@]}" 77 | [ "$status" -eq 0 ] 78 | run element_in "bar" "${array[@]}" 79 | [ "$status" -eq 0 ] 80 | run element_in "baz" "${array[@]}" 81 | [ "$status" -eq 0 ] 82 | 83 | run element_in "fob" "${array[@]}" 84 | [ "$status" -eq 1 ] 85 | } 86 | 87 | 88 | # test VOLTA_HOME settings 89 | 90 | @test "volta_home_is_ok - true cases" { 91 | # unset is fine 92 | unset VOLTA_HOME 93 | run volta_home_is_ok 94 | [ "$status" -eq 0 ] 95 | 96 | # empty is fine 97 | VOLTA_HOME="" 98 | run volta_home_is_ok 99 | [ "$status" -eq 0 ] 100 | 101 | # non-existing dir is fine 102 | VOLTA_HOME="/some/dir/that/does/not/exist/anywhere" 103 | run volta_home_is_ok 104 | [ "$status" -eq 0 ] 105 | 106 | # existing dir is fine 107 | VOLTA_HOME="$HOME" 108 | run volta_home_is_ok 109 | [ "$status" -eq 0 ] 110 | } 111 | 112 | @test "volta_home_is_ok - not ok" { 113 | # file is not ok 114 | VOLTA_HOME="$(mktemp)" 115 | run volta_home_is_ok 116 | [ "$status" -eq 1 ] 117 | } 118 | 119 | # TODO: test creating symlinks 120 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.75" 3 | components = ["clippy", "rustfmt"] 4 | profile = "minimal" 5 | -------------------------------------------------------------------------------- /src/command/completions.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::CommandFactory; 4 | use clap_complete::Shell; 5 | use log::info; 6 | 7 | use volta_core::{ 8 | error::{Context, ErrorKind, ExitCode, Fallible}, 9 | session::{ActivityKind, Session}, 10 | style::{note_prefix, success_prefix}, 11 | }; 12 | 13 | use crate::command::Command; 14 | 15 | #[derive(Debug, clap::Args)] 16 | pub(crate) struct Completions { 17 | /// Shell to generate completions for 18 | #[arg(index = 1, ignore_case = true, required = true)] 19 | shell: Shell, 20 | 21 | /// File to write generated completions to 22 | #[arg(short, long = "output")] 23 | out_file: Option<PathBuf>, 24 | 25 | /// Write over an existing file, if any. 26 | #[arg(short, long)] 27 | force: bool, 28 | } 29 | 30 | impl Command for Completions { 31 | fn run(self, session: &mut Session) -> Fallible<ExitCode> { 32 | session.add_event_start(ActivityKind::Completions); 33 | 34 | let mut app = crate::cli::Volta::command(); 35 | let app_name = app.get_name().to_owned(); 36 | match self.out_file { 37 | Some(path) => { 38 | if path.is_file() && !self.force { 39 | return Err(ErrorKind::CompletionsOutFileError { path }.into()); 40 | } 41 | 42 | // The user may have passed a path that does not yet exist. If 43 | // so, we create it, informing the user we have done so. 44 | if let Some(parent) = path.parent() { 45 | if !parent.is_dir() { 46 | info!( 47 | "{} {} does not exist, creating it", 48 | note_prefix(), 49 | parent.display() 50 | ); 51 | std::fs::create_dir_all(parent).with_context(|| { 52 | ErrorKind::CreateDirError { 53 | dir: parent.to_path_buf(), 54 | } 55 | })?; 56 | } 57 | } 58 | 59 | let mut file = &std::fs::File::create(&path).with_context(|| { 60 | ErrorKind::CompletionsOutFileError { 61 | path: path.to_path_buf(), 62 | } 63 | })?; 64 | 65 | clap_complete::generate(self.shell, &mut app, app_name, &mut file); 66 | 67 | info!( 68 | "{} installed completions to {}", 69 | success_prefix(), 70 | path.display() 71 | ); 72 | } 73 | None => clap_complete::generate(self.shell, &mut app, app_name, &mut std::io::stdout()), 74 | }; 75 | 76 | session.add_event_end(ActivityKind::Completions, ExitCode::Success); 77 | Ok(ExitCode::Success) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/command/fetch.rs: -------------------------------------------------------------------------------- 1 | use volta_core::error::{ExitCode, Fallible}; 2 | use volta_core::session::{ActivityKind, Session}; 3 | use volta_core::tool; 4 | 5 | use crate::command::Command; 6 | 7 | #[derive(clap::Args)] 8 | pub(crate) struct Fetch { 9 | /// Tools to fetch, like `node`, `yarn@latest` or `your-package@^14.4.3`. 10 | #[arg(value_name = "tool[@version]", required = true)] 11 | tools: Vec<String>, 12 | } 13 | 14 | impl Command for Fetch { 15 | fn run(self, session: &mut Session) -> Fallible<ExitCode> { 16 | session.add_event_start(ActivityKind::Fetch); 17 | 18 | for tool in tool::Spec::from_strings(&self.tools, "fetch")? { 19 | tool.resolve(session)?.fetch(session)?; 20 | } 21 | 22 | session.add_event_end(ActivityKind::Fetch, ExitCode::Success); 23 | Ok(ExitCode::Success) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/command/install.rs: -------------------------------------------------------------------------------- 1 | use volta_core::error::{ExitCode, Fallible}; 2 | use volta_core::session::{ActivityKind, Session}; 3 | use volta_core::tool::Spec; 4 | 5 | use crate::command::Command; 6 | 7 | #[derive(clap::Args)] 8 | pub(crate) struct Install { 9 | /// Tools to install, like `node`, `yarn@latest` or `your-package@^14.4.3`. 10 | #[arg(value_name = "tool[@version]", required = true)] 11 | tools: Vec<String>, 12 | } 13 | 14 | impl Command for Install { 15 | fn run(self, session: &mut Session) -> Fallible<ExitCode> { 16 | session.add_event_start(ActivityKind::Install); 17 | 18 | for tool in Spec::from_strings(&self.tools, "install")? { 19 | tool.resolve(session)?.install(session)?; 20 | } 21 | 22 | session.add_event_end(ActivityKind::Install, ExitCode::Success); 23 | Ok(ExitCode::Success) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/command/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod completions; 2 | pub(crate) mod fetch; 3 | pub(crate) mod install; 4 | pub(crate) mod list; 5 | pub(crate) mod pin; 6 | pub(crate) mod run; 7 | pub(crate) mod setup; 8 | pub(crate) mod uninstall; 9 | pub(crate) mod r#use; 10 | pub(crate) mod which; 11 | 12 | pub(crate) use self::which::Which; 13 | pub(crate) use completions::Completions; 14 | pub(crate) use fetch::Fetch; 15 | pub(crate) use install::Install; 16 | pub(crate) use list::List; 17 | pub(crate) use pin::Pin; 18 | pub(crate) use r#use::Use; 19 | pub(crate) use run::Run; 20 | pub(crate) use setup::Setup; 21 | pub(crate) use uninstall::Uninstall; 22 | 23 | use volta_core::error::{ExitCode, Fallible}; 24 | use volta_core::session::Session; 25 | 26 | /// A Volta command. 27 | pub(crate) trait Command: Sized { 28 | /// Executes the command. Returns `Ok(true)` if the process should return 0, 29 | /// `Ok(false)` if the process should return 1, and `Err(e)` if the process 30 | /// should return `e.exit_code()`. 31 | fn run(self, session: &mut Session) -> Fallible<ExitCode>; 32 | } 33 | -------------------------------------------------------------------------------- /src/command/pin.rs: -------------------------------------------------------------------------------- 1 | use volta_core::error::{ExitCode, Fallible}; 2 | use volta_core::session::{ActivityKind, Session}; 3 | use volta_core::tool::Spec; 4 | 5 | use crate::command::Command; 6 | 7 | #[derive(clap::Args)] 8 | pub(crate) struct Pin { 9 | /// Tools to pin, like `node@lts` or `yarn@^1.14`. 10 | #[arg(value_name = "tool[@version]", required = true)] 11 | tools: Vec<String>, 12 | } 13 | 14 | impl Command for Pin { 15 | fn run(self, session: &mut Session) -> Fallible<ExitCode> { 16 | session.add_event_start(ActivityKind::Pin); 17 | 18 | for tool in Spec::from_strings(&self.tools, "pin")? { 19 | tool.resolve(session)?.pin(session)?; 20 | } 21 | 22 | session.add_event_end(ActivityKind::Pin, ExitCode::Success); 23 | Ok(ExitCode::Success) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/command/uninstall.rs: -------------------------------------------------------------------------------- 1 | use volta_core::error::{ErrorKind, ExitCode, Fallible}; 2 | use volta_core::session::{ActivityKind, Session}; 3 | use volta_core::tool; 4 | use volta_core::version::VersionSpec; 5 | 6 | use crate::command::Command; 7 | 8 | #[derive(clap::Args)] 9 | pub(crate) struct Uninstall { 10 | /// The tool to uninstall, like `ember-cli-update`, `typescript`, or <package> 11 | tool: String, 12 | } 13 | 14 | impl Command for Uninstall { 15 | fn run(self, session: &mut Session) -> Fallible<ExitCode> { 16 | session.add_event_start(ActivityKind::Uninstall); 17 | 18 | let tool = tool::Spec::try_from_str(&self.tool)?; 19 | 20 | // For packages, specifically report that we do not support uninstalling 21 | // specific versions. For runtimes and package managers, we currently 22 | // *intentionally* let this fall through to inform the user that we do 23 | // not support uninstalling those *at all*. 24 | if let tool::Spec::Package(_name, version) = &tool { 25 | let VersionSpec::None = version else { 26 | return Err(ErrorKind::Unimplemented { 27 | feature: "uninstalling specific versions of tools".into(), 28 | } 29 | .into()); 30 | }; 31 | } 32 | 33 | tool.uninstall()?; 34 | 35 | session.add_event_end(ActivityKind::Uninstall, ExitCode::Success); 36 | Ok(ExitCode::Success) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/command/use.rs: -------------------------------------------------------------------------------- 1 | use crate::command::Command; 2 | use volta_core::error::{ErrorKind, ExitCode, Fallible}; 3 | use volta_core::session::{ActivityKind, Session}; 4 | 5 | // NOTE: These use the same text as the `long_about` in crate::cli. 6 | // It's hard to abstract since it's in an attribute string. 7 | 8 | pub(crate) const USAGE: &str = "The subcommand `use` is deprecated. 9 | 10 | To install a tool in your toolchain, use `volta install`. 11 | To pin your project's runtime or package manager, use `volta pin`. 12 | "; 13 | 14 | const ADVICE: &str = " 15 | To install a tool in your toolchain, use `volta install`. 16 | To pin your project's runtime or package manager, use `volta pin`. 17 | "; 18 | 19 | #[derive(clap::Args)] 20 | pub(crate) struct Use { 21 | #[allow(dead_code)] 22 | anything: Vec<String>, // Prevent Clap argument errors when invoking e.g. `volta use node` 23 | } 24 | 25 | impl Command for Use { 26 | fn run(self, session: &mut Session) -> Fallible<ExitCode> { 27 | session.add_event_start(ActivityKind::Help); 28 | let result = Err(ErrorKind::DeprecatedCommandError { 29 | command: "use".to_string(), 30 | advice: ADVICE.to_string(), 31 | } 32 | .into()); 33 | session.add_event_end(ActivityKind::Help, ExitCode::InvalidArguments); 34 | result 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/command/which.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::ffi::OsString; 3 | 4 | use which::which_in; 5 | 6 | use volta_core::error::{Context, ErrorKind, ExitCode, Fallible}; 7 | use volta_core::platform::{Platform, System}; 8 | use volta_core::run::binary::DefaultBinary; 9 | use volta_core::session::{ActivityKind, Session}; 10 | 11 | use crate::command::Command; 12 | 13 | #[derive(clap::Args)] 14 | pub(crate) struct Which { 15 | /// The binary to find, e.g. `node` or `npm` 16 | binary: OsString, 17 | } 18 | 19 | impl Command for Which { 20 | // 1. Start by checking if the user has a tool installed in the project or 21 | // as a user default. If so, we're done. 22 | // 2. Otherwise, use the platform image and/or the system environment to 23 | // determine a lookup path to run `which` in. 24 | fn run(self, session: &mut Session) -> Fallible<ExitCode> { 25 | session.add_event_start(ActivityKind::Which); 26 | 27 | let default_tool = DefaultBinary::from_name(&self.binary, session)?; 28 | let project_bin_path = session 29 | .project()? 30 | .and_then(|project| project.find_bin(&self.binary)); 31 | 32 | let tool_path = match (default_tool, project_bin_path) { 33 | (Some(_), Some(bin_path)) => Some(bin_path), 34 | (Some(tool), _) => Some(tool.bin_path), 35 | _ => None, 36 | }; 37 | 38 | if let Some(path) = tool_path { 39 | println!("{}", path.to_string_lossy()); 40 | 41 | let exit_code = ExitCode::Success; 42 | session.add_event_end(ActivityKind::Which, exit_code); 43 | return Ok(exit_code); 44 | } 45 | 46 | // Treat any error with obtaining the current platform image as if the image doesn't exist 47 | // However, errors in obtaining the current working directory or the System path should 48 | // still be treated as errors. 49 | let path = match Platform::current(session) 50 | .unwrap_or(None) 51 | .and_then(|platform| platform.checkout(session).ok()) 52 | .and_then(|image| image.path().ok()) 53 | { 54 | Some(path) => path, 55 | None => System::path()?, 56 | }; 57 | 58 | let cwd = env::current_dir().with_context(|| ErrorKind::CurrentDirError)?; 59 | let exit_code = match which_in(&self.binary, Some(path), cwd) { 60 | Ok(result) => { 61 | println!("{}", result.to_string_lossy()); 62 | ExitCode::Success 63 | } 64 | Err(_) => { 65 | // `which_in` Will return an Err if it can't find the binary in the path 66 | // In that case, we don't want to print anything out, but we want to return 67 | // Exit Code 1 (ExitCode::UnknownError) 68 | ExitCode::UnknownError 69 | } 70 | }; 71 | 72 | session.add_event_end(ActivityKind::Which, exit_code); 73 | Ok(exit_code) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | use std::process::{Command, ExitStatus}; 2 | 3 | use volta_core::error::{Context, ErrorKind, VoltaError}; 4 | use volta_core::layout::{volta_home, volta_install}; 5 | 6 | pub enum Error { 7 | Volta(VoltaError), 8 | Tool(i32), 9 | } 10 | 11 | pub fn ensure_layout() -> Result<(), Error> { 12 | let home = volta_home().map_err(Error::Volta)?; 13 | 14 | if !home.layout_file().exists() { 15 | let install = volta_install().map_err(Error::Volta)?; 16 | Command::new(install.migrate_executable()) 17 | .env("VOLTA_LOGLEVEL", format!("{}", log::max_level())) 18 | .status() 19 | .with_context(|| ErrorKind::CouldNotStartMigration) 20 | .into_result()?; 21 | } 22 | 23 | Ok(()) 24 | } 25 | 26 | pub trait IntoResult<T> { 27 | fn into_result(self) -> Result<T, Error>; 28 | } 29 | 30 | impl IntoResult<()> for Result<ExitStatus, VoltaError> { 31 | fn into_result(self) -> Result<(), Error> { 32 | match self { 33 | Ok(status) => { 34 | if status.success() { 35 | Ok(()) 36 | } else { 37 | let code = status.code().unwrap_or(1); 38 | Err(Error::Tool(code)) 39 | } 40 | } 41 | Err(err) => Err(Error::Volta(err)), 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod command; 3 | mod cli; 4 | 5 | use clap::Parser; 6 | 7 | use volta_core::error::report_error; 8 | use volta_core::log::{LogContext, LogVerbosity, Logger}; 9 | use volta_core::session::{ActivityKind, Session}; 10 | 11 | mod common; 12 | use common::{ensure_layout, Error}; 13 | 14 | /// The entry point for the `volta` CLI. 15 | pub fn main() { 16 | let volta = cli::Volta::parse(); 17 | let verbosity = match (&volta.verbose, &volta.quiet) { 18 | (false, false) => LogVerbosity::Default, 19 | (true, false) => { 20 | if volta.very_verbose { 21 | LogVerbosity::VeryVerbose 22 | } else { 23 | LogVerbosity::Verbose 24 | } 25 | } 26 | (false, true) => LogVerbosity::Quiet, 27 | (true, true) => { 28 | unreachable!("Clap should prevent the user from providing both --verbose and --quiet") 29 | } 30 | }; 31 | Logger::init(LogContext::Volta, verbosity).expect("Only a single logger should be initialized"); 32 | log::trace!("log level: {verbosity:?}"); 33 | 34 | let mut session = Session::init(); 35 | session.add_event_start(ActivityKind::Volta); 36 | 37 | let result = ensure_layout().and_then(|()| volta.run(&mut session).map_err(Error::Volta)); 38 | match result { 39 | Ok(exit_code) => { 40 | session.add_event_end(ActivityKind::Volta, exit_code); 41 | session.exit(exit_code); 42 | } 43 | Err(Error::Tool(code)) => { 44 | session.add_event_tool_end(ActivityKind::Volta, code); 45 | session.exit_tool(code); 46 | } 47 | Err(Error::Volta(err)) => { 48 | report_error(env!("CARGO_PKG_VERSION"), &err); 49 | session.add_event_error(ActivityKind::Volta, &err); 50 | let code = err.exit_code(); 51 | session.add_event_end(ActivityKind::Volta, code); 52 | session.exit(code); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/volta-migrate.rs: -------------------------------------------------------------------------------- 1 | use volta_core::error::{report_error, ExitCode}; 2 | use volta_core::layout::volta_home; 3 | use volta_core::log::{LogContext, LogVerbosity, Logger}; 4 | use volta_migrate::run_migration; 5 | 6 | pub fn main() { 7 | Logger::init(LogContext::Migration, LogVerbosity::Default) 8 | .expect("Only a single Logger should be initialized"); 9 | 10 | // In order to migrate the existing Volta directory while avoiding unconditional changes to the user's system, 11 | // the Homebrew formula runs volta-migrate with `--no-create` flag in the post-install phase. 12 | let no_create = matches!(std::env::args_os().nth(1), Some(flag) if flag == "--no-create"); 13 | if no_create && volta_home().map_or(true, |home| !home.root().exists()) { 14 | ExitCode::Success.exit(); 15 | } 16 | 17 | let exit_code = match run_migration() { 18 | Ok(()) => ExitCode::Success, 19 | Err(err) => { 20 | report_error(env!("CARGO_PKG_VERSION"), &err); 21 | err.exit_code() 22 | } 23 | }; 24 | 25 | exit_code.exit(); 26 | } 27 | -------------------------------------------------------------------------------- /src/volta-shim.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use common::{ensure_layout, Error, IntoResult}; 4 | use volta_core::error::{report_error, ExitCode}; 5 | use volta_core::log::{LogContext, LogVerbosity, Logger}; 6 | use volta_core::run::execute_shim; 7 | use volta_core::session::{ActivityKind, Session}; 8 | use volta_core::signal::setup_signal_handler; 9 | 10 | pub fn main() { 11 | Logger::init(LogContext::Shim, LogVerbosity::Default) 12 | .expect("Only a single Logger should be initialized"); 13 | setup_signal_handler(); 14 | 15 | let mut session = Session::init(); 16 | session.add_event_start(ActivityKind::Tool); 17 | 18 | let result = ensure_layout().and_then(|()| execute_shim(&mut session).into_result()); 19 | match result { 20 | Ok(()) => { 21 | session.add_event_end(ActivityKind::Tool, ExitCode::Success); 22 | session.exit(ExitCode::Success); 23 | } 24 | Err(Error::Tool(code)) => { 25 | session.add_event_tool_end(ActivityKind::Tool, code); 26 | session.exit_tool(code); 27 | } 28 | Err(Error::Volta(err)) => { 29 | report_error(env!("CARGO_PKG_VERSION"), &err); 30 | session.add_event_error(ActivityKind::Tool, &err); 31 | session.add_event_end(ActivityKind::Tool, err.exit_code()); 32 | session.exit(ExitCode::ExecutionFailure); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/acceptance/main.rs: -------------------------------------------------------------------------------- 1 | use cfg_if::cfg_if; 2 | 3 | cfg_if! { 4 | if #[cfg(feature = "mock-network")] { 5 | mod support; 6 | 7 | // test files 8 | mod corrupted_download; 9 | mod direct_install; 10 | mod direct_uninstall; 11 | mod execute_binary; 12 | mod hooks; 13 | mod merged_platform; 14 | mod migrations; 15 | mod run_shim_directly; 16 | mod verbose_errors; 17 | mod volta_bypass; 18 | mod volta_install; 19 | mod volta_pin; 20 | mod volta_run; 21 | mod volta_uninstall; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/acceptance/run_shim_directly.rs: -------------------------------------------------------------------------------- 1 | use crate::support::sandbox::{sandbox, shim_exe}; 2 | use hamcrest2::assert_that; 3 | use hamcrest2::prelude::*; 4 | use test_support::matchers::execs; 5 | 6 | use volta_core::error::ExitCode; 7 | 8 | #[test] 9 | fn shows_pretty_error_when_calling_shim_directly() { 10 | let s = sandbox().build(); 11 | 12 | assert_that!( 13 | s.process(shim_exe()), 14 | execs() 15 | .with_status(ExitCode::ExecutionFailure as i32) 16 | .with_stderr_contains("[..]should not be called directly[..]") 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /tests/acceptance/support/events_helpers.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | use crate::support::sandbox::Sandbox; 4 | use hamcrest2::assert_that; 5 | use hamcrest2::prelude::*; 6 | 7 | use volta_core::event::{Event, EventKind}; 8 | 9 | pub enum EventKindMatcher<'a> { 10 | Start, 11 | End { exit_code: i32 }, 12 | Error { exit_code: i32, error: &'a str }, 13 | ToolEnd { exit_code: i32 }, 14 | Args { argv: &'a str }, 15 | } 16 | 17 | pub fn match_start() -> EventKindMatcher<'static> { 18 | EventKindMatcher::Start 19 | } 20 | 21 | pub fn match_error(exit_code: i32, error: &str) -> EventKindMatcher { 22 | EventKindMatcher::Error { exit_code, error } 23 | } 24 | 25 | pub fn match_end(exit_code: i32) -> EventKindMatcher<'static> { 26 | EventKindMatcher::End { exit_code } 27 | } 28 | 29 | pub fn match_tool_end(exit_code: i32) -> EventKindMatcher<'static> { 30 | EventKindMatcher::ToolEnd { exit_code } 31 | } 32 | 33 | pub fn match_args(argv: &str) -> EventKindMatcher { 34 | EventKindMatcher::Args { argv } 35 | } 36 | 37 | pub fn assert_events(sandbox: &Sandbox, matchers: Vec<(&str, EventKindMatcher)>) { 38 | let events_path = sandbox.root().join("events.json"); 39 | assert_that!(&events_path, file_exists()); 40 | 41 | let events_file = File::open(events_path).expect("Error reading 'events.json' file in sandbox"); 42 | let events: Vec<Event> = serde_json::de::from_reader(events_file) 43 | .expect("Error parsing 'events.json' file in sandbox"); 44 | assert_that!(events.len(), eq(matchers.len())); 45 | 46 | for (i, matcher) in matchers.iter().enumerate() { 47 | assert_that!(&events[i].name, eq(matcher.0)); 48 | match matcher.1 { 49 | EventKindMatcher::Start => { 50 | assert_that!(&events[i].event, eq(&EventKind::Start)); 51 | } 52 | EventKindMatcher::End { 53 | exit_code: expected_exit_code, 54 | } => { 55 | if let EventKind::End { exit_code } = &events[i].event { 56 | assert_that!(*exit_code, eq(expected_exit_code)); 57 | } else { 58 | panic!( 59 | "Expected: End {{ exit_code: {} }}, Got: {:?}", 60 | expected_exit_code, events[i].event 61 | ); 62 | } 63 | } 64 | EventKindMatcher::Error { 65 | exit_code: expected_exit_code, 66 | error: expected_error, 67 | } => { 68 | if let EventKind::Error { 69 | exit_code, error, .. 70 | } = &events[i].event 71 | { 72 | assert_that!(*exit_code, eq(expected_exit_code)); 73 | assert_that!(error.clone(), matches_regex(expected_error)); 74 | } else { 75 | panic!( 76 | "Expected: Error {{ exit_code: {}, error: {} }}, Got: {:?}", 77 | expected_exit_code, expected_error, events[i].event 78 | ); 79 | } 80 | } 81 | EventKindMatcher::ToolEnd { 82 | exit_code: expected_exit_code, 83 | } => { 84 | if let EventKind::End { exit_code } = &events[i].event { 85 | assert_that!(*exit_code, eq(expected_exit_code)); 86 | } else { 87 | panic!( 88 | "Expected: ToolEnd {{ exit_code: {} }}, Got: {:?}", 89 | expected_exit_code, events[i].event 90 | ); 91 | } 92 | } 93 | EventKindMatcher::Args { 94 | argv: expected_argv, 95 | } => { 96 | if let EventKind::Args { argv } = &events[i].event { 97 | assert_that!(argv.clone(), matches_regex(expected_argv)); 98 | } else { 99 | panic!( 100 | "Expected: Args {{ argv: {} }}, Got: {:?}", 101 | expected_argv, events[i].event 102 | ); 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/acceptance/support/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod events_helpers; 2 | pub mod sandbox; 3 | -------------------------------------------------------------------------------- /tests/acceptance/volta_bypass.rs: -------------------------------------------------------------------------------- 1 | use crate::support::sandbox::{sandbox, shim_exe}; 2 | use hamcrest2::assert_that; 3 | use hamcrest2::prelude::*; 4 | use test_support::matchers::execs; 5 | 6 | use volta_core::error::ExitCode; 7 | 8 | #[test] 9 | fn shim_skips_platform_checks_on_bypass() { 10 | let s = sandbox() 11 | .env("VOLTA_BYPASS", "1") 12 | .env( 13 | "VOLTA_INSTALL_DIR", 14 | &shim_exe().parent().unwrap().to_string_lossy(), 15 | ) 16 | .build(); 17 | 18 | #[cfg(unix)] 19 | assert_that!( 20 | s.process(shim_exe()), 21 | execs() 22 | .with_status(ExitCode::ExecutionFailure as i32) 23 | .with_stderr_contains("VOLTA_BYPASS is enabled[..]") 24 | ); 25 | 26 | #[cfg(windows)] 27 | assert_that!( 28 | s.process(shim_exe()), 29 | execs() 30 | .with_status(ExitCode::UnknownError as i32) 31 | .with_stderr_contains("[..]is not recognized as an internal or external command[..]") 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /tests/fixtures/cli-dist-2.4.159.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/cli-dist-2.4.159.tgz -------------------------------------------------------------------------------- /tests/fixtures/cli-dist-3.12.99.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/cli-dist-3.12.99.tgz -------------------------------------------------------------------------------- /tests/fixtures/cli-dist-3.2.42.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/cli-dist-3.2.42.tgz -------------------------------------------------------------------------------- /tests/fixtures/cli-dist-3.7.71.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/cli-dist-3.7.71.tgz -------------------------------------------------------------------------------- /tests/fixtures/node-v0.0.1-darwin-x64.tar.gz: -------------------------------------------------------------------------------- 1 | CORRUPTED 2 | -------------------------------------------------------------------------------- /tests/fixtures/node-v0.0.1-linux-arm64.tar.gz: -------------------------------------------------------------------------------- 1 | CORRUPTED 2 | -------------------------------------------------------------------------------- /tests/fixtures/node-v0.0.1-linux-x64.tar.gz: -------------------------------------------------------------------------------- 1 | CORRUPTED 2 | -------------------------------------------------------------------------------- /tests/fixtures/node-v0.0.1-win-x64.zip: -------------------------------------------------------------------------------- 1 | CORRUPTED 2 | -------------------------------------------------------------------------------- /tests/fixtures/node-v0.0.1-win-x86.zip: -------------------------------------------------------------------------------- 1 | CORRUPTED 2 | -------------------------------------------------------------------------------- /tests/fixtures/node-v10.99.1040-darwin-x64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v10.99.1040-darwin-x64.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/node-v10.99.1040-linux-arm64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v10.99.1040-linux-arm64.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/node-v10.99.1040-linux-x64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v10.99.1040-linux-x64.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/node-v10.99.1040-win-x64.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v10.99.1040-win-x64.zip -------------------------------------------------------------------------------- /tests/fixtures/node-v10.99.1040-win-x86.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v10.99.1040-win-x86.zip -------------------------------------------------------------------------------- /tests/fixtures/node-v6.19.62-darwin-x64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v6.19.62-darwin-x64.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/node-v6.19.62-linux-arm64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v6.19.62-linux-arm64.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/node-v6.19.62-linux-x64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v6.19.62-linux-x64.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/node-v6.19.62-win-x64.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v6.19.62-win-x64.zip -------------------------------------------------------------------------------- /tests/fixtures/node-v6.19.62-win-x86.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v6.19.62-win-x86.zip -------------------------------------------------------------------------------- /tests/fixtures/node-v8.9.10-darwin-x64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v8.9.10-darwin-x64.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/node-v8.9.10-linux-arm64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v8.9.10-linux-arm64.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/node-v8.9.10-linux-x64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v8.9.10-linux-x64.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/node-v8.9.10-win-x64.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v8.9.10-win-x64.zip -------------------------------------------------------------------------------- /tests/fixtures/node-v8.9.10-win-x86.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v8.9.10-win-x86.zip -------------------------------------------------------------------------------- /tests/fixtures/node-v9.27.6-darwin-x64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v9.27.6-darwin-x64.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/node-v9.27.6-linux-arm64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v9.27.6-linux-arm64.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/node-v9.27.6-linux-x64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v9.27.6-linux-x64.tar.gz -------------------------------------------------------------------------------- /tests/fixtures/node-v9.27.6-win-x64.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v9.27.6-win-x64.zip -------------------------------------------------------------------------------- /tests/fixtures/node-v9.27.6-win-x86.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/node-v9.27.6-win-x86.zip -------------------------------------------------------------------------------- /tests/fixtures/npm-1.2.3.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/npm-1.2.3.tgz -------------------------------------------------------------------------------- /tests/fixtures/npm-4.5.6.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/npm-4.5.6.tgz -------------------------------------------------------------------------------- /tests/fixtures/npm-8.1.5.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/npm-8.1.5.tgz -------------------------------------------------------------------------------- /tests/fixtures/pnpm-0.0.1.tgz: -------------------------------------------------------------------------------- 1 | CORRUPTED 2 | -------------------------------------------------------------------------------- /tests/fixtures/pnpm-6.34.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/pnpm-6.34.0.tgz -------------------------------------------------------------------------------- /tests/fixtures/pnpm-7.7.1.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/pnpm-7.7.1.tgz -------------------------------------------------------------------------------- /tests/fixtures/volta-test-1.0.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/volta-test-1.0.0.tgz -------------------------------------------------------------------------------- /tests/fixtures/yarn-0.0.1.tgz: -------------------------------------------------------------------------------- 1 | CORRUPTED 2 | -------------------------------------------------------------------------------- /tests/fixtures/yarn-1.12.99.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/yarn-1.12.99.tgz -------------------------------------------------------------------------------- /tests/fixtures/yarn-1.2.42.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/yarn-1.2.42.tgz -------------------------------------------------------------------------------- /tests/fixtures/yarn-1.4.159.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/yarn-1.4.159.tgz -------------------------------------------------------------------------------- /tests/fixtures/yarn-1.7.71.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/tests/fixtures/yarn-1.7.71.tgz -------------------------------------------------------------------------------- /tests/smoke/autodownload.rs: -------------------------------------------------------------------------------- 1 | use crate::support::temp_project::temp_project; 2 | 3 | use hamcrest2::assert_that; 4 | use hamcrest2::prelude::*; 5 | use test_support::matchers::execs; 6 | 7 | static PACKAGE_JSON_WITH_PINNED_NODE: &str = r#"{ 8 | "name": "test-package", 9 | "volta": { 10 | "node": "14.15.5" 11 | } 12 | }"#; 13 | 14 | static PACKAGE_JSON_WITH_PINNED_NODE_NPM: &str = r#"{ 15 | "name": "test-package", 16 | "volta": { 17 | "node": "17.3.0", 18 | "npm": "8.5.1" 19 | } 20 | }"#; 21 | 22 | static PACKAGE_JSON_WITH_PINNED_NODE_YARN_1: &str = r#"{ 23 | "name": "test-package", 24 | "volta": { 25 | "node": "16.11.1", 26 | "yarn": "1.22.16" 27 | } 28 | }"#; 29 | 30 | static PACKAGE_JSON_WITH_PINNED_NODE_YARN_3: &str = r#"{ 31 | "name": "test-package", 32 | "volta": { 33 | "node": "16.14.0", 34 | "yarn": "3.1.0" 35 | } 36 | }"#; 37 | 38 | #[test] 39 | fn autodownload_node() { 40 | let p = temp_project() 41 | .package_json(PACKAGE_JSON_WITH_PINNED_NODE) 42 | .build(); 43 | 44 | assert_that!( 45 | p.node("--version"), 46 | execs().with_status(0).with_stdout_contains("v14.15.5") 47 | ); 48 | } 49 | 50 | #[test] 51 | fn autodownload_npm() { 52 | let p = temp_project() 53 | .package_json(PACKAGE_JSON_WITH_PINNED_NODE_NPM) 54 | .build(); 55 | 56 | assert_that!( 57 | p.npm("--version"), 58 | execs().with_status(0).with_stdout_contains("8.5.1") 59 | ); 60 | } 61 | 62 | #[test] 63 | fn autodownload_yarn_1() { 64 | let p = temp_project() 65 | .package_json(PACKAGE_JSON_WITH_PINNED_NODE_YARN_1) 66 | .build(); 67 | 68 | assert_that!( 69 | p.yarn("--version"), 70 | execs().with_status(0).with_stdout_contains("1.22.16") 71 | ); 72 | } 73 | 74 | #[test] 75 | fn autodownload_yarn_3() { 76 | let p = temp_project() 77 | .package_json(PACKAGE_JSON_WITH_PINNED_NODE_YARN_3) 78 | .build(); 79 | 80 | assert_that!( 81 | p.yarn("--version"), 82 | execs().with_status(0).with_stdout_contains("3.1.0") 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /tests/smoke/direct_install.rs: -------------------------------------------------------------------------------- 1 | use crate::support::temp_project::temp_project; 2 | use hamcrest2::assert_that; 3 | use hamcrest2::prelude::*; 4 | use test_support::matchers::execs; 5 | 6 | #[test] 7 | fn npm_global_install() { 8 | let p = temp_project().build(); 9 | 10 | // Have to install node to ensure npm is available 11 | assert_that!(p.volta("install node@14.1.0"), execs().with_status(0)); 12 | 13 | assert_that!( 14 | p.npm("install --global typescript@3.9.4 yarn@1.16.0 ../../../../tests/fixtures/volta-test-1.0.0.tgz"), 15 | execs().with_status(0) 16 | ); 17 | 18 | assert!(p.shim_exists("tsc")); 19 | assert!(p.shim_exists("tsserver")); 20 | assert!(p.package_is_installed("typescript")); 21 | assert_that!( 22 | p.exec_shim("tsc", "--version"), 23 | execs().with_status(0).with_stdout_contains("Version 3.9.4") 24 | ); 25 | 26 | assert!(p.yarn_version_is_fetched("1.16.0")); 27 | assert!(p.yarn_version_is_unpacked("1.16.0")); 28 | p.assert_yarn_version_is_installed("1.16.0"); 29 | 30 | assert_that!( 31 | p.yarn("--version"), 32 | execs().with_status(0).with_stdout_contains("1.16.0") 33 | ); 34 | 35 | assert!(p.shim_exists("volta-test")); 36 | assert!(p.package_is_installed("volta-test")); 37 | assert_that!( 38 | p.exec_shim("volta-test", ""), 39 | execs() 40 | .with_status(0) 41 | .with_stdout_contains("Volta test successful") 42 | ); 43 | } 44 | 45 | #[test] 46 | fn yarn_global_add() { 47 | let p = temp_project().build(); 48 | 49 | let tarball_path = p 50 | .root() 51 | .join("../../../../tests/fixtures/volta-test-1.0.0.tgz") 52 | .canonicalize() 53 | .unwrap(); 54 | 55 | // Have to install node and yarn first 56 | assert_that!( 57 | p.volta("install node@14.2.0 yarn@1.22.5"), 58 | execs().with_status(0) 59 | ); 60 | 61 | assert_that!( 62 | p.yarn(&format!( 63 | "global add typescript@4.0.2 npm@6.4.0 file:{}", 64 | tarball_path.display() 65 | )), 66 | execs().with_status(0) 67 | ); 68 | 69 | assert!(p.shim_exists("tsc")); 70 | assert!(p.shim_exists("tsserver")); 71 | assert!(p.package_is_installed("typescript")); 72 | assert_that!( 73 | p.exec_shim("tsc", "--version"), 74 | execs().with_status(0).with_stdout_contains("Version 4.0.2") 75 | ); 76 | 77 | assert!(p.npm_version_is_fetched("6.4.0")); 78 | assert!(p.npm_version_is_unpacked("6.4.0")); 79 | p.assert_npm_version_is_installed("6.4.0"); 80 | 81 | assert_that!( 82 | p.npm("--version"), 83 | execs().with_status(0).with_stdout_contains("6.4.0") 84 | ); 85 | 86 | assert!(p.shim_exists("volta-test")); 87 | assert!(p.package_is_installed("volta-test")); 88 | assert_that!( 89 | p.exec_shim("volta-test", ""), 90 | execs() 91 | .with_status(0) 92 | .with_stdout_contains("Volta test successful") 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /tests/smoke/direct_upgrade.rs: -------------------------------------------------------------------------------- 1 | use crate::support::temp_project::temp_project; 2 | use hamcrest2::assert_that; 3 | use hamcrest2::prelude::*; 4 | use test_support::matchers::execs; 5 | use volta_core::error::ExitCode; 6 | 7 | #[test] 8 | fn npm_global_update() { 9 | let p = temp_project().build(); 10 | 11 | // Install Node and typescript 12 | assert_that!( 13 | p.volta("install node@14.10.1 typescript@2.8.4"), 14 | execs().with_status(0) 15 | ); 16 | // Confirm correct version of typescript installed 17 | assert_that!( 18 | p.exec_shim("tsc", "--version"), 19 | execs().with_status(0).with_stdout_contains("Version 2.8.4") 20 | ); 21 | 22 | // Update typescript 23 | assert_that!(p.npm("update --global typescript"), execs().with_status(0)); 24 | // Confirm update completed successfully 25 | assert_that!( 26 | p.exec_shim("tsc", "--version"), 27 | execs().with_status(0).with_stdout_contains("Version 2.9.2") 28 | ); 29 | 30 | // Revert typescript update 31 | assert_that!(p.npm("i -g typescript@2.8.4"), execs().with_status(0)); 32 | // Update all packages (should include typescript) 33 | assert_that!(p.npm("update --global"), execs().with_status(0)); 34 | // Confirm update 35 | assert_that!( 36 | p.exec_shim("tsc", "--version"), 37 | execs().with_status(0).with_stdout_contains("Version 2.9.2") 38 | ); 39 | 40 | // Confirm that attempting to upgrade using `yarn` fails 41 | assert_that!( 42 | p.yarn("global upgrade typescript"), 43 | execs() 44 | .with_status(ExitCode::ExecutionFailure as i32) 45 | .with_stderr_contains("[..]The package 'typescript' was installed using npm.") 46 | ); 47 | } 48 | 49 | #[test] 50 | fn yarn_global_update() { 51 | let p = temp_project().build(); 52 | 53 | // Install Node and Yarn 54 | assert_that!( 55 | p.volta("install node@14.10.1 yarn@1.22.5"), 56 | execs().with_status(0) 57 | ); 58 | // Install typescript 59 | assert_that!( 60 | p.yarn("global add typescript@2.8.4"), 61 | execs().with_status(0) 62 | ); 63 | // Confirm correct version of typescript installed 64 | assert_that!( 65 | p.exec_shim("tsc", "--version"), 66 | execs().with_status(0).with_stdout_contains("Version 2.8.4") 67 | ); 68 | 69 | // Upgrade typescript 70 | assert_that!( 71 | p.yarn("global upgrade typescript@2.9"), 72 | execs().with_status(0) 73 | ); 74 | // Confirm upgrade completed successfully 75 | assert_that!( 76 | p.exec_shim("tsc", "--version"), 77 | execs().with_status(0).with_stdout_contains("Version 2.9.2") 78 | ); 79 | 80 | // Note: Since Yarn always installs the latest version that matches your requirements and 81 | // 'upgrade' also gets the latest version that matches (which can change over time), an 82 | // immediate call to 'yarn upgrade' without packages won't result in any change. 83 | 84 | // This is in contrast to npm, which treats your installed version as a caret specifier when 85 | // runnin `npm update` 86 | 87 | // Confirm that attempting to upgrade using `npm` fails 88 | assert_that!( 89 | p.npm("update -g typescript"), 90 | execs() 91 | .with_status(ExitCode::ExecutionFailure as i32) 92 | .with_stderr_contains("[..]The package 'typescript' was installed using Yarn.") 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /tests/smoke/main.rs: -------------------------------------------------------------------------------- 1 | // Smoke tests for Volta, that will be run in CI. 2 | // 3 | // To run these locally: 4 | // (CAUTION: this will destroy the Volta installation on the system where this is run) 5 | // 6 | // ``` 7 | // VOLTA_LOGLEVEL=debug cargo test --test smoke --features smoke-tests -- --test-threads 1 8 | // ``` 9 | // 10 | // Also note that each test uses a different version of node and yarn. This is to prevent 11 | // false positives if the tests are not cleaned up correctly. Any new tests should use 12 | // different versions of node and yarn. 13 | 14 | cfg_if::cfg_if! { 15 | if #[cfg(all(unix, feature = "smoke-tests"))] { 16 | mod autodownload; 17 | mod direct_install; 18 | mod direct_upgrade; 19 | mod npm_link; 20 | mod package_migration; 21 | pub mod support; 22 | mod volta_fetch; 23 | mod volta_install; 24 | mod volta_run; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/smoke/npm_link.rs: -------------------------------------------------------------------------------- 1 | use crate::support::temp_project::temp_project; 2 | use hamcrest2::assert_that; 3 | use hamcrest2::prelude::*; 4 | use test_support::matchers::execs; 5 | 6 | const PACKAGE_JSON: &str = r#" 7 | { 8 | "name": "my-library", 9 | "version": "1.0.0", 10 | "bin": { 11 | "mylibrary": "./index.js" 12 | } 13 | }"#; 14 | 15 | const INDEX_JS: &str = r#"#!/usr/bin/env node 16 | 17 | console.log('VOLTA TEST'); 18 | "#; 19 | 20 | #[test] 21 | fn link_unlink_local_project() { 22 | let p = temp_project() 23 | .package_json(PACKAGE_JSON) 24 | .project_file("index.js", INDEX_JS) 25 | .build(); 26 | 27 | // Install node to ensure npm is available 28 | assert_that!(p.volta("install node@14.15.1"), execs().with_status(0)); 29 | 30 | // Link the current project as a global 31 | assert_that!(p.npm("link"), execs().with_status(0)); 32 | // Executable should be available 33 | assert!(p.shim_exists("mylibrary")); 34 | assert!(p.package_is_installed("my-library")); 35 | assert_that!( 36 | p.exec_shim("mylibrary", ""), 37 | execs().with_status(0).with_stdout_contains("VOLTA TEST") 38 | ); 39 | 40 | // Unlink the current project 41 | assert_that!(p.npm("unlink"), execs().with_status(0)); 42 | // Executable should no longer be available 43 | assert!(!p.shim_exists("mylibrary")); 44 | assert!(!p.package_is_installed("my-library")); 45 | } 46 | 47 | #[test] 48 | fn link_global_into_current_project() { 49 | let p = temp_project().package_json(PACKAGE_JSON).build(); 50 | 51 | assert_that!( 52 | p.volta("install node@14.19.0 typescript@4.1.2"), 53 | execs().with_status(0) 54 | ); 55 | 56 | // Link typescript into the current project 57 | assert_that!(p.npm("link typescript"), execs().with_status(0)); 58 | // Typescript should now be available inside the node_modules directory 59 | assert!(p.project_path_exists("node_modules/typescript")); 60 | assert!(p.project_path_exists("node_modules/typescript/package.json")); 61 | } 62 | -------------------------------------------------------------------------------- /tests/smoke/package_migration.rs: -------------------------------------------------------------------------------- 1 | use crate::support::temp_project::temp_project; 2 | 3 | use hamcrest2::assert_that; 4 | use hamcrest2::prelude::*; 5 | use test_support::matchers::execs; 6 | 7 | const LEGACY_PACKAGE_CONFIG: &str = r#"{ 8 | "name": "cowsay", 9 | "version": "1.1.7", 10 | "platform": { 11 | "node": { 12 | "runtime": "14.18.2", 13 | "npm": null 14 | }, 15 | "yarn": null 16 | }, 17 | "bins": [ 18 | "cowsay", 19 | "cowthink" 20 | ] 21 | }"#; 22 | 23 | const LEGACY_BIN_CONFIG: &str = r#"{ 24 | "name": "cowsay", 25 | "package": "cowsay", 26 | "version": "1.1.7", 27 | "path": "./cli.js", 28 | "platform": { 29 | "node": { 30 | "runtime": "14.18.2", 31 | "npm": null 32 | }, 33 | "yarn": null 34 | }, 35 | "loader": { 36 | "command": "node", 37 | "args": [] 38 | } 39 | }"#; 40 | 41 | const COWSAY_HELLO: &str = r#" _______ 42 | < hello > 43 | ------- 44 | \ ^__^ 45 | \ (oo)\_______ 46 | (__)\ )\/\ 47 | ||----w | 48 | || ||"#; 49 | 50 | #[test] 51 | fn legacy_package_upgrade() { 52 | let p = temp_project() 53 | .volta_home_file("tools/user/packages/cowsay.json", LEGACY_PACKAGE_CONFIG) 54 | .volta_home_file("tools/user/bins/cowsay.json", LEGACY_BIN_CONFIG) 55 | .volta_home_file( 56 | "tools/image/packages/cowsay/1.3.1/README.md", 57 | "Mock of installed package", 58 | ) 59 | .volta_home_file("layout.v2", "") 60 | .build(); 61 | 62 | assert_that!(p.volta("--version"), execs().with_status(0)); 63 | 64 | assert!(p.package_is_installed("cowsay")); 65 | 66 | assert_that!( 67 | p.exec_shim("cowsay", "hello"), 68 | execs().with_status(0).with_stdout_contains(COWSAY_HELLO) 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /tests/smoke/support/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod temp_project; 2 | -------------------------------------------------------------------------------- /tests/smoke/volta_fetch.rs: -------------------------------------------------------------------------------- 1 | use crate::support::temp_project::temp_project; 2 | 3 | use hamcrest2::assert_that; 4 | use hamcrest2::prelude::*; 5 | use test_support::matchers::execs; 6 | 7 | #[test] 8 | fn fetch_node() { 9 | let p = temp_project().build(); 10 | 11 | assert_that!(p.volta("fetch node@14.17.6"), execs().with_status(0)); 12 | assert!(p.node_version_is_fetched("14.17.6")); 13 | assert!(p.node_version_is_unpacked("14.17.6")); 14 | } 15 | 16 | #[test] 17 | fn fetch_yarn_1() { 18 | let p = temp_project().build(); 19 | 20 | assert_that!(p.volta("fetch yarn@1.22.1"), execs().with_status(0)); 21 | assert!(p.yarn_version_is_fetched("1.22.1")); 22 | assert!(p.yarn_version_is_unpacked("1.22.1")); 23 | } 24 | 25 | #[test] 26 | fn fetch_yarn_3() { 27 | let p = temp_project().build(); 28 | 29 | assert_that!(p.volta("fetch yarn@3.2.0"), execs().with_status(0)); 30 | assert!(p.yarn_version_is_fetched("3.2.0")); 31 | assert!(p.yarn_version_is_unpacked("3.2.0")); 32 | } 33 | 34 | #[test] 35 | fn fetch_npm() { 36 | let p = temp_project().build(); 37 | 38 | assert_that!(p.volta("fetch npm@8.3.1"), execs().with_status(0)); 39 | assert!(p.npm_version_is_fetched("8.3.1")); 40 | assert!(p.npm_version_is_unpacked("8.3.1")); 41 | } 42 | -------------------------------------------------------------------------------- /tests/smoke/volta_run.rs: -------------------------------------------------------------------------------- 1 | use crate::support::temp_project::temp_project; 2 | 3 | use hamcrest2::assert_that; 4 | use hamcrest2::prelude::*; 5 | use test_support::matchers::execs; 6 | 7 | // Note: Node 14.11.0 is bundled with npm 6.14.8 8 | const PACKAGE_JSON: &str = r#"{ 9 | "name": "test-package", 10 | "volta": { 11 | "node": "14.11.0", 12 | "npm": "6.14.15", 13 | "yarn": "1.22.10" 14 | } 15 | }"#; 16 | 17 | #[test] 18 | fn run_node() { 19 | let p = temp_project().build(); 20 | 21 | assert_that!( 22 | p.volta("run --node 14.16.0 node --version"), 23 | execs().with_status(0).with_stdout_contains("v14.16.0") 24 | ); 25 | } 26 | 27 | #[test] 28 | fn run_npm() { 29 | let p = temp_project().build(); 30 | 31 | assert_that!( 32 | p.volta("run --node 14.14.0 --npm 6.14.16 npm --version"), 33 | execs().with_status(0).with_stdout_contains("6.14.16") 34 | ) 35 | } 36 | 37 | #[test] 38 | fn run_yarn_1() { 39 | let p = temp_project().build(); 40 | 41 | assert_that!( 42 | p.volta("run --node 14.16.1 --yarn 1.22.0 yarn --version"), 43 | execs().with_status(0).with_stdout_contains("1.22.0") 44 | ); 45 | } 46 | 47 | #[test] 48 | fn run_yarn_3() { 49 | let p = temp_project().build(); 50 | 51 | assert_that!( 52 | p.volta("run --node 16.14.1 --yarn 3.1.1 yarn --version"), 53 | execs().with_status(0).with_stdout_contains("3.1.1") 54 | ); 55 | } 56 | 57 | #[test] 58 | fn inherits_project_platform() { 59 | let p = temp_project().package_json(PACKAGE_JSON).build(); 60 | 61 | assert_that!( 62 | p.volta("run --yarn 1.21.0 yarn --version"), 63 | execs().with_status(0).with_stdout_contains("1.21.0") 64 | ); 65 | } 66 | 67 | #[test] 68 | fn run_environment() { 69 | let p = temp_project().build(); 70 | 71 | assert_that!( 72 | p.volta("run --node 14.15.3 --env VOLTA_SMOKE_1234=hello node -e console.log(process.env.VOLTA_SMOKE_1234)"), 73 | execs().with_status(0).with_stdout_contains("hello") 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /volta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/volta.png -------------------------------------------------------------------------------- /wix/License.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033{\fonttbl{\f0\fnil\fcharset0 Calibri;}} 2 | {\*\generator Riched20 10.0.17134}\viewkind4\uc1 3 | \pard\sl240\slmult1\f0\fs24\lang9 BSD 2-CLAUSE LICENSE\par 4 | \par 5 | Copyright (c) 2017, The Volta Contributors.\par 6 | All rights reserved.\par 7 | \par 8 | This product includes:\par 9 | \par 10 | Contributions from LinkedIn Corporation\par 11 | Copyright (c) 2017, LinkedIn Corporation.\par 12 | \par 13 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\par 14 | \par 15 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\par 16 | \par 17 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\par 18 | \par 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\par 20 | } 21 | � -------------------------------------------------------------------------------- /wix/shim.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | "%~dpn0.exe" %* 3 | -------------------------------------------------------------------------------- /wix/volta.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volta-cli/volta/a7384fa4fc7a0eca961032da4d962d94218b5868/wix/volta.ico --------------------------------------------------------------------------------