1use std::{io, sync::Arc};
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 progress::{MultiProgress, Progress},
12 remote_package_db::{RemotePackageDB, RemotePackageDBError},
13 remote_package_source::RemotePackageSource,
14 tree::{self, InstallTree, Tree, TreeError},
15 workspace::{Workspace, WorkspaceError, WorkspaceTreeError},
16};
17use bon::Builder;
18use itertools::Itertools;
19use miette::Diagnostic;
20use thiserror::Error;
21
22use super::{Install, InstallError, PackageInstallSpec, RemoveError, SyncError, Uninstall};
23
24#[derive(Error, Debug, Diagnostic)]
25pub enum UpdateError {
26 #[error(transparent)]
27 #[diagnostic(transparent)]
28 RockConstraintUnsatisfied(#[from] RockConstraintUnsatisfied),
29 #[error("failed to update rock: {0}")]
30 #[diagnostic(forward(0))]
31 Install(#[from] InstallError),
32 #[error("failed to remove old rock: {0}")]
33 #[diagnostic(forward(0))]
34 Remove(#[from] RemoveError),
35 #[error("error initialising remote package DB:\n{0}")]
36 #[diagnostic(forward(0))]
37 RemotePackageDB(#[from] RemotePackageDBError),
38 #[error("error loading the workspace:\n{0}")]
39 #[diagnostic(forward(0))]
40 Workspace(#[from] WorkspaceError),
41 #[error(transparent)]
42 #[diagnostic(transparent)]
43 LuaVersionUnset(#[from] LuaVersionUnset),
44 #[error(transparent)]
45 Io(#[from] io::Error),
46 #[error(transparent)]
47 #[diagnostic(transparent)]
48 Tree(#[from] TreeError),
49 #[error("error initialising the workspace install tree:\n{0}")]
50 #[diagnostic(forward(0))]
51 WorkspaceTree(#[from] WorkspaceTreeError),
52 #[error("error syncing the workspace install tree:\n{0}")]
53 #[diagnostic(forward(0))]
54 Sync(#[from] SyncError),
55}
56
57#[derive(Builder)]
61#[builder(start_fn = new, finish_fn(name = _update, vis = ""))]
62pub struct Update<'a> {
63 #[builder(start_fn)]
64 config: &'a Config,
65
66 #[builder(field)]
68 packages: Option<Vec<PackageReq>>,
69
70 #[builder(field)]
72 test_dependencies: Option<Vec<PackageReq>>,
73
74 #[builder(field)]
76 build_dependencies: Option<Vec<PackageReq>>,
77
78 validate_integrity: Option<bool>,
80
81 package_db: Option<RemotePackageDB>,
82
83 progress: Option<Arc<Progress<MultiProgress>>>,
84}
85
86impl<State: update_builder::State> UpdateBuilder<'_, State> {
87 pub fn packages(mut self, packages: Option<Vec<PackageReq>>) -> Self {
88 self.packages = packages;
89 self
90 }
91 pub fn build_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
92 self.build_dependencies = packages;
93 self
94 }
95 pub fn test_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
96 self.test_dependencies = packages;
97 self
98 }
99}
100
101impl<State: update_builder::State> UpdateBuilder<'_, State> {
102 pub async fn update(self) -> Result<Vec<LocalPackage>, UpdateError>
104 where
105 State: update_builder::IsComplete,
106 {
107 let args = self._update();
108
109 if args
110 .packages
111 .as_ref()
112 .is_some_and(|packages| packages.is_empty())
113 {
114 return Ok(Vec::default());
115 }
116
117 let progress = args
118 .progress
119 .clone()
120 .unwrap_or_else(|| MultiProgress::new_arc(args.config));
121
122 let package_db = match &args.package_db {
123 Some(db) => db.clone(),
124 None => {
125 let bar = progress.map(|p| p.new_bar());
126 let db = RemotePackageDB::from_config(args.config, &bar).await?;
127 bar.map(|b| b.finish_and_clear());
128 db
129 }
130 };
131
132 match Workspace::current()? {
133 Some(workspace) => update_workspace(workspace, args, package_db, progress).await,
134 None => update_install_tree(args, package_db, progress).await,
135 }
136 }
137}
138
139async fn update_workspace(
140 workspace: Workspace,
141 args: Update<'_>,
142 package_db: RemotePackageDB,
143 progress: Arc<Progress<MultiProgress>>,
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 progress.clone(),
160 &args.packages,
161 )
162 .await?
163 .into_iter()
164 .chain(dep_report.added)
165 .chain(dep_report.removed);
166
167 let test_tree = workspace.test_tree(args.config)?;
168 let dep_report = super::Sync::new(&workspace, args.config)
169 .validate_integrity(false)
170 .sync_test_dependencies()
171 .await?;
172 let updated_test_dependencies = update_dependency_tree(
173 test_tree,
174 &mut project_lockfile,
175 LocalPackageLockType::Test,
176 package_db.clone(),
177 args.config,
178 progress.clone(),
179 &args.test_dependencies,
180 )
181 .await?
182 .into_iter()
183 .chain(dep_report.added)
184 .chain(dep_report.removed);
185
186 let build_tree = workspace.build_tree(args.config)?;
187
188 let dep_report = super::Sync::new(&workspace, args.config)
189 .validate_integrity(false)
190 .sync_build_dependencies()
191 .await?;
192 let updated_build_dependencies = update_dependency_tree(
193 build_tree,
194 &mut project_lockfile,
195 LocalPackageLockType::Build,
196 package_db.clone(),
197 args.config,
198 progress.clone(),
199 &args.build_dependencies,
200 )
201 .await?
202 .into_iter()
203 .chain(dep_report.added)
204 .chain(dep_report.removed);
205
206 Ok(updated_dependencies
207 .into_iter()
208 .chain(updated_test_dependencies)
209 .chain(updated_build_dependencies)
210 .collect_vec())
211}
212
213async fn update_dependency_tree(
214 tree: Tree,
215 project_lockfile: &mut WorkspaceLockfile<ReadWrite>,
216 lock_type: LocalPackageLockType,
217 package_db: RemotePackageDB,
218 config: &Config,
219 progress: Arc<Progress<MultiProgress>>,
220 packages: &Option<Vec<PackageReq>>,
221) -> Result<Vec<LocalPackage>, UpdateError> {
222 let lockfile = tree.lockfile()?;
223 let dependencies = updatable_packages(&lockfile)
224 .into_iter()
225 .filter(|pkg| is_included(pkg, packages))
226 .collect_vec();
227 let updated_lockfile = tree.lockfile()?;
228 let updated_dependencies =
229 update(dependencies, package_db, tree, &lockfile, config, progress).await?;
230 if !updated_dependencies.is_empty() {
231 project_lockfile.sync(updated_lockfile.local_pkg_lock(), &lock_type);
232 }
233 Ok(updated_dependencies)
234}
235
236fn is_included(
237 (pkg, _): &(LocalPackage, PackageReq),
238 package_reqs: &Option<Vec<PackageReq>>,
239) -> bool {
240 package_reqs.is_none()
241 || package_reqs.as_ref().is_some_and(|packages| {
242 packages
243 .iter()
244 .any(|req| req.matches(&pkg.as_package_spec()))
245 })
246}
247
248async fn update_install_tree(
249 args: Update<'_>,
250 package_db: RemotePackageDB,
251 progress: Arc<Progress<MultiProgress>>,
252) -> Result<Vec<LocalPackage>, UpdateError> {
253 let tree = args
254 .config
255 .user_tree(LuaVersion::from(args.config)?.clone())?;
256 let lockfile = tree.lockfile()?;
257 let packages = updatable_packages(&lockfile)
258 .into_iter()
259 .filter(|pkg| is_included(pkg, &args.packages))
260 .collect_vec();
261 update(packages, package_db, tree, &lockfile, args.config, progress).await
262}
263
264async fn update(
265 packages: Vec<(LocalPackage, PackageReq)>,
266 package_db: RemotePackageDB,
267 tree: Tree,
268 lockfile: &Lockfile<ReadOnly>,
269 config: &Config,
270 progress: Arc<Progress<MultiProgress>>,
271) -> Result<Vec<LocalPackage>, UpdateError> {
272 let updatable = packages
273 .clone()
274 .into_iter()
275 .filter_map(|(package, constraint)| {
276 match package
277 .to_package()
278 .has_update_with(&constraint, &package_db)
279 {
280 Ok(Some(_)) if package.pinned() == PinnedState::Unpinned => {
281 Some((package, constraint))
282 }
283 _ => None,
284 }
285 })
286 .collect_vec();
287 if updatable.is_empty() {
288 Ok(Vec::new())
289 } else {
290 Uninstall::new()
291 .config(config)
292 .packages(updatable.iter().map(|(package, _)| package.id()))
293 .progress(progress.clone())
294 .remove()
295 .await?;
296 let updated_packages = Install::new(config)
297 .packages(
298 updatable
299 .iter()
300 .map(|updatable| mk_install_spec(updatable, lockfile))
301 .collect(),
302 )
303 .tree(tree)
304 .package_db(package_db)
305 .progress(progress)
306 .install()
307 .await?;
308 Ok(updated_packages)
309 }
310}
311
312fn updatable_packages(lockfile: &Lockfile<ReadOnly>) -> Vec<(LocalPackage, PackageReq)> {
313 lockfile
314 .rocks()
315 .values()
316 .filter(|package| {
317 package.pinned() == PinnedState::Unpinned
318 && match package.source() {
319 RemotePackageSource::LuarocksRockspec(_) => true,
320 RemotePackageSource::LuarocksSrcRock(_) => true,
321 RemotePackageSource::LuarocksBinaryRock(_) => true,
322 RemotePackageSource::RockspecContent(_) => false,
325 RemotePackageSource::Local => false,
326 #[cfg(test)]
327 RemotePackageSource::Test => false,
328 }
329 })
330 .map(|package| (package.clone(), package.to_package().into_package_req()))
331 .collect_vec()
332}
333
334fn mk_install_spec(
335 (package, req): &(LocalPackage, PackageReq),
336 lockfile: &Lockfile<ReadOnly>,
337) -> PackageInstallSpec {
338 let entry_type = if lockfile.is_entrypoint(&package.id()) {
339 tree::EntryType::Entrypoint
340 } else {
341 tree::EntryType::DependencyOnly
342 };
343 PackageInstallSpec::new(req.clone(), entry_type)
344 .pin(PinnedState::Unpinned)
345 .opt(package.opt())
346 .build()
347}