{siteConfig.tagline}
24 |./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 | [](https://github.com/facebook/dotslash/actions/workflows/test-python.yml)
4 | [](https://pypi.org/project/dotslash/)
5 | [](https://pypi.org/project/dotslash/)
6 | [](https://github.com/pypa/hatch)
7 | [](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 |
--------------------------------------------------------------------------------