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    /// Workspace to use for resolving the dependency tree.
78    /// Uses the current workspace if None.
79    workspace: Option<Workspace>,
80
81    /// Whether to validate the integrity when syncing the project lockfile.
82    validate_integrity: Option<bool>,
83
84    package_db: Option<RemotePackageDB>,
85}
86
87impl<State: update_builder::State> UpdateBuilder<'_, State> {
88    pub fn packages(mut self, packages: Option<Vec<PackageReq>>) -> Self {
89        self.packages = packages;
90        self
91    }
92    pub fn build_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
93        self.build_dependencies = packages;
94        self
95    }
96    pub fn test_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
97        self.test_dependencies = packages;
98        self
99    }
100}
101
102impl<State: update_builder::State> UpdateBuilder<'_, State> {
103    #[tracing::instrument(name = "Updating packages", skip_all)]
104
105    /// Returns the packages that were installed or removed
106    pub async fn update(self) -> Result<Vec<LocalPackage>, UpdateError>
107    where
108        State: update_builder::IsComplete,
109    {
110        let args = self._update();
111
112        if args
113            .packages
114            .as_ref()
115            .is_some_and(|packages| packages.is_empty())
116        {
117            return Ok(Vec::default());
118        }
119
120        let package_db = match &args.package_db {
121            Some(db) => db.clone(),
122            None => {
123                let db = RemotePackageDB::from_config(args.config).await?;
124                db
125            }
126        };
127
128        let workspace = match args.workspace.clone() {
129            Some(ws) => Some(ws),
130            None => Workspace::current()?,
131        };
132
133        match workspace {
134            Some(workspace) => update_workspace(workspace, args, package_db).await,
135            None => update_install_tree(args, package_db).await,
136        }
137    }
138}
139
140async fn update_workspace(
141    workspace: Workspace,
142    args: Update<'_>,
143    package_db: RemotePackageDB,
144) -> Result<Vec<LocalPackage>, UpdateError> {
145    let mut project_lockfile = workspace.lockfile()?.write_guard();
146    let tree = workspace.tree(args.config)?;
147
148    let dep_report = super::Sync::new(&workspace, args.config)
149        .validate_integrity(args.validate_integrity.unwrap_or(false))
150        .sync_dependencies()
151        .await?;
152
153    let updated_dependencies = update_dependency_tree(
154        tree,
155        &mut project_lockfile,
156        LocalPackageLockType::Regular,
157        package_db.clone(),
158        args.config,
159        &args.packages,
160    )
161    .await?
162    .into_iter()
163    .chain(dep_report.added)
164    .chain(dep_report.removed);
165
166    let test_tree = workspace.test_tree(args.config)?;
167    let dep_report = super::Sync::new(&workspace, args.config)
168        .validate_integrity(false)
169        .sync_test_dependencies()
170        .await?;
171    let updated_test_dependencies = update_dependency_tree(
172        test_tree,
173        &mut project_lockfile,
174        LocalPackageLockType::Test,
175        package_db.clone(),
176        args.config,
177        &args.test_dependencies,
178    )
179    .await?
180    .into_iter()
181    .chain(dep_report.added)
182    .chain(dep_report.removed);
183
184    let build_tree = workspace.build_tree(args.config)?;
185
186    let dep_report = super::Sync::new(&workspace, args.config)
187        .validate_integrity(false)
188        .sync_build_dependencies()
189        .await?;
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    .into_iter()
200    .chain(dep_report.added)
201    .chain(dep_report.removed);
202
203    Ok(updated_dependencies
204        .into_iter()
205        .chain(updated_test_dependencies)
206        .chain(updated_build_dependencies)
207        .collect_vec())
208}
209
210async fn update_dependency_tree(
211    tree: Tree,
212    project_lockfile: &mut WorkspaceLockfile<ReadWrite>,
213    lock_type: LocalPackageLockType,
214    package_db: RemotePackageDB,
215    config: &Config,
216    packages: &Option<Vec<PackageReq>>,
217) -> Result<Vec<LocalPackage>, UpdateError> {
218    let lockfile = tree.lockfile()?;
219    let dependencies = updatable_packages(&lockfile)
220        .into_iter()
221        .filter(|pkg| is_included(pkg, packages))
222        .collect_vec();
223    let updated_lockfile = tree.lockfile()?;
224    let updated_dependencies = update(dependencies, package_db, tree, &lockfile, config).await?;
225    if !updated_dependencies.is_empty() {
226        project_lockfile.sync(updated_lockfile.local_pkg_lock(), &lock_type);
227    }
228    Ok(updated_dependencies)
229}
230
231fn is_included(
232    (pkg, _): &(LocalPackage, PackageReq),
233    package_reqs: &Option<Vec<PackageReq>>,
234) -> bool {
235    package_reqs.is_none()
236        || package_reqs.as_ref().is_some_and(|packages| {
237            packages
238                .iter()
239                .any(|req| req.matches(&pkg.as_package_spec()))
240        })
241}
242
243async fn update_install_tree(
244    args: Update<'_>,
245    package_db: RemotePackageDB,
246) -> Result<Vec<LocalPackage>, UpdateError> {
247    let tree = args
248        .config
249        .user_tree(LuaVersion::from(args.config)?.clone())?;
250    let lockfile = tree.lockfile()?;
251    let packages = updatable_packages(&lockfile)
252        .into_iter()
253        .filter(|pkg| is_included(pkg, &args.packages))
254        .collect_vec();
255    update(packages, package_db, tree, &lockfile, args.config).await
256}
257
258async fn update(
259    packages: Vec<(LocalPackage, PackageReq)>,
260    package_db: RemotePackageDB,
261    tree: Tree,
262    lockfile: &Lockfile<ReadOnly>,
263    config: &Config,
264) -> Result<Vec<LocalPackage>, UpdateError> {
265    let updatable = packages
266        .clone()
267        .into_iter()
268        .filter_map(|(package, constraint)| {
269            match package
270                .to_package()
271                .has_update_with(&constraint, &package_db)
272            {
273                Ok(Some(_)) if package.pinned() == PinnedState::Unpinned => {
274                    Some((package, constraint))
275                }
276                _ => None,
277            }
278        })
279        .collect_vec();
280    if updatable.is_empty() {
281        Ok(Vec::new())
282    } else {
283        Uninstall::new()
284            .config(config)
285            .packages(updatable.iter().map(|(package, _)| package.id()))
286            .remove()
287            .await?;
288        let updated_packages = Install::new(config)
289            .packages(
290                updatable
291                    .iter()
292                    .map(|updatable| mk_install_spec(updatable, lockfile))
293                    .collect(),
294            )
295            .tree(tree)
296            .package_db(package_db)
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}