lux_lib/operations/
update.rs

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