Skip to main content

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, RemoveError, SyncError, Uninstall};
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:\n{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    progress: Option<Arc<Progress<MultiProgress>>>,
74}
75
76impl<State: update_builder::State> UpdateBuilder<'_, State> {
77    pub fn packages(mut self, packages: Option<Vec<PackageReq>>) -> Self {
78        self.packages = packages;
79        self
80    }
81    pub fn build_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
82        self.build_dependencies = packages;
83        self
84    }
85    pub fn test_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
86        self.test_dependencies = packages;
87        self
88    }
89}
90
91impl<State: update_builder::State> UpdateBuilder<'_, State> {
92    /// Returns the packages that were installed or removed
93    pub async fn update(self) -> Result<Vec<LocalPackage>, UpdateError>
94    where
95        State: update_builder::IsComplete,
96    {
97        let args = self._update();
98
99        if args
100            .packages
101            .as_ref()
102            .is_some_and(|packages| packages.is_empty())
103        {
104            return Ok(Vec::default());
105        }
106
107        let progress = args
108            .progress
109            .clone()
110            .unwrap_or_else(|| MultiProgress::new_arc(args.config));
111
112        let package_db = match &args.package_db {
113            Some(db) => db.clone(),
114            None => {
115                let bar = progress.map(|p| p.new_bar());
116                let db = RemotePackageDB::from_config(args.config, &bar).await?;
117                bar.map(|b| b.finish_and_clear());
118                db
119            }
120        };
121
122        match Project::current()? {
123            Some(project) => update_project(project, args, package_db, progress).await,
124            None => update_install_tree(args, package_db, progress).await,
125        }
126    }
127}
128
129async fn update_project(
130    project: Project,
131    args: Update<'_>,
132    package_db: RemotePackageDB,
133    progress: Arc<Progress<MultiProgress>>,
134) -> Result<Vec<LocalPackage>, UpdateError> {
135    let mut project_lockfile = project.lockfile()?.write_guard();
136    let tree = project.tree(args.config)?;
137
138    let dep_report = super::Sync::new(&project, args.config)
139        .validate_integrity(args.validate_integrity.unwrap_or(false))
140        .sync_dependencies()
141        .await?;
142
143    let updated_dependencies = update_dependency_tree(
144        tree,
145        &mut project_lockfile,
146        LocalPackageLockType::Regular,
147        package_db.clone(),
148        args.config,
149        progress.clone(),
150        &args.packages,
151    )
152    .await?
153    .into_iter()
154    .chain(dep_report.added)
155    .chain(dep_report.removed);
156
157    let test_tree = project.test_tree(args.config)?;
158    let dep_report = super::Sync::new(&project, args.config)
159        .validate_integrity(false)
160        .sync_test_dependencies()
161        .await?;
162    let updated_test_dependencies = update_dependency_tree(
163        test_tree,
164        &mut project_lockfile,
165        LocalPackageLockType::Test,
166        package_db.clone(),
167        args.config,
168        progress.clone(),
169        &args.test_dependencies,
170    )
171    .await?
172    .into_iter()
173    .chain(dep_report.added)
174    .chain(dep_report.removed);
175
176    let build_tree = project.build_tree(args.config)?;
177
178    let dep_report = super::Sync::new(&project, args.config)
179        .validate_integrity(false)
180        .sync_build_dependencies()
181        .await?;
182    let updated_build_dependencies = update_dependency_tree(
183        build_tree,
184        &mut project_lockfile,
185        LocalPackageLockType::Build,
186        package_db.clone(),
187        args.config,
188        progress.clone(),
189        &args.build_dependencies,
190    )
191    .await?
192    .into_iter()
193    .chain(dep_report.added)
194    .chain(dep_report.removed);
195
196    Ok(updated_dependencies
197        .into_iter()
198        .chain(updated_test_dependencies)
199        .chain(updated_build_dependencies)
200        .collect_vec())
201}
202
203async fn update_dependency_tree(
204    tree: Tree,
205    project_lockfile: &mut ProjectLockfile<ReadWrite>,
206    lock_type: LocalPackageLockType,
207    package_db: RemotePackageDB,
208    config: &Config,
209    progress: Arc<Progress<MultiProgress>>,
210    packages: &Option<Vec<PackageReq>>,
211) -> Result<Vec<LocalPackage>, UpdateError> {
212    let lockfile = tree.lockfile()?;
213    let dependencies = updatable_packages(&lockfile)
214        .into_iter()
215        .filter(|pkg| is_included(pkg, packages))
216        .collect_vec();
217    let updated_lockfile = tree.lockfile()?;
218    let updated_dependencies =
219        update(dependencies, package_db, tree, &lockfile, config, progress).await?;
220    if !updated_dependencies.is_empty() {
221        project_lockfile.sync(updated_lockfile.local_pkg_lock(), &lock_type);
222    }
223    Ok(updated_dependencies)
224}
225
226fn is_included(
227    (pkg, _): &(LocalPackage, PackageReq),
228    package_reqs: &Option<Vec<PackageReq>>,
229) -> bool {
230    package_reqs.is_none()
231        || package_reqs.as_ref().is_some_and(|packages| {
232            packages
233                .iter()
234                .any(|req| req.matches(&pkg.as_package_spec()))
235        })
236}
237
238async fn update_install_tree(
239    args: Update<'_>,
240    package_db: RemotePackageDB,
241    progress: Arc<Progress<MultiProgress>>,
242) -> Result<Vec<LocalPackage>, UpdateError> {
243    let tree = args
244        .config
245        .user_tree(LuaVersion::from(args.config)?.clone())?;
246    let lockfile = tree.lockfile()?;
247    let packages = updatable_packages(&lockfile)
248        .into_iter()
249        .filter(|pkg| is_included(pkg, &args.packages))
250        .collect_vec();
251    update(packages, package_db, tree, &lockfile, args.config, progress).await
252}
253
254async fn update(
255    packages: Vec<(LocalPackage, PackageReq)>,
256    package_db: RemotePackageDB,
257    tree: Tree,
258    lockfile: &Lockfile<ReadOnly>,
259    config: &Config,
260    progress: Arc<Progress<MultiProgress>>,
261) -> Result<Vec<LocalPackage>, UpdateError> {
262    let updatable = packages
263        .clone()
264        .into_iter()
265        .filter_map(|(package, constraint)| {
266            match package
267                .to_package()
268                .has_update_with(&constraint, &package_db)
269            {
270                Ok(Some(_)) if package.pinned() == PinnedState::Unpinned => {
271                    Some((package, constraint))
272                }
273                _ => None,
274            }
275        })
276        .collect_vec();
277    if updatable.is_empty() {
278        Ok(Vec::new())
279    } else {
280        Uninstall::new()
281            .config(config)
282            .packages(updatable.iter().map(|(package, _)| package.id()))
283            .progress(progress.clone())
284            .remove()
285            .await?;
286        let updated_packages = Install::new(config)
287            .packages(
288                updatable
289                    .iter()
290                    .map(|updatable| mk_install_spec(updatable, lockfile))
291                    .collect(),
292            )
293            .tree(tree)
294            .package_db(package_db)
295            .progress(progress)
296            .install()
297            .await?;
298        Ok(updated_packages)
299    }
300}
301
302fn updatable_packages(lockfile: &Lockfile<ReadOnly>) -> Vec<(LocalPackage, PackageReq)> {
303    lockfile
304        .rocks()
305        .values()
306        .filter(|package| {
307            package.pinned() == PinnedState::Unpinned
308                && match package.source() {
309                    RemotePackageSource::LuarocksRockspec(_) => true,
310                    RemotePackageSource::LuarocksSrcRock(_) => true,
311                    RemotePackageSource::LuarocksBinaryRock(_) => true,
312                    // We don't support updating git sources or local packages
313                    // Git sources can be updated with the --toml flag
314                    RemotePackageSource::RockspecContent(_) => false,
315                    RemotePackageSource::Local => false,
316                    #[cfg(test)]
317                    RemotePackageSource::Test => false,
318                }
319        })
320        .map(|package| (package.clone(), package.to_package().into_package_req()))
321        .collect_vec()
322}
323
324fn mk_install_spec(
325    (package, req): &(LocalPackage, PackageReq),
326    lockfile: &Lockfile<ReadOnly>,
327) -> PackageInstallSpec {
328    let entry_type = if lockfile.is_entrypoint(&package.id()) {
329        tree::EntryType::Entrypoint
330    } else {
331        tree::EntryType::DependencyOnly
332    };
333    PackageInstallSpec::new(req.clone(), entry_type)
334        .pin(PinnedState::Unpinned)
335        .opt(package.opt())
336        .build()
337}