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