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")]
29 #[diagnostic(forward(0))]
30 Install(#[from] Box<InstallError>),
31 #[error("failed to remove old rock")]
32 #[diagnostic(forward(0))]
33 Remove(#[from] RemoveError),
34 #[error("error initialising remote package DB")]
35 #[diagnostic(forward(0))]
36 RemotePackageDB(#[from] RemotePackageDBError),
37 #[error("error loading the workspace")]
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")]
49 #[diagnostic(forward(0))]
50 WorkspaceTree(#[from] WorkspaceTreeError),
51 #[error("error syncing the workspace install tree")]
52 #[diagnostic(forward(0))]
53 Sync(#[from] Box<SyncError>),
54}
55
56impl From<InstallError> for UpdateError {
57 fn from(source: InstallError) -> Self {
58 Self::Install(Box::new(source))
59 }
60}
61
62impl From<SyncError> for UpdateError {
63 fn from(source: SyncError) -> Self {
64 Self::Sync(Box::new(source))
65 }
66}
67
68#[derive(Builder)]
72#[builder(start_fn = new, finish_fn(name = _update, vis = ""))]
73pub struct Update<'a> {
74 #[builder(start_fn)]
75 config: &'a Config,
76
77 #[builder(field)]
79 packages: Option<Vec<PackageReq>>,
80
81 #[builder(field)]
83 test_dependencies: Option<Vec<PackageReq>>,
84
85 #[builder(field)]
87 build_dependencies: Option<Vec<PackageReq>>,
88
89 workspace: Option<Workspace>,
92
93 validate_integrity: Option<bool>,
95
96 package_db: Option<RemotePackageDB>,
97}
98
99impl<State: update_builder::State> UpdateBuilder<'_, State> {
100 pub fn packages(mut self, packages: Option<Vec<PackageReq>>) -> Self {
101 self.packages = packages;
102 self
103 }
104 pub fn build_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
105 self.build_dependencies = packages;
106 self
107 }
108 pub fn test_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
109 self.test_dependencies = packages;
110 self
111 }
112}
113
114impl<State: update_builder::State> UpdateBuilder<'_, State> {
115 #[tracing::instrument(name = "Updating packages", skip_all)]
116
117 pub async fn update(self) -> Result<Vec<LocalPackage>, UpdateError>
119 where
120 State: update_builder::IsComplete,
121 {
122 let args = self._update();
123
124 if args
125 .packages
126 .as_ref()
127 .is_some_and(|packages| packages.is_empty())
128 {
129 return Ok(Vec::default());
130 }
131
132 let package_db = match &args.package_db {
133 Some(db) => db.clone(),
134 None => {
135 let db = RemotePackageDB::from_config(args.config).await?;
136 db
137 }
138 };
139
140 let workspace = match args.workspace.clone() {
141 Some(ws) => Some(ws),
142 None => Workspace::current()?,
143 };
144
145 match workspace {
146 Some(workspace) => update_workspace(workspace, args, package_db).await,
147 None => update_install_tree(args, package_db).await,
148 }
149 }
150}
151
152async fn update_workspace(
153 workspace: Workspace,
154 args: Update<'_>,
155 package_db: RemotePackageDB,
156) -> Result<Vec<LocalPackage>, UpdateError> {
157 let mut project_lockfile = workspace.lockfile()?.write_guard();
158 let tree = workspace.tree(args.config)?;
159
160 let sync_report = super::Sync::new(&workspace, args.config)
161 .validate_integrity(args.validate_integrity.unwrap_or(false))
162 .test(true)
163 .sync()
164 .await?;
165
166 let updated_dependencies = update_dependency_tree(
167 tree,
168 &mut project_lockfile,
169 LocalPackageLockType::Regular,
170 package_db.clone(),
171 args.config,
172 &args.packages,
173 )
174 .await?;
175
176 let test_tree = workspace.test_tree(args.config)?;
177
178 let updated_test_dependencies = update_dependency_tree(
179 test_tree,
180 &mut project_lockfile,
181 LocalPackageLockType::Test,
182 package_db.clone(),
183 args.config,
184 &args.test_dependencies,
185 )
186 .await?;
187
188 let build_tree = workspace.build_tree(args.config)?;
189
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
200 Ok(updated_dependencies
201 .into_iter()
202 .chain(sync_report.added)
203 .chain(sync_report.removed)
204 .chain(updated_build_dependencies)
205 .chain(updated_test_dependencies)
206 .collect_vec())
207}
208
209async fn update_dependency_tree(
210 tree: Tree,
211 project_lockfile: &mut WorkspaceLockfile<ReadWrite>,
212 lock_type: LocalPackageLockType,
213 package_db: RemotePackageDB,
214 config: &Config,
215 packages: &Option<Vec<PackageReq>>,
216) -> Result<Vec<LocalPackage>, UpdateError> {
217 let lockfile = tree.lockfile()?;
218 let dependencies = updatable_packages(&lockfile)
219 .into_iter()
220 .filter(|pkg| is_included(pkg, packages))
221 .collect_vec();
222 let updated_lockfile = tree.lockfile()?;
223 let updated_dependencies = update(dependencies, package_db, tree, &lockfile, config).await?;
224 if !updated_dependencies.is_empty() {
225 project_lockfile.sync(updated_lockfile.local_pkg_lock(), &lock_type);
226 }
227 Ok(updated_dependencies)
228}
229
230fn is_included(
231 (pkg, _): &(LocalPackage, PackageReq),
232 package_reqs: &Option<Vec<PackageReq>>,
233) -> bool {
234 package_reqs.is_none()
235 || package_reqs.as_ref().is_some_and(|packages| {
236 packages
237 .iter()
238 .any(|req| req.matches(&pkg.as_package_spec()))
239 })
240}
241
242async fn update_install_tree(
243 args: Update<'_>,
244 package_db: RemotePackageDB,
245) -> Result<Vec<LocalPackage>, UpdateError> {
246 let tree = args
247 .config
248 .user_tree(LuaVersion::from(args.config)?.clone())?;
249 let lockfile = tree.lockfile()?;
250 let packages = updatable_packages(&lockfile)
251 .into_iter()
252 .filter(|pkg| is_included(pkg, &args.packages))
253 .collect_vec();
254 update(packages, package_db, tree, &lockfile, args.config).await
255}
256
257async fn update(
258 packages: Vec<(LocalPackage, PackageReq)>,
259 package_db: RemotePackageDB,
260 tree: Tree,
261 lockfile: &Lockfile<ReadOnly>,
262 config: &Config,
263) -> Result<Vec<LocalPackage>, UpdateError> {
264 let updatable = packages
265 .clone()
266 .into_iter()
267 .filter_map(|(package, constraint)| {
268 match package
269 .to_package()
270 .has_update_with(&constraint, &package_db)
271 {
272 Ok(Some(_)) if package.pinned() == PinnedState::Unpinned => {
273 Some((package, constraint))
274 }
275 _ => None,
276 }
277 })
278 .collect_vec();
279 if updatable.is_empty() {
280 Ok(Vec::new())
281 } else {
282 Uninstall::new()
283 .config(config)
284 .packages(updatable.iter().map(|(package, _)| package.id()))
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 .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}