lux_lib/operations/
pin.rs1use 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#[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}