Skip to main content

lux_lib/operations/
update.rs

1use std::{io, sync::Arc};
2
3use crate::{
4    config::Config,
5    lockfile::{
6        LocalPackage, LocalPackageLockType, Lockfile, PinnedState, ReadOnly, ReadWrite,
7        WorkspaceLockfile,
8    },
9    lua_version::{LuaVersion, LuaVersionUnset},
10    package::{PackageReq, RockConstraintUnsatisfied},
11    progress::{MultiProgress, Progress},
12    remote_package_db::{RemotePackageDB, RemotePackageDBError},
13    remote_package_source::RemotePackageSource,
14    tree::{self, InstallTree, Tree, TreeError},
15    workspace::{Workspace, WorkspaceError, WorkspaceTreeError},
16};
17use bon::Builder;
18use itertools::Itertools;
19use miette::Diagnostic;
20use thiserror::Error;
21
22use super::{Install, InstallError, PackageInstallSpec, RemoveError, SyncError, Uninstall};
23
24#[derive(Error, Debug, Diagnostic)]
25pub enum UpdateError {
26    #[error(transparent)]
27    #[diagnostic(transparent)]
28    RockConstraintUnsatisfied(#[from] RockConstraintUnsatisfied),
29    #[error("failed to update rock: {0}")]
30    #[diagnostic(forward(0))]
31    Install(#[from] InstallError),
32    #[error("failed to remove old rock: {0}")]
33    #[diagnostic(forward(0))]
34    Remove(#[from] RemoveError),
35    #[error("error initialising remote package DB:\n{0}")]
36    #[diagnostic(forward(0))]
37    RemotePackageDB(#[from] RemotePackageDBError),
38    #[error("error loading the workspace:\n{0}")]
39    #[diagnostic(forward(0))]
40    Workspace(#[from] WorkspaceError),
41    #[error(transparent)]
42    #[diagnostic(transparent)]
43    LuaVersionUnset(#[from] LuaVersionUnset),
44    #[error(transparent)]
45    Io(#[from] io::Error),
46    #[error(transparent)]
47    #[diagnostic(transparent)]
48    Tree(#[from] TreeError),
49    #[error("error initialising the workspace install tree:\n{0}")]
50    #[diagnostic(forward(0))]
51    WorkspaceTree(#[from] WorkspaceTreeError),
52    #[error("error syncing the workspace install tree:\n{0}")]
53    #[diagnostic(forward(0))]
54    Sync(#[from] SyncError),
55}
56
57/// A rocks package updater, providing fine-grained control
58/// over how packages should be updated.
59/// Can update multiple packages in parallel.
60#[derive(Builder)]
61#[builder(start_fn = new, finish_fn(name = _update, vis = ""))]
62pub struct Update<'a> {
63    #[builder(start_fn)]
64    config: &'a Config,
65
66    /// Packages to update.
67    #[builder(field)]
68    packages: Option<Vec<PackageReq>>,
69
70    /// Test dependencies to update.
71    #[builder(field)]
72    test_dependencies: Option<Vec<PackageReq>>,
73
74    /// Build dependencies to update.
75    #[builder(field)]
76    build_dependencies: Option<Vec<PackageReq>>,
77
78    /// Whether to validate the integrity when syncing the project lockfile.
79    validate_integrity: Option<bool>,
80
81    package_db: Option<RemotePackageDB>,
82
83    progress: Option<Arc<Progress<MultiProgress>>>,
84}
85
86impl<State: update_builder::State> UpdateBuilder<'_, State> {
87    pub fn packages(mut self, packages: Option<Vec<PackageReq>>) -> Self {
88        self.packages = packages;
89        self
90    }
91    pub fn build_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
92        self.build_dependencies = packages;
93        self
94    }
95    pub fn test_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
96        self.test_dependencies = packages;
97        self
98    }
99}
100
101impl<State: update_builder::State> UpdateBuilder<'_, State> {
102    /// Returns the packages that were installed or removed
103    pub async fn update(self) -> Result<Vec<LocalPackage>, UpdateError>
104    where
105        State: update_builder::IsComplete,
106    {
107        let args = self._update();
108
109        if args
110            .packages
111            .as_ref()
112            .is_some_and(|packages| packages.is_empty())
113        {
114            return Ok(Vec::default());
115        }
116
117        let progress = args
118            .progress
119            .clone()
120            .unwrap_or_else(|| MultiProgress::new_arc(args.config));
121
122        let package_db = match &args.package_db {
123            Some(db) => db.clone(),
124            None => {
125                let bar = progress.map(|p| p.new_bar());
126                let db = RemotePackageDB::from_config(args.config, &bar).await?;
127                bar.map(|b| b.finish_and_clear());
128                db
129            }
130        };
131
132        match Workspace::current()? {
133            Some(workspace) => update_workspace(workspace, args, package_db, progress).await,
134            None => update_install_tree(args, package_db, progress).await,
135        }
136    }
137}
138
139async fn update_workspace(
140    workspace: Workspace,
141    args: Update<'_>,
142    package_db: RemotePackageDB,
143    progress: Arc<Progress<MultiProgress>>,
144) -> Result<Vec<LocalPackage>, UpdateError> {
145    let mut project_lockfile = workspace.lockfile()?.write_guard();
146    let tree = workspace.tree(args.config)?;
147
148    let dep_report = super::Sync::new(&workspace, args.config)
149        .validate_integrity(args.validate_integrity.unwrap_or(false))
150        .sync_dependencies()
151        .await?;
152
153    let updated_dependencies = update_dependency_tree(
154        tree,
155        &mut project_lockfile,
156        LocalPackageLockType::Regular,
157        package_db.clone(),
158        args.config,
159        progress.clone(),
160        &args.packages,
161    )
162    .await?
163    .into_iter()
164    .chain(dep_report.added)
165    .chain(dep_report.removed);
166
167    let test_tree = workspace.test_tree(args.config)?;
168    let dep_report = super::Sync::new(&workspace, args.config)
169        .validate_integrity(false)
170        .sync_test_dependencies()
171        .await?;
172    let updated_test_dependencies = update_dependency_tree(
173        test_tree,
174        &mut project_lockfile,
175        LocalPackageLockType::Test,
176        package_db.clone(),
177        args.config,
178        progress.clone(),
179        &args.test_dependencies,
180    )
181    .await?
182    .into_iter()
183    .chain(dep_report.added)
184    .chain(dep_report.removed);
185
186    let build_tree = workspace.build_tree(args.config)?;
187
188    let dep_report = super::Sync::new(&workspace, args.config)
189        .validate_integrity(false)
190        .sync_build_dependencies()
191        .await?;
192    let updated_build_dependencies = update_dependency_tree(
193        build_tree,
194        &mut project_lockfile,
195        LocalPackageLockType::Build,
196        package_db.clone(),
197        args.config,
198        progress.clone(),
199        &args.build_dependencies,
200    )
201    .await?
202    .into_iter()
203    .chain(dep_report.added)
204    .chain(dep_report.removed);
205
206    Ok(updated_dependencies
207        .into_iter()
208        .chain(updated_test_dependencies)
209        .chain(updated_build_dependencies)
210        .collect_vec())
211}
212
213async fn update_dependency_tree(
214    tree: Tree,
215    project_lockfile: &mut WorkspaceLockfile<ReadWrite>,
216    lock_type: LocalPackageLockType,
217    package_db: RemotePackageDB,
218    config: &Config,
219    progress: Arc<Progress<MultiProgress>>,
220    packages: &Option<Vec<PackageReq>>,
221) -> Result<Vec<LocalPackage>, UpdateError> {
222    let lockfile = tree.lockfile()?;
223    let dependencies = updatable_packages(&lockfile)
224        .into_iter()
225        .filter(|pkg| is_included(pkg, packages))
226        .collect_vec();
227    let updated_lockfile = tree.lockfile()?;
228    let updated_dependencies =
229        update(dependencies, package_db, tree, &lockfile, config, progress).await?;
230    if !updated_dependencies.is_empty() {
231        project_lockfile.sync(updated_lockfile.local_pkg_lock(), &lock_type);
232    }
233    Ok(updated_dependencies)
234}
235
236fn is_included(
237    (pkg, _): &(LocalPackage, PackageReq),
238    package_reqs: &Option<Vec<PackageReq>>,
239) -> bool {
240    package_reqs.is_none()
241        || package_reqs.as_ref().is_some_and(|packages| {
242            packages
243                .iter()
244                .any(|req| req.matches(&pkg.as_package_spec()))
245        })
246}
247
248async fn update_install_tree(
249    args: Update<'_>,
250    package_db: RemotePackageDB,
251    progress: Arc<Progress<MultiProgress>>,
252) -> Result<Vec<LocalPackage>, UpdateError> {
253    let tree = args
254        .config
255        .user_tree(LuaVersion::from(args.config)?.clone())?;
256    let lockfile = tree.lockfile()?;
257    let packages = updatable_packages(&lockfile)
258        .into_iter()
259        .filter(|pkg| is_included(pkg, &args.packages))
260        .collect_vec();
261    update(packages, package_db, tree, &lockfile, args.config, progress).await
262}
263
264async fn update(
265    packages: Vec<(LocalPackage, PackageReq)>,
266    package_db: RemotePackageDB,
267    tree: Tree,
268    lockfile: &Lockfile<ReadOnly>,
269    config: &Config,
270    progress: Arc<Progress<MultiProgress>>,
271) -> Result<Vec<LocalPackage>, UpdateError> {
272    let updatable = packages
273        .clone()
274        .into_iter()
275        .filter_map(|(package, constraint)| {
276            match package
277                .to_package()
278                .has_update_with(&constraint, &package_db)
279            {
280                Ok(Some(_)) if package.pinned() == PinnedState::Unpinned => {
281                    Some((package, constraint))
282                }
283                _ => None,
284            }
285        })
286        .collect_vec();
287    if updatable.is_empty() {
288        Ok(Vec::new())
289    } else {
290        Uninstall::new()
291            .config(config)
292            .packages(updatable.iter().map(|(package, _)| package.id()))
293            .progress(progress.clone())
294            .remove()
295            .await?;
296        let updated_packages = Install::new(config)
297            .packages(
298                updatable
299                    .iter()
300                    .map(|updatable| mk_install_spec(updatable, lockfile))
301                    .collect(),
302            )
303            .tree(tree)
304            .package_db(package_db)
305            .progress(progress)
306            .install()
307            .await?;
308        Ok(updated_packages)
309    }
310}
311
312fn updatable_packages(lockfile: &Lockfile<ReadOnly>) -> Vec<(LocalPackage, PackageReq)> {
313    lockfile
314        .rocks()
315        .values()
316        .filter(|package| {
317            package.pinned() == PinnedState::Unpinned
318                && match package.source() {
319                    RemotePackageSource::LuarocksRockspec(_) => true,
320                    RemotePackageSource::LuarocksSrcRock(_) => true,
321                    RemotePackageSource::LuarocksBinaryRock(_) => true,
322                    // We don't support updating git sources or local packages
323                    // Git sources can be updated with the --toml flag
324                    RemotePackageSource::RockspecContent(_) => false,
325                    RemotePackageSource::Local => false,
326                    #[cfg(test)]
327                    RemotePackageSource::Test => false,
328                }
329        })
330        .map(|package| (package.clone(), package.to_package().into_package_req()))
331        .collect_vec()
332}
333
334fn mk_install_spec(
335    (package, req): &(LocalPackage, PackageReq),
336    lockfile: &Lockfile<ReadOnly>,
337) -> PackageInstallSpec {
338    let entry_type = if lockfile.is_entrypoint(&package.id()) {
339        tree::EntryType::Entrypoint
340    } else {
341        tree::EntryType::DependencyOnly
342    };
343    PackageInstallSpec::new(req.clone(), entry_type)
344        .pin(PinnedState::Unpinned)
345        .opt(package.opt())
346        .build()
347}