1use std::{io, sync::Arc};
2
3use crate::{
4 build::BuildBehaviour,
5 config::Config,
6 lockfile::{LocalPackage, LocalPackageLockType, LockfileIntegrityError},
7 luarocks::luarocks_installation::LUAROCKS_VERSION,
8 package::{PackageName, PackageReq},
9 progress::{MultiProgress, Progress},
10 project::{
11 project_toml::LocalProjectTomlValidationError, Project, ProjectError, ProjectTreeError,
12 },
13 rockspec::Rockspec,
14 tree::{self, TreeError},
15};
16use bon::{builder, Builder};
17use itertools::Itertools;
18use thiserror::Error;
19
20use super::{Install, InstallError, PackageInstallSpec, Remove, RemoveError};
21
22#[derive(Builder)]
24#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
25pub struct Sync<'a> {
26 #[builder(start_fn)]
27 project: &'a Project,
28 #[builder(start_fn)]
29 config: &'a Config,
30
31 #[builder(field)]
32 extra_packages: Vec<PackageReq>,
33
34 progress: Option<Arc<Progress<MultiProgress>>>,
35 validate_integrity: Option<bool>,
37}
38
39impl<State> SyncBuilder<'_, State>
40where
41 State: sync_builder::State,
42{
43 pub fn add_package(mut self, package: PackageReq) -> Self {
44 self.extra_packages.push(package);
45 self
46 }
47}
48
49impl<State> SyncBuilder<'_, State>
50where
51 State: sync_builder::State + sync_builder::IsComplete,
52{
53 pub async fn sync_dependencies(self) -> Result<SyncReport, SyncError> {
54 do_sync(self._build(), &LocalPackageLockType::Regular).await
55 }
56
57 pub async fn sync_test_dependencies(mut self) -> Result<SyncReport, SyncError> {
58 let toml = self.project.toml().into_local()?;
59 for test_dep in toml
60 .test()
61 .current_platform()
62 .test_dependencies(self.project)
63 .iter()
64 .filter(|test_dep| {
65 !toml
66 .test_dependencies()
67 .current_platform()
68 .iter()
69 .any(|dep| dep.name() == test_dep.name())
70 })
71 .cloned()
72 {
73 self.extra_packages.push(test_dep);
74 }
75 do_sync(self._build(), &LocalPackageLockType::Test).await
76 }
77
78 pub async fn sync_build_dependencies(mut self) -> Result<SyncReport, SyncError> {
79 if cfg!(target_family = "unix") && !self.extra_packages.is_empty() {
80 let luarocks =
81 PackageReq::new("luarocks".into(), Some(LUAROCKS_VERSION.into())).unwrap();
82 self = self.add_package(luarocks);
83 }
84 do_sync(self._build(), &LocalPackageLockType::Build).await
85 }
86}
87
88#[derive(Debug)]
89pub struct SyncReport {
90 pub(crate) added: Vec<LocalPackage>,
91 pub(crate) removed: Vec<LocalPackage>,
92}
93
94#[derive(Error, Debug)]
95pub enum SyncError {
96 #[error(transparent)]
97 Io(#[from] io::Error),
98 #[error(transparent)]
99 Tree(#[from] TreeError),
100 #[error(transparent)]
101 Install(#[from] InstallError),
102 #[error(transparent)]
103 Remove(#[from] RemoveError),
104 #[error("integrity error for package {0}: {1}\n")]
105 Integrity(PackageName, LockfileIntegrityError),
106 #[error(transparent)]
107 ProjectTreeError(#[from] ProjectTreeError),
108 #[error(transparent)]
109 ProjectError(#[from] ProjectError),
110 #[error(transparent)]
111 LocalProjectTomlValidationError(#[from] LocalProjectTomlValidationError),
112}
113
114async fn do_sync(
115 args: Sync<'_>,
116 lock_type: &LocalPackageLockType,
117) -> Result<SyncReport, SyncError> {
118 let tree = match lock_type {
119 LocalPackageLockType::Regular => args.project.tree(args.config)?,
120 LocalPackageLockType::Test => args.project.test_tree(args.config)?,
121 LocalPackageLockType::Build => args.project.build_tree(args.config)?,
122 };
123 std::fs::create_dir_all(tree.root())?;
124
125 let mut project_lockfile = args.project.lockfile()?.write_guard();
126 let dest_lockfile = tree.lockfile()?;
127
128 let progress = args.progress.unwrap_or(MultiProgress::new_arc());
129
130 let packages = match lock_type {
131 LocalPackageLockType::Regular => args
132 .project
133 .toml()
134 .into_local()?
135 .dependencies()
136 .current_platform()
137 .clone(),
138 LocalPackageLockType::Build => args
139 .project
140 .toml()
141 .into_local()?
142 .build_dependencies()
143 .current_platform()
144 .clone(),
145 LocalPackageLockType::Test => args
146 .project
147 .toml()
148 .into_local()?
149 .test_dependencies()
150 .current_platform()
151 .clone(),
152 }
153 .into_iter()
154 .chain(args.extra_packages.into_iter().map_into())
155 .collect_vec();
156
157 let package_sync_spec = project_lockfile.package_sync_spec(&packages, lock_type);
158
159 package_sync_spec
160 .to_remove
161 .iter()
162 .for_each(|pkg| project_lockfile.remove(pkg, lock_type));
163
164 let mut to_add: Vec<(tree::EntryType, LocalPackage)> = Vec::new();
165
166 let mut report = SyncReport {
167 added: Vec::new(),
168 removed: Vec::new(),
169 };
170 for (id, local_package) in project_lockfile.rocks(lock_type) {
171 if dest_lockfile.get(id).is_none() {
172 let entry_type = if project_lockfile.is_entrypoint(&local_package.id(), lock_type) {
173 tree::EntryType::Entrypoint
174 } else {
175 tree::EntryType::DependencyOnly
176 };
177 to_add.push((entry_type, local_package.clone()));
178 }
179 }
180 for (id, local_package) in dest_lockfile.rocks() {
181 if project_lockfile.get(id, lock_type).is_none() {
182 report.removed.push(local_package.clone());
183 }
184 }
185
186 let packages_to_install = to_add
187 .iter()
188 .cloned()
189 .map(|(entry_type, pkg)| {
190 PackageInstallSpec::new(pkg.clone().into_package_req(), entry_type)
191 .build_behaviour(BuildBehaviour::Force)
192 .pin(pkg.pinned())
193 .opt(pkg.opt())
194 .constraint(pkg.constraint())
195 .build()
196 })
197 .collect_vec();
198 report
199 .added
200 .extend(to_add.iter().map(|(_, pkg)| pkg).cloned());
201
202 let package_db = project_lockfile.local_pkg_lock(lock_type).clone().into();
203
204 Install::new(args.config)
205 .package_db(package_db)
206 .packages(packages_to_install)
207 .tree(tree.clone())
208 .progress(progress.clone())
209 .install()
210 .await?;
211
212 let dest_lockfile = tree.lockfile()?;
214
215 if args.validate_integrity.unwrap_or(true) {
216 for (_, package) in &to_add {
217 dest_lockfile
218 .validate_integrity(package)
219 .map_err(|err| SyncError::Integrity(package.name().clone(), err))?;
220 }
221 }
222
223 let packages_to_remove = report
224 .removed
225 .iter()
226 .cloned()
227 .map(|pkg| pkg.id())
228 .collect_vec();
229
230 Remove::new(args.config)
231 .packages(packages_to_remove)
232 .progress(progress.clone())
233 .remove()
234 .await?;
235
236 dest_lockfile.map_then_flush(|lockfile| -> Result<(), io::Error> {
237 lockfile.sync(project_lockfile.local_pkg_lock(lock_type));
238 Ok(())
239 })?;
240
241 if !package_sync_spec.to_add.is_empty() {
242 let missing_packages = package_sync_spec
244 .to_add
245 .into_iter()
246 .map(|dep| {
247 PackageInstallSpec::new(dep.package_req().clone(), tree::EntryType::Entrypoint)
248 .build_behaviour(BuildBehaviour::Force)
249 .pin(*dep.pin())
250 .opt(*dep.opt())
251 .maybe_source(dep.source.clone())
252 .build()
253 })
254 .collect();
255
256 let added = Install::new(args.config)
257 .packages(missing_packages)
258 .tree(tree.clone())
259 .progress(progress.clone())
260 .install()
261 .await?;
262
263 report.added.extend(added);
264
265 let dest_lockfile = tree.lockfile()?;
267 project_lockfile.sync(dest_lockfile.local_pkg_lock(), lock_type);
268 }
269
270 Ok(report)
271}
272
273#[cfg(test)]
274mod tests {
275 use super::Sync;
276 use crate::{
277 config::ConfigBuilder, lockfile::LocalPackageLockType, package::PackageReq,
278 project::Project,
279 };
280 use assert_fs::{prelude::PathCopy, TempDir};
281 use std::path::PathBuf;
282
283 #[tokio::test]
284 async fn test_sync_add_rocks() {
285 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
286 println!("Skipping impure test");
287 return;
288 }
289 let temp_dir = TempDir::new().unwrap();
290 temp_dir
291 .copy_from(
292 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
293 .join("resources/test/sample-project-dependencies"),
294 &["**"],
295 )
296 .unwrap();
297 let project = Project::from_exact(temp_dir.path()).unwrap().unwrap();
298 let config = ConfigBuilder::new().unwrap().build().unwrap();
299 let report = Sync::new(&project, &config)
300 .sync_dependencies()
301 .await
302 .unwrap();
303 assert!(report.removed.is_empty());
304 assert!(!report.added.is_empty());
305
306 let lockfile_after_sync = project.lockfile().unwrap();
307 assert!(!lockfile_after_sync
308 .rocks(&LocalPackageLockType::Regular)
309 .is_empty());
310 }
311
312 #[tokio::test]
313 async fn test_sync_add_rocks_with_new_package() {
314 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
315 println!("Skipping impure test");
316 return;
317 }
318 let temp_dir = TempDir::new().unwrap();
319 temp_dir
320 .copy_from(
321 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
322 .join("resources/test/sample-project-dependencies"),
323 &["**"],
324 )
325 .unwrap();
326 let temp_dir = temp_dir.into_persistent();
327 let config = ConfigBuilder::new().unwrap().build().unwrap();
328 let project = Project::from_exact(temp_dir.path()).unwrap().unwrap();
329 {
330 let report = Sync::new(&project, &config)
331 .add_package(PackageReq::new("toml-edit".into(), None).unwrap())
332 .sync_dependencies()
333 .await
334 .unwrap();
335 assert!(report.removed.is_empty());
336 assert!(!report.added.is_empty());
337 assert!(report
338 .added
339 .iter()
340 .any(|pkg| pkg.name().to_string() == "toml-edit"));
341 }
342 let lockfile_after_sync = project.lockfile().unwrap();
343 assert!(!lockfile_after_sync
344 .rocks(&LocalPackageLockType::Regular)
345 .is_empty());
346 }
347
348 #[tokio::test]
349 async fn regression_sync_nonexistent_lock() {
350 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
353 println!("Skipping impure test");
354 return;
355 }
356 let temp_dir = TempDir::new().unwrap();
357 temp_dir
358 .copy_from(
359 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
360 .join("resources/test/sample-project-dependencies"),
361 &["**"],
362 )
363 .unwrap();
364 let config = ConfigBuilder::new().unwrap().build().unwrap();
365 let project = Project::from_exact(temp_dir.path()).unwrap().unwrap();
366 {
367 let report = Sync::new(&project, &config)
368 .add_package(PackageReq::new("toml-edit".into(), None).unwrap())
369 .sync_dependencies()
370 .await
371 .unwrap();
372 assert!(report.removed.is_empty());
373 assert!(!report.added.is_empty());
374 assert!(report
375 .added
376 .iter()
377 .any(|pkg| pkg.name().to_string() == "toml-edit"));
378 }
379 let lockfile_after_sync = project.lockfile().unwrap();
380 assert!(!lockfile_after_sync
381 .rocks(&LocalPackageLockType::Regular)
382 .is_empty());
383 }
384
385 #[tokio::test]
386 async fn test_sync_remove_rocks() {
387 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
388 println!("Skipping impure test");
389 return;
390 }
391 let temp_dir = TempDir::new().unwrap();
392 temp_dir
393 .copy_from(
394 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
395 .join("resources/test/sample-project-dependencies"),
396 &["**"],
397 )
398 .unwrap();
399 let config = ConfigBuilder::new().unwrap().build().unwrap();
400 let project = Project::from_exact(temp_dir.path()).unwrap().unwrap();
401 Sync::new(&project, &config)
403 .add_package(PackageReq::new("toml-edit".into(), None).unwrap())
404 .sync_dependencies()
405 .await
406 .unwrap();
407 let report = Sync::new(&project, &config)
408 .sync_dependencies()
409 .await
410 .unwrap();
411 assert!(!report.removed.is_empty());
412 assert!(report.added.is_empty());
413
414 let lockfile_after_sync = project.lockfile().unwrap();
415 assert!(!lockfile_after_sync
416 .rocks(&LocalPackageLockType::Regular)
417 .is_empty());
418 }
419}