1use std::io;
2
3use super::{Install, InstallError, PackageInstallSpec, RemoveError, Uninstall};
4use crate::{
5 build::BuildBehaviour,
6 config::Config,
7 fs,
8 lockfile::{
9 FlushLockfileError, LocalPackage, LocalPackageLockType, LockfileIntegrityError,
10 SyncStrategy,
11 },
12 luarocks::luarocks_installation::LUAROCKS_VERSION,
13 operations::{self, GenLuaRcError},
14 package::{PackageName, PackageReq},
15 project::{project_toml::LocalProjectTomlValidationError, ProjectError},
16 rockspec::Rockspec,
17 tree::{self, InstallTree, TreeError},
18 workspace::{Workspace, WorkspaceError, WorkspaceTreeError},
19};
20use bon::Builder;
21use itertools::Itertools;
22use miette::Diagnostic;
23use thiserror::Error;
24
25#[derive(Builder)]
27#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
28pub struct Sync<'a> {
29 #[builder(start_fn)]
30 workspace: &'a Workspace,
31 #[builder(start_fn)]
32 config: &'a Config,
33
34 #[builder(field)]
35 extra_packages: Vec<PackageReq>,
36
37 validate_integrity: Option<bool>,
39
40 test: Option<bool>,
42
43 fast: Option<bool>,
46}
47
48impl<State> SyncBuilder<'_, State>
49where
50 State: sync_builder::State,
51{
52 pub fn add_package(mut self, package: PackageReq) -> Self {
53 self.extra_packages.push(package);
54 self
55 }
56}
57
58impl<State> SyncBuilder<'_, State>
59where
60 State: sync_builder::State + sync_builder::IsComplete,
61{
62 pub async fn sync(self) -> Result<SyncReport, SyncError> {
63 let mut args = self._build();
64 let test_report = if args.test.unwrap_or(false) {
65 Some(do_sync(&args, &LocalPackageLockType::Test).await?)
66 } else {
67 None
68 };
69 let mut report = do_sync(&args, &LocalPackageLockType::Regular).await?;
70
71 let lockfile = args.workspace.tree(args.config)?.lockfile()?;
76 let test_tree = args.workspace.test_tree(args.config)?;
77 let test_build_lockfile = test_tree.build_tree(args.config)?.lockfile()?;
78 let build_lockfile = args.workspace.build_tree(args.config)?.lockfile()?;
79 let transitive_build_dependencies = lockfile
80 .local_pkg_lock()
81 .rocks()
82 .values()
83 .flat_map(|rock| rock.build_dependencies())
84 .filter_map(|dep_id| build_lockfile.get(dep_id))
85 .chain(
86 test_tree
87 .lockfile()?
88 .local_pkg_lock()
89 .rocks()
90 .values()
91 .flat_map(|rock| rock.build_dependencies())
92 .filter_map(|dep_id| test_build_lockfile.get(dep_id)),
93 )
94 .map(|pkg| pkg.spec.as_package_req())
95 .collect_vec();
96 args.extra_packages.extend(transitive_build_dependencies);
97
98 let build_report = do_sync(&args, &LocalPackageLockType::Build).await?;
99
100 operations::GenLuaRc::new()
101 .config(args.config)
102 .workspace(args.workspace)
103 .generate_luarc()
104 .await?;
105
106 report.merge(build_report);
107 if let Some(test_report) = test_report {
108 report.merge(test_report);
109 }
110
111 Ok(report)
112 }
113}
114
115#[derive(Debug)]
116pub struct SyncReport {
117 pub(crate) added: Vec<LocalPackage>,
118 pub(crate) removed: Vec<LocalPackage>,
119}
120
121impl SyncReport {
122 pub fn added(&self) -> &[LocalPackage] {
123 &self.added
124 }
125 pub fn removed(&self) -> &[LocalPackage] {
126 &self.removed
127 }
128
129 fn merge(&mut self, other: SyncReport) {
130 self.added.extend(other.added);
131 self.removed.extend(other.removed);
132 }
133}
134
135#[derive(Error, Debug, Diagnostic)]
136pub enum SyncError {
137 #[error(transparent)]
138 #[diagnostic(transparent)]
139 FlushLockfile(#[from] FlushLockfileError),
140 #[error(transparent)]
141 #[diagnostic(transparent)]
142 Fs(#[from] fs::FsError),
143 #[error(transparent)]
144 #[diagnostic(transparent)]
145 Tree(#[from] TreeError),
146 #[error(transparent)]
147 #[diagnostic(transparent)]
148 Install(#[from] InstallError),
149 #[error(transparent)]
150 #[diagnostic(transparent)]
151 Remove(#[from] RemoveError),
152 #[error("integrity error for package '{package}'")]
153 Integrity {
154 package: PackageName,
155 #[diagnostic_source]
156 source: LockfileIntegrityError,
157 },
158 #[error(transparent)]
159 #[diagnostic(transparent)]
160 WorkspaceTree(#[from] WorkspaceTreeError),
161 #[error(transparent)]
162 #[diagnostic(transparent)]
163 Workspace(#[from] WorkspaceError),
164 #[error(transparent)]
165 #[diagnostic(transparent)]
166 Project(#[from] ProjectError),
167 #[error(transparent)]
168 #[diagnostic(transparent)]
169 LocalProjectTomlValidationError(#[from] LocalProjectTomlValidationError),
170 #[error("failed to generate `.luarc.json`")]
171 #[diagnostic(forward(0))]
172 GenLuaRc(#[from] GenLuaRcError),
173}
174
175#[tracing::instrument(name = "Syncing dependencies", skip_all)]
176async fn do_sync(
177 args: &Sync<'_>,
178 lock_type: &LocalPackageLockType,
179) -> Result<SyncReport, SyncError> {
180 let _stdout_gag = gag::Gag::stdout();
183
184 let tree = match lock_type {
185 LocalPackageLockType::Regular => args.workspace.tree(args.config)?,
186 LocalPackageLockType::Test => args.workspace.test_tree(args.config)?,
187 LocalPackageLockType::Build => args.workspace.build_tree(args.config)?,
188 };
189 fs::sync::create_dir_all(tree.root())?;
190
191 let mut workspace_lockfile = args.workspace.lockfile()?.write_guard();
192 let dest_lockfile = tree.lockfile()?;
193
194 let mut packages = Vec::new();
195 for project in args.workspace.members() {
196 match lock_type {
197 LocalPackageLockType::Regular => packages.extend(
198 project
199 .toml()
200 .into_local()?
201 .dependencies()
202 .current_platform()
203 .clone(),
204 ),
205 LocalPackageLockType::Build => packages.extend(
206 project
207 .toml()
208 .into_local()?
209 .build_dependencies()
210 .current_platform()
211 .clone(),
212 ),
213 LocalPackageLockType::Test => packages.extend(
214 project
215 .toml()
216 .into_local()?
217 .test_dependencies()
218 .current_platform()
219 .clone(),
220 ),
221 }
222 }
223
224 let mut extra_packages = args.extra_packages.iter().cloned().collect_vec();
225 if lock_type == &LocalPackageLockType::Build {
226 for project in args.workspace.members() {
227 let toml = project.toml().into_local()?;
228 if let Some(backend) = operations::resolve::luarocks_build_backend_name(&toml) {
229 extra_packages.push(backend.into());
230 if cfg!(target_family = "unix") {
231 let luarocks = unsafe {
232 PackageReq::new_unchecked("luarocks".into(), Some(LUAROCKS_VERSION.into()))
233 };
234 extra_packages.push(luarocks);
235 }
236 }
237 }
238 } else if lock_type == &LocalPackageLockType::Test {
239 for project in args.workspace.members() {
240 let toml = project.toml().into_local()?;
241 for test_dep in toml
242 .test()
243 .current_platform()
244 .test_dependencies(project)
245 .iter()
246 .filter(|test_dep| {
247 !toml
248 .test_dependencies()
249 .current_platform()
250 .iter()
251 .any(|dep| dep.name() == test_dep.name())
252 })
253 .cloned()
254 {
255 extra_packages.push(test_dep);
256 }
257 }
258 }
259
260 let packages = packages
261 .into_iter()
262 .chain(extra_packages.into_iter().unique().map_into())
263 .collect_vec();
264
265 let strategy = if args.fast.unwrap_or(false) {
266 SyncStrategy::LockfileOnly
267 } else {
268 SyncStrategy::EnsureInstalled(&tree)
269 };
270 let package_sync_spec = workspace_lockfile.package_sync_spec(&packages, lock_type, &strategy);
271
272 package_sync_spec
273 .to_remove
274 .iter()
275 .for_each(|pkg| workspace_lockfile.remove(pkg, lock_type));
276
277 let mut to_add: Vec<(tree::EntryType, LocalPackage)> = Vec::new();
278
279 let mut report = SyncReport {
280 added: Vec::new(),
281 removed: Vec::new(),
282 };
283 for (id, local_package) in workspace_lockfile.rocks(lock_type) {
284 if dest_lockfile.get(id).is_none() {
285 let entry_type = if workspace_lockfile.is_entrypoint(&local_package.id(), lock_type) {
286 tree::EntryType::Entrypoint
287 } else {
288 tree::EntryType::DependencyOnly
289 };
290 to_add.push((entry_type, local_package.clone()));
291 }
292 }
293 for (id, local_package) in dest_lockfile.rocks() {
294 if workspace_lockfile.get(id, lock_type).is_none() {
295 report.removed.push(local_package.clone());
296 }
297 }
298
299 let packages_to_install = to_add
300 .iter()
301 .map(|(entry_type, pkg)| {
302 PackageInstallSpec::new(pkg.clone().into_package_req(), *entry_type)
303 .build_behaviour(BuildBehaviour::Force)
304 .pin(pkg.pinned())
305 .opt(pkg.opt())
306 .constraint(pkg.constraint())
307 .build()
308 })
309 .unique()
310 .collect_vec();
311 report
312 .added
313 .extend(to_add.iter().map(|(_, pkg)| pkg).cloned());
314
315 let package_db = workspace_lockfile.local_pkg_locks().into();
316
317 Install::new(args.config)
318 .package_db(package_db)
319 .packages(packages_to_install)
320 .tree(tree.clone())
321 .install()
322 .await?;
323
324 let install_tree_lockfile = tree.lockfile()?;
326
327 if args.validate_integrity.unwrap_or(true) {
328 for (_, package) in &to_add {
329 install_tree_lockfile
330 .validate_integrity(package)
331 .map_err(|source| SyncError::Integrity {
332 package: package.name().clone(),
333 source,
334 })?;
335 }
336 }
337
338 let packages_to_remove = report.removed.iter().map(|pkg| pkg.id()).collect_vec();
339
340 Uninstall::new()
341 .config(args.config)
342 .packages(packages_to_remove)
343 .tree(tree.clone())
344 .remove()
345 .await?;
346
347 install_tree_lockfile.map_then_flush(|lockfile| {
348 lockfile.sync(workspace_lockfile.local_pkg_lock(lock_type));
349 Ok::<_, io::Error>(())
350 })?;
351
352 if !package_sync_spec.to_add.is_empty() {
353 let missing_packages = package_sync_spec
355 .to_add
356 .into_iter()
357 .map(|dep| {
358 PackageInstallSpec::new(dep.package_req().clone(), tree::EntryType::Entrypoint)
359 .build_behaviour(BuildBehaviour::Force)
360 .pin(*dep.pin())
361 .opt(*dep.opt())
362 .maybe_source(dep.source.clone())
363 .build()
364 })
365 .unique()
366 .collect();
367
368 let added = Install::new(args.config)
369 .packages(missing_packages)
370 .tree(tree.clone())
371 .install()
372 .await?;
373
374 report.added.extend(added);
375
376 let dest_lockfile = tree.lockfile()?;
378 workspace_lockfile.sync(dest_lockfile.local_pkg_lock(), lock_type);
379 }
380
381 Ok(report)
382}
383
384#[cfg(test)]
385mod tests {
386 use super::Sync;
387 use crate::{
388 config::ConfigBuilder, lockfile::LocalPackageLockType, package::PackageReq,
389 workspace::Workspace,
390 };
391 use assert_fs::{prelude::PathCopy, TempDir};
392 use std::path::PathBuf;
393
394 #[tokio::test]
395 async fn test_sync_add_rocks() {
396 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
397 println!("Skipping impure test");
398 return;
399 }
400 let temp_dir = TempDir::new().unwrap();
401 temp_dir
402 .copy_from(
403 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
404 .join("resources/test/sample-projects/dependencies/"),
405 &["**"],
406 )
407 .unwrap();
408 let workspace = Workspace::from_exact(temp_dir.path()).unwrap().unwrap();
409 let config = ConfigBuilder::new().unwrap().build().unwrap();
410 let report = Sync::new(&workspace, &config).sync().await.unwrap();
411 assert!(report.removed.is_empty());
412 assert!(!report.added.is_empty());
413
414 let lockfile_after_sync = workspace.lockfile().unwrap();
415 assert!(!lockfile_after_sync
416 .rocks(&LocalPackageLockType::Regular)
417 .is_empty());
418 }
419
420 #[tokio::test]
421 async fn test_sync_add_rocks_with_new_package() {
422 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
423 println!("Skipping impure test");
424 return;
425 }
426 let temp_dir = TempDir::new().unwrap();
427 temp_dir
428 .copy_from(
429 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
430 .join("resources/test/sample-projects/dependencies/"),
431 &["**"],
432 )
433 .unwrap();
434 let temp_dir = temp_dir.into_persistent();
435 let config = ConfigBuilder::new().unwrap().build().unwrap();
436 let workspace = Workspace::from_exact(temp_dir.path()).unwrap().unwrap();
437 {
438 let report = Sync::new(&workspace, &config)
439 .add_package(PackageReq::new("toml-edit".into(), None).unwrap())
440 .sync()
441 .await
442 .unwrap();
443 assert!(report.removed.is_empty());
444 assert!(!report.added.is_empty());
445 assert!(report
446 .added
447 .iter()
448 .any(|pkg| pkg.name().to_string() == "toml-edit"));
449 }
450 let lockfile_after_sync = workspace.lockfile().unwrap();
451 assert!(!lockfile_after_sync
452 .rocks(&LocalPackageLockType::Regular)
453 .is_empty());
454 }
455
456 #[tokio::test]
457 async fn regression_sync_nonexistent_lock() {
458 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
461 println!("Skipping impure test");
462 return;
463 }
464 let temp_dir = TempDir::new().unwrap();
465 temp_dir
466 .copy_from(
467 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
468 .join("resources/test/sample-projects/dependencies/"),
469 &["**"],
470 )
471 .unwrap();
472 let config = ConfigBuilder::new().unwrap().build().unwrap();
473 let workspace = Workspace::from_exact(temp_dir.path()).unwrap().unwrap();
474 {
475 let report = Sync::new(&workspace, &config)
476 .add_package(PackageReq::new("toml-edit".into(), None).unwrap())
477 .sync()
478 .await
479 .unwrap();
480 assert!(report.removed.is_empty());
481 assert!(!report.added.is_empty());
482 assert!(report
483 .added
484 .iter()
485 .any(|pkg| pkg.name().to_string() == "toml-edit"));
486 }
487 let lockfile_after_sync = workspace.lockfile().unwrap();
488 assert!(!lockfile_after_sync
489 .rocks(&LocalPackageLockType::Regular)
490 .is_empty());
491 }
492
493 #[tokio::test]
494 async fn test_sync_remove_rocks() {
495 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
496 println!("Skipping impure test");
497 return;
498 }
499 let temp_dir = TempDir::new().unwrap();
500 temp_dir
501 .copy_from(
502 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
503 .join("resources/test/sample-projects/dependencies/"),
504 &["**"],
505 )
506 .unwrap();
507 let config = ConfigBuilder::new().unwrap().build().unwrap();
508 let workspace = Workspace::from_exact(temp_dir.path()).unwrap().unwrap();
509 Sync::new(&workspace, &config)
511 .add_package(PackageReq::new("toml-edit".into(), None).unwrap())
512 .sync()
513 .await
514 .unwrap();
515 let report = Sync::new(&workspace, &config).sync().await.unwrap();
516 assert!(!report.removed.is_empty());
517 assert!(report.added.is_empty());
518
519 let lockfile_after_sync = workspace.lockfile().unwrap();
520 assert!(!lockfile_after_sync
521 .rocks(&LocalPackageLockType::Regular)
522 .is_empty());
523 }
524}