1use std::{io, sync::Arc};
2
3use bon::Builder;
4use itertools::Itertools;
5use thiserror::Error;
6
7use crate::{
8 config::{Config, LuaVersion, LuaVersionUnset},
9 lockfile::{
10 LocalPackage, LocalPackageLockType, Lockfile, PinnedState, ProjectLockfile, ReadOnly,
11 ReadWrite,
12 },
13 package::{PackageReq, RockConstraintUnsatisfied},
14 progress::{MultiProgress, Progress},
15 project::{Project, ProjectError, ProjectTreeError},
16 remote_package_db::{RemotePackageDB, RemotePackageDBError},
17 remote_package_source::RemotePackageSource,
18 tree::{self, Tree, TreeError},
19};
20
21use super::{Install, InstallError, PackageInstallSpec, RemoveError, SyncError, Uninstall};
22
23#[derive(Error, Debug)]
24pub enum UpdateError {
25 #[error(transparent)]
26 RockConstraintUnsatisfied(#[from] RockConstraintUnsatisfied),
27 #[error("failed to update rock: {0}")]
28 Install(#[from] InstallError),
29 #[error("failed to remove old rock: {0}")]
30 Remove(#[from] RemoveError),
31 #[error("error initialising remote package DB:\n{0}")]
32 RemotePackageDB(#[from] RemotePackageDBError),
33 #[error("error loading project: {0}")]
34 Project(#[from] ProjectError),
35 #[error(transparent)]
36 LuaVersionUnset(#[from] LuaVersionUnset),
37 #[error(transparent)]
38 Io(#[from] io::Error),
39 #[error(transparent)]
40 Tree(#[from] TreeError),
41 #[error("error initialising project tree: {0}")]
42 ProjectTree(#[from] ProjectTreeError),
43 #[error("error syncing the project tree: {0}")]
44 Sync(#[from] SyncError),
45}
46
47#[derive(Builder)]
51#[builder(start_fn = new, finish_fn(name = _update, vis = ""))]
52pub struct Update<'a> {
53 #[builder(start_fn)]
54 config: &'a Config,
55
56 #[builder(field)]
58 packages: Option<Vec<PackageReq>>,
59
60 #[builder(field)]
62 test_dependencies: Option<Vec<PackageReq>>,
63
64 #[builder(field)]
66 build_dependencies: Option<Vec<PackageReq>>,
67
68 validate_integrity: Option<bool>,
70
71 package_db: Option<RemotePackageDB>,
72
73 progress: Option<Arc<Progress<MultiProgress>>>,
74}
75
76impl<State: update_builder::State> UpdateBuilder<'_, State> {
77 pub fn packages(mut self, packages: Option<Vec<PackageReq>>) -> Self {
78 self.packages = packages;
79 self
80 }
81 pub fn build_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
82 self.build_dependencies = packages;
83 self
84 }
85 pub fn test_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
86 self.test_dependencies = packages;
87 self
88 }
89}
90
91impl<State: update_builder::State> UpdateBuilder<'_, State> {
92 pub async fn update(self) -> Result<Vec<LocalPackage>, UpdateError>
94 where
95 State: update_builder::IsComplete,
96 {
97 let args = self._update();
98
99 if args
100 .packages
101 .as_ref()
102 .is_some_and(|packages| packages.is_empty())
103 {
104 return Ok(Vec::default());
105 }
106
107 let progress = args
108 .progress
109 .clone()
110 .unwrap_or_else(|| MultiProgress::new_arc(args.config));
111
112 let package_db = match &args.package_db {
113 Some(db) => db.clone(),
114 None => {
115 let bar = progress.map(|p| p.new_bar());
116 let db = RemotePackageDB::from_config(args.config, &bar).await?;
117 bar.map(|b| b.finish_and_clear());
118 db
119 }
120 };
121
122 match Project::current()? {
123 Some(project) => update_project(project, args, package_db, progress).await,
124 None => update_install_tree(args, package_db, progress).await,
125 }
126 }
127}
128
129async fn update_project(
130 project: Project,
131 args: Update<'_>,
132 package_db: RemotePackageDB,
133 progress: Arc<Progress<MultiProgress>>,
134) -> Result<Vec<LocalPackage>, UpdateError> {
135 let mut project_lockfile = project.lockfile()?.write_guard();
136 let tree = project.tree(args.config)?;
137
138 let dep_report = super::Sync::new(&project, args.config)
139 .validate_integrity(args.validate_integrity.unwrap_or(false))
140 .sync_dependencies()
141 .await?;
142
143 let updated_dependencies = update_dependency_tree(
144 tree,
145 &mut project_lockfile,
146 LocalPackageLockType::Regular,
147 package_db.clone(),
148 args.config,
149 progress.clone(),
150 &args.packages,
151 )
152 .await?
153 .into_iter()
154 .chain(dep_report.added)
155 .chain(dep_report.removed);
156
157 let test_tree = project.test_tree(args.config)?;
158 let dep_report = super::Sync::new(&project, 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 progress.clone(),
169 &args.test_dependencies,
170 )
171 .await?
172 .into_iter()
173 .chain(dep_report.added)
174 .chain(dep_report.removed);
175
176 let build_tree = project.build_tree(args.config)?;
177
178 let dep_report = super::Sync::new(&project, args.config)
179 .validate_integrity(false)
180 .sync_build_dependencies()
181 .await?;
182 let updated_build_dependencies = update_dependency_tree(
183 build_tree,
184 &mut project_lockfile,
185 LocalPackageLockType::Build,
186 package_db.clone(),
187 args.config,
188 progress.clone(),
189 &args.build_dependencies,
190 )
191 .await?
192 .into_iter()
193 .chain(dep_report.added)
194 .chain(dep_report.removed);
195
196 Ok(updated_dependencies
197 .into_iter()
198 .chain(updated_test_dependencies)
199 .chain(updated_build_dependencies)
200 .collect_vec())
201}
202
203async fn update_dependency_tree(
204 tree: Tree,
205 project_lockfile: &mut ProjectLockfile<ReadWrite>,
206 lock_type: LocalPackageLockType,
207 package_db: RemotePackageDB,
208 config: &Config,
209 progress: Arc<Progress<MultiProgress>>,
210 packages: &Option<Vec<PackageReq>>,
211) -> Result<Vec<LocalPackage>, UpdateError> {
212 let lockfile = tree.lockfile()?;
213 let dependencies = updatable_packages(&lockfile)
214 .into_iter()
215 .filter(|pkg| is_included(pkg, packages))
216 .collect_vec();
217 let updated_lockfile = tree.lockfile()?;
218 let updated_dependencies =
219 update(dependencies, package_db, tree, &lockfile, config, progress).await?;
220 if !updated_dependencies.is_empty() {
221 project_lockfile.sync(updated_lockfile.local_pkg_lock(), &lock_type);
222 }
223 Ok(updated_dependencies)
224}
225
226fn is_included(
227 (pkg, _): &(LocalPackage, PackageReq),
228 package_reqs: &Option<Vec<PackageReq>>,
229) -> bool {
230 package_reqs.is_none()
231 || package_reqs.as_ref().is_some_and(|packages| {
232 packages
233 .iter()
234 .any(|req| req.matches(&pkg.as_package_spec()))
235 })
236}
237
238async fn update_install_tree(
239 args: Update<'_>,
240 package_db: RemotePackageDB,
241 progress: Arc<Progress<MultiProgress>>,
242) -> Result<Vec<LocalPackage>, UpdateError> {
243 let tree = args
244 .config
245 .user_tree(LuaVersion::from(args.config)?.clone())?;
246 let lockfile = tree.lockfile()?;
247 let packages = updatable_packages(&lockfile)
248 .into_iter()
249 .filter(|pkg| is_included(pkg, &args.packages))
250 .collect_vec();
251 update(packages, package_db, tree, &lockfile, args.config, progress).await
252}
253
254async fn update(
255 packages: Vec<(LocalPackage, PackageReq)>,
256 package_db: RemotePackageDB,
257 tree: Tree,
258 lockfile: &Lockfile<ReadOnly>,
259 config: &Config,
260 progress: Arc<Progress<MultiProgress>>,
261) -> Result<Vec<LocalPackage>, UpdateError> {
262 let updatable = packages
263 .clone()
264 .into_iter()
265 .filter_map(|(package, constraint)| {
266 match package
267 .to_package()
268 .has_update_with(&constraint, &package_db)
269 {
270 Ok(Some(_)) if package.pinned() == PinnedState::Unpinned => {
271 Some((package, constraint))
272 }
273 _ => None,
274 }
275 })
276 .collect_vec();
277 if updatable.is_empty() {
278 Ok(Vec::new())
279 } else {
280 Uninstall::new()
281 .config(config)
282 .packages(updatable.iter().map(|(package, _)| package.id()))
283 .progress(progress.clone())
284 .remove()
285 .await?;
286 let updated_packages = Install::new(config)
287 .packages(
288 updatable
289 .iter()
290 .map(|updatable| mk_install_spec(updatable, lockfile))
291 .collect(),
292 )
293 .tree(tree)
294 .package_db(package_db)
295 .progress(progress)
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 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}