diff --git a/docs/src/content/docs/reference/cli.mdx b/docs/src/content/docs/reference/cli.mdx index 8874d30..9c0aa95 100644 --- a/docs/src/content/docs/reference/cli.mdx +++ b/docs/src/content/docs/reference/cli.mdx @@ -110,6 +110,7 @@ Installs dependencies for the current project. - `--locked`: Whether to error if the lockfile is out of date. - `--prod`: Whether to not linking dev dependencies. +- `--dev`: Whether to only link dev dependencies. - `--network-concurrency `: The number of concurrent network requests to make at most. Defaults to 16. - `--force`: Whether to force reinstall all packages even if they are already diff --git a/src/cli/commands/execute.rs b/src/cli/commands/execute.rs index 24efacd..fdade01 100644 --- a/src/cli/commands/execute.rs +++ b/src/cli/commands/execute.rs @@ -9,7 +9,7 @@ use console::style; use fs_err::tokio as fs; use indicatif::MultiProgress; use pesde::{ - download_and_link::DownloadAndLinkOptions, + download_and_link::{DownloadAndLinkOptions, InstallDependenciesMode}, linking::generator::generate_bin_linking_module, manifest::target::TargetKind, names::{PackageName, PackageNames}, @@ -178,7 +178,7 @@ impl ExecuteCommand { DownloadAndLinkOptions::, ()>::new(reqwest) .reporter(reporter) .refreshed_sources(refreshed_sources) - .prod(true), + .install_dependencies_mode(InstallDependenciesMode::Prod), ) .await .context("failed to download and link dependencies")?; diff --git a/src/cli/commands/install.rs b/src/cli/commands/install.rs index c307b35..c811fb3 100644 --- a/src/cli/commands/install.rs +++ b/src/cli/commands/install.rs @@ -3,7 +3,7 @@ use crate::cli::{ run_on_workspace_members, }; use clap::Args; -use pesde::Project; +use pesde::{download_and_link::InstallDependenciesMode, Project}; use std::num::NonZeroUsize; #[derive(Debug, Args, Copy, Clone)] @@ -16,6 +16,10 @@ pub struct InstallCommand { #[arg(long)] prod: bool, + /// Whether to only install dev dependencies + #[arg(long)] + dev: bool, + /// The maximum number of concurrent network requests #[arg(long, default_value = "16")] network_concurrency: NonZeroUsize, @@ -30,9 +34,16 @@ pub struct InstallCommand { struct CallbackError(#[from] anyhow::Error); impl InstallCommand { pub async fn run(self, project: Project, reqwest: reqwest::Client) -> anyhow::Result<()> { + let install_dependencies_mode = match (self.prod, self.dev) { + (true, true) => anyhow::bail!("cannot have both prod and dev flags enabled"), + (true, false) => InstallDependenciesMode::Prod, + (false, true) => InstallDependenciesMode::Dev, + (false, false) => InstallDependenciesMode::All, + }; + let options = InstallOptions { locked: self.locked, - prod: self.prod, + install_dependencies_mode, write: true, network_concurrency: self.network_concurrency, use_lockfile: true, diff --git a/src/cli/commands/update.rs b/src/cli/commands/update.rs index ffb065c..7bf9473 100644 --- a/src/cli/commands/update.rs +++ b/src/cli/commands/update.rs @@ -3,7 +3,7 @@ use crate::cli::{ run_on_workspace_members, }; use clap::Args; -use pesde::Project; +use pesde::{download_and_link::InstallDependenciesMode, Project}; use std::num::NonZeroUsize; #[derive(Debug, Args, Copy, Clone)] @@ -25,7 +25,7 @@ impl UpdateCommand { pub async fn run(self, project: Project, reqwest: reqwest::Client) -> anyhow::Result<()> { let options = InstallOptions { locked: false, - prod: false, + install_dependencies_mode: InstallDependenciesMode::All, write: !self.no_install, network_concurrency: self.network_concurrency, use_lockfile: false, diff --git a/src/cli/install.rs b/src/cli/install.rs index e7b290f..6c9a6cc 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -10,7 +10,7 @@ use anyhow::Context as _; use console::style; use fs_err::tokio as fs; use pesde::{ - download_and_link::{DownloadAndLinkHooks, DownloadAndLinkOptions}, + download_and_link::{DownloadAndLinkHooks, DownloadAndLinkOptions, InstallDependenciesMode}, engine::EngineKind, graph::{DependencyGraph, DependencyGraphWithTarget}, lockfile::Lockfile, @@ -123,7 +123,7 @@ impl DownloadAndLinkHooks for InstallHooks { #[derive(Debug, Clone, Copy)] pub struct InstallOptions { pub locked: bool, - pub prod: bool, + pub install_dependencies_mode: InstallDependenciesMode, pub write: bool, pub use_lockfile: bool, pub network_concurrency: NonZeroUsize, @@ -285,7 +285,7 @@ pub async fn install( .reporter(reporter) .hooks(hooks) .refreshed_sources(refreshed_sources.clone()) - .prod(options.prod) + .install_dependencies_mode(options.install_dependencies_mode) .network_concurrency(options.network_concurrency) .force(options.force || has_irrecoverable_changes), ) diff --git a/src/download_and_link.rs b/src/download_and_link.rs index bfe6ea3..e2d903d 100644 --- a/src/download_and_link.rs +++ b/src/download_and_link.rs @@ -15,9 +15,9 @@ use crate::{ }; use fs_err::tokio as fs; use futures::TryStreamExt as _; + use std::{ - borrow::Cow, - collections::HashMap, + collections::{HashMap, VecDeque}, convert::Infallible, future::{self, Future}, num::NonZeroUsize, @@ -65,6 +65,28 @@ impl DownloadAndLinkHooks for () { type Error = Infallible; } +/// Options for which dependencies to install. +#[derive(Debug, Clone, Copy)] +pub enum InstallDependenciesMode { + /// Install all dependencies + All, + /// Install all dependencies, then remove [DependencyType::Dev] dependencies + Prod, + /// Only install dependencies which are [DependencyType::Dev] + Dev, +} + +impl InstallDependenciesMode { + fn fits(self, dep_ty: DependencyType) -> bool { + match (self, dep_ty) { + (InstallDependenciesMode::Prod, DependencyType::Dev) => false, + (InstallDependenciesMode::Dev, dep_ty) => dep_ty == DependencyType::Dev, + + _ => true, + } + } +} + /// Options for downloading and linking. #[derive(Debug)] pub struct DownloadAndLinkOptions { @@ -76,8 +98,8 @@ pub struct DownloadAndLinkOptions { pub hooks: Option>, /// The refreshed sources. pub refreshed_sources: RefreshedSources, - /// Whether to skip dev dependencies. - pub prod: bool, + /// Which dependencies to install. + pub install_dependencies_mode: InstallDependenciesMode, /// The max number of concurrent network requests. pub network_concurrency: NonZeroUsize, /// Whether to re-install all dependencies even if they are already installed @@ -97,7 +119,7 @@ where reporter: None, hooks: None, refreshed_sources: Default::default(), - prod: false, + install_dependencies_mode: InstallDependenciesMode::All, network_concurrency: NonZeroUsize::new(16).unwrap(), force: false, } @@ -124,10 +146,13 @@ where self } - /// Sets whether to skip dev dependencies. + /// Sets which dependencies to install #[must_use] - pub fn prod(mut self, prod: bool) -> Self { - self.prod = prod; + pub fn install_dependencies_mode( + mut self, + install_dependencies: InstallDependenciesMode, + ) -> Self { + self.install_dependencies_mode = install_dependencies; self } @@ -153,7 +178,7 @@ impl Clone for DownloadAndLinkOptions { reporter: self.reporter.clone(), hooks: self.hooks.clone(), refreshed_sources: self.refreshed_sources.clone(), - prod: self.prod, + install_dependencies_mode: self.install_dependencies_mode, network_concurrency: self.network_concurrency, force: self.force, } @@ -162,7 +187,7 @@ impl Clone for DownloadAndLinkOptions { impl Project { /// Downloads a graph of dependencies and links them in the correct order - #[instrument(skip_all, fields(prod = options.prod), level = "debug")] + #[instrument(skip_all, fields(install_dependencies = debug(options.install_dependencies_mode)), level = "debug")] pub async fn download_and_link( &self, graph: &DependencyGraph, @@ -177,7 +202,7 @@ impl Project { reporter, hooks, refreshed_sources, - prod, + install_dependencies_mode, network_concurrency, force, } = options; @@ -219,16 +244,41 @@ impl Project { download_graph_options = download_graph_options.reporter(reporter); } + let correct_deps = if matches!(install_dependencies_mode, InstallDependenciesMode::All) + { + graph.clone() + } else { + let mut queue = graph + .iter() + .filter(|(_, node)| { + node.direct.is_some() && install_dependencies_mode.fits(node.resolved_ty) + }) + .collect::>(); + + let mut correct_deps = DependencyGraph::new(); + while let Some((id, node)) = queue.pop_front() { + if correct_deps.insert(id.clone(), node.clone()).is_some() { + // prevent an infinite loop with recursive dependencies + continue; + } + + node.dependencies + .keys() + .filter_map(|id| graph.get(id).map(|node| (id, node))) + .for_each(|x| queue.push_back(x)); + } + + correct_deps + }; + let mut downloaded_graph = DependencyGraph::new(); let graph_to_download = if force { - Cow::Borrowed(graph) + correct_deps } else { - let mut tasks = graph - .iter() + let mut tasks = correct_deps + .into_iter() .map(|(id, node)| { - let id = id.clone(); - let node = node.clone(); let container_folder = node.container_folder_from_project(&id, self, manifest.target.kind()); @@ -249,7 +299,7 @@ impl Project { graph_to_download.insert(id, node); } - Cow::Owned(graph_to_download) + graph_to_download }; let downloaded = self @@ -418,11 +468,7 @@ impl Project { .map_err(errors::DownloadAndLinkError::Hook)?; } - if prod { - graph.retain(|_, node| node.node.resolved_ty != DependencyType::Dev); - } - - if prod || !force { + if matches!(install_dependencies_mode, InstallDependenciesMode::Prod) || !force { self.remove_unused(&graph).await?; }