Skip to main content

lux_lib/operations/
pin.rs

1use std::io;
2
3use crate::{
4    fs,
5    lockfile::{FlushLockfileError, LockedPackageId, 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 LockedPackages 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(LockedPackageId),
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(
42        "Lux does not allow pinning dependencies, as doing so could break the version requirement in a future update."
43    ))]
44    NotAnEntrypoint { rock: PackageSpec },
45    #[error(transparent)]
46    #[diagnostic(transparent)]
47    Fs(#[from] fs::FsError),
48}
49
50pub fn set_pinned_state(
51    package_id: &LockedPackageId,
52    tree: &Tree,
53    pin: PinnedState,
54) -> Result<(), PinError> {
55    let lockfile = tree.lockfile()?;
56    let 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 layout = tree.layout_for(&package.spec);
76    let items = fs::sync::read_dir(&layout.root)?
77        .filter_map(Result::ok)
78        .map(|dir| dir.path())
79        .collect_vec();
80
81    let package = package.repin(pin);
82
83    if lockfile.get(&package.id()).is_some() {
84        return Err(PinError::PinStateConflict {
85            pin_state: package.pinned(),
86            rock: package.to_package(),
87        });
88    }
89
90    fs::sync::create_dir_all(&layout.root)?;
91
92    fs_extra::move_items(&items, layout.root, &CopyOptions::new())?;
93
94    lockfile.map_then_flush(|lockfile| {
95        let binaries = lockfile.binaries(&old_package.id()).cloned();
96        lockfile.remove(&old_package);
97        lockfile.add_entrypoint(&package);
98        if let Some(binaries) = binaries {
99            lockfile.set_binaries(&package, binaries);
100        }
101
102        Ok::<_, io::Error>(())
103    })?;
104
105    Ok(())
106}