zoi_cli/cmd/pin.rs
1//! Pinning packages to specific versions.
2
3use std::collections::HashMap;
4
5use anyhow::Result;
6use colored::Colorize;
7
8use crate::pkg::{config, pin, resolve};
9
10/// Pins a package to a specific version.
11///
12/// This prevents the package from being updated to a newer version during
13/// general upgrades, and records the version as the preferred global version
14/// for the binaries the package provides.
15///
16/// Pinning touches two different stores, because they answer different
17/// questions:
18/// - `pinned.json` answers "may this package be upgraded?", which is what keeps
19/// a general `zoi update` from moving it off the pinned version.
20/// - The `versions:` map in `config.yaml` answers "which version should the
21/// shim launch?", and is the last fallback in shim resolution after the env
22/// var, plugin hook, `zoi.lua` and `.tool-versions` are consulted.
23///
24/// Pinning both keeps the two consistent. Setting only the first would let the
25/// package stay installed at the old version while the shim still preferred a
26/// newer one.
27///
28/// # Errors
29///
30/// Returns an error if the package cannot be resolved, or if the pinned
31/// packages file or the user config cannot be read or written.
32pub fn run(source: &str, version: &str, yes: bool) -> Result<()> {
33 let (pkg, _, _, _, _registry_handle, _, _) =
34 resolve::resolve_package_and_version(source, None, false, yes)?;
35 let mut pinned_packages = pin::get_pinned_packages()?;
36
37 if pinned_packages.iter().any(|p| p.source == pkg.name) {
38 println!(
39 "Package '{}' is already pinned. Unpin it first to change the \
40 version.",
41 pkg.name
42 );
43 return Ok(());
44 }
45
46 let new_pin = pin::PinnedPackage {
47 source: pkg.name.clone(),
48 version: version.to_string()
49 };
50 pinned_packages.push(new_pin);
51 pin::write_pinned_packages(&pinned_packages)?;
52
53 // Record the preferred version for every binary the package provides. The
54 // map is keyed by binary name because that is what the shim looks up. A
55 // package with no declared `bins` still gets an entry under its own name,
56 // which covers the common case where the binary and the package share a
57 // name.
58 let mut versions: HashMap<String, String> = pkg
59 .bins
60 .clone()
61 .unwrap_or_default()
62 .into_iter()
63 .map(|bin| (bin, version.to_string()))
64 .collect();
65 if versions.is_empty() {
66 versions.insert(pkg.name.clone(), version.to_string());
67 }
68 config::update_global_versions(versions)?;
69
70 println!("Pinned {}@{}", pkg.name.green(), version.yellow());
71 Ok(())
72}