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