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")]
29    #[diagnostic(forward(0))]
30    Install(#[from] Box<InstallError>),
31    #[error("failed to remove old rock")]
32    #[diagnostic(forward(0))]
33    Remove(#[from] RemoveError),
34    #[error("error initialising remote package DB")]
35    #[diagnostic(forward(0))]
36    RemotePackageDB(#[from] RemotePackageDBError),
37    #[error("error loading the workspace")]
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")]
49    #[diagnostic(forward(0))]
50    WorkspaceTree(#[from] WorkspaceTreeError),
51    #[error("error syncing the workspace install tree")]
52    #[diagnostic(forward(0))]
53    Sync(#[from] Box<SyncError>),
54}
55
56impl From<InstallError> for UpdateError {
57    fn from(source: InstallError) -> Self {
58        Self::Install(Box::new(source))
59    }
60}
61
62impl From<SyncError> for UpdateError {
63    fn from(source: SyncError) -> Self {
64        Self::Sync(Box::new(source))
65    }
66}
67
68/// A rocks package updater, providing fine-grained control
69/// over how packages should be updated.
70/// Can update multiple packages in parallel.
71#[derive(Builder)]
72#[builder(start_fn = new, finish_fn(name = _update, vis = ""))]
73pub struct Update<'a> {
74    #[builder(start_fn)]
75    config: &'a Config,
76
77    /// Packages to update.
78    #[builder(field)]
79    packages: Option<Vec<PackageReq>>,
80
81    /// Test dependencies to update.
82    #[builder(field)]
83    test_dependencies: Option<Vec<PackageReq>>,
84
85    /// Build dependencies to update.
86    #[builder(field)]
87    build_dependencies: Option<Vec<PackageReq>>,
88
89    /// Workspace to use for resolving the dependency tree.
90    /// Uses the current workspace if None.
91    workspace: Option<Workspace>,
92
93    /// Whether to validate the integrity when syncing the project lockfile.
94    validate_integrity: Option<bool>,
95
96    package_db: Option<RemotePackageDB>,
97}
98
99impl<State: update_builder::State> UpdateBuilder<'_, State> {
100    pub fn packages(mut self, packages: Option<Vec<PackageReq>>) -> Self {
101        self.packages = packages;
102        self
103    }
104    pub fn build_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
105        self.build_dependencies = packages;
106        self
107    }
108    pub fn test_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
109        self.test_dependencies = packages;
110        self
111    }
112}
113
114impl<State: update_builder::State> UpdateBuilder<'_, State> {
115    #[tracing::instrument(name = "Updating packages", skip_all)]
116
117    /// Returns the packages that were installed or removed
118    pub async fn update(self) -> Result<Vec<LocalPackage>, UpdateError>
119    where
120        State: update_builder::IsComplete,
121    {
122        let args = self._update();
123
124        if args
125            .packages
126            .as_ref()
127            .is_some_and(|packages| packages.is_empty())
128        {
129            return Ok(Vec::default());
130        }
131
132        let package_db = match &args.package_db {
133            Some(db) => db.clone(),
134            None => {
135                let db = RemotePackageDB::from_config(args.config).await?;
136                db
137            }
138        };
139
140        let workspace = match args.workspace.clone() {
141            Some(ws) => Some(ws),
142            None => Workspace::current()?,
143        };
144
145        match workspace {
146            Some(workspace) => update_workspace(workspace, args, package_db).await,
147            None => update_install_tree(args, package_db).await,
148        }
149    }
150}
151
152async fn update_workspace(
153    workspace: Workspace,
154    args: Update<'_>,
155    package_db: RemotePackageDB,
156) -> Result<Vec<LocalPackage>, UpdateError> {
157    let mut project_lockfile = workspace.lockfile()?.write_guard();
158    let tree = workspace.tree(args.config)?;
159
160    let sync_report = super::Sync::new(&workspace, args.config)
161        .validate_integrity(args.validate_integrity.unwrap_or(false))
162        .test(true)
163        .sync()
164        .await?;
165
166    let updated_dependencies = update_dependency_tree(
167        tree,
168        &mut project_lockfile,
169        LocalPackageLockType::Regular,
170        package_db.clone(),
171        args.config,
172        &args.packages,
173    )
174    .await?;
175
176    let test_tree = workspace.test_tree(args.config)?;
177
178    let updated_test_dependencies = update_dependency_tree(
179        test_tree,
180        &mut project_lockfile,
181        LocalPackageLockType::Test,
182        package_db.clone(),
183        args.config,
184        &args.test_dependencies,
185    )
186    .await?;
187
188    let build_tree = workspace.build_tree(args.config)?;
189
190    let updated_build_dependencies = update_dependency_tree(
191        build_tree,
192        &mut project_lockfile,
193        LocalPackageLockType::Build,
194        package_db.clone(),
195        args.config,
196        &args.build_dependencies,
197    )
198    .await?;
199
200    Ok(updated_dependencies
201        .into_iter()
202        .chain(sync_report.added)
203        .chain(sync_report.removed)
204        .chain(updated_build_dependencies)
205        .chain(updated_test_dependencies)
206        .collect_vec())
207}
208
209async fn update_dependency_tree(
210    tree: Tree,
211    project_lockfile: &mut WorkspaceLockfile<ReadWrite>,
212    lock_type: LocalPackageLockType,
213    package_db: RemotePackageDB,
214    config: &Config,
215    packages: &Option<Vec<PackageReq>>,
216) -> Result<Vec<LocalPackage>, UpdateError> {
217    let lockfile = tree.lockfile()?;
218    let dependencies = updatable_packages(&lockfile)
219        .into_iter()
220        .filter(|pkg| is_included(pkg, packages))
221        .collect_vec();
222    let updated_lockfile = tree.lockfile()?;
223    let updated_dependencies = update(dependencies, package_db, tree, &lockfile, config).await?;
224    if !updated_dependencies.is_empty() {
225        project_lockfile.sync(updated_lockfile.local_pkg_lock(), &lock_type);
226    }
227    Ok(updated_dependencies)
228}
229
230fn is_included(
231    (pkg, _): &(LocalPackage, PackageReq),
232    package_reqs: &Option<Vec<PackageReq>>,
233) -> bool {
234    package_reqs.is_none()
235        || package_reqs.as_ref().is_some_and(|packages| {
236            packages
237                .iter()
238                .any(|req| req.matches(&pkg.as_package_spec()))
239        })
240}
241
242async fn update_install_tree(
243    args: Update<'_>,
244    package_db: RemotePackageDB,
245) -> Result<Vec<LocalPackage>, UpdateError> {
246    let tree = args
247        .config
248        .user_tree(LuaVersion::from(args.config)?.clone())?;
249    let lockfile = tree.lockfile()?;
250    let packages = updatable_packages(&lockfile)
251        .into_iter()
252        .filter(|pkg| is_included(pkg, &args.packages))
253        .collect_vec();
254    update(packages, package_db, tree, &lockfile, args.config).await
255}
256
257async fn update(
258    packages: Vec<(LocalPackage, PackageReq)>,
259    package_db: RemotePackageDB,
260    tree: Tree,
261    lockfile: &Lockfile<ReadOnly>,
262    config: &Config,
263) -> Result<Vec<LocalPackage>, UpdateError> {
264    let updatable = packages
265        .clone()
266        .into_iter()
267        .filter_map(|(package, constraint)| {
268            match package
269                .to_package()
270                .has_update_with(&constraint, &package_db)
271            {
272                Ok(Some(_)) if package.pinned() == PinnedState::Unpinned => {
273                    Some((package, constraint))
274                }
275                _ => None,
276            }
277        })
278        .collect_vec();
279    if updatable.is_empty() {
280        Ok(Vec::new())
281    } else {
282        Uninstall::new()
283            .config(config)
284            .packages(updatable.iter().map(|(package, _)| package.id()))
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            .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}