From f4050abec8fd06ffc13f76bcfc46b96741ed9e2b Mon Sep 17 00:00:00 2001 From: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Thu, 16 Jan 2025 19:11:16 +0100 Subject: [PATCH] feat: add engines Squashed commit of the following: commit 57670429642e1cc9ef0e8dd79c6627f5b6e05fe9 Author: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Thu Jan 16 18:28:52 2025 +0100 fix(engines): correct engine detection on unix The `current_exe` function doesn't return the symlinked path on Unix, so the engine detection was failing there. This commit fixes that by using the 0th argument of the program to get the path of the executable on Unix. commit b51c9d95718ad840e86c31f6de328bdb4e8d9e98 Author: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Wed Jan 15 22:43:50 2025 +0100 refactor: print deprecated warning on CLI side Prints the deprecated warning on the CLI side which means it'll have a more consistent look with the rest of the CLI output. commit 5ace844035e842dfd6748331cd7a69529a6025f2 Author: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Wed Jan 15 22:21:36 2025 +0100 feat: add alias validation Ensures aliases don't contain characters which could cause issues. They are now also forbidden from being the same as an engine name to avoid issues. commit a33302aff9eba46eb2414d0b885ae6475dd39720 Author: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Wed Jan 15 21:23:40 2025 +0100 refactor: apply clippy lints commit 2d534a534d937efc4f51b855aa0ff2f7904bf52c Author: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Wed Jan 15 21:22:14 2025 +0100 feat(engines): print incompatibility warning for dependencies Adds a warning message when a dependency depends on an incompatible engine. commit 4946a19f8b83664a9c3f69870918faeae8b434fd Author: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Wed Jan 15 18:33:38 2025 +0100 feat(engines): create linkers at install time Additionally fixes engines being executed as scripts, and fixes downloading pesde from GitHub. commit e3177eeb7569ad5790fc93c032c0093013ee0f24 Author: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Tue Jan 14 14:33:26 2025 +0100 fix(engines): store & link engines correctly Fixes issues with how engines were stored which resulted in errors. Also makes outdated linkers get updated. commit 037ead66bb4011895243cf67c8555b52762285c0 Author: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Mon Jan 13 12:26:19 2025 +0100 docs: remove prerequisites commit ddb496ff7d95fc541b2285fcac8c265e8d3dd31b Author: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Mon Jan 13 12:25:53 2025 +0100 ci: remove tar builds commit e9f0c25554b8c2ac0e0a4513a5da250ce0943084 Author: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Mon Jan 13 12:25:11 2025 +0100 chore(docs): update astro and starlight commit fc349e6f21ee803675857ec683c6ceb963fe13dc Author: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Sun Jan 12 23:12:27 2025 +0100 feat: add engines Adds the initial implementation of the engines feature. Not tested yet. Requires documentation and more work for non-pesde engines to be usable. --- .github/workflows/release.yaml | 8 - Cargo.toml | 7 +- docs/bun.lockb | Bin 272238 -> 267035 bytes docs/package.json | 22 +- docs/src/content/config.ts | 3 +- docs/src/content/docs/installation.mdx | 14 +- registry/src/endpoints/publish_version.rs | 1 + registry/src/endpoints/search.rs | 6 +- registry/src/package.rs | 4 +- src/cli/commands/add.rs | 41 +-- src/cli/commands/init.rs | 2 +- src/cli/commands/self_install.rs | 5 +- src/cli/commands/self_upgrade.rs | 27 +- src/cli/install.rs | 190 +++++++++-- src/cli/reporters.rs | 28 +- src/cli/version.rs | 373 ++++++++++------------ src/download.rs | 8 +- src/download_and_link.rs | 4 +- src/engine/mod.rs | 63 ++++ src/engine/source/archive.rs | 320 +++++++++++++++++++ src/engine/source/github/engine_ref.rs | 19 ++ src/engine/source/github/mod.rs | 146 +++++++++ src/engine/source/mod.rs | 143 +++++++++ src/engine/source/traits.rs | 51 +++ src/graph.rs | 6 +- src/lib.rs | 33 +- src/linking/mod.rs | 7 +- src/lockfile.rs | 6 +- src/main.rs | 107 ++++--- src/manifest/mod.rs | 105 +++++- src/manifest/overrides.rs | 23 +- src/patches.rs | 4 +- src/reporters.rs | 54 +++- src/resolver.rs | 18 +- src/source/git/pkg_ref.rs | 6 +- src/source/path/pkg_ref.rs | 6 +- src/source/pesde/mod.rs | 78 ++--- src/source/pesde/pkg_ref.rs | 6 +- src/source/refs.rs | 4 +- src/source/traits.rs | 4 +- src/source/wally/manifest.rs | 18 +- src/source/wally/mod.rs | 26 +- src/source/wally/pkg_ref.rs | 6 +- src/source/workspace/pkg_ref.rs | 6 +- src/util.rs | 7 + 45 files changed, 1497 insertions(+), 518 deletions(-) create mode 100644 src/engine/mod.rs create mode 100644 src/engine/source/archive.rs create mode 100644 src/engine/source/github/engine_ref.rs create mode 100644 src/engine/source/github/mod.rs create mode 100644 src/engine/source/mod.rs create mode 100644 src/engine/source/traits.rs diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7733e58..f30c058 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -96,11 +96,9 @@ jobs: if [ ${{ matrix.host }} = "windows" ]; then mv target/${{ matrix.target }}/release/${{ env.BIN_NAME }}.exe ${{ env.BIN_NAME }}.exe 7z a ${{ env.ARCHIVE_NAME }}.zip ${{ env.BIN_NAME }}.exe - tar -czf ${{ env.ARCHIVE_NAME }}.tar.gz ${{ env.BIN_NAME }}.exe else mv target/${{ matrix.target }}/release/${{ env.BIN_NAME }} ${{ env.BIN_NAME }} zip -r ${{ env.ARCHIVE_NAME }}.zip ${{ env.BIN_NAME }} - tar -czf ${{ env.ARCHIVE_NAME }}.tar.gz ${{ env.BIN_NAME }} fi - name: Upload zip artifact @@ -109,12 +107,6 @@ jobs: name: ${{ env.ARCHIVE_NAME }}.zip path: ${{ env.ARCHIVE_NAME }}.zip - - name: Upload tar.gz artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ env.ARCHIVE_NAME }}.tar.gz - path: ${{ env.ARCHIVE_NAME }}.tar.gz - publish: name: Publish to crates.io runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 5f5c616..c27420b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ bin = [ "dep:clap", "dep:dirs", "dep:tracing-subscriber", - "reqwest/json", "dep:indicatif", "dep:inquire", "dep:toml_edit", @@ -30,7 +29,7 @@ bin = [ "tokio/rt-multi-thread", "tokio/macros", ] -wally-compat = ["dep:async_zip", "dep:serde_json"] +wally-compat = ["dep:serde_json"] patches = ["dep:git2"] version-management = ["bin"] schema = ["dep:schemars"] @@ -49,7 +48,7 @@ toml = "0.8.19" serde_with = "3.11.0" gix = { version = "0.68.0", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls", "revparse-regex", "credentials", "parallel"] } semver = { version = "1.0.24", features = ["serde"] } -reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls", "stream"] } +reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls", "stream", "json"] } tokio-tar = "0.3.1" async-compression = { version = "0.4.18", features = ["tokio", "gzip"] } pathdiff = "0.2.3" @@ -68,11 +67,11 @@ tempfile = "3.14.0" wax = { version = "0.6.0", default-features = false } fs-err = { version = "3.0.0", features = ["tokio"] } urlencoding = "2.1.3" +async_zip = { version = "0.0.17", features = ["tokio", "deflate", "deflate64", "tokio-fs"] } # TODO: remove this when gitoxide adds support for: committing, pushing, adding git2 = { version = "0.19.0", optional = true } -async_zip = { version = "0.0.17", features = ["tokio", "deflate", "deflate64", "tokio-fs"], optional = true } serde_json = { version = "1.0.133", optional = true } schemars = { git = "https://github.com/daimond113/schemars", rev = "bc7c7d6", features = ["semver1", "url2"], optional = true } diff --git a/docs/bun.lockb b/docs/bun.lockb index 655bdf5c63c56a59621cf1c5bd5f97cf7b81df78..7b2d78d09068625951d764d9058dea13cf3bce6e 100755 GIT binary patch delta 68032 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 H`NRkSearim delta 72514 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

MovJrH}axOd-l+-0xs+&Ap$0&;1V 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 N4wugn$WInQ?O)_@Q#=3w diff --git a/docs/package.json b/docs/package.json index d20f74e..ac7ee54 100644 --- a/docs/package.json +++ b/docs/package.json @@ -10,20 +10,20 @@ "astro": "astro" }, "dependencies": { - "@astrojs/check": "^0.9.3", - "@astrojs/starlight": "^0.28.2", - "@astrojs/starlight-tailwind": "^2.0.3", - "@astrojs/tailwind": "^5.1.1", - "@fontsource-variable/nunito-sans": "^5.1.0", - "@shikijs/rehype": "^1.21.0", - "astro": "^4.15.9", + "@astrojs/check": "0.9.4", + "@astrojs/starlight": "0.30.6", + "@astrojs/starlight-tailwind": "3.0.0", + "@astrojs/tailwind": "5.1.4", + "@fontsource-variable/nunito-sans": "^5.1.1", + "@shikijs/rehype": "^1.26.2", + "astro": "5.1.5", "sharp": "^0.33.5", - "shiki": "^1.21.0", - "tailwindcss": "^3.4.13", - "typescript": "^5.6.2" + "shiki": "^1.26.2", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3" }, "devDependencies": { "prettier-plugin-astro": "^0.14.1", - "prettier-plugin-tailwindcss": "^0.6.8" + "prettier-plugin-tailwindcss": "^0.6.9" } } \ No newline at end of file diff --git a/docs/src/content/config.ts b/docs/src/content/config.ts index 31ba171..4ed5caa 100644 --- a/docs/src/content/config.ts +++ b/docs/src/content/config.ts @@ -1,6 +1,7 @@ import { defineCollection } from "astro:content" +import { docsLoader } from "@astrojs/starlight/loaders" import { docsSchema } from "@astrojs/starlight/schema" export const collections = { - docs: defineCollection({ schema: docsSchema() }), + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), } diff --git a/docs/src/content/docs/installation.mdx b/docs/src/content/docs/installation.mdx index 1ed7c1e..7b08ac8 100644 --- a/docs/src/content/docs/installation.mdx +++ b/docs/src/content/docs/installation.mdx @@ -5,22 +5,11 @@ description: Install pesde import { Aside, Steps, TabItem, Tabs } from "@astrojs/starlight/components" -## Prerequisites - -pesde requires [Lune](https://lune-org.github.io/docs) to be installed on your -system in order to function properly. - -You can follow the installation instructions in the -[Lune documentation](https://lune-org.github.io/docs/getting-started/1-installation). - -## Installing pesde - 1. Go to the [GitHub releases page](https://github.com/pesde-pkg/pesde/releases/latest). -2. Download the corresponding archive for your operating system. You can choose - whether to use the `.zip` or `.tar.gz` files. +2. Download the corresponding archive for your operating system. 3. Extract the downloaded archive to a folder on your computer. @@ -76,6 +65,7 @@ You can follow the installation instructions in the +
5. Verify that pesde is installed by running the following command: diff --git a/registry/src/endpoints/publish_version.rs b/registry/src/endpoints/publish_version.rs index 0d89b9e..766f1ef 100644 --- a/registry/src/endpoints/publish_version.rs +++ b/registry/src/endpoints/publish_version.rs @@ -368,6 +368,7 @@ pub async fn publish_package( let new_entry = IndexFileEntry { target: manifest.target.clone(), published_at: chrono::Utc::now(), + engines: manifest.engines.clone(), description: manifest.description.clone(), license: manifest.license.clone(), authors: manifest.authors.clone(), diff --git a/registry/src/endpoints/search.rs b/registry/src/endpoints/search.rs index 3f23049..9efcb5c 100644 --- a/registry/src/endpoints/search.rs +++ b/registry/src/endpoints/search.rs @@ -50,8 +50,10 @@ pub async fn search_packages( let source = Arc::new(app_state.source.clone().read_owned().await); - let mut results = Vec::with_capacity(top_docs.len()); - results.extend((0..top_docs.len()).map(|_| None::)); + let mut results = top_docs + .iter() + .map(|_| None::) + .collect::>(); let mut tasks = top_docs .into_iter() diff --git a/registry/src/package.rs b/registry/src/package.rs index b3cf068..e848e29 100644 --- a/registry/src/package.rs +++ b/registry/src/package.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use pesde::{ manifest::{ target::{Target, TargetKind}, - DependencyType, + Alias, DependencyType, }, names::PackageName, source::{ @@ -125,7 +125,7 @@ pub struct PackageResponseInner { #[serde(skip_serializing_if = "BTreeSet::is_empty")] docs: BTreeSet, #[serde(skip_serializing_if = "BTreeMap::is_empty")] - dependencies: BTreeMap, + dependencies: BTreeMap, } impl PackageResponseInner { diff --git a/src/cli/commands/add.rs b/src/cli/commands/add.rs index aa7b759..0caded4 100644 --- a/src/cli/commands/add.rs +++ b/src/cli/commands/add.rs @@ -7,7 +7,7 @@ use semver::VersionReq; use crate::cli::{config::read_config, AnyPackageIdentifier, VersionedPackageName}; use pesde::{ - manifest::target::TargetKind, + manifest::{target::TargetKind, Alias}, names::PackageNames, source::{ git::{specifier::GitDependencySpecifier, GitPackageSource}, @@ -37,7 +37,7 @@ pub struct AddCommand { /// The alias to use for the package #[arg(short, long)] - alias: Option, + alias: Option, /// Whether to add the package as a peer dependency #[arg(short, long)] @@ -180,24 +180,29 @@ impl AddCommand { "dependencies" }; - let alias = self.alias.unwrap_or_else(|| match &self.name { - AnyPackageIdentifier::PackageName(versioned) => versioned.0.name().to_string(), - AnyPackageIdentifier::Url((url, _)) => url - .path - .to_string() - .split('/') - .last() - .map(|s| s.to_string()) - .unwrap_or(url.path.to_string()), - AnyPackageIdentifier::Workspace(versioned) => versioned.0.name().to_string(), - AnyPackageIdentifier::Path(path) => path - .file_name() - .map(|s| s.to_string_lossy().to_string()) - .expect("path has no file name"), - }); + let alias = match self.alias { + Some(alias) => alias, + None => match &self.name { + AnyPackageIdentifier::PackageName(versioned) => versioned.0.name().to_string(), + AnyPackageIdentifier::Url((url, _)) => url + .path + .to_string() + .split('/') + .next_back() + .map(|s| s.to_string()) + .unwrap_or(url.path.to_string()), + AnyPackageIdentifier::Workspace(versioned) => versioned.0.name().to_string(), + AnyPackageIdentifier::Path(path) => path + .file_name() + .map(|s| s.to_string_lossy().to_string()) + .expect("path has no file name"), + } + .parse() + .context("auto-generated alias is invalid. use --alias to specify one")?, + }; let field = &mut manifest[dependency_key] - .or_insert(toml_edit::Item::Table(toml_edit::Table::new()))[&alias]; + .or_insert(toml_edit::Item::Table(toml_edit::Table::new()))[alias.as_str()]; match specifier { DependencySpecifiers::Pesde(spec) => { diff --git a/src/cli/commands/init.rs b/src/cli/commands/init.rs index b861902..a6d7198 100644 --- a/src/cli/commands/init.rs +++ b/src/cli/commands/init.rs @@ -259,7 +259,7 @@ impl InitCommand { continue; }; - let field = &mut dev_deps[alias]; + let field = &mut dev_deps[alias.as_str()]; field["name"] = toml_edit::value(spec.name.to_string()); field["version"] = toml_edit::value(spec.version.to_string()); field["target"] = toml_edit::value( diff --git a/src/cli/commands/self_install.rs b/src/cli/commands/self_install.rs index 3763d71..84eb4a1 100644 --- a/src/cli/commands/self_install.rs +++ b/src/cli/commands/self_install.rs @@ -1,8 +1,9 @@ -use crate::cli::{version::update_bin_exe, HOME_DIR}; +use crate::cli::{version::replace_pesde_bin_exe, HOME_DIR}; use anyhow::Context; use clap::Args; use colored::Colorize; use std::env::current_exe; + #[derive(Debug, Args)] pub struct SelfInstallCommand { /// Skip adding the bin directory to the PATH @@ -70,7 +71,7 @@ and then restart your shell. ); } - update_bin_exe(¤t_exe().context("failed to get current exe path")?).await?; + replace_pesde_bin_exe(¤t_exe().context("failed to get current exe path")?).await?; Ok(()) } diff --git a/src/cli/commands/self_upgrade.rs b/src/cli/commands/self_upgrade.rs index 2759fff..0ad5f6a 100644 --- a/src/cli/commands/self_upgrade.rs +++ b/src/cli/commands/self_upgrade.rs @@ -1,13 +1,17 @@ -use crate::cli::{ - config::read_config, - version::{ - current_version, get_or_download_version, get_remote_version, no_build_metadata, - update_bin_exe, TagInfo, VersionType, +use crate::{ + cli::{ + config::read_config, + version::{ + current_version, find_latest_version, get_or_download_engine, replace_pesde_bin_exe, + }, }, + util::no_build_metadata, }; use anyhow::Context; use clap::Args; use colored::Colorize; +use pesde::engine::EngineKind; +use semver::VersionReq; #[derive(Debug, Args)] pub struct SelfUpgradeCommand { @@ -25,7 +29,7 @@ impl SelfUpgradeCommand { .context("no cached version found")? .1 } else { - get_remote_version(&reqwest, VersionType::Latest).await? + find_latest_version(&reqwest).await? }; let latest_version_no_metadata = no_build_metadata(&latest_version); @@ -46,10 +50,13 @@ impl SelfUpgradeCommand { return Ok(()); } - let path = get_or_download_version(&reqwest, TagInfo::Complete(latest_version), true) - .await? - .unwrap(); - update_bin_exe(&path).await?; + let path = get_or_download_engine( + &reqwest, + EngineKind::Pesde, + VersionReq::parse(&format!("={latest_version}")).unwrap(), + ) + .await?; + replace_pesde_bin_exe(&path).await?; println!("upgraded to version {display_latest_version}!"); diff --git a/src/cli/install.rs b/src/cli/install.rs index 1891267..20a0b17 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -1,10 +1,3 @@ -use std::{ - collections::{BTreeMap, BTreeSet, HashMap}, - num::NonZeroUsize, - sync::Arc, - time::Instant, -}; - use super::files::make_executable; use crate::cli::{ bin_dir, @@ -16,14 +9,23 @@ use colored::Colorize; use fs_err::tokio as fs; use pesde::{ download_and_link::{DownloadAndLinkHooks, DownloadAndLinkOptions}, + engine::EngineKind, graph::{DependencyGraph, DependencyGraphWithTarget}, lockfile::Lockfile, - manifest::{target::TargetKind, DependencyType}, - Project, RefreshedSources, LOCKFILE_FILE_NAME, MANIFEST_FILE_NAME, + manifest::{target::TargetKind, Alias, DependencyType, Manifest}, + names::PackageNames, + source::{pesde::PesdePackageSource, refs::PackageRefs, traits::PackageRef, PackageSources}, + version_matches, Project, RefreshedSources, LOCKFILE_FILE_NAME, MANIFEST_FILE_NAME, +}; +use std::{ + collections::{BTreeMap, BTreeSet, HashMap}, + num::NonZeroUsize, + sync::Arc, + time::Instant, }; use tokio::task::JoinSet; -fn bin_link_file(alias: &str) -> String { +fn bin_link_file(alias: &Alias) -> String { let mut all_combinations = BTreeSet::new(); for a in TargetKind::VARIANTS { @@ -68,23 +70,13 @@ impl DownloadAndLinkHooks for InstallHooks { .values() .filter(|node| node.target.bin_path().is_some()) .filter_map(|node| node.node.direct.as_ref()) - .map(|(alias, _, _)| alias) - .filter(|alias| { - if *alias == env!("CARGO_BIN_NAME") { - tracing::warn!( - "package {alias} has the same name as the CLI, skipping bin link" - ); - return false; - } - true - }) - .map(|alias| { + .map(|(alias, _, _)| { let bin_folder = self.bin_folder.clone(); let alias = alias.clone(); async move { let bin_exec_file = bin_folder - .join(&alias) + .join(alias.as_str()) .with_extension(std::env::consts::EXE_EXTENSION); let impl_folder = bin_folder.join(".impl"); @@ -92,7 +84,7 @@ impl DownloadAndLinkHooks for InstallHooks { .await .context("failed to create bin link folder")?; - let bin_file = impl_folder.join(&alias).with_extension("luau"); + let bin_file = impl_folder.join(alias.as_str()).with_extension("luau"); fs::write(&bin_file, bin_link_file(&alias)) .await .context("failed to write bin link file")?; @@ -196,10 +188,26 @@ pub async fn install( let overrides = resolve_overrides(&manifest)?; let (new_lockfile, old_graph) = - reporters::run_with_reporter(|_, root_progress, reporter| async { + reporters::run_with_reporter(|multi, root_progress, reporter| async { + let multi = multi; let root_progress = root_progress; root_progress.set_prefix(format!("{} {}: ", manifest.name, manifest.target)); + #[cfg(feature = "version-management")] + { + root_progress.set_message("update engine linkers"); + + let mut tasks = manifest + .engines + .keys() + .map(|engine| crate::cli::version::make_linker_if_needed(*engine)) + .collect::>(); + + while let Some(task) = tasks.join_next().await { + task.unwrap()?; + } + } + root_progress.set_message("clean"); if options.write { @@ -246,6 +254,41 @@ pub async fn install( ) .await .context("failed to build dependency graph")?; + + let mut tasks = graph + .iter() + .filter_map(|(id, node)| { + let PackageSources::Pesde(source) = node.pkg_ref.source() else { + return None; + }; + #[allow(irrefutable_let_patterns)] + let PackageNames::Pesde(name) = id.name().clone() else { + panic!("unexpected package name"); + }; + let project = project.clone(); + + Some(async move { + let file = source.read_index_file(&name, &project).await.context("failed to read package index file")?.context("package not found in index")?; + + Ok::<_, anyhow::Error>(if file.meta.deprecated.is_empty() { + None + } else { + Some((name, file.meta.deprecated)) + }) + }) + }) + .collect::>(); + + while let Some(task) = tasks.join_next().await { + let Some((name, reason)) = task.unwrap()? else { + continue; + }; + + multi.suspend(|| { + println!("{}: package {name} is deprecated: {reason}", "warn".yellow().bold()); + }); + } + let graph = Arc::new(graph); if options.write { @@ -285,9 +328,104 @@ pub async fn install( root_progress.set_message("patch"); project - .apply_patches(&downloaded_graph.convert(), reporter) + .apply_patches(&downloaded_graph.clone().convert(), reporter) .await?; } + + #[cfg(feature = "version-management")] + { + let mut tasks = manifest + .engines + .into_iter() + .map(|(engine, req)| async move { + Ok::<_, anyhow::Error>( + crate::cli::version::get_installed_versions(engine) + .await? + .into_iter() + .filter(|version| version_matches(version, &req)) + .next_back() + .map(|version| (engine, version)), + ) + }) + .collect::>(); + + let mut resolved_engine_versions = HashMap::new(); + while let Some(task) = tasks.join_next().await { + let Some((engine, version)) = task.unwrap()? else { + continue; + }; + resolved_engine_versions.insert(engine, version); + } + + let manifest_target_kind = manifest.target.kind(); + let mut tasks = downloaded_graph.iter() + .map(|(id, node)| { + let id = id.clone(); + let node = node.clone(); + let project = project.clone(); + + async move { + let engines = match &node.node.pkg_ref { + PackageRefs::Pesde(pkg_ref) => { + let source = PesdePackageSource::new(pkg_ref.index_url.clone()); + #[allow(irrefutable_let_patterns)] + let PackageNames::Pesde(name) = id.name() else { + panic!("unexpected package name"); + }; + + let mut file = source.read_index_file(name, &project).await.context("failed to read package index file")?.context("package not found in index")?; + file + .entries + .remove(id.version_id()) + .context("package version not found in index")? + .engines + } + #[cfg(feature = "wally-compat")] + PackageRefs::Wally(_) => Default::default(), + _ => { + let path = node.node.container_folder_from_project( + &id, + &project, + manifest_target_kind, + ); + + match fs::read_to_string(path.join(MANIFEST_FILE_NAME)).await { + Ok(manifest) => match toml::from_str::(&manifest) { + Ok(manifest) => manifest.engines, + Err(e) => return Err(e).context("failed to read package manifest"), + }, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Default::default(), + Err(e) => return Err(e).context("failed to read package manifest"), + } + } + }; + + Ok((id, engines)) + } + }) + .collect::>(); + + while let Some(task) = tasks.join_next().await { + let (id, required_engines) = task.unwrap()?; + + for (engine, req) in required_engines { + if engine == EngineKind::Pesde { + continue; + } + + let Some(version) = resolved_engine_versions.get(&engine) else { + tracing::debug!("package {id} requires {engine} {req}, but it is not installed"); + continue; + }; + + if !version_matches(version, &req) { + multi.suspend(|| { + println!("{}: package {id} requires {engine} {req}, but {version} is installed", "warn".yellow().bold()); + }); + } + } + } + } } root_progress.set_message("finish"); @@ -310,7 +448,7 @@ pub async fn install( anyhow::Ok((new_lockfile, old_graph.unwrap_or_default())) }) - .await?; + .await?; let elapsed = start.elapsed(); diff --git a/src/cli/reporters.rs b/src/cli/reporters.rs index 2cf1ca4..8f88768 100644 --- a/src/cli/reporters.rs +++ b/src/cli/reporters.rs @@ -99,31 +99,29 @@ impl CliReporter { } } -pub struct CliDownloadProgressReporter<'a, W> { - root_reporter: &'a CliReporter, +pub struct CliDownloadProgressReporter { + root_reporter: Arc>, name: String, progress: OnceLock, set_progress: Once, } -impl<'a, W: Write + Send + Sync + 'static> DownloadsReporter<'a> for CliReporter { - type DownloadProgressReporter = CliDownloadProgressReporter<'a, W>; +impl DownloadsReporter for CliReporter { + type DownloadProgressReporter = CliDownloadProgressReporter; - fn report_download<'b>(&'a self, name: &'b str) -> Self::DownloadProgressReporter { + fn report_download(self: Arc, name: String) -> Self::DownloadProgressReporter { self.root_progress.inc_length(1); CliDownloadProgressReporter { root_reporter: self, - name: name.to_string(), + name, progress: OnceLock::new(), set_progress: Once::new(), } } } -impl DownloadProgressReporter - for CliDownloadProgressReporter<'_, W> -{ +impl DownloadProgressReporter for CliDownloadProgressReporter { fn report_start(&self) { let progress = self.root_reporter.multi_progress.add(ProgressBar::new(0)); progress.set_style(self.root_reporter.child_style.clone()); @@ -171,16 +169,16 @@ impl DownloadProgressReporter } } -pub struct CliPatchProgressReporter<'a, W> { - root_reporter: &'a CliReporter, +pub struct CliPatchProgressReporter { + root_reporter: Arc>, name: String, progress: ProgressBar, } -impl<'a, W: Write + Send + Sync + 'static> PatchesReporter<'a> for CliReporter { - type PatchProgressReporter = CliPatchProgressReporter<'a, W>; +impl PatchesReporter for CliReporter { + type PatchProgressReporter = CliPatchProgressReporter; - fn report_patch<'b>(&'a self, name: &'b str) -> Self::PatchProgressReporter { + fn report_patch(self: Arc, name: String) -> Self::PatchProgressReporter { let progress = self.multi_progress.add(ProgressBar::new(0)); progress.set_style(self.child_style.clone()); progress.set_message(format!("- {name}")); @@ -195,7 +193,7 @@ impl<'a, W: Write + Send + Sync + 'static> PatchesReporter<'a> for CliReporter PatchProgressReporter for CliPatchProgressReporter<'_, W> { +impl PatchProgressReporter for CliPatchProgressReporter { fn report_done(&self) { if self.progress.is_hidden() { writeln!( diff --git a/src/cli/version.rs b/src/cli/version.rs index c88d9df..5ed0641 100644 --- a/src/cli/version.rs +++ b/src/cli/version.rs @@ -1,97 +1,59 @@ -use crate::cli::{ - bin_dir, - config::{read_config, write_config, CliConfig}, - files::make_executable, - home_dir, +use crate::{ + cli::{ + bin_dir, + config::{read_config, write_config, CliConfig}, + files::make_executable, + home_dir, + reporters::run_with_reporter, + }, + util::no_build_metadata, }; use anyhow::Context; use colored::Colorize; use fs_err::tokio as fs; -use futures::StreamExt; -use reqwest::header::ACCEPT; -use semver::Version; -use serde::Deserialize; +use pesde::{ + engine::{ + source::{ + traits::{DownloadOptions, EngineSource, ResolveOptions}, + EngineSources, + }, + EngineKind, + }, + reporters::DownloadsReporter, + version_matches, +}; +use semver::{Version, VersionReq}; use std::{ + collections::BTreeSet, env::current_exe, path::{Path, PathBuf}, + sync::Arc, }; -use tokio::io::AsyncWrite; use tracing::instrument; pub fn current_version() -> Version { Version::parse(env!("CARGO_PKG_VERSION")).unwrap() } -#[derive(Debug, Deserialize)] -struct Release { - tag_name: String, - assets: Vec, -} - -#[derive(Debug, Deserialize)] -struct Asset { - name: String, - url: url::Url, -} - -#[instrument(level = "trace")] -fn get_repo() -> (String, String) { - let mut parts = env!("CARGO_PKG_REPOSITORY").split('/').skip(3); - let (owner, repo) = ( - parts.next().unwrap().to_string(), - parts.next().unwrap().to_string(), - ); - - tracing::trace!("repository for updates: {owner}/{repo}"); - - (owner, repo) -} - -#[derive(Debug)] -pub enum VersionType { - Latest, - Specific(Version), -} - -#[instrument(skip(reqwest), level = "trace")] -pub async fn get_remote_version( - reqwest: &reqwest::Client, - ty: VersionType, -) -> anyhow::Result { - let (owner, repo) = get_repo(); - - let mut releases = reqwest - .get(format!( - "https://api.github.com/repos/{owner}/{repo}/releases", - )) - .send() - .await - .context("failed to send request to GitHub API")? - .error_for_status() - .context("failed to get GitHub API response")? - .json::>() - .await - .context("failed to parse GitHub API response")? - .into_iter() - .filter_map(|release| Version::parse(release.tag_name.trim_start_matches('v')).ok()); - - match ty { - VersionType::Latest => releases.max(), - VersionType::Specific(version) => { - releases.find(|v| no_build_metadata(v) == no_build_metadata(&version)) - } - } - .context("failed to find latest version") -} - -pub fn no_build_metadata(version: &Version) -> Version { - let mut version = version.clone(); - version.build = semver::BuildMetadata::EMPTY; - version -} - const CHECK_INTERVAL: chrono::Duration = chrono::Duration::hours(6); +pub async fn find_latest_version(reqwest: &reqwest::Client) -> anyhow::Result { + let version = EngineSources::pesde() + .resolve( + &VersionReq::STAR, + &ResolveOptions { + reqwest: reqwest.clone(), + }, + ) + .await + .context("failed to resolve version")? + .pop_last() + .context("no versions found")? + .0; + + Ok(version) +} + #[instrument(skip(reqwest), level = "trace")] pub async fn check_for_updates(reqwest: &reqwest::Client) -> anyhow::Result<()> { let config = read_config().await?; @@ -104,7 +66,7 @@ pub async fn check_for_updates(reqwest: &reqwest::Client) -> anyhow::Result<()> version } else { tracing::debug!("checking for updates"); - let version = get_remote_version(reqwest, VersionType::Latest).await?; + let version = find_latest_version(reqwest).await?; write_config(&CliConfig { last_checked_updates: Some((chrono::Utc::now(), version.clone())), @@ -180,154 +142,132 @@ pub async fn check_for_updates(reqwest: &reqwest::Client) -> anyhow::Result<()> Ok(()) } -#[instrument(skip(reqwest, writer), level = "trace")] -pub async fn download_github_release( - reqwest: &reqwest::Client, - version: &Version, - mut writer: W, -) -> anyhow::Result<()> { - let (owner, repo) = get_repo(); +const ENGINES_DIR: &str = "engines"; - let release = reqwest - .get(format!( - "https://api.github.com/repos/{owner}/{repo}/releases/tags/v{version}", - )) - .send() - .await - .context("failed to send request to GitHub API")? - .error_for_status() - .context("failed to get GitHub API response")? - .json::() - .await - .context("failed to parse GitHub API response")?; +#[instrument(level = "trace")] +pub async fn get_installed_versions(engine: EngineKind) -> anyhow::Result> { + let source = engine.source(); + let path = home_dir()?.join(ENGINES_DIR).join(source.directory()); + let mut installed_versions = BTreeSet::new(); - let asset = release - .assets - .into_iter() - .find(|asset| { - asset.name.ends_with(&format!( - "-{}-{}.tar.gz", - std::env::consts::OS, - std::env::consts::ARCH - )) - }) - .context("failed to find asset for current platform")?; + let mut read_dir = match fs::read_dir(&path).await { + Ok(read_dir) => read_dir, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(installed_versions), + Err(e) => return Err(e).context("failed to read engines directory"), + }; - let bytes = reqwest - .get(asset.url) - .header(ACCEPT, "application/octet-stream") - .send() - .await - .context("failed to send request to download asset")? - .error_for_status() - .context("failed to download asset")? - .bytes() - .await - .context("failed to download asset")?; + while let Some(entry) = read_dir.next_entry().await? { + let path = entry.path(); - let mut decoder = async_compression::tokio::bufread::GzipDecoder::new(bytes.as_ref()); - let mut archive = tokio_tar::Archive::new(&mut decoder); + let Some(version) = path.file_name().and_then(|s| s.to_str()) else { + continue; + }; - let mut entry = archive - .entries() - .context("failed to read archive entries")? - .next() - .await - .context("archive has no entry")? - .context("failed to get first archive entry")?; + if let Ok(version) = Version::parse(version) { + installed_versions.insert(version); + } + } - tokio::io::copy(&mut entry, &mut writer) - .await - .context("failed to write archive entry to file") - .map(|_| ()) -} - -#[derive(Debug)] -pub enum TagInfo { - Complete(Version), - Incomplete(Version), + Ok(installed_versions) } #[instrument(skip(reqwest), level = "trace")] -pub async fn get_or_download_version( +pub async fn get_or_download_engine( reqwest: &reqwest::Client, - tag: TagInfo, - always_give_path: bool, -) -> anyhow::Result> { - let path = home_dir()?.join("versions"); + engine: EngineKind, + req: VersionReq, +) -> anyhow::Result { + let source = engine.source(); + let path = home_dir()?.join(ENGINES_DIR).join(source.directory()); + + let installed_versions = get_installed_versions(engine).await?; + + let max_matching = installed_versions + .iter() + .filter(|v| version_matches(v, &req)) + .next_back(); + if let Some(version) = max_matching { + return Ok(path + .join(version.to_string()) + .join(source.expected_file_name()) + .with_extension(std::env::consts::EXE_EXTENSION)); + } + + let mut versions = source + .resolve( + &req, + &ResolveOptions { + reqwest: reqwest.clone(), + }, + ) + .await + .context("failed to resolve versions")?; + let (version, engine_ref) = versions.pop_last().context("no matching versions found")?; + + let path = path.join(version.to_string()); + fs::create_dir_all(&path) .await - .context("failed to create versions directory")?; + .context("failed to create engine container folder")?; - let version = match &tag { - TagInfo::Complete(version) => version, - // don't fetch the version since it could be cached - TagInfo::Incomplete(version) => version, - }; + let path = path + .join(source.expected_file_name()) + .with_extension(std::env::consts::EXE_EXTENSION); - let path = path.join(format!( - "{}{}", - no_build_metadata(version), - std::env::consts::EXE_SUFFIX - )); + let mut file = fs::File::create(&path) + .await + .context("failed to create new file")?; - let is_requested_version = !always_give_path && *version == current_version(); + run_with_reporter(|_, root_progress, reporter| async { + let root_progress = root_progress; - if path.exists() { - tracing::debug!("version already exists"); + root_progress.set_message("download"); - return Ok(if is_requested_version { - None - } else { - Some(path) - }); - } + let reporter = reporter.report_download(format!("{engine} v{version}")); - if is_requested_version { - tracing::debug!("copying current executable to version directory"); - fs::copy(current_exe()?, &path) + let archive = source + .download( + &engine_ref, + &DownloadOptions { + reqwest: reqwest.clone(), + reporter: Arc::new(reporter), + version: version.clone(), + }, + ) .await - .context("failed to copy current executable to version directory")?; - } else { - let version = match tag { - TagInfo::Complete(version) => version, - TagInfo::Incomplete(version) => { - get_remote_version(reqwest, VersionType::Specific(version)) - .await - .context("failed to get remote version")? - } - }; + .context("failed to download engine")?; - tracing::debug!("downloading version"); - download_github_release( - reqwest, - &version, - fs::File::create(&path) + tokio::io::copy( + &mut archive + .find_executable(source.expected_file_name()) .await - .context("failed to create version file")?, + .context("failed to find executable")?, + &mut file, ) - .await?; - } + .await + .context("failed to write to file")?; + + Ok::<_, anyhow::Error>(()) + }) + .await?; make_executable(&path) .await .context("failed to make downloaded version executable")?; - Ok(if is_requested_version { - None - } else { - Some(path) - }) + if engine != EngineKind::Pesde { + make_linker_if_needed(engine).await?; + } + + Ok(path) } #[instrument(level = "trace")] -pub async fn update_bin_exe(downloaded_file: &Path) -> anyhow::Result<()> { - let bin_exe_path = bin_dir().await?.join(format!( - "{}{}", - env!("CARGO_BIN_NAME"), - std::env::consts::EXE_SUFFIX - )); - let mut downloaded_file = downloaded_file.to_path_buf(); +pub async fn replace_pesde_bin_exe(with: &Path) -> anyhow::Result<()> { + let bin_exe_path = bin_dir() + .await? + .join(EngineKind::Pesde.to_string()) + .with_extension(std::env::consts::EXE_EXTENSION); let exists = bin_exe_path.exists(); @@ -339,23 +279,42 @@ pub async fn update_bin_exe(downloaded_file: &Path) -> anyhow::Result<()> { let tempfile = tempfile::Builder::new() .make(|_| Ok(())) .context("failed to create temporary file")?; - let path = tempfile.into_temp_path().to_path_buf(); + let temp_path = tempfile.into_temp_path().to_path_buf(); #[cfg(windows)] - let path = path.with_extension("exe"); + let temp_path = temp_path.with_extension("exe"); - let current_exe = current_exe().context("failed to get current exe path")?; - if current_exe == downloaded_file { - downloaded_file = path.to_path_buf(); + match fs::rename(&bin_exe_path, &temp_path).await { + Ok(_) => {} + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => return Err(e).context("failed to rename existing executable"), } - - fs::rename(&bin_exe_path, &path) - .await - .context("failed to rename current executable")?; } - fs::copy(downloaded_file, &bin_exe_path) + fs::copy(with, &bin_exe_path) .await .context("failed to copy executable to bin folder")?; make_executable(&bin_exe_path).await } + +#[instrument(level = "trace")] +pub async fn make_linker_if_needed(engine: EngineKind) -> anyhow::Result<()> { + let bin_dir = bin_dir().await?; + let linker = bin_dir + .join(engine.to_string()) + .with_extension(std::env::consts::EXE_EXTENSION); + let exists = linker.exists(); + + if !exists { + let exe = current_exe().context("failed to get current exe path")?; + + #[cfg(windows)] + let result = fs::symlink_file(exe, linker); + #[cfg(not(windows))] + let result = fs::symlink(exe, linker); + + result.await.context("failed to create symlink")?; + } + + Ok(()) +} diff --git a/src/download.rs b/src/download.rs index 3e16935..3ced222 100644 --- a/src/download.rs +++ b/src/download.rs @@ -29,7 +29,7 @@ pub(crate) struct DownloadGraphOptions { impl DownloadGraphOptions where - Reporter: for<'a> DownloadsReporter<'a> + Send + Sync + 'static, + Reporter: DownloadsReporter + Send + Sync + 'static, { /// Creates a new download options with the given reqwest client and reporter. pub(crate) fn new(reqwest: reqwest::Client) -> Self { @@ -85,7 +85,7 @@ impl Project { errors::DownloadGraphError, > where - Reporter: for<'a> DownloadsReporter<'a> + Send + Sync + 'static, + Reporter: DownloadsReporter + Send + Sync + 'static, { let DownloadGraphOptions { reqwest, @@ -111,8 +111,8 @@ impl Project { async move { let progress_reporter = reporter - .as_deref() - .map(|reporter| reporter.report_download(&package_id.to_string())); + .clone() + .map(|reporter| reporter.report_download(package_id.to_string())); let _permit = semaphore.acquire().await; diff --git a/src/download_and_link.rs b/src/download_and_link.rs index b8c74e5..d799297 100644 --- a/src/download_and_link.rs +++ b/src/download_and_link.rs @@ -81,7 +81,7 @@ pub struct DownloadAndLinkOptions { impl DownloadAndLinkOptions where - Reporter: for<'a> DownloadsReporter<'a> + Send + Sync + 'static, + Reporter: DownloadsReporter + Send + Sync + 'static, Hooks: DownloadAndLinkHooks + Send + Sync + 'static, { /// Creates a new download options with the given reqwest client and reporter. @@ -149,7 +149,7 @@ impl Project { options: DownloadAndLinkOptions, ) -> Result> where - Reporter: for<'a> DownloadsReporter<'a> + 'static, + Reporter: DownloadsReporter + 'static, Hooks: DownloadAndLinkHooks + 'static, { let DownloadAndLinkOptions { diff --git a/src/engine/mod.rs b/src/engine/mod.rs new file mode 100644 index 0000000..3dd11e1 --- /dev/null +++ b/src/engine/mod.rs @@ -0,0 +1,63 @@ +/// Sources of engines +pub mod source; + +use crate::engine::source::EngineSources; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use std::{fmt::Display, str::FromStr}; + +/// All supported engines +#[derive( + SerializeDisplay, DeserializeFromStr, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, +)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "schema", schemars(rename_all = "snake_case"))] +pub enum EngineKind { + /// The pesde package manager + Pesde, + /// The Lune runtime + Lune, +} + +impl Display for EngineKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EngineKind::Pesde => write!(f, "pesde"), + EngineKind::Lune => write!(f, "lune"), + } + } +} + +impl FromStr for EngineKind { + type Err = errors::EngineKindFromStrError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "pesde" => Ok(EngineKind::Pesde), + "lune" => Ok(EngineKind::Lune), + _ => Err(errors::EngineKindFromStrError::Unknown(s.to_string())), + } + } +} + +impl EngineKind { + /// Returns the source to get this engine from + pub fn source(&self) -> EngineSources { + match self { + EngineKind::Pesde => EngineSources::pesde(), + EngineKind::Lune => EngineSources::lune(), + } + } +} + +/// Errors related to engine kinds +pub mod errors { + use thiserror::Error; + + /// Errors which can occur while using the FromStr implementation of EngineKind + #[derive(Debug, Error)] + pub enum EngineKindFromStrError { + /// The string isn't a recognized EngineKind + #[error("unknown engine kind {0}")] + Unknown(String), + } +} diff --git a/src/engine/source/archive.rs b/src/engine/source/archive.rs new file mode 100644 index 0000000..28a2476 --- /dev/null +++ b/src/engine/source/archive.rs @@ -0,0 +1,320 @@ +use futures::StreamExt; +use std::{ + collections::BTreeSet, + mem::ManuallyDrop, + path::{Path, PathBuf}, + pin::Pin, + str::FromStr, + task::{Context, Poll}, +}; +use tokio::{ + io::{AsyncBufRead, AsyncRead, AsyncReadExt, ReadBuf}, + pin, +}; +use tokio_util::compat::{Compat, FuturesAsyncReadCompatExt}; + +/// The kind of encoding used for the archive +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EncodingKind { + /// Gzip + Gzip, +} + +/// The kind of archive +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ArchiveKind { + /// Tar + Tar, + /// Zip + Zip, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct ArchiveInfo(ArchiveKind, Option); + +impl FromStr for ArchiveInfo { + type Err = errors::ArchiveInfoFromStrError; + + fn from_str(s: &str) -> Result { + let parts = s.split('.').collect::>(); + + Ok(match &*parts { + [.., "tar", "gz"] => ArchiveInfo(ArchiveKind::Tar, Some(EncodingKind::Gzip)), + [.., "tar"] => ArchiveInfo(ArchiveKind::Tar, None), + [.., "zip", "gz"] => { + return Err(errors::ArchiveInfoFromStrError::Unsupported( + ArchiveKind::Zip, + Some(EncodingKind::Gzip), + )) + } + [.., "zip"] => ArchiveInfo(ArchiveKind::Zip, None), + _ => return Err(errors::ArchiveInfoFromStrError::Invalid(s.to_string())), + }) + } +} + +pub(crate) type ArchiveReader = Pin>; + +/// An archive +pub struct Archive { + pub(crate) info: ArchiveInfo, + pub(crate) reader: ArchiveReader, +} + +enum TarReader { + Gzip(async_compression::tokio::bufread::GzipDecoder), + Plain(ArchiveReader), +} + +// TODO: try to see if we can avoid the unsafe blocks + +impl AsyncRead for TarReader { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + unsafe { + match self.get_unchecked_mut() { + Self::Gzip(r) => Pin::new_unchecked(r).poll_read(cx, buf), + Self::Plain(r) => Pin::new_unchecked(r).poll_read(cx, buf), + } + } + } +} + +enum ArchiveEntryInner { + Tar(tokio_tar::Entry>), + Zip { + archive: *mut async_zip::tokio::read::seek::ZipFileReader>>, + reader: ManuallyDrop< + Compat< + async_zip::tokio::read::ZipEntryReader< + 'static, + std::io::Cursor>, + async_zip::base::read::WithoutEntry, + >, + >, + >, + }, +} + +impl Drop for ArchiveEntryInner { + fn drop(&mut self) { + match self { + Self::Tar(_) => {} + Self::Zip { archive, reader } => unsafe { + ManuallyDrop::drop(reader); + drop(Box::from_raw(*archive)); + }, + } + } +} + +/// An entry in an archive. Usually the executable +pub struct ArchiveEntry(ArchiveEntryInner); + +impl AsyncRead for ArchiveEntry { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + unsafe { + match &mut self.get_unchecked_mut().0 { + ArchiveEntryInner::Tar(r) => Pin::new_unchecked(r).poll_read(cx, buf), + ArchiveEntryInner::Zip { reader, .. } => { + Pin::new_unchecked(&mut **reader).poll_read(cx, buf) + } + } + } + } +} + +impl Archive { + /// Finds the executable in the archive and returns it as an [`ArchiveEntry`] + pub async fn find_executable( + self, + expected_file_name: &str, + ) -> Result { + #[derive(Debug, PartialEq, Eq)] + struct Candidate { + path: PathBuf, + file_name_matches: bool, + extension_matches: bool, + has_permissions: bool, + } + + impl Candidate { + fn new(path: PathBuf, perms: u32, expected_file_name: &str) -> Self { + Self { + file_name_matches: path + .file_name() + .is_some_and(|name| name == expected_file_name), + extension_matches: match path.extension() { + Some(ext) if ext == std::env::consts::EXE_EXTENSION => true, + None if std::env::consts::EXE_EXTENSION.is_empty() => true, + _ => false, + }, + path, + has_permissions: perms & 0o111 != 0, + } + } + + fn should_be_considered(&self) -> bool { + // if nothing matches, we should not consider this candidate as it is most likely not + self.file_name_matches || self.extension_matches || self.has_permissions + } + } + + impl Ord for Candidate { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.file_name_matches + .cmp(&other.file_name_matches) + .then(self.extension_matches.cmp(&other.extension_matches)) + .then(self.has_permissions.cmp(&other.has_permissions)) + } + } + + impl PartialOrd for Candidate { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + let mut candidates = BTreeSet::new(); + + match self.info { + ArchiveInfo(ArchiveKind::Tar, encoding) => { + use async_compression::tokio::bufread as decoders; + + let reader = match encoding { + Some(EncodingKind::Gzip) => { + TarReader::Gzip(decoders::GzipDecoder::new(self.reader)) + } + None => TarReader::Plain(self.reader), + }; + + let mut archive = tokio_tar::Archive::new(reader); + let mut entries = archive.entries()?; + + while let Some(entry) = entries.next().await.transpose()? { + if entry.header().entry_type().is_dir() { + continue; + } + + let candidate = Candidate::new( + entry.path()?.to_path_buf(), + entry.header().mode()?, + expected_file_name, + ); + if candidate.should_be_considered() { + candidates.insert(candidate); + } + } + + let Some(candidate) = candidates.pop_last() else { + return Err(errors::FindExecutableError::ExecutableNotFound); + }; + + let mut entries = archive.entries()?; + + while let Some(entry) = entries.next().await.transpose()? { + if entry.header().entry_type().is_dir() { + continue; + } + + let path = entry.path()?; + if path == candidate.path { + return Ok(ArchiveEntry(ArchiveEntryInner::Tar(entry))); + } + } + } + ArchiveInfo(ArchiveKind::Zip, _) => { + let reader = self.reader; + pin!(reader); + + // TODO: would be lovely to not have to read the whole archive into memory + let mut buf = vec![]; + reader.read_to_end(&mut buf).await?; + + let archive = async_zip::base::read::seek::ZipFileReader::with_tokio( + std::io::Cursor::new(buf), + ) + .await?; + for entry in archive.file().entries() { + if entry.dir()? { + continue; + } + + let path: &Path = entry.filename().as_str()?.as_ref(); + let candidate = Candidate::new( + path.to_path_buf(), + entry.unix_permissions().unwrap_or(0) as u32, + expected_file_name, + ); + if candidate.should_be_considered() { + candidates.insert(candidate); + } + } + + let Some(candidate) = candidates.pop_last() else { + return Err(errors::FindExecutableError::ExecutableNotFound); + }; + + for (i, entry) in archive.file().entries().iter().enumerate() { + if entry.dir()? { + continue; + } + + let path: &Path = entry.filename().as_str()?.as_ref(); + if candidate.path == path { + let ptr = Box::into_raw(Box::new(archive)); + let reader = (unsafe { &mut *ptr }).reader_without_entry(i).await?; + return Ok(ArchiveEntry(ArchiveEntryInner::Zip { + archive: ptr, + reader: ManuallyDrop::new(reader.compat()), + })); + } + } + } + } + + Err(errors::FindExecutableError::ExecutableNotFound) + } +} + +/// Errors that can occur when working with archives +pub mod errors { + use thiserror::Error; + + /// Errors that can occur when parsing archive info + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum ArchiveInfoFromStrError { + /// The string is not a valid archive descriptor. E.g. `{name}.tar.gz` + #[error("string `{0}` is not a valid archive descriptor")] + Invalid(String), + + /// The archive type is not supported. E.g. `{name}.zip.gz` + #[error("archive type {0:?} with encoding {1:?} is not supported")] + Unsupported(super::ArchiveKind, Option), + } + + /// Errors that can occur when finding an executable in an archive + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum FindExecutableError { + /// The executable was not found in the archive + #[error("failed to find executable in archive")] + ExecutableNotFound, + + /// An IO error occurred + #[error("IO error")] + Io(#[from] std::io::Error), + + /// An error occurred reading the zip archive + #[error("failed to read zip archive")] + Zip(#[from] async_zip::error::ZipError), + } +} diff --git a/src/engine/source/github/engine_ref.rs b/src/engine/source/github/engine_ref.rs new file mode 100644 index 0000000..8792b02 --- /dev/null +++ b/src/engine/source/github/engine_ref.rs @@ -0,0 +1,19 @@ +use serde::Deserialize; + +/// A GitHub release +#[derive(Debug, Eq, PartialEq, Hash, Clone, Deserialize)] +pub struct Release { + /// The tag name of the release + pub tag_name: String, + /// The assets of the release + pub assets: Vec, +} + +/// An asset of a GitHub release +#[derive(Debug, Eq, PartialEq, Hash, Clone, Deserialize)] +pub struct Asset { + /// The name of the asset + pub name: String, + /// The download URL of the asset + pub url: url::Url, +} diff --git a/src/engine/source/github/mod.rs b/src/engine/source/github/mod.rs new file mode 100644 index 0000000..316e95e --- /dev/null +++ b/src/engine/source/github/mod.rs @@ -0,0 +1,146 @@ +/// The GitHub engine reference +pub mod engine_ref; + +use crate::{ + engine::source::{ + archive::Archive, + github::engine_ref::Release, + traits::{DownloadOptions, EngineSource, ResolveOptions}, + }, + reporters::{response_to_async_read, DownloadProgressReporter}, + util::no_build_metadata, + version_matches, +}; +use reqwest::header::ACCEPT; +use semver::{Version, VersionReq}; +use std::{collections::BTreeMap, path::PathBuf}; + +/// The GitHub engine source +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub struct GitHubEngineSource { + /// The owner of the repository to download from + pub owner: String, + /// The repository of which to download releases from + pub repo: String, + /// The template for the asset name. `{VERSION}` will be replaced with the version + pub asset_template: String, +} + +impl EngineSource for GitHubEngineSource { + type Ref = Release; + type ResolveError = errors::ResolveError; + type DownloadError = errors::DownloadError; + + fn directory(&self) -> PathBuf { + PathBuf::from("github").join(&self.owner).join(&self.repo) + } + + fn expected_file_name(&self) -> &str { + &self.repo + } + + async fn resolve( + &self, + requirement: &VersionReq, + options: &ResolveOptions, + ) -> Result, Self::ResolveError> { + let ResolveOptions { reqwest, .. } = options; + + Ok(reqwest + .get(format!( + "https://api.github.com/repos/{}/{}/releases", + urlencoding::encode(&self.owner), + urlencoding::encode(&self.repo), + )) + .send() + .await? + .error_for_status()? + .json::>() + .await? + .into_iter() + .filter_map( + |release| match release.tag_name.trim_start_matches('v').parse() { + Ok(version) if version_matches(&version, requirement) => { + Some((version, release)) + } + _ => None, + }, + ) + .collect()) + } + + async fn download( + &self, + engine_ref: &Self::Ref, + options: &DownloadOptions, + ) -> Result { + let DownloadOptions { + reqwest, + reporter, + version, + .. + } = options; + + let desired_asset_names = [ + self.asset_template + .replace("{VERSION}", &version.to_string()), + self.asset_template + .replace("{VERSION}", &no_build_metadata(version).to_string()), + ]; + + let asset = engine_ref + .assets + .iter() + .find(|asset| { + desired_asset_names + .iter() + .any(|name| asset.name.eq_ignore_ascii_case(name)) + }) + .ok_or(errors::DownloadError::AssetNotFound)?; + + reporter.report_start(); + + let response = reqwest + .get(asset.url.clone()) + .header(ACCEPT, "application/octet-stream") + .send() + .await? + .error_for_status()?; + + Ok(Archive { + info: asset.name.parse()?, + reader: Box::pin(response_to_async_read(response, reporter.clone())), + }) + } +} + +/// Errors that can occur when working with the GitHub engine source +pub mod errors { + use thiserror::Error; + + /// Errors that can occur when resolving a GitHub engine + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum ResolveError { + /// Handling the request failed + #[error("failed to handle GitHub API request")] + Request(#[from] reqwest::Error), + } + + /// Errors that can occur when downloading a GitHub engine + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum DownloadError { + /// An asset for the current platform could not be found + #[error("failed to find asset for current platform")] + AssetNotFound, + + /// Handling the request failed + #[error("failed to handle GitHub API request")] + Request(#[from] reqwest::Error), + + /// The asset's name could not be parsed + #[error("failed to parse asset name")] + ParseAssetName(#[from] crate::engine::source::archive::errors::ArchiveInfoFromStrError), + } +} diff --git a/src/engine/source/mod.rs b/src/engine/source/mod.rs new file mode 100644 index 0000000..b8a8a67 --- /dev/null +++ b/src/engine/source/mod.rs @@ -0,0 +1,143 @@ +use crate::{ + engine::source::{ + archive::Archive, + traits::{DownloadOptions, EngineSource, ResolveOptions}, + }, + reporters::DownloadProgressReporter, +}; +use semver::{Version, VersionReq}; +use std::{collections::BTreeMap, path::PathBuf}; + +/// Archives +pub mod archive; +/// The GitHub engine source +pub mod github; +/// Traits for engine sources +pub mod traits; + +/// Engine references +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub enum EngineRefs { + /// A GitHub engine reference + GitHub(github::engine_ref::Release), +} + +/// Engine sources +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub enum EngineSources { + /// A GitHub engine source + GitHub(github::GitHubEngineSource), +} + +impl EngineSource for EngineSources { + type Ref = EngineRefs; + type ResolveError = errors::ResolveError; + type DownloadError = errors::DownloadError; + + fn directory(&self) -> PathBuf { + match self { + EngineSources::GitHub(source) => source.directory(), + } + } + + fn expected_file_name(&self) -> &str { + match self { + EngineSources::GitHub(source) => source.expected_file_name(), + } + } + + async fn resolve( + &self, + requirement: &VersionReq, + options: &ResolveOptions, + ) -> Result, Self::ResolveError> { + match self { + EngineSources::GitHub(source) => source + .resolve(requirement, options) + .await + .map(|map| { + map.into_iter() + .map(|(version, release)| (version, EngineRefs::GitHub(release))) + .collect() + }) + .map_err(Into::into), + } + } + + async fn download( + &self, + engine_ref: &Self::Ref, + options: &DownloadOptions, + ) -> Result { + match (self, engine_ref) { + (EngineSources::GitHub(source), EngineRefs::GitHub(release)) => { + source.download(release, options).await.map_err(Into::into) + } + + // for the future + #[allow(unreachable_patterns)] + _ => Err(errors::DownloadError::Mismatch), + } + } +} + +impl EngineSources { + /// Returns the source for the pesde engine + pub fn pesde() -> Self { + let mut parts = env!("CARGO_PKG_REPOSITORY").split('/').skip(3); + let (owner, repo) = ( + parts.next().unwrap().to_string(), + parts.next().unwrap().to_string(), + ); + + EngineSources::GitHub(github::GitHubEngineSource { + owner, + repo, + asset_template: format!( + "pesde-{{VERSION}}-{}-{}.zip", + std::env::consts::OS, + std::env::consts::ARCH + ), + }) + } + + /// Returns the source for the lune engine + pub fn lune() -> Self { + EngineSources::GitHub(github::GitHubEngineSource { + owner: "lune-org".into(), + repo: "lune".into(), + asset_template: format!( + "lune-{{VERSION}}-{}-{}.zip", + std::env::consts::OS, + std::env::consts::ARCH + ), + }) + } +} + +/// Errors that can occur when working with engine sources +pub mod errors { + use thiserror::Error; + + /// Errors that can occur when resolving an engine + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum ResolveError { + /// Failed to resolve the GitHub engine + #[error("failed to resolve github engine")] + GitHub(#[from] super::github::errors::ResolveError), + } + + /// Errors that can occur when downloading an engine + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum DownloadError { + /// Failed to download the GitHub engine + #[error("failed to download github engine")] + GitHub(#[from] super::github::errors::DownloadError), + + /// Mismatched engine reference + #[error("mismatched engine reference")] + Mismatch, + } +} diff --git a/src/engine/source/traits.rs b/src/engine/source/traits.rs new file mode 100644 index 0000000..a06787b --- /dev/null +++ b/src/engine/source/traits.rs @@ -0,0 +1,51 @@ +use crate::{engine::source::archive::Archive, reporters::DownloadProgressReporter}; +use semver::{Version, VersionReq}; +use std::{collections::BTreeMap, fmt::Debug, future::Future, path::PathBuf, sync::Arc}; + +/// Options for resolving an engine +#[derive(Debug, Clone)] +pub struct ResolveOptions { + /// The reqwest client to use + pub reqwest: reqwest::Client, +} + +/// Options for downloading an engine +#[derive(Debug, Clone)] +pub struct DownloadOptions { + /// The reqwest client to use + pub reqwest: reqwest::Client, + /// The reporter to use + pub reporter: Arc, + /// The version of the engine to be downloaded + pub version: Version, +} + +/// A source of engines +pub trait EngineSource: Debug { + /// The reference type for this source + type Ref; + /// The error type for resolving an engine from this source + type ResolveError: std::error::Error + Send + Sync + 'static; + /// The error type for downloading an engine from this source + type DownloadError: std::error::Error + Send + Sync + 'static; + + /// Returns the folder to store the engine's versions in + fn directory(&self) -> PathBuf; + + /// Returns the expected file name of the engine in the archive + fn expected_file_name(&self) -> &str; + + /// Resolves a requirement to a reference + fn resolve( + &self, + requirement: &VersionReq, + options: &ResolveOptions, + ) -> impl Future, Self::ResolveError>> + Send + Sync; + + /// Downloads an engine + fn download( + &self, + engine_ref: &Self::Ref, + options: &DownloadOptions, + ) -> impl Future> + Send + Sync; +} diff --git a/src/graph.rs b/src/graph.rs index f11292b..7acb574 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,7 +1,7 @@ use crate::{ manifest::{ target::{Target, TargetKind}, - DependencyType, + Alias, DependencyType, }, source::{ ids::{PackageId, VersionId}, @@ -22,10 +22,10 @@ pub type Graph = BTreeMap; pub struct DependencyGraphNode { /// The alias, specifier, and original (as in the manifest) type for the dependency, if it is a direct dependency (i.e. used by the current project) #[serde(default, skip_serializing_if = "Option::is_none")] - pub direct: Option<(String, DependencySpecifiers, DependencyType)>, + pub direct: Option<(Alias, DependencySpecifiers, DependencyType)>, /// The dependencies of the package #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub dependencies: BTreeMap, + pub dependencies: BTreeMap, /// The resolved (transformed, for example Peer -> Standard) type of the dependency pub resolved_ty: DependencyType, /// Whether the resolved type should be Peer if this isn't depended on diff --git a/src/lib.rs b/src/lib.rs index 9595a4d..642bbd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ use async_stream::try_stream; use fs_err::tokio as fs; use futures::Stream; use gix::sec::identity::Account; +use semver::{Version, VersionReq}; use std::{ collections::{HashMap, HashSet}, fmt::Debug, @@ -29,6 +30,8 @@ use wax::Pattern; pub mod download; /// Utility for downloading and linking in the correct order pub mod download_and_link; +/// Handling of engines +pub mod engine; /// Graphs pub mod graph; /// Linking packages @@ -117,8 +120,8 @@ struct ProjectShared { package_dir: PathBuf, workspace_dir: Option, data_dir: PathBuf, - auth_config: AuthConfig, cas_dir: PathBuf, + auth_config: AuthConfig, } /// The main struct of the pesde library, representing a project @@ -130,11 +133,11 @@ pub struct Project { impl Project { /// Create a new `Project` - pub fn new, Q: AsRef, R: AsRef, S: AsRef>( - package_dir: P, - workspace_dir: Option, - data_dir: R, - cas_dir: S, + pub fn new( + package_dir: impl AsRef, + workspace_dir: Option>, + data_dir: impl AsRef, + cas_dir: impl AsRef, auth_config: AuthConfig, ) -> Self { Project { @@ -142,8 +145,8 @@ impl Project { package_dir: package_dir.as_ref().to_path_buf(), workspace_dir: workspace_dir.map(|d| d.as_ref().to_path_buf()), data_dir: data_dir.as_ref().to_path_buf(), - auth_config, cas_dir: cas_dir.as_ref().to_path_buf(), + auth_config, }), } } @@ -163,16 +166,16 @@ impl Project { &self.shared.data_dir } - /// The authentication configuration - pub fn auth_config(&self) -> &AuthConfig { - &self.shared.auth_config - } - /// The CAS (content-addressable storage) directory pub fn cas_dir(&self) -> &Path { &self.shared.cas_dir } + /// The authentication configuration + pub fn auth_config(&self) -> &AuthConfig { + &self.shared.auth_config + } + /// Read the manifest file #[instrument(skip(self), ret(level = "trace"), level = "debug")] pub async fn read_manifest(&self) -> Result { @@ -425,6 +428,12 @@ pub async fn find_roots( Ok((project_root.unwrap_or(cwd), workspace_dir)) } +/// Returns whether a version matches a version requirement +/// Differs from `VersionReq::matches` in that EVERY version matches `*` +pub fn version_matches(version: &Version, req: &VersionReq) -> bool { + *req == VersionReq::STAR || req.matches(version) +} + /// Errors that can occur when using the pesde library pub mod errors { use std::path::PathBuf; diff --git a/src/linking/mod.rs b/src/linking/mod.rs index dd770ec..1feee0f 100644 --- a/src/linking/mod.rs +++ b/src/linking/mod.rs @@ -1,7 +1,7 @@ use crate::{ graph::{DependencyGraphNodeWithTarget, DependencyGraphWithTarget}, linking::generator::get_file_types, - manifest::Manifest, + manifest::{Alias, Manifest}, scripts::{execute_script, ExecuteScriptHooks, ScriptName}, source::{ fs::{cas_path, store_in_cas}, @@ -169,7 +169,7 @@ impl Project { relative_container_folder: &Path, node: &DependencyGraphNodeWithTarget, package_id: &PackageId, - alias: &str, + alias: &Alias, package_types: &Arc, manifest: &Arc, remove: bool, @@ -243,7 +243,8 @@ impl Project { .filter(|s| !s.is_empty() && node.node.direct.is_some() && is_root) { let scripts_container = self.package_dir().join(SCRIPTS_LINK_FOLDER); - let scripts_base = create_and_canonicalize(scripts_container.join(alias)).await?; + let scripts_base = + create_and_canonicalize(scripts_container.join(alias.as_str())).await?; if remove { tasks.spawn(async move { diff --git a/src/lockfile.rs b/src/lockfile.rs index 51bfcb8..157790c 100644 --- a/src/lockfile.rs +++ b/src/lockfile.rs @@ -41,7 +41,7 @@ pub mod old { manifest::{ overrides::OverrideKey, target::{Target, TargetKind}, - DependencyType, + Alias, DependencyType, }, names::{PackageName, PackageNames}, source::{ @@ -60,10 +60,10 @@ pub mod old { pub struct DependencyGraphNodeOld { /// The alias, specifier, and original (as in the manifest) type for the dependency, if it is a direct dependency (i.e. used by the current project) #[serde(default, skip_serializing_if = "Option::is_none")] - pub direct: Option<(String, DependencySpecifiers, DependencyType)>, + pub direct: Option<(Alias, DependencySpecifiers, DependencyType)>, /// The dependencies of the package #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub dependencies: BTreeMap, + pub dependencies: BTreeMap, /// The resolved (transformed, for example Peer -> Standard) type of the dependency pub resolved_ty: DependencyType, /// Whether the resolved type should be Peer if this isn't depended on diff --git a/src/main.rs b/src/main.rs index 984147c..f6bf023 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,16 @@ #[cfg(feature = "version-management")] -use crate::cli::version::{check_for_updates, get_or_download_version, TagInfo}; +use crate::cli::version::{check_for_updates, current_version, get_or_download_engine}; use crate::cli::{auth::get_tokens, display_err, home_dir, HOME_DIR}; use anyhow::Context; use clap::{builder::styling::AnsiColor, Parser}; use fs_err::tokio as fs; use indicatif::MultiProgress; -use pesde::{find_roots, AuthConfig, Project}; +use pesde::{engine::EngineKind, find_roots, AuthConfig, Project}; +use semver::VersionReq; use std::{ io, path::{Path, PathBuf}, + str::FromStr, sync::Mutex, }; use tempfile::NamedTempFile; @@ -135,27 +137,39 @@ impl<'a> MakeWriter<'a> for IndicatifWriter { async fn run() -> anyhow::Result<()> { let cwd = std::env::current_dir().expect("failed to get current working directory"); + // Unix doesn't return the symlinked path, so we need to get it from the 0 argument + #[cfg(unix)] + let current_exe = PathBuf::from(std::env::args_os().next().expect("argument 0 not set")); + #[cfg(not(unix))] + let current_exe = std::env::current_exe().expect("failed to get current executable path"); + let exe_name = current_exe + .file_stem() + .unwrap() + .to_str() + .expect("exe name is not valid utf-8"); + let exe_name_engine = EngineKind::from_str(exe_name); #[cfg(windows)] 'scripts: { - let exe = std::env::current_exe().expect("failed to get current executable path"); - if exe.parent().is_some_and(|parent| { - parent.file_name().is_some_and(|parent| parent != "bin") - || parent - .parent() - .and_then(|parent| parent.file_name()) - .is_some_and(|parent| parent != HOME_DIR) - }) { + // if we're an engine, we don't want to run any scripts + if exe_name_engine.is_ok() { break 'scripts; } - let exe_name = exe.file_name().unwrap().to_string_lossy(); - let exe_name = exe_name - .strip_suffix(std::env::consts::EXE_SUFFIX) - .unwrap_or(&exe_name); + if let Some(bin_folder) = current_exe.parent() { + // we're not in {path}/bin/{exe} + if bin_folder.file_name().is_some_and(|parent| parent != "bin") { + break 'scripts; + } - if exe_name == env!("CARGO_BIN_NAME") { - break 'scripts; + // we're not in {path}/.pesde/bin/{exe} + if bin_folder + .parent() + .and_then(|home_folder| home_folder.file_name()) + .is_some_and(|home_folder| home_folder != HOME_DIR) + { + break 'scripts; + } } // the bin script will search for the project root itself, so we do that to ensure @@ -164,9 +178,11 @@ async fn run() -> anyhow::Result<()> { let status = std::process::Command::new("lune") .arg("run") .arg( - exe.parent() - .map(|p| p.join(".impl").join(exe.file_name().unwrap())) - .unwrap_or(exe) + current_exe + .parent() + .unwrap_or(¤t_exe) + .join(".impl") + .join(current_exe.file_name().unwrap()) .with_extension("luau"), ) .arg("--") @@ -265,34 +281,47 @@ async fn run() -> anyhow::Result<()> { }; #[cfg(feature = "version-management")] - { - let target_version = project + 'engines: { + let Ok(engine) = exe_name_engine else { + break 'engines; + }; + + let req = project .deser_manifest() .await .ok() - .and_then(|manifest| manifest.pesde_version); + .and_then(|mut manifest| manifest.engines.remove(&engine)); - let exe_path = if let Some(version) = target_version { - get_or_download_version(&reqwest, TagInfo::Incomplete(version), false).await? - } else { - None - }; - - if let Some(exe_path) = exe_path { - let status = std::process::Command::new(exe_path) - .args(std::env::args_os().skip(1)) - .status() - .expect("failed to run new version"); - - std::process::exit(status.code().unwrap()); + if engine == EngineKind::Pesde { + match &req { + // we're already running a compatible version + Some(req) if req.matches(¤t_version()) => break 'engines, + // the user has not requested a specific version, so we'll just use the current one + None => break 'engines, + _ => (), + } } - display_err( - check_for_updates(&reqwest).await, - " while checking for updates", - ); + let exe_path = + get_or_download_engine(&reqwest, engine, req.unwrap_or(VersionReq::STAR)).await?; + if exe_path == current_exe { + anyhow::bail!("engine linker executed by itself") + } + + let status = std::process::Command::new(exe_path) + .args(std::env::args_os().skip(1)) + .status() + .expect("failed to run new version"); + + std::process::exit(status.code().unwrap()); } + #[cfg(feature = "version-management")] + display_err( + check_for_updates(&reqwest).await, + " while checking for updates", + ); + let cli = Cli::parse(); cli.subcommand.run(project, reqwest).await diff --git a/src/manifest/mod.rs b/src/manifest/mod.rs index 1e6a671..192f1d2 100644 --- a/src/manifest/mod.rs +++ b/src/manifest/mod.rs @@ -1,4 +1,5 @@ use crate::{ + engine::EngineKind, manifest::{ overrides::{OverrideKey, OverrideSpecifier}, target::Target, @@ -7,9 +8,14 @@ use crate::{ source::specifiers::DependencySpecifiers, }; use relative_path::RelativePathBuf; -use semver::Version; +use semver::{Version, VersionReq}; use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, HashMap}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use std::{ + collections::{BTreeMap, HashMap}, + fmt::Display, + str::FromStr, +}; use tracing::instrument; /// Overrides @@ -85,31 +91,88 @@ pub struct Manifest { crate::names::PackageNames, BTreeMap, >, - #[serde(default, skip_serializing)] - /// Which version of the pesde CLI this package uses - pub pesde_version: Option, /// A list of globs pointing to workspace members' directories #[serde(default, skip_serializing_if = "Vec::is_empty")] pub workspace_members: Vec, /// The Roblox place of this project #[serde(default, skip_serializing)] pub place: BTreeMap, + /// The engines this package supports + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + #[cfg_attr(feature = "schema", schemars(with = "BTreeMap"))] + pub engines: BTreeMap, /// The standard dependencies of the package #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub dependencies: BTreeMap, + pub dependencies: BTreeMap, /// The peer dependencies of the package #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub peer_dependencies: BTreeMap, + pub peer_dependencies: BTreeMap, /// The dev dependencies of the package #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub dev_dependencies: BTreeMap, + pub dev_dependencies: BTreeMap, /// The user-defined fields of the package #[cfg_attr(feature = "schema", schemars(skip))] #[serde(flatten)] pub user_defined_fields: HashMap, } +/// An alias of a dependency +#[derive( + SerializeDisplay, DeserializeFromStr, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, +)] +pub struct Alias(String); + +impl Display for Alias { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.pad(&self.0) + } +} + +impl FromStr for Alias { + type Err = errors::AliasFromStr; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Err(errors::AliasFromStr::Empty); + } + + if !s + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + return Err(errors::AliasFromStr::InvalidCharacters(s.to_string())); + } + + if EngineKind::from_str(s).is_ok() { + return Err(errors::AliasFromStr::EngineName(s.to_string())); + } + + Ok(Self(s.to_string())) + } +} + +#[cfg(feature = "schema")] +impl schemars::JsonSchema for Alias { + fn schema_name() -> std::borrow::Cow<'static, str> { + "Alias".into() + } + + fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + schemars::json_schema!({ + "type": "string", + "pattern": r#"^[a-zA-Z0-9_-]+$"#, + }) + } +} + +impl Alias { + /// Get the alias as a string + pub fn as_str(&self) -> &str { + &self.0 + } +} + /// A dependency type #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[serde(rename_all = "snake_case")] @@ -127,10 +190,8 @@ impl Manifest { #[instrument(skip(self), ret(level = "trace"), level = "debug")] pub fn all_dependencies( &self, - ) -> Result< - BTreeMap, - errors::AllDependenciesError, - > { + ) -> Result, errors::AllDependenciesError> + { let mut all_deps = BTreeMap::new(); for (deps, ty) in [ @@ -151,14 +212,32 @@ impl Manifest { /// Errors that can occur when interacting with manifests pub mod errors { + use crate::manifest::Alias; use thiserror::Error; + /// Errors that can occur when parsing an alias from a string + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum AliasFromStr { + /// The alias is empty + #[error("the alias is empty")] + Empty, + + /// The alias contains characters outside a-z, A-Z, 0-9, -, and _ + #[error("alias `{0}` contains characters outside a-z, A-Z, 0-9, -, and _")] + InvalidCharacters(String), + + /// The alias is an engine name + #[error("alias `{0}` is an engine name")] + EngineName(String), + } + /// Errors that can occur when trying to get all dependencies from a manifest #[derive(Debug, Error)] #[non_exhaustive] pub enum AllDependenciesError { /// Another specifier is already using the alias #[error("another specifier is already using the alias {0}")] - AliasConflict(String), + AliasConflict(Alias), } } diff --git a/src/manifest/overrides.rs b/src/manifest/overrides.rs index 764fa88..807f008 100644 --- a/src/manifest/overrides.rs +++ b/src/manifest/overrides.rs @@ -1,4 +1,4 @@ -use crate::source::specifiers::DependencySpecifiers; +use crate::{manifest::Alias, source::specifiers::DependencySpecifiers}; use serde::{Deserialize, Serialize}; use serde_with::{DeserializeFromStr, SerializeDisplay}; use std::{ @@ -10,7 +10,7 @@ use std::{ #[derive( Debug, DeserializeFromStr, SerializeDisplay, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, )] -pub struct OverrideKey(pub Vec>); +pub struct OverrideKey(pub Vec>); impl FromStr for OverrideKey { type Err = errors::OverrideKeyFromStr; @@ -18,8 +18,13 @@ impl FromStr for OverrideKey { fn from_str(s: &str) -> Result { let overrides = s .split(',') - .map(|overrides| overrides.split('>').map(ToString::to_string).collect()) - .collect::>>(); + .map(|overrides| { + overrides + .split('>') + .map(Alias::from_str) + .collect::>() + }) + .collect::>, _>>()?; if overrides.is_empty() { return Err(errors::OverrideKeyFromStr::Empty); @@ -38,7 +43,7 @@ impl schemars::JsonSchema for OverrideKey { fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { schemars::json_schema!({ "type": "string", - "pattern": r#"^([a-zA-Z]+(>[a-zA-Z]+)+)(,([a-zA-Z]+(>[a-zA-Z]+)+))*$"#, + "pattern": r#"^(?:[a-zA-Z0-9_-]+>[a-zA-Z0-9_-]+(?:>[a-zA-Z0-9_-]+)*)(?:,(?:[a-zA-Z0-9_-]+>[a-zA-Z0-9_-]+(?:>[a-zA-Z0-9_-]+)*))*$"#, }) } } @@ -53,7 +58,7 @@ impl Display for OverrideKey { .map(|overrides| { overrides .iter() - .map(String::as_str) + .map(Alias::as_str) .collect::>() .join(">") }) @@ -71,7 +76,7 @@ pub enum OverrideSpecifier { /// A specifier for a dependency Specifier(DependencySpecifiers), /// An alias for a dependency the current project depends on - Alias(String), + Alias(Alias), } /// Errors that can occur when interacting with override keys @@ -85,5 +90,9 @@ pub mod errors { /// The override key is empty #[error("empty override key")] Empty, + + /// An alias in the override key is invalid + #[error("invalid alias in override key")] + InvalidAlias(#[from] crate::manifest::errors::AliasFromStr), } } diff --git a/src/patches.rs b/src/patches.rs index df4d8b2..7aab8ac 100644 --- a/src/patches.rs +++ b/src/patches.rs @@ -84,7 +84,7 @@ impl Project { reporter: Arc, ) -> Result<(), errors::ApplyPatchesError> where - Reporter: for<'a> PatchesReporter<'a> + Send + Sync + 'static, + Reporter: PatchesReporter + Send + Sync + 'static, { let manifest = self.deser_manifest().await?; @@ -112,7 +112,7 @@ impl Project { async move { tracing::debug!("applying patch"); - let progress_reporter = reporter.report_patch(&package_id.to_string()); + let progress_reporter = reporter.report_patch(package_id.to_string()); let patch = fs::read(&patch_path) .await diff --git a/src/reporters.rs b/src/reporters.rs index d34dc52..1305f4e 100644 --- a/src/reporters.rs +++ b/src/reporters.rs @@ -9,18 +9,23 @@ #![allow(unused_variables)] +use async_stream::stream; +use futures::StreamExt; +use std::sync::Arc; +use tokio::io::AsyncBufRead; + /// Reports downloads. -pub trait DownloadsReporter<'a>: Send + Sync { +pub trait DownloadsReporter: Send + Sync { /// The [`DownloadProgressReporter`] type associated with this reporter. - type DownloadProgressReporter: DownloadProgressReporter + 'a; + type DownloadProgressReporter: DownloadProgressReporter + 'static; /// Starts a new download. - fn report_download<'b>(&'a self, name: &'b str) -> Self::DownloadProgressReporter; + fn report_download(self: Arc, name: String) -> Self::DownloadProgressReporter; } -impl DownloadsReporter<'_> for () { +impl DownloadsReporter for () { type DownloadProgressReporter = (); - fn report_download(&self, name: &str) -> Self::DownloadProgressReporter {} + fn report_download(self: Arc, name: String) -> Self::DownloadProgressReporter {} } /// Reports the progress of a single download. @@ -41,17 +46,17 @@ pub trait DownloadProgressReporter: Send + Sync { impl DownloadProgressReporter for () {} /// Reports the progress of applying patches. -pub trait PatchesReporter<'a>: Send + Sync { +pub trait PatchesReporter: Send + Sync { /// The [`PatchProgressReporter`] type associated with this reporter. - type PatchProgressReporter: PatchProgressReporter + 'a; + type PatchProgressReporter: PatchProgressReporter + 'static; /// Starts a new patch. - fn report_patch<'b>(&'a self, name: &'b str) -> Self::PatchProgressReporter; + fn report_patch(self: Arc, name: String) -> Self::PatchProgressReporter; } -impl PatchesReporter<'_> for () { +impl PatchesReporter for () { type PatchProgressReporter = (); - fn report_patch(&self, name: &str) -> Self::PatchProgressReporter {} + fn report_patch(self: Arc, name: String) -> Self::PatchProgressReporter {} } /// Reports the progress of a single patch. @@ -61,3 +66,32 @@ pub trait PatchProgressReporter: Send + Sync { } impl PatchProgressReporter for () {} + +pub(crate) fn response_to_async_read( + response: reqwest::Response, + reporter: Arc, +) -> impl AsyncBufRead { + let total_len = response.content_length().unwrap_or(0); + reporter.report_progress(total_len, 0); + + let mut bytes_downloaded = 0; + let mut stream = response.bytes_stream(); + let bytes = stream!({ + while let Some(chunk) = stream.next().await { + let chunk = match chunk { + Ok(chunk) => chunk, + Err(err) => { + yield Err(std::io::Error::new(std::io::ErrorKind::Other, err)); + continue; + } + }; + bytes_downloaded += chunk.len() as u64; + reporter.report_progress(total_len, bytes_downloaded); + yield Ok(chunk); + } + + reporter.report_done(); + }); + + tokio_util::io::StreamReader::new(bytes) +} diff --git a/src/resolver.rs b/src/resolver.rs index c28fcd7..5fa219c 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -1,6 +1,6 @@ use crate::{ graph::{DependencyGraph, DependencyGraphNode}, - manifest::{overrides::OverrideSpecifier, DependencyType}, + manifest::{overrides::OverrideSpecifier, Alias, DependencyType}, source::{ ids::PackageId, pesde::PesdePackageSource, @@ -92,12 +92,12 @@ impl Project { let Some(alias) = all_specifiers.remove(&(specifier.clone(), *source_ty)) else { tracing::debug!( - "dependency {package_id} (old alias {old_alias}) from old dependency graph is no longer in the manifest", - ); + "dependency {package_id} (old alias {old_alias}) from old dependency graph is no longer in the manifest", + ); continue; }; - let span = tracing::info_span!("resolve from old graph", alias); + let span = tracing::info_span!("resolve from old graph", alias = alias.as_str()); let _guard = span.enter(); tracing::debug!("resolved {package_id} from old dependency graph"); @@ -121,6 +121,7 @@ impl Project { let inner_span = tracing::info_span!("resolve dependency", path = path.join(">")); let _inner_guard = inner_span.enter(); + if let Some(dep_node) = previous_graph.get(dep_id) { tracing::debug!("resolved sub-dependency {dep_id}"); insert_node(&mut graph, dep_id, dep_node.clone(), false); @@ -262,7 +263,7 @@ impl Project { .get_mut(&dependant_id) .expect("dependant package not found in graph") .dependencies - .insert(package_id.clone(), alias.clone()); + .insert(package_id.clone(), alias.clone()); } let pkg_ref = &resolved[package_id.version_id()]; @@ -339,7 +340,7 @@ impl Project { tracing::debug!( "overridden specifier found for {} ({dependency_spec})", path.iter() - .map(String::as_str) + .map(Alias::as_str) .chain(std::iter::once(dependency_alias.as_str())) .collect::>() .join(">"), @@ -368,7 +369,7 @@ impl Project { Ok(()) } - .instrument(tracing::info_span!("resolve new/changed", path = path.join(">"))) + .instrument(tracing::info_span!("resolve new/changed", path = path.iter().map(Alias::as_str).collect::>().join(">"))) .await?; } @@ -388,6 +389,7 @@ impl Project { /// Errors that can occur when resolving dependencies pub mod errors { + use crate::manifest::Alias; use thiserror::Error; /// Errors that can occur when creating a dependency graph @@ -425,6 +427,6 @@ pub mod errors { /// An alias for an override was not found in the manifest #[error("alias `{0}` not found in manifest")] - AliasNotFound(String), + AliasNotFound(Alias), } } diff --git a/src/source/git/pkg_ref.rs b/src/source/git/pkg_ref.rs index 9db6100..037b0a8 100644 --- a/src/source/git/pkg_ref.rs +++ b/src/source/git/pkg_ref.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use crate::{ - manifest::DependencyType, + manifest::{Alias, DependencyType}, source::{git::GitPackageSource, DependencySpecifiers, PackageRef, PackageSources}, }; @@ -19,12 +19,12 @@ pub struct GitPackageRef { pub tree_id: String, /// The dependencies of the package #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub dependencies: BTreeMap, + pub dependencies: BTreeMap, /// Whether this package uses the new structure pub new_structure: bool, } impl PackageRef for GitPackageRef { - fn dependencies(&self) -> &BTreeMap { + fn dependencies(&self) -> &BTreeMap { &self.dependencies } diff --git a/src/source/path/pkg_ref.rs b/src/source/path/pkg_ref.rs index 40c7ab1..df9491e 100644 --- a/src/source/path/pkg_ref.rs +++ b/src/source/path/pkg_ref.rs @@ -1,5 +1,5 @@ use crate::{ - manifest::DependencyType, + manifest::{Alias, DependencyType}, source::{path::PathPackageSource, DependencySpecifiers, PackageRef, PackageSources}, }; use serde::{Deserialize, Serialize}; @@ -12,10 +12,10 @@ pub struct PathPackageRef { pub path: PathBuf, /// The dependencies of the package #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub dependencies: BTreeMap, + pub dependencies: BTreeMap, } impl PackageRef for PathPackageRef { - fn dependencies(&self) -> &BTreeMap { + fn dependencies(&self) -> &BTreeMap { &self.dependencies } diff --git a/src/source/pesde/mod.rs b/src/source/pesde/mod.rs index ca5bc7b..d04f687 100644 --- a/src/source/pesde/mod.rs +++ b/src/source/pesde/mod.rs @@ -8,15 +8,15 @@ use std::{ hash::Hash, path::PathBuf, }; -use tokio_util::io::StreamReader; use pkg_ref::PesdePackageRef; use specifier::PesdeDependencySpecifier; use crate::{ - manifest::{target::Target, DependencyType}, + engine::EngineKind, + manifest::{target::Target, Alias, DependencyType}, names::{PackageName, PackageNames}, - reporters::DownloadProgressReporter, + reporters::{response_to_async_read, DownloadProgressReporter}, source::{ fs::{store_in_cas, FsEntry, PackageFs}, git_index::{read_file, root_tree, GitBasedSource}, @@ -28,7 +28,8 @@ use crate::{ }; use fs_err::tokio as fs; use futures::StreamExt; -use tokio::task::spawn_blocking; +use semver::VersionReq; +use tokio::{pin, task::spawn_blocking}; use tracing::instrument; /// The pesde package reference @@ -95,23 +96,31 @@ impl PesdePackageSource { .unwrap() } - fn read_index_file( + /// Reads the index file of a package + pub async fn read_index_file( &self, name: &PackageName, project: &Project, ) -> Result, errors::ReadIndexFileError> { - let (scope, name) = name.as_str(); - let repo = gix::open(self.path(project)).map_err(Box::new)?; - let tree = root_tree(&repo).map_err(Box::new)?; - let string = match read_file(&tree, [scope, name]) { - Ok(Some(s)) => s, - Ok(None) => return Ok(None), - Err(e) => { - return Err(errors::ReadIndexFileError::ReadFile(e)); - } - }; + let path = self.path(project); + let name = name.clone(); - toml::from_str(&string).map_err(Into::into) + spawn_blocking(move || { + let (scope, name) = name.as_str(); + let repo = gix::open(&path).map_err(Box::new)?; + let tree = root_tree(&repo).map_err(Box::new)?; + let string = match read_file(&tree, [scope, name]) { + Ok(Some(s)) => s, + Ok(None) => return Ok(None), + Err(e) => { + return Err(errors::ReadIndexFileError::ReadFile(e)); + } + }; + + toml::from_str(&string).map_err(Into::into) + }) + .await + .unwrap() } } @@ -140,16 +149,12 @@ impl PackageSource for PesdePackageSource { .. } = options; - let Some(IndexFile { meta, entries, .. }) = - self.read_index_file(&specifier.name, project)? + let Some(IndexFile { entries, .. }) = + self.read_index_file(&specifier.name, project).await? else { return Err(errors::ResolveError::NotFound(specifier.name.to_string())); }; - if !meta.deprecated.is_empty() { - tracing::warn!("{} is deprecated: {}", specifier.name, meta.deprecated); - } - tracing::debug!("{} has {} possible entries", specifier.name, entries.len()); Ok(( @@ -229,23 +234,8 @@ impl PackageSource for PesdePackageSource { let response = request.send().await?.error_for_status()?; - let total_len = response.content_length().unwrap_or(0); - reporter.report_progress(total_len, 0); - - let mut bytes_downloaded = 0; - let bytes = response - .bytes_stream() - .inspect(|chunk| { - chunk.as_ref().ok().inspect(|chunk| { - bytes_downloaded += chunk.len() as u64; - reporter.report_progress(total_len, bytes_downloaded); - }); - }) - .map(|result| { - result.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) - }); - - let bytes = StreamReader::new(bytes); + let bytes = response_to_async_read(response, reporter.clone()); + pin!(bytes); let mut decoder = async_compression::tokio::bufread::GzipDecoder::new(bytes); let mut archive = tokio_tar::Archive::new(&mut decoder); @@ -297,8 +287,6 @@ impl PackageSource for PesdePackageSource { .await .map_err(errors::DownloadError::WriteIndex)?; - reporter.report_done(); - Ok(fs) } @@ -314,7 +302,8 @@ impl PackageSource for PesdePackageSource { panic!("unexpected package name"); }; - let Some(IndexFile { mut entries, .. }) = self.read_index_file(name, &options.project)? + let Some(IndexFile { mut entries, .. }) = + self.read_index_file(name, &options.project).await? else { return Err(errors::GetTargetError::NotFound(name.to_string())); }; @@ -478,6 +467,9 @@ pub struct IndexFileEntry { /// When this package was published #[serde(default = "chrono::Utc::now")] pub published_at: chrono::DateTime, + /// The engines this package supports + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub engines: BTreeMap, /// The description of this package #[serde(default, skip_serializing_if = "Option::is_none")] @@ -502,7 +494,7 @@ pub struct IndexFileEntry { /// The dependencies of this package #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub dependencies: BTreeMap, + pub dependencies: BTreeMap, } /// The package metadata in the index file diff --git a/src/source/pesde/pkg_ref.rs b/src/source/pesde/pkg_ref.rs index f5e6cf4..9f65921 100644 --- a/src/source/pesde/pkg_ref.rs +++ b/src/source/pesde/pkg_ref.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use crate::{ - manifest::DependencyType, + manifest::{Alias, DependencyType}, source::{pesde::PesdePackageSource, DependencySpecifiers, PackageRef, PackageSources}, }; @@ -18,10 +18,10 @@ pub struct PesdePackageRef { pub index_url: gix::Url, /// The dependencies of the package #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub dependencies: BTreeMap, + pub dependencies: BTreeMap, } impl PackageRef for PesdePackageRef { - fn dependencies(&self) -> &BTreeMap { + fn dependencies(&self) -> &BTreeMap { &self.dependencies } diff --git a/src/source/refs.rs b/src/source/refs.rs index 247b950..5c18cda 100644 --- a/src/source/refs.rs +++ b/src/source/refs.rs @@ -1,5 +1,5 @@ use crate::{ - manifest::DependencyType, + manifest::{Alias, DependencyType}, source::{pesde, specifiers::DependencySpecifiers, traits::PackageRef, PackageSources}, }; use serde::{Deserialize, Serialize}; @@ -35,7 +35,7 @@ impl PackageRefs { } impl PackageRef for PackageRefs { - fn dependencies(&self) -> &BTreeMap { + fn dependencies(&self) -> &BTreeMap { match self { PackageRefs::Pesde(pkg_ref) => pkg_ref.dependencies(), #[cfg(feature = "wally-compat")] diff --git a/src/source/traits.rs b/src/source/traits.rs index 46512b3..4449576 100644 --- a/src/source/traits.rs +++ b/src/source/traits.rs @@ -1,7 +1,7 @@ use crate::{ manifest::{ target::{Target, TargetKind}, - DependencyType, + Alias, DependencyType, }, reporters::DownloadProgressReporter, source::{ids::PackageId, DependencySpecifiers, PackageFs, PackageSources, ResolveResult}, @@ -21,7 +21,7 @@ pub trait DependencySpecifier: Debug + Display {} /// A reference to a package pub trait PackageRef: Debug { /// The dependencies of this package - fn dependencies(&self) -> &BTreeMap; + fn dependencies(&self) -> &BTreeMap; /// Whether to use the new structure (`packages` folders inside the package's content folder) or the old structure (Wally-style, with linker files in the parent of the folder containing the package's contents) fn use_new_structure(&self) -> bool; /// The source of this package diff --git a/src/source/wally/manifest.rs b/src/source/wally/manifest.rs index 1bfe188..85303f7 100644 --- a/src/source/wally/manifest.rs +++ b/src/source/wally/manifest.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use crate::{ - manifest::{errors, DependencyType}, + manifest::{errors, Alias, DependencyType}, names::wally::WallyPackageName, source::{specifiers::DependencySpecifiers, wally::specifier::WallyDependencySpecifier}, }; @@ -28,9 +28,9 @@ pub struct WallyPackage { pub fn deserialize_specifiers<'de, D: Deserializer<'de>>( deserializer: D, -) -> Result, D::Error> { +) -> Result, D::Error> { // specifier is in form of `name@version_req` - BTreeMap::::deserialize(deserializer)? + BTreeMap::::deserialize(deserializer)? .into_iter() .map(|(k, v)| { let (name, version) = v.split_once('@').ok_or_else(|| { @@ -54,11 +54,11 @@ pub fn deserialize_specifiers<'de, D: Deserializer<'de>>( pub struct WallyManifest { pub package: WallyPackage, #[serde(default, deserialize_with = "deserialize_specifiers")] - pub dependencies: BTreeMap, + pub dependencies: BTreeMap, #[serde(default, deserialize_with = "deserialize_specifiers")] - pub server_dependencies: BTreeMap, + pub server_dependencies: BTreeMap, #[serde(default, deserialize_with = "deserialize_specifiers")] - pub dev_dependencies: BTreeMap, + pub dev_dependencies: BTreeMap, } impl WallyManifest { @@ -66,10 +66,8 @@ impl WallyManifest { #[instrument(skip(self), ret(level = "trace"), level = "debug")] pub fn all_dependencies( &self, - ) -> Result< - BTreeMap, - errors::AllDependenciesError, - > { + ) -> Result, errors::AllDependenciesError> + { let mut all_deps = BTreeMap::new(); for (deps, ty) in [ diff --git a/src/source/wally/mod.rs b/src/source/wally/mod.rs index 49d5e4e..36b13c4 100644 --- a/src/source/wally/mod.rs +++ b/src/source/wally/mod.rs @@ -1,7 +1,7 @@ use crate::{ manifest::target::{Target, TargetKind}, names::PackageNames, - reporters::DownloadProgressReporter, + reporters::{response_to_async_read, DownloadProgressReporter}, source::{ fs::{store_in_cas, FsEntry, PackageFs}, git_index::{read_file, root_tree, GitBasedSource}, @@ -20,14 +20,13 @@ use crate::{ Project, }; use fs_err::tokio as fs; -use futures::StreamExt; use gix::Url; use relative_path::RelativePathBuf; use reqwest::header::AUTHORIZATION; use serde::Deserialize; use std::{collections::BTreeMap, path::PathBuf}; -use tokio::{io::AsyncReadExt, task::spawn_blocking}; -use tokio_util::{compat::FuturesAsyncReadCompatExt, io::StreamReader}; +use tokio::{io::AsyncReadExt, pin, task::spawn_blocking}; +use tokio_util::compat::FuturesAsyncReadCompatExt; use tracing::instrument; pub(crate) mod compat_util; @@ -268,22 +267,9 @@ impl PackageSource for WallyPackageSource { let response = request.send().await?.error_for_status()?; let total_len = response.content_length().unwrap_or(0); - reporter.report_progress(total_len, 0); + let bytes = response_to_async_read(response, reporter.clone()); + pin!(bytes); - let mut bytes_downloaded = 0; - let bytes = response - .bytes_stream() - .inspect(|chunk| { - chunk.as_ref().ok().inspect(|chunk| { - bytes_downloaded += chunk.len() as u64; - reporter.report_progress(total_len, bytes_downloaded); - }); - }) - .map(|result| { - result.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) - }); - - let mut bytes = StreamReader::new(bytes); let mut buf = Vec::with_capacity(total_len as usize); bytes.read_to_end(&mut buf).await?; @@ -335,8 +321,6 @@ impl PackageSource for WallyPackageSource { .await .map_err(errors::DownloadError::WriteIndex)?; - reporter.report_done(); - Ok(fs) } diff --git a/src/source/wally/pkg_ref.rs b/src/source/wally/pkg_ref.rs index f7f9769..3d157d3 100644 --- a/src/source/wally/pkg_ref.rs +++ b/src/source/wally/pkg_ref.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use crate::{ - manifest::DependencyType, + manifest::{Alias, DependencyType}, source::{wally::WallyPackageSource, DependencySpecifiers, PackageRef, PackageSources}, }; @@ -18,10 +18,10 @@ pub struct WallyPackageRef { pub index_url: gix::Url, /// The dependencies of the package #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub dependencies: BTreeMap, + pub dependencies: BTreeMap, } impl PackageRef for WallyPackageRef { - fn dependencies(&self) -> &BTreeMap { + fn dependencies(&self) -> &BTreeMap { &self.dependencies } diff --git a/src/source/workspace/pkg_ref.rs b/src/source/workspace/pkg_ref.rs index aa2beae..c6d2458 100644 --- a/src/source/workspace/pkg_ref.rs +++ b/src/source/workspace/pkg_ref.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use crate::{ - manifest::DependencyType, + manifest::{Alias, DependencyType}, source::{workspace::WorkspacePackageSource, DependencySpecifiers, PackageRef, PackageSources}, }; @@ -14,10 +14,10 @@ pub struct WorkspacePackageRef { pub path: RelativePathBuf, /// The dependencies of the package #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub dependencies: BTreeMap, + pub dependencies: BTreeMap, } impl PackageRef for WorkspacePackageRef { - fn dependencies(&self) -> &BTreeMap { + fn dependencies(&self) -> &BTreeMap { &self.dependencies } diff --git a/src/util.rs b/src/util.rs index c80ac09..5cf4e20 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,5 +1,6 @@ use crate::AuthConfig; use gix::bstr::BStr; +use semver::Version; use serde::{Deserialize, Deserializer, Serializer}; use sha2::{Digest, Sha256}; use std::collections::{BTreeMap, HashSet}; @@ -88,3 +89,9 @@ pub fn hash>(struc: S) -> String { pub fn is_default(t: &T) -> bool { t == &T::default() } + +pub fn no_build_metadata(version: &Version) -> Version { + let mut version = version.clone(); + version.build = semver::BuildMetadata::EMPTY; + version +}