Repository: LPGhatguy/aftman (language: rust)

Issue:
`aftman install --skip-untrusted`
It would be useful for automated workflows to be able to install only previously trusted tools, so that a new tool being added to a checked-in `aftman.toml` doesn't break any existing tooling.


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 480a7ca..14db2bd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -21,6 +21,7 @@ dependencies = [
  "env_logger",
  "fs-err",
  "insta",
+ "itertools",
  "log",
  "once_cell",
  "reqwest",
@@ -579,9 +580,9 @@ checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
 
 [[package]]
 name = "itertools"
-version = "0.10.3"
+version = "0.10.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
 dependencies = [
  "either",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 2013701..4457439 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ dialoguer = "0.9.0"
 dirs = "3.0.2"
 env_logger = "0.9.0"
 fs-err = "2.6.0"
+itertools = "0.10.5"
 log = "0.4.14"
 once_cell = "1.9.0"
 reqwest = { version = "0.11.4", features = ["blocking"] }
diff --git a/README.md b/README.md
index 1740f0a..0797034 100644
--- a/README.md
+++ b/README.md
@@ -123,13 +123,15 @@ aftman add rojo-rbx/rojo@6.2.0 rojo6
 Usage:
 
 ```bash
-aftman install [--no-trust-check]
+aftman install [--no-trust-check] [--skip-untrusted]
 ```
 
 Install all tools listed in `aftman.toml` files based on your current directory.
 
 If `--no-trust-check` is given, all tools will be installed, regardless of whether they are known. This should generally only be used in CI environments. To trust a specific tool before running `aftman install`, use `aftman trust <tool>` instead.
 
+If `--skip-untrusted` is given, only already trusted tools will be installed, others will be skipped and not emit any errors.
+
 ### `aftman self-install`
 Usage:
 
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index 3430b56..ff2bafa 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -155,6 +155,9 @@ pub struct InstallSubcommand {
     /// recommended to only run this on CI machines.
     #[clap(long)]
     pub no_trust_check: bool,
+    /// Skip / don't error if a tool was not trusted during install.
+    #[clap(long)]
+    pub skip_untrusted: bool,
 }
 
 impl InstallSubcommand {
@@ -165,7 +168,7 @@ impl InstallSubcommand {
             TrustMode::Check
         };
 
-        tools.install_all(trust)
+        tools.install_all(trust, self.skip_untrusted)
     }
 }
 
diff --git a/src/tool_storage.rs b/src/tool_storage.rs
index cf5ec27..32ed2ce 100644
--- a/src/tool_storage.rs
+++ b/src/tool_storage.rs
@@ -9,6 +9,7 @@ use std::path::{Path, PathBuf};
 
 use anyhow::{bail, Context};
 use fs_err::File;
+use itertools::{Either, Itertools};
 use once_cell::unsync::OnceCell;
 
 use crate::auth::AuthManifest;
@@ -19,7 +20,7 @@ use crate::tool_id::ToolId;
 use crate::tool_name::ToolName;
 use crate::tool_source::{Asset, GitHubSource, Release};
 use crate::tool_spec::ToolSpec;
-use crate::trust::{TrustCache, TrustMode};
+use crate::trust::{TrustCache, TrustMode, TrustStatus};
 
 pub struct ToolStorage {
     pub storage_dir: PathBuf,
@@ -135,15 +136,35 @@ impl ToolStorage {
     }
 
     /// Install all tools from all reachable manifest files.
-    pub fn install_all(&self, trust: TrustMode) -> anyhow::Result<()> {
+    pub fn install_all(&self, trust: TrustMode, skip_untrusted: bool) -> anyhow::Result<()> {
         let current_dir = current_dir().context("Failed to get current working directory")?;
         let manifests = Manifest::discover(&self.home, &current_dir)?;
 
-        for manifest in manifests {
-            for (alias, tool_id) in manifest.tools {
-                self.install_exact(&tool_id, trust)?;
-                self.link(&alias)?;
-            }
+        // Installing all tools is split into multiple steps:
+        // 1. Trust check, which may prompt the user and yield if untrusted
+        // 2. Installation of trusted tools, which will be in parallel in the future
+        // 3. Reporting of installation trust errors, unless trust errors are skipped
+
+        let (trusted_tools, trust_errors): (Vec<_>, Vec<_>) = manifests
+            .iter()
+            .flat_map(|manifest| &manifest.tools)
+            .partition_map(
+                |(alias, tool_id)| match self.trust_check(tool_id.name(), trust) {
+                    Ok(_) => Either::Left((alias, tool_id)),
+                    Err(e) => Either::Right(e),
+                },
+            );
+
+        for (alias, tool_id) in &trusted_tools {
+            self.install_exact(tool_id, trust)?;
+            self.link(alias)?;
+        }
+
+        if !trust_errors.is_empty() && !skip_untrusted {
+            bail!(
+                "Installation trust check failed for the following tools:\n{}",
+                trust_errors.iter().map(|e| format!("    {e}")).join("\n")
+            )
         }
 
         Ok(())
@@ -320,10 +341,9 @@ impl ToolStorage {
     }
 
     fn trust_check(&self, name: &ToolName, mode: TrustMode) -> anyhow::Result<()> {
-        let trusted = TrustCache::read(&self.home)?;
-        let is_trusted = trusted.tools.contains(name);
+        let status = self.trust_status(name)?;
 
-        if !is_trusted {
+        if status == TrustStatus::NotTrusted {
             if mode == TrustMode::Check {
                 // If the terminal isn't interactive, tell the user that they
                 // need to open an interactive terminal to trust this tool.
@@ -344,11 +364,10 @@ impl ToolStorage {
                     .interact_opt()?;
 
                 if let Some(false) | None = proceed {
-                    eprintln!(
-                        "Skipping installation of {} and exiting with an error.",
-                        name
+                    bail!(
+                        "Tool {name} is not trusted. \
+                         Run `aftman trust {name}` in your terminal to trust it.",
                     );
-                    std::process::exit(1);
                 }
             }
 
@@ -358,6 +377,16 @@ impl ToolStorage {
         Ok(())
     }
 
+    fn trust_status(&self, name: &ToolName) -> anyhow::Result<TrustStatus> {
+        let trusted = TrustCache::read(&self.home)?;
+        let is_trusted = trusted.tools.contains(name);
+        if is_trusted {
+            Ok(TrustStatus::Trusted)
+        } else {
+            Ok(TrustStatus::NotTrusted)
+        }
+    }
+
     fn install_executable(&self, id: &ToolId, mut contents: impl Read) -> anyhow::Result<()> {
         let output_path = self.exe_path(id);
 
diff --git a/src/trust.rs b/src/trust.rs
index 3ec0a86..a670933 100644
--- a/src/trust.rs
+++ b/src/trust.rs
@@ -13,6 +13,12 @@ pub enum TrustMode {
     NoCheck,
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum TrustStatus {
+    Trusted,
+    NotTrusted,
+}
+
 #[derive(Debug)]
 pub struct TrustCache {
     pub tools: BTreeSet<ToolName>,


Test patch: