From 2d534a534d937efc4f51b855aa0ff2f7904bf52c Mon Sep 17 00:00:00 2001 From: daimond113 <72147841+daimond113@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:22:14 +0100 Subject: [PATCH] feat(engines): print incompatibility warning for dependencies Adds a warning message when a dependency depends on an incompatible engine. --- registry/src/endpoints/publish_version.rs | 1 + src/cli/install.rs | 122 +++++++++++++++++++--- src/cli/version.rs | 41 +++++--- src/source/pesde/mod.rs | 42 +++++--- 4 files changed, 164 insertions(+), 42 deletions(-) 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/src/cli/install.rs b/src/cli/install.rs index 98c0305..c0ce8df 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,10 +9,19 @@ 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, DependencyType, Manifest}, + names::PackageNames, + source::{pesde::PesdePackageSource, refs::PackageRefs}, + 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; @@ -196,7 +198,8 @@ 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)); @@ -300,9 +303,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)) + .last() + .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"); @@ -325,7 +423,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/version.rs b/src/cli/version.rs index 3d5f59f..066136f 100644 --- a/src/cli/version.rs +++ b/src/cli/version.rs @@ -142,25 +142,20 @@ pub async fn check_for_updates(reqwest: &reqwest::Client) -> anyhow::Result<()> Ok(()) } -#[instrument(skip(reqwest), level = "trace")] -pub async fn get_or_download_engine( - reqwest: &reqwest::Client, - engine: EngineKind, - req: VersionReq, -) -> anyhow::Result { +const ENGINES_DIR: &str = "engines"; + +#[instrument(level = "trace")] +pub async fn get_installed_versions(engine: EngineKind) -> anyhow::Result> { let source = engine.source(); - - let path = home_dir()?.join("engines").join(source.directory()); - fs::create_dir_all(&path) - .await - .context("failed to create engines directory")?; - - let mut read_dir = fs::read_dir(&path) - .await - .context("failed to read engines directory")?; - + let path = home_dir()?.join(ENGINES_DIR).join(source.directory()); let mut installed_versions = BTreeSet::new(); + 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"), + }; + while let Some(entry) = read_dir.next_entry().await? { let path = entry.path(); @@ -173,6 +168,20 @@ pub async fn get_or_download_engine( } } + Ok(installed_versions) +} + +#[instrument(skip(reqwest), level = "trace")] +pub async fn get_or_download_engine( + reqwest: &reqwest::Client, + 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)) diff --git a/src/source/pesde/mod.rs b/src/source/pesde/mod.rs index fe0ebe3..7a48f6a 100644 --- a/src/source/pesde/mod.rs +++ b/src/source/pesde/mod.rs @@ -13,6 +13,7 @@ use pkg_ref::PesdePackageRef; use specifier::PesdeDependencySpecifier; use crate::{ + engine::EngineKind, manifest::{target::Target, DependencyType}, names::{PackageName, PackageNames}, reporters::{response_to_async_read, DownloadProgressReporter}, @@ -27,6 +28,7 @@ use crate::{ }; use fs_err::tokio as fs; use futures::StreamExt; +use semver::VersionReq; use tokio::{pin, task::spawn_blocking}; use tracing::instrument; @@ -94,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,7 +150,7 @@ impl PackageSource for PesdePackageSource { } = options; let Some(IndexFile { meta, entries, .. }) = - self.read_index_file(&specifier.name, project)? + self.read_index_file(&specifier.name, project).await? else { return Err(errors::ResolveError::NotFound(specifier.name.to_string())); }; @@ -296,7 +306,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())); }; @@ -460,6 +471,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")]