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#[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 #[builder(field)]
59 packages: Option<Vec<PackageReq>>,
60
61 #[builder(field)]
63 test_dependencies: Option<Vec<PackageReq>>,
64
65 #[builder(field)]
67 build_dependencies: Option<Vec<PackageReq>>,
68
69 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 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 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}