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    PackageNotFound(LocalPackageId),
19    #[error("rock {rock} is already {}pinned!", if *.pin_state == PinnedState::Unpinned { "un" } else { "" })]
20    PinStateUnchanged {
21        pin_state: PinnedState,
22        rock: PackageSpec,
23    },
24    #[error("cannot change pin state of {rock}, since a second version of {rock} is already installed with `pin: {}`", .pin_state.as_bool())]
25    PinStateConflict {
26        pin_state: PinnedState,
27        rock: PackageSpec,
28    },
29    #[error(transparent)]
30    #[diagnostic(transparent)]
31    FlushLockfile(#[from] FlushLockfileError),
32    #[error(transparent)]
33    #[diagnostic(transparent)]
34    Tree(#[from] TreeError),
35    #[error("failed to move old package: {0}")]
36    MoveItemsFailure(#[from] fs_extra::error::Error),
37    #[error("cannot change pin state of {rock}, because it is not an entrypoint")]
38    NotAnEntrypoint { rock: PackageSpec },
39    #[error("error reading directory {0}:\n{1}")]
40    ReadDir(String, io::Error),
41    #[error("error creating directory {0}:\n{1}")]
42    CreateDir(String, io::Error),
43}
44
45pub fn set_pinned_state(
46    package_id: &LocalPackageId,
47    tree: &Tree,
48    pin: PinnedState,
49) -> Result<(), PinError> {
50    let lockfile = tree.lockfile()?;
51    let mut package = lockfile
52        .get(package_id)
53        .ok_or_else(|| PinError::PackageNotFound(package_id.clone()))?
54        .clone();
55
56    if !lockfile.is_entrypoint(&package.id()) {
57        return Err(PinError::NotAnEntrypoint {
58            rock: package.to_package(),
59        });
60    }
61
62    if pin == package.pinned() {
63        return Err(PinError::PinStateUnchanged {
64            pin_state: package.pinned(),
65            rock: package.to_package(),
66        });
67    }
68
69    let old_package = package.clone();
70    let package_root = tree.root_for(&package);
71    let items = std::fs::read_dir(&package_root)
72        .map_err(|err| PinError::ReadDir(package_root.to_string_lossy().to_string(), err))?
73        .filter_map(Result::ok)
74        .map(|dir| dir.path())
75        .collect_vec();
76
77    package.spec.pinned = pin;
78
79    if lockfile.get(&package.id()).is_some() {
80        return Err(PinError::PinStateConflict {
81            pin_state: package.pinned(),
82            rock: package.to_package(),
83        });
84    }
85
86    let new_root = tree.root_for(&package);
87
88    std::fs::create_dir_all(&new_root)
89        .map_err(|err| PinError::CreateDir(new_root.to_string_lossy().to_string(), err))?;
90
91    fs_extra::move_items(&items, new_root, &CopyOptions::new())?;
92
93    lockfile.map_then_flush(|lockfile| {
94        lockfile.remove(&old_package);
95        lockfile.add_entrypoint(&package);
96
97        Ok::<_, io::Error>(())
98    })?;
99
100    Ok(())
101}