diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..377bc158 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[tests/snapshots/*] +trim_trailing_whitespace = false + +[*.{cff,yml}] +indent_size = 2 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5e4980b5..c168124d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,8 +2,29 @@ version: 2 updates: - - package-ecosystem: "cargo" + - package-ecosystem: "github-actions" + # Workflow files stored in the + # default location of `.github/workflows` directory: "/" schedule: - interval: "weekly" - + interval: "daily" + - package-ecosystem: "cargo" + directory: "/crates/bin" + schedule: + interval: "daily" + - package-ecosystem: "cargo" + directory: "/crates/detect-wasi" + schedule: + interval: "daily" + - package-ecosystem: "cargo" + directory: "/crates/flock" + schedule: + interval: "daily" + - package-ecosystem: "cargo" + directory: "/crates/lib" + schedule: + interval: "daily" + - package-ecosystem: "cargo" + directory: "/crates/normalize-path" + schedule: + interval: "daily" diff --git a/ci-scripts/compile-settings.jq b/.github/scripts/compile-settings.jq similarity index 100% rename from ci-scripts/compile-settings.jq rename to .github/scripts/compile-settings.jq diff --git a/ci-scripts/extract-release-notes.sh b/.github/scripts/extract-release-notes.sh similarity index 100% rename from ci-scripts/extract-release-notes.sh rename to .github/scripts/extract-release-notes.sh diff --git a/ci-scripts/extract-tag-from-release-commit.sh b/.github/scripts/extract-tag-from-release-commit.sh similarity index 100% rename from ci-scripts/extract-tag-from-release-commit.sh rename to .github/scripts/extract-tag-from-release-commit.sh diff --git a/ci-scripts/install-deps.sh b/.github/scripts/install-deps.sh similarity index 100% rename from ci-scripts/install-deps.sh rename to .github/scripts/install-deps.sh diff --git a/ci-scripts/pack-release-archives.sh b/.github/scripts/pack-release-archives.sh similarity index 100% rename from ci-scripts/pack-release-archives.sh rename to .github/scripts/pack-release-archives.sh diff --git a/ci-scripts/release-pr.txt b/.github/scripts/release-pr.txt similarity index 100% rename from ci-scripts/release-pr.txt rename to .github/scripts/release-pr.txt diff --git a/ci-scripts/tests.sh b/.github/scripts/tests.sh similarity index 90% rename from ci-scripts/tests.sh rename to .github/scripts/tests.sh index a83e797e..d2ee228d 100755 --- a/ci-scripts/tests.sh +++ b/.github/scripts/tests.sh @@ -19,7 +19,8 @@ done cargo binstall --help >/dev/null # Install binaries using `--manifest-path` -"./$1" binstall --force --log-level debug --manifest-path . --no-confirm cargo-binstall +"./$1" binstall --force --log-level debug --manifest-path crates/bin/Cargo.toml --no-confirm cargo-binstall +"./$1" binstall --force --log-level debug --manifest-path crates/bin --no-confirm cargo-binstall # Test that the installed binaries can be run cargo binstall --help >/dev/null diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e8c2eb4d..d753e358 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -84,7 +84,7 @@ jobs: jq \ --argjson for_release '${{ toJSON(inputs.for_release) }}' \ --argjson matrix '${{ toJSON(matrix) }}' \ - -nrf ci-scripts/compile-settings.jq \ + -nrf .github/scripts/compile-settings.jq \ | tee -a $GITHUB_ENV - name: Configure caching @@ -107,7 +107,7 @@ jobs: - name: Install deps if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' && !startsWith(github.ref, 'refs/tags/v') }} - run: sudo ./ci-scripts/install-deps.sh + run: sudo .github/scripts/install-deps.sh - name: Build run: ${{ env.CTOOL }} build ${{ env.CARGS }} diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index a132dd31..3810c6b8 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -50,7 +50,7 @@ jobs: - name: Test shell: bash - run: ./ci-scripts/tests.sh ${{ matrix.bin }} ${{ runner.os }} + run: .github/scripts/tests.sh ${{ matrix.bin }} ${{ runner.os }} env: CARGO_HOME: /tmp/cargo-home-for-test/ diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index fb6cc115..5e450417 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -62,7 +62,7 @@ jobs: ecnef=$'\n```' title='release: v${{ inputs.version }}' - body=$(sed 's/%version%/${{ inputs.version }}/g' ci-scripts/release-pr.txt) + body=$(sed 's/%version%/${{ inputs.version }}/g' .github/scripts/release-pr.txt) gh pr create --title "$title" --body "$body" --base main --head "${{ env.branch_name }}" --label "release" env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb7c3417..9226a7da 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,13 +20,13 @@ jobs: - uses: actions/checkout@v2 - name: Extract tag from commit message id: version - run: ./ci-scripts/extract-tag-from-release-commit.sh + run: .github/scripts/extract-tag-from-release-commit.sh - name: Extract release notes id: notes env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_REPO: ${{ github.repository }} - run: ./ci-scripts/extract-release-notes.sh + run: .github/scripts/extract-release-notes.sh tag: needs: info @@ -61,7 +61,7 @@ jobs: path: outputs/ - name: Pack archives - run: ./ci-scripts/pack-release-archives.sh + run: .github/scripts/pack-release-archives.sh - name: Publish release uses: softprops/action-gh-release@50195ba7f6f93d1ac97ba8332a178e008ad176aa diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 73950629..d9a8ceaf 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -21,7 +21,7 @@ jobs: - windows runs-on: ${{ matrix.os }}-latest - name: on ${{ matrix.os }} + name: unit tests on ${{ matrix.os }} steps: - uses: actions/checkout@v2 @@ -44,7 +44,7 @@ jobs: - name: Install deps if: matrix.os == 'ubuntu' - run: sudo ./ci-scripts/install-deps.sh + run: sudo .github/scripts/install-deps.sh - name: Test (Unix) if: matrix.os != 'windows' diff --git a/Cargo.lock b/Cargo.lock index 6192bc9f..c19c238c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,6 +75,49 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "binstall" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "bzip2", + "cargo_toml", + "clap", + "compact_str", + "crates_io_api", + "env_logger", + "flate2", + "flock", + "futures-util", + "guess_host_triple", + "home", + "itertools", + "jobserver", + "log", + "miette", + "normalize-path", + "once_cell", + "reqwest", + "scopeguard", + "semver", + "serde", + "serde-tuple-vec-map", + "serde_json", + "strum", + "strum_macros", + "tar", + "tempfile", + "thiserror", + "tinytemplate", + "tokio", + "toml_edit", + "url", + "xz2", + "zip", + "zstd", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -124,46 +167,19 @@ dependencies = [ name = "cargo-binstall" version = "0.12.0" dependencies = [ - "async-trait", - "bytes", - "bzip2", - "cargo_toml", + "binstall", "clap", - "compact_str", "crates_io_api", "dirs", "embed-resource", - "env_logger", - "flate2", - "fs4", - "futures-util", - "guess_host_triple", - "home", - "itertools", - "jobserver", "log", "miette", "mimalloc", - "once_cell", "reqwest", - "scopeguard", "semver", - "serde", - "serde-tuple-vec-map", - "serde_json", "simplelog", - "strum", - "strum_macros", - "tar", "tempfile", - "thiserror", - "tinytemplate", "tokio", - "toml_edit", - "url", - "xz2", - "zip", - "zstd", ] [[package]] @@ -334,6 +350,13 @@ dependencies = [ "once_cell", ] +[[package]] +name = "detect-wasi" +version = "1.0.0" +dependencies = [ + "tempfile", +] + [[package]] name = "dirs" version = "4.0.0" @@ -448,6 +471,13 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flock" +version = "0.1.0" +dependencies = [ + "fs4", +] + [[package]] name = "fnv" version = "1.0.7" @@ -982,6 +1012,10 @@ dependencies = [ "tempfile", ] +[[package]] +name = "normalize-path" +version = "0.1.0" + [[package]] name = "num-integer" version = "0.1.45" diff --git a/Cargo.toml b/Cargo.toml index 1c45e74e..80872e3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,99 +1,11 @@ -[package] -name = "cargo-binstall" -description = "Rust binary package installer for CI integration" -repository = "https://github.com/ryankurte/cargo-binstall" -documentation = "https://docs.rs/cargo-binstall" -version = "0.12.0" -rust-version = "1.61.0" -authors = ["ryan "] -edition = "2021" -license = "GPL-3.0" - -[package.metadata.binstall] -pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }.{ archive-format }" -bin-dir = "{ bin }{ binary-ext }" - -[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] -pkg-fmt = "zip" -[package.metadata.binstall.overrides.x86_64-apple-darwin] -pkg-fmt = "zip" - -[dependencies] -async-trait = "0.1.57" -bytes = "1.2.1" -bzip2 = "0.4.3" -cargo_toml = "0.11.5" -clap = { version = "3.2.16", features = ["derive"] } -compact_str = { version = "0.5.2", features = ["serde"] } -crates_io_api = { version = "0.8.0", default-features = false } -dirs = "4.0.0" -flate2 = { version = "1.0.24", default-features = false } -fs4 = "0.6.2" -futures-util = { version = "0.3.21", default-features = false } -home = "0.5.3" -itertools = "0.10.3" -jobserver = "0.1.24" -log = "0.4.17" -miette = "5.2.0" -mimalloc = { version = "0.1.29", default-features = false, optional = true } -once_cell = "1.13.0" -reqwest = { version = "0.11.11", features = ["stream"], default-features = false } -scopeguard = "1.1.0" -semver = { version = "1.0.13", features = ["serde"] } -serde = { version = "1.0.142", features = ["derive"] } -serde-tuple-vec-map = "1.0.1" -serde_json = "1.0.83" -simplelog = "0.12.0" -strum = "0.24.1" -strum_macros = "0.24.3" -tar = "0.4.38" -tempfile = "3.3.0" -thiserror = "1.0.32" -tinytemplate = "1.2.1" -tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread", "process", "sync", "signal"], default-features = false } -toml_edit = { version = "0.14.4", features = ["easy"] } -url = { version = "2.2.2", features = ["serde"] } -xz2 = "0.1.7" - -# Disable all features of zip except for features of compression algorithms: -# Disabled features include: -# - aes-crypto: Enables decryption of files which were encrypted with AES, absolutely zero use for -# this crate. -# - time: Enables features using the [time](https://github.com/time-rs/time) crate, -# which is not used by this crate. -zip = { version = "0.6.2", default-features = false, features = ["deflate", "bzip2", "zstd"] } - -# zstd is also depended by zip. -# Since zip 0.6.2 depends on zstd 0.10.0, we also have to use 0.10.0 here, -# otherwise there will be a link conflict. -zstd = { version = "0.10.0", default-features = false } - -[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] -guess_host_triple = "0.1.3" - -[features] -default = ["static", "zlib-ng", "rustls", "fancy-no-backtrace"] - -mimalloc = ["dep:mimalloc"] - -static = ["bzip2/static", "xz2/static"] -pkg-config = ["zstd/pkg-config"] - -zlib-ng = ["flate2/zlib-ng"] - -rustls = ["crates_io_api/rustls", "reqwest/rustls-tls"] -native-tls = ["reqwest/native-tls"] - -fancy-no-backtrace = ["miette/fancy-no-backtrace"] -fancy-with-backtrace = ["fancy-no-backtrace", "miette/fancy"] - -log_release_max_level_info = ["log/release_max_level_info"] - -[dev-dependencies] -env_logger = "0.9.0" - -[build-dependencies] -embed-resource = "1.7.3" +[workspace] +members = [ + "crates/bin", + "crates/lib", + "crates/detect-wasi", + "crates/flock", + "crates/normalize-path", +] [profile.release] opt-level = "z" diff --git a/crates/bin/Cargo.toml b/crates/bin/Cargo.toml new file mode 100644 index 00000000..d1f36598 --- /dev/null +++ b/crates/bin/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "cargo-binstall" +description = "Rust binary package installer for CI integration" +repository = "https://github.com/ryankurte/cargo-binstall" +documentation = "https://docs.rs/cargo-binstall" +version = "0.12.0" +rust-version = "1.61.0" +authors = ["ryan "] +edition = "2021" +license = "GPL-3.0" + +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }.{ archive-format }" +bin-dir = "{ bin }{ binary-ext }" + +[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] +pkg-fmt = "zip" +[package.metadata.binstall.overrides.x86_64-apple-darwin] +pkg-fmt = "zip" + +[dependencies] +binstall = { path = "../lib", version = "0.1.0" } +clap = { version = "3.2.16", features = ["derive"] } +crates_io_api = { version = "0.8.0", default-features = false } +dirs = "4.0.0" +log = "0.4.17" +miette = "5.2.0" +mimalloc = { version = "0.1.29", default-features = false, optional = true } +reqwest = { version = "0.11.11", default-features = false } +semver = "1.0.13" +simplelog = "0.12.0" +tempfile = "3.3.0" +tokio = { version = "1.20.1", features = ["rt-multi-thread"], default-features = false } + +[build-dependencies] +embed-resource = "1.7.3" + +[features] +default = ["static", "zlib-ng", "rustls", "fancy-no-backtrace"] + +mimalloc = ["dep:mimalloc"] + +static = ["binstall/static"] +pkg-config = ["binstall/pkg-config"] + +zlib-ng = ["binstall/zlib-ng"] + +rustls = ["binstall/rustls"] +native-tls = ["binstall/native-tls"] + +fancy-no-backtrace = ["miette/fancy-no-backtrace"] +fancy-with-backtrace = ["fancy-no-backtrace", "miette/fancy"] + +log_release_max_level_info = ["log/release_max_level_info"] diff --git a/LICENSE.txt b/crates/bin/LICENSE similarity index 100% rename from LICENSE.txt rename to crates/bin/LICENSE diff --git a/crates/bin/build.rs b/crates/bin/build.rs new file mode 100644 index 00000000..b7db24d6 --- /dev/null +++ b/crates/bin/build.rs @@ -0,0 +1,3 @@ +fn main() { + embed_resource::compile("manifest.rc"); +} diff --git a/manifest.rc b/crates/bin/manifest.rc similarity index 100% rename from manifest.rc rename to crates/bin/manifest.rc diff --git a/release.toml b/crates/bin/release.toml similarity index 99% rename from release.toml rename to crates/bin/release.toml index 4c308c4a..e72abc9c 100644 --- a/release.toml +++ b/crates/bin/release.toml @@ -13,4 +13,3 @@ search = "^ version=\"[\\d.]+[.]0\"" replace = " version=\"{{version}}.0\"" prerelease = false max = 1 - diff --git a/crates/bin/src/args.rs b/crates/bin/src/args.rs new file mode 100644 index 00000000..bc8403e3 --- /dev/null +++ b/crates/bin/src/args.rs @@ -0,0 +1,252 @@ +use std::{ffi::OsString, path::PathBuf}; + +use binstall::{ + errors::BinstallError, + manifests::cargo_toml_binstall::PkgFmt, + ops::resolve::{CrateName, VersionReqExt}, +}; +use clap::{builder::PossibleValue, AppSettings, ArgEnum, Parser}; +use log::LevelFilter; +use reqwest::tls::Version; +use semver::VersionReq; + +#[derive(Debug, Parser)] +#[clap(version, about = "Install a Rust binary... from binaries!", setting = AppSettings::ArgRequiredElseHelp)] +pub struct Args { + /// Packages to install. + /// + /// Syntax: crate[@version] + /// + /// Each value is either a crate name alone, or a crate name followed by @ and the version to + /// install. The version syntax is as with the --version option. + /// + /// When multiple names are provided, the --version option and any override options are + /// unavailable due to ambiguity. + /// + /// If duplicate names are provided, the last one (and their version requirement) + /// is kept. + #[clap( + help_heading = "Package selection", + value_name = "crate[@version]", + required_unless_present_any = ["version", "help"], +)] + pub crate_names: Vec, + + /// Package version to install. + /// + /// Takes either an exact semver version or a semver version requirement expression, which will + /// be resolved to the highest matching version available. + /// + /// Cannot be used when multiple packages are installed at once, use the attached version + /// syntax in that case. + #[clap(help_heading = "Package selection", long = "version", parse(try_from_str = VersionReq::parse_from_cli))] + pub version_req: Option, + + /// Override binary target set. + /// + /// Binstall is able to look for binaries for several targets, installing the first one it finds + /// in the order the targets were given. For example, on a 64-bit glibc Linux distribution, the + /// default is to look first for a `x86_64-unknown-linux-gnu` binary, then for a + /// `x86_64-unknown-linux-musl` binary. However, on a musl system, the gnu version will not be + /// considered. + /// + /// This option takes a comma-separated list of target triples, which will be tried in order. + /// They override the default list, which is detected automatically from the current platform. + /// + /// If falling back to installing from source, the first target will be used. + #[clap( + help_heading = "Package selection", + alias = "target", + long, + value_name = "TRIPLE" + )] + pub targets: Option, + + /// Override Cargo.toml package manifest path. + /// + /// This skips searching crates.io for a manifest and uses the specified path directly, useful + /// for debugging and when adding Binstall support. This may be either the path to the folder + /// containing a Cargo.toml file, or the Cargo.toml file itself. + #[clap(help_heading = "Overrides", long)] + pub manifest_path: Option, + + /// Override Cargo.toml package manifest bin-dir. + #[clap(help_heading = "Overrides", long)] + pub bin_dir: Option, + + /// Override Cargo.toml package manifest pkg-fmt. + #[clap(help_heading = "Overrides", long)] + pub pkg_fmt: Option, + + /// Override Cargo.toml package manifest pkg-url. + #[clap(help_heading = "Overrides", long)] + pub pkg_url: Option, + + /// Disable symlinking / versioned updates. + /// + /// By default, Binstall will install a binary named `-` in the install path, and + /// either symlink or copy it to (depending on platform) the plain binary name. This makes it + /// possible to have multiple versions of the same binary, for example for testing or rollback. + /// + /// Pass this flag to disable this behavior. + #[clap(help_heading = "Options", long)] + pub no_symlinks: bool, + + /// Dry run, fetch and show changes without installing binaries. + #[clap(help_heading = "Options", long)] + pub dry_run: bool, + + /// Disable interactive mode / confirmation prompts. + #[clap(help_heading = "Options", long)] + pub no_confirm: bool, + + /// Do not cleanup temporary files. + #[clap(help_heading = "Options", long)] + pub no_cleanup: bool, + + /// Install binaries in a custom location. + /// + /// By default, binaries are installed to the global location `$CARGO_HOME/bin`, and global + /// metadata files are updated with the package information. Specifying another path here + /// switches over to a "local" install, where binaries are installed at the path given, and the + /// global metadata files are not updated. + #[clap(help_heading = "Options", long)] + pub install_path: Option, + + /// Enforce downloads over secure transports only. + /// + /// Insecure HTTP downloads will be removed completely in the future; in the meantime this + /// option forces a fail when the remote endpoint uses plaintext HTTP or insecure TLS suites. + /// + /// Without this option, plain HTTP will warn. + /// + /// Implies `--min-tls-version=1.2`. + #[clap(help_heading = "Options", long)] + pub secure: bool, + + /// Force a crate to be installed even if it is already installed. + #[clap(help_heading = "Options", long)] + pub force: bool, + + /// Require a minimum TLS version from remote endpoints. + /// + /// The default is not to require any minimum TLS version, and use the negotiated highest + /// version available to both this client and the remote server. + #[clap(help_heading = "Options", long, arg_enum, value_name = "VERSION")] + pub min_tls_version: Option, + + /// Print help information + #[clap(help_heading = "Meta", short, long)] + pub help: bool, + + /// Print version information + #[clap(help_heading = "Meta", short = 'V')] + pub version: bool, + + /// Utility log level + /// + /// Set to `trace` to print very low priority, often extremely + /// verbose information. + /// + /// Set to `debug` when submitting a bug report. + /// + /// Set to `info` to only print useful information. + /// + /// Set to `warn` to only print on hazardous situations. + /// + /// Set to `error` to only print serious errors. + /// + /// Set to `off` to disable logging completely, this will also + /// disable output from `cargo-install`. + #[clap( + help_heading = "Meta", + long, + default_value = "info", + value_name = "LEVEL", + possible_values = [ + PossibleValue::new("trace").help( + "Set to `trace` to print very low priority, often extremely verbose information." + ), + PossibleValue::new("debug").help("Set to debug when submitting a bug report."), + PossibleValue::new("info").help("Set to info to only print useful information."), + PossibleValue::new("warn").help("Set to warn to only print on hazardous situations."), + PossibleValue::new("error").help("Set to error to only print serious errors."), + PossibleValue::new("off").help( + "Set to off to disable logging completely, this will also disable output from `cargo-install`." + ), + ] + )] + pub log_level: LevelFilter, + + /// Equivalent to setting `log_level` to `off`. + /// + /// This would override the `log_level`. + #[clap(help_heading = "Meta", short, long)] + pub quiet: bool, +} + +#[derive(Debug, Copy, Clone, ArgEnum)] +pub enum TLSVersion { + #[clap(name = "1.2")] + Tls1_2, + #[clap(name = "1.3")] + Tls1_3, +} + +impl From for Version { + fn from(ver: TLSVersion) -> Self { + match ver { + TLSVersion::Tls1_2 => Version::TLS_1_2, + TLSVersion::Tls1_3 => Version::TLS_1_3, + } + } +} + +pub fn parse() -> Result { + // Filter extraneous arg when invoked by cargo + // `cargo run -- --help` gives ["target/debug/cargo-binstall", "--help"] + // `cargo binstall --help` gives ["/home/ryan/.cargo/bin/cargo-binstall", "binstall", "--help"] + let mut args: Vec = std::env::args_os().collect(); + let args = if args.len() > 1 && args[1] == "binstall" { + // Equivalent to + // + // args.remove(1); + // + // But is O(1) + args.swap(0, 1); + let mut args = args.into_iter(); + drop(args.next().unwrap()); + + args + } else { + args.into_iter() + }; + + // Load options + let mut opts = Args::parse_from(args); + if opts.quiet { + opts.log_level = LevelFilter::Off; + } + + if opts.crate_names.len() > 1 { + let option = if opts.version_req.is_some() { + "version" + } else if opts.manifest_path.is_some() { + "manifest-path" + } else if opts.bin_dir.is_some() { + "bin-dir" + } else if opts.pkg_fmt.is_some() { + "pkg-fmt" + } else if opts.pkg_url.is_some() { + "pkg-url" + } else { + "" + }; + + if !option.is_empty() { + return Err(BinstallError::OverrideOptionUsedWithMultiInstall { option }.into()); + } + } + + Ok(opts) +} diff --git a/crates/bin/src/entry.rs b/crates/bin/src/entry.rs new file mode 100644 index 00000000..5d23a316 --- /dev/null +++ b/crates/bin/src/entry.rs @@ -0,0 +1,237 @@ +use std::{fs, path::Path, sync::Arc, time::Duration}; + +use binstall::{ + errors::BinstallError, + helpers::{ + jobserver_client::LazyJobserverClient, remote::create_reqwest_client, + tasks::AutoAbortJoinHandle, + }, + manifests::{ + binstall_crates_v1::Records, cargo_crates_v1::CratesToml, cargo_toml_binstall::PkgOverride, + }, + ops::{ + self, + resolve::{CrateName, Resolution, VersionReqExt}, + }, + targets::get_desired_targets, +}; +use log::{debug, error, info, warn, LevelFilter}; +use miette::{miette, Result, WrapErr}; +use tokio::task::block_in_place; + +use crate::{args::Args, install_path, ui::UIThread}; + +pub async fn install_crates(mut args: Args, jobserver_client: LazyJobserverClient) -> Result<()> { + let cli_overrides = PkgOverride { + pkg_url: args.pkg_url.take(), + pkg_fmt: args.pkg_fmt.take(), + bin_dir: args.bin_dir.take(), + }; + + // Launch target detection + let desired_targets = get_desired_targets(&args.targets); + + // Initialize reqwest client + let client = create_reqwest_client(args.secure, args.min_tls_version.map(|v| v.into()))?; + + // Build crates.io api client + let crates_io_api_client = crates_io_api::AsyncClient::new( + "cargo-binstall (https://github.com/ryankurte/cargo-binstall)", + Duration::from_millis(100), + ) + .expect("bug: invalid user agent"); + + // Initialize UI thread + let mut uithread = UIThread::new(!args.no_confirm); + + let (install_path, metadata, temp_dir) = block_in_place(|| -> Result<_> { + // Compute install directory + let (install_path, custom_install_path) = + install_path::get_install_path(args.install_path.as_deref()); + let install_path = install_path.ok_or_else(|| { + error!("No viable install path found of specified, try `--install-path`"); + miette!("No install path found or specified") + })?; + fs::create_dir_all(&install_path).map_err(BinstallError::Io)?; + debug!("Using install path: {}", install_path.display()); + + // Load metadata + let metadata = if !custom_install_path { + debug!("Reading binstall/crates-v1.json"); + Some(Records::load()?) + } else { + None + }; + + // Create a temporary directory for downloads etc. + // + // Put all binaries to a temporary directory under `dst` first, catching + // some failure modes (e.g., out of space) before touching the existing + // binaries. This directory will get cleaned up via RAII. + let temp_dir = tempfile::Builder::new() + .prefix("cargo-binstall") + .tempdir_in(&install_path) + .map_err(BinstallError::from) + .wrap_err("Creating a temporary directory failed.")?; + + Ok((install_path, metadata, temp_dir)) + })?; + + // Remove installed crates + let crate_names = CrateName::dedup(&args.crate_names) + .filter_map(|crate_name| { + match ( + args.force, + metadata.as_ref().and_then(|records| records.get(&crate_name.name)), + &crate_name.version_req, + ) { + (false, Some(metadata), Some(version_req)) + if version_req.is_latest_compatible(&metadata.current_version) => + { + debug!("Bailing out early because we can assume wanted is already installed from metafile"); + info!( + "{} v{} is already installed, use --force to override", + crate_name.name, metadata.current_version + ); + None + } + + // we have to assume that the version req could be *, + // and therefore a remote upgraded version could exist + (false, Some(metadata), _) => { + Some((crate_name, Some(metadata.current_version.clone()))) + } + + _ => Some((crate_name, None)), + } + }) + .collect::>(); + + if crate_names.is_empty() { + debug!("Nothing to do"); + return Ok(()); + } + + let temp_dir_path: Arc = Arc::from(temp_dir.path()); + + // Create binstall_opts + let binstall_opts = Arc::new(ops::Options { + no_symlinks: args.no_symlinks, + dry_run: args.dry_run, + force: args.force, + version_req: args.version_req.take(), + manifest_path: args.manifest_path.take(), + cli_overrides, + desired_targets, + quiet: args.log_level == LevelFilter::Off, + }); + + let tasks: Vec<_> = if !args.dry_run && !args.no_confirm { + // Resolve crates + let tasks: Vec<_> = crate_names + .into_iter() + .map(|(crate_name, current_version)| { + AutoAbortJoinHandle::spawn(ops::resolve::resolve( + binstall_opts.clone(), + crate_name, + current_version, + temp_dir_path.clone(), + install_path.clone(), + client.clone(), + crates_io_api_client.clone(), + )) + }) + .collect(); + + // Confirm + let mut resolutions = Vec::with_capacity(tasks.len()); + for task in tasks { + match task.await?? { + Resolution::AlreadyUpToDate => {} + res => resolutions.push(res), + } + } + + if resolutions.is_empty() { + debug!("Nothing to do"); + return Ok(()); + } + + uithread.confirm().await?; + + // Install + resolutions + .into_iter() + .map(|resolution| { + AutoAbortJoinHandle::spawn(ops::install::install( + resolution, + binstall_opts.clone(), + jobserver_client.clone(), + )) + }) + .collect() + } else { + // Resolve crates and install without confirmation + crate_names + .into_iter() + .map(|(crate_name, current_version)| { + let opts = binstall_opts.clone(); + let temp_dir_path = temp_dir_path.clone(); + let jobserver_client = jobserver_client.clone(); + let client = client.clone(); + let crates_io_api_client = crates_io_api_client.clone(); + let install_path = install_path.clone(); + + AutoAbortJoinHandle::spawn(async move { + let resolution = ops::resolve::resolve( + opts.clone(), + crate_name, + current_version, + temp_dir_path, + install_path, + client, + crates_io_api_client, + ) + .await?; + + ops::install::install(resolution, opts, jobserver_client).await + }) + }) + .collect() + }; + + let mut metadata_vec = Vec::with_capacity(tasks.len()); + for task in tasks { + if let Some(metadata) = task.await?? { + metadata_vec.push(metadata); + } + } + + block_in_place(|| { + if let Some(mut records) = metadata { + // If using standardised install path, + // then create_dir_all(&install_path) would also + // create .cargo. + + debug!("Writing .crates.toml"); + CratesToml::append(metadata_vec.iter())?; + + debug!("Writing binstall/crates-v1.json"); + for metadata in metadata_vec { + records.replace(metadata); + } + records.overwrite()?; + } + + if args.no_cleanup { + // Consume temp_dir without removing it from fs. + temp_dir.into_path(); + } else { + temp_dir.close().unwrap_or_else(|err| { + warn!("Failed to clean up some resources: {err}"); + }); + } + + Ok(()) + }) +} diff --git a/crates/bin/src/install_path.rs b/crates/bin/src/install_path.rs new file mode 100644 index 00000000..a64f6260 --- /dev/null +++ b/crates/bin/src/install_path.rs @@ -0,0 +1,39 @@ +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use binstall::helpers::statics::cargo_home; +use log::debug; + +/// Fetch install path from environment +/// roughly follows +/// +/// Return (install_path, is_custom_install_path) +pub fn get_install_path>(install_path: Option

) -> (Option>, bool) { + // Command line override first first + if let Some(p) = install_path { + return (Some(Arc::from(p.as_ref())), true); + } + + // Environmental variables + if let Ok(p) = std::env::var("CARGO_INSTALL_ROOT") { + debug!("using CARGO_INSTALL_ROOT ({p})"); + let b = PathBuf::from(p); + return (Some(Arc::from(b.join("bin"))), true); + } + + if let Ok(p) = cargo_home() { + debug!("using ({}) as cargo home", p.display()); + return (Some(p.join("bin").into()), false); + } + + // Local executable dir if no cargo is found + let dir = dirs::executable_dir(); + + if let Some(d) = &dir { + debug!("Fallback to {}", d.display()); + } + + (dir.map(Arc::from), true) +} diff --git a/crates/bin/src/main.rs b/crates/bin/src/main.rs new file mode 100644 index 00000000..d02828d2 --- /dev/null +++ b/crates/bin/src/main.rs @@ -0,0 +1,78 @@ +use std::{ + process::{ExitCode, Termination}, + time::{Duration, Instant}, +}; + +use binstall::{ + errors::BinstallError, + helpers::{ + jobserver_client::LazyJobserverClient, signal::cancel_on_user_sig_term, + tasks::AutoAbortJoinHandle, + }, +}; +use log::{debug, error, info}; +use tokio::runtime::Runtime; + +mod args; +mod entry; +mod install_path; +mod ui; + +#[cfg(feature = "mimalloc")] +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +fn main() -> MainExit { + // This must be the very first thing to happen + let jobserver_client = LazyJobserverClient::new(); + + let args = match args::parse() { + Ok(args) => args, + Err(err) => return MainExit::Error(err), + }; + + ui::logging(&args); + + let start = Instant::now(); + + let result = { + let rt = Runtime::new().unwrap(); + let handle = + AutoAbortJoinHandle::new(rt.spawn(entry::install_crates(args, jobserver_client))); + rt.block_on(cancel_on_user_sig_term(handle)) + }; + + let done = start.elapsed(); + debug!("run time: {done:?}"); + + result.map_or_else(MainExit::Error, |res| { + res.map(|()| MainExit::Success(done)).unwrap_or_else(|err| { + err.downcast::() + .map(MainExit::Error) + .unwrap_or_else(MainExit::Report) + }) + }) +} + +enum MainExit { + Success(Duration), + Error(BinstallError), + Report(miette::Report), +} + +impl Termination for MainExit { + fn report(self) -> ExitCode { + match self { + Self::Success(spent) => { + info!("Done in {spent:?}"); + ExitCode::SUCCESS + } + Self::Error(err) => err.report(), + Self::Report(err) => { + error!("Fatal error:"); + eprintln!("{err:?}"); + ExitCode::from(16) + } + } + } +} diff --git a/src/helpers/ui_thread.rs b/crates/bin/src/ui.rs similarity index 80% rename from src/helpers/ui_thread.rs rename to crates/bin/src/ui.rs index 64a20c55..e10932da 100644 --- a/src/helpers/ui_thread.rs +++ b/crates/bin/src/ui.rs @@ -3,9 +3,13 @@ use std::{ thread, }; +use log::LevelFilter; +use simplelog::{ColorChoice, ConfigBuilder, TermLogger, TerminalMode}; use tokio::sync::mpsc; -use crate::BinstallError; +use binstall::errors::BinstallError; + +use crate::args::Args; #[derive(Debug)] struct UIThreadInner { @@ -97,3 +101,19 @@ impl UIThread { } } } + +pub fn logging(args: &Args) { + // Setup logging + let mut log_config = ConfigBuilder::new(); + log_config.add_filter_ignore("hyper".to_string()); + log_config.add_filter_ignore("reqwest".to_string()); + log_config.add_filter_ignore("rustls".to_string()); + log_config.set_location_level(LevelFilter::Off); + TermLogger::init( + args.log_level, + log_config.build(), + TerminalMode::Mixed, + ColorChoice::Auto, + ) + .unwrap(); +} diff --git a/windows.manifest b/crates/bin/windows.manifest similarity index 100% rename from windows.manifest rename to crates/bin/windows.manifest diff --git a/crates/detect-wasi/Cargo.toml b/crates/detect-wasi/Cargo.toml new file mode 100644 index 00000000..e9d4241c --- /dev/null +++ b/crates/detect-wasi/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "detect-wasi" +description = "Detect if WASI can be run" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/detect-wasi" +version = "1.0.0" +rust-version = "1.61.0" +authors = ["Félix Saparelli "] +edition = "2021" +license = "Apache-2.0 OR MIT" + +[dependencies] +tempfile = "3.3.0" diff --git a/crates/detect-wasi/LICENSE-APACHE b/crates/detect-wasi/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/detect-wasi/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/detect-wasi/LICENSE-MIT b/crates/detect-wasi/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/detect-wasi/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/examples/detect-wasi.rs b/crates/detect-wasi/src/bin/detect-wasi.rs similarity index 81% rename from examples/detect-wasi.rs rename to crates/detect-wasi/src/bin/detect-wasi.rs index d3224fce..c5cc8f99 100644 --- a/examples/detect-wasi.rs +++ b/crates/detect-wasi/src/bin/detect-wasi.rs @@ -1,6 +1,6 @@ use std::process::exit; -use cargo_binstall::wasi::detect_wasi_runability; +use detect_wasi::detect_wasi_runability; fn main() { if detect_wasi_runability().unwrap() { diff --git a/src/wasi.rs b/crates/detect-wasi/src/lib.rs similarity index 88% rename from src/wasi.rs rename to crates/detect-wasi/src/lib.rs index c1c1340f..e780276f 100644 --- a/src/wasi.rs +++ b/crates/detect-wasi/src/lib.rs @@ -1,11 +1,13 @@ -use std::{fs::File, io::Write, process::Command}; +use std::{ + fs::File, + io::{Result, Write}, + process::Command, +}; #[cfg(unix)] use std::{fs::Permissions, os::unix::fs::PermissionsExt}; use tempfile::tempdir; -use crate::errors::BinstallError; - const WASI_PROGRAM: &[u8] = include_bytes!("miniwasi.wasm"); /// Detect the ability to run WASI @@ -21,7 +23,7 @@ const WASI_PROGRAM: &[u8] = include_bytes!("miniwasi.wasm"); /// ```plain /// :wasi:M::\x00asm::/usr/bin/wasmtime: /// ``` -pub fn detect_wasi_runability() -> Result { +pub fn detect_wasi_runability() -> Result { let progdir = tempdir()?; let prog = progdir.path().join("miniwasi.wasm"); diff --git a/src/miniwasi.wasm b/crates/detect-wasi/src/miniwasi.wasm similarity index 100% rename from src/miniwasi.wasm rename to crates/detect-wasi/src/miniwasi.wasm diff --git a/src/miniwasi.wast b/crates/detect-wasi/src/miniwasi.wast similarity index 100% rename from src/miniwasi.wast rename to crates/detect-wasi/src/miniwasi.wast diff --git a/crates/flock/Cargo.toml b/crates/flock/Cargo.toml new file mode 100644 index 00000000..e9ce16b9 --- /dev/null +++ b/crates/flock/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "flock" +description = "Locked files that can be used like normal File" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/flock" +version = "0.1.0" +rust-version = "1.61.0" +authors = ["Jiahao XU "] +edition = "2021" +license = "Apache-2.0 OR MIT" + +[dependencies] +fs4 = "0.6.2" diff --git a/crates/flock/LICENSE-APACHE b/crates/flock/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/flock/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/flock/LICENSE-MIT b/crates/flock/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/flock/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/crates/flock/src/lib.rs b/crates/flock/src/lib.rs new file mode 100644 index 00000000..9adb4927 --- /dev/null +++ b/crates/flock/src/lib.rs @@ -0,0 +1,90 @@ +//! Locked files with the same API as normal [`File`]s. +//! +//! These use the same mechanisms as, and are interoperable with, Cargo. + +use std::{ + fs::File, + io::{self, IoSlice, IoSliceMut, Result, SeekFrom}, + ops, +}; + +use fs4::FileExt; + +/// A locked file. +#[derive(Debug)] +pub struct FileLock(File); + +impl FileLock { + /// Take an exclusive lock on a [`File`]. + /// + /// Note that this operation is blocking, and should not be called in async contexts. + pub fn new_exclusive(file: File) -> Result { + file.lock_exclusive()?; + + Ok(Self(file)) + } + + /// Take a shared lock on a [`File`]. + /// + /// Note that this operation is blocking, and should not be called in async contexts. + pub fn new_shared(file: File) -> Result { + file.lock_shared()?; + + Ok(Self(file)) + } +} + +impl Drop for FileLock { + fn drop(&mut self) { + let _ = self.unlock(); + } +} + +impl ops::Deref for FileLock { + type Target = File; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl ops::DerefMut for FileLock { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl io::Write for FileLock { + fn write(&mut self, buf: &[u8]) -> Result { + self.0.write(buf) + } + fn flush(&mut self) -> Result<()> { + self.0.flush() + } + + fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result { + self.0.write_vectored(bufs) + } +} + +impl io::Read for FileLock { + fn read(&mut self, buf: &mut [u8]) -> Result { + self.0.read(buf) + } + + fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> Result { + self.0.read_vectored(bufs) + } +} + +impl io::Seek for FileLock { + fn seek(&mut self, pos: SeekFrom) -> Result { + self.0.seek(pos) + } + + fn rewind(&mut self) -> Result<()> { + self.0.rewind() + } + fn stream_position(&mut self) -> Result { + self.0.stream_position() + } +} diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml new file mode 100644 index 00000000..92b088fa --- /dev/null +++ b/crates/lib/Cargo.toml @@ -0,0 +1,75 @@ +[package] +name = "binstall" +description = "Library backend for cargo-binstall" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/binstall" +version = "0.1.0" +rust-version = "1.61.0" +authors = ["ryan "] +edition = "2021" +license = "GPL-3.0" + +[dependencies] +async-trait = "0.1.57" +bytes = "1.2.1" +bzip2 = "0.4.3" +cargo_toml = "0.11.5" +clap = { version = "3.2.16", features = ["derive"] } +compact_str = { version = "0.5.2", features = ["serde"] } +crates_io_api = { version = "0.8.0", default-features = false } +flate2 = { version = "1.0.24", default-features = false } +flock = { version = "0.1.0", path = "../flock" } +futures-util = { version = "0.3.21", default-features = false } +home = "0.5.3" +itertools = "0.10.3" +jobserver = "0.1.24" +log = { version = "0.4.17", features = ["std"] } +miette = "5.2.0" +normalize-path = { version = "0.1.0", path = "../normalize-path" } +once_cell = "1.13.0" +reqwest = { version = "0.11.11", features = ["stream"], default-features = false } +scopeguard = "1.1.0" +semver = { version = "1.0.13", features = ["serde"] } +serde = { version = "1.0.142", features = ["derive"] } +serde-tuple-vec-map = "1.0.1" +serde_json = "1.0.83" +strum = "0.24.1" +strum_macros = "0.24.3" +tar = "0.4.38" +tempfile = "3.3.0" +thiserror = "1.0.32" +tinytemplate = "1.2.1" +tokio = { version = "1.20.1", features = ["macros", "rt", "process", "sync", "signal"], default-features = false } +toml_edit = { version = "0.14.4", features = ["easy"] } +url = { version = "2.2.2", features = ["serde"] } +xz2 = "0.1.7" + +# Disable all features of zip except for features of compression algorithms: +# Disabled features include: +# - aes-crypto: Enables decryption of files which were encrypted with AES, absolutely zero use for +# this crate. +# - time: Enables features using the [time](https://github.com/time-rs/time) crate, +# which is not used by this crate. +zip = { version = "0.6.2", default-features = false, features = ["deflate", "bzip2", "zstd"] } + +# zstd is also depended by zip. +# Since zip 0.6.2 depends on zstd 0.10.0, we also have to use 0.10.0 here, +# otherwise there will be a link conflict. +zstd = { version = "0.10.0", default-features = false } + +[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] +guess_host_triple = "0.1.3" + +[dev-dependencies] +env_logger = "0.9.0" + +[features] +default = ["static", "zlib-ng", "rustls"] + +static = ["bzip2/static", "xz2/static"] +pkg-config = ["zstd/pkg-config"] + +zlib-ng = ["flate2/zlib-ng"] + +rustls = ["crates_io_api/rustls", "reqwest/rustls-tls"] +native-tls = ["reqwest/native-tls"] diff --git a/crates/lib/LICENSE b/crates/lib/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/crates/lib/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/build.rs b/crates/lib/build.rs similarity index 79% rename from build.rs rename to crates/lib/build.rs index 961d0241..729c4c54 100644 --- a/build.rs +++ b/crates/lib/build.rs @@ -1,6 +1,4 @@ fn main() { - embed_resource::compile("manifest.rc"); - // Fetch build target and define this for the compiler println!( "cargo:rustc-env=TARGET={}", diff --git a/src/bins.rs b/crates/lib/src/bins.rs similarity index 90% rename from src/bins.rs rename to crates/lib/src/bins.rs index f7d2041c..c3b330ef 100644 --- a/src/bins.rs +++ b/crates/lib/src/bins.rs @@ -4,8 +4,13 @@ use cargo_toml::Product; use compact_str::CompactString; use log::debug; use serde::Serialize; +use tinytemplate::TinyTemplate; -use crate::{atomic_install, atomic_symlink_file, BinstallError, PkgFmt, PkgMeta, Template}; +use crate::{ + errors::BinstallError, + fs::{atomic_install, atomic_symlink_file}, + manifests::cargo_toml_binstall::{PkgFmt, PkgMeta}, +}; pub struct BinFile { pub base_name: CompactString, @@ -148,4 +153,10 @@ struct Context<'c> { pub binary_ext: &'c str, } -impl<'c> Template for Context<'c> {} +impl<'c> Context<'c> { + fn render(&self, template: &str) -> Result { + let mut tt = TinyTemplate::new(); + tt.add_template("path", template)?; + Ok(tt.render("path", self)?) + } +} diff --git a/crates/lib/src/drivers.rs b/crates/lib/src/drivers.rs new file mode 100644 index 00000000..54304a92 --- /dev/null +++ b/crates/lib/src/drivers.rs @@ -0,0 +1,5 @@ +mod version; +use version::find_version; + +mod crates_io; +pub use crates_io::fetch_crate_cratesio; diff --git a/src/drivers/crates_io.rs b/crates/lib/src/drivers/crates_io.rs similarity index 90% rename from src/drivers/crates_io.rs rename to crates/lib/src/drivers/crates_io.rs index 7dda6fa6..0ac81d64 100644 --- a/src/drivers/crates_io.rs +++ b/crates/lib/src/drivers/crates_io.rs @@ -7,8 +7,13 @@ use reqwest::Client; use semver::VersionReq; use url::Url; +use crate::{ + errors::BinstallError, + helpers::download::download_tar_based_and_visit, + manifests::cargo_toml_binstall::{Meta, TarBasedFmt}, +}; + use super::find_version; -use crate::{helpers::*, BinstallError, Meta, TarBasedFmt}; mod vfs; diff --git a/src/drivers/crates_io/vfs.rs b/crates/lib/src/drivers/crates_io/vfs.rs similarity index 88% rename from src/drivers/crates_io/vfs.rs rename to crates/lib/src/drivers/crates_io/vfs.rs index 66e4875e..3cd53627 100644 --- a/src/drivers/crates_io/vfs.rs +++ b/crates/lib/src/drivers/crates_io/vfs.rs @@ -1,10 +1,11 @@ -use std::collections::{hash_map::HashMap, hash_set::HashSet}; -use std::io; -use std::path::Path; +use std::{ + collections::{hash_map::HashMap, hash_set::HashSet}, + io, + path::Path, +}; use cargo_toml::AbstractFilesystem; - -use crate::helpers::PathExt; +use normalize_path::NormalizePath; /// This type stores the filesystem structure for the crate tarball /// extracted in memory and can be passed to @@ -39,7 +40,7 @@ impl Vfs { impl AbstractFilesystem for Vfs { fn file_names_in(&self, rel_path: &str) -> io::Result>> { - let rel_path = Path::new(rel_path).normalize_path(); + let rel_path = Path::new(rel_path).normalize(); Ok(self.0.get(&*rel_path).map(Clone::clone).unwrap_or_default()) } diff --git a/src/drivers/crates_io/visitor.rs b/crates/lib/src/drivers/crates_io/visitor.rs similarity index 90% rename from src/drivers/crates_io/visitor.rs rename to crates/lib/src/drivers/crates_io/visitor.rs index 0eb7bd6c..5efc8012 100644 --- a/src/drivers/crates_io/visitor.rs +++ b/crates/lib/src/drivers/crates_io/visitor.rs @@ -1,14 +1,17 @@ -use std::io::Read; -use std::path::{Path, PathBuf}; +use std::{ + io::Read, + path::{Path, PathBuf}, +}; use cargo_toml::Manifest; use log::debug; +use normalize_path::NormalizePath; use tar::Entries; use super::vfs::Vfs; use crate::{ - helpers::{PathExt, TarEntriesVisitor}, - BinstallError, Meta, + errors::BinstallError, helpers::async_extracter::TarEntriesVisitor, + manifests::cargo_toml_binstall::Meta, }; #[derive(Debug)] @@ -38,7 +41,7 @@ impl TarEntriesVisitor for ManifestVisitor { for res in entries { let mut entry = res?; let path = entry.path()?; - let path = path.normalize_path(); + let path = path.normalize(); let path = if let Ok(path) = path.strip_prefix(&self.manifest_dir_path) { path diff --git a/src/drivers/version.rs b/crates/lib/src/drivers/version.rs similarity index 97% rename from src/drivers/version.rs rename to crates/lib/src/drivers/version.rs index abddbc61..4b8b7413 100644 --- a/src/drivers/version.rs +++ b/crates/lib/src/drivers/version.rs @@ -1,7 +1,7 @@ use log::debug; use semver::VersionReq; -use crate::BinstallError; +use crate::errors::BinstallError; pub(super) trait Version { /// Return `None` on error. diff --git a/src/errors.rs b/crates/lib/src/errors.rs similarity index 100% rename from src/errors.rs rename to crates/lib/src/errors.rs diff --git a/src/fetchers.rs b/crates/lib/src/fetchers.rs similarity index 94% rename from src/fetchers.rs rename to crates/lib/src/fetchers.rs index 2d12e13e..a61742aa 100644 --- a/src/fetchers.rs +++ b/crates/lib/src/fetchers.rs @@ -1,5 +1,4 @@ -use std::path::Path; -use std::sync::Arc; +use std::{path::Path, sync::Arc}; use compact_str::CompactString; pub use gh_crate_meta::*; @@ -7,7 +6,11 @@ pub use log::debug; pub use quickinstall::*; use reqwest::Client; -use crate::{AutoAbortJoinHandle, BinstallError, PkgFmt, PkgMeta}; +use crate::{ + errors::BinstallError, + helpers::tasks::AutoAbortJoinHandle, + manifests::cargo_toml_binstall::{PkgFmt, PkgMeta}, +}; mod gh_crate_meta; mod quickinstall; diff --git a/src/fetchers/gh_crate_meta.rs b/crates/lib/src/fetchers/gh_crate_meta.rs similarity index 95% rename from src/fetchers/gh_crate_meta.rs rename to crates/lib/src/fetchers/gh_crate_meta.rs index 2c009ff8..bac96a20 100644 --- a/src/fetchers/gh_crate_meta.rs +++ b/crates/lib/src/fetchers/gh_crate_meta.rs @@ -1,19 +1,21 @@ -use std::path::Path; -use std::sync::Arc; +use std::{path::Path, sync::Arc}; use compact_str::{CompactString, ToCompactString}; use log::{debug, warn}; use once_cell::sync::OnceCell; -use reqwest::Client; -use reqwest::Method; +use reqwest::{Client, Method}; use serde::Serialize; +use tinytemplate::TinyTemplate; use url::Url; -use super::Data; use crate::{ - download_and_extract, remote_exists, AutoAbortJoinHandle, BinstallError, PkgFmt, Template, + errors::BinstallError, + helpers::{download::download_and_extract, remote::remote_exists, tasks::AutoAbortJoinHandle}, + manifests::cargo_toml_binstall::PkgFmt, }; +use super::Data; + pub struct GhCrateMeta { client: Client, data: Data, @@ -124,8 +126,6 @@ struct Context<'c> { pub binary_ext: &'c str, } -impl<'c> Template for Context<'c> {} - impl<'c> Context<'c> { pub(self) fn from_data(data: &'c Data, archive_format: &'c str) -> Self { Self { @@ -145,14 +145,18 @@ impl<'c> Context<'c> { pub(self) fn render_url(&self, template: &str) -> Result { debug!("Render {template:?} using context: {:?}", self); - Ok(Url::parse(&self.render(template)?)?) + + let mut tt = TinyTemplate::new(); + tt.add_template("path", template)?; + Ok(Url::parse(&tt.render("path", self)?)?) } } #[cfg(test)] mod test { + use crate::manifests::cargo_toml_binstall::{PkgFmt, PkgMeta}; + use super::{super::Data, Context}; - use crate::{PkgFmt, PkgMeta}; use url::Url; fn url(s: &str) -> Url { diff --git a/src/fetchers/quickinstall.rs b/crates/lib/src/fetchers/quickinstall.rs similarity index 93% rename from src/fetchers/quickinstall.rs rename to crates/lib/src/fetchers/quickinstall.rs index 0bdaa290..015e6273 100644 --- a/src/fetchers/quickinstall.rs +++ b/crates/lib/src/fetchers/quickinstall.rs @@ -1,5 +1,4 @@ -use std::path::Path; -use std::sync::Arc; +use std::{path::Path, sync::Arc}; use compact_str::CompactString; use log::debug; @@ -8,8 +7,13 @@ use reqwest::Method; use tokio::task::JoinHandle; use url::Url; +use crate::{ + errors::BinstallError, + helpers::{download::download_and_extract, remote::remote_exists}, + manifests::cargo_toml_binstall::PkgFmt, +}; + use super::Data; -use crate::{download_and_extract, remote_exists, BinstallError, PkgFmt}; const BASE_URL: &str = "https://github.com/alsuren/cargo-quickinstall/releases/download"; const STATS_URL: &str = "https://warehouse-clerk-tmp.vercel.app/api/crate"; diff --git a/crates/lib/src/fs.rs b/crates/lib/src/fs.rs new file mode 100644 index 00000000..2e4f09c6 --- /dev/null +++ b/crates/lib/src/fs.rs @@ -0,0 +1,103 @@ +use std::{fs, io, path::Path}; + +use log::debug; +use tempfile::NamedTempFile; + +/// Returned file is readable and writable. +pub fn create_if_not_exist(path: impl AsRef) -> io::Result { + let path = path.as_ref(); + + let mut options = fs::File::options(); + options.read(true).write(true); + + options + .clone() + .create_new(true) + .open(path) + .or_else(|_| options.open(path)) +} + +/// Atomically install a file. +/// +/// This is a blocking function, must be called in `block_in_place` mode. +pub fn atomic_install(src: &Path, dst: &Path) -> io::Result<()> { + debug!( + "Attempting to atomically rename from '{}' to '{}'", + src.display(), + dst.display() + ); + + if let Err(err) = fs::rename(src, dst) { + debug!("Attempting at atomic rename failed: {err:#?}, fallback to creating tempfile."); + // src and dst is not on the same filesystem/mountpoint. + // Fallback to creating NamedTempFile on the parent dir of + // dst. + + let mut src_file = fs::File::open(src)?; + + let parent = dst.parent().unwrap(); + debug!("Creating named tempfile at '{}'", parent.display()); + let mut tempfile = NamedTempFile::new_in(parent)?; + + debug!( + "Copying from '{}' to '{}'", + src.display(), + tempfile.path().display() + ); + io::copy(&mut src_file, tempfile.as_file_mut())?; + + debug!("Retrieving permissions of '{}'", src.display()); + let permissions = src_file.metadata()?.permissions(); + + debug!( + "Setting permissions of '{}' to '{permissions:#?}'", + tempfile.path().display() + ); + tempfile.as_file().set_permissions(permissions)?; + + debug!( + "Persisting '{}' to '{}'", + tempfile.path().display(), + dst.display() + ); + tempfile.persist(dst).map_err(io::Error::from)?; + } else { + debug!("Attempting at atomically succeeded."); + } + + Ok(()) +} + +fn symlink_file, Q: AsRef>(original: P, link: Q) -> io::Result<()> { + #[cfg(target_family = "unix")] + let f = std::os::unix::fs::symlink; + #[cfg(target_family = "windows")] + let f = std::os::windows::fs::symlink_file; + + f(original, link) +} + +/// Atomically install symlink "link" to a file "dst". +/// +/// This is a blocking function, must be called in `block_in_place` mode. +pub fn atomic_symlink_file(dest: &Path, link: &Path) -> io::Result<()> { + let parent = link.parent().unwrap(); + + debug!("Creating tempPath at '{}'", parent.display()); + let temp_path = NamedTempFile::new_in(parent)?.into_temp_path(); + fs::remove_file(&temp_path)?; + + debug!( + "Creating symlink '{}' to file '{}'", + temp_path.display(), + dest.display() + ); + symlink_file(dest, &temp_path)?; + + debug!( + "Persisting '{}' to '{}'", + temp_path.display(), + link.display() + ); + temp_path.persist(link).map_err(io::Error::from) +} diff --git a/crates/lib/src/helpers.rs b/crates/lib/src/helpers.rs new file mode 100644 index 00000000..16ade8b4 --- /dev/null +++ b/crates/lib/src/helpers.rs @@ -0,0 +1,9 @@ +pub mod async_extracter; +pub mod download; +pub mod extracter; +pub mod jobserver_client; +pub mod remote; +pub mod signal; +pub mod statics; +pub mod stream_readable; +pub mod tasks; diff --git a/src/helpers/async_extracter.rs b/crates/lib/src/helpers/async_extracter.rs similarity index 79% rename from src/helpers/async_extracter.rs rename to crates/lib/src/helpers/async_extracter.rs index 9986c48a..844fbc0b 100644 --- a/src/helpers/async_extracter.rs +++ b/crates/lib/src/helpers/async_extracter.rs @@ -1,21 +1,9 @@ -//! # Advantages -//! -//! Using this mod has the following advantages over downloading -//! to file then extracting: -//! -//! - The code is pipelined instead of storing the downloaded file in memory -//! and extract it, except for `PkgFmt::Zip`, since `ZipArchiver::new` -//! requires `std::io::Seek`, so it fallbacks to writing the a file then -//! unzip it. -//! - Compressing/writing which takes a lot of CPU time will not block -//! the runtime anymore. -//! - For all `tar` based formats, it can extract only specified files and -//! process them in memory, without any disk I/O. - -use std::fmt::Debug; -use std::fs; -use std::io::{copy, Read, Seek}; -use std::path::Path; +use std::{ + fmt::Debug, + fs, + io::{copy, Read, Seek}, + path::Path, +}; use bytes::Bytes; use futures_util::stream::Stream; @@ -25,8 +13,9 @@ use tar::Entries; use tempfile::tempfile; use tokio::task::block_in_place; +use crate::{errors::BinstallError, manifests::cargo_toml_binstall::TarBasedFmt}; + use super::{extracter::*, stream_readable::StreamReadable}; -use crate::{BinstallError, TarBasedFmt}; pub async fn extract_bin(stream: S, path: &Path) -> Result<(), BinstallError> where diff --git a/crates/lib/src/helpers/download.rs b/crates/lib/src/helpers/download.rs new file mode 100644 index 00000000..25d94f49 --- /dev/null +++ b/crates/lib/src/helpers/download.rs @@ -0,0 +1,62 @@ +use std::{fmt::Debug, path::Path}; + +use log::debug; +use reqwest::{Client, Url}; + +use crate::{ + errors::BinstallError, + helpers::{ + async_extracter::{ + extract_bin, extract_tar_based_stream, extract_tar_based_stream_and_visit, extract_zip, + }, + remote::create_request, + }, + manifests::cargo_toml_binstall::{PkgFmt, PkgFmtDecomposed, TarBasedFmt}, +}; + +use super::async_extracter::TarEntriesVisitor; + +/// Download a file from the provided URL and extract it to the provided path. +pub async fn download_and_extract>( + client: &Client, + url: &Url, + fmt: PkgFmt, + path: P, +) -> Result<(), BinstallError> { + let stream = create_request(client, url.clone()).await?; + + let path = path.as_ref(); + debug!("Downloading and extracting to: '{}'", path.display()); + + match fmt.decompose() { + PkgFmtDecomposed::Tar(fmt) => extract_tar_based_stream(stream, path, fmt).await?, + PkgFmtDecomposed::Bin => extract_bin(stream, path).await?, + PkgFmtDecomposed::Zip => extract_zip(stream, path).await?, + } + + debug!("Download OK, extracted to: '{}'", path.display()); + + Ok(()) +} + +/// Download a file from the provided URL and extract part of it to +/// the provided path. +/// +/// * `filter` - If Some, then it will pass the path of the file to it +/// and only extract ones which filter returns `true`. +pub async fn download_tar_based_and_visit( + client: &Client, + url: Url, + fmt: TarBasedFmt, + visitor: V, +) -> Result { + let stream = create_request(client, url).await?; + + debug!("Downloading and extracting then in-memory processing"); + + let ret = extract_tar_based_stream_and_visit(stream, fmt, visitor).await?; + + debug!("Download, extraction and in-memory procession OK"); + + Ok(ret) +} diff --git a/src/helpers/extracter.rs b/crates/lib/src/helpers/extracter.rs similarity index 77% rename from src/helpers/extracter.rs rename to crates/lib/src/helpers/extracter.rs index 211f628e..43fa6d3c 100644 --- a/src/helpers/extracter.rs +++ b/crates/lib/src/helpers/extracter.rs @@ -1,6 +1,8 @@ -use std::fs::File; -use std::io::{self, BufRead, Read}; -use std::path::Path; +use std::{ + fs::File, + io::{self, BufRead, Read}, + path::Path, +}; use bzip2::bufread::BzDecoder; use flate2::bufread::GzDecoder; @@ -10,7 +12,7 @@ use xz2::bufread::XzDecoder; use zip::read::ZipArchive; use zstd::stream::Decoder as ZstdDecoder; -use crate::{BinstallError, TarBasedFmt}; +use crate::{errors::BinstallError, manifests::cargo_toml_binstall::TarBasedFmt}; pub(super) fn create_tar_decoder( dat: impl BufRead + 'static, @@ -24,9 +26,8 @@ pub(super) fn create_tar_decoder( Tgz => Box::new(GzDecoder::new(dat)), Txz => Box::new(XzDecoder::new(dat)), Tzstd => { - // The error can only come from raw::Decoder::with_dictionary - // as of zstd 0.10.2 and 0.11.2, which is specified - // as &[] by ZstdDecoder::new, thus ZstdDecoder::new + // The error can only come from raw::Decoder::with_dictionary as of zstd 0.10.2 and + // 0.11.2, which is specified as `&[]` by `ZstdDecoder::new`, thus `ZstdDecoder::new` // should not return any error. Box::new(ZstdDecoder::with_buffer(dat)?) } diff --git a/src/helpers/jobserver_client.rs b/crates/lib/src/helpers/jobserver_client.rs similarity index 88% rename from src/helpers/jobserver_client.rs rename to crates/lib/src/helpers/jobserver_client.rs index 14a6c5f2..acfbc6c5 100644 --- a/src/helpers/jobserver_client.rs +++ b/crates/lib/src/helpers/jobserver_client.rs @@ -1,11 +1,9 @@ -use std::num::NonZeroUsize; -use std::sync::Arc; -use std::thread::available_parallelism; +use std::{num::NonZeroUsize, sync::Arc, thread::available_parallelism}; use jobserver::Client; use tokio::sync::OnceCell; -use crate::BinstallError; +use crate::errors::BinstallError; #[derive(Clone)] pub struct LazyJobserverClient(Arc>); diff --git a/crates/lib/src/helpers/remote.rs b/crates/lib/src/helpers/remote.rs new file mode 100644 index 00000000..042ae7ce --- /dev/null +++ b/crates/lib/src/helpers/remote.rs @@ -0,0 +1,62 @@ +use std::env; + +use bytes::Bytes; +use futures_util::stream::Stream; +use log::debug; +use reqwest::{tls, Client, ClientBuilder, Method, Response}; +use url::Url; + +use crate::errors::BinstallError; + +pub fn create_reqwest_client( + secure: bool, + min_tls: Option, +) -> Result { + const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); + + let mut builder = ClientBuilder::new().user_agent(USER_AGENT); + + if secure { + builder = builder + .https_only(true) + .min_tls_version(tls::Version::TLS_1_2); + } + + if let Some(ver) = min_tls { + builder = builder.min_tls_version(ver); + } + + Ok(builder.build()?) +} + +pub async fn remote_exists( + client: Client, + url: Url, + method: Method, +) -> Result { + let req = client + .request(method.clone(), url.clone()) + .send() + .await + .map_err(|err| BinstallError::Http { method, url, err })?; + Ok(req.status().is_success()) +} + +pub(crate) async fn create_request( + client: &Client, + url: Url, +) -> Result>, BinstallError> { + debug!("Downloading from: '{url}'"); + + client + .get(url.clone()) + .send() + .await + .and_then(|r| r.error_for_status()) + .map_err(|err| BinstallError::Http { + method: Method::GET, + url, + err, + }) + .map(Response::bytes_stream) +} diff --git a/src/helpers/signal.rs b/crates/lib/src/helpers/signal.rs similarity index 97% rename from src/helpers/signal.rs rename to crates/lib/src/helpers/signal.rs index ecfe7c4a..e15ed8e1 100644 --- a/src/helpers/signal.rs +++ b/crates/lib/src/helpers/signal.rs @@ -1,8 +1,10 @@ -use futures_util::future::pending; use std::io; + +use futures_util::future::pending; use tokio::signal; -use super::{AutoAbortJoinHandle, BinstallError}; +use super::tasks::AutoAbortJoinHandle; +use crate::errors::BinstallError; /// This function will poll the handle while listening for ctrl_c, /// `SIGINT`, `SIGHUP`, `SIGTERM` and `SIGQUIT`. diff --git a/crates/lib/src/helpers/statics.rs b/crates/lib/src/helpers/statics.rs new file mode 100644 index 00000000..ecc490c8 --- /dev/null +++ b/crates/lib/src/helpers/statics.rs @@ -0,0 +1,23 @@ +use std::{ + io::Error, + ops::Deref, + path::{Path, PathBuf}, +}; + +use once_cell::sync::{Lazy, OnceCell}; +use url::Url; + +pub fn cargo_home() -> Result<&'static Path, Error> { + static CARGO_HOME: OnceCell = OnceCell::new(); + + CARGO_HOME + .get_or_try_init(home::cargo_home) + .map(Deref::deref) +} + +pub fn cratesio_url() -> &'static Url { + static CRATESIO: Lazy Url> = + Lazy::new(|| Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()); + + &*CRATESIO +} diff --git a/src/helpers/stream_readable.rs b/crates/lib/src/helpers/stream_readable.rs similarity index 95% rename from src/helpers/stream_readable.rs rename to crates/lib/src/helpers/stream_readable.rs index 17113591..0f26d0a1 100644 --- a/src/helpers/stream_readable.rs +++ b/crates/lib/src/helpers/stream_readable.rs @@ -1,11 +1,13 @@ -use std::cmp::min; -use std::io::{self, BufRead, Read}; +use std::{ + cmp::min, + io::{self, BufRead, Read}, +}; use bytes::{Buf, Bytes}; use futures_util::stream::{Stream, StreamExt}; use tokio::runtime::Handle; -use super::BinstallError; +use crate::errors::BinstallError; /// This wraps an AsyncIterator as a `Read`able. /// It must be used in non-async context only, diff --git a/src/helpers/auto_abort_join_handle.rs b/crates/lib/src/helpers/tasks.rs similarity index 97% rename from src/helpers/auto_abort_join_handle.rs rename to crates/lib/src/helpers/tasks.rs index 669f352a..f1820be9 100644 --- a/src/helpers/auto_abort_join_handle.rs +++ b/crates/lib/src/helpers/tasks.rs @@ -7,7 +7,7 @@ use std::{ use tokio::task::JoinHandle; -use super::BinstallError; +use crate::errors::BinstallError; #[derive(Debug)] pub struct AutoAbortJoinHandle(JoinHandle); diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs new file mode 100644 index 00000000..261a008b --- /dev/null +++ b/crates/lib/src/lib.rs @@ -0,0 +1,9 @@ +pub mod bins; +pub mod drivers; +pub mod errors; +pub mod fetchers; +pub mod fs; +pub mod helpers; +pub mod manifests; +pub mod ops; +pub mod targets; diff --git a/crates/lib/src/manifests.rs b/crates/lib/src/manifests.rs new file mode 100644 index 00000000..d5dc2542 --- /dev/null +++ b/crates/lib/src/manifests.rs @@ -0,0 +1,14 @@ +//! Manifest formats and utilities. +//! +//! There are three types of manifests Binstall may deal with: +//! - manifests that define how to fetch and install a package +//! ([Cargo.toml's `[metadata.binstall]`][cargo_toml_binstall]); +//! - manifests that record which packages _are_ installed +//! ([Cargo's `.crates.toml`][cargo_crates_v1] and +//! [Binstall's `.crates-v1.json`][binstall_crates_v1]); +//! - manifests that specify which packages _to_ install (currently none). + +pub mod binstall_crates_v1; +pub mod cargo_crates_v1; +pub mod cargo_toml_binstall; +pub mod crate_info; diff --git a/src/metafiles/binstall_v1.rs b/crates/lib/src/manifests/binstall_crates_v1.rs similarity index 68% rename from src/metafiles/binstall_v1.rs rename to crates/lib/src/manifests/binstall_crates_v1.rs index 2d429ec2..6593b81a 100644 --- a/src/metafiles/binstall_v1.rs +++ b/crates/lib/src/manifests/binstall_crates_v1.rs @@ -1,93 +1,27 @@ +//! Binstall's `crates-v1.json` manifest. +//! +//! This manifest is used by Binstall to record which crates were installed, and may be used by +//! other (third party) tooling to act upon these crates (e.g. upgrade them, list them, etc). +//! +//! The format is a series of JSON object concatenated together. It is _not_ NLJSON, though writing +//! NLJSON to the file will be understood fine. + use std::{ - borrow, cmp, collections::{btree_set, BTreeSet}, - fs, hash, + fs, io::{self, Seek, Write}, iter::{IntoIterator, Iterator}, path::{Path, PathBuf}, }; -use compact_str::CompactString; +use flock::FileLock; use miette::Diagnostic; -use semver::Version; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use thiserror::Error; -use url::Url; -use crate::{cargo_home, cratesio_url, create_if_not_exist, FileLock}; +use crate::{fs::create_if_not_exist, helpers::statics::cargo_home}; -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct MetaData { - pub name: CompactString, - pub version_req: CompactString, - pub current_version: Version, - pub source: Source, - pub target: CompactString, - pub bins: Vec, - - /// Forwards compatibility. Unknown keys from future versions - /// will be stored here and retained when the file is saved. - /// - /// We use an `Vec` here since it is never accessed in Rust. - #[serde(flatten, with = "tuple_vec_map")] - pub other: Vec<(CompactString, serde_json::Value)>, -} - -impl borrow::Borrow for MetaData { - fn borrow(&self) -> &str { - &self.name - } -} - -impl PartialEq for MetaData { - fn eq(&self, other: &Self) -> bool { - self.name == other.name - } -} -impl Eq for MetaData {} - -impl PartialOrd for MetaData { - fn partial_cmp(&self, other: &Self) -> Option { - self.name.partial_cmp(&other.name) - } -} - -impl Ord for MetaData { - fn cmp(&self, other: &Self) -> cmp::Ordering { - self.name.cmp(&other.name) - } -} - -impl hash::Hash for MetaData { - fn hash(&self, state: &mut H) - where - H: hash::Hasher, - { - self.name.hash(state) - } -} - -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] -pub enum SourceType { - Git, - Path, - Registry, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Source { - pub source_type: SourceType, - pub url: Url, -} - -impl Source { - pub fn cratesio_registry() -> Source { - Self { - source_type: SourceType::Registry, - url: cratesio_url().clone(), - } - } -} +use super::crate_info::CrateInfo; #[derive(Debug, Diagnostic, Error)] pub enum Error { @@ -100,7 +34,7 @@ pub enum Error { pub fn append_to_path(path: impl AsRef, iter: Iter) -> Result<(), Error> where - Iter: IntoIterator, + Iter: IntoIterator, { let mut file = FileLock::new_exclusive(create_if_not_exist(path.as_ref())?)?; // Move the cursor to EOF @@ -111,14 +45,14 @@ where pub fn append(iter: Iter) -> Result<(), Error> where - Iter: IntoIterator, + Iter: IntoIterator, { append_to_path(default_path()?, iter) } pub fn write_to( file: &mut FileLock, - iter: &mut dyn Iterator, + iter: &mut dyn Iterator, ) -> Result<(), Error> { let writer = io::BufWriter::with_capacity(512, file); @@ -145,7 +79,7 @@ pub fn default_path() -> Result { pub struct Records { file: FileLock, /// Use BTreeSet to dedup the metadata - data: BTreeSet, + data: BTreeSet, } impl Records { @@ -186,7 +120,7 @@ impl Records { Ok(()) } - pub fn get(&self, value: impl AsRef) -> Option<&MetaData> { + pub fn get(&self, value: impl AsRef) -> Option<&CrateInfo> { self.data.get(value.as_ref()) } @@ -198,11 +132,11 @@ impl Records { /// If the set did not have an equal element present, true is returned. /// If the set did have an equal element present, false is returned, /// and the entry is not updated. - pub fn insert(&mut self, value: MetaData) -> bool { + pub fn insert(&mut self, value: CrateInfo) -> bool { self.data.insert(value) } - pub fn replace(&mut self, value: MetaData) -> Option { + pub fn replace(&mut self, value: CrateInfo) -> Option { self.data.replace(value) } @@ -210,7 +144,7 @@ impl Records { self.data.remove(value.as_ref()) } - pub fn take(&mut self, value: impl AsRef) -> Option { + pub fn take(&mut self, value: impl AsRef) -> Option { self.data.take(value.as_ref()) } @@ -224,9 +158,9 @@ impl Records { } impl<'a> IntoIterator for &'a Records { - type Item = &'a MetaData; + type Item = &'a CrateInfo; - type IntoIter = btree_set::Iter<'a, MetaData>; + type IntoIter = btree_set::Iter<'a, CrateInfo>; fn into_iter(self) -> Self::IntoIter { self.data.iter() @@ -236,8 +170,10 @@ impl<'a> IntoIterator for &'a Records { #[cfg(test)] mod test { use super::*; - use crate::target::TARGET; + use crate::{manifests::crate_info::CrateSource, targets::TARGET}; + use compact_str::CompactString; + use semver::Version; use tempfile::NamedTempFile; macro_rules! assert_records_eq { @@ -257,29 +193,29 @@ mod test { let path = named_tempfile.path(); let metadata_vec = [ - MetaData { + CrateInfo { name: "a".into(), version_req: "*".into(), current_version: Version::new(0, 1, 0), - source: Source::cratesio_registry(), + source: CrateSource::cratesio_registry(), target: target.clone(), bins: vec!["1".into(), "2".into()], other: Default::default(), }, - MetaData { + CrateInfo { name: "b".into(), version_req: "0.1.0".into(), current_version: Version::new(0, 1, 0), - source: Source::cratesio_registry(), + source: CrateSource::cratesio_registry(), target: target.clone(), bins: vec!["1".into(), "2".into()], other: Default::default(), }, - MetaData { + CrateInfo { name: "a".into(), version_req: "*".into(), current_version: Version::new(0, 2, 0), - source: Source::cratesio_registry(), + source: CrateSource::cratesio_registry(), target: target.clone(), bins: vec!["1".into()], other: Default::default(), @@ -306,11 +242,11 @@ mod test { // Drop the exclusive file lock drop(records); - let new_metadata = MetaData { + let new_metadata = CrateInfo { name: "b".into(), version_req: "0.1.0".into(), current_version: Version::new(0, 1, 1), - source: Source::cratesio_registry(), + source: CrateSource::cratesio_registry(), target, bins: vec!["1".into(), "2".into()], other: Default::default(), diff --git a/src/metafiles/v1.rs b/crates/lib/src/manifests/cargo_crates_v1.rs similarity index 80% rename from src/metafiles/v1.rs rename to crates/lib/src/manifests/cargo_crates_v1.rs index a4857530..a593ad0a 100644 --- a/src/metafiles/v1.rs +++ b/crates/lib/src/manifests/cargo_crates_v1.rs @@ -1,3 +1,12 @@ +//! Cargo's `.crates.toml` manifest. +//! +//! This manifest is used by Cargo to record which crates were installed by `cargo-install` and by +//! other Cargo (first and third party) tooling to act upon these crates (e.g. upgrade them, list +//! them, etc). +//! +//! Binstall writes to this manifest when installing a crate, for interoperability with the Cargo +//! ecosystem. + use std::{ collections::BTreeMap, fs::File, @@ -7,12 +16,17 @@ use std::{ }; use compact_str::CompactString; +use flock::FileLock; use miette::Diagnostic; use serde::{Deserialize, Serialize}; use thiserror::Error; -use super::{binstall_v1::MetaData, CrateVersionSource}; -use crate::{cargo_home, create_if_not_exist, FileLock}; +use crate::{fs::create_if_not_exist, helpers::statics::cargo_home}; + +use super::crate_info::CrateInfo; + +mod crate_version_source; +use crate_version_source::*; #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct CratesToml { @@ -71,7 +85,7 @@ impl CratesToml { iter: Iter, ) -> Result<(), CratesTomlParseError> where - Iter: IntoIterator, + Iter: IntoIterator, { let mut file = FileLock::new_exclusive(create_if_not_exist(path.as_ref())?)?; let mut c1 = if file.metadata()?.len() != 0 { @@ -92,7 +106,7 @@ impl CratesToml { pub fn append<'a, Iter>(iter: Iter) -> Result<(), CratesTomlParseError> where - Iter: IntoIterator, + Iter: IntoIterator, { Self::append_to_path(Self::default_path()?, iter) } @@ -110,13 +124,13 @@ pub enum CratesTomlParseError { TomlWrite(#[from] toml_edit::easy::ser::Error), #[error(transparent)] - CvsParse(#[from] super::CvsParseError), + CvsParse(#[from] CvsParseError), } #[cfg(test)] mod tests { - use super::{super::binstall_v1, *}; - use crate::target::TARGET; + use super::*; + use crate::{manifests::crate_info::CrateSource, targets::TARGET}; use semver::Version; use tempfile::TempDir; @@ -128,11 +142,11 @@ mod tests { CratesToml::append_to_path( &path, - &[MetaData { + &[CrateInfo { name: "cargo-binstall".into(), version_req: "*".into(), current_version: Version::new(0, 11, 1), - source: binstall_v1::Source::cratesio_registry(), + source: CrateSource::cratesio_registry(), target: TARGET.into(), bins: vec!["cargo-binstall".into()], other: Default::default(), diff --git a/src/metafiles/cvs.rs b/crates/lib/src/manifests/cargo_crates_v1/crate_version_source.rs similarity index 92% rename from src/metafiles/cvs.rs rename to crates/lib/src/manifests/cargo_crates_v1/crate_version_source.rs index f8bd7777..bf41e808 100644 --- a/src/metafiles/cvs.rs +++ b/crates/lib/src/manifests/cargo_crates_v1/crate_version_source.rs @@ -7,7 +7,10 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; use url::Url; -use crate::cratesio_url; +use crate::{ + helpers::statics::cratesio_url, + manifests::crate_info::{CrateInfo, CrateSource, SourceType}, +}; #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub struct CrateVersionSource { @@ -16,8 +19,8 @@ pub struct CrateVersionSource { pub source: Source, } -impl From<&super::binstall_v1::MetaData> for CrateVersionSource { - fn from(metadata: &super::binstall_v1::MetaData) -> Self { +impl From<&CrateInfo> for CrateVersionSource { + fn from(metadata: &CrateInfo) -> Self { super::CrateVersionSource { name: metadata.name.clone(), version: metadata.current_version.clone(), @@ -39,9 +42,9 @@ impl Source { } } -impl From<&super::binstall_v1::Source> for Source { - fn from(source: &super::binstall_v1::Source) -> Self { - use super::binstall_v1::SourceType::*; +impl From<&CrateSource> for Source { + fn from(source: &CrateSource) -> Self { + use SourceType::*; let url = source.url.clone(); diff --git a/src/lib.rs b/crates/lib/src/manifests/cargo_toml_binstall.rs similarity index 67% rename from src/lib.rs rename to crates/lib/src/manifests/cargo_toml_binstall.rs index 60691853..9f1ce933 100644 --- a/src/lib.rs +++ b/crates/lib/src/manifests/cargo_toml_binstall.rs @@ -1,27 +1,15 @@ +//! The format of the `[package.metadata.binstall]` manifest. +//! +//! This manifest defines how a particular binary crate may be installed by Binstall. + use std::collections::HashMap; use serde::{Deserialize, Serialize}; -pub mod drivers; -pub use drivers::*; +#[doc(inline)] +pub use package_formats::*; -pub mod errors; -pub use errors::*; - -pub mod helpers; -pub use helpers::*; - -pub mod bins; -pub mod binstall; -pub mod fetchers; -pub mod metafiles; -pub mod wasi; - -mod target; -pub use target::*; - -mod formats; -pub use formats::*; +mod package_formats; /// Default package path template (may be overridden in package Cargo.toml) pub const DEFAULT_PKG_URL: &str = @@ -112,43 +100,3 @@ pub struct BinMeta { /// Binary template path (within package) pub path: String, } - -#[cfg(test)] -mod test { - use crate::load_manifest_path; - - use cargo_toml::Product; - - fn init() { - let _ = env_logger::builder().is_test(true).try_init(); - } - - #[test] - fn parse_meta() { - init(); - - let mut manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - manifest_dir.push_str("/Cargo.toml"); - - let manifest = load_manifest_path(&manifest_dir).expect("Error parsing metadata"); - let package = manifest.package.unwrap(); - let meta = package.metadata.and_then(|m| m.binstall).unwrap(); - - assert_eq!(&package.name, "cargo-binstall"); - - assert_eq!( - &meta.pkg_url, - "{ repo }/releases/download/v{ version }/{ name }-{ target }.{ archive-format }" - ); - - assert_eq!( - manifest.bin.as_slice(), - &[Product { - name: Some("cargo-binstall".to_string()), - path: Some("src/main.rs".to_string()), - edition: Some(cargo_toml::Edition::E2021), - ..Default::default() - },], - ); - } -} diff --git a/src/formats.rs b/crates/lib/src/manifests/cargo_toml_binstall/package_formats.rs similarity index 100% rename from src/formats.rs rename to crates/lib/src/manifests/cargo_toml_binstall/package_formats.rs diff --git a/crates/lib/src/manifests/crate_info.rs b/crates/lib/src/manifests/crate_info.rs new file mode 100644 index 00000000..221390a6 --- /dev/null +++ b/crates/lib/src/manifests/crate_info.rs @@ -0,0 +1,83 @@ +//! Common structure for crate information for post-install manifests. + +use std::{borrow, cmp, hash}; + +use compact_str::CompactString; +use semver::Version; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::helpers::statics::cratesio_url; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CrateInfo { + pub name: CompactString, + pub version_req: CompactString, + pub current_version: Version, + pub source: CrateSource, + pub target: CompactString, + pub bins: Vec, + + /// Forwards compatibility. Unknown keys from future versions + /// will be stored here and retained when the file is saved. + /// + /// We use an `Vec` here since it is never accessed in Rust. + #[serde(flatten, with = "tuple_vec_map")] + pub other: Vec<(CompactString, serde_json::Value)>, +} + +impl borrow::Borrow for CrateInfo { + fn borrow(&self) -> &str { + &self.name + } +} + +impl PartialEq for CrateInfo { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} +impl Eq for CrateInfo {} + +impl PartialOrd for CrateInfo { + fn partial_cmp(&self, other: &Self) -> Option { + self.name.partial_cmp(&other.name) + } +} + +impl Ord for CrateInfo { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.name.cmp(&other.name) + } +} + +impl hash::Hash for CrateInfo { + fn hash(&self, state: &mut H) + where + H: hash::Hasher, + { + self.name.hash(state) + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub enum SourceType { + Git, + Path, + Registry, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CrateSource { + pub source_type: SourceType, + pub url: Url, +} + +impl CrateSource { + pub fn cratesio_registry() -> CrateSource { + Self { + source_type: SourceType::Registry, + url: cratesio_url().clone(), + } + } +} diff --git a/src/binstall.rs b/crates/lib/src/ops.rs similarity index 68% rename from src/binstall.rs rename to crates/lib/src/ops.rs index 20b582b7..4274df96 100644 --- a/src/binstall.rs +++ b/crates/lib/src/ops.rs @@ -1,14 +1,13 @@ +//! Concrete Binstall operations. + use std::path::PathBuf; use semver::VersionReq; -use crate::{metafiles::binstall_v1::MetaData, DesiredTargets, PkgOverride}; +use crate::{manifests::cargo_toml_binstall::PkgOverride, targets::DesiredTargets}; -mod resolve; -pub use resolve::*; - -mod install; -pub use install::*; +pub mod install; +pub mod resolve; pub struct Options { pub no_symlinks: bool, diff --git a/src/binstall/install.rs b/crates/lib/src/ops/install.rs similarity index 91% rename from src/binstall/install.rs rename to crates/lib/src/ops/install.rs index 8e78b2e3..b060b2f4 100644 --- a/src/binstall/install.rs +++ b/crates/lib/src/ops/install.rs @@ -5,14 +5,23 @@ use compact_str::CompactString; use log::{debug, error, info}; use tokio::{process::Command, task::block_in_place}; -use super::{MetaData, Options, Resolution}; -use crate::{bins, fetchers::Fetcher, metafiles::binstall_v1::Source, BinstallError, *}; +use super::{resolve::Resolution, Options}; +use crate::{ + bins, + errors::BinstallError, + fetchers::Fetcher, + helpers::jobserver_client::LazyJobserverClient, + manifests::{ + cargo_toml_binstall::Meta, + crate_info::{CrateInfo, CrateSource}, + }, +}; pub async fn install( resolution: Resolution, opts: Arc, jobserver_client: LazyJobserverClient, -) -> Result, BinstallError> { +) -> Result, BinstallError> { match resolution { Resolution::AlreadyUpToDate => Ok(None), Resolution::Fetch { @@ -36,11 +45,11 @@ pub async fn install( install_from_package(fetcher, opts, bin_path, bin_files) .await .map(|option| { - option.map(|bins| MetaData { + option.map(|bins| CrateInfo { name, version_req, current_version, - source: Source::cratesio_registry(), + source: CrateSource::cratesio_registry(), target, bins, other: Default::default(), diff --git a/src/binstall/resolve.rs b/crates/lib/src/ops/resolve.rs similarity index 86% rename from src/binstall/resolve.rs rename to crates/lib/src/ops/resolve.rs index 97cc6cec..0dbb1e0b 100644 --- a/src/binstall/resolve.rs +++ b/crates/lib/src/ops/resolve.rs @@ -3,19 +3,29 @@ use std::{ sync::Arc, }; -use cargo_toml::{Package, Product}; +use cargo_toml::{Manifest, Package, Product}; use compact_str::{CompactString, ToCompactString}; use log::{debug, info, warn}; use reqwest::Client; use semver::{Version, VersionReq}; +use tokio::task::block_in_place; use super::Options; use crate::{ bins, + drivers::fetch_crate_cratesio, + errors::BinstallError, fetchers::{Data, Fetcher, GhCrateMeta, MultiFetcher, QuickInstall}, - BinstallError, *, + manifests::cargo_toml_binstall::{Meta, PkgMeta}, }; +mod crate_name; +#[doc(inline)] +pub use crate_name::CrateName; +mod version_ext; +#[doc(inline)] +pub use version_ext::VersionReqExt; + pub enum Resolution { Fetch { fetcher: Arc, @@ -265,3 +275,30 @@ fn collect_bin_files( Ok(bin_files) } + +/// Load binstall metadata from the crate `Cargo.toml` at the provided path +pub fn load_manifest_path>( + manifest_path: P, +) -> Result, BinstallError> { + block_in_place(|| { + let manifest_path = manifest_path.as_ref(); + let manifest_path = if manifest_path.is_dir() { + manifest_path.join("Cargo.toml") + } else if manifest_path.is_file() { + manifest_path.into() + } else { + return Err(BinstallError::CargoManifestPath); + }; + + debug!( + "Reading manifest at local path: {}", + manifest_path.display() + ); + + // Load and parse manifest (this checks file system for binary output names) + let manifest = Manifest::::from_path_with_metadata(manifest_path)?; + + // Return metadata + Ok(manifest) + }) +} diff --git a/src/helpers/crate_name.rs b/crates/lib/src/ops/resolve/crate_name.rs similarity index 88% rename from src/helpers/crate_name.rs rename to crates/lib/src/ops/resolve/crate_name.rs index 500f5614..398f9793 100644 --- a/src/helpers/crate_name.rs +++ b/crates/lib/src/ops/resolve/crate_name.rs @@ -4,7 +4,7 @@ use compact_str::CompactString; use itertools::Itertools; use semver::{Error, VersionReq}; -use super::parse_version; +use super::version_ext::VersionReqExt; #[derive(Debug, Clone, Eq, PartialEq)] pub struct CrateName { @@ -31,7 +31,7 @@ impl FromStr for CrateName { Ok(if let Some((name, version)) = s.split_once('@') { CrateName { name: name.into(), - version_req: Some(parse_version(version)?), + version_req: Some(VersionReq::parse_from_cli(version)?), } } else { CrateName { @@ -43,7 +43,8 @@ impl FromStr for CrateName { } impl CrateName { - pub fn dedup(mut crate_names: Vec) -> impl Iterator { + pub fn dedup(crate_names: &[Self]) -> impl Iterator { + let mut crate_names = crate_names.to_vec(); crate_names.sort_by(|x, y| x.name.cmp(&y.name)); crate_names.into_iter().coalesce(|previous, current| { if previous.name == current.name { @@ -61,7 +62,7 @@ mod tests { macro_rules! assert_dedup { ([ $( ( $input_name:expr, $input_version:expr ) ),* ], [ $( ( $output_name:expr, $output_version:expr ) ),* ]) => { - let input_crate_names = vec![$( CrateName { + let input_crate_names = [$( CrateName { name: $input_name.into(), version_req: Some($input_version.parse().unwrap()) }, )*]; @@ -71,7 +72,7 @@ mod tests { }, )*]; output_crate_names.sort_by(|x, y| x.name.cmp(&y.name)); - let crate_names: Vec<_> = CrateName::dedup(input_crate_names).collect(); + let crate_names: Vec<_> = CrateName::dedup(&input_crate_names).collect(); assert_eq!(crate_names, output_crate_names); }; } diff --git a/src/helpers/version.rs b/crates/lib/src/ops/resolve/version_ext.rs similarity index 81% rename from src/helpers/version.rs rename to crates/lib/src/ops/resolve/version_ext.rs index f994dc93..7c83e045 100644 --- a/src/helpers/version.rs +++ b/crates/lib/src/ops/resolve/version_ext.rs @@ -6,6 +6,14 @@ pub trait VersionReqExt { /// Return `true` if `self.matches(version)` returns `true` /// and the `version` is the latest one acceptable by `self`. fn is_latest_compatible(&self, version: &Version) -> bool; + + /// Parse from CLI option. + /// + /// Notably, a bare version is treated as if preceded by `=`, not by `^` as in Cargo.toml + /// dependencies. + fn parse_from_cli(str: &str) -> Result + where + Self: Sized; } impl VersionReqExt for VersionReq { @@ -42,6 +50,19 @@ impl VersionReqExt for VersionReq { true } + + fn parse_from_cli(version: &str) -> Result { + if version + .chars() + .next() + .map(|ch| ch.is_ascii_digit()) + .unwrap_or(false) + { + format_compact!("={version}").parse() + } else { + version.parse() + } + } } #[cfg(test)] diff --git a/src/target.rs b/crates/lib/src/targets.rs similarity index 98% rename from src/target.rs rename to crates/lib/src/targets.rs index c6392c23..dc905383 100644 --- a/src/target.rs +++ b/crates/lib/src/targets.rs @@ -1,9 +1,10 @@ -use std::io::{BufRead, Cursor}; -use std::process::Output; -use std::sync::Arc; +use std::{ + io::{BufRead, Cursor}, + process::Output, + sync::Arc, +}; -use tokio::process::Command; -use tokio::sync::OnceCell; +use tokio::{process::Command, sync::OnceCell}; /// Compiled target triple, used as default for binary fetching pub const TARGET: &str = env!("TARGET"); diff --git a/crates/lib/tests/parse-meta.Cargo.toml b/crates/lib/tests/parse-meta.Cargo.toml new file mode 100644 index 00000000..26d6e052 --- /dev/null +++ b/crates/lib/tests/parse-meta.Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "cargo-binstall-test" +repository = "https://github.com/cargo-bins/cargo-binstall" +version = "1.2.3" + +[[bin]] +name = "cargo-binstall" +path = "src/main.rs" +edition = "2021" + +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }.{ archive-format }" +bin-dir = "{ bin }{ binary-ext }" + +[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] +pkg-fmt = "zip" +[package.metadata.binstall.overrides.x86_64-apple-darwin] +pkg-fmt = "zip" diff --git a/crates/lib/tests/parse-meta.rs b/crates/lib/tests/parse-meta.rs new file mode 100644 index 00000000..7433974b --- /dev/null +++ b/crates/lib/tests/parse-meta.rs @@ -0,0 +1,31 @@ +use binstall::ops::resolve::load_manifest_path; +use cargo_toml::Product; + +#[test] +fn parse_meta() { + let _ = env_logger::builder().is_test(true).try_init(); + + let mut manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + manifest_dir.push_str("/tests/parse-meta.Cargo.toml"); + + let manifest = load_manifest_path(&manifest_dir).expect("Error parsing metadata"); + let package = manifest.package.unwrap(); + let meta = package.metadata.and_then(|m| m.binstall).unwrap(); + + assert_eq!(&package.name, "cargo-binstall-test"); + + assert_eq!( + &meta.pkg_url, + "{ repo }/releases/download/v{ version }/{ name }-{ target }.{ archive-format }" + ); + + assert_eq!( + manifest.bin.as_slice(), + &[Product { + name: Some("cargo-binstall".to_string()), + path: Some("src/main.rs".to_string()), + edition: Some(cargo_toml::Edition::E2021), + ..Default::default() + },], + ); +} diff --git a/crates/normalize-path/Cargo.toml b/crates/normalize-path/Cargo.toml new file mode 100644 index 00000000..d22ad55b --- /dev/null +++ b/crates/normalize-path/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "normalize-path" +description = "Like canonicalize, but without performing I/O" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/normalize-path" +version = "0.1.0" +rust-version = "1.61.0" +authors = ["Jiahao XU "] +edition = "2021" +license = "Apache-2.0 OR MIT" diff --git a/crates/normalize-path/LICENSE-APACHE b/crates/normalize-path/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/normalize-path/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/normalize-path/LICENSE-MIT b/crates/normalize-path/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/normalize-path/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/src/helpers/path_ext.rs b/crates/normalize-path/src/lib.rs similarity index 54% rename from src/helpers/path_ext.rs rename to crates/normalize-path/src/lib.rs index 78f26687..20e9ea68 100644 --- a/src/helpers/path_ext.rs +++ b/crates/normalize-path/src/lib.rs @@ -1,13 +1,36 @@ -//! Shamelessly adapted from: -//! https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61 +//! Normalizes paths similarly to canonicalize, but without performing I/O. +//! +//! This is like Python's `os.path.normpath`. +//! +//! Initially adapted from [Cargo's implementation][cargo-paths]. +//! +//! [cargo-paths]: https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61 +//! +//! # Example +//! +//! ``` +//! use normalize_path::NormalizePath; +//! use std::path::Path; +//! +//! assert_eq!( +//! Path::new("/A/foo/../B/./").normalize(), +//! Path::new("/A/B") +//! ); +//! ``` -use std::borrow::Cow; -use std::path::{Component, Path, PathBuf}; +use std::{ + borrow::Cow, + path::{Component, Path, PathBuf}, +}; -pub trait PathExt { - /// Similiar to `os.path.normpath`: It does not perform - /// any fs operation. - fn normalize_path(&self) -> Cow<'_, Path>; +/// Extension trait to add `normalize_path` to std's [`Path`]. +pub trait NormalizePath { + /// Normalize a path without performing I/O. + /// + /// All redundant separator and up-level references are collapsed. + /// + /// However, this does not resolve links. + fn normalize(&self) -> Cow<'_, Path>; } fn is_normalized(path: &Path) -> bool { @@ -23,8 +46,8 @@ fn is_normalized(path: &Path) -> bool { true } -impl PathExt for Path { - fn normalize_path(&self) -> Cow<'_, Path> { +impl NormalizePath for Path { + fn normalize(&self) -> Cow<'_, Path> { if is_normalized(self) { return Cow::Borrowed(self); } diff --git a/src/drivers.rs b/src/drivers.rs deleted file mode 100644 index 933a6d2f..00000000 --- a/src/drivers.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::path::{Path, PathBuf}; - -use crate::BinstallError; - -mod version; -use version::find_version; - -mod crates_io; -pub use crates_io::fetch_crate_cratesio; - -/// Fetch a crate by name and version from github -/// TODO: implement this -pub async fn fetch_crate_gh_releases( - _name: &str, - _version: Option<&str>, - _temp_dir: &Path, -) -> Result { - unimplemented!(); -} diff --git a/src/helpers.rs b/src/helpers.rs deleted file mode 100644 index 0742c1f2..00000000 --- a/src/helpers.rs +++ /dev/null @@ -1,363 +0,0 @@ -use std::env; -use std::fmt::Debug; -use std::fs; -use std::io; -use std::ops; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use bytes::Bytes; -use cargo_toml::Manifest; -use compact_str::format_compact; -use futures_util::stream::Stream; -use log::debug; -use once_cell::sync::{Lazy, OnceCell}; -use reqwest::{tls, Client, ClientBuilder, Method, Response}; -use serde::Serialize; -use tempfile::NamedTempFile; -use tinytemplate::TinyTemplate; -use tokio::task::block_in_place; -use url::Url; - -use crate::{BinstallError, Meta, PkgFmt, PkgFmtDecomposed, TarBasedFmt}; - -mod async_extracter; -pub use async_extracter::*; - -mod auto_abort_join_handle; -pub use auto_abort_join_handle::AutoAbortJoinHandle; - -mod ui_thread; -pub use ui_thread::UIThread; - -mod extracter; -mod stream_readable; - -mod jobserver_client; -pub use jobserver_client::*; - -mod path_ext; -pub use path_ext::*; - -mod tls_version; -pub use tls_version::TLSVersion; - -mod crate_name; -pub use crate_name::CrateName; - -mod flock; -pub use flock::FileLock; - -mod signal; -pub use signal::cancel_on_user_sig_term; - -mod version; -pub use version::VersionReqExt; - -pub fn cargo_home() -> Result<&'static Path, io::Error> { - static CARGO_HOME: OnceCell = OnceCell::new(); - - CARGO_HOME - .get_or_try_init(home::cargo_home) - .map(ops::Deref::deref) -} - -pub fn cratesio_url() -> &'static Url { - static CRATESIO: Lazy Url> = - Lazy::new(|| url::Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()); - - &*CRATESIO -} - -/// Returned file is readable and writable. -pub fn create_if_not_exist(path: impl AsRef) -> io::Result { - let path = path.as_ref(); - - let mut options = fs::File::options(); - options.read(true).write(true); - - options - .clone() - .create_new(true) - .open(path) - .or_else(|_| options.open(path)) -} - -pub async fn await_task(task: tokio::task::JoinHandle>) -> miette::Result { - match task.await { - Ok(res) => res, - Err(join_err) => Err(BinstallError::from(join_err).into()), - } -} - -pub fn parse_version(version: &str) -> Result { - // Treat 0.1.2 as =0.1.2 - if version - .chars() - .next() - .map(|ch| ch.is_ascii_digit()) - .unwrap_or(false) - { - format_compact!("={version}").parse() - } else { - version.parse() - } -} - -/// Load binstall metadata from the crate `Cargo.toml` at the provided path -pub fn load_manifest_path>( - manifest_path: P, -) -> Result, BinstallError> { - block_in_place(|| { - let manifest_path = manifest_path.as_ref(); - let manifest_path = if manifest_path.is_dir() { - manifest_path.join("Cargo.toml") - } else if manifest_path.is_file() { - manifest_path.into() - } else { - return Err(BinstallError::CargoManifestPath); - }; - - debug!( - "Reading manifest at local path: {}", - manifest_path.display() - ); - - // Load and parse manifest (this checks file system for binary output names) - let manifest = Manifest::::from_path_with_metadata(manifest_path)?; - - // Return metadata - Ok(manifest) - }) -} - -pub fn create_reqwest_client( - secure: bool, - min_tls: Option, -) -> Result { - const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); - - let mut builder = ClientBuilder::new().user_agent(USER_AGENT); - - if secure { - builder = builder - .https_only(true) - .min_tls_version(tls::Version::TLS_1_2); - } - - if let Some(ver) = min_tls { - builder = builder.min_tls_version(ver); - } - - Ok(builder.build()?) -} - -pub async fn remote_exists( - client: Client, - url: Url, - method: Method, -) -> Result { - let req = client - .request(method.clone(), url.clone()) - .send() - .await - .map_err(|err| BinstallError::Http { method, url, err })?; - Ok(req.status().is_success()) -} - -async fn create_request( - client: &Client, - url: Url, -) -> Result>, BinstallError> { - debug!("Downloading from: '{url}'"); - - client - .get(url.clone()) - .send() - .await - .and_then(|r| r.error_for_status()) - .map_err(|err| BinstallError::Http { - method: Method::GET, - url, - err, - }) - .map(Response::bytes_stream) -} - -/// Download a file from the provided URL and extract it to the provided path. -pub async fn download_and_extract>( - client: &Client, - url: &Url, - fmt: PkgFmt, - path: P, -) -> Result<(), BinstallError> { - let stream = create_request(client, url.clone()).await?; - - let path = path.as_ref(); - debug!("Downloading and extracting to: '{}'", path.display()); - - match fmt.decompose() { - PkgFmtDecomposed::Tar(fmt) => extract_tar_based_stream(stream, path, fmt).await?, - PkgFmtDecomposed::Bin => extract_bin(stream, path).await?, - PkgFmtDecomposed::Zip => extract_zip(stream, path).await?, - } - - debug!("Download OK, extracted to: '{}'", path.display()); - - Ok(()) -} - -/// Download a file from the provided URL and extract part of it to -/// the provided path. -/// -/// * `filter` - If Some, then it will pass the path of the file to it -/// and only extract ones which filter returns `true`. -pub async fn download_tar_based_and_visit( - client: &Client, - url: Url, - fmt: TarBasedFmt, - visitor: V, -) -> Result { - let stream = create_request(client, url).await?; - - debug!("Downloading and extracting then in-memory processing"); - - let ret = extract_tar_based_stream_and_visit(stream, fmt, visitor).await?; - - debug!("Download, extraction and in-memory procession OK"); - - Ok(ret) -} - -/// Fetch install path from environment -/// roughly follows -/// -/// Return (install_path, is_custom_install_path) -pub fn get_install_path>(install_path: Option

) -> (Option>, bool) { - // Command line override first first - if let Some(p) = install_path { - return (Some(Arc::from(p.as_ref())), true); - } - - // Environmental variables - if let Ok(p) = std::env::var("CARGO_INSTALL_ROOT") { - debug!("using CARGO_INSTALL_ROOT ({p})"); - let b = PathBuf::from(p); - return (Some(Arc::from(b.join("bin"))), true); - } - - if let Ok(p) = cargo_home() { - debug!("using ({}) as cargo home", p.display()); - return (Some(p.join("bin").into()), false); - } - - // Local executable dir if no cargo is found - let dir = dirs::executable_dir(); - - if let Some(d) = &dir { - debug!("Fallback to {}", d.display()); - } - - (dir.map(Arc::from), true) -} - -/// Atomically install a file. -/// -/// This is a blocking function, must be called in `block_in_place` mode. -pub fn atomic_install(src: &Path, dst: &Path) -> io::Result<()> { - debug!( - "Attempting to atomically rename from '{}' to '{}'", - src.display(), - dst.display() - ); - - if let Err(err) = fs::rename(src, dst) { - debug!("Attempting at atomically failed: {err:#?}, fallback to creating tempfile."); - // src and dst is not on the same filesystem/mountpoint. - // Fallback to creating NamedTempFile on the parent dir of - // dst. - - let mut src_file = fs::File::open(src)?; - - let parent = dst.parent().unwrap(); - debug!("Creating named tempfile at '{}'", parent.display()); - let mut tempfile = NamedTempFile::new_in(parent)?; - - debug!( - "Copying from '{}' to '{}'", - src.display(), - tempfile.path().display() - ); - io::copy(&mut src_file, tempfile.as_file_mut())?; - - debug!("Retrieving permissions of '{}'", src.display()); - let permissions = src_file.metadata()?.permissions(); - - debug!( - "Setting permissions of '{}' to '{permissions:#?}'", - tempfile.path().display() - ); - tempfile.as_file().set_permissions(permissions)?; - - debug!( - "Persisting '{}' to '{}'", - tempfile.path().display(), - dst.display() - ); - tempfile.persist(dst).map_err(io::Error::from)?; - } else { - debug!("Attempting at atomically succeeded."); - } - - Ok(()) -} - -fn symlink_file, Q: AsRef>(original: P, link: Q) -> io::Result<()> { - #[cfg(target_family = "unix")] - let f = std::os::unix::fs::symlink; - #[cfg(target_family = "windows")] - let f = std::os::windows::fs::symlink_file; - - f(original, link) -} - -/// Atomically install symlink "link" to a file "dst". -/// -/// This is a blocking function, must be called in `block_in_place` mode. -pub fn atomic_symlink_file(dest: &Path, link: &Path) -> io::Result<()> { - let parent = link.parent().unwrap(); - - debug!("Creating tempPath at '{}'", parent.display()); - let temp_path = NamedTempFile::new_in(parent)?.into_temp_path(); - fs::remove_file(&temp_path)?; - - debug!( - "Creating symlink '{}' to file '{}'", - temp_path.display(), - dest.display() - ); - symlink_file(dest, &temp_path)?; - - debug!( - "Persisting '{}' to '{}'", - temp_path.display(), - link.display() - ); - temp_path.persist(link).map_err(io::Error::from) -} - -pub trait Template: Serialize { - fn render(&self, template: &str) -> Result - where - Self: Sized, - { - // Create template instance - let mut tt = TinyTemplate::new(); - - // Add template to instance - tt.add_template("path", template)?; - - // Render output - Ok(tt.render("path", self)?) - } -} diff --git a/src/helpers/flock.rs b/src/helpers/flock.rs deleted file mode 100644 index f8b4514a..00000000 --- a/src/helpers/flock.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::fs::File; -use std::io; -use std::ops; - -use fs4::FileExt; - -#[derive(Debug)] -pub struct FileLock(File); - -impl FileLock { - /// NOTE that this function blocks, so it cannot - /// be called in async context. - pub fn new_exclusive(file: File) -> io::Result { - file.lock_exclusive()?; - - Ok(Self(file)) - } - - /// NOTE that this function blocks, so it cannot - /// be called in async context. - pub fn new_shared(file: File) -> io::Result { - file.lock_shared()?; - - Ok(Self(file)) - } -} - -impl Drop for FileLock { - fn drop(&mut self) { - let _ = self.unlock(); - } -} - -impl ops::Deref for FileLock { - type Target = File; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} -impl ops::DerefMut for FileLock { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl io::Write for FileLock { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.write(buf) - } - fn flush(&mut self) -> io::Result<()> { - self.0.flush() - } - - fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result { - self.0.write_vectored(bufs) - } -} - -impl io::Read for FileLock { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.0.read(buf) - } - - fn read_vectored(&mut self, bufs: &mut [io::IoSliceMut<'_>]) -> io::Result { - self.0.read_vectored(bufs) - } -} - -impl io::Seek for FileLock { - fn seek(&mut self, pos: io::SeekFrom) -> io::Result { - self.0.seek(pos) - } - - fn rewind(&mut self) -> io::Result<()> { - self.0.rewind() - } - fn stream_position(&mut self) -> io::Result { - self.0.stream_position() - } -} diff --git a/src/helpers/tls_version.rs b/src/helpers/tls_version.rs deleted file mode 100644 index 35f86123..00000000 --- a/src/helpers/tls_version.rs +++ /dev/null @@ -1,19 +0,0 @@ -use clap::ArgEnum; -use reqwest::tls::Version; - -#[derive(Debug, Copy, Clone, ArgEnum)] -pub enum TLSVersion { - #[clap(name = "1.2")] - Tls1_2, - #[clap(name = "1.3")] - Tls1_3, -} - -impl From for Version { - fn from(ver: TLSVersion) -> Self { - match ver { - TLSVersion::Tls1_2 => Version::TLS_1_2, - TLSVersion::Tls1_3 => Version::TLS_1_3, - } - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 77b51ddd..00000000 --- a/src/main.rs +++ /dev/null @@ -1,517 +0,0 @@ -use std::{ - ffi::OsString, - fs, - mem::take, - path::{Path, PathBuf}, - process::{ExitCode, Termination}, - sync::Arc, - time::{Duration, Instant}, -}; - -use clap::{builder::PossibleValue, AppSettings, Parser}; -use log::{debug, error, info, warn, LevelFilter}; -use miette::{miette, Result, WrapErr}; -use semver::VersionReq; -use simplelog::{ColorChoice, ConfigBuilder, TermLogger, TerminalMode}; -use tokio::{runtime::Runtime, task::block_in_place}; - -use cargo_binstall::{binstall, *}; - -#[cfg(feature = "mimalloc")] -#[global_allocator] -static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; - -#[derive(Debug, Parser)] -#[clap(version, about = "Install a Rust binary... from binaries!", setting = AppSettings::ArgRequiredElseHelp)] -struct Options { - /// Packages to install. - /// - /// Syntax: crate[@version] - /// - /// Each value is either a crate name alone, or a crate name followed by @ and the version to - /// install. The version syntax is as with the --version option. - /// - /// When multiple names are provided, the --version option and any override options are - /// unavailable due to ambiguity. - /// - /// If duplicate names are provided, the last one (and their version requirement) - /// is kept. - #[clap( - help_heading = "Package selection", - value_name = "crate[@version]", - required_unless_present_any = ["version", "help"], - )] - crate_names: Vec, - - /// Package version to install. - /// - /// Takes either an exact semver version or a semver version requirement expression, which will - /// be resolved to the highest matching version available. - /// - /// Cannot be used when multiple packages are installed at once, use the attached version - /// syntax in that case. - #[clap(help_heading = "Package selection", long = "version", parse(try_from_str = parse_version))] - version_req: Option, - - /// Override binary target set. - /// - /// Binstall is able to look for binaries for several targets, installing the first one it finds - /// in the order the targets were given. For example, on a 64-bit glibc Linux distribution, the - /// default is to look first for a `x86_64-unknown-linux-gnu` binary, then for a - /// `x86_64-unknown-linux-musl` binary. However, on a musl system, the gnu version will not be - /// considered. - /// - /// This option takes a comma-separated list of target triples, which will be tried in order. - /// They override the default list, which is detected automatically from the current platform. - /// - /// If falling back to installing from source, the first target will be used. - #[clap( - help_heading = "Package selection", - alias = "target", - long, - value_name = "TRIPLE" - )] - targets: Option, - - /// Override Cargo.toml package manifest path. - /// - /// This skips searching crates.io for a manifest and uses the specified path directly, useful - /// for debugging and when adding Binstall support. This may be either the path to the folder - /// containing a Cargo.toml file, or the Cargo.toml file itself. - #[clap(help_heading = "Overrides", long)] - manifest_path: Option, - - /// Override Cargo.toml package manifest bin-dir. - #[clap(help_heading = "Overrides", long)] - bin_dir: Option, - - /// Override Cargo.toml package manifest pkg-fmt. - #[clap(help_heading = "Overrides", long)] - pkg_fmt: Option, - - /// Override Cargo.toml package manifest pkg-url. - #[clap(help_heading = "Overrides", long)] - pkg_url: Option, - - /// Disable symlinking / versioned updates. - /// - /// By default, Binstall will install a binary named `-` in the install path, and - /// either symlink or copy it to (depending on platform) the plain binary name. This makes it - /// possible to have multiple versions of the same binary, for example for testing or rollback. - /// - /// Pass this flag to disable this behavior. - #[clap(help_heading = "Options", long)] - no_symlinks: bool, - - /// Dry run, fetch and show changes without installing binaries. - #[clap(help_heading = "Options", long)] - dry_run: bool, - - /// Disable interactive mode / confirmation prompts. - #[clap(help_heading = "Options", long)] - no_confirm: bool, - - /// Do not cleanup temporary files. - #[clap(help_heading = "Options", long)] - no_cleanup: bool, - - /// Install binaries in a custom location. - /// - /// By default, binaries are installed to the global location `$CARGO_HOME/bin`, and global - /// metadata files are updated with the package information. Specifying another path here - /// switches over to a "local" install, where binaries are installed at the path given, and the - /// global metadata files are not updated. - #[clap(help_heading = "Options", long)] - install_path: Option, - - /// Enforce downloads over secure transports only. - /// - /// Insecure HTTP downloads will be removed completely in the future; in the meantime this - /// option forces a fail when the remote endpoint uses plaintext HTTP or insecure TLS suites. - /// - /// Without this option, plain HTTP will warn. - /// - /// Implies `--min-tls-version=1.2`. - #[clap(help_heading = "Options", long)] - secure: bool, - - /// Force a crate to be installed even if it is already installed. - #[clap(help_heading = "Options", long)] - force: bool, - - /// Require a minimum TLS version from remote endpoints. - /// - /// The default is not to require any minimum TLS version, and use the negotiated highest - /// version available to both this client and the remote server. - #[clap(help_heading = "Options", long, arg_enum, value_name = "VERSION")] - min_tls_version: Option, - - /// Print help information - #[clap(help_heading = "Meta", short, long)] - help: bool, - - /// Print version information - #[clap(help_heading = "Meta", short = 'V')] - version: bool, - - /// Utility log level - /// - /// Set to `trace` to print very low priority, often extremely - /// verbose information. - /// - /// Set to `debug` when submitting a bug report. - /// - /// Set to `info` to only print useful information. - /// - /// Set to `warn` to only print on hazardous situations. - /// - /// Set to `error` to only print serious errors. - /// - /// Set to `off` to disable logging completely, this will also - /// disable output from `cargo-install`. - #[clap( - help_heading = "Meta", - long, - default_value = "info", - value_name = "LEVEL", - possible_values = [ - PossibleValue::new("trace").help( - "Set to `trace` to print very low priority, often extremely verbose information." - ), - PossibleValue::new("debug").help("Set to debug when submitting a bug report."), - PossibleValue::new("info").help("Set to info to only print useful information."), - PossibleValue::new("warn").help("Set to warn to only print on hazardous situations."), - PossibleValue::new("error").help("Set to error to only print serious errors."), - PossibleValue::new("off").help( - "Set to off to disable logging completely, this will also disable output from `cargo-install`." - ), - ] - )] - log_level: LevelFilter, - - /// Equivalent to setting `log_level` to `off`. - /// - /// This would override the `log_level`. - #[clap(help_heading = "Meta", short, long)] - quiet: bool, -} - -enum MainExit { - Success(Duration), - Error(BinstallError), - Report(miette::Report), -} - -impl Termination for MainExit { - fn report(self) -> ExitCode { - match self { - Self::Success(spent) => { - info!("Done in {spent:?}"); - ExitCode::SUCCESS - } - Self::Error(err) => err.report(), - Self::Report(err) => { - error!("Fatal error:"); - eprintln!("{err:?}"); - ExitCode::from(16) - } - } - } -} - -fn main() -> MainExit { - // Create jobserver client - let jobserver_client = LazyJobserverClient::new(); - - let start = Instant::now(); - - let rt = Runtime::new().unwrap(); - let handle = AutoAbortJoinHandle::new(rt.spawn(entry(jobserver_client))); - let result = rt.block_on(cancel_on_user_sig_term(handle)); - drop(rt); - - let done = start.elapsed(); - debug!("run time: {done:?}"); - - result.map_or_else(MainExit::Error, |res| { - res.map(|()| MainExit::Success(done)).unwrap_or_else(|err| { - err.downcast::() - .map(MainExit::Error) - .unwrap_or_else(MainExit::Report) - }) - }) -} - -async fn entry(jobserver_client: LazyJobserverClient) -> Result<()> { - // Filter extraneous arg when invoked by cargo - // `cargo run -- --help` gives ["target/debug/cargo-binstall", "--help"] - // `cargo binstall --help` gives ["/home/ryan/.cargo/bin/cargo-binstall", "binstall", "--help"] - let mut args: Vec = std::env::args_os().collect(); - let args = if args.len() > 1 && args[1] == "binstall" { - // Equivalent to - // - // args.remove(1); - // - // But is O(1) - args.swap(0, 1); - let mut args = args.into_iter(); - drop(args.next().unwrap()); - - args - } else { - args.into_iter() - }; - - // Load options - let mut opts = Options::parse_from(args); - if opts.quiet { - opts.log_level = LevelFilter::Off; - } - - let crate_names = take(&mut opts.crate_names); - if crate_names.len() > 1 { - let option = if opts.version_req.is_some() { - "version" - } else if opts.manifest_path.is_some() { - "manifest-path" - } else if opts.bin_dir.is_some() { - "bin-dir" - } else if opts.pkg_fmt.is_some() { - "pkg-fmt" - } else if opts.pkg_url.is_some() { - "pkg-url" - } else { - "" - }; - - if !option.is_empty() { - return Err(BinstallError::OverrideOptionUsedWithMultiInstall { option }.into()); - } - } - - let cli_overrides = PkgOverride { - pkg_url: opts.pkg_url.take(), - pkg_fmt: opts.pkg_fmt.take(), - bin_dir: opts.bin_dir.take(), - }; - - // Launch target detection - let desired_targets = get_desired_targets(&opts.targets); - - // Initialize reqwest client - let client = create_reqwest_client(opts.secure, opts.min_tls_version.map(|v| v.into()))?; - - // Build crates.io api client - let crates_io_api_client = crates_io_api::AsyncClient::new( - "cargo-binstall (https://github.com/ryankurte/cargo-binstall)", - Duration::from_millis(100), - ) - .expect("bug: invalid user agent"); - - // Setup logging - let mut log_config = ConfigBuilder::new(); - log_config.add_filter_ignore("hyper".to_string()); - log_config.add_filter_ignore("reqwest".to_string()); - log_config.add_filter_ignore("rustls".to_string()); - log_config.set_location_level(LevelFilter::Off); - TermLogger::init( - opts.log_level, - log_config.build(), - TerminalMode::Mixed, - ColorChoice::Auto, - ) - .unwrap(); - - // Initialize UI thread - let mut uithread = UIThread::new(!opts.no_confirm); - - let (install_path, metadata, temp_dir) = block_in_place(|| -> Result<_> { - // Compute install directory - let (install_path, custom_install_path) = get_install_path(opts.install_path.as_deref()); - let install_path = install_path.ok_or_else(|| { - error!("No viable install path found of specified, try `--install-path`"); - miette!("No install path found or specified") - })?; - fs::create_dir_all(&install_path).map_err(BinstallError::Io)?; - debug!("Using install path: {}", install_path.display()); - - // Load metadata - let metadata = if !custom_install_path { - debug!("Reading binstall/crates-v1.json"); - Some(metafiles::binstall_v1::Records::load()?) - } else { - None - }; - - // Create a temporary directory for downloads etc. - // - // Put all binaries to a temporary directory under `dst` first, catching - // some failure modes (e.g., out of space) before touching the existing - // binaries. This directory will get cleaned up via RAII. - let temp_dir = tempfile::Builder::new() - .prefix("cargo-binstall") - .tempdir_in(&install_path) - .map_err(BinstallError::from) - .wrap_err("Creating a temporary directory failed.")?; - - Ok((install_path, metadata, temp_dir)) - })?; - - // Remove installed crates - let crate_names = CrateName::dedup(crate_names) - .filter_map(|crate_name| { - match ( - opts.force, - metadata.as_ref().and_then(|records| records.get(&crate_name.name)), - &crate_name.version_req, - ) { - (false, Some(metadata), Some(version_req)) - if version_req.is_latest_compatible(&metadata.current_version) => - { - debug!("Bailing out early because we can assume wanted is already installed from metafile"); - info!( - "{} v{} is already installed, use --force to override", - crate_name.name, metadata.current_version - ); - None - } - - // we have to assume that the version req could be *, - // and therefore a remote upgraded version could exist - (false, Some(metadata), _) => { - Some((crate_name, Some(metadata.current_version.clone()))) - } - - _ => Some((crate_name, None)), - } - }) - .collect::>(); - - if crate_names.is_empty() { - debug!("Nothing to do"); - return Ok(()); - } - - let temp_dir_path: Arc = Arc::from(temp_dir.path()); - - // Create binstall_opts - let binstall_opts = Arc::new(binstall::Options { - no_symlinks: opts.no_symlinks, - dry_run: opts.dry_run, - force: opts.force, - version_req: opts.version_req.take(), - manifest_path: opts.manifest_path.take(), - cli_overrides, - desired_targets, - quiet: opts.log_level == LevelFilter::Off, - }); - - let tasks: Vec<_> = if !opts.dry_run && !opts.no_confirm { - // Resolve crates - let tasks: Vec<_> = crate_names - .into_iter() - .map(|(crate_name, current_version)| { - AutoAbortJoinHandle::spawn(binstall::resolve( - binstall_opts.clone(), - crate_name, - current_version, - temp_dir_path.clone(), - install_path.clone(), - client.clone(), - crates_io_api_client.clone(), - )) - }) - .collect(); - - // Confirm - let mut resolutions = Vec::with_capacity(tasks.len()); - for task in tasks { - match task.await?? { - binstall::Resolution::AlreadyUpToDate => {} - res => resolutions.push(res), - } - } - - if resolutions.is_empty() { - debug!("Nothing to do"); - return Ok(()); - } - - uithread.confirm().await?; - - // Install - resolutions - .into_iter() - .map(|resolution| { - AutoAbortJoinHandle::spawn(binstall::install( - resolution, - binstall_opts.clone(), - jobserver_client.clone(), - )) - }) - .collect() - } else { - // Resolve crates and install without confirmation - crate_names - .into_iter() - .map(|(crate_name, current_version)| { - let opts = binstall_opts.clone(); - let temp_dir_path = temp_dir_path.clone(); - let jobserver_client = jobserver_client.clone(); - let client = client.clone(); - let crates_io_api_client = crates_io_api_client.clone(); - let install_path = install_path.clone(); - - AutoAbortJoinHandle::spawn(async move { - let resolution = binstall::resolve( - opts.clone(), - crate_name, - current_version, - temp_dir_path, - install_path, - client, - crates_io_api_client, - ) - .await?; - - binstall::install(resolution, opts, jobserver_client).await - }) - }) - .collect() - }; - - let mut metadata_vec = Vec::with_capacity(tasks.len()); - for task in tasks { - if let Some(metadata) = task.await?? { - metadata_vec.push(metadata); - } - } - - block_in_place(|| { - if let Some(mut records) = metadata { - // If using standardised install path, - // then create_dir_all(&install_path) would also - // create .cargo. - - debug!("Writing .crates.toml"); - metafiles::v1::CratesToml::append(metadata_vec.iter())?; - - debug!("Writing binstall/crates-v1.json"); - for metadata in metadata_vec { - records.replace(metadata); - } - records.overwrite()?; - } - - if opts.no_cleanup { - // Consume temp_dir without removing it from fs. - temp_dir.into_path(); - } else { - temp_dir.close().unwrap_or_else(|err| { - warn!("Failed to clean up some resources: {err}"); - }); - } - - Ok(()) - }) -} diff --git a/src/metafiles.rs b/src/metafiles.rs deleted file mode 100644 index b8a2b4ee..00000000 --- a/src/metafiles.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod cvs; -pub use cvs::*; - -pub mod v1; - -pub mod binstall_v1;