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, Remove, RemoveError, SyncError};
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: {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 #[builder(default = MultiProgress::new_arc())]
74 progress: 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 let package_db = match &args.package_db {
101 Some(db) => db.clone(),
102 None => {
103 let bar = args.progress.map(|p| p.new_bar());
104 let db = RemotePackageDB::from_config(args.config, &bar).await?;
105 bar.map(|b| b.finish_and_clear());
106 db
107 }
108 };
109
110 match Project::current()? {
111 Some(project) => update_project(project, args, package_db).await,
112 None => update_install_tree(args, package_db).await,
113 }
114 }
115}
116
117async fn update_project(
118 project: Project,
119 args: Update<'_>,
120 package_db: RemotePackageDB,
121) -> Result<Vec<LocalPackage>, UpdateError> {
122 let mut project_lockfile = project.lockfile()?.write_guard();
123 let tree = project.tree(args.config)?;
124
125 let dep_report = super::Sync::new(&project, args.config)
126 .validate_integrity(args.validate_integrity.unwrap_or(false))
127 .sync_dependencies()
128 .await?;
129
130 let updated_dependencies = update_dependency_tree(
131 tree,
132 &mut project_lockfile,
133 LocalPackageLockType::Regular,
134 package_db.clone(),
135 args.config,
136 args.progress.clone(),
137 &args.packages,
138 )
139 .await?
140 .into_iter()
141 .chain(dep_report.added)
142 .chain(dep_report.removed);
143
144 let test_tree = project.test_tree(args.config)?;
145 let dep_report = super::Sync::new(&project, args.config)
146 .validate_integrity(false)
147 .sync_test_dependencies()
148 .await?;
149 let updated_test_dependencies = update_dependency_tree(
150 test_tree,
151 &mut project_lockfile,
152 LocalPackageLockType::Test,
153 package_db.clone(),
154 args.config,
155 args.progress.clone(),
156 &args.test_dependencies,
157 )
158 .await?
159 .into_iter()
160 .chain(dep_report.added)
161 .chain(dep_report.removed);
162
163 let build_tree = project.build_tree(args.config)?;
164
165 let dep_report = super::Sync::new(&project, args.config)
166 .validate_integrity(false)
167 .sync_build_dependencies()
168 .await?;
169 let updated_build_dependencies = update_dependency_tree(
170 build_tree,
171 &mut project_lockfile,
172 LocalPackageLockType::Build,
173 package_db.clone(),
174 args.config,
175 args.progress.clone(),
176 &args.build_dependencies,
177 )
178 .await?
179 .into_iter()
180 .chain(dep_report.added)
181 .chain(dep_report.removed);
182
183 Ok(updated_dependencies
184 .into_iter()
185 .chain(updated_test_dependencies)
186 .chain(updated_build_dependencies)
187 .collect_vec())
188}
189
190async fn update_dependency_tree(
191 tree: Tree,
192 project_lockfile: &mut ProjectLockfile<ReadWrite>,
193 lock_type: LocalPackageLockType,
194 package_db: RemotePackageDB,
195 config: &Config,
196 progress: Arc<Progress<MultiProgress>>,
197 packages: &Option<Vec<PackageReq>>,
198) -> Result<Vec<LocalPackage>, UpdateError> {
199 let lockfile = tree.lockfile()?;
200 let dependencies = updatable_packages(&lockfile)
201 .into_iter()
202 .filter(|pkg| is_included(pkg, packages))
203 .collect_vec();
204 let updated_lockfile = tree.lockfile()?;
205 let updated_dependencies =
206 update(dependencies, package_db, tree, &lockfile, config, progress).await?;
207 if !updated_dependencies.is_empty() {
208 project_lockfile.sync(updated_lockfile.local_pkg_lock(), &lock_type);
209 }
210 Ok(updated_dependencies)
211}
212
213fn is_included(
214 (pkg, _): &(LocalPackage, PackageReq),
215 package_reqs: &Option<Vec<PackageReq>>,
216) -> bool {
217 package_reqs.is_none()
218 || package_reqs.as_ref().is_some_and(|packages| {
219 packages
220 .iter()
221 .any(|req| req.matches(&pkg.as_package_spec()))
222 })
223}
224
225async fn update_install_tree(
226 args: Update<'_>,
227 package_db: RemotePackageDB,
228) -> Result<Vec<LocalPackage>, UpdateError> {
229 let tree = args
230 .config
231 .user_tree(LuaVersion::from(args.config)?.clone())?;
232 let lockfile = tree.lockfile()?;
233 let packages = updatable_packages(&lockfile)
234 .into_iter()
235 .filter(|pkg| is_included(pkg, &args.packages))
236 .collect_vec();
237 update(
238 packages,
239 package_db,
240 tree,
241 &lockfile,
242 args.config,
243 args.progress,
244 )
245 .await
246}
247
248async fn update(
249 packages: Vec<(LocalPackage, PackageReq)>,
250 package_db: RemotePackageDB,
251 tree: Tree,
252 lockfile: &Lockfile<ReadOnly>,
253 config: &Config,
254 progress: Arc<Progress<MultiProgress>>,
255) -> Result<Vec<LocalPackage>, UpdateError> {
256 let updatable = packages
257 .clone()
258 .into_iter()
259 .filter_map(|(package, constraint)| {
260 match package
261 .to_package()
262 .has_update_with(&constraint, &package_db)
263 {
264 Ok(Some(_)) if package.pinned() == PinnedState::Unpinned => {
265 Some((package, constraint))
266 }
267 _ => None,
268 }
269 })
270 .collect_vec();
271 if updatable.is_empty() {
272 Ok(Vec::new())
273 } else {
274 Remove::new(config)
275 .packages(updatable.iter().map(|(package, _)| package.id()))
276 .progress(progress.clone())
277 .remove()
278 .await?;
279 let updated_packages = Install::new(config)
280 .packages(
281 updatable
282 .iter()
283 .map(|updatable| mk_install_spec(updatable, lockfile))
284 .collect(),
285 )
286 .tree(tree)
287 .package_db(package_db)
288 .progress(progress)
289 .install()
290 .await?;
291 Ok(updated_packages)
292 }
293}
294
295fn updatable_packages(lockfile: &Lockfile<ReadOnly>) -> Vec<(LocalPackage, PackageReq)> {
296 lockfile
297 .rocks()
298 .values()
299 .filter(|package| {
300 package.pinned() == PinnedState::Unpinned
301 && match package.source() {
302 RemotePackageSource::LuarocksRockspec(_) => true,
303 RemotePackageSource::LuarocksSrcRock(_) => true,
304 RemotePackageSource::LuarocksBinaryRock(_) => true,
305 RemotePackageSource::RockspecContent(_) => false,
308 RemotePackageSource::Local => false,
309 #[cfg(test)]
310 RemotePackageSource::Test => false,
311 }
312 })
313 .map(|package| (package.clone(), package.to_package().into_package_req()))
314 .collect_vec()
315}
316
317fn mk_install_spec(
318 (package, req): &(LocalPackage, PackageReq),
319 lockfile: &Lockfile<ReadOnly>,
320) -> PackageInstallSpec {
321 let entry_type = if lockfile.is_entrypoint(&package.id()) {
322 tree::EntryType::Entrypoint
323 } else {
324 tree::EntryType::DependencyOnly
325 };
326 PackageInstallSpec::new(req.clone(), entry_type)
327 .pin(PinnedState::Unpinned)
328 .opt(package.opt())
329 .build()
330}