Skip to main content

lux_lib/operations/
pin.rs

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