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 validate_integrity: Option<bool>,
79
80 package_db: Option<RemotePackageDB>,
81}
82
83impl<State: update_builder::State> UpdateBuilder<'_, State> {
84 pub fn packages(mut self, packages: Option<Vec<PackageReq>>) -> Self {
85 self.packages = packages;
86 self
87 }
88 pub fn build_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
89 self.build_dependencies = packages;
90 self
91 }
92 pub fn test_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
93 self.test_dependencies = packages;
94 self
95 }
96}
97
98impl<State: update_builder::State> UpdateBuilder<'_, State> {
99 #[tracing::instrument(name = "Updating packages", skip_all)]
100
101 pub async fn update(self) -> Result<Vec<LocalPackage>, UpdateError>
103 where
104 State: update_builder::IsComplete,
105 {
106 let args = self._update();
107
108 if args
109 .packages
110 .as_ref()
111 .is_some_and(|packages| packages.is_empty())
112 {
113 return Ok(Vec::default());
114 }
115
116 let package_db = match &args.package_db {
117 Some(db) => db.clone(),
118 None => {
119 let db = RemotePackageDB::from_config(args.config).await?;
120 db
121 }
122 };
123
124 match Workspace::current()? {
125 Some(workspace) => update_workspace(workspace, args, package_db).await,
126 None => update_install_tree(args, package_db).await,
127 }
128 }
129}
130
131async fn update_workspace(
132 workspace: Workspace,
133 args: Update<'_>,
134 package_db: RemotePackageDB,
135) -> Result<Vec<LocalPackage>, UpdateError> {
136 let mut project_lockfile = workspace.lockfile()?.write_guard();
137 let tree = workspace.tree(args.config)?;
138
139 let dep_report = super::Sync::new(&workspace, 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 &args.packages,
151 )
152 .await?
153 .into_iter()
154 .chain(dep_report.added)
155 .chain(dep_report.removed);
156
157 let test_tree = workspace.test_tree(args.config)?;
158 let dep_report = super::Sync::new(&workspace, 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 &args.test_dependencies,
169 )
170 .await?
171 .into_iter()
172 .chain(dep_report.added)
173 .chain(dep_report.removed);
174
175 let build_tree = workspace.build_tree(args.config)?;
176
177 let dep_report = super::Sync::new(&workspace, args.config)
178 .validate_integrity(false)
179 .sync_build_dependencies()
180 .await?;
181 let updated_build_dependencies = update_dependency_tree(
182 build_tree,
183 &mut project_lockfile,
184 LocalPackageLockType::Build,
185 package_db.clone(),
186 args.config,
187 &args.build_dependencies,
188 )
189 .await?
190 .into_iter()
191 .chain(dep_report.added)
192 .chain(dep_report.removed);
193
194 Ok(updated_dependencies
195 .into_iter()
196 .chain(updated_test_dependencies)
197 .chain(updated_build_dependencies)
198 .collect_vec())
199}
200
201async fn update_dependency_tree(
202 tree: Tree,
203 project_lockfile: &mut WorkspaceLockfile<ReadWrite>,
204 lock_type: LocalPackageLockType,
205 package_db: RemotePackageDB,
206 config: &Config,
207 packages: &Option<Vec<PackageReq>>,
208) -> Result<Vec<LocalPackage>, UpdateError> {
209 let lockfile = tree.lockfile()?;
210 let dependencies = updatable_packages(&lockfile)
211 .into_iter()
212 .filter(|pkg| is_included(pkg, packages))
213 .collect_vec();
214 let updated_lockfile = tree.lockfile()?;
215 let updated_dependencies = update(dependencies, package_db, tree, &lockfile, config).await?;
216 if !updated_dependencies.is_empty() {
217 project_lockfile.sync(updated_lockfile.local_pkg_lock(), &lock_type);
218 }
219 Ok(updated_dependencies)
220}
221
222fn is_included(
223 (pkg, _): &(LocalPackage, PackageReq),
224 package_reqs: &Option<Vec<PackageReq>>,
225) -> bool {
226 package_reqs.is_none()
227 || package_reqs.as_ref().is_some_and(|packages| {
228 packages
229 .iter()
230 .any(|req| req.matches(&pkg.as_package_spec()))
231 })
232}
233
234async fn update_install_tree(
235 args: Update<'_>,
236 package_db: RemotePackageDB,
237) -> Result<Vec<LocalPackage>, UpdateError> {
238 let tree = args
239 .config
240 .user_tree(LuaVersion::from(args.config)?.clone())?;
241 let lockfile = tree.lockfile()?;
242 let packages = updatable_packages(&lockfile)
243 .into_iter()
244 .filter(|pkg| is_included(pkg, &args.packages))
245 .collect_vec();
246 update(packages, package_db, tree, &lockfile, args.config).await
247}
248
249async fn update(
250 packages: Vec<(LocalPackage, PackageReq)>,
251 package_db: RemotePackageDB,
252 tree: Tree,
253 lockfile: &Lockfile<ReadOnly>,
254 config: &Config,
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 Uninstall::new()
275 .config(config)
276 .packages(updatable.iter().map(|(package, _)| package.id()))
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 .install()
289 .await?;
290 Ok(updated_packages)
291 }
292}
293
294fn updatable_packages(lockfile: &Lockfile<ReadOnly>) -> Vec<(LocalPackage, PackageReq)> {
295 lockfile
296 .rocks()
297 .values()
298 .filter(|package| {
299 package.pinned() == PinnedState::Unpinned
300 && match package.source() {
301 RemotePackageSource::LuarocksRockspec(_) => true,
302 RemotePackageSource::LuarocksSrcRock(_) => true,
303 RemotePackageSource::LuarocksBinaryRock(_) => true,
304 RemotePackageSource::RockspecContent(_) => false,
307 RemotePackageSource::Local => false,
308 #[cfg(test)]
309 RemotePackageSource::Test => false,
310 }
311 })
312 .map(|package| (package.clone(), package.to_package().into_package_req()))
313 .collect_vec()
314}
315
316fn mk_install_spec(
317 (package, req): &(LocalPackage, PackageReq),
318 lockfile: &Lockfile<ReadOnly>,
319) -> PackageInstallSpec {
320 let entry_type = if lockfile.is_entrypoint(&package.id()) {
321 tree::EntryType::Entrypoint
322 } else {
323 tree::EntryType::DependencyOnly
324 };
325 PackageInstallSpec::new(req.clone(), entry_type)
326 .pin(PinnedState::Unpinned)
327 .opt(package.opt())
328 .build()
329}