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#[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 #[builder(field)]
67 packages: Option<Vec<PackageReq>>,
68
69 #[builder(field)]
71 test_dependencies: Option<Vec<PackageReq>>,
72
73 #[builder(field)]
75 build_dependencies: Option<Vec<PackageReq>>,
76
77 workspace: Option<Workspace>,
80
81 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 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 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}