1use std::io;
2
3use crate::{
4 build::BuildBehaviour,
5 config::Config,
6 lockfile::{
7 LockedPackage, LockedPackageLockType, Lockfile, PinnedState, ReadOnly, ReadWrite,
8 WorkspaceLockfile,
9 },
10 lua_version::{LuaVersion, LuaVersionUnset},
11 package::{PackageReq, RockConstraintUnsatisfied},
12 package_db::{PackageDB, PackageDBError},
13 remote_package_source::RemotePackageSource,
14 tree::{self, InstallTree, Tree, TreeError},
15 workspace::{Workspace, WorkspaceError, WorkspaceTreeError},
16};
17use bon::Builder;
18use itertools::Itertools;
19use miette::Diagnostic;
20use thiserror::Error;
21
22use super::{PackageInstallSpec, RemoveError, Uninstall};
23
24use crate::drivers::install_packages::{InstallPackages, InstallPackagesError};
25use crate::drivers::sync::{Sync, SyncError, SyncMode, TargetSet};
26
27#[derive(Error, Debug, Diagnostic)]
28pub enum UpdateError {
29 #[error(transparent)]
30 #[diagnostic(transparent)]
31 RockConstraintUnsatisfied(#[from] RockConstraintUnsatisfied),
32 #[error("failed to update rock")]
33 #[diagnostic(forward(0))]
34 Install(#[from] Box<InstallPackagesError>),
35 #[error("failed to remove old rock")]
36 #[diagnostic(forward(0))]
37 Remove(#[from] RemoveError),
38 #[error("error initialising remote package DB")]
39 #[diagnostic(forward(0))]
40 PackageDB(#[from] PackageDBError),
41 #[error("error loading the workspace")]
42 #[diagnostic(forward(0))]
43 Workspace(#[from] WorkspaceError),
44 #[error(transparent)]
45 #[diagnostic(transparent)]
46 LuaVersionUnset(#[from] LuaVersionUnset),
47 #[error(transparent)]
48 Io(#[from] io::Error),
49 #[error(transparent)]
50 #[diagnostic(transparent)]
51 Tree(#[from] TreeError),
52 #[error("error initialising the workspace install tree")]
53 #[diagnostic(forward(0))]
54 WorkspaceTree(#[from] WorkspaceTreeError),
55 #[error("error syncing the workspace install tree")]
56 #[diagnostic(forward(0))]
57 Sync(#[from] Box<SyncError>),
58}
59
60impl From<InstallPackagesError> for UpdateError {
61 fn from(source: InstallPackagesError) -> Self {
62 Self::Install(Box::new(source))
63 }
64}
65
66impl From<SyncError> for UpdateError {
67 fn from(source: SyncError) -> Self {
68 Self::Sync(Box::new(source))
69 }
70}
71
72#[derive(Builder)]
76#[builder(start_fn = new, finish_fn(name = _update, vis = ""))]
77pub struct Update<'a> {
78 #[builder(start_fn)]
79 config: &'a Config,
80
81 #[builder(field)]
83 packages: Option<Vec<PackageReq>>,
84
85 #[builder(field)]
87 test_dependencies: Option<Vec<PackageReq>>,
88
89 #[builder(field)]
91 build_dependencies: Option<Vec<PackageReq>>,
92
93 workspace: Option<Workspace>,
96
97 validate_integrity: Option<bool>,
99
100 package_db: Option<PackageDB>,
101}
102
103impl<State: update_builder::State> UpdateBuilder<'_, State> {
104 pub fn packages(mut self, packages: Option<Vec<PackageReq>>) -> Self {
105 self.packages = packages;
106 self
107 }
108 pub fn build_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
109 self.build_dependencies = packages;
110 self
111 }
112 pub fn test_dependencies(mut self, packages: Option<Vec<PackageReq>>) -> Self {
113 self.test_dependencies = packages;
114 self
115 }
116}
117
118impl<State: update_builder::State> UpdateBuilder<'_, State> {
119 #[tracing::instrument(name = "Updating packages", skip_all)]
120 pub async fn update(self) -> Result<Vec<LockedPackage>, UpdateError>
121 where
122 State: update_builder::IsComplete,
123 {
124 let args = self._update();
125
126 if args
127 .packages
128 .as_ref()
129 .is_some_and(|packages| packages.is_empty())
130 {
131 return Ok(Vec::default());
132 }
133
134 let package_db = match &args.package_db {
135 Some(db) => db.clone(),
136 None => {
137 let db = PackageDB::from_config(args.config).await?;
138 db
139 }
140 };
141
142 let workspace = match args.workspace.clone() {
143 Some(ws) => Some(ws),
144 None => Workspace::current()?,
145 };
146
147 match workspace {
148 Some(workspace) => Box::pin(update_workspace(workspace, args, package_db)).await,
149 None => Box::pin(update_install_tree(args, package_db)).await,
150 }
151 }
152}
153
154async fn update_workspace(
155 workspace: Workspace,
156 args: Update<'_>,
157 package_db: PackageDB,
158) -> Result<Vec<LockedPackage>, UpdateError> {
159 let tree = workspace.tree(args.config)?;
160
161 let sync_report = Sync::new(&workspace, args.config)
162 .mode(SyncMode::Frozen)
163 .targets(TargetSet::all(&workspace, true))
164 .validate_integrity(args.validate_integrity.unwrap_or(false))
165 .sync()
166 .await?;
167
168 let mut project_lockfile = workspace.lockfile()?.write_guard();
169
170 let updated_dependencies = update_dependency_tree(
171 tree,
172 &mut project_lockfile,
173 LockedPackageLockType::Regular,
174 package_db.clone(),
175 args.config,
176 &args.packages,
177 )
178 .await?;
179
180 let test_tree = workspace.test_tree(args.config)?;
181
182 let updated_test_dependencies = update_dependency_tree(
183 test_tree,
184 &mut project_lockfile,
185 LockedPackageLockType::Test,
186 package_db.clone(),
187 args.config,
188 &args.test_dependencies,
189 )
190 .await?;
191
192 let build_tree = workspace.build_tree(args.config)?;
193
194 let updated_build_dependencies = update_dependency_tree(
195 build_tree,
196 &mut project_lockfile,
197 LockedPackageLockType::Build,
198 package_db,
199 args.config,
200 &args.build_dependencies,
201 )
202 .await?;
203
204 Ok(updated_dependencies
205 .into_iter()
206 .chain(sync_report.added)
207 .chain(sync_report.removed)
208 .chain(updated_build_dependencies)
209 .chain(updated_test_dependencies)
210 .collect_vec())
211}
212
213async fn update_dependency_tree(
214 tree: Tree,
215 project_lockfile: &mut WorkspaceLockfile<ReadWrite>,
216 lock_type: LockedPackageLockType,
217 package_db: PackageDB,
218 config: &Config,
219 packages: &Option<Vec<PackageReq>>,
220) -> Result<Vec<LockedPackage>, UpdateError> {
221 let lockfile = tree.lockfile()?;
222 let dependencies = updatable_packages(&lockfile)
223 .into_iter()
224 .filter(|pkg| is_included(pkg, packages))
225 .collect_vec();
226 let updated_dependencies = update(dependencies, package_db, &tree, &lockfile, config).await?;
227 if !updated_dependencies.is_empty() {
228 let updated_lockfile = tree.lockfile()?;
229 project_lockfile.sync(updated_lockfile.local_pkg_lock(), &lock_type);
230 }
231 Ok(updated_dependencies)
232}
233
234fn is_included(
235 (pkg, _): &(LockedPackage, PackageReq),
236 package_reqs: &Option<Vec<PackageReq>>,
237) -> bool {
238 package_reqs.is_none()
239 || package_reqs.as_ref().is_some_and(|packages| {
240 packages
241 .iter()
242 .any(|req| req.matches(&pkg.as_package_spec()))
243 })
244}
245
246async fn update_install_tree(
247 args: Update<'_>,
248 package_db: PackageDB,
249) -> Result<Vec<LockedPackage>, UpdateError> {
250 let tree = args
251 .config
252 .user_tree(LuaVersion::from(args.config)?.clone())?;
253 let lockfile = tree.lockfile()?;
254 let packages = updatable_packages(&lockfile)
255 .into_iter()
256 .filter(|pkg| is_included(pkg, &args.packages))
257 .collect_vec();
258 update(packages, package_db, &tree, &lockfile, args.config).await
259}
260
261async fn update(
262 packages: Vec<(LockedPackage, PackageReq)>,
263 package_db: PackageDB,
264 tree: &Tree,
265 lockfile: &Lockfile<ReadOnly>,
266 config: &Config,
267) -> Result<Vec<LockedPackage>, UpdateError> {
268 let mut updatable = Vec::new();
269 for (package, constraint) in packages.clone() {
270 match package
271 .to_package()
272 .has_update_with(&constraint, &package_db)
273 .await
274 {
275 Ok(Some(_)) if package.pinned() == PinnedState::Unpinned => {
276 updatable.push((package, constraint));
277 }
278 _ => {}
279 }
280 }
281 if updatable.is_empty() {
282 Ok(Vec::new())
283 } else {
284 Uninstall::new()
285 .config(config)
286 .packages(updatable.iter().map(|(package, _)| package.id()))
287 .remove()
288 .await?;
289 let (updated_packages, _lockfile) = InstallPackages::new(config, tree)
290 .packages(
291 updatable
292 .iter()
293 .map(|updatable| mk_install_spec(updatable, lockfile))
294 .collect(),
295 )
296 .package_db(package_db)
297 .install()
298 .await?;
299 Ok(updated_packages)
300 }
301}
302
303fn updatable_packages(lockfile: &Lockfile<ReadOnly>) -> Vec<(LockedPackage, 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::Wally(_) => true,
314 RemotePackageSource::RockspecContent(_) => false,
316 RemotePackageSource::Local => false,
317 #[cfg(test)]
318 RemotePackageSource::Test => false,
319 }
320 })
321 .map(|package| (package.clone(), package.spec().as_package_req()))
322 .collect_vec()
323}
324
325fn mk_install_spec(
326 (package, req): &(LockedPackage, 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 .build_behaviour(BuildBehaviour::Force)
336 .pin(PinnedState::Unpinned)
337 .opt(package.opt())
338 .build()
339}
340
341#[cfg(all(test, feature = "impure_tests"))]
342mod tests {
343 use std::path::PathBuf;
344
345 use assert_fs::prelude::PathCopy;
346
347 use crate::{
348 config::ConfigBuilder, lockfile::LockedPackageLockType, lua_version::LuaVersion,
349 package_db::PackageDB, workspace::Workspace,
350 };
351
352 use super::Update;
353
354 #[tokio::test]
355 async fn update_preserves_pessimistic_req_and_persists_lockfile() {
356 let sample = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
357 .join("resources/test/sample-projects/no-build-spec/");
358 let temp = assert_fs::TempDir::new().unwrap();
359 temp.copy_from(sample, &["**"]).unwrap();
360 let toml_path = temp.path().join("lux.toml");
361 let manifest = std::fs::read_to_string(&toml_path).unwrap();
362 std::fs::write(
363 &toml_path,
364 format!("{manifest}\n[dependencies]\n30log = \"~> 0.9\"\n"),
365 )
366 .unwrap();
367
368 let config = ConfigBuilder::new()
369 .unwrap()
370 .lua_version(Some(LuaVersion::Lua51))
371 .build()
372 .unwrap();
373 let package_db = PackageDB::from_config(&config).await.unwrap();
374
375 let mut workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
376 workspace
377 .members_mut()
378 .iter_mut()
379 .next()
380 .unwrap()
381 .upgrade_all(&package_db, &config)
382 .await
383 .unwrap();
384
385 let rewritten = std::fs::read_to_string(&toml_path).unwrap();
386 assert!(
387 rewritten.contains("30log = \"~>"),
388 "upgrade overwrote the pessimistic req: {rewritten}"
389 );
390 assert!(
391 !rewritten.contains("30log = \"~> 0.9\""),
392 "upgrade did not widen the pessimistic req: {rewritten}"
393 );
394
395 Update::new(&config)
396 .workspace(workspace.clone())
397 .package_db(package_db.clone())
398 .update()
399 .await
400 .unwrap();
401
402 let reloaded = Workspace::from_exact(temp.path()).unwrap().unwrap();
403 let lockfile = reloaded.lockfile().unwrap();
404 assert!(
405 lockfile
406 .rocks(&LockedPackageLockType::Regular)
407 .values()
408 .any(|pkg| pkg.name().to_string() == "30log"),
409 "lockfile was not persisted"
410 );
411 }
412}