Skip to main content

lux_lib/operations/
update.rs

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