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