├── python ├── src │ └── dotslash │ │ ├── py.typed │ │ ├── __main__.py │ │ ├── __init__.py │ │ └── _locate.py ├── requirements-fmt.txt ├── tests │ ├── __init__.py │ ├── test_api.py │ └── test_cli.py ├── pyproject.toml └── README.md ├── website ├── static │ ├── .nojekyll │ ├── CNAME │ └── img │ │ ├── favicon.svg │ │ └── favicon-on-black.svg ├── .npmrc ├── assets │ └── Final │ │ ├── AI │ │ └── DotSlash_Final.ai │ │ ├── PNG │ │ ├── DotSlash_Final_symbol_white.png │ │ ├── DotSlash_Final_brankmark_white.png │ │ ├── DotSlash_Final_symbol_rayscale.png │ │ ├── DotSlash_Final_symbol_full-color.png │ │ ├── DotSlash_Final_brandmark_full-color.png │ │ ├── DotSlash_Final_brandmark_grayscale.png │ │ ├── DotSlash_Final_symbol_full-color-on-black.png │ │ └── DotSlash_Final_brandmark_color-and-white-on-black.png │ │ └── SVG │ │ ├── DotSlash_Final_symbol_rayscale.svg │ │ ├── DotSlash_Final_symbol_white.svg │ │ ├── DotSlash_Final_symbol_full-color.svg │ │ └── DotSlash_Final_symbol_full-color-on-black.svg ├── src │ ├── pages │ │ ├── markdown-page.md │ │ ├── index.module.css │ │ └── index.js │ ├── components │ │ ├── HomepageFeatures.module.css │ │ └── HomepageFeatures.js │ └── css │ │ └── custom.css ├── babel.config.js ├── .gitignore ├── README.md ├── sidebars.js ├── package.json ├── docs │ ├── execution.md │ ├── flags.md │ └── index.md └── docusaurus.config.js ├── windows_shim ├── .gitignore ├── rust-toolchain.toml ├── dotslash_windows_shim-x86_64.exe ├── dotslash_windows_shim-aarch64.exe ├── Cargo.toml ├── README.md ├── release.py ├── run_test.py └── Cargo.lock ├── .github ├── dependabot.yml └── workflows │ ├── test-node.yml │ ├── test.yml │ ├── build.yml │ ├── release-feature.yml │ ├── test-python.yml │ ├── publish-website.yml │ ├── devcontainer.yml │ └── release-downstream.yml ├── node ├── .prettierrc ├── index.d.ts ├── index.js.flow ├── bin │ └── dotslash ├── index.js ├── package-lock.json ├── package.json ├── platforms.js ├── scripts │ ├── clean-package.js │ └── build-package.js └── README.md ├── .gitignore ├── devcontainer-features ├── test │ └── dotslash │ │ ├── scenarios.json │ │ ├── test.sh │ │ ├── versioned.sh │ │ └── install_buck.sh └── src │ └── dotslash │ ├── devcontainer-feature.json │ ├── README.md │ └── install.sh ├── .devcontainer ├── Containerfile └── devcontainer.json ├── benches └── util │ └── mod.rs ├── src ├── util │ ├── update_mtime.rs │ ├── is_path_safe_to_own.rs │ ├── tree_perms.rs │ ├── is_not_found_error.rs │ ├── execv.rs │ ├── file_lock.rs │ ├── chmodx.rs │ ├── progress.rs │ ├── display.rs │ ├── mv_no_clobber.rs │ └── unarchive.rs ├── main.rs ├── default_provider_factory.rs ├── util.rs ├── http_provider.rs ├── platform.rs ├── provider.rs ├── locate.rs ├── s3_provider.rs ├── fetch_method.rs ├── digest.rs ├── dotslash_cache.rs └── github_release_provider.rs ├── tests ├── snapshots │ └── http__dummy_values.out ├── common │ └── ci.rs ├── fixtures │ ├── http__dummy_values.in │ ├── http__tar__print_argv │ ├── http__plain__print_argv │ ├── http__zip__print_argv │ ├── http__tar_xz__print_argv │ ├── http__nonexistent_url │ ├── http__tar_gz__print_argv │ ├── http__tar_zst__print_argv │ ├── http__gz__print_argv │ ├── http__xz__print_argv │ └── http__zst__print_argv └── generate_fixtures.py ├── LICENSE-MIT ├── CONTRIBUTING.md ├── Justfile ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── README.md └── CHANGELOG.md /python/src/dotslash/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/static/CNAME: -------------------------------------------------------------------------------- 1 | dotslash-cli.com 2 | -------------------------------------------------------------------------------- /windows_shim/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /target 3 | -------------------------------------------------------------------------------- /windows_shim/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | -------------------------------------------------------------------------------- /website/.npmrc: -------------------------------------------------------------------------------- 1 | # Stop people use npm instead of yarn by accident 2 | engine-strict = true 3 | -------------------------------------------------------------------------------- /website/assets/Final/AI/DotSlash_Final.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/dotslash/HEAD/website/assets/Final/AI/DotSlash_Final.ai -------------------------------------------------------------------------------- /windows_shim/dotslash_windows_shim-x86_64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/dotslash/HEAD/windows_shim/dotslash_windows_shim-x86_64.exe -------------------------------------------------------------------------------- /windows_shim/dotslash_windows_shim-aarch64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/dotslash/HEAD/windows_shim/dotslash_windows_shim-aarch64.exe -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "devcontainers" 5 | directory: "/" 6 | schedule: 7 | interval: weekly 8 | -------------------------------------------------------------------------------- /website/assets/Final/PNG/DotSlash_Final_symbol_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/dotslash/HEAD/website/assets/Final/PNG/DotSlash_Final_symbol_white.png -------------------------------------------------------------------------------- /python/requirements-fmt.txt: -------------------------------------------------------------------------------- 1 | # generated by `pyfmt --requirements` 2 | black==24.4.2 3 | ruff-api==0.1.0 4 | stdlibs==2024.1.28 5 | ufmt==2.8.0 6 | usort==1.0.8.post1 7 | -------------------------------------------------------------------------------- /website/assets/Final/PNG/DotSlash_Final_brankmark_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/dotslash/HEAD/website/assets/Final/PNG/DotSlash_Final_brankmark_white.png -------------------------------------------------------------------------------- /website/assets/Final/PNG/DotSlash_Final_symbol_rayscale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/dotslash/HEAD/website/assets/Final/PNG/DotSlash_Final_symbol_rayscale.png -------------------------------------------------------------------------------- /website/assets/Final/PNG/DotSlash_Final_symbol_full-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/dotslash/HEAD/website/assets/Final/PNG/DotSlash_Final_symbol_full-color.png -------------------------------------------------------------------------------- /website/assets/Final/PNG/DotSlash_Final_brandmark_full-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/dotslash/HEAD/website/assets/Final/PNG/DotSlash_Final_brandmark_full-color.png -------------------------------------------------------------------------------- /website/assets/Final/PNG/DotSlash_Final_brandmark_grayscale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/dotslash/HEAD/website/assets/Final/PNG/DotSlash_Final_brandmark_grayscale.png -------------------------------------------------------------------------------- /website/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /node/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "overrides": [ 4 | { 5 | "files": "bin/dotslash", 6 | "options": { "parser": "babel" } 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /website/assets/Final/PNG/DotSlash_Final_symbol_full-color-on-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/dotslash/HEAD/website/assets/Final/PNG/DotSlash_Final_symbol_full-color-on-black.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Global Files 2 | .DS_Store 3 | 4 | # Rust 5 | /target/ 6 | 7 | # Python 8 | __pycache__/ 9 | .venv/ 10 | *.pyc 11 | 12 | # Node.js 13 | node_modules/ 14 | /node/bin/*/ 15 | -------------------------------------------------------------------------------- /website/assets/Final/PNG/DotSlash_Final_brandmark_color-and-white-on-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/dotslash/HEAD/website/assets/Final/PNG/DotSlash_Final_brandmark_color-and-white-on-black.png -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 3 | */ 4 | 5 | module.exports = { 6 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 7 | }; 8 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 3 | */ 4 | 5 | .features { 6 | display: flex; 7 | align-items: center; 8 | padding: 2rem 0; 9 | width: 100%; 10 | } 11 | 12 | .featureSvg { 13 | height: 200px; 14 | width: 200px; 15 | } 16 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /devcontainer-features/test/dotslash/scenarios.json: -------------------------------------------------------------------------------- 1 | { 2 | "install_buck": { 3 | "image": "mcr.microsoft.com/devcontainers/base:ubuntu", 4 | "features": { 5 | "dotslash": {} 6 | } 7 | }, 8 | "versioned": { 9 | "image": "mcr.microsoft.com/devcontainers/base:ubuntu", 10 | "features": { 11 | "dotslash": { 12 | "version": "v0.5.5" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /python/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Meta Platforms, Inc. and affiliates. 2 | # 3 | # This source code is dual-licensed under either the MIT license found in the 4 | # LICENSE-MIT file in the root directory of this source tree or the Apache 5 | # License, Version 2.0 found in the LICENSE-APACHE file in the root directory 6 | # of this source tree. You may select, at your option, one of the 7 | # above-listed licenses. 8 | -------------------------------------------------------------------------------- /.devcontainer/Containerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/astral-sh/uv as uv 2 | 3 | FROM mcr.microsoft.com/devcontainers/rust:bookworm 4 | 5 | RUN apt-get update \ 6 | && apt-get upgrade -y \ 7 | && apt-get install -y --no-install-recommends \ 8 | nodejs \ 9 | npm \ 10 | && apt-get clean \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | COPY --chmod=755 --from=uv /uv /usr/local/bin/uv 14 | COPY --chmod=755 --from=uv /uvx /usr/local/bin/uvx 15 | -------------------------------------------------------------------------------- /node/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | * 10 | * @format 11 | */ 12 | 13 | declare const binaryPath: string; 14 | export = binaryPath; 15 | -------------------------------------------------------------------------------- /.github/workflows/test-node.yml: -------------------------------------------------------------------------------- 1 | name: JavaScript chores 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | defaults: 12 | run: 13 | working-directory: node 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: actions/setup-node@v6 17 | with: 18 | node-version: '20.x' 19 | registry-url: 'https://registry.npmjs.org' 20 | - run: npm ci 21 | - run: npm run lint 22 | -------------------------------------------------------------------------------- /devcontainer-features/src/dotslash/devcontainer-feature.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dotslash", 3 | "id": "dotslash", 4 | "version": "1.0.0", 5 | "description": "Install the \"dotslash\" binary.", 6 | "documentationURL": "https://github.com/facebook/dotslash/tree/main/devcontainer-feature/src", 7 | "options": { 8 | "version": { 9 | "default": "latest", 10 | "description": "The version of \"dotslash\" to install", 11 | "proposals": [ 12 | "latest" 13 | ], 14 | "type": "string" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /node/index.js.flow: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | * 10 | * @format 11 | * @flow strict-local 12 | */ 13 | 14 | declare var binaryPath: string; 15 | module.exports = binaryPath; 16 | -------------------------------------------------------------------------------- /benches/util/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | #[path = "../../src/util/fs_ctx.rs"] 12 | pub mod fs_ctx; 13 | #[path = "../../src/util/unarchive.rs"] 14 | pub mod unarchive; 15 | -------------------------------------------------------------------------------- /website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 3 | */ 4 | 5 | /** 6 | * CSS files with the .module.css suffix will be treated as CSS modules 7 | * and scoped locally. 8 | */ 9 | 10 | .heroBanner { 11 | padding: 4rem 0; 12 | text-align: center; 13 | position: relative; 14 | overflow: hidden; 15 | } 16 | 17 | @media screen and (max-width: 966px) { 18 | .heroBanner { 19 | padding: 2rem; 20 | } 21 | } 22 | 23 | .buttons { 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | } 28 | -------------------------------------------------------------------------------- /devcontainer-features/src/dotslash/README.md: -------------------------------------------------------------------------------- 1 | # DotSlash Dev Container Feature 2 | 3 | A feature to install DotSlash into a Dev Container. 4 | 5 | It is installed at `/usr/local/bin/dotslash`. 6 | 7 | ## Example Usage 8 | 9 | ```json 10 | "features": { 11 | "ghcr.io/facebook/dotslash/feature:1": { 12 | "version": "latest" 13 | } 14 | } 15 | ``` 16 | 17 | ## Options 18 | 19 | | Options Id | Description | Type | Default Value | 20 | | ---------- | ----------------------------------- | ------ | ------------- | 21 | | version | The version of DotSlash to install. | string | latest | 22 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern 4 | static website generator. 5 | 6 | ### Installation 7 | 8 | ``` 9 | $ yarn 10 | ``` 11 | 12 | ### Local Development 13 | 14 | ``` 15 | $ yarn start 16 | ``` 17 | 18 | This command starts a local development server and opens up a browser window. 19 | Most changes are reflected live without having to restart the server. 20 | 21 | ### Build 22 | 23 | ``` 24 | $ yarn build 25 | ``` 26 | 27 | This command generates static content into the `build` directory and can be 28 | served using any static contents hosting service. 29 | -------------------------------------------------------------------------------- /devcontainer-features/test/dotslash/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | # 4 | # This source code is dual-licensed under either the MIT license found in the 5 | # LICENSE-MIT file in the root directory of this source tree or the Apache 6 | # License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | # of this source tree. You may select, at your option, one of the 8 | # above-listed licenses. 9 | 10 | set -e 11 | 12 | # shellcheck source=/dev/null 13 | source dev-container-features-test-lib 14 | 15 | check "ensure dotslash is installed and works" dotslash --version 16 | 17 | reportResults 18 | -------------------------------------------------------------------------------- /devcontainer-features/test/dotslash/versioned.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | # 4 | # This source code is dual-licensed under either the MIT license found in the 5 | # LICENSE-MIT file in the root directory of this source tree or the Apache 6 | # License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | # of this source tree. You may select, at your option, one of the 8 | # above-listed licenses. 9 | 10 | set -e 11 | 12 | # shellcheck source=/dev/null 13 | source dev-container-features-test-lib 14 | 15 | check "ensure dotslash is installed and works" test "$(dotslash --version)" = "DotSlash 0.5.5" 16 | 17 | reportResults 18 | -------------------------------------------------------------------------------- /node/bin/dotslash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Copyright (c) Meta Platforms, Inc. and affiliates. 4 | * 5 | * This source code is licensed under both the MIT license found in the 6 | * LICENSE-MIT file in the root directory of this source tree and the Apache 7 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 8 | * of this source tree. 9 | */ 10 | 11 | 'use strict'; 12 | 13 | const spawn = require('child_process').spawn; 14 | 15 | const input = process.argv.slice(2); 16 | const bin = require('../'); 17 | 18 | if (bin !== null) { 19 | spawn(bin, input, { stdio: 'inherit' }).on('exit', process.exit); 20 | } else { 21 | throw new Error('Platform not supported.'); 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Testing 3 | 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | validate-devcontainer-feature: 13 | runs-on: ${{ matrix.runsOn }} 14 | strategy: 15 | matrix: 16 | runsOn: 17 | - ubuntu-24.04-arm 18 | - ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | - name: Validate devcontainer-feature.json files 22 | uses: devcontainers/action@1082abd5d2bf3a11abccba70eef98df068277772 # v1.4.3 23 | with: 24 | validate-only: "true" 25 | base-path-to-features: ./devcontainer-features/src 26 | -------------------------------------------------------------------------------- /node/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | 'use strict'; 12 | 13 | const os = require('os'); 14 | const path = require('path'); 15 | const { artifactsByPlatformAndArch } = require('./platforms'); 16 | 17 | const artifacts = artifactsByPlatformAndArch[os.platform()]; 18 | const { slug, binary } = artifacts[os.arch()] ?? artifacts['*']; 19 | 20 | module.exports = path.join(__dirname, 'bin', slug, binary); 21 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 3 | */ 4 | 5 | /** 6 | * Creating a sidebar enables you to: 7 | - create an ordered group of docs 8 | - render a sidebar for each doc of that group 9 | - provide next/previous navigation 10 | 11 | The sidebars can be generated from the filesystem, or explicitly defined here. 12 | 13 | Create as many sidebars as you want. 14 | */ 15 | 16 | module.exports = { 17 | // By default, Docusaurus generates a sidebar from the docs folder structure 18 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 19 | 20 | // But you can create a sidebar manually 21 | /* 22 | tutorialSidebar: [ 23 | { 24 | type: 'category', 25 | label: 'Tutorial', 26 | items: ['hello'], 27 | }, 28 | ], 29 | */ 30 | }; 31 | -------------------------------------------------------------------------------- /python/src/dotslash/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Meta Platforms, Inc. and affiliates. 2 | # 3 | # This source code is dual-licensed under either the MIT license found in the 4 | # LICENSE-MIT file in the root directory of this source tree or the Apache 5 | # License, Version 2.0 found in the LICENSE-APACHE file in the root directory 6 | # of this source tree. You may select, at your option, one of the 7 | # above-listed licenses. 8 | 9 | if __name__ == "__main__": 10 | import sys 11 | 12 | from dotslash import locate 13 | 14 | dotslash = locate() 15 | 16 | if sys.platform == "win32": 17 | import subprocess 18 | 19 | process = subprocess.run([dotslash, *sys.argv[1:]]) 20 | sys.exit(process.returncode) 21 | else: 22 | import os 23 | 24 | os.execvp(dotslash, [dotslash, *sys.argv[1:]]) 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: cargo build/test/clippy 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | # macos-13 appears to be x86 while macos-latest is arm64 13 | # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners 14 | os: [ubuntu-latest, windows-latest, windows-11-arm, macos-13, macos-latest] 15 | runs-on: ${{ matrix.os }} 16 | timeout-minutes: 20 17 | steps: 18 | - uses: actions/checkout@v6 19 | - uses: dtolnay/rust-toolchain@stable 20 | with: 21 | components: clippy 22 | - run: cargo build 23 | - run: cargo build --release 24 | - run: cargo test 25 | - run: cargo clippy 26 | -------------------------------------------------------------------------------- /.github/workflows/release-feature.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release DotSlash Dev Container Feature 3 | 4 | on: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | deploy: 9 | if: ${{ github.ref == 'refs/heads/main' }} 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | packages: write 14 | pull-requests: write 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | - name: Publish DotSlash Feature 18 | uses: devcontainers/action@1082abd5d2bf3a11abccba70eef98df068277772 # v1.4.3 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | base-path-to-features: ./devcontainer-features/src 23 | features-namespace: ${{ github.repository_owner }}/devcontainers/features 24 | publish-features: "true" 25 | -------------------------------------------------------------------------------- /python/tests/test_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Meta Platforms, Inc. and affiliates. 2 | # 3 | # This source code is dual-licensed under either the MIT license found in the 4 | # LICENSE-MIT file in the root directory of this source tree or the Apache 5 | # License, Version 2.0 found in the LICENSE-APACHE file in the root directory 6 | # of this source tree. You may select, at your option, one of the 7 | # above-listed licenses. 8 | 9 | from __future__ import annotations 10 | 11 | import os 12 | import sys 13 | 14 | import dotslash 15 | 16 | 17 | def test_locate() -> None: 18 | path = dotslash.locate() 19 | assert isinstance(path, str) 20 | assert os.path.isabs(path) 21 | assert os.path.isfile(path) 22 | 23 | stem, extension = os.path.splitext(os.path.basename(path)) 24 | assert stem == "dotslash" 25 | 26 | expected_extension = ".exe" if sys.platform == "win32" else "" 27 | assert extension == expected_extension 28 | -------------------------------------------------------------------------------- /src/util/update_mtime.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use std::io; 12 | use std::path::Path; 13 | 14 | use filetime::FileTime; 15 | 16 | /// Update the file/directory mtime to now. 17 | /// 18 | /// DotSlash can unpack old artifacts which can be reaped by tools like 19 | /// tmpwatch or tmpreaper. Those tools work better using the mtime rather than 20 | /// atime which is why we update the mtime. But this doesn't work on 21 | /// Windows sometimes. 22 | pub fn update_mtime(path: &Path) -> io::Result<()> { 23 | filetime::set_file_mtime(path, FileTime::now()) 24 | } 25 | -------------------------------------------------------------------------------- /python/tests/test_cli.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Meta Platforms, Inc. and affiliates. 2 | # 3 | # This source code is dual-licensed under either the MIT license found in the 4 | # LICENSE-MIT file in the root directory of this source tree or the Apache 5 | # License, Version 2.0 found in the LICENSE-APACHE file in the root directory 6 | # of this source tree. You may select, at your option, one of the 7 | # above-listed licenses. 8 | 9 | from __future__ import annotations 10 | 11 | import subprocess 12 | import sys 13 | from importlib import metadata 14 | 15 | 16 | def test_cli() -> None: 17 | result = subprocess.run( 18 | [sys.executable, "-m", "dotslash", "--version"], 19 | capture_output=True, 20 | encoding="utf-8", 21 | ) 22 | output = result.stdout.strip() 23 | assert result.returncode == 0, output 24 | 25 | name, version = output.split() 26 | assert name == "DotSlash" 27 | assert version == metadata.version("dotslash") 28 | -------------------------------------------------------------------------------- /website/static/img/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /website/assets/Final/SVG/DotSlash_Final_symbol_rayscale.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /website/assets/Final/SVG/DotSlash_Final_symbol_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /website/assets/Final/SVG/DotSlash_Final_symbol_full-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/test-python.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test Python 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 14 | cancel-in-progress: true 15 | 16 | permissions: 17 | contents: read 18 | 19 | defaults: 20 | run: 21 | shell: bash 22 | working-directory: python 23 | 24 | jobs: 25 | test: 26 | name: Test 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 32 | 33 | - name: Install UV 34 | uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 35 | 36 | - uses: taiki-e/install-action@ebb229c6baa68383264f2822689b07b4916d9177 # v2.62.36 37 | with: 38 | tool: just 39 | 40 | - name: Static analysis 41 | run: just check-python 42 | 43 | - name: Test 44 | run: just test-python 45 | -------------------------------------------------------------------------------- /website/static/img/favicon-on-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/snapshots/http__dummy_values.out: -------------------------------------------------------------------------------- 1 | {"name":"my_bin","platforms":{"linux-aarch64":{"digest":"1234567890123456789012345678901234567890123456789012345678901234","format":"tar.gz","hash":"sha256","path":"linux.aarch64","providers":[{"url":"https://fake/foo"}],"size":123},"linux-x86_64":{"digest":"1234567890123456789012345678901234567890123456789012345678901234","format":"tar.gz","hash":"sha256","path":"linux.x86_64","providers":[{"url":"https://fake/foo"}],"size":123},"macos-aarch64":{"digest":"1234567890123456789012345678901234567890123456789012345678901234","format":"tar.gz","hash":"sha256","path":"macos.aarch64","providers":[{"url":"https://fake/foo"}],"size":123},"macos-x86_64":{"digest":"1234567890123456789012345678901234567890123456789012345678901234","format":"tar.gz","hash":"sha256","path":"macos.x86_64","providers":[{"url":"https://fake/foo"}],"size":123},"windows-x86_64":{"digest":"1234567890123456789012345678901234567890123456789012345678901234","format":"tar.gz","hash":"sha256","path":"windows.x86_64.exe","providers":[{"url":"https://fake/foo"}],"size":123}}} 2 | -------------------------------------------------------------------------------- /website/assets/Final/SVG/DotSlash_Final_symbol_full-color-on-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /node/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fb-dotslash", 3 | "version": "0.0.0-dev", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "fb-dotslash", 9 | "version": "0.0.0-dev", 10 | "license": "(MIT OR Apache-2.0)", 11 | "bin": { 12 | "dotslash": "bin/dotslash" 13 | }, 14 | "devDependencies": { 15 | "prettier": "3.6.2" 16 | }, 17 | "engines": { 18 | "node": ">=20" 19 | } 20 | }, 21 | "node_modules/prettier": { 22 | "version": "3.6.2", 23 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", 24 | "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", 25 | "dev": true, 26 | "license": "MIT", 27 | "bin": { 28 | "prettier": "bin/prettier.cjs" 29 | }, 30 | "engines": { 31 | "node": ">=14" 32 | }, 33 | "funding": { 34 | "url": "https://github.com/prettier/prettier?sponsor=1" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /windows_shim/Cargo.toml: -------------------------------------------------------------------------------- 1 | # @generated by autocargo from //scm/dotslash/oss/windows_shim:dotslash_windows_shim 2 | 3 | [package] 4 | name = "dotslash_windows_shim" 5 | version = "0.0.0" 6 | edition = "2024" 7 | homepage = "https://dotslash-cli.com" 8 | repository = "https://github.com/facebook/dotslash" 9 | license = "MIT OR Apache-2.0" 10 | publish = false 11 | 12 | [[bin]] 13 | name = "dotslash_windows_shim" 14 | path = "dotslash_windows_shim.rs" 15 | test = false 16 | 17 | [dependencies] 18 | cfg-if = "1.0.1" 19 | windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Security", "Win32_Security_Credentials", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_Environment", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Threading", "Win32_UI_Shell"] } 20 | 21 | [features] 22 | default = ["no_std"] 23 | no_std = [] 24 | 25 | [profile.release] 26 | opt-level = "z" 27 | lto = true 28 | codegen-units = 1 29 | panic = "abort" 30 | 31 | [profile.dev] 32 | opt-level = 1 33 | lto = true 34 | codegen-units = 1 35 | panic = "abort" 36 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | mod artifact_location; 12 | mod artifact_path; 13 | mod config; 14 | mod curl; 15 | mod default_provider_factory; 16 | mod digest; 17 | mod dotslash_cache; 18 | mod download; 19 | mod execution; 20 | mod fetch_method; 21 | mod github_release_provider; 22 | mod http_provider; 23 | mod locate; 24 | mod platform; 25 | mod print_entry_for_url; 26 | mod provider; 27 | mod s3_provider; 28 | mod subcommand; 29 | mod util; 30 | 31 | use std::env; 32 | use std::process::ExitCode; 33 | 34 | use crate::default_provider_factory::DefaultProviderFactory; 35 | 36 | fn main() -> ExitCode { 37 | let args = env::args_os(); 38 | let provider_factory = DefaultProviderFactory {}; 39 | execution::run(args, &provider_factory) 40 | } 41 | -------------------------------------------------------------------------------- /tests/common/ci.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use std::env; 12 | use std::ffi::OsStr; 13 | use std::io; 14 | use std::iter; 15 | use std::path::Path; 16 | use std::path::PathBuf; 17 | 18 | use snapbox::Data; 19 | use snapbox::data::DataFormat; 20 | 21 | pub fn current_dir() -> io::Result { 22 | env::current_dir() 23 | } 24 | 25 | pub fn dotslash_bin() -> PathBuf { 26 | env!("CARGO_BIN_EXE_dotslash").into() 27 | } 28 | 29 | pub fn envs() -> impl IntoIterator, impl AsRef)> { 30 | iter::empty::<(&str, &str)>() 31 | } 32 | 33 | pub fn snapshot_file(name: &str) -> Data { 34 | let path = Path::new(".").join("tests").join("snapshots").join(name); 35 | Data::read_from(&path, Some(DataFormat::Text)) 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Meta Platforms, Inc. and affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fb-dotslash", 3 | "version": "0.0.0-dev", 4 | "bin": { 5 | "dotslash": "bin/dotslash" 6 | }, 7 | "description": "Command-line tool to facilitate fetching an executable, caching it, and then running it.", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/facebook/dotslash.git" 11 | }, 12 | "homepage": "https://dotslash-cli.com/", 13 | "bugs": "https://github.com/facebook/dotslash/issues", 14 | "contributors": [ 15 | "Michael Bolin ", 16 | "Andres Suarez ", 17 | "Moti Zilberman " 18 | ], 19 | "main": "index.js", 20 | "files": [ 21 | "bin", 22 | "platforms.js", 23 | "index.js", 24 | "index.js.flow", 25 | "index.d.ts" 26 | ], 27 | "scripts": { 28 | "clean": "node scripts/clean-package", 29 | "build": "npm run fix && node scripts/build-package", 30 | "fix": "prettier --write .", 31 | "lint": "prettier --check ." 32 | }, 33 | "license": "(MIT OR Apache-2.0)", 34 | "engines": { 35 | "node": ">=20" 36 | }, 37 | "devDependencies": { 38 | "prettier": "3.6.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/default_provider_factory.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use crate::github_release_provider::GitHubReleaseProvider; 12 | use crate::http_provider::HttpProvider; 13 | use crate::provider::Provider; 14 | use crate::provider::ProviderFactory; 15 | use crate::s3_provider::S3Provider; 16 | 17 | pub struct DefaultProviderFactory; 18 | 19 | impl ProviderFactory for DefaultProviderFactory { 20 | fn get_provider(&self, provider_type: &str) -> anyhow::Result> { 21 | match provider_type { 22 | "http" => Ok(Box::new(HttpProvider {})), 23 | "github-release" => Ok(Box::new(GitHubReleaseProvider {})), 24 | "s3" => Ok(Box::new(S3Provider {})), 25 | _ => Err(anyhow::format_err!( 26 | "unknown provider type: `{provider_type}`", 27 | )), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ubuntu", 3 | "build": { 4 | "cacheFrom": "ghcr.io/facebook/devcontainers/dotslash", 5 | "dockerfile": "Containerfile" 6 | }, 7 | "customizations": { 8 | "vscode": { 9 | "extensions": [ 10 | "GitHub.vscode-github-actions", 11 | "ms-python.python", 12 | "nefrob.vscode-just-syntax", 13 | "omnilib.ufmt" 14 | ], 15 | "settings": { 16 | "python.analysis.include": [ 17 | "python/**" 18 | ], 19 | "python.defaultInterpreterPath": ".venv/bin/python3", 20 | "python.editor.defaultFormatter": "omnilib.ufmt", 21 | "python.editor.formatOnSave": true, 22 | "python.terminal.activateEnvInCurrentTerminal": true, 23 | "rust-analyzer.linkedProjects": [ 24 | "./Cargo.toml" 25 | ] 26 | } 27 | } 28 | }, 29 | "features": { 30 | "ghcr.io/devcontainers/features/docker-in-docker:2": { 31 | // We do not need this. 32 | "dockerDashComposeVersion": "none", 33 | // Install OSS Moby build instead of Docker CE. 34 | "moby": true 35 | }, 36 | "ghcr.io/devcontainers/features/rust:1": {}, 37 | "ghcr.io/devcontainers-extra/features/devcontainers-cli:1": {}, 38 | "ghcr.io/guiyomh/features/just:0": {} 39 | }, 40 | "postCreateCommand": { 41 | "installPythonDependencies": "cd /workspaces/dotslash/python && uv venv --no-project && uv pip install -r requirements-fmt.txt" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /node/platforms.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | module.exports = { 12 | // Keep in sync with .github/workflows/release.yml - the 'npm-publish' job's dependencies 13 | // MUST include the build job for each artifact listed below. 14 | artifactsByPlatformAndArch: { 15 | linux: { 16 | arm64: { 17 | // Build job: 'linux-musl-arm64' 18 | slug: 'linux-musl.aarch64', 19 | binary: 'dotslash', 20 | }, 21 | x64: { 22 | // Build job: 'linux-musl-x86_64' 23 | slug: 'linux-musl.x86_64', 24 | binary: 'dotslash', 25 | }, 26 | }, 27 | darwin: { 28 | '*': { 29 | // Build job: 'macos' 30 | slug: 'macos', 31 | binary: 'dotslash', 32 | }, 33 | }, 34 | win32: { 35 | arm64: { 36 | // Build job: 'windows-arm64' 37 | slug: 'windows-arm64', 38 | binary: 'dotslash.exe', 39 | }, 40 | x64: { 41 | // Build job: 'windows' 42 | slug: 'windows', 43 | binary: 'dotslash.exe', 44 | }, 45 | }, 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | #[cfg(unix)] 12 | mod chmodx; 13 | mod display; 14 | mod execv; 15 | mod file_lock; 16 | pub mod fs_ctx; 17 | mod http_status; 18 | mod is_not_found_error; 19 | #[cfg(unix)] 20 | mod is_path_safe_to_own; 21 | mod mv_no_clobber; 22 | mod progress; 23 | mod tree_perms; 24 | pub mod unarchive; 25 | mod update_mtime; 26 | 27 | #[cfg(unix)] 28 | pub use self::chmodx::chmodx; 29 | pub use self::display::CommandDisplay; 30 | pub use self::display::CommandStderrDisplay; 31 | pub use self::display::ListOf; 32 | pub use self::execv::execv; 33 | pub use self::file_lock::FileLock; 34 | pub use self::file_lock::FileLockError; 35 | pub use self::http_status::HttpStatus; 36 | pub use self::is_not_found_error::is_not_found_error; 37 | #[cfg(unix)] 38 | pub use self::is_path_safe_to_own::is_path_safe_to_own; 39 | pub use self::mv_no_clobber::mv_no_clobber; 40 | pub use self::progress::display_progress; 41 | pub use self::tree_perms::make_tree_entries_read_only; 42 | pub use self::tree_perms::make_tree_entries_writable; 43 | pub use self::update_mtime::update_mtime; 44 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dotslash-website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "clean": "docusaurus clear", 13 | "serve": "docusaurus serve", 14 | "write-translations": "docusaurus write-translations", 15 | "write-heading-ids": "docusaurus write-heading-ids" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "^2.1.0", 19 | "@docusaurus/preset-classic": "^2.1.0", 20 | "@docusaurus/theme-mermaid": "^3.0.1", 21 | "@mdx-js/react": "^1.6.21", 22 | "clsx": "^1.1.1", 23 | "docusaurus-plugin-internaldocs-fb": "1.8.0", 24 | "prism-react-renderer": "^1.3.3", 25 | "react": "^17.0.2", 26 | "react-dom": "^17.0.2" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.5%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "engines": { 41 | "node": ">=16", 42 | "npm": "use yarn instead", 43 | "yarn": "^1.5" 44 | }, 45 | "devDependencies": { 46 | "yarn-audit-fix": "^9.3.10" 47 | }, 48 | "resolutions": { 49 | "**/image-size": "^1.2.1", 50 | "**/http-proxy-middleware": "^2.0.9", 51 | "**/dompurify": "^3.2.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /website/src/pages/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 3 | */ 4 | 5 | import React from 'react'; 6 | import clsx from 'clsx'; 7 | import Layout from '@theme/Layout'; 8 | import Link from '@docusaurus/Link'; 9 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 10 | import styles from './index.module.css'; 11 | import HomepageFeatures from '../components/HomepageFeatures'; 12 | 13 | function HomepageHeader() { 14 | const { siteConfig } = useDocusaurusContext(); 15 | return ( 16 |
17 |
18 | DotSlash logo 23 |

{siteConfig.tagline}

24 |
25 | 28 | Introduction to DotSlash 29 | 30 |
31 |
32 |
33 | ); 34 | } 35 | 36 | export default function Home() { 37 | const { siteConfig } = useDocusaurusContext(); 38 | return ( 39 | 41 | 42 |
43 | 44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 3 | */ 4 | 5 | /** 6 | * Any CSS included here will be global. The classic template 7 | * bundles Infima by default. Infima is a CSS framework designed to 8 | * work well for content-centric websites. 9 | */ 10 | 11 | /* You can override the default Infima variables here. */ 12 | :root { 13 | /* Overrides introduced to match logo. */ 14 | --ifm-hero-background-color: #343434; 15 | 16 | /* Original overrides from Docusaurus template */ 17 | --ifm-color-primary-dark: rgb(33, 175, 144); 18 | --ifm-color-primary-darker: rgb(31, 165, 136); 19 | --ifm-color-primary-darkest: rgb(26, 136, 112); 20 | --ifm-color-primary-light: rgb(70, 203, 174); 21 | --ifm-color-primary-lighter: rgb(102, 212, 189); 22 | --ifm-color-primary-lightest: rgb(146, 224, 208); 23 | --ifm-code-font-size: 95%; 24 | } 25 | 26 | .table-of-contents__link:hover { 27 | color: var(--ifm-link-color); 28 | } 29 | 30 | .docusaurus-highlight-code-line { 31 | background-color: rgba(0, 0, 0, 0.1); 32 | display: block; 33 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 34 | padding: 0 var(--ifm-pre-padding); 35 | } 36 | 37 | :root[data-theme='light'] { 38 | --ifm-color-primary: #343434; 39 | --ifm-link-color: #14C7B8; 40 | --ifm-toc-link-hover-color: #14C7B8; 41 | --ifm-navbar-link-hover-color: #14C7B8; 42 | } 43 | 44 | .hero--primary { 45 | --ifm-hero-background-color: #343434; 46 | --ifm-hero-text-color: #fff; 47 | } 48 | 49 | html[data-theme='dark'] .docusaurus-highlight-code-line { 50 | background-color: rgba(0, 0, 0, 0.3); 51 | } 52 | -------------------------------------------------------------------------------- /windows_shim/README.md: -------------------------------------------------------------------------------- 1 | # DotSlash Windows Shim 2 | 3 | The _DotSlash Windows Shim_ aims to workaround the fact that Windows does not 4 | support [shebangs]() and depends 5 | on a file's extension to determine if it is executable. 6 | 7 | ## How to use it 8 | 9 | Place the _DotSlash Windows Shim_ executable next to a DotSlash file with the 10 | same file name as the DotSlash file plus the `.exe` extension. For example, if 11 | the DotSlash file is named `node`, copy the shim executable as `node.exe` into 12 | the same directory as `node`. When `node.exe` is run, it will run `dotslash` 13 | with the sibiling DotSlash file, and forward all arguments and IO streams. 14 | 15 | ## How it works 16 | 17 | The _DotSlash Windows Shim_ does this: 18 | 19 | - Gets it own executable name (e.g. `C:\dir\node.exe`) and removes the extention 20 | (e.g. `C:\dir\node`). 21 | - It takes this path, plus whatever arguments were passed, and runs 22 | `dotslash C:\dir\node arg1 arg2 ...`. 23 | - Waits to exit and forwards the exit code. 24 | 25 | ## Binary size 26 | 27 | _DotSlash Windows Shim_ builds without a standard library and only uses Windows 28 | APIs. Release binaries are around ~5KB. 29 | 30 | ## Release 31 | 32 | ```shell 33 | py release.py 34 | ``` 35 | 36 | ## Testing 37 | 38 | ```shell 39 | py run_tests.py 40 | ``` 41 | 42 | ## Debugging 43 | 44 | It may be useful to have the standard library (e.g. `dbg!`) when debugging. 45 | Build with `--no-default-features` (avoids the default `no_std` feature) to have 46 | access to the standard library. 47 | 48 | ```shell 49 | cargo build --no-default-features 50 | ``` 51 | -------------------------------------------------------------------------------- /.github/workflows/publish-website.yml: -------------------------------------------------------------------------------- 1 | name: Publish https://dotslash-cli.com 2 | 3 | on: 4 | # Whenever something under website/ changes, publish a new version of the site. 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - 'website/**' 10 | 11 | # Though if the build process for the site evolves to include steps that rely 12 | # on paths outside website/, it is helpful to have additional triggers 13 | # available. For example, allow ad-hoc pushes to rebuild immediately if the 14 | # push-based trigger missed something: 15 | workflow_dispatch: 16 | 17 | # Also push every weekday at midnight to ensure changes make it out in in a 18 | # timely manner. If the first trigger is doing its job, this should be a noop. 19 | schedule: 20 | - cron: '0 0 * * 1-5' 21 | 22 | jobs: 23 | deploy: 24 | runs-on: ubuntu-22.04 25 | permissions: 26 | contents: write 27 | concurrency: 28 | group: ${{ github.workflow }} 29 | defaults: 30 | run: 31 | working-directory: website 32 | steps: 33 | - uses: actions/checkout@v6 34 | - name: Setup Node 35 | uses: actions/setup-node@v6 36 | with: 37 | node-version: 18 38 | cache: yarn 39 | cache-dependency-path: ./website 40 | - name: Install dependencies 41 | run: yarn install --frozen-lockfile 42 | - name: Build website 43 | run: yarn build 44 | - name: Deploy 45 | uses: peaceiris/actions-gh-pages@v4 46 | if: ${{ github.ref == 'refs/heads/main' }} 47 | with: 48 | github_token: ${{ secrets.GITHUB_TOKEN }} 49 | publish_dir: ./website/build 50 | -------------------------------------------------------------------------------- /src/util/is_path_safe_to_own.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use std::io; 12 | use std::os::unix::fs::MetadataExt as _; 13 | use std::path::Path; 14 | 15 | use nix::unistd; 16 | 17 | use crate::util; 18 | 19 | /// A path is considered "safe to own" if: 20 | /// (1) it exists and we own it, or, 21 | /// (2) it doesn't exist and we own the nearest parent that does exist. 22 | #[must_use] 23 | pub fn is_path_safe_to_own(path: &Path) -> bool { 24 | for ancestor in path.ancestors() { 25 | // Use `symlink_metadata` and not `metadata` because we're not 26 | // interested in following symlinks. If the path is a broken 27 | // symlink we want to still check the owner on that, instead of 28 | // treating it like a "NotFound". 29 | match ancestor.symlink_metadata() { 30 | Ok(meta) => { 31 | return unistd::getuid().as_raw() == meta.uid(); 32 | } 33 | Err(ref e) if util::is_not_found_error(e) => (), 34 | Err(ref e) if e.kind() == io::ErrorKind::PermissionDenied => { 35 | return false; 36 | } 37 | // Not sure how this can happen. 38 | Err(_) => { 39 | return false; 40 | } 41 | } 42 | } 43 | 44 | false 45 | } 46 | -------------------------------------------------------------------------------- /node/scripts/clean-package.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Meta Platforms, Inc. and affiliates. 4 | * 5 | * This source code is dual-licensed under either the MIT license found in the 6 | * LICENSE-MIT file in the root directory of this source tree or the Apache 7 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 8 | * of this source tree. You may select, at your option, one of the 9 | * above-listed licenses. 10 | */ 11 | 12 | 'use strict'; 13 | 14 | const { promises: fs } = require('fs'); 15 | const path = require('path'); 16 | 17 | const PACKAGE_JSON_PATH = path.join(__dirname, '..', 'package.json'); 18 | const BIN_PATH = path.join(__dirname, '..', 'bin'); 19 | const DEFAULT_VERSION = '0.0.0-dev'; 20 | 21 | async function deleteOldBinaries() { 22 | const entries = await fs.readdir(BIN_PATH, { withFileTypes: true }); 23 | for (const entry of entries) { 24 | if (!entry.isDirectory()) { 25 | continue; 26 | } 27 | await fs.rm(path.join(BIN_PATH, entry.name), { 28 | recursive: true, 29 | force: true, 30 | }); 31 | } 32 | } 33 | 34 | async function cleanPackageJson() { 35 | const packageJson = await fs.readFile(PACKAGE_JSON_PATH, 'utf8'); 36 | const packageJsonObj = JSON.parse(packageJson); 37 | packageJsonObj.version = DEFAULT_VERSION; 38 | await fs.writeFile( 39 | PACKAGE_JSON_PATH, 40 | JSON.stringify(packageJsonObj, null, 2) + '\n', 41 | ); 42 | } 43 | 44 | async function main() { 45 | await deleteOldBinaries(); 46 | await cleanPackageJson(); 47 | } 48 | 49 | module.exports = { deleteOldBinaries }; 50 | 51 | if (require.main === module) { 52 | main().catch((err) => { 53 | console.error(err); 54 | process.exitCode = 1; 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /devcontainer-features/test/dotslash/install_buck.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | # 4 | # This source code is dual-licensed under either the MIT license found in the 5 | # LICENSE-MIT file in the root directory of this source tree or the Apache 6 | # License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | # of this source tree. You may select, at your option, one of the 8 | # above-listed licenses. 9 | 10 | set -e 11 | 12 | # shellcheck source=/dev/null 13 | source dev-container-features-test-lib 14 | 15 | echo "#!/usr/bin/env dotslash 16 | 17 | { 18 | \"name\": \"buck2\", 19 | \"platforms\": { 20 | \"linux-aarch64\": { 21 | \"size\": 30289600, 22 | \"hash\": \"blake3\", 23 | \"digest\": \"bbb4d04da8deca8a197bffd9cf60b6057e4765a32d01dd28d495f5571dbdc96b\", 24 | \"format\": \"zst\", 25 | \"path\": \"buck2-aarch64-unknown-linux-musl\", 26 | \"providers\": [ 27 | { 28 | \"url\": \"https://github.com/facebook/buck2/releases/download/2025-05-06/buck2-aarch64-unknown-linux-musl.zst\" 29 | } 30 | ] 31 | }, 32 | \"linux-x86_64\": { 33 | \"size\": 31572599, 34 | \"hash\": \"blake3\", 35 | \"digest\": \"1499fa841ba87adb5cceaf3b4680db1db79967a14470bd40a344788d03e75082\", 36 | \"format\": \"zst\", 37 | \"path\": \"buck2-x86_64-unknown-linux-musl\", 38 | \"providers\": [ 39 | { 40 | \"url\": \"https://github.com/facebook/buck2/releases/download/2025-05-06/buck2-x86_64-unknown-linux-musl.zst\" 41 | } 42 | ] 43 | } 44 | } 45 | }" > buck2 46 | chmod +x buck2 47 | 48 | touch .buckconfig 49 | 50 | check "ensure buck2 is runnable" ./buck2 --help 51 | 52 | reportResults 53 | -------------------------------------------------------------------------------- /python/src/dotslash/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Meta Platforms, Inc. and affiliates. 2 | # 3 | # This source code is dual-licensed under either the MIT license found in the 4 | # LICENSE-MIT file in the root directory of this source tree or the Apache 5 | # License, Version 2.0 found in the LICENSE-APACHE file in the root directory 6 | # of this source tree. You may select, at your option, one of the 7 | # above-listed licenses. 8 | 9 | from __future__ import annotations 10 | 11 | 12 | def locate() -> str: 13 | """ 14 | Returns: 15 | The path to the DotSlash binary that was installed by this package. 16 | """ 17 | import os 18 | import sys 19 | import sysconfig 20 | 21 | from dotslash._locate import _search_paths 22 | 23 | if extension := sysconfig.get_config_var("EXE"): 24 | binary_name = f"dotslash{extension}" 25 | elif sys.platform == "win32": 26 | binary_name = "dotslash.exe" 27 | else: 28 | binary_name = "dotslash" 29 | 30 | # Keep an insertion-ordered map of normalized paths to the actual path so 31 | # that potential error messages are deterministic. 32 | seen_paths: dict[str, str] = {} 33 | for search_path in _search_paths(): 34 | normalized_path = os.path.normcase(search_path) 35 | if normalized_path in seen_paths: 36 | continue 37 | 38 | seen_paths[normalized_path] = search_path 39 | binary_path = os.path.join(search_path, binary_name) 40 | if os.path.isfile(binary_path): 41 | return binary_path 42 | 43 | search_paths = "\n".join(f"- {search_path}" for search_path in seen_paths.values()) 44 | msg = f"The `{binary_name}` binary was not found in any of the following paths:\n{search_paths}" 45 | raise FileNotFoundError(msg) 46 | -------------------------------------------------------------------------------- /website/docs/execution.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 20 3 | --- 4 | 5 | # How DotSlash Works 6 | 7 | Here is a flowchart that demonstrates what happens when a user invokes 8 | `./scripts/node --version` on the command line when `./scripts/node` is a 9 | DotSlash file. Note this is what happens on Mac and Linux, as the behavior on 10 | [Windows](../windows) is slightly different. 11 | 12 | ```mermaid 13 | flowchart TD 14 | USER_INVOCATION(./scripts/node --version) -->SHEBANG_EXPANSION 15 | SHEBANG_EXPANSION(/usr/local/bin/dotslash ./scripts/node --version) -->|DotSlash parses ./scripts/node to build the exec invocation| EXEC 16 | EXEC{{exec $DOTSLASH_CACHE/fe/40b2ce9a.../node --version}} -->|exec fails with ENOENT because
the artifact is not in the cache| ACQUIRE_LOCK 17 | EXEC -->|artifact in cache| EXEC_SUCCEEDS 18 | EXEC_SUCCEEDS(exec replaces dotslash process) 19 | ACQUIRE_LOCK(acquire file lock for artifact) -->F 20 | F{{check if artifact exists in cache}} -->|No| FETCH 21 | F -->|Yes: the artifact must have
been fetched by another caller
while we were waiting
to acquire the lock| RELEASE_LOCK 22 | FETCH(fetch artifact using providers
in the DotSlash file) --> ON_FETCH 23 | ON_FETCH(verify artifact size and hash) --> DECOMPRESS 24 | DECOMPRESS(decompress artifact in temp directory) --> SANITIZE 25 | SANITIZE(sanitize temp directory) --> RENAME 26 | RENAME(mv temp directory to final destination) --> RELEASE_LOCK 27 | RELEASE_LOCK(release file lock) --> EXEC_TAKE2 28 | EXEC_TAKE2(exec $DOTSLASH_CACHE/fe/40b2ce9a.../node --version) 29 | EXEC_TAKE2 --> EXEC_SUCCEEDS 30 | ``` 31 | -------------------------------------------------------------------------------- /src/http_provider.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use std::path::Path; 12 | 13 | use anyhow::Context as _; 14 | use serde::Deserialize; 15 | use serde_json::Value; 16 | 17 | use crate::config::ArtifactEntry; 18 | use crate::curl::CurlCommand; 19 | use crate::curl::FetchContext; 20 | use crate::provider::Provider; 21 | use crate::util::FileLock; 22 | 23 | pub struct HttpProvider {} 24 | 25 | #[derive(Deserialize, Debug)] 26 | struct HttpProviderConfig { 27 | url: String, 28 | } 29 | 30 | impl Provider for HttpProvider { 31 | fn fetch_artifact( 32 | &self, 33 | provider_config: &Value, 34 | destination: &Path, 35 | _fetch_lock: &FileLock, 36 | artifact_entry: &ArtifactEntry, 37 | ) -> anyhow::Result<()> { 38 | let HttpProviderConfig { url } = <_>::deserialize(provider_config)?; 39 | let curl_cmd = CurlCommand::new(url.as_ref()); 40 | // Currently, we always disable the progress bar, but we plan to add a 41 | // configuration option to enable it. 42 | let show_progress = false; 43 | let fetch_context = FetchContext { 44 | artifact_name: url.as_str(), 45 | content_length: artifact_entry.size, 46 | show_progress, 47 | }; 48 | curl_cmd 49 | .get_request(destination, &fetch_context) 50 | .with_context(|| format!("failed to fetch `{}`", url))?; 51 | Ok(()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/platform.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | macro_rules! if_platform { 12 | ( 13 | linux_aarch64 = $linux_aarch64:tt, 14 | linux_x86_64 = $linux_x86_64:tt, 15 | macos_aarch64 = $macos_aarch64:tt, 16 | macos_x86_64 = $macos_x86_64:tt, 17 | windows_aarch64 = $windows_aarch64:tt, 18 | windows_x86_64 = $windows_x86_64:tt, 19 | ) => { 20 | if cfg!(all(target_os = "linux", target_arch = "aarch64")) { 21 | $linux_aarch64 22 | } else if cfg!(all(target_os = "linux", target_arch = "x86_64")) { 23 | $linux_x86_64 24 | } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) { 25 | $macos_aarch64 26 | } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) { 27 | $macos_x86_64 28 | } else if cfg!(all(target_os = "windows", target_arch = "aarch64")) { 29 | $windows_aarch64 30 | } else if cfg!(all(target_os = "windows", target_arch = "x86_64")) { 31 | $windows_x86_64 32 | } else { 33 | panic!("unknown arch"); 34 | } 35 | }; 36 | } 37 | 38 | pub(crate) use if_platform; 39 | 40 | pub const SUPPORTED_PLATFORM: &str = if_platform! { 41 | linux_aarch64 = "linux-aarch64", 42 | linux_x86_64 = "linux-x86_64", 43 | macos_aarch64 = "macos-aarch64", 44 | macos_x86_64 = "macos-x86_64", 45 | windows_aarch64 = "windows-aarch64", 46 | windows_x86_64 = "windows-x86_64", 47 | }; 48 | -------------------------------------------------------------------------------- /src/util/tree_perms.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use std::io; 12 | use std::path::Path; 13 | 14 | use crate::util::fs_ctx; 15 | 16 | fn make_tree_entries_impl(folder: &Path, read_only: bool) -> io::Result<()> { 17 | for entry in fs_ctx::read_dir(folder)? { 18 | let entry = entry?; 19 | let metadata = fs_ctx::symlink_metadata(entry.path())?; 20 | 21 | if metadata.is_symlink() { 22 | continue; 23 | } 24 | if metadata.is_dir() { 25 | make_tree_entries_impl(&entry.path(), read_only)?; 26 | } 27 | 28 | let mut perms = metadata.permissions(); 29 | perms.set_readonly(read_only); 30 | fs_ctx::set_permissions(entry.path(), perms)?; 31 | } 32 | 33 | Ok(()) 34 | } 35 | 36 | /// Makes all entries within the specified `folder` read-only. 37 | /// 38 | /// Takes the specified `folder` (which must point to a directory) and 39 | /// recursively makes all entries within it read-only, but it does *not* change 40 | /// the permissions on the folder itself. Symlinks are not followed and no 41 | /// attempt is made to change their permissions. 42 | pub fn make_tree_entries_read_only(folder: &Path) -> io::Result<()> { 43 | make_tree_entries_impl(folder, true) 44 | } 45 | 46 | /// Like `make_tree_entries_read_only` but does the reverse - makes the 47 | /// entries writable. 48 | pub fn make_tree_entries_writable(folder: &Path) -> io::Result<()> { 49 | make_tree_entries_impl(folder, false) 50 | } 51 | -------------------------------------------------------------------------------- /src/provider.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use std::path::Path; 12 | 13 | use serde_json::Value; 14 | 15 | use crate::config::ArtifactEntry; 16 | use crate::util::FileLock; 17 | 18 | pub trait Provider { 19 | /// When called, the provider should fetch the artifact as specified by the 20 | /// `provider_config` and write it to `destination`. 21 | /// 22 | /// provider_config: JSON value parsed from the DotSlash file that defines 23 | /// the configuration for this provider. 24 | /// 25 | /// destination: Where the artifact should be written. The caller ensures 26 | /// the parent folder of `destination` exists. 27 | /// 28 | /// fetch_lock: A lock file that should be held while the artifact is being 29 | /// fetched. 30 | /// 31 | /// artifact_entry: In general, the Provider should not rely on the 32 | /// information in the entry to perform the fetch, as such information 33 | /// should be defined in the provider_config. It is primarily provided 34 | /// so the Provider can show an appropriate progess indicator based on 35 | /// the expected size of the artifact. 36 | fn fetch_artifact( 37 | &self, 38 | provider_config: &Value, 39 | destination: &Path, 40 | fetch_lock: &FileLock, 41 | artifact_entry: &ArtifactEntry, 42 | ) -> anyhow::Result<()>; 43 | } 44 | 45 | pub trait ProviderFactory { 46 | fn get_provider(&self, provider_type: &str) -> anyhow::Result>; 47 | } 48 | -------------------------------------------------------------------------------- /tests/fixtures/http__dummy_values.in: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotslash 2 | 3 | // This is a comment. 4 | 5 | { 6 | "name": "my_bin", 7 | "platforms": { 8 | "linux-aarch64": { 9 | "size": 123, 10 | "hash": "sha256", 11 | "digest": "1234567890123456789012345678901234567890123456789012345678901234", 12 | "format": "tar.gz", 13 | "path": "linux.aarch64", 14 | "providers": [ 15 | { 16 | "url": "https://fake/foo" 17 | } 18 | ], 19 | }, 20 | "linux-x86_64": { 21 | "size": 123, 22 | "hash": "sha256", 23 | "digest": "1234567890123456789012345678901234567890123456789012345678901234", 24 | "format": "tar.gz", 25 | "path": "linux.x86_64", 26 | "providers": [ 27 | { 28 | "url": "https://fake/foo" 29 | } 30 | ], 31 | }, 32 | "macos-aarch64": { 33 | "size": 123, 34 | "hash": "sha256", 35 | "digest": "1234567890123456789012345678901234567890123456789012345678901234", 36 | "format": "tar.gz", 37 | "path": "macos.aarch64", 38 | "providers": [ 39 | { 40 | "url": "https://fake/foo" 41 | } 42 | ], 43 | }, 44 | "macos-x86_64": { 45 | "size": 123, 46 | "hash": "sha256", 47 | "digest": "1234567890123456789012345678901234567890123456789012345678901234", 48 | "format": "tar.gz", 49 | "path": "macos.x86_64", 50 | "providers": [ 51 | { 52 | "url": "https://fake/foo" 53 | } 54 | ], 55 | }, 56 | "windows-x86_64": { 57 | "size": 123, 58 | "hash": "sha256", 59 | "digest": "1234567890123456789012345678901234567890123456789012345678901234", 60 | "format": "tar.gz", 61 | "path": "windows.x86_64.exe", 62 | "providers": [ 63 | { 64 | "url": "https://fake/foo" 65 | } 66 | ], 67 | }, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Meta Open Source Projects 2 | 3 | We want to make contributing to this project as easy and transparent as 4 | possible. 5 | 6 | ## Pull Requests 7 | We actively welcome your pull requests. 8 | 9 | Note: pull requests are not imported into the GitHub directory in the usual way. There is an internal Meta repository that is the "source of truth" for the project. The GitHub repository is generated *from* the internal Meta repository. So we don't merge GitHub PRs directly to the GitHub repository -- they must first be imported into internal Meta repository. When Meta employees look at the GitHub PR, there is a special button visible only to them that executes that import. The changes are then automatically reflected from the internal Meta repository back to GitHub. This is why you won't see your PR having being directly merged, but you still see your changes in the repository once it reflects the imported changes. 10 | 11 | 1. Fork the repo and create your branch from `main`. 12 | 2. If you've added code that should be tested, add tests. 13 | 3. If you've changed APIs, update the documentation. 14 | 4. Ensure the test suite passes. 15 | 5. Make sure your code lints. 16 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 17 | 18 | ## Contributor License Agreement ("CLA") 19 | In order to accept your pull request, we need you to submit a CLA. You only need 20 | to do this once to work on any of Meta's open source projects. 21 | 22 | Complete your CLA here: 23 | 24 | ## Issues 25 | We use GitHub issues to track public bugs. Please ensure your description is 26 | clear and has sufficient instructions to be able to reproduce the issue. 27 | 28 | Meta has a [bounty program](https://www.facebook.com/whitehat/) for the safe 29 | disclosure of security bugs. In those cases, please go through the process 30 | outlined on that page and do not file a public issue. 31 | 32 | ## License 33 | By contributing to this project, you agree that your contributions will be licensed 34 | under the LICENSE file in the root directory of this source tree. 35 | -------------------------------------------------------------------------------- /devcontainer-features/src/dotslash/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | # 4 | # This source code is dual-licensed under either the MIT license found in the 5 | # LICENSE-MIT file in the root directory of this source tree or the Apache 6 | # License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | # of this source tree. You may select, at your option, one of the 8 | # above-listed licenses. 9 | 10 | set -o allexport 11 | set -o errexit 12 | set -o noclobber 13 | set -o nounset 14 | set -o pipefail 15 | 16 | ensure_dependencies() { 17 | apt-get update -y 18 | DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends --no-install-suggests \ 19 | ca-certificates \ 20 | curl \ 21 | tar 22 | apt-get clean 23 | rm -rf /var/lib/apt/lists/* 24 | } 25 | 26 | download() { 27 | local version="$1" 28 | local url 29 | if [ "${version}" = "latest" ]; then 30 | url="https://github.com/facebook/dotslash/releases/latest/download/dotslash-linux-musl.$(uname -m).tar.gz" 31 | else 32 | url="https://github.com/facebook/dotslash/releases/download/${version}/dotslash-linux-musl.$(uname -m).tar.gz" 33 | fi 34 | 35 | # First, verify the release exists! 36 | echo "Fetching version ${version} from ${url}..." 37 | local http_status 38 | http_status=$(curl -s -o /dev/null -w '%{http_code}' "${url}") 39 | if [ "${http_status}" -ne 200 ] && [ "${http_status}" -ne 302 ]; then 40 | echo "Failed to download version ${version}! Does it exist?" 41 | return 1 42 | fi 43 | 44 | # Download and untar 45 | echo "Installing dotslash version ${version} to /usr/local/bin..." 46 | curl --silent --location --output '-' "${url}" | tar -xz -f '-' -C /usr/local/bin dotslash 47 | } 48 | 49 | echo "Activating feature 'dotslash' with version ${VERSION}" 50 | 51 | if [ -z "${VERSION}" ]; then 52 | echo "No version specified!" 53 | return 1 54 | fi 55 | 56 | ensure_dependencies 57 | 58 | # Remove any double quotes that might be in the version string. 59 | VERSION="${VERSION//\"/}" 60 | 61 | download "${VERSION}" 62 | -------------------------------------------------------------------------------- /src/locate.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use anyhow::Context as _; 12 | 13 | use crate::artifact_location::ArtifactLocation; 14 | use crate::artifact_location::determine_location; 15 | use crate::config; 16 | use crate::config::ArtifactEntry; 17 | use crate::dotslash_cache::DotslashCache; 18 | use crate::platform::SUPPORTED_PLATFORM; 19 | use crate::util; 20 | use crate::util::ListOf; 21 | 22 | pub fn locate_artifact( 23 | dotslash_data: &str, 24 | dotslash_cache: &DotslashCache, 25 | ) -> anyhow::Result<(ArtifactEntry, ArtifactLocation)> { 26 | let (_original_json, mut config_file) = 27 | config::parse_file(dotslash_data).context("failed to parse DotSlash file")?; 28 | 29 | let (_platform, artifact_entry) = config_file 30 | .platforms 31 | .remove_entry(SUPPORTED_PLATFORM) 32 | .ok_or_else(|| { 33 | anyhow::format_err!( 34 | "expected platform `{}` - but found {}", 35 | SUPPORTED_PLATFORM, 36 | ListOf::new(config_file.platforms.keys()), 37 | ) 38 | }) 39 | .context("platform not supported")?; 40 | 41 | let artifact_location = determine_location(&artifact_entry, dotslash_cache); 42 | 43 | // Update the mtime to work around tmpwatch and tmpreaper behavior 44 | // with old artifacts. 45 | // 46 | // Not on macOS because something (macOS security?) adds a 50-100ms 47 | // delay after modifying the file. 48 | // 49 | // Not on Windows because of "file used by another process" errors. 50 | if cfg!(target_os = "linux") { 51 | let _ = util::update_mtime(&artifact_location.executable); 52 | } 53 | 54 | Ok((artifact_entry, artifact_location)) 55 | } 56 | -------------------------------------------------------------------------------- /windows_shim/release.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | # 4 | # This source code is dual-licensed under either the MIT license found in the 5 | # LICENSE-MIT file in the root directory of this source tree or the Apache 6 | # License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | # of this source tree. You may select, at your option, one of the 8 | # above-listed licenses. 9 | 10 | 11 | import os 12 | import shutil 13 | import subprocess 14 | from pathlib import Path 15 | 16 | IS_WINDOWS: bool = os.name == "nt" 17 | 18 | target_triplets: list[str] = ["x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"] 19 | 20 | 21 | def main() -> None: 22 | if not IS_WINDOWS: 23 | raise Exception("Only Windows is supported.") 24 | 25 | dotslash_windows_shim_root = Path(os.path.realpath(__file__)).parent 26 | 27 | target_dir = ( 28 | Path(os.environ["CARGO_TARGET_DIR"]) 29 | if "CARGO_TARGET_DIR" in os.environ 30 | else None 31 | ) 32 | 33 | for triplet in target_triplets: 34 | subprocess.run( 35 | [ 36 | "cargo", 37 | "build", 38 | "--quiet", 39 | "--manifest-path", 40 | str(dotslash_windows_shim_root / "Cargo.toml"), 41 | "--bin=dotslash_windows_shim", 42 | "--release", 43 | f"--target={triplet}", 44 | ], 45 | check=True, 46 | env={ 47 | **os.environ, 48 | "RUSTC_BOOTSTRAP": "1", 49 | "RUSTFLAGS": "-Clink-arg=/DEBUG:NONE", # Avoid embedded pdb path 50 | }, 51 | ) 52 | 53 | src = ( 54 | (target_dir or (dotslash_windows_shim_root / "target" / triplet)) 55 | / "release" 56 | / "dotslash_windows_shim.exe" 57 | ) 58 | 59 | arch = triplet.partition("-")[0] 60 | 61 | dest = dotslash_windows_shim_root / f"dotslash_windows_shim-{arch}.exe" 62 | 63 | shutil.copy(src, dest) 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures.js: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 3 | */ 4 | 5 | import React from 'react'; 6 | import clsx from 'clsx'; 7 | import styles from './HomepageFeatures.module.css'; 8 | 9 | /** 10 | * Use an empty element until we have imagery to complement the text. Note that 11 | * Buck2 does not use SVGs here, but emoji: 12 | * https://github.com/facebook/buck2/blob/5b0aa923ea621a02331612f7e557d5c946c44561/website/src/components/HomepageFeatures.js#L16 13 | */ 14 | function EmptyElement() { 15 | return <>; 16 | } 17 | 18 | const FeatureList = [ 19 | { 20 | title: 'Simple', 21 | Svg: EmptyElement, 22 | description: ( 23 | <> 24 | DotSlash enables you to replace a set of platform-specific, heavyweight 25 | executables with an equivalent small, easy-to-read text file. 26 | 27 | ), 28 | }, 29 | { 30 | title: 'No Overhead', 31 | Svg: EmptyElement, 32 | description: ( 33 | <> 34 | DotSlash is written in Rust so it can run your executables quickly 35 | and transparently. 36 | 37 | ), 38 | }, 39 | { 40 | title: 'Painless Automation', 41 | Svg: EmptyElement, 42 | description: ( 43 | <> 44 | We provide tools for generating DotSlash 45 | files for GitHub releases. 46 | 47 | ), 48 | }, 49 | ]; 50 | 51 | function Feature({ Svg, title, description }) { 52 | return ( 53 |
54 |
55 | 56 |
57 |
58 |

{title}

59 |

{description}

60 |
61 |
62 | ); 63 | } 64 | 65 | export default function HomepageFeatures() { 66 | return ( 67 |
68 |
69 |
70 | {FeatureList.map((props, idx) => ( 71 | 72 | ))} 73 |
74 |
75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/s3_provider.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use std::path::Path; 12 | 13 | use anyhow::Context as _; 14 | use serde::Deserialize; 15 | use serde_json::Value; 16 | 17 | use crate::config::ArtifactEntry; 18 | use crate::provider::Provider; 19 | use crate::util::CommandDisplay; 20 | use crate::util::CommandStderrDisplay; 21 | use crate::util::FileLock; 22 | 23 | pub struct S3Provider {} 24 | 25 | #[derive(Deserialize, Debug)] 26 | struct S3ProviderConfig { 27 | bucket: String, 28 | key: String, 29 | region: Option, 30 | } 31 | 32 | impl Provider for S3Provider { 33 | fn fetch_artifact( 34 | &self, 35 | provider_config: &Value, 36 | destination: &Path, 37 | _fetch_lock: &FileLock, 38 | _: &ArtifactEntry, 39 | ) -> anyhow::Result<()> { 40 | let S3ProviderConfig { 41 | bucket, 42 | key, 43 | region, 44 | } = <_>::deserialize(provider_config)?; 45 | let mut command = std::process::Command::new("aws"); 46 | command.args(["s3", "cp"]); 47 | if let Some(region) = region { 48 | command.args(["--region", ®ion]); 49 | } 50 | command.arg(format!("s3://{bucket}/{key}")); 51 | command.arg(destination); 52 | let output = command 53 | .output() 54 | .with_context(|| format!("{}", CommandDisplay::new(&command))) 55 | .context("failed to run the AWS CLI")?; 56 | 57 | if !output.status.success() { 58 | return Err(anyhow::format_err!( 59 | "{}", 60 | CommandStderrDisplay::new(&output) 61 | )) 62 | .with_context(|| format!("{}", CommandDisplay::new(&command))) 63 | .context("the AWS CLI failed"); 64 | } 65 | Ok(()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /node/README.md: -------------------------------------------------------------------------------- 1 | # DotSlash: simplified executable deployment 2 | 3 | [DotSlash](https://dotslash-cli.com/docs/) (`dotslash`) is a command-line tool that lets you represent a set of 4 | platform-specific, heavyweight executables with an equivalent small, 5 | easy-to-read text file. In turn, this makes it efficient to store executables in 6 | source control without hurting repository size. This paves the way for checking 7 | build toolchains and other tools directly into the repo, reducing dependencies 8 | on the host environment and thereby facilitating reproducible builds. 9 | 10 | The `fb-dotslash` npm package allows you to use DotSlash in your Node.js projects without having to install DotSlash globally. This is particularly useful for package authors, who have traditionally needed to either include binaries for _all_ platforms or manage their own download and caching in a postinstall script. 11 | 12 | ## Using DotSlash in an npm package 13 | 14 | First, you'll need to write a [DotSlash file](https://dotslash-cli.com/docs/dotslash-file/) that describes the binary you want to distribute. 15 | 16 | If your npm package declares `fb-dotslash` as a dependency, any commands executed as part of `npm run` and `npm exec` will have `dotslash` available on the `PATH`. This means you can, for example, directly reference DotSlash files in your `package.json` scripts with no further setup: 17 | 18 | ```json 19 | { 20 | "name": "my-package", 21 | "scripts": { 22 | "foo": "path/to/dotslash/file" 23 | }, 24 | "dependencies": { 25 | "fb-dotslash": "^0.5.8" 26 | } 27 | } 28 | ``` 29 | 30 | If you need to use `dotslash` in some other context, you can use `require('fb-dotslash')` to get the path to the DotSlash executable appropriate for the current platform: 31 | 32 | ```js 33 | const dotslash = require('fb-dotslash'); 34 | const {spawnSync} = require('child_process'); 35 | spawnSync(dotslash, ['path/to/dotslash/file'], {stdio: 'inherit']); 36 | ``` 37 | 38 | ## License 39 | 40 | DotSlash is licensed under both the MIT license and Apache-2.0 license; the 41 | exact terms can be found in the [LICENSE-MIT](https://github.com/facebook/dotslash/blob/main/LICENSE-MIT) and 42 | [LICENSE-APACHE](https://github.com/facebook/dotslash/blob/main/LICENSE-APACHE) files, respectively. 43 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | set export 2 | 3 | # Configure shell for Windows. 4 | set windows-shell := ["pwsh", "-NoLogo", "-Command"] 5 | 6 | # Lists all targets 7 | [private] 8 | default: 9 | @just --list 10 | 11 | # Run static analysis on all code. 12 | [group('Static Analysis')] 13 | check-all: check-python 14 | 15 | # Fix static analysis issues for all code. 16 | [group('Static Analysis')] 17 | fix-all: fix-python 18 | 19 | # Test dotslash feature with automated test located at devcontainer-features/test/dotslash/test.sh. 20 | [group('Development Container Feature')] 21 | test-feature-autogenerated: 22 | #!/usr/bin/env bash 23 | set -euo pipefail 24 | 25 | base_images=( 26 | debian:latest 27 | mcr.microsoft.com/devcontainers/base:ubuntu 28 | ubuntu:latest 29 | ) 30 | 31 | for base_image in ${base_images[@]}; do 32 | devcontainer features test \ 33 | --base-image debian:latest \ 34 | --features dotslash \ 35 | --project-folder devcontainer-features \ 36 | --skip-scenarios 37 | done 38 | 39 | # Test dotslash feature with scenarios defined in devcontainer-features/test/dotslash/scenarios.json. 40 | [group('Development Container Feature')] 41 | test-feature-scenarios: 42 | #!/usr/bin/env bash 43 | set -euo pipefail 44 | 45 | devcontainer features test \ 46 | --features dotslash \ 47 | --project-folder devcontainer-features \ 48 | --skip-autogenerated \ 49 | --skip-duplicated 50 | 51 | # Run static analysis on the Python package. 52 | [group('Downstream')] 53 | [working-directory: 'python'] 54 | check-python: 55 | uv run --no-project --with-requirements requirements-fmt.txt -- ufmt diff src tests 56 | uv run --no-project --with-requirements requirements-fmt.txt -- ufmt check src tests 57 | 58 | # Fix static analysis issues for the Python package. 59 | [group('Downstream')] 60 | [working-directory: 'python'] 61 | fix-python: 62 | uv run --no-project --with-requirements requirements-fmt.txt -- ufmt format src tests 63 | 64 | # Test the Python package. 65 | [group('Downstream')] 66 | [working-directory: 'python'] 67 | test-python DOTSLASH_VERSION="latest": 68 | uv run --reinstall --isolated --no-editable --with pytest pytest 69 | 70 | # Build the Python distributions. 71 | [group('Downstream')] 72 | [working-directory: 'python'] 73 | build-python DOTSLASH_VERSION="latest": 74 | uv build 75 | -------------------------------------------------------------------------------- /tests/fixtures/http__tar__print_argv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotslash 2 | 3 | { 4 | "name": "print_argv", 5 | "platforms": { 6 | "linux-aarch64": { 7 | "size": 593920, 8 | "hash": "blake3", 9 | "digest": "0c9dc2e64339e620037bc96793752f7d69b060516fa11dee01068c91420657d4", 10 | "format": "tar", 11 | "path": "subdir/print_argv.linux.aarch64", 12 | "providers": [ 13 | { 14 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar" 15 | } 16 | ] 17 | }, 18 | "linux-x86_64": { 19 | "size": 593920, 20 | "hash": "blake3", 21 | "digest": "0c9dc2e64339e620037bc96793752f7d69b060516fa11dee01068c91420657d4", 22 | "format": "tar", 23 | "path": "subdir/print_argv.linux.x86_64", 24 | "providers": [ 25 | { 26 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar" 27 | } 28 | ] 29 | }, 30 | "macos-aarch64": { 31 | "size": 593920, 32 | "hash": "blake3", 33 | "digest": "0c9dc2e64339e620037bc96793752f7d69b060516fa11dee01068c91420657d4", 34 | "format": "tar", 35 | "path": "subdir/print_argv.macos.aarch64", 36 | "providers": [ 37 | { 38 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar" 39 | } 40 | ] 41 | }, 42 | "macos-x86_64": { 43 | "size": 593920, 44 | "hash": "blake3", 45 | "digest": "0c9dc2e64339e620037bc96793752f7d69b060516fa11dee01068c91420657d4", 46 | "format": "tar", 47 | "path": "subdir/print_argv.macos.x86_64", 48 | "providers": [ 49 | { 50 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar" 51 | } 52 | ] 53 | }, 54 | "windows-x86_64": { 55 | "size": 593920, 56 | "hash": "blake3", 57 | "digest": "0c9dc2e64339e620037bc96793752f7d69b060516fa11dee01068c91420657d4", 58 | "format": "tar", 59 | "path": "subdir/print_argv.windows.x86_64.exe", 60 | "providers": [ 61 | { 62 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar" 63 | } 64 | ] 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /python/src/dotslash/_locate.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Meta Platforms, Inc. and affiliates. 2 | # 3 | # This source code is dual-licensed under either the MIT license found in the 4 | # LICENSE-MIT file in the root directory of this source tree or the Apache 5 | # License, Version 2.0 found in the LICENSE-APACHE file in the root directory 6 | # of this source tree. You may select, at your option, one of the 7 | # above-listed licenses. 8 | 9 | from __future__ import annotations 10 | 11 | import os 12 | import sys 13 | import sysconfig 14 | 15 | 16 | def _search_paths(): 17 | # This is the scripts directory for the current Python installation. 18 | yield sysconfig.get_path("scripts") 19 | 20 | # This is the scripts directory for the base prefix only if presently in a virtual environment. 21 | yield sysconfig.get_path("scripts", vars={"base": sys.base_prefix}) 22 | 23 | module_dir = os.path.dirname(os.path.abspath(__file__)) 24 | package_parent, package_name = os.path.split(module_dir) 25 | if package_name == "dotslash": 26 | # Running things like `pip install --prefix` or `uv run --with` will put the scripts directory 27 | # above the package root. Examples: 28 | # - Windows: \Lib\site-packages\dotslash 29 | # - macOS: /lib/pythonX.Y/site-packages/dotslash 30 | # - Linux: 31 | # - /lib/pythonX.Y/site-packages/dotslash 32 | # - /lib/pythonX.Y/dist-packages/dotslash (Debian-based distributions) 33 | head, tail = os.path.split(package_parent) 34 | if tail.endswith("-packages"): 35 | head, tail = os.path.split(head) 36 | if sys.platform == "win32": 37 | if tail == "Lib": 38 | yield os.path.join(head, "Scripts") 39 | elif tail.startswith("python"): 40 | head, tail = os.path.split(head) 41 | if tail == sys.platlibdir: 42 | yield os.path.join(head, "bin") 43 | else: 44 | # Using the `--target` option of pip-like installers will put the scripts directory 45 | # adjacent to the package root in a subdirectory named `bin` regardless of the platform. 46 | yield os.path.join(package_parent, "bin") 47 | 48 | # This is the scripts directory for user installations. 49 | yield sysconfig.get_path("scripts", scheme=sysconfig.get_preferred_scheme("user")) 50 | -------------------------------------------------------------------------------- /src/fetch_method.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use serde::Deserialize; 12 | use serde::Serialize; 13 | 14 | use crate::util::unarchive::ArchiveType; 15 | 16 | #[derive(Deserialize, Serialize, Copy, Clone, Default, Debug)] 17 | #[cfg_attr(test, derive(PartialEq))] 18 | pub enum ArtifactFormat { 19 | /// Artifact is a single file with no compression applied. 20 | #[default] 21 | #[serde(skip)] 22 | Plain, 23 | 24 | #[serde(rename = "bz2")] 25 | Bzip2, 26 | 27 | #[serde(rename = "gz")] 28 | Gz, 29 | 30 | #[serde(rename = "tar")] 31 | Tar, 32 | 33 | #[serde(rename = "tar.bz2")] 34 | TarBzip2, 35 | 36 | #[serde(rename = "tar.gz")] 37 | TarGz, 38 | 39 | #[serde(rename = "tar.zst")] 40 | TarZstd, 41 | 42 | #[serde(rename = "tar.xz")] 43 | TarXz, 44 | 45 | #[serde(rename = "xz")] 46 | Xz, 47 | 48 | #[serde(rename = "zst")] 49 | Zstd, 50 | 51 | #[serde(rename = "zip")] 52 | Zip, 53 | } 54 | 55 | impl ArtifactFormat { 56 | #[must_use] 57 | pub fn as_archive_type(self) -> Option { 58 | match self { 59 | Self::Plain => None, 60 | Self::Bzip2 => Some(ArchiveType::Bzip2), 61 | Self::Gz => Some(ArchiveType::Gz), 62 | Self::Xz => Some(ArchiveType::Xz), 63 | Self::Zstd => Some(ArchiveType::Zstd), 64 | Self::Tar => Some(ArchiveType::Tar), 65 | Self::TarBzip2 => Some(ArchiveType::TarBzip2), 66 | Self::TarGz => Some(ArchiveType::TarGz), 67 | Self::TarXz => Some(ArchiveType::TarXz), 68 | Self::TarZstd => Some(ArchiveType::TarZstd), 69 | Self::Zip => Some(ArchiveType::Zip), 70 | } 71 | } 72 | 73 | #[must_use] 74 | pub fn is_container(self) -> bool { 75 | match self { 76 | Self::Plain | Self::Bzip2 | Self::Gz | Self::Xz | Self::Zstd => false, 77 | Self::Tar | Self::TarBzip2 | Self::TarGz | Self::TarXz | Self::TarZstd | Self::Zip => { 78 | true 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = [ 4 | "hatchling>=1.27.0", 5 | "hatch-fancy-pypi-readme", 6 | ] 7 | 8 | [project] 9 | name = "dotslash" 10 | dynamic = [ 11 | "readme", 12 | "version", 13 | ] 14 | authors = [ 15 | { name = "Ofek Lev", email = "oss@ofek.dev" }, 16 | ] 17 | description = "Command-line tool to facilitate fetching an executable, caching it, and then running it." 18 | keywords = [ 19 | "dotslash", 20 | ] 21 | license = "MIT OR Apache-2.0" 22 | requires-python = ">=3.10" 23 | classifiers = [ 24 | "Development Status :: 5 - Production/Stable", 25 | "Intended Audience :: Developers", 26 | "Natural Language :: English", 27 | "Operating System :: OS Independent", 28 | "Programming Language :: Python :: Implementation :: CPython", 29 | "Programming Language :: Python :: Implementation :: PyPy", 30 | ] 31 | 32 | [project.urls] 33 | Homepage = "https://dotslash-cli.com" 34 | Tracker = "https://github.com/facebook/dotslash/issues" 35 | Source = "https://github.com/facebook/dotslash" 36 | 37 | [tool.black] 38 | target-version = ["py310"] 39 | 40 | [tool.cibuildwheel] 41 | enable = ["pypy"] 42 | test-command = "python -m dotslash -- cache-dir" 43 | # Use UV for build environment creation and installation of build dependencies. 44 | build-frontend = "build[uv]" 45 | # Only build on one version of Python since all distributions of a given platform/arch pair are the same. 46 | build = "cp314-*" 47 | # Disable wheel repair since it's not necessary. 48 | repair-wheel-command = "" 49 | 50 | [tool.cibuildwheel.linux] 51 | environment-pass = ["DOTSLASH_SOURCE", "DOTSLASH_VERSION"] 52 | 53 | [tool.ufmt] 54 | formatter = "ruff-api" 55 | sorter = "ruff-api" 56 | 57 | [tool.usort] 58 | first_party_detection = false 59 | 60 | [tool.hatch.build.targets.sdist.force-include] 61 | "../LICENSE-MIT" = "LICENSE-MIT" 62 | "../LICENSE-APACHE" = "LICENSE-APACHE" 63 | 64 | [tool.hatch.build.targets.wheel.hooks.custom] 65 | 66 | [tool.hatch.metadata.hooks.custom] 67 | 68 | [tool.hatch.metadata.hooks.fancy-pypi-readme] 69 | content-type = "text/markdown" 70 | 71 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] 72 | path = "README.md" 73 | end-before = "## Building from source" 74 | 75 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] 76 | path = "README.md" 77 | start-at = "## License" 78 | 79 | [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] 80 | pattern = "(?m)^- \\[Building from source.+$\\s+-" 81 | replacement = "-" 82 | -------------------------------------------------------------------------------- /windows_shim/run_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | # 4 | # This source code is dual-licensed under either the MIT license found in the 5 | # LICENSE-MIT file in the root directory of this source tree or the Apache 6 | # License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | # of this source tree. You may select, at your option, one of the 8 | # above-listed licenses. 9 | 10 | 11 | import os 12 | import subprocess 13 | import sys 14 | from pathlib import Path 15 | 16 | IS_WINDOWS: bool = os.name == "nt" 17 | 18 | 19 | def main() -> None: 20 | if not IS_WINDOWS: 21 | raise Exception("Only Windows is supported.") 22 | 23 | dotslash_windows_shim_root = Path(os.path.realpath(__file__)).parent 24 | dotslash_root = dotslash_windows_shim_root.parent 25 | 26 | target_dir = ( 27 | Path(os.environ["CARGO_TARGET_DIR"]) 28 | if "CARGO_TARGET_DIR" in os.environ 29 | else None 30 | ) 31 | 32 | if "DOTSLASH_BIN" not in os.environ: 33 | subprocess.run( 34 | [ 35 | "cargo", 36 | "build", 37 | "--quiet", 38 | "--manifest-path", 39 | str(dotslash_root / "Cargo.toml"), 40 | "--bin=dotslash", 41 | "--release", 42 | ], 43 | check=True, 44 | ) 45 | os.environ["DOTSLASH_BIN"] = str( 46 | (target_dir or (dotslash_root / "target")) / "release" / "dotslash.exe" 47 | ) 48 | 49 | if "DOTSLASH_WINDOWS_SHIM" not in os.environ: 50 | subprocess.run( 51 | [ 52 | "cargo", 53 | "build", 54 | "--quiet", 55 | "--manifest-path", 56 | str(dotslash_windows_shim_root / "Cargo.toml"), 57 | "--bin=dotslash_windows_shim", 58 | "--release", 59 | # UNCOMMENT to compile allowing std use - useful for debugging. 60 | # "--no-default-features", 61 | ], 62 | check=True, 63 | env={**os.environ, "RUSTC_BOOTSTRAP": "1"}, 64 | ) 65 | os.environ["DOTSLASH_WINDOWS_SHIM"] = str( 66 | (target_dir or (dotslash_windows_shim_root / "target")) 67 | / "release" 68 | / "dotslash_windows_shim.exe" 69 | ) 70 | 71 | subprocess.run( 72 | [ 73 | sys.executable, 74 | str(dotslash_windows_shim_root / "tests" / "test.py"), 75 | ], 76 | check=True, 77 | ) 78 | 79 | 80 | if __name__ == "__main__": 81 | main() 82 | -------------------------------------------------------------------------------- /tests/fixtures/http__plain__print_argv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotslash 2 | 3 | { 4 | "name": "print_argv", 5 | "platforms": { 6 | "linux-aarch64": { 7 | "size": 11872, 8 | "hash": "blake3", 9 | "digest": "6f9ae1662dd3d974a6f01684ab963415009eec0c006aaed500148e04ab940fd8", 10 | "path": "subdir/print_argv.linux.aarch64", 11 | "providers": [ 12 | { 13 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.linux.aarch64" 14 | } 15 | ] 16 | }, 17 | "linux-x86_64": { 18 | "size": 11712, 19 | "hash": "blake3", 20 | "digest": "025d64cb9e77350243b2f594d7a0a9a335ef0c4d6994ee39dd71d551d8179713", 21 | "path": "subdir/print_argv.linux.x86_64", 22 | "providers": [ 23 | { 24 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.linux.x86_64" 25 | } 26 | ] 27 | }, 28 | "macos-aarch64": { 29 | "size": 55800, 30 | "hash": "blake3", 31 | "digest": "57ce32d91c9bfb9e14aac3225046314fff6b808d43c60c75654142a3e5735b2e", 32 | "path": "subdir/print_argv.macos.aarch64", 33 | "providers": [ 34 | { 35 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.macos.aarch64" 36 | } 37 | ] 38 | }, 39 | "macos-x86_64": { 40 | "size": 26648, 41 | "hash": "blake3", 42 | "digest": "ab6470b9d5528bb9956a0b0e38eeb19fb12809b5ec2c695b6ad98b6c3ddad5bd", 43 | "path": "subdir/print_argv.macos.x86_64", 44 | "providers": [ 45 | { 46 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.macos.x86_64" 47 | } 48 | ] 49 | }, 50 | "windows-aarch64": { 51 | "size": 23552, 52 | "hash": "blake3", 53 | "digest": "898ffab742ec8f6bbfb31516bbd5207b38e8018e8b10e8d2e7474e84bf3bcc19", 54 | "path": "subdir/print_argv.windows.aarch64.exe", 55 | "providers": [ 56 | { 57 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.windows.aarch64.exe" 58 | } 59 | ] 60 | }, 61 | "windows-x86_64": { 62 | "size": 25088, 63 | "hash": "blake3", 64 | "digest": "77645fa17bdd100f68d2de98f4de5c18cfbaae5c76150d8d1f02998dbde2053a", 65 | "path": "subdir/print_argv.windows.x86_64.exe", 66 | "providers": [ 67 | { 68 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.windows.x86_64.exe" 69 | } 70 | ] 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/util/is_not_found_error.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use std::io; 12 | 13 | /// Determine if an `io::Error` means that a path does not exist. 14 | /// 15 | /// A file cannot be the child path of another file. For example, 16 | /// given that `/a/b` is a file, a path `/a/b/c` is an error because 17 | /// `c` cannot exist as a child of the file `/a/b`. 18 | /// 19 | /// On Windows, this is an `io::ErrorKind::NotFound` error. 20 | /// On Unix, this is an `io::ErrorKind::NotADirectory`. 21 | /// 22 | /// Note on execv: 23 | /// 24 | /// If execv fails with ENOENT, that means we need to fetch the artifact. 25 | /// This is the most likely error returned by execv. 26 | /// 27 | /// ENOTDIR can happen if the program passed to execv is: 28 | /// 29 | /// ~/.cache/dotslash/obj/ha/xx/abc/extract/my_tool 30 | /// 31 | /// but the following is a regular file: 32 | /// 33 | /// ~/.cache/dotslash/obj/ha/xx/abc/extract 34 | /// 35 | /// This could happen if a previous release of DotSlash wrote this entry in 36 | /// the cache in a different way that is not consistent with the current 37 | /// directory structure. We should attempt to fetch the artifact again in 38 | /// this case. 39 | #[must_use] 40 | pub fn is_not_found_error(err: &io::Error) -> bool { 41 | err.kind() == io::ErrorKind::NotFound 42 | || (cfg!(unix) && err.kind() == io::ErrorKind::NotADirectory) 43 | } 44 | 45 | #[cfg(test)] 46 | mod tests { 47 | use std::fs; 48 | 49 | use tempfile::NamedTempFile; 50 | use tempfile::TempDir; 51 | 52 | use super::*; 53 | 54 | #[test] 55 | fn test_is_not_found_error_not_found() { 56 | let temp_dir = TempDir::with_prefix("dotslash-").unwrap(); 57 | let err = fs::read(temp_dir.path().join("fake_file.txt")).unwrap_err(); 58 | assert_eq!(err.kind(), io::ErrorKind::NotFound); 59 | assert!(is_not_found_error(&err)); 60 | } 61 | 62 | #[test] 63 | fn test_is_not_found_error_enotdir() { 64 | let temp_file = NamedTempFile::with_prefix("dotslash-").unwrap(); 65 | let err = fs::read(temp_file.path().join("fake_file.txt")).unwrap_err(); 66 | assert_eq!( 67 | err.kind().to_string(), 68 | if cfg!(windows) { 69 | "entity not found" 70 | } else { 71 | "not a directory" 72 | }, 73 | ); 74 | assert!(is_not_found_error(&err)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/util/execv.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | //! Cross-platform approximation of execv(3) on Unix. 12 | //! On Unix, this will use exec(2) directly. 13 | //! On Windows, this will spawn a child process with stdin/stdout/stderr 14 | //! inherited from the current process and will exit() with the result. 15 | 16 | use std::io; 17 | use std::process::Command; 18 | 19 | #[cfg(unix)] 20 | pub fn execv(command: &mut Command) -> io::Error { 21 | std::os::unix::process::CommandExt::exec(command) 22 | } 23 | 24 | #[cfg(windows)] 25 | pub fn execv(command: &mut Command) -> io::Error { 26 | // Starting in Rust 1.62.0, batch scripts are passed to `cmd.exe /c` rather 27 | // than directly to `CreateProcessW`. So if a script doesn't exist, we get 28 | // a `cmd.exe` process error, rather than a system error. We need to be 29 | // able to distinguish between "Not Found" and process errors because 30 | // artifact downloading depends on this. 31 | // 32 | // See https://github.com/rust-lang/rust/pull/95246 33 | 34 | use std::path::Path; 35 | use std::process; 36 | use std::process::Child; 37 | 38 | fn spawn(command: &mut Command) -> io::Result { 39 | let program = Path::new(command.get_program()); 40 | // This check must be done before the process is spawned, otherwise 41 | // we'll get a "The system cannot find the path specified." 42 | // printed to stderr. 43 | if program.extension().is_some_and(|x| { 44 | // .bat` and `.cmd` are the extensions checked for in std: 45 | // https://github.com/rust-lang/rust/blob/1.64.0/library/std/src/sys/windows/process.rs#L266-L269 46 | x.eq_ignore_ascii_case("bat") || x.eq_ignore_ascii_case("cmd") 47 | }) && !program.exists() 48 | { 49 | // Mimic the error pre-1.63.0 error. 50 | Err(io::Error::new( 51 | io::ErrorKind::NotFound, 52 | "The system cannot find the file specified. (os error 2)", 53 | )) 54 | } else { 55 | command.spawn() 56 | } 57 | } 58 | 59 | match spawn(command) { 60 | Ok(mut child) => match child.wait() { 61 | Ok(exit_code) => process::exit(exit_code.code().unwrap_or(1)), 62 | Err(e) => e, 63 | }, 64 | // This could be ENOENT if the executable does not exist. 65 | Err(e) => e, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/fixtures/http__zip__print_argv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotslash 2 | 3 | { 4 | "name": "print_argv", 5 | "platforms": { 6 | "linux-aarch64": { 7 | "size": 206651, 8 | "hash": "blake3", 9 | "digest": "59fe30efdbe0fe8647245ff5247fd13497d0970ac60c7e457d75af0d3f738d8f", 10 | "format": "zip", 11 | "path": "subdir/print_argv.linux.aarch64", 12 | "providers": [ 13 | { 14 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.zip" 15 | } 16 | ] 17 | }, 18 | "linux-x86_64": { 19 | "size": 206651, 20 | "hash": "blake3", 21 | "digest": "59fe30efdbe0fe8647245ff5247fd13497d0970ac60c7e457d75af0d3f738d8f", 22 | "format": "zip", 23 | "path": "subdir/print_argv.linux.x86_64", 24 | "providers": [ 25 | { 26 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.zip" 27 | } 28 | ] 29 | }, 30 | "macos-aarch64": { 31 | "size": 206651, 32 | "hash": "blake3", 33 | "digest": "59fe30efdbe0fe8647245ff5247fd13497d0970ac60c7e457d75af0d3f738d8f", 34 | "format": "zip", 35 | "path": "subdir/print_argv.macos.aarch64", 36 | "providers": [ 37 | { 38 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.zip" 39 | } 40 | ] 41 | }, 42 | "macos-x86_64": { 43 | "size": 206651, 44 | "hash": "blake3", 45 | "digest": "59fe30efdbe0fe8647245ff5247fd13497d0970ac60c7e457d75af0d3f738d8f", 46 | "format": "zip", 47 | "path": "subdir/print_argv.macos.x86_64", 48 | "providers": [ 49 | { 50 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.zip" 51 | } 52 | ] 53 | }, 54 | "windows-aarch64": { 55 | "size": 206651, 56 | "hash": "blake3", 57 | "digest": "59fe30efdbe0fe8647245ff5247fd13497d0970ac60c7e457d75af0d3f738d8f", 58 | "format": "zip", 59 | "path": "subdir/print_argv.windows.aarch64.exe", 60 | "providers": [ 61 | { 62 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.zip" 63 | } 64 | ] 65 | }, 66 | "windows-x86_64": { 67 | "size": 206651, 68 | "hash": "blake3", 69 | "digest": "59fe30efdbe0fe8647245ff5247fd13497d0970ac60c7e457d75af0d3f738d8f", 70 | "format": "zip", 71 | "path": "subdir/print_argv.windows.x86_64.exe", 72 | "providers": [ 73 | { 74 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.zip" 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/fixtures/http__tar_xz__print_argv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotslash 2 | 3 | { 4 | "name": "print_argv", 5 | "platforms": { 6 | "linux-aarch64": { 7 | "size": 52960, 8 | "hash": "blake3", 9 | "digest": "c6d1e53af2e09e838d1041d039cc32f43c8f28eacbfc63f9195d3b4fa6bb8de5", 10 | "format": "tar.xz", 11 | "path": "subdir/print_argv.linux.aarch64", 12 | "providers": [ 13 | { 14 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.xz" 15 | } 16 | ] 17 | }, 18 | "linux-x86_64": { 19 | "size": 52960, 20 | "hash": "blake3", 21 | "digest": "c6d1e53af2e09e838d1041d039cc32f43c8f28eacbfc63f9195d3b4fa6bb8de5", 22 | "format": "tar.xz", 23 | "path": "subdir/print_argv.linux.x86_64", 24 | "providers": [ 25 | { 26 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.xz" 27 | } 28 | ] 29 | }, 30 | "macos-aarch64": { 31 | "size": 52960, 32 | "hash": "blake3", 33 | "digest": "c6d1e53af2e09e838d1041d039cc32f43c8f28eacbfc63f9195d3b4fa6bb8de5", 34 | "format": "tar.xz", 35 | "path": "subdir/print_argv.macos.aarch64", 36 | "providers": [ 37 | { 38 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.xz" 39 | } 40 | ] 41 | }, 42 | "macos-x86_64": { 43 | "size": 52960, 44 | "hash": "blake3", 45 | "digest": "c6d1e53af2e09e838d1041d039cc32f43c8f28eacbfc63f9195d3b4fa6bb8de5", 46 | "format": "tar.xz", 47 | "path": "subdir/print_argv.macos.x86_64", 48 | "providers": [ 49 | { 50 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.xz" 51 | } 52 | ] 53 | }, 54 | "windows-aarch64": { 55 | "size": 52960, 56 | "hash": "blake3", 57 | "digest": "c6d1e53af2e09e838d1041d039cc32f43c8f28eacbfc63f9195d3b4fa6bb8de5", 58 | "format": "tar.xz", 59 | "path": "subdir/print_argv.windows.aarch64.exe", 60 | "providers": [ 61 | { 62 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.xz" 63 | } 64 | ] 65 | }, 66 | "windows-x86_64": { 67 | "size": 52960, 68 | "hash": "blake3", 69 | "digest": "c6d1e53af2e09e838d1041d039cc32f43c8f28eacbfc63f9195d3b4fa6bb8de5", 70 | "format": "tar.xz", 71 | "path": "subdir/print_argv.windows.x86_64.exe", 72 | "providers": [ 73 | { 74 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.xz" 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/fixtures/http__nonexistent_url: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotslash 2 | 3 | { 4 | "name": "print_argv", 5 | "platforms": { 6 | "linux-aarch64": { 7 | "size": 155817, 8 | "hash": "blake3", 9 | "digest": "0a7ec8e59cc9f7266b89db9a3e81558dee49650cc341252b1cab29e60cee7da0", 10 | "format": "tar.gz", 11 | "path": "subdir/print_argv.linux.aarch64", 12 | "providers": [ 13 | { 14 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/fake.tar.gz" 15 | } 16 | ] 17 | }, 18 | "linux-x86_64": { 19 | "size": 155817, 20 | "hash": "blake3", 21 | "digest": "0a7ec8e59cc9f7266b89db9a3e81558dee49650cc341252b1cab29e60cee7da0", 22 | "format": "tar.gz", 23 | "path": "subdir/print_argv.linux.x86_64", 24 | "providers": [ 25 | { 26 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/fake.tar.gz" 27 | } 28 | ] 29 | }, 30 | "macos-aarch64": { 31 | "size": 155817, 32 | "hash": "blake3", 33 | "digest": "0a7ec8e59cc9f7266b89db9a3e81558dee49650cc341252b1cab29e60cee7da0", 34 | "format": "tar.gz", 35 | "path": "subdir/print_argv.macos.aarch64", 36 | "providers": [ 37 | { 38 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/fake.tar.gz" 39 | } 40 | ] 41 | }, 42 | "macos-x86_64": { 43 | "size": 155817, 44 | "hash": "blake3", 45 | "digest": "0a7ec8e59cc9f7266b89db9a3e81558dee49650cc341252b1cab29e60cee7da0", 46 | "format": "tar.gz", 47 | "path": "subdir/print_argv.macos.x86_64", 48 | "providers": [ 49 | { 50 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/fake.tar.gz" 51 | } 52 | ] 53 | }, 54 | "windows-aarch64": { 55 | "size": 155817, 56 | "hash": "blake3", 57 | "digest": "0a7ec8e59cc9f7266b89db9a3e81558dee49650cc341252b1cab29e60cee7da0", 58 | "format": "tar.gz", 59 | "path": "subdir/print_argv.windows.aarch64.exe", 60 | "providers": [ 61 | { 62 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/fake.tar.gz" 63 | } 64 | ] 65 | }, 66 | "windows-x86_64": { 67 | "size": 155817, 68 | "hash": "blake3", 69 | "digest": "0a7ec8e59cc9f7266b89db9a3e81558dee49650cc341252b1cab29e60cee7da0", 70 | "format": "tar.gz", 71 | "path": "subdir/print_argv.windows.x86_64.exe", 72 | "providers": [ 73 | { 74 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/fake.tar.gz" 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/fixtures/http__tar_gz__print_argv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotslash 2 | 3 | { 4 | "name": "print_argv", 5 | "platforms": { 6 | "linux-aarch64": { 7 | "size": 155817, 8 | "hash": "blake3", 9 | "digest": "0a7ec8e59cc9f7266b89db9a3e81558dee49650cc341252b1cab29e60cee7da0", 10 | "format": "tar.gz", 11 | "path": "subdir/print_argv.linux.aarch64", 12 | "providers": [ 13 | { 14 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.gz" 15 | } 16 | ] 17 | }, 18 | "linux-x86_64": { 19 | "size": 155817, 20 | "hash": "blake3", 21 | "digest": "0a7ec8e59cc9f7266b89db9a3e81558dee49650cc341252b1cab29e60cee7da0", 22 | "format": "tar.gz", 23 | "path": "subdir/print_argv.linux.x86_64", 24 | "providers": [ 25 | { 26 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.gz" 27 | } 28 | ] 29 | }, 30 | "macos-aarch64": { 31 | "size": 155817, 32 | "hash": "blake3", 33 | "digest": "0a7ec8e59cc9f7266b89db9a3e81558dee49650cc341252b1cab29e60cee7da0", 34 | "format": "tar.gz", 35 | "path": "subdir/print_argv.macos.aarch64", 36 | "providers": [ 37 | { 38 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.gz" 39 | } 40 | ] 41 | }, 42 | "macos-x86_64": { 43 | "size": 155817, 44 | "hash": "blake3", 45 | "digest": "0a7ec8e59cc9f7266b89db9a3e81558dee49650cc341252b1cab29e60cee7da0", 46 | "format": "tar.gz", 47 | "path": "subdir/print_argv.macos.x86_64", 48 | "providers": [ 49 | { 50 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.gz" 51 | } 52 | ] 53 | }, 54 | "windows-aarch64": { 55 | "size": 155817, 56 | "hash": "blake3", 57 | "digest": "0a7ec8e59cc9f7266b89db9a3e81558dee49650cc341252b1cab29e60cee7da0", 58 | "format": "tar.gz", 59 | "path": "subdir/print_argv.windows.aarch64.exe", 60 | "providers": [ 61 | { 62 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.gz" 63 | } 64 | ] 65 | }, 66 | "windows-x86_64": { 67 | "size": 155817, 68 | "hash": "blake3", 69 | "digest": "0a7ec8e59cc9f7266b89db9a3e81558dee49650cc341252b1cab29e60cee7da0", 70 | "format": "tar.gz", 71 | "path": "subdir/print_argv.windows.x86_64.exe", 72 | "providers": [ 73 | { 74 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.gz" 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/fixtures/http__tar_zst__print_argv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotslash 2 | 3 | { 4 | "name": "print_argv", 5 | "platforms": { 6 | "linux-aarch64": { 7 | "size": 57910, 8 | "hash": "blake3", 9 | "digest": "8c4ff49bad3199f3f5519dc9f9b430c4ca9637cf7b19f1b8ca405a3ccbf08ba0", 10 | "format": "tar.zst", 11 | "path": "subdir/print_argv.linux.aarch64", 12 | "providers": [ 13 | { 14 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.zst" 15 | } 16 | ] 17 | }, 18 | "linux-x86_64": { 19 | "size": 57910, 20 | "hash": "blake3", 21 | "digest": "8c4ff49bad3199f3f5519dc9f9b430c4ca9637cf7b19f1b8ca405a3ccbf08ba0", 22 | "format": "tar.zst", 23 | "path": "subdir/print_argv.linux.x86_64", 24 | "providers": [ 25 | { 26 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.zst" 27 | } 28 | ] 29 | }, 30 | "macos-aarch64": { 31 | "size": 57910, 32 | "hash": "blake3", 33 | "digest": "8c4ff49bad3199f3f5519dc9f9b430c4ca9637cf7b19f1b8ca405a3ccbf08ba0", 34 | "format": "tar.zst", 35 | "path": "subdir/print_argv.macos.aarch64", 36 | "providers": [ 37 | { 38 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.zst" 39 | } 40 | ] 41 | }, 42 | "macos-x86_64": { 43 | "size": 57910, 44 | "hash": "blake3", 45 | "digest": "8c4ff49bad3199f3f5519dc9f9b430c4ca9637cf7b19f1b8ca405a3ccbf08ba0", 46 | "format": "tar.zst", 47 | "path": "subdir/print_argv.macos.x86_64", 48 | "providers": [ 49 | { 50 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.zst" 51 | } 52 | ] 53 | }, 54 | "windows-aarch64": { 55 | "size": 57910, 56 | "hash": "blake3", 57 | "digest": "8c4ff49bad3199f3f5519dc9f9b430c4ca9637cf7b19f1b8ca405a3ccbf08ba0", 58 | "format": "tar.zst", 59 | "path": "subdir/print_argv.windows.aarch64.exe", 60 | "providers": [ 61 | { 62 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.zst" 63 | } 64 | ] 65 | }, 66 | "windows-x86_64": { 67 | "size": 57910, 68 | "hash": "blake3", 69 | "digest": "8c4ff49bad3199f3f5519dc9f9b430c4ca9637cf7b19f1b8ca405a3ccbf08ba0", 70 | "format": "tar.zst", 71 | "path": "subdir/print_argv.windows.x86_64.exe", 72 | "providers": [ 73 | { 74 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/pack.tar.zst" 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/fixtures/http__gz__print_argv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotslash 2 | 3 | { 4 | "name": "print_argv", 5 | "platforms": { 6 | "linux-aarch64": { 7 | "size": 6986, 8 | "hash": "blake3", 9 | "digest": "8b8b07c4edfe23990083e815308e89c4f168ade0603164f62fc21206232d796a", 10 | "format": "gz", 11 | "path": "subdir/print_argv.linux.aarch64", 12 | "providers": [ 13 | { 14 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.linux.aarch64.gz" 15 | } 16 | ] 17 | }, 18 | "linux-x86_64": { 19 | "size": 6621, 20 | "hash": "blake3", 21 | "digest": "2e923d656b74e27ab88532aea86a2ea78e1baa0f86b5a7f8d3da475c8ad93a64", 22 | "format": "gz", 23 | "path": "subdir/print_argv.linux.x86_64", 24 | "providers": [ 25 | { 26 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.linux.x86_64.gz" 27 | } 28 | ] 29 | }, 30 | "macos-aarch64": { 31 | "size": 9437, 32 | "hash": "blake3", 33 | "digest": "74974497b5750271431dcd66a0d01249d64c0fd638a61c3e497fc84e442a0860", 34 | "format": "gz", 35 | "path": "subdir/print_argv.macos.aarch64", 36 | "providers": [ 37 | { 38 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.macos.aarch64.gz" 39 | } 40 | ] 41 | }, 42 | "macos-x86_64": { 43 | "size": 8541, 44 | "hash": "blake3", 45 | "digest": "5a446458c1f8e689a2cefc3dc7fe43e0d191c783b22ec3381491555ae20cece9", 46 | "format": "gz", 47 | "path": "subdir/print_argv.macos.x86_64", 48 | "providers": [ 49 | { 50 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.macos.x86_64.gz" 51 | } 52 | ] 53 | }, 54 | "windows-aarch64": { 55 | "size": 13974, 56 | "hash": "blake3", 57 | "digest": "b89e4d9959ba402443612b2f5b9beeec31ce3aa825db41169715b07a9f817ca6", 58 | "format": "gz", 59 | "path": "subdir/print_argv.windows.aarch64.exe", 60 | "providers": [ 61 | { 62 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.windows.aarch64.exe.gz" 63 | } 64 | ] 65 | }, 66 | "windows-x86_64": { 67 | "size": 14598, 68 | "hash": "blake3", 69 | "digest": "2f7dddf1485560d1a6c43366d540cc6538039e405027ed35131fc36726c61d45", 70 | "format": "gz", 71 | "path": "subdir/print_argv.windows.x86_64.exe", 72 | "providers": [ 73 | { 74 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.windows.x86_64.exe.gz" 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/fixtures/http__xz__print_argv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotslash 2 | 3 | { 4 | "name": "print_argv", 5 | "platforms": { 6 | "linux-aarch64": { 7 | "size": 6176, 8 | "hash": "blake3", 9 | "digest": "1368be795cf00c9a22d5195b5006252012be47f2f0e22aeccf5885b3d691109d", 10 | "format": "xz", 11 | "path": "subdir/print_argv.linux.aarch64", 12 | "providers": [ 13 | { 14 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.linux.aarch64.xz" 15 | } 16 | ] 17 | }, 18 | "linux-x86_64": { 19 | "size": 6152, 20 | "hash": "blake3", 21 | "digest": "757ffbaef76729ed0ca2361ebb3d5de8e491b09d9cdce24bf80073bb092caf74", 22 | "format": "xz", 23 | "path": "subdir/print_argv.linux.x86_64", 24 | "providers": [ 25 | { 26 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.linux.x86_64.xz" 27 | } 28 | ] 29 | }, 30 | "macos-aarch64": { 31 | "size": 7932, 32 | "hash": "blake3", 33 | "digest": "5d408257d3c96fff0cd9585ff667cca4f2af4e70b9b0c123245de0ac3db7353f", 34 | "format": "xz", 35 | "path": "subdir/print_argv.macos.aarch64", 36 | "providers": [ 37 | { 38 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.macos.aarch64.xz" 39 | } 40 | ] 41 | }, 42 | "macos-x86_64": { 43 | "size": 7652, 44 | "hash": "blake3", 45 | "digest": "fa0cfc17d536e1df1a2f66e2e67cb6512ad52c7694a4e6a94b437a10e018aac6", 46 | "format": "xz", 47 | "path": "subdir/print_argv.macos.x86_64", 48 | "providers": [ 49 | { 50 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.macos.x86_64.xz" 51 | } 52 | ] 53 | }, 54 | "windows-aarch64": { 55 | "size": 12284, 56 | "hash": "blake3", 57 | "digest": "c8ccef2d45d2ed47612fc78b64bc3671b3efc925b9d8a9e4a6d0516166584611", 58 | "format": "xz", 59 | "path": "subdir/print_argv.windows.aarch64.exe", 60 | "providers": [ 61 | { 62 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.windows.aarch64.exe.xz" 63 | } 64 | ] 65 | }, 66 | "windows-x86_64": { 67 | "size": 13136, 68 | "hash": "blake3", 69 | "digest": "a81b9851cab0a1c695c4d3d1c8b2f078ff05556391369a19abd7ed6b4998619b", 70 | "format": "xz", 71 | "path": "subdir/print_argv.windows.x86_64.exe", 72 | "providers": [ 73 | { 74 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.windows.x86_64.exe.xz" 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/fixtures/http__zst__print_argv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotslash 2 | 3 | { 4 | "name": "print_argv", 5 | "platforms": { 6 | "linux-aarch64": { 7 | "size": 6773, 8 | "hash": "blake3", 9 | "digest": "f7dc3a3ae95a9d5e72de583eec55fdc375fe0e650264065783aa028701dc1701", 10 | "format": "zst", 11 | "path": "subdir/print_argv.linux.aarch64", 12 | "providers": [ 13 | { 14 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.linux.aarch64.zst" 15 | } 16 | ] 17 | }, 18 | "linux-x86_64": { 19 | "size": 6484, 20 | "hash": "blake3", 21 | "digest": "d50b9777c0f01318dbb69d63a09e861518ed1861a57c0fb590c47f640a4ae4ee", 22 | "format": "zst", 23 | "path": "subdir/print_argv.linux.x86_64", 24 | "providers": [ 25 | { 26 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.linux.x86_64.zst" 27 | } 28 | ] 29 | }, 30 | "macos-aarch64": { 31 | "size": 8695, 32 | "hash": "blake3", 33 | "digest": "30c0bfef037d34317216025dde587c29ddc913726ca593c1e1332df956029483", 34 | "format": "zst", 35 | "path": "subdir/print_argv.macos.aarch64", 36 | "providers": [ 37 | { 38 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.macos.aarch64.zst" 39 | } 40 | ] 41 | }, 42 | "macos-x86_64": { 43 | "size": 8172, 44 | "hash": "blake3", 45 | "digest": "f95c8e61d7c95a1997da7d92ab776df7ab410f1554f53ef6569924363b503dc3", 46 | "format": "zst", 47 | "path": "subdir/print_argv.macos.x86_64", 48 | "providers": [ 49 | { 50 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.macos.x86_64.zst" 51 | } 52 | ] 53 | }, 54 | "windows-aarch64": { 55 | "size": 13448, 56 | "hash": "blake3", 57 | "digest": "63089805077b73b5e4d41a706fbf382f759c43d12b6945df603cd4778cc0acf0", 58 | "format": "zst", 59 | "path": "subdir/print_argv.windows.aarch64.exe", 60 | "providers": [ 61 | { 62 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.windows.aarch64.exe.zst" 63 | } 64 | ] 65 | }, 66 | "windows-x86_64": { 67 | "size": 14127, 68 | "hash": "blake3", 69 | "digest": "a669802122f036549362f327c7c531f444e8bce5762f8f608059c42ebb48ef42", 70 | "format": "zst", 71 | "path": "subdir/print_argv.windows.x86_64.exe", 72 | "providers": [ 73 | { 74 | "url": "https://github.com/zertosh/dotslash_fixtures/raw/5adea95f2eac6509cad9ca87eb770596a1a21379/print_argv.windows.x86_64.exe.zst" 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/util/file_lock.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | //! Wrapper around `fs2::lock_exclusive`. 12 | 13 | use std::fs::File; 14 | use std::io; 15 | use std::path::Path; 16 | use std::path::PathBuf; 17 | 18 | use thiserror::Error; 19 | 20 | #[derive(Debug, Error)] 21 | pub enum FileLockError { 22 | #[error("failed to create lock file `{0}`")] 23 | Create(PathBuf, #[source] io::Error), 24 | 25 | #[error("failed to get exclusive lock `{0}`")] 26 | LockExclusive(PathBuf, #[source] io::Error), 27 | 28 | #[error("failed to get shared lock `{0}`")] 29 | LockShared(PathBuf, #[source] io::Error), 30 | } 31 | 32 | #[derive(Debug, Default)] 33 | pub struct FileLock { 34 | /// If file is Some, then it is holding the lock. 35 | file: Option, 36 | } 37 | 38 | impl FileLock { 39 | pub fn acquire

(path: P) -> Result 40 | where 41 | P: AsRef, 42 | { 43 | fn inner(path: &Path) -> Result { 44 | let lock_file = File::options() 45 | .read(true) 46 | .write(true) 47 | .create(true) 48 | .truncate(false) 49 | .open(path) 50 | .map_err(|e| FileLockError::Create(path.to_path_buf(), e))?; 51 | 52 | fs2::FileExt::lock_exclusive(&lock_file) 53 | .map_err(|e| FileLockError::LockExclusive(path.to_path_buf(), e))?; 54 | 55 | Ok(FileLock { 56 | file: Some(lock_file), 57 | }) 58 | } 59 | inner(path.as_ref()) 60 | } 61 | 62 | pub fn acquire_shared_lock

(path: P) -> Result 63 | where 64 | P: AsRef, 65 | { 66 | fn inner(path: &Path) -> Result { 67 | let lock_file = File::options() 68 | .read(true) 69 | .write(true) 70 | .create(true) 71 | .truncate(false) 72 | .open(path) 73 | .map_err(|e| FileLockError::Create(path.to_path_buf(), e))?; 74 | 75 | fs2::FileExt::lock_shared(&lock_file) 76 | .map_err(|e| FileLockError::LockShared(path.to_path_buf(), e))?; 77 | 78 | Ok(FileLock { 79 | file: Some(lock_file), 80 | }) 81 | } 82 | inner(path.as_ref()) 83 | } 84 | } 85 | 86 | impl Drop for FileLock { 87 | fn drop(&mut self) { 88 | if let Some(file) = self.file.take() { 89 | drop(fs2::FileExt::unlock(&file)); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /windows_shim/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "cfg-if" 7 | version = "1.0.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 10 | 11 | [[package]] 12 | name = "dotslash_windows_shim" 13 | version = "0.0.0" 14 | dependencies = [ 15 | "cfg-if", 16 | "windows-sys", 17 | ] 18 | 19 | [[package]] 20 | name = "windows-sys" 21 | version = "0.59.0" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 24 | dependencies = [ 25 | "windows-targets", 26 | ] 27 | 28 | [[package]] 29 | name = "windows-targets" 30 | version = "0.52.6" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 33 | dependencies = [ 34 | "windows_aarch64_gnullvm", 35 | "windows_aarch64_msvc", 36 | "windows_i686_gnu", 37 | "windows_i686_gnullvm", 38 | "windows_i686_msvc", 39 | "windows_x86_64_gnu", 40 | "windows_x86_64_gnullvm", 41 | "windows_x86_64_msvc", 42 | ] 43 | 44 | [[package]] 45 | name = "windows_aarch64_gnullvm" 46 | version = "0.52.6" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 49 | 50 | [[package]] 51 | name = "windows_aarch64_msvc" 52 | version = "0.52.6" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 55 | 56 | [[package]] 57 | name = "windows_i686_gnu" 58 | version = "0.52.6" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 61 | 62 | [[package]] 63 | name = "windows_i686_gnullvm" 64 | version = "0.52.6" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 67 | 68 | [[package]] 69 | name = "windows_i686_msvc" 70 | version = "0.52.6" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 73 | 74 | [[package]] 75 | name = "windows_x86_64_gnu" 76 | version = "0.52.6" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 79 | 80 | [[package]] 81 | name = "windows_x86_64_gnullvm" 82 | version = "0.52.6" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 85 | 86 | [[package]] 87 | name = "windows_x86_64_msvc" 88 | version = "0.52.6" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 91 | -------------------------------------------------------------------------------- /tests/generate_fixtures.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) Meta Platforms, Inc. and affiliates. 3 | # 4 | # This source code is dual-licensed under either the MIT license found in the 5 | # LICENSE-MIT file in the root directory of this source tree or the Apache 6 | # License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | # of this source tree. You may select, at your option, one of the 8 | # above-listed licenses. 9 | 10 | import argparse 11 | import json 12 | import os 13 | import subprocess 14 | 15 | """Run this script to regenerate the runnable print_argv DotSlash files in the 16 | fixtures/ directory. 17 | """ 18 | 19 | 20 | platform_configs = [ 21 | ("linux.aarch64", "linux-aarch64"), 22 | ("linux.x86_64", "linux-x86_64"), 23 | ("macos.aarch64", "macos-aarch64"), 24 | ("macos.x86_64", "macos-x86_64"), 25 | ("windows.aarch64.exe", "windows-aarch64"), 26 | ("windows.x86_64.exe", "windows-x86_64"), 27 | ] 28 | 29 | GITHUB_REPO = "https://github.com/zertosh/dotslash_fixtures" 30 | DOTSLASH_EXE_NAME = "print_argv" 31 | 32 | 33 | def main() -> None: 34 | parser = argparse.ArgumentParser() 35 | parser.add_argument("--commit-hash", required=True) 36 | args = parser.parse_args() 37 | 38 | commit_hash: str = args.commit_hash 39 | 40 | # TODO(asuarez): Make the artifacts for [".tar", ".tar.gz", ".tar.zst"] 41 | # available as well? 42 | file_extensions = ["", ".gz", ".xz", ".zst"] 43 | for file_extension in file_extensions: 44 | generate_dotslash_file_for_file_extension( 45 | commit_hash=commit_hash, 46 | file_extension=file_extension, 47 | ) 48 | 49 | 50 | def generate_dotslash_file_for_file_extension( 51 | commit_hash: str, 52 | file_extension: str, 53 | ) -> None: 54 | platforms = {} 55 | for platform_name, platform_id in platform_configs: 56 | url = f"{GITHUB_REPO}/raw/{commit_hash}/print_argv.{platform_name}{file_extension}" 57 | entry_json = subprocess.check_output( 58 | [ 59 | "cargo", 60 | "run", 61 | "--release", 62 | "--quiet", 63 | "--", 64 | "--", 65 | "create-url-entry", 66 | url, 67 | ] 68 | ) 69 | entry = json.loads(entry_json) 70 | if file_extension == "": 71 | del entry["format"] 72 | entry["path"] = f"subdir/print_argv.{platform_name}" 73 | platforms[platform_id] = entry 74 | 75 | dotslash_json = { 76 | "name": DOTSLASH_EXE_NAME, 77 | "platforms": platforms, 78 | } 79 | 80 | if file_extension == "": 81 | format_id = "plain" 82 | else: 83 | format_id = file_extension[1:].replace(".", "_") 84 | dotslash_file = os.path.join( 85 | os.path.dirname(__file__), "fixtures", f"http__{format_id}__print_argv" 86 | ) 87 | 88 | with open(dotslash_file, "w") as f: 89 | f.write("#!/usr/bin/env dotslash\n\n") 90 | f.write(json.dumps(dotslash_json, indent=2)) 91 | f.write("\n") 92 | 93 | 94 | if __name__ == "__main__": 95 | main() 96 | -------------------------------------------------------------------------------- /src/util/chmodx.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | //! Make a file executable on Unix. 12 | 13 | use std::io; 14 | use std::os::unix::fs::PermissionsExt as _; 15 | use std::path::Path; 16 | 17 | use crate::util::fs_ctx; 18 | 19 | const DEFAULT_FILE_PERMISSIONS: u32 = 0o500; 20 | 21 | pub fn chmodx>(path: P) -> io::Result<()> { 22 | fn inner(path: &Path) -> io::Result<()> { 23 | let mut perms = fs_ctx::metadata(path)?.permissions(); 24 | // Includes extra bits not just rwx permissions. 25 | // See: https://github.com/rust-lang/rust/issues/45330 26 | let mode = perms.mode(); 27 | 28 | // Remove any extra bits. 29 | let file_permissions = mode & 0o777; 30 | 31 | // Only overwrite if the file isn't executable. 32 | if file_permissions & 0o111 == 0 { 33 | perms.set_mode(DEFAULT_FILE_PERMISSIONS); 34 | fs_ctx::set_permissions(path, perms)?; 35 | } 36 | 37 | Ok(()) 38 | } 39 | 40 | inner(path.as_ref()) 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use std::fs; 46 | 47 | use tempfile::NamedTempFile; 48 | 49 | use super::*; 50 | 51 | #[test] 52 | fn test_chmodx() -> io::Result<()> { 53 | #[track_caller] 54 | fn t(before: u32, after: u32) -> io::Result<()> { 55 | let temp_path = NamedTempFile::new()?.into_temp_path(); 56 | 57 | let mut perms = fs::metadata(&temp_path)?.permissions(); 58 | perms.set_mode(before); 59 | fs::set_permissions(&temp_path, perms)?; 60 | assert_eq!( 61 | fs::metadata(&temp_path)?.permissions().mode() & 0o777, 62 | before, 63 | ); 64 | 65 | chmodx(&temp_path)?; 66 | 67 | assert_eq!( 68 | fs::metadata(&temp_path)?.permissions().mode() & 0o777, 69 | after, 70 | ); 71 | 72 | Ok(()) 73 | } 74 | 75 | t(DEFAULT_FILE_PERMISSIONS, DEFAULT_FILE_PERMISSIONS)?; 76 | t(0o505, 0o505)?; 77 | t(0o550, 0o550)?; 78 | t(0o555, 0o555)?; 79 | 80 | t(0o100, 0o100)?; 81 | t(0o300, 0o300)?; 82 | t(0o700, 0o700)?; 83 | 84 | t(0o010, 0o010)?; 85 | t(0o030, 0o030)?; 86 | t(0o070, 0o070)?; 87 | 88 | t(0o001, 0o001)?; 89 | t(0o003, 0o003)?; 90 | t(0o007, 0o007)?; 91 | 92 | t(0o412, 0o412)?; 93 | 94 | t(0o000, DEFAULT_FILE_PERMISSIONS)?; 95 | t(0o200, DEFAULT_FILE_PERMISSIONS)?; 96 | t(0o400, DEFAULT_FILE_PERMISSIONS)?; 97 | t(0o600, DEFAULT_FILE_PERMISSIONS)?; 98 | 99 | Ok(()) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # DotSlash: simplified executable deployment 2 | 3 | [![CI - Test](https://github.com/facebook/dotslash/actions/workflows/test-python.yml/badge.svg)](https://github.com/facebook/dotslash/actions/workflows/test-python.yml) 4 | [![PyPI - Version](https://img.shields.io/pypi/v/dotslash.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/dotslash/) 5 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/dotslash.svg?color=blue&label=Downloads&logo=pypi&logoColor=gold)](https://pypi.org/project/dotslash/) 6 | [![Built by Hatch](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pypa/hatch/master/docs/assets/badge/v0.json)](https://github.com/pypa/hatch) 7 | [![Ruff linting/formatting](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 8 | 9 | --- 10 | 11 | [DotSlash](https://dotslash-cli.com/docs/) (`dotslash`) is a command-line tool 12 | that lets you represent a set of platform-specific, heavyweight executables with 13 | an equivalent small, easy-to-read text file. In turn, this makes it efficient to 14 | store executables in source control without hurting repository size. This paves 15 | the way for checking build toolchains and other tools directly into the repo, 16 | reducing dependencies on the host environment and thereby facilitating 17 | reproducible builds. 18 | 19 | The `dotslash` package allows you to use DotSlash in your Python projects 20 | without having to install DotSlash globally. 21 | 22 | **_Table of Contents_** 23 | 24 | - [Using as a library](#using-as-a-library) 25 | - [Using as a command-line tool](#using-as-a-command-line-tool) 26 | - [Building from source](#building-from-source) 27 | - [License](#license) 28 | 29 | ## Using as a library 30 | 31 | The `dotslash.locate` function returns the path to the DotSlash binary that was 32 | installed by this package. 33 | 34 | ```pycon 35 | >>> import dotslash 36 | >>> dotslash.locate() 37 | '/root/.local/bin/dotslash' 38 | ``` 39 | 40 | ## Using as a command-line tool 41 | 42 | The installed DotSlash binary can be invoked directly by running the `dotslash` 43 | module as a script. 44 | 45 | ``` 46 | python -m dotslash path/to/dotslash-file.json 47 | ``` 48 | 49 | ## Building from source 50 | 51 | When building or installing from this directory, the `DOTSLASH_VERSION` 52 | environment variable must be set to the version of DotSlash to use. A preceding 53 | `v` is accepted but not required. 54 | 55 | ``` 56 | DOTSLASH_VERSION=0.5.8 python -m build 57 | ``` 58 | 59 | This will use the binaries from DotSlash's 60 | [GitHub releases](https://github.com/facebook/dotslash/releases). If there is a 61 | directory of GitHub release assets, you can use that directly with the 62 | `DOTSLASH_SOURCE` environment variable. 63 | 64 | ``` 65 | DOTSLASH_VERSION=0.5.8 DOTSLASH_SOURCE=path/to/dotslash-assets python -m build 66 | ``` 67 | 68 | The DotSlash source is set to `release` by default. 69 | 70 | ## License 71 | 72 | DotSlash is licensed under both the MIT license and Apache-2.0 license; the 73 | exact terms can be found in the 74 | [LICENSE-MIT](https://github.com/facebook/dotslash/blob/main/LICENSE-MIT) and 75 | [LICENSE-APACHE](https://github.com/facebook/dotslash/blob/main/LICENSE-APACHE) 76 | files, respectively. 77 | -------------------------------------------------------------------------------- /src/digest.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use std::fmt; 12 | 13 | use serde::Deserialize; 14 | use serde::Serialize; 15 | use thiserror::Error; 16 | 17 | #[derive(Debug, Error)] 18 | pub enum DigestError { 19 | #[error("invalid hash characters `{0}`")] 20 | InvalidHashCharacters(String), 21 | 22 | #[error("invalid hash length `{0}`")] 23 | InvalidHashLength(String), 24 | } 25 | 26 | #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] 27 | #[serde(try_from = "String")] 28 | pub struct Digest(String); 29 | 30 | impl fmt::Display for Digest { 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | write!(f, "{}", self.0) 33 | } 34 | } 35 | 36 | impl TryFrom for Digest { 37 | type Error = DigestError; 38 | 39 | fn try_from(hash: String) -> Result { 40 | if !hash.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f')) { 41 | Err(DigestError::InvalidHashCharacters(hash)) 42 | } else if hash.len() != 64 { 43 | Err(DigestError::InvalidHashLength(hash)) 44 | } else { 45 | Ok(Digest(hash)) 46 | } 47 | } 48 | } 49 | 50 | impl Digest { 51 | pub fn as_str(&self) -> &str { 52 | &self.0 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use assert_matches::assert_matches; 59 | 60 | use super::*; 61 | 62 | #[test] 63 | fn test_digest_try_from_string_invalid() { 64 | assert_matches!( 65 | Digest::try_from("".to_owned()), 66 | Err(DigestError::InvalidHashLength(x)) if x.is_empty() 67 | ); 68 | assert_matches!( 69 | Digest::try_from("z".to_owned()), 70 | Err(DigestError::InvalidHashCharacters(x)) if x == "z" 71 | ); 72 | assert_matches!( 73 | Digest::try_from("7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d906".to_owned()), 74 | Err(DigestError::InvalidHashLength(x)) 75 | if x == "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d906" 76 | ); 77 | assert_matches!( 78 | Digest::try_from("7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d90690".to_owned()), 79 | Err(DigestError::InvalidHashLength(x)) 80 | if x == "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d90690" 81 | ); 82 | } 83 | 84 | #[test] 85 | fn test_digest_try_from_string_valid() { 86 | let digest = Digest::try_from( 87 | "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069".to_owned(), 88 | ) 89 | .unwrap(); 90 | let expected = 91 | Digest("7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069".to_owned()); 92 | assert_eq!(digest, expected); 93 | assert_eq!( 94 | format!("{}", digest), 95 | "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069", 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /website/docs/flags.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 14 3 | --- 4 | 5 | # Command Line Flags 6 | 7 | Because the usage of DotSlash is: 8 | 9 | ```shell 10 | dotslash DOTSLASH_FILE [OPTIONS] 11 | ``` 12 | 13 | where `[OPTIONS]` is forwarded to the executable represented by `DOTSLASH_FILE`, 14 | DotSlash's own command line flags must be able to be disambiguated from 15 | `DOTSLASH_FILE`. In practice, that means any flag recognized by DotSlash is an 16 | unsupported DotSlash file name. For this reason, the set of supported flags is 17 | fairly limited. 18 | 19 | ## Supported Flags 20 | 21 | 22 | 23 | | flag | description | 24 | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | 25 | | `--help` | prints basic usage info, as well as the _platform_ it was compiled for (which is the entry it will use from the `"platforms"` map in a DotSlash file) | 26 | | `--version` | prints the DotSlash version number and exits | 27 | 28 | 29 | 30 | ## Experimental Commands 31 | 32 | Experimental commands are special flags that we are not committed to supporting, 33 | and whose output format should be considered unstable. These commands are 34 | "hidden" behind `--` (using `--` as the first argument to `dotslash` tells it to 35 | use a special argument parser) and are used like so: 36 | 37 | ```shell 38 | $ dotslash -- cache-dir 39 | /Users/mbolin/Library/Caches/dotslash 40 | ``` 41 | 42 | | command | description | 43 | | ---------------------- | ------------------------------------------------------------------------------------ | 44 | | `b3sum FILE` | prints the BLAKE3 hash of `FILE` | 45 | | `cache-dir` | prints the absolute path to the user's DotSlash cache and exits | 46 | | `create-url-entry URL` | generates the DotSlash JSON snippet for the artifact at the URL | 47 | | `fetch DOTSLASH_FILE` | fetches the artifact identified by `DOTSLASH_FILE` if it is not already in the cache | 48 | | `parse DOTSLASH_FILE` | parses `DOTSLASH_FILE` and prints the data as pure JSON to stdout | 49 | | `sha256 FILE` | prints the SHA-256 hash of `FILE` | 50 | 51 | ## Environment Variables 52 | 53 | The `DOTSLASH_CACHE` environment variable can be used to override the default 54 | location of the DotSlash cache. By default, the DotSlash cache resides at: 55 | 56 | | platform | path | 57 | | -------- | ----------------------------------------------------- | 58 | | Linux | `$XDG_CACHE_HOME/dotslash` or `$HOME/.cache/dotslash` | 59 | | macOS | `$HOME/Library/Caches/dotslash` | 60 | | Windows | `{FOLDERID_LocalAppData}/dotslash` | 61 | 62 | DotSlash relies on 63 | [`dirs::cache_dir()`](https://docs.rs/dirs/5.0.1/dirs/fn.cache_dir.html) to use 64 | the appropriate default directory on each platform. 65 | -------------------------------------------------------------------------------- /.github/workflows/devcontainer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Development Container-Based CI 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | permissions: 13 | contents: read 14 | packages: write 15 | runs-on: ${{ matrix.runsOn }} 16 | strategy: 17 | matrix: 18 | runsOn: 19 | - ubuntu-24.04-arm 20 | - ubuntu-latest 21 | steps: 22 | - name: Checkout Repo 23 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 24 | - name: Login to GitHub Container Registry 25 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 26 | with: 27 | registry: ghcr.io 28 | username: ${{ github.repository_owner }} 29 | password: ${{ secrets.GITHUB_TOKEN }} 30 | - name: Set up Buildx 31 | uses: docker/setup-buildx-action@v3 32 | with: 33 | driver: docker-container 34 | - if: ${{ github.event_name != 'pull_request' }} 35 | id: cache_to_helper 36 | run: echo "cacheTo=ghcr.io/${{ github.repository_owner }}/devcontainers/dotslash" >> $GITHUB_OUTPUT 37 | - name: Build devcontainer image 38 | uses: devcontainers/ci@8bf61b26e9c3a98f69cb6ce2f88d24ff59b785c6 # v0.3.1900000417 39 | with: 40 | cacheFrom: ghcr.io/${{ github.repository_owner }}/devcontainers/dotslash 41 | cacheTo: ${{ steps.cache_to_helper.outputs.cacheTo }} 42 | env: | 43 | CI=1 44 | imageName: ghcr.io/${{ github.repository_owner }}/devcontainers/dotslash 45 | push: filter 46 | refFilterForPush: refs/heads/main 47 | 48 | lint: 49 | needs: build 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout Repo 53 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 54 | - uses: devcontainers/ci@8bf61b26e9c3a98f69cb6ce2f88d24ff59b785c6 # v0.3.1900000417 55 | with: 56 | cacheFrom: ghcr.io/${{ github.repository_owner }}/devcontainers/dotslash 57 | imageName: ghcr.io/${{ github.repository_owner }}/devcontainers/dotslash 58 | push: never 59 | runCmd: just check-all 60 | 61 | test-devcontainer-feature-autogenerated: 62 | needs: build 63 | runs-on: ${{ matrix.runsOn }} 64 | strategy: 65 | matrix: 66 | runsOn: 67 | - ubuntu-24.04-arm 68 | - ubuntu-latest 69 | steps: 70 | - name: Checkout Repo 71 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 72 | - uses: devcontainers/ci@8bf61b26e9c3a98f69cb6ce2f88d24ff59b785c6 # v0.3.1900000417 73 | with: 74 | cacheFrom: ghcr.io/${{ github.repository_owner }}/devcontainers/dotslash 75 | imageName: ghcr.io/${{ github.repository_owner }}/devcontainers/dotslash 76 | push: never 77 | runCmd: just test-feature-autogenerated 78 | 79 | test-devcontainer-feature-scenarios: 80 | needs: build 81 | runs-on: ${{ matrix.runsOn }} 82 | strategy: 83 | matrix: 84 | runsOn: 85 | - ubuntu-24.04-arm 86 | - ubuntu-latest 87 | steps: 88 | - name: Checkout Repo 89 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 90 | - uses: devcontainers/ci@8bf61b26e9c3a98f69cb6ce2f88d24ff59b785c6 # v0.3.1900000417 91 | with: 92 | cacheFrom: ghcr.io/${{ github.repository_owner }}/devcontainers/dotslash 93 | imageName: ghcr.io/${{ github.repository_owner }}/devcontainers/dotslash 94 | push: never 95 | runCmd: just test-feature-scenarios 96 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # 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 make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and 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 within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when there is a 56 | reasonable belief that an individual's behavior may have a negative impact on 57 | the project or its community. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported by contacting the project team at . All 63 | complaints will be reviewed and investigated and will result in a response that 64 | is deemed necessary and appropriate to the circumstances. The project team is 65 | obligated to maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted separately. 67 | 68 | Project maintainers who do not follow or enforce the Code of Conduct in good 69 | faith may face temporary or permanent repercussions as determined by other 70 | members of the project's leadership. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 75 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | 79 | For answers to common questions about this code of conduct, see 80 | https://www.contributor-covenant.org/faq 81 | -------------------------------------------------------------------------------- /src/util/progress.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use std::fs::File; 12 | use std::path::Path; 13 | use std::sync::mpsc; 14 | use std::sync::mpsc::Receiver; 15 | use std::sync::mpsc::Sender; 16 | use std::sync::mpsc::TryRecvError; 17 | use std::thread; 18 | use std::thread::JoinHandle; 19 | use std::time::Duration; 20 | 21 | /// A bit less than 80 chars so it fits on standard terminals. 22 | const NUM_PROGRESS_BAR_CHARS: u8 = 70; 23 | 24 | #[must_use] 25 | pub fn display_progress(content_length: u64, output_path: &Path) -> (Sender<()>, JoinHandle<()>) { 26 | let path = output_path.to_path_buf(); 27 | 28 | // Channel to inform the progress thread that the download has finished 29 | // early. This can be because of an error (`send` is dropped) or because 30 | // the `content_length` is incorrect (`send` sends `()`). 31 | let (send, recv) = mpsc::channel(); 32 | 33 | let handle = thread::spawn(move || { 34 | // This is the progress against NUM_PROGRESS_BAR_CHARS. 35 | let mut last_progress: u8 = 0; 36 | eprint!("[{}]", " ".repeat(NUM_PROGRESS_BAR_CHARS as usize)); 37 | 38 | // Poll for the creation of the file. 39 | let short_pause = Duration::from_millis(10); 40 | let output_file = loop { 41 | if should_end_progress(&recv) { 42 | return; 43 | } 44 | if let Ok(file) = File::open(&path) { 45 | break file; 46 | } 47 | // File was not created yet: pause and try again. 48 | thread::sleep(short_pause); 49 | }; 50 | 51 | let pause = Duration::from_millis(100); 52 | loop { 53 | let attr = output_file.metadata().unwrap(); 54 | let size = attr.len(); 55 | let is_complete = size >= content_length; 56 | let delta = if is_complete { 57 | NUM_PROGRESS_BAR_CHARS - last_progress 58 | } else { 59 | let current_progress = (f64::from(NUM_PROGRESS_BAR_CHARS) 60 | * (size as f64 / content_length as f64)) 61 | as u8; 62 | let delta = current_progress - last_progress; 63 | last_progress = current_progress; 64 | delta 65 | }; 66 | if delta != 0 && last_progress > 0 { 67 | let num_equals = last_progress - 1; 68 | let num_space = NUM_PROGRESS_BAR_CHARS - last_progress; 69 | // Admittedly, this is not the most efficient way to animate 70 | // the progress bar, but it is simple so that it works 71 | // cross-platform without pulling in a more heavyweight crate 72 | // for dealing with ANSI escape codes. 73 | eprint!( 74 | "\r[{}>{}]", 75 | "=".repeat(num_equals as usize), 76 | " ".repeat(num_space as usize) 77 | ); 78 | } 79 | 80 | if is_complete || should_end_progress(&recv) { 81 | eprintln!("\r[{}]", "=".repeat(NUM_PROGRESS_BAR_CHARS as usize)); 82 | break; 83 | } 84 | 85 | thread::sleep(pause); 86 | } 87 | }); 88 | 89 | (send, handle) 90 | } 91 | 92 | fn should_end_progress(recv: &Receiver<()>) -> bool { 93 | match recv.try_recv() { 94 | Ok(()) | Err(TryRecvError::Disconnected) => true, 95 | Err(TryRecvError::Empty) => false, 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # @generated by autocargo from //scm/dotslash/oss:[dotslash,dotslash_tests,parse_file] 2 | 3 | [package] 4 | name = "dotslash" 5 | version = "0.5.8" 6 | authors = ["Michael Bolin ", "Andres Suarez "] 7 | edition = "2024" 8 | rust-version = "1.85" 9 | description = "Command-line tool to facilitate fetching an executable, caching it, and then running it." 10 | readme = "README.md" 11 | homepage = "https://dotslash-cli.com" 12 | repository = "https://github.com/facebook/dotslash" 13 | license = "MIT OR Apache-2.0" 14 | keywords = ["cli"] 15 | include = ["/LICENSE-APACHE", "/LICENSE-MIT", "/README.md", "/src/**", "/tests/**"] 16 | 17 | [[bench]] 18 | name = "parse_file" 19 | harness = false 20 | 21 | [dependencies] 22 | anyhow = "1.0.98" 23 | blake3 = { version = "=1.8.2", features = ["mmap", "rayon", "traits-preview"] } 24 | bzip2 = "0.5" 25 | digest = "0.10" 26 | dirs = "6.0" 27 | dunce = "1.0.5" 28 | filetime = "0.2.25" 29 | flate2 = { version = "1.0.33", features = ["rust_backend"], default-features = false } 30 | fs2 = "0.4" 31 | jsonc-parser = { version = "0.26", features = ["serde"] } 32 | liblzma = { version = "0.4.4", features = ["parallel", "static"] } 33 | rand = { version = "0.8", features = ["small_rng"] } 34 | serde = { version = "1.0.219", features = ["derive", "rc"] } 35 | serde_json = "1.0.140" 36 | sha2 = "0.10.6" 37 | tar = "0.4.44" 38 | tempfile = "3.22" 39 | thiserror = "2.0.12" 40 | zip = { version = "3.0.0", features = ["deflate"], default-features = false } 41 | zstd = { version = "0.13", features = ["experimental", "zstdmt"] } 42 | 43 | [dev-dependencies] 44 | assert_matches = "1.5" 45 | buck-resources = "1" 46 | criterion = "0.5.1" 47 | snapbox = { version = "0.6.18", features = ["color-auto", "diff", "json", "regex"], default-features = false } 48 | 49 | [target.'cfg(target_os = "linux")'.dependencies] 50 | nix = { version = "0.30.1", features = ["dir", "event", "hostname", "inotify", "ioctl", "mman", "mount", "net", "poll", "ptrace", "reboot", "resource", "sched", "signal", "term", "time", "user", "zerocopy"] } 51 | 52 | [target.'cfg(target_os = "macos")'.dependencies] 53 | nix = { version = "0.30.1", features = ["dir", "event", "hostname", "inotify", "ioctl", "mman", "mount", "net", "poll", "ptrace", "reboot", "resource", "sched", "signal", "term", "time", "user", "zerocopy"] } 54 | 55 | [profile.release] 56 | lto = true 57 | codegen-units = 1 58 | strip = true 59 | 60 | [lints] 61 | clippy = { nursery = { level = "warn", priority = -2 }, pedantic = { level = "warn", priority = -2 }, allow_attributes = { level = "warn", priority = -1 }, bool_assert_comparison = { level = "allow", priority = -1 }, cast_lossless = { level = "allow", priority = -1 }, cast_possible_truncation = { level = "allow", priority = -1 }, cast_precision_loss = { level = "allow", priority = -1 }, cast_sign_loss = { level = "allow", priority = -1 }, cognitive_complexity = { level = "allow", priority = -1 }, derive_partial_eq_without_eq = { level = "allow", priority = -1 }, doc_markdown = { level = "allow", priority = -1 }, manual_string_new = { level = "allow", priority = -1 }, missing_const_for_fn = { level = "allow", priority = -1 }, missing_errors_doc = { level = "allow", priority = -1 }, missing_panics_doc = { level = "allow", priority = -1 }, module_name_repetitions = { level = "allow", priority = -1 }, no_effect_underscore_binding = { level = "allow", priority = -1 }, option_if_let_else = { level = "allow", priority = -1 }, struct_excessive_bools = { level = "allow", priority = -1 }, struct_field_names = { level = "allow", priority = -1 }, too_many_lines = { level = "allow", priority = -1 }, uninlined_format_args = { level = "allow", priority = -1 }, unreadable_literal = { level = "allow", priority = -1 }, use_self = { level = "allow", priority = -1 } } 62 | rust = { rust_2018_idioms = { level = "warn", priority = -2 }, unexpected_cfgs = { check-cfg = ["cfg(fbcode_build)", "cfg(dotslash_internal)"], level = "warn", priority = 0 } } 63 | -------------------------------------------------------------------------------- /src/util/display.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | //! `Display` wrappers for pretty printing. 12 | 13 | use std::fmt; 14 | use std::process::Command; 15 | use std::process::Output; 16 | 17 | /// TODO 18 | #[derive(Debug)] 19 | #[must_use] 20 | pub struct CommandDisplay<'a>(&'a Command); 21 | 22 | impl<'a> CommandDisplay<'a> { 23 | pub fn new(cmd: &'a Command) -> Self { 24 | CommandDisplay(cmd) 25 | } 26 | } 27 | 28 | impl fmt::Display for CommandDisplay<'_> { 29 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 30 | // TODO: Properly quote when necessary. 31 | f.write_str(&self.0.get_program().to_string_lossy())?; 32 | for arg in self.0.get_args() { 33 | f.write_str(" ")?; 34 | f.write_str(&arg.to_string_lossy())?; 35 | } 36 | Ok(()) 37 | } 38 | } 39 | 40 | /// TODO 41 | #[derive(Debug)] 42 | #[must_use] 43 | pub struct CommandStderrDisplay<'a>(&'a Output); 44 | 45 | impl<'a> CommandStderrDisplay<'a> { 46 | pub fn new(output: &'a Output) -> Self { 47 | CommandStderrDisplay(output) 48 | } 49 | } 50 | 51 | impl fmt::Display for CommandStderrDisplay<'_> { 52 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 53 | if self.0.status.success() { 54 | write!(f, "command finished with ")?; 55 | } else { 56 | write!(f, "command failed with ")?; 57 | } 58 | if let Some(exit_code) = self.0.status.code() { 59 | write!(f, "exit code {} and ", exit_code)?; 60 | } 61 | write!(f, "stderr: ")?; 62 | if self.0.stderr.is_empty() { 63 | write!(f, "(empty stderr)")?; 64 | } else { 65 | // TODO: Truncate stderr. 66 | write!(f, "{}", String::from_utf8_lossy(&self.0.stderr).trim_end())?; 67 | } 68 | Ok(()) 69 | } 70 | } 71 | 72 | /// Pretty sorted lists for use in error messages. 73 | /// 74 | /// - expected nothing 75 | /// - expected `a` 76 | /// - expected `a`, `b` 77 | /// - expected `a`, `b`, `c` 78 | #[derive(Clone, Debug)] 79 | pub struct ListOf(Vec); 80 | 81 | impl ListOf 82 | where 83 | T: fmt::Display + Ord, 84 | { 85 | pub fn new(it: I) -> Self 86 | where 87 | I: IntoIterator, 88 | { 89 | let mut list = it.into_iter().collect::>(); 90 | list.sort(); 91 | ListOf(list) 92 | } 93 | } 94 | 95 | impl fmt::Display for ListOf 96 | where 97 | T: fmt::Display, 98 | { 99 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 100 | let mut it = self.0.iter(); 101 | match it.next() { 102 | None => write!(f, "nothing"), 103 | Some(first) => { 104 | write!(f, "`{}`", first)?; 105 | for item in it { 106 | write!(f, ", `{}`", item)?; 107 | } 108 | Ok(()) 109 | } 110 | } 111 | } 112 | } 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use super::*; 117 | 118 | #[test] 119 | fn test_list_of() { 120 | assert_eq!(format!("{}", ListOf::new(&[] as &[&str])), "nothing"); 121 | assert_eq!(format!("{}", ListOf::new(&["a"])), "`a`"); 122 | assert_eq!(format!("{}", ListOf::new(&["a", "b"])), "`a`, `b`"); 123 | assert_eq!( 124 | format!("{}", ListOf::new(&["c", "a", "b"])), 125 | "`a`, `b`, `c`", 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 3 | */ 4 | 5 | const lightCodeTheme = require('prism-react-renderer/themes/github'); 6 | const darkCodeTheme = require('prism-react-renderer/themes/dracula'); 7 | 8 | // With JSDoc @type annotations, IDEs can provide config autocompletion 9 | /** @type {import('@docusaurus/types').DocusaurusConfig} */ 10 | (module.exports = { 11 | title: 'DotSlash', 12 | tagline: 'Simplified executable deployment.', 13 | url: 'https://dotslash-cli.com', 14 | baseUrl: '/', 15 | onBrokenLinks: 'throw', 16 | onBrokenMarkdownLinks: 'throw', 17 | trailingSlash: true, 18 | favicon: 'img/favicon-on-black.svg', 19 | organizationName: 'facebook', 20 | projectName: 'dotslash', 21 | customFields: { 22 | fbRepoName: 'fbsource', 23 | ossRepoPath: 'fbcode/scm/dotslash/website', 24 | }, 25 | 26 | presets: [ 27 | [ 28 | 'docusaurus-plugin-internaldocs-fb/docusaurus-preset', 29 | /** @type {import('docusaurus-plugin-internaldocs-fb').PresetOptions} */ 30 | ({ 31 | docs: { 32 | sidebarPath: require.resolve('./sidebars.js'), 33 | editUrl: 'https://github.com/facebook/dotslash/tree/main/website', 34 | }, 35 | experimentalXRepoSnippets: { 36 | baseDir: '.', 37 | }, 38 | staticDocsProject: 'dotslash', 39 | trackingFile: 'fbcode/staticdocs/WATCHED_FILES', 40 | theme: { 41 | customCss: require.resolve('./src/css/custom.css'), 42 | }, 43 | }), 44 | ], 45 | ], 46 | 47 | themeConfig: 48 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 49 | ({ 50 | navbar: { 51 | title: 'DotSlash', 52 | logo: { 53 | alt: 'DotSlash logo', 54 | src: 'img/favicon-on-black.svg', 55 | style: { 56 | 'border-radius': '5px', 57 | } 58 | }, 59 | items: [ 60 | { 61 | type: 'doc', 62 | docId: 'index', 63 | position: 'left', 64 | label: 'Docs', 65 | }, 66 | { 67 | href: 'https://github.com/facebook/dotslash', 68 | label: 'GitHub', 69 | position: 'right', 70 | }, 71 | ], 72 | }, 73 | footer: { 74 | style: 'dark', 75 | links: [ 76 | { 77 | title: 'Docs', 78 | items: [ 79 | { 80 | label: 'Introduction', 81 | to: '/docs/', 82 | }, 83 | { 84 | label: 'Motivation', 85 | to: '/docs/motivation/', 86 | }, 87 | ], 88 | }, 89 | { 90 | title: 'Community', 91 | items: [ 92 | { 93 | label: 'Stack Overflow', 94 | href: 'https://stackoverflow.com/questions/tagged/dotslash', 95 | }, 96 | { 97 | label: 'GitHub issues', 98 | href: 'https://github.com/facebook/dotslash/issues', 99 | }, 100 | ], 101 | }, 102 | { 103 | title: 'More', 104 | items: [ 105 | { 106 | label: 'Code', 107 | href: 'https://github.com/facebook/dotslash', 108 | }, 109 | { 110 | label: 'Terms of Use', 111 | href: 'https://opensource.fb.com/legal/terms', 112 | }, 113 | { 114 | label: 'Privacy Policy', 115 | href: 'https://opensource.fb.com/legal/privacy', 116 | }, 117 | ], 118 | }, 119 | ], 120 | copyright: `Copyright © ${new Date().getFullYear()} Meta Platforms, Inc. Built with Docusaurus.`, 121 | }, 122 | prism: { 123 | theme: lightCodeTheme, 124 | darkTheme: darkCodeTheme, 125 | }, 126 | }), 127 | }); 128 | -------------------------------------------------------------------------------- /src/util/mv_no_clobber.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use std::io; 12 | use std::path::Path; 13 | use std::thread; 14 | use std::time::Duration; 15 | 16 | use crate::util::fs_ctx; 17 | 18 | /// Move a file or directory only if destination does not exist. 19 | /// 20 | /// This is conceptually equivalent to `mv --no-clobber`, though like `mv -n`, 21 | /// this is susceptible to a TOCTTOU issue because another process could create 22 | /// the file at the destination between the initial check for the destination and 23 | /// the write. 24 | /// 25 | /// If no move is performed because the destination already exists, this function 26 | /// returns Ok, not Err. 27 | /// 28 | /// TODO(T57290904): When possible, use platform-specific syscalls to make this 29 | /// atomic. Specifically, the following should be available in newer OS versions: 30 | /// * Linux: renameat2 with RENAME_NOREPLACE flag 31 | /// * macOS: renamex_np with RENAME_EXCL flag 32 | pub fn mv_no_clobber, Q: AsRef>(from: P, to: Q) -> io::Result<()> { 33 | fn mv_no_clobber_impl(from: &Path, to: &Path) -> io::Result<()> { 34 | // If the destination already exists, do nothing. 35 | if to.exists() { 36 | return Ok(()); 37 | } 38 | 39 | match fs_ctx::rename(from, to) { 40 | Ok(()) => Ok(()), 41 | Err(error) => { 42 | // If the rename failed, but the destination exists now, 43 | // assume we hit the TOCTTOU case and return Ok. 44 | if to.exists() { Ok(()) } else { Err(error) } 45 | } 46 | } 47 | } 48 | 49 | if cfg!(windows) { 50 | // It is a mystery why we get a permission error on Windows, that 51 | // then quickly clears up. This seems to happen when the system is 52 | // under heavy load. 53 | for wait in [1, 4, 9] { 54 | match mv_no_clobber_impl(from.as_ref(), to.as_ref()) { 55 | Err(err) if err.kind() == io::ErrorKind::PermissionDenied => { 56 | thread::sleep(Duration::from_secs(wait)); 57 | } 58 | ret => return ret, 59 | } 60 | } 61 | } 62 | 63 | mv_no_clobber_impl(from.as_ref(), to.as_ref()) 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use std::fs; 69 | 70 | use tempfile::NamedTempFile; 71 | 72 | use super::*; 73 | 74 | #[test] 75 | fn test_file() { 76 | let temp_path_1 = NamedTempFile::new().unwrap().into_temp_path(); 77 | let temp_path_2 = NamedTempFile::new().unwrap().into_temp_path(); 78 | 79 | assert!(temp_path_1.exists()); 80 | assert!(temp_path_2.exists()); 81 | 82 | // Should not fail just because dest exists. 83 | mv_no_clobber(&temp_path_1, &temp_path_2).unwrap(); 84 | 85 | assert!(temp_path_1.exists()); 86 | assert!(temp_path_2.exists()); 87 | 88 | fs::remove_file(&temp_path_2).unwrap(); 89 | 90 | assert!(temp_path_1.exists()); 91 | assert!(!temp_path_2.exists()); 92 | 93 | mv_no_clobber(&temp_path_1, &temp_path_2).unwrap(); 94 | 95 | assert!(!temp_path_1.exists()); 96 | assert!(temp_path_2.exists()); 97 | } 98 | 99 | #[test] 100 | fn test_directory() { 101 | let temp_dir_1 = tempfile::tempdir().unwrap(); 102 | let temp_dir_2 = tempfile::tempdir().unwrap(); 103 | 104 | assert!(temp_dir_1.path().exists()); 105 | assert!(temp_dir_2.path().exists()); 106 | 107 | // Should not fail just because dest exists. 108 | mv_no_clobber(&temp_dir_1, &temp_dir_2).unwrap(); 109 | 110 | assert!(temp_dir_1.path().exists()); 111 | assert!(temp_dir_2.path().exists()); 112 | 113 | fs::remove_dir_all(&temp_dir_2).unwrap(); 114 | 115 | assert!(temp_dir_1.path().exists()); 116 | assert!(!temp_dir_2.path().exists()); 117 | 118 | mv_no_clobber(&temp_dir_1, &temp_dir_2).unwrap(); 119 | 120 | assert!(!temp_dir_1.path().exists()); 121 | assert!(temp_dir_2.path().exists()); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | # DotSlash: simplified executable deployment 4 | 5 | ![License] [![Build Status]][CI] 6 | 7 | [License]: 8 | https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blueviolet.svg 9 | [Build Status]: 10 | https://github.com/facebook/dotslash/actions/workflows/build.yml/badge.svg?branch=main 11 | [CI]: https://github.com/facebook/dotslash/actions/workflows/build.yml 12 | 13 |
14 | 15 | DotSlash (`dotslash`) is a command-line tool that lets you represent a set of 16 | platform-specific, heavyweight executables with an equivalent small, 17 | easy-to-read text file. In turn, this makes it efficient to store executables in 18 | source control without hurting repository size. This paves the way for checking 19 | build toolchains and other tools directly into the repo, reducing dependencies 20 | on the host environment and thereby facilitating reproducible builds. 21 | 22 | We will illustrate this with 23 | [an example taken from the DotSlash website](https://dotslash-cli.com/docs/). 24 | Traditionally, if you want to vendor a specific version of Node.js into your 25 | project and you want to support both macOS and Linux, you likely need at least 26 | two binaries (one for macOS and one for Linux) as well as a shell script like 27 | this: 28 | 29 | ```shell 30 | #!/bin/bash 31 | 32 | # Copied from https://stackoverflow.com/a/246128. 33 | DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 34 | 35 | if [ "$(uname)" == "Darwin" ]; then 36 | # In this example, assume node-mac-v18.16.0 is a universal macOS binary. 37 | "$DIR/node-mac-v18.16.0" "$@" 38 | else 39 | "$DIR/node-linux-v18.16.0" "$@" 40 | fi 41 | 42 | exit $? 43 | ``` 44 | 45 | With DotSlash, the shell script and the binaries can be replaced with a single 46 | file named `node`: 47 | 48 | ```jsonc 49 | #!/usr/bin/env dotslash 50 | 51 | // The URLs in this file were taken from https://nodejs.org/dist/v18.19.0/ 52 | 53 | { 54 | "name": "node-v18.19.0", 55 | "platforms": { 56 | "macos-aarch64": { 57 | "size": 40660307, 58 | "hash": "blake3", 59 | "digest": "6e2ca33951e586e7670016dd9e503d028454bf9249d5ff556347c3d98c347c34", 60 | "format": "tar.gz", 61 | "path": "node-v18.19.0-darwin-arm64/bin/node", 62 | "providers": [ 63 | { 64 | "url": "https://nodejs.org/dist/v18.19.0/node-v18.19.0-darwin-arm64.tar.gz" 65 | } 66 | ] 67 | }, 68 | // Note that with DotSlash, it is straightforward to specify separate 69 | // binaries for different platforms, such as x86 vs. arm64 on macOS. 70 | "macos-x86_64": { 71 | "size": 42202872, 72 | "hash": "blake3", 73 | "digest": "37521058114e7f71e0de3fe8042c8fa7908305e9115488c6c29b514f9cd2a24c", 74 | "format": "tar.gz", 75 | "path": "node-v18.19.0-darwin-x64/bin/node", 76 | "providers": [ 77 | { 78 | "url": "https://nodejs.org/dist/v18.19.0/node-v18.19.0-darwin-x64.tar.gz" 79 | } 80 | ] 81 | }, 82 | "linux-x86_64": { 83 | "size": 44694523, 84 | "hash": "blake3", 85 | "digest": "72b81fc3a30b7bedc1a09a3fafc4478a1b02e5ebf0ad04ea15d23b3e9dc89212", 86 | "format": "tar.gz", 87 | "path": "node-v18.19.0-linux-x64/bin/node", 88 | "providers": [ 89 | { 90 | "url": "https://nodejs.org/dist/v18.19.0/node-v18.19.0-linux-x64.tar.gz" 91 | } 92 | ] 93 | } 94 | } 95 | } 96 | ``` 97 | 98 | Assuming `dotslash` is on your `$PATH` and you remembered to `chmod +x node` to 99 | mark it as executable, you can now run your Node.js wrapper exactly as you did 100 | before: 101 | 102 | ```shell 103 | $ ./node --version 104 | v18.16.0 105 | ``` 106 | 107 | The first time you run `./node --version`, you will likely experience a small 108 | delay while DotSlash fetches, decompresses, and verifies the appropriate 109 | `.tar.gz`, but subsequent invocations should be instantaneous. 110 | 111 | To understand what is happening under the hood, read the article on 112 | [how DotSlash works](https://dotslash-cli.com/docs/execution/). 113 | 114 | ## Installing DotSlash 115 | 116 | See the [installation instructions](https://dotslash-cli.com/docs/installation/) 117 | on the DotSlash website. 118 | 119 | ## License 120 | 121 | DotSlash is licensed under both the MIT license and Apache-2.0 license; the 122 | exact terms can be found in the [LICENSE-MIT](LICENSE-MIT) and 123 | [LICENSE-APACHE](LICENSE-APACHE) files, respectively. 124 | -------------------------------------------------------------------------------- /src/dotslash_cache.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use std::env; 12 | use std::ffi::OsString; 13 | use std::path::Path; 14 | use std::path::PathBuf; 15 | 16 | #[cfg(unix)] 17 | use nix::unistd; 18 | 19 | #[cfg(unix)] 20 | use crate::util; 21 | 22 | pub const DOTSLASH_CACHE_ENV: &str = "DOTSLASH_CACHE"; 23 | 24 | #[derive(Debug)] 25 | pub struct DotslashCache { 26 | cache_dir: PathBuf, 27 | } 28 | 29 | /// The DotSlash cache is organized as follows: 30 | /// - Any subfolder that starts with two lowercase hex digits is the parent 31 | /// folder for artifacts whose *artifact hash* starts with those two hex 32 | /// digits (see `ArtifactLocation::artifact_directory`). 33 | /// - The only other subfolder is `locks/`, which internally is organized 34 | /// to the root of the cache folder. 35 | /// 36 | /// The motivation behind this organization is to keep the paths to artifacts 37 | /// as short as reasonably possible to avoid exceeding `MAX_PATH` on Windows. 38 | /// The `locks/` folder is kept separate so it can be blown away independent of 39 | /// the artifacts. 40 | impl DotslashCache { 41 | pub fn new() -> Self { 42 | Self::new_in(get_dotslash_cache()) 43 | } 44 | 45 | pub fn new_in>(p: P) -> Self { 46 | Self { 47 | cache_dir: p.into(), 48 | } 49 | } 50 | 51 | pub fn cache_dir(&self) -> &Path { 52 | &self.cache_dir 53 | } 54 | 55 | pub fn artifacts_dir(&self) -> &Path { 56 | &self.cache_dir 57 | } 58 | 59 | /// artifact_hash_prefix should be two lowercase hex digits. 60 | pub fn locks_dir(&self, artifact_hash_prefix: &str) -> PathBuf { 61 | self.cache_dir.join("locks").join(artifact_hash_prefix) 62 | } 63 | } 64 | 65 | impl Default for DotslashCache { 66 | fn default() -> Self { 67 | Self::new() 68 | } 69 | } 70 | 71 | /// Return the directory where DotSlash should write its cached artifacts. 72 | /// Although DotSlash does not currently have any global config files, 73 | /// if it did, most platforms would prefer config files to be stored in 74 | /// a separate directory that is backed up and should not be blown away 75 | /// when the user is low on space like /tmp. 76 | fn get_dotslash_cache() -> PathBuf { 77 | if let Some(val) = env::var_os(DOTSLASH_CACHE_ENV) { 78 | return PathBuf::from(val); 79 | } 80 | 81 | // `dirs` returns the preferred cache directory for the user and the 82 | // platform based on these rules: https://docs.rs/dirs/*/dirs/fn.cache_dir.html 83 | let cache_dir = match dirs::cache_dir() { 84 | Some(cache_dir) => cache_dir.join("dotslash"), 85 | None => panic!("could not find DotSlash root - specify $DOTSLASH_CACHE"), 86 | }; 87 | 88 | // `dirs` relies on `$HOME`. When running under `sudo` `$HOME` may not be 89 | // the sudoer's home dir. We want to avoid the situation where some 90 | // privileged user (like `root`) owns the cache dir in some other user's 91 | // home dir. 92 | // 93 | // Note that on a devserver (and macOS is basically the same): 94 | // 95 | // ``` 96 | // $ bash -c 'echo $SUDO_USER $USER $HOME' 97 | // asuarez asuarez /home/asuarez 98 | // $ sudo bash -c 'echo $SUDO_USER $USER $HOME' 99 | // asuarez root /home/asuarez 100 | // $ sudo -H bash -c 'echo $SUDO_USER $USER $HOME' 101 | // asuarez root /root 102 | // ``` 103 | // 104 | // i.e., `$USER` is reliable in the presence of sudo but `$HOME` is not. 105 | #[cfg(unix)] 106 | if !util::is_path_safe_to_own(&cache_dir) { 107 | let temp_dir = env::temp_dir(); 108 | // e.g. $TEMP/dotslash-UID 109 | return named_cache_dir_at(temp_dir); 110 | } 111 | 112 | cache_dir 113 | } 114 | 115 | #[cfg_attr(windows, expect(dead_code))] 116 | fn named_cache_dir_at>(dir: P) -> PathBuf { 117 | let mut name = OsString::from("dotslash-"); 118 | 119 | // e.g. dotslash-UID 120 | #[cfg(unix)] 121 | name.push(unistd::getuid().as_raw().to_string()); 122 | 123 | // e.g. dotslash-$USERNAME 124 | #[cfg(windows)] 125 | name.push(env::var_os("USERNAME").unwrap_or_else(|| "".into())); 126 | 127 | // e.g. $DIR/dotslash-UID 128 | let mut dir = dir.into(); 129 | dir.push(name); 130 | 131 | dir 132 | } 133 | -------------------------------------------------------------------------------- /src/github_release_provider.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use std::path::Path; 12 | use std::process::Command; 13 | 14 | use anyhow::Context as _; 15 | use serde::Deserialize; 16 | use serde_json::Value; 17 | 18 | use crate::config::ArtifactEntry; 19 | use crate::provider::Provider; 20 | use crate::util::CommandDisplay; 21 | use crate::util::CommandStderrDisplay; 22 | use crate::util::FileLock; 23 | 24 | pub struct GitHubReleaseProvider {} 25 | 26 | #[derive(Deserialize, Debug)] 27 | struct GitHubReleaseProviderConfig { 28 | tag: String, 29 | repo: String, 30 | name: String, 31 | } 32 | 33 | impl Provider for GitHubReleaseProvider { 34 | fn fetch_artifact( 35 | &self, 36 | provider_config: &Value, 37 | destination: &Path, 38 | _fetch_lock: &FileLock, 39 | _artifact_entry: &ArtifactEntry, 40 | ) -> anyhow::Result<()> { 41 | let GitHubReleaseProviderConfig { tag, repo, name } = <_>::deserialize(provider_config)?; 42 | let mut command = Command::new("gh"); 43 | command 44 | .arg("release") 45 | .arg("download") 46 | .arg(tag) 47 | .arg("--repo") 48 | .arg(repo) 49 | .arg("--pattern") 50 | // We want to match an a release by name, but unfortunately, 51 | // `gh release download` only supports --pattern, which takes a 52 | // regex. Adding ^ and $ as anchors only seems to break things. 53 | .arg(regex_escape(&name)) 54 | .arg("--output") 55 | .arg(destination); 56 | 57 | let output = command 58 | .output() 59 | .with_context(|| format!("{}", CommandDisplay::new(&command))) 60 | .context("failed to run the GitHub CLI")?; 61 | 62 | if !output.status.success() { 63 | return Err(anyhow::format_err!( 64 | "{}", 65 | CommandStderrDisplay::new(&output) 66 | )) 67 | .with_context(|| format!("{}", CommandDisplay::new(&command))) 68 | .context("the GitHub CLI failed"); 69 | } 70 | 71 | Ok(()) 72 | } 73 | } 74 | 75 | /// We want the functionality comparable to regex::escape() without pulling in 76 | /// the entire crate. 77 | fn regex_escape(s: &str) -> String { 78 | s.chars().fold( 79 | // Releases filenames likely have at least one `.` in there that needs 80 | // to be escaped, so add some padding, by default. 81 | String::with_capacity(s.len() + 4), 82 | |mut output, c| { 83 | if let '\\' | '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' 84 | | '$' = c 85 | { 86 | output.push('\\'); 87 | } 88 | output.push(c); 89 | output 90 | }, 91 | ) 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use super::*; 97 | 98 | #[test] 99 | fn regex_escape_no_quotable_chars() { 100 | assert_eq!("foo", regex_escape("foo")); 101 | assert_eq!("FOO", regex_escape("FOO")); 102 | // Spaces do not get escaped. 103 | assert_eq!("foo bar", regex_escape("foo bar")); 104 | // Angle brackets do not get escaped. 105 | assert_eq!("", regex_escape("")); 106 | // Slashes do not get escaped. 107 | assert_eq!("foo/bar", regex_escape("foo/bar")); 108 | } 109 | 110 | #[test] 111 | fn regex_escape_punctuation() { 112 | assert_eq!("abc\\.tar\\.gz", regex_escape("abc.tar.gz")); 113 | assert_eq!("what\\?!\\?", regex_escape("what?!?")); 114 | } 115 | 116 | #[test] 117 | fn regex_escape_brackets() { 118 | assert_eq!("\\[abc\\]", regex_escape("[abc]")); 119 | assert_eq!("\\{abc\\}", regex_escape("{abc}")); 120 | assert_eq!("\\(abc\\)", regex_escape("(abc)")); 121 | } 122 | 123 | #[test] 124 | fn regex_escape_anchors() { 125 | assert_eq!("\\^foobarbaz\\$", regex_escape("^foobarbaz$")); 126 | } 127 | 128 | #[test] 129 | fn regex_escape_quantifiers() { 130 | assert_eq!("https\\?://", regex_escape("https?://")); 131 | assert_eq!("foo\\+foo\\+", regex_escape("foo+foo+")); 132 | assert_eq!("foo\\*foo\\*", regex_escape("foo*foo*")); 133 | } 134 | 135 | #[test] 136 | fn regex_escape_alternation() { 137 | assert_eq!("foo\\|bar", regex_escape("foo|bar")); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /.github/workflows/release-downstream.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: publish downstream packages 3 | 4 | on: 5 | workflow_call: 6 | inputs: 7 | tag: 8 | description: The release tag to publish 9 | required: true 10 | type: string 11 | 12 | defaults: 13 | run: 14 | shell: bash 15 | 16 | jobs: 17 | nodejs-publish: 18 | name: Publish Node.js release 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: read 22 | id-token: write 23 | 24 | defaults: 25 | run: 26 | working-directory: node 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 30 | 31 | - name: Setup Node.js 32 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 33 | with: 34 | node-version: "20.x" 35 | registry-url: "https://registry.npmjs.org" 36 | 37 | - name: Install dependencies cleanly 38 | run: npm ci 39 | 40 | - name: Build package 41 | run: npm run build -- --tag ${{ inputs.tag }} 42 | 43 | - name: Publish to NPM 44 | run: npm publish --provenance --access public 45 | env: 46 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 47 | 48 | python-sdist: 49 | name: Build source distribution 50 | runs-on: ubuntu-latest 51 | 52 | outputs: 53 | artifact-name: ${{ steps.locate-artifact.outputs.file-name }} 54 | 55 | defaults: 56 | run: 57 | working-directory: python 58 | steps: 59 | - name: Checkout repository 60 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 61 | 62 | - name: Install UV 63 | uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 64 | 65 | - name: Build source distribution 66 | run: uv build --sdist 67 | env: 68 | DOTSLASH_VERSION: ${{ inputs.tag }} 69 | 70 | - name: Locate source distribution 71 | id: locate-artifact 72 | run: |- 73 | sdist_name=$(basename dist/*) 74 | echo "file-name=${sdist_name}" >> $GITHUB_OUTPUT 75 | 76 | - name: Upload source distribution artifact 77 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 78 | with: 79 | name: artifact-sdist 80 | path: dist/${{ steps.locate-artifact.outputs.file-name }} 81 | if-no-files-found: error 82 | 83 | python-wheels: 84 | name: Build wheels for ${{ matrix.archs }} on ${{ matrix.os }} 85 | needs: 86 | - python-sdist 87 | runs-on: ${{ matrix.os }} 88 | strategy: 89 | fail-fast: false 90 | matrix: 91 | include: 92 | - os: ubuntu-24.04-arm 93 | archs: aarch64 94 | - os: ubuntu-latest 95 | archs: x86_64 96 | - os: macos-latest 97 | archs: arm64 98 | - os: macos-15-intel 99 | archs: x86_64 100 | - os: windows-11-arm 101 | archs: ARM64 102 | - os: windows-latest 103 | archs: AMD64 104 | 105 | defaults: 106 | run: 107 | working-directory: python 108 | steps: 109 | - name: Checkout repository 110 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 111 | 112 | - name: Download source distribution 113 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 114 | with: 115 | name: artifact-sdist 116 | path: dist 117 | 118 | # TODO: Remove this once the action supports specifying extras, see: 119 | # https://github.com/pypa/cibuildwheel/pull/2630 120 | - name: Install UV 121 | if: runner.os != 'Linux' 122 | uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 123 | 124 | - name: Build wheels 125 | uses: pypa/cibuildwheel@9c00cb4f6b517705a3794b22395aedc36257242c # v3.2.1 126 | with: 127 | package-dir: dist/${{ needs.python-sdist.outputs.artifact-name }} 128 | env: 129 | DOTSLASH_VERSION: ${{ inputs.tag }} 130 | CIBW_ARCHS: ${{ matrix.archs }} 131 | 132 | - name: Upload wheel artifacts 133 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 134 | with: 135 | name: artifact-wheels-${{ matrix.os }}-${{ matrix.archs }} 136 | path: wheelhouse/*.whl 137 | if-no-files-found: error 138 | 139 | python-publish: 140 | name: Publish Python release 141 | needs: 142 | - python-sdist 143 | - python-wheels 144 | runs-on: ubuntu-latest 145 | 146 | permissions: 147 | id-token: write 148 | 149 | steps: 150 | - name: Download artifacts 151 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 152 | with: 153 | pattern: artifact-* 154 | merge-multiple: true 155 | path: dist 156 | 157 | - name: Push build artifacts to PyPI 158 | uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 159 | with: 160 | skip-existing: true 161 | -------------------------------------------------------------------------------- /src/util/unarchive.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is dual-licensed under either the MIT license found in the 5 | * LICENSE-MIT file in the root directory of this source tree or the Apache 6 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 7 | * of this source tree. You may select, at your option, one of the 8 | * above-listed licenses. 9 | */ 10 | 11 | use std::io; 12 | use std::io::BufRead; 13 | use std::io::Read; 14 | use std::io::Seek; 15 | use std::path::Path; 16 | 17 | use bzip2::read::BzDecoder; 18 | use flate2::bufread::GzDecoder; 19 | #[cfg(not(dotslash_internal))] 20 | use liblzma::bufread::XzDecoder; 21 | use tar::Archive; 22 | #[cfg(not(dotslash_internal))] 23 | use zip::ZipArchive; 24 | use zstd::stream::read::Decoder as ZstdDecoder; 25 | 26 | use crate::util::fs_ctx; 27 | 28 | #[derive(Copy, Clone)] 29 | pub enum ArchiveType { 30 | Tar, 31 | #[cfg(not(dotslash_internal))] 32 | Bzip2, 33 | TarBzip2, 34 | #[cfg(not(dotslash_internal))] 35 | Gz, 36 | TarGz, 37 | #[cfg(not(dotslash_internal))] 38 | Xz, 39 | #[cfg(not(dotslash_internal))] 40 | TarXz, 41 | #[cfg(not(dotslash_internal))] 42 | Zstd, 43 | TarZstd, 44 | #[cfg(not(dotslash_internal))] 45 | Zip, 46 | } 47 | 48 | /// Attempts to extract the tar/zip archive into the specified directory 49 | /// or file. 50 | /// 51 | /// To extract tars, this uses the tar crate (https://crates.io/crates/tar) 52 | /// directly. Those who create compressed artifacts for DotSlash are 53 | /// responsible for ensuring they can be decompressed with its version of tar. 54 | pub fn unarchive(reader: R, destination: &Path, archive_type: ArchiveType) -> io::Result<()> 55 | where 56 | R: BufRead + Seek, 57 | { 58 | match archive_type { 59 | ArchiveType::Tar => unpack_tar(reader, destination), 60 | 61 | #[cfg(not(dotslash_internal))] 62 | ArchiveType::Bzip2 => write_out(BzDecoder::new(reader), destination), 63 | ArchiveType::TarBzip2 => unpack_tar(BzDecoder::new(reader), destination), 64 | 65 | #[cfg(not(dotslash_internal))] 66 | ArchiveType::Gz => write_out(GzDecoder::new(reader), destination), 67 | ArchiveType::TarGz => unpack_tar(GzDecoder::new(reader), destination), 68 | 69 | #[cfg(not(dotslash_internal))] 70 | ArchiveType::Xz => write_out(XzDecoder::new(reader), destination), 71 | #[cfg(not(dotslash_internal))] 72 | ArchiveType::TarXz => unpack_tar(XzDecoder::new(reader), destination), 73 | 74 | #[cfg(not(dotslash_internal))] 75 | ArchiveType::Zstd => write_out(ZstdDecoder::with_buffer(reader)?, destination), 76 | ArchiveType::TarZstd => unpack_tar(ZstdDecoder::with_buffer(reader)?, destination), 77 | 78 | #[cfg(not(dotslash_internal))] 79 | ArchiveType::Zip => { 80 | let destination = fs_ctx::canonicalize(destination)?; 81 | let mut archive = ZipArchive::new(reader)?; 82 | archive.extract(destination)?; 83 | Ok(()) 84 | } 85 | } 86 | } 87 | 88 | #[cfg(not(dotslash_internal))] 89 | fn write_out(mut reader: R, destination_dir: &Path) -> io::Result<()> 90 | where 91 | R: Read, 92 | { 93 | let mut output_file = fs_ctx::file_create(destination_dir)?; 94 | io::copy(&mut reader, &mut output_file)?; 95 | Ok(()) 96 | } 97 | 98 | fn unpack_tar(reader: R, destination_dir: &Path) -> io::Result<()> 99 | where 100 | R: Read, 101 | { 102 | // The destination dir is canonicalized for the benefit of Windows, but we 103 | // do it on all platforms for consistency of behavior. 104 | // 105 | // Windows has a path length limit of 255 chars. "Extended-length paths"[1] 106 | // are paths starting with `\\?\`. These are not subject to the length 107 | // limit, but have other issues: they cannot use forward slashes. 108 | // 109 | // `fs::canonicalize` will both prefix the path with `\\?\` and normalize 110 | // the slashes[2]. This is important because we don't know the depth of the 111 | // tarball file structure (so we need to avoid possible path length 112 | // limits), and we don't know if the destination path is mixing slashes. 113 | // 114 | // We only use extended-length paths here and not earlier because you 115 | // can't exec `.bat` files with `\\?\` (although `.exe` files are ok). 116 | // 117 | // We canonicalize for all platforms because `fs::canonicalize` can 118 | // error[3] and not everyone can test on Windows. 119 | // 120 | // [1] https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maxpath 121 | // [2] https://doc.rust-lang.org/std/fs/fn.canonicalize.html#platform-specific-behavior 122 | // [3] https://doc.rust-lang.org/std/fs/fn.canonicalize.html#errors 123 | 124 | let destination_dir = fs_ctx::canonicalize(destination_dir)?; 125 | 126 | let mut archive = Archive::new(reader); 127 | archive.set_preserve_permissions(true); 128 | archive.set_preserve_mtime(true); 129 | archive.unpack(destination_dir) 130 | } 131 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.5.8 (2025-08-19) 4 | 5 | - DotSlash is now available as an 6 | [npm package](https://www.npmjs.com/package/fb-dotslash): 7 | 8 | - Optimized the size of the 9 | [DotSlash Windows shim](https://dotslash-cli.com/docs/windows/#dotslash-windows-shim): 10 | 11 | 12 | ## v0.5.7 (2025-07-09) 13 | 14 | - Fix release pipeline for ARM64 Windows: 15 | 16 | - v0.5.6 was never published because of this. 17 | 18 | ## v0.5.6 (2025-07-08) 19 | 20 | - [Fixed a bug](https://github.com/facebook/dotslash/pull/75) where DotSlash 21 | would sometimes write corrupted zip files to its cache 22 | - One-liner installations are now possible again, see 23 | [the new installation instructions](https://dotslash-cli.com/docs/installation/#prebuilt-binaries) 24 | - [ARM64 Windows binaries](https://github.com/facebook/dotslash/pull/76) for 25 | DotSlash are available 26 | 27 | ## v0.5.5 (2025-06-25) 28 | 29 | - Added support for 30 | [provider order randomization](https://github.com/facebook/dotslash/pull/49) 31 | - Added support for 32 | [.bz2 and .tar.bz2](https://github.com/facebook/dotslash/pull/53) 33 | 34 | Additionally, as of this release we are now attaching binaries to releases that 35 | don't have the release version in the filename. These files are in addition to 36 | the files that mention the version number for backwards compatibility with the 37 | `install-dotslash` action. WARNING: We will be removing the legacy versioned 38 | filenames in a future release, follow 39 | [this issue](https://github.com/facebook/dotslash/issues/68). 40 | 41 | ## v0.5.4 (2025-05-19) 42 | 43 | - Reverted "One-liner installations are now possible, see 44 | [the new installation instructions](https://dotslash-cli.com/docs/installation/#prebuilt-binaries) 45 | " 46 | 47 | ## v0.5.3 (2025-05-19) 48 | 49 | - One-liner installations are now possible, see 50 | [the new installation instructions](https://dotslash-cli.com/docs/installation/#prebuilt-binaries) 51 | 52 | - Precompiled arch-specific binaries are now available for macOS: 53 | 54 | 55 | ## v0.5.2 (2025-02-05) 56 | 57 | - Include experimental commands in --help: 58 | 59 | 60 | ## v0.5.1 (2025-02-03) 61 | 62 | - Improved the error message for GitHub provider auth failures: 63 | 64 | 65 | ## v0.5.0 (2025-01-13) 66 | 67 | - Added `arg0` artifact entry config field: 68 | 69 | - MSRV 1.83. 70 | 71 | ## v0.4.3 (2024-10-13) 72 | 73 | - Fix MUSL aarch64 linux releases: 74 | 75 | - v0.4.2 was never actually published because of this. 76 | 77 | ## v0.4.2 (2024-10-11) 78 | 79 | - Added MUSL Linux releases: 80 | - Many dependency updates, but key among them is 81 | [`tar` 0.4.40 to 0.4.42](https://github.com/facebook/dotslash/commit/4ee240e788eaaa7ddad15a835819fb624d1f11f6). 82 | 83 | ## v0.4.1 (2024-04-10) 84 | 85 | - Fixed macos builds 86 | 87 | 88 | ## v0.4.0 (2024-04-10) 89 | 90 | - Added support for `.zip` archives: 91 | 92 | - Added --fetch subcommand 93 | - Fixed new clippy lints from Rust 1.77 94 | 95 | - Updated various dependencies 96 | 97 | ## v0.3.0 (2024-03-25) 98 | 99 | - Added support for `.tar.xz` archives: 100 | 101 | - Ensure the root of the artifact directory is read-only on UNIX: 102 | 103 | - `aarch64` Linux added to the set of prebuilt releases (though this did not 104 | require code changes to DotSlash itself): 105 | 106 | 107 | ## v0.2.0 (2024-02-05) 108 | 109 | [Release](https://github.com/facebook/dotslash/releases/tag/v0.2.0) | 110 | [Tag](https://github.com/facebook/dotslash/tree/v0.2.0) 111 | 112 | - Apparently the v0.1.0 create published to crates.io inadvertently contained 113 | the `website/` folder. 114 | [Added `package.include` in `Cargo.toml` to fix this](https://github.com/facebook/dotslash/commit/10faac39bfaad87d293394c58b777bbbc50224c8) 115 | and republished as v0.2.0. No other code changes. 116 | 117 | ## v0.1.0 (2024-02-05) 118 | 119 | [Release](https://github.com/facebook/dotslash/releases/tag/v0.1.0) | 120 | [Tag](https://github.com/facebook/dotslash/tree/v0.1.0) 121 | 122 | - Initial version built from the first commit in the repo, following the 123 | [project announcement](https://engineering.fb.com/2024/02/06/developer-tools/dotslash-simplified-executable-deployment/). 124 | -------------------------------------------------------------------------------- /node/scripts/build-package.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Meta Platforms, Inc. and affiliates. 4 | * 5 | * This source code is dual-licensed under either the MIT license found in the 6 | * LICENSE-MIT file in the root directory of this source tree or the Apache 7 | * License, Version 2.0 found in the LICENSE-APACHE file in the root directory 8 | * of this source tree. You may select, at your option, one of the 9 | * above-listed licenses. 10 | */ 11 | 12 | 'use strict'; 13 | 14 | const { parseArgs } = require('util'); 15 | const { promises: fs } = require('fs'); 16 | const path = require('path'); 17 | const os = require('os'); 18 | const { artifactsByPlatformAndArch } = require('../platforms'); 19 | const { spawnSync } = require('child_process'); 20 | 21 | const PACKAGE_JSON_PATH = path.join(__dirname, '..', 'package.json'); 22 | const BIN_PATH = path.join(__dirname, '..', 'bin'); 23 | const GITHUB_REPO = 'facebook/dotslash'; 24 | 25 | async function main() { 26 | const { 27 | values: { tag, prerelease }, 28 | } = parseArgs({ 29 | options: { 30 | tag: { 31 | short: 't', 32 | type: 'string', 33 | }, 34 | prerelease: { 35 | type: 'boolean', 36 | }, 37 | }, 38 | }); 39 | 40 | if (tag == null) { 41 | throw new Error('Missing required argument: --tag'); 42 | } 43 | 44 | await deleteOldBinaries(); 45 | const versionInfo = getVersionInfoFromArgs(tag, prerelease); 46 | if (versionInfo.prerelease && !prerelease) { 47 | console.warn( 48 | `Building a prerelease version because the tag ${tag} does not seem to denote a valid semver string.`, 49 | ); 50 | } 51 | await fetchBinaries(tag); 52 | await updatePackageJson(versionInfo); 53 | } 54 | 55 | function getVersionInfoFromArgs(tag, prerelease) { 56 | // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string 57 | const SEMVER_WITH_LEADING_V = 58 | /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; 59 | if (SEMVER_WITH_LEADING_V.test(tag)) { 60 | return { tag, version: tag.slice(1), prerelease }; 61 | } 62 | return { 63 | tag, 64 | version: '0.0.0-' + tag.replaceAll(/[^0-9a-zA-Z-]+/g, '-'), 65 | prerelease: true, 66 | }; 67 | } 68 | 69 | async function deleteOldBinaries() { 70 | const entries = await fs.readdir(BIN_PATH, { withFileTypes: true }); 71 | for (const entry of entries) { 72 | if (!entry.isDirectory()) { 73 | continue; 74 | } 75 | await fs.rm(path.join(BIN_PATH, entry.name), { 76 | recursive: true, 77 | force: true, 78 | }); 79 | } 80 | } 81 | 82 | async function fetchBinaries(tag) { 83 | const scratchDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dotslash')); 84 | try { 85 | for (const [platform, archToArtifact] of Object.entries( 86 | artifactsByPlatformAndArch, 87 | )) { 88 | for (const [arch, descriptor] of Object.entries(archToArtifact)) { 89 | const { slug, binary } = descriptor; 90 | console.log( 91 | `Fetching ${platform} ${arch} binary (${slug} ${binary})...`, 92 | ); 93 | const tarballName = `dotslash-${slug}.tar.gz`; 94 | const downloadURL = `https://github.com/${GITHUB_REPO}/releases/download/${tag}/${tarballName}`; 95 | const tarballPath = path.join(scratchDir, tarballName); 96 | await download(downloadURL, tarballPath); 97 | const extractDir = path.join(BIN_PATH, slug); 98 | await fs.mkdir(extractDir, { recursive: true }); 99 | spawnSyncSafe('tar', ['-xzf', tarballPath, '-C', extractDir]); 100 | await fs.rm(tarballPath); 101 | if (!(await existsAndIsExecutable(path.join(extractDir, binary)))) { 102 | throw new Error( 103 | `Failed to extract ${binary} from ${tarballPath} to ${extractDir}`, 104 | ); 105 | } 106 | } 107 | } 108 | } finally { 109 | await fs.rm(scratchDir, { force: true, recursive: true }); 110 | } 111 | } 112 | 113 | async function existsAndIsExecutable(filePath) { 114 | try { 115 | await fs.access(filePath, fs.constants.R_OK | fs.constants.X_OK); 116 | return true; 117 | } catch (e) { 118 | return false; 119 | } 120 | } 121 | 122 | async function download(url, dest) { 123 | spawnSyncSafe('curl', ['-L', url, '-o', dest, '--fail-with-body'], { 124 | stdio: 'inherit', 125 | }); 126 | } 127 | 128 | async function updatePackageJson({ version, prerelease }) { 129 | const packageJson = await fs.readFile(PACKAGE_JSON_PATH, 'utf8'); 130 | const packageJsonObj = JSON.parse(packageJson); 131 | packageJsonObj.version = version + (prerelease ? '-' + Date.now() : ''); 132 | await fs.writeFile( 133 | PACKAGE_JSON_PATH, 134 | JSON.stringify(packageJsonObj, null, 2) + '\n', 135 | ); 136 | console.log('Updated package.json to version', packageJsonObj.version); 137 | } 138 | 139 | function spawnSyncSafe(command, args, options) { 140 | args = args ?? []; 141 | console.log('Running:', command, args.join(' ')); 142 | const result = spawnSync(command, args, options); 143 | if (result.status != null && result.status !== 0) { 144 | throw new Error(`Command ${command} exited with status ${result.status}`); 145 | } 146 | if (result.error != null) { 147 | throw result.error; 148 | } 149 | if (result.signal != null) { 150 | throw new Error( 151 | `Command ${command} was killed with signal ${result.signal}`, 152 | ); 153 | } 154 | return result; 155 | } 156 | 157 | main().catch((e) => { 158 | console.error(e); 159 | process.exitCode = 1; 160 | }); 161 | -------------------------------------------------------------------------------- /website/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 0 3 | --- 4 | 5 | # Introduction 6 | 7 | DotSlash (`dotslash`) is a command-line tool that is designed to facilitate 8 | fetching an executable, verifying it, and then running it. It maintains a local 9 | cache of fetched executables so that subsequent invocations are fast. 10 | 11 | DotSlash helps keeps heavyweight binaries out of your repo while ensuring your 12 | developers seamlessly get the tools they need, ensuring consistent builds across 13 | platforms. At Meta, DotSlash is executed _hundreds of millions of times per day_ 14 | to deliver a mix of first-party and third-party tools to end-user developers as 15 | well as hermetic build environments. 16 | 17 | While the page on [Motivation](./motivation) details the benefits of DotSlash 18 | and the thinking behind its design, here we will try to illustrate the value 19 | with a concrete example: 20 | 21 | ## Example: Vendoring Node.js in a Repo (Traditional) 22 | 23 | Suppose you have a project that depends on Node.js. To ensure that everyone on 24 | your team uses the same version of Node.js, traditionally, you would add the 25 | following files to your repo and ask contributors to reference `scripts/node` 26 | from your repo instead of assuming `node` is on the `$PATH`: 27 | 28 | - `scripts/node-mac-v18.19.0` the universal macOS binary for Node.js 29 | - `scripts/node-linux-v18.19.0` the x86_64 Linux binary for Node.js 30 | - `scripts/node` a shell script with the following contents: 31 | 32 | ```bash 33 | #!/bin/bash 34 | 35 | # Copied from https://stackoverflow.com/a/246128. 36 | DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 37 | 38 | if [ "$(uname)" == "Darwin" ]; then 39 | # In this example, assume node-mac-v18.19.0 is a universal macOS binary. 40 | "$DIR/node-mac-v18.19.0" "$@" 41 | else 42 | "$DIR/node-linux-v18.19.0" "$@" 43 | fi 44 | 45 | exit $? 46 | ``` 47 | 48 | Downsides with this approach: 49 | 50 | - Binary files are checked into the repo, making `git clone` more expensive. 51 | - Further, every user has to pay the cost of downloading an executable they are 52 | guaranteed not to use because it is for a different operating system. 53 | - Three files are being used to represent one executable, making it all too easy 54 | to update one of the files but not the others. 55 | - The Bash script has to execute additional processes (for `dirname`, `uname`, 56 | and `[`) before it ultimately runs Node.js. 57 | 58 | ## Example: Vendoring Node.js in a Repo (with DotSlash!) 59 | 60 | To solve the above problem with DotSlash, do the following: 61 | 62 | - Compress each platform-specific executable (or `.tar` file containing the 63 | executable) with `gzip` or [`zstd`](https://facebook.github.io/zstd/) and 64 | record the resulting size, in bytes, as well as the 65 | [BLAKE3]() or 66 | [SHA-256](https://en.wikipedia.org/wiki/SHA-2) hash. 67 | - Upload each artifact to a URL accessible to your target audience. For example, 68 | an internal-only executable might be served from a URL that is restricted to 69 | users on a VPN. 70 | - Rewrite the shell script at `scripts/node` with this information, structured 71 | as follows: 72 | 73 | ```bash 74 | #!/usr/bin/env dotslash 75 | 76 | // The URLs in this file were taken from https://nodejs.org/dist/v18.19.0/ 77 | 78 | { 79 | "name": "node-v18.19.0", 80 | "platforms": { 81 | "macos-aarch64": { 82 | "size": 40660307, 83 | "hash": "blake3", 84 | "digest": "6e2ca33951e586e7670016dd9e503d028454bf9249d5ff556347c3d98c347c34", 85 | "format": "tar.gz", 86 | "path": "node-v18.19.0-darwin-arm64/bin/node", 87 | "providers": [ 88 | { 89 | "url": "https://nodejs.org/dist/v18.19.0/node-v18.19.0-darwin-arm64.tar.gz" 90 | } 91 | ] 92 | }, 93 | "macos-x86_64": { 94 | "size": 42202872, 95 | "hash": "blake3", 96 | "digest": "37521058114e7f71e0de3fe8042c8fa7908305e9115488c6c29b514f9cd2a24c", 97 | "format": "tar.gz", 98 | "path": "node-v18.19.0-darwin-x64/bin/node", 99 | "providers": [ 100 | { 101 | "url": "https://nodejs.org/dist/v18.19.0/node-v18.19.0-darwin-x64.tar.gz" 102 | } 103 | ] 104 | }, 105 | "linux-x86_64": { 106 | "size": 44694523, 107 | "hash": "blake3", 108 | "digest": "72b81fc3a30b7bedc1a09a3fafc4478a1b02e5ebf0ad04ea15d23b3e9dc89212", 109 | "format": "tar.gz", 110 | "path": "node-v18.19.0-linux-x64/bin/node", 111 | "providers": [ 112 | { 113 | "url": "https://nodejs.org/dist/v18.19.0/node-v18.19.0-linux-x64.tar.gz" 114 | } 115 | ] 116 | } 117 | } 118 | } 119 | ``` 120 | 121 | Note that in the above example, we leverage DotSlash to distribute 122 | _architecture_-specific executables for macOS so users can download smaller, 123 | more specific binaries. If the archive contained a universal macOS binary, there 124 | would still be individual entries for both `"macos-x86_64"` and 125 | `"macos-aarch64"` in the DotSlash file, but the values would be the same. 126 | 127 | Assuming `dotslash` is on your `$PATH` and you remembered to 128 | `chmod +x ./scripts/node` to mark it as executable, you should be able to run 129 | your Node.js wrapper exactly as you did before: 130 | 131 | ```shell 132 | $ ./scripts/node --version 133 | v18.19.0 134 | ``` 135 | 136 | The first time you run `./scripts/node --version`, you will likely experience a 137 | small delay while DotSlash fetches, decompresses, and verifies the appropriate 138 | `.zst`, but subsequent invocations should be instantaneous. 139 | 140 | To understand what is happening under the hood, see 141 | [How DotSlash Works](./execution). 142 | --------------------------------------------------------------------------------