Skip to main content

lux_lib/operations/
pin.rs

1use std::io;
2
3use crate::{
4    lockfile::{FlushLockfileError, LocalPackageId, PinnedState},
5    package::PackageSpec,
6    tree::{InstallTree, Tree, TreeError},
7};
8use fs_extra::dir::CopyOptions;
9use itertools::Itertools;
10use miette::Diagnostic;
11use thiserror::Error;
12
13// TODO(vhyrro): Differentiate pinned LocalPackages at the type level?
14
15#[derive(Error, Debug, Diagnostic)]
16pub enum PinError {
17    #[error("package with ID '{0}' not found in the lockfile")]
18    #[diagnostic(help("this is probably a bug"))]
19    PackageNotFound(LocalPackageId),
20    #[error("rock {rock} is already {}pinned!", if *.pin_state == PinnedState::Unpinned { "un" } else { "" })]
21    PinStateUnchanged {
22        pin_state: PinnedState,
23        rock: PackageSpec,
24    },
25    #[error("cannot change the pin state of '{rock}', since a second version of '{rock}' is already installed with 'pin: {}'", .pin_state.as_bool())]
26    PinStateConflict {
27        pin_state: PinnedState,
28        rock: PackageSpec,
29    },
30    #[error(transparent)]
31    #[diagnostic(transparent)]
32    FlushLockfile(#[from] FlushLockfileError),
33    #[error(transparent)]
34    #[diagnostic(transparent)]
35    Tree(#[from] TreeError),
36    #[error("failed to move the old package")]
37    #[diagnostic(help("make sure Lux has write access to the install directory"))]
38    MoveItemsFailure(#[from] fs_extra::error::Error),
39    #[error("cannot change pin state of {rock}, because it is not an entrypoint")]
40    #[diagnostic(help("Lux does not allow pinning dependencies, as doing so could break the version requirement in a future update."))]
41    NotAnEntrypoint { rock: PackageSpec },
42    #[error("error reading directory '{dir}'")]
43    #[diagnostic(help("make sure Lux has write access to the directory"))]
44    ReadDir { dir: String, source: io::Error },
45    #[error("error creating directory '{dir}'")]
46    #[diagnostic(help("make sure Lux has write access to the parent directory"))]
47    CreateDir { dir: String, source: io::Error },
48}
49
50pub fn set_pinned_state(
51    package_id: &LocalPackageId,
52    tree: &Tree,
53    pin: PinnedState,
54) -> Result<(), PinError> {
55    let lockfile = tree.lockfile()?;
56    let mut package = lockfile
57        .get(package_id)
58        .ok_or_else(|| PinError::PackageNotFound(package_id.clone()))?
59        .clone();
60
61    if !lockfile.is_entrypoint(&package.id()) {
62        return Err(PinError::NotAnEntrypoint {
63            rock: package.to_package(),
64        });
65    }
66
67    if pin == package.pinned() {
68        return Err(PinError::PinStateUnchanged {
69            pin_state: package.pinned(),
70            rock: package.to_package(),
71        });
72    }
73
74    let old_package = package.clone();
75    let package_root = tree.root_for(&package);
76    let items = std::fs::read_dir(&package_root)
77        .map_err(|source| PinError::ReadDir {
78            dir: package_root.to_string_lossy().to_string(),
79            source,
80        })?
81        .filter_map(Result::ok)
82        .map(|dir| dir.path())
83        .collect_vec();
84
85    package.spec.pinned = pin;
86
87    if lockfile.get(&package.id()).is_some() {
88        return Err(PinError::PinStateConflict {
89            pin_state: package.pinned(),
90            rock: package.to_package(),
91        });
92    }
93
94    let new_root = tree.root_for(&package);
95
96    std::fs::create_dir_all(&new_root).map_err(|source| PinError::CreateDir {
97        dir: new_root.to_string_lossy().to_string(),
98        source,
99    })?;
100
101    fs_extra::move_items(&items, new_root, &CopyOptions::new())?;
102
103    lockfile.map_then_flush(|lockfile| {
104        lockfile.remove(&old_package);
105        lockfile.add_entrypoint(&package);
106
107        Ok::<_, io::Error>(())
108    })?;
109
110    Ok(())
111}