1use self::reconcile::SyncReport;
2use bon::Builder;
3use miette::Diagnostic;
4use nonempty::NonEmpty;
5use thiserror::Error;
6
7mod frozen;
8pub(crate) mod reconcile;
9
10use crate::{
11 build::BuildBehaviour,
12 config::Config,
13 drivers::install_packages::{InstallPackages, InstallPackagesError},
14 lockfile::LockedPackageLockType,
15 operations::{
16 GenLuaRc, GenLuaRcError, InstallProject, InstallProjectError, PackageInstallSpec,
17 },
18 package::PackageName,
19 package_db::{PackageDB, PackageDBError},
20 project::project_toml::LocalProjectTomlValidationError,
21 rockspec::Rockspec,
22 tree::{EntryType, InstallTree, TreeError},
23 workspace::{Workspace, WorkspaceError, WorkspaceTreeError},
24};
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum SyncMode {
28 Open,
31 Frozen,
33}
34
35#[derive(Clone, Debug)]
36pub struct TargetSet {
37 pub test: bool,
38 pub members: NonEmpty<PackageName>,
39}
40
41impl TargetSet {
42 pub fn all(workspace: &Workspace, test: bool) -> Self {
43 Self {
44 test,
45 members: workspace
46 .members()
47 .clone()
48 .map(|project| project.toml().package.clone()),
49 }
50 }
51
52 pub fn member(member: PackageName, test: bool) -> Self {
53 Self {
54 test,
55 members: NonEmpty::new(member),
56 }
57 }
58}
59
60#[derive(Error, Debug, Diagnostic)]
61pub enum SyncError {
62 #[error(transparent)]
63 #[diagnostic(transparent)]
64 InstallPackages(#[from] Box<InstallPackagesError>),
65 #[error(transparent)]
66 #[diagnostic(transparent)]
67 InstallProject(Box<InstallProjectError>),
68 #[error(transparent)]
69 #[diagnostic(transparent)]
70 Workspace(#[from] WorkspaceError),
71 #[error(transparent)]
72 #[diagnostic(transparent)]
73 WorkspaceTree(#[from] WorkspaceTreeError),
74 #[error(transparent)]
75 #[diagnostic(transparent)]
76 Tree(#[from] TreeError),
77 #[error(transparent)]
78 #[diagnostic(transparent)]
79 Project(#[from] LocalProjectTomlValidationError),
80 #[error(transparent)]
81 #[diagnostic(transparent)]
82 PackageDB(#[from] PackageDBError),
83 #[error(transparent)]
84 #[diagnostic(transparent)]
85 GenLuaRc(#[from] GenLuaRcError),
86 #[error(transparent)]
87 #[diagnostic(transparent)]
88 Frozen(Box<frozen::SyncError>),
89}
90
91impl From<InstallPackagesError> for SyncError {
92 fn from(source: InstallPackagesError) -> Self {
93 Self::InstallPackages(Box::new(source))
94 }
95}
96
97impl From<InstallProjectError> for SyncError {
98 fn from(source: InstallProjectError) -> Self {
99 Self::InstallProject(Box::new(source))
100 }
101}
102
103impl From<frozen::SyncError> for SyncError {
104 fn from(source: frozen::SyncError) -> Self {
105 Self::Frozen(Box::new(source))
106 }
107}
108
109#[derive(Builder)]
111#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
112pub struct Sync<'a> {
113 #[builder(start_fn)]
114 workspace: &'a Workspace,
115 #[builder(start_fn)]
116 config: &'a Config,
117
118 mode: SyncMode,
119
120 targets: TargetSet,
121
122 #[builder(default = BuildBehaviour::Force)]
123 behaviour: BuildBehaviour,
124
125 #[builder(default = false)]
127 only_deps: bool,
128
129 #[builder(default = false)]
131 no_lock: bool,
132
133 validate_integrity: Option<bool>,
135}
136
137impl<State> SyncBuilder<'_, State>
138where
139 State: sync_builder::State + sync_builder::IsComplete,
140{
141 pub async fn sync(self) -> Result<SyncReport, SyncError> {
142 let args = self._build();
143 match args.mode {
144 SyncMode::Open => sync_open(&args).await,
145 SyncMode::Frozen => sync_frozen(&args).await,
146 }
147 }
148}
149
150async fn sync_open(args: &Sync<'_>) -> Result<SyncReport, SyncError> {
151 let config = args.config;
152 let workspace = args.workspace;
153
154 let tree = workspace.tree(config)?;
155 let build_tree = tree.build_tree(config)?;
156
157 let regular = gather_dependencies(
158 workspace,
159 DependencyKind::Regular,
160 &workspace.member_names()?,
161 )?
162 .into_iter()
163 .filter(|spec| !tree.match_rocks(&spec.package).is_ok_and(|m| m.is_found()))
164 .map(|spec| PackageInstallSpec {
165 build_behaviour: BuildBehaviour::Force,
166 ..spec
167 })
168 .collect::<Vec<_>>();
169
170 let build = gather_dependencies(workspace, DependencyKind::Build, &workspace.member_names()?)?
171 .into_iter()
172 .filter(|spec| {
173 !build_tree
174 .match_rocks(&spec.package)
175 .is_ok_and(|m| m.is_found())
176 })
177 .map(|spec| PackageInstallSpec {
178 build_behaviour: BuildBehaviour::Force,
179 ..spec
180 })
181 .collect::<Vec<_>>();
182
183 let test_tree = tree.test_tree(config)?;
184 let test = if args.targets.test {
185 gather_dependencies(workspace, DependencyKind::Test, &args.targets.members)?
186 .into_iter()
187 .filter(|spec| {
188 !test_tree
189 .match_rocks(&spec.package)
190 .is_ok_and(|m| m.is_found())
191 })
192 .map(|spec| PackageInstallSpec {
193 build_behaviour: BuildBehaviour::Force,
194 ..spec
195 })
196 .collect::<Vec<_>>()
197 } else {
198 Vec::new()
199 };
200
201 let package_db = PackageDB::open(config, workspace).await?;
202
203 let mut install = InstallPackages::new(config, &tree)
204 .packages(regular)
205 .build_packages(build)
206 .package_db(package_db);
207 if !test.is_empty() {
208 install = install.test_packages(test);
209 }
210
211 let (mut added, lockfile) = install.install().await?;
212
213 if !args.no_lock {
214 let mut workspace_lockfile = workspace.lockfile()?.write_guard();
215 lockfile.commit(&mut workspace_lockfile);
216 if args.targets.test {
217 workspace_lockfile.sync(
218 test_tree.lockfile()?.local_pkg_lock(),
219 &LockedPackageLockType::Test,
220 );
221 }
222 }
223
224 if !args.only_deps {
225 let projects = args
226 .targets
227 .members
228 .iter()
229 .map(|name| workspace.select_member(name))
230 .collect::<Result<Vec<_>, _>>()?;
231 for project in projects {
232 added.push(
233 InstallProject::new()
234 .project(project)
235 .config(config)
236 .tree(&tree)
237 .behaviour(args.behaviour)
238 .workspace(workspace)
239 .build()
240 .await?,
241 );
242 }
243 }
244
245 if !args.no_lock {
246 GenLuaRc::new()
247 .config(config)
248 .workspace(workspace)
249 .generate_luarc()
250 .await?;
251 }
252
253 Ok(SyncReport {
254 added,
255 removed: Vec::new(),
256 })
257}
258
259#[derive(Clone, Copy)]
260pub(crate) enum DependencyKind {
261 Regular,
262 Build,
263 Test,
264}
265
266pub(crate) fn gather_dependencies(
267 workspace: &Workspace,
268 kind: DependencyKind,
269 members: &NonEmpty<PackageName>,
270) -> Result<Vec<PackageInstallSpec>, SyncError> {
271 let mut packages = Vec::new();
272 for project in workspace.members() {
273 if !members.contains(project.toml().package()) {
274 continue;
275 }
276 let toml = project.toml().into_local()?;
277 if let DependencyKind::Test = kind {
278 let explicit = toml.test_dependencies().current_platform();
279 for dependency in toml.test().current_platform().test_dependencies(project) {
280 if explicit.iter().any(|dep| dep.name() == dependency.name()) {
281 continue;
282 }
283 packages.push(
284 PackageInstallSpec::new(dependency, EntryType::Entrypoint)
285 .build_behaviour(BuildBehaviour::Ignore)
286 .build(),
287 );
288 }
289 for dependency in explicit {
290 if dependency.name().eq(&PackageName::new("lua".into())) {
291 continue;
292 }
293 packages.push(
294 PackageInstallSpec::new(
295 dependency.package_req().clone(),
296 EntryType::Entrypoint,
297 )
298 .build_behaviour(BuildBehaviour::Ignore)
299 .pin(*dependency.pin())
300 .opt(*dependency.opt())
301 .maybe_source(dependency.source.clone())
302 .build(),
303 );
304 }
305 } else {
306 let dependencies = match kind {
307 DependencyKind::Regular => toml.dependencies().current_platform().clone(),
308 _ => toml.build_dependencies().current_platform().clone(),
309 };
310 for dependency in dependencies {
311 packages.push(
313 PackageInstallSpec::new(
314 dependency.package_req().clone(),
315 EntryType::Entrypoint,
316 )
317 .build_behaviour(BuildBehaviour::Ignore)
318 .maybe_source(dependency.source().clone())
319 .build(),
320 );
321 }
322 }
323 }
324 Ok(packages)
325}
326
327async fn sync_frozen(args: &Sync<'_>) -> Result<SyncReport, SyncError> {
328 let report = frozen::Sync::new(args.workspace, args.config)
329 .validate_integrity(args.validate_integrity.unwrap_or(true))
330 .test(args.targets.test)
331 .sync()
332 .await?;
333 Ok(report)
334}
335
336#[cfg(all(test, feature = "impure_tests"))]
337mod tests {
338 use std::path::PathBuf;
339
340 use assert_fs::prelude::{FileWriteStr, PathChild, PathCopy};
341
342 use crate::{
343 config::ConfigBuilder, lockfile::LockedPackageLockType, lua_version::LuaVersion,
344 package::PackageName, package_db::PackageDB, tree::InstallTree, workspace::Workspace,
345 };
346
347 use super::{Sync, SyncMode, TargetSet};
348
349 #[tokio::test]
350 async fn installs_workspace_dependencies() {
351 let sample = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
352 .join("resources/test/sample-projects/dependencies/");
353 let temp = assert_fs::TempDir::new().unwrap();
354 temp.copy_from(sample, &["**"]).unwrap();
355
356 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
357 let config = ConfigBuilder::new()
358 .unwrap()
359 .lua_version(Some(LuaVersion::Lua51))
360 .build()
361 .unwrap();
362
363 Sync::new(&workspace, &config)
364 .mode(SyncMode::Open)
365 .targets(TargetSet::all(&workspace, false))
366 .only_deps(true)
367 .sync()
368 .await
369 .unwrap();
370
371 let lockfile = workspace.lockfile().unwrap();
372 let rocks = lockfile.rocks(&LockedPackageLockType::Regular);
373 assert!(rocks
374 .values()
375 .any(|pkg| pkg.name().to_string() == "lua-cjson"));
376 assert!(rocks
377 .values()
378 .any(|pkg| pkg.name().to_string() == "plenary.nvim"));
379 }
380
381 #[tokio::test]
382 async fn install_honors_locked_version() {
383 let temp = assert_fs::TempDir::new().unwrap();
384 let toml = temp.child("lux.toml");
385 toml.write_str(
386 r#"
387 package = "locktest"
388 version = "0.1.0"
389 lua = ">=5.1"
390
391 [build]
392 type = "builtin"
393
394 [dependencies]
395 argparse = "==0.7.1-1"
396 "#,
397 )
398 .unwrap();
399
400 let config = ConfigBuilder::new()
401 .unwrap()
402 .lua_version(Some(LuaVersion::Lua51))
403 .build()
404 .unwrap();
405 let name = PackageName::from("argparse");
406
407 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
408 Sync::new(&workspace, &config)
409 .mode(SyncMode::Open)
410 .targets(TargetSet::all(&workspace, false))
411 .only_deps(true)
412 .sync()
413 .await
414 .unwrap();
415 let tree = workspace.tree(&config).unwrap();
416 assert_eq!(
417 tree.lockfile()
418 .unwrap()
419 .entrypoint(&name)
420 .unwrap()
421 .version()
422 .to_string(),
423 "0.7.1-1"
424 );
425
426 toml.write_str(
427 r#"
428 package = "locktest"
429 version = "0.1.0"
430 lua = ">=5.1"
431
432 [build]
433 type = "builtin"
434
435 [dependencies]
436 argparse = ">=0.7.1"
437 "#,
438 )
439 .unwrap();
440 std::fs::remove_dir_all(tree.root()).unwrap();
441
442 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
443 Sync::new(&workspace, &config)
444 .mode(SyncMode::Open)
445 .targets(TargetSet::all(&workspace, false))
446 .only_deps(true)
447 .sync()
448 .await
449 .unwrap();
450 let tree = workspace.tree(&config).unwrap();
451 assert_eq!(
452 tree.lockfile()
453 .unwrap()
454 .entrypoint(&name)
455 .unwrap()
456 .version()
457 .to_string(),
458 "0.7.1-1"
459 );
460 }
461
462 #[tokio::test]
463 async fn install_without_lock_resolves_latest() {
464 let temp = assert_fs::TempDir::new().unwrap();
465 temp.child("lux.toml")
466 .write_str(
467 r#"
468 package = "locktest"
469 version = "0.1.0"
470 lua = ">=5.1"
471
472 [build]
473 type = "builtin"
474
475 [dependencies]
476 argparse = ">=0.7.1"
477 "#,
478 )
479 .unwrap();
480
481 let config = ConfigBuilder::new()
482 .unwrap()
483 .lua_version(Some(LuaVersion::Lua51))
484 .build()
485 .unwrap();
486 let name = PackageName::from("argparse");
487
488 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
489 Sync::new(&workspace, &config)
490 .mode(SyncMode::Open)
491 .targets(TargetSet::all(&workspace, false))
492 .only_deps(true)
493 .sync()
494 .await
495 .unwrap();
496 let tree = workspace.tree(&config).unwrap();
497 let installed = tree
498 .lockfile()
499 .unwrap()
500 .entrypoint(&name)
501 .unwrap()
502 .version()
503 .clone();
504 let latest = PackageDB::from_config(&config)
505 .await
506 .unwrap()
507 .latest_version(&name)
508 .await
509 .unwrap();
510 assert_eq!(installed, latest);
511 assert_ne!(installed.to_string(), "0.7.1-1");
512 }
513
514 #[tokio::test]
515 async fn replaces_dependency_on_version_change() {
516 let temp = assert_fs::TempDir::new().unwrap();
517 let toml = temp.child("lux.toml");
518 toml.write_str(
519 r#"
520 package = "versionchange"
521 version = "0.1.0"
522 lua = ">=5.1"
523
524 [build]
525 type = "builtin"
526
527 [dependencies]
528 argparse = "==0.7.1-1"
529 "#,
530 )
531 .unwrap();
532
533 let config = ConfigBuilder::new()
534 .unwrap()
535 .lua_version(Some(LuaVersion::Lua51))
536 .build()
537 .unwrap();
538 let name = PackageName::from("argparse");
539
540 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
541 Sync::new(&workspace, &config)
542 .mode(SyncMode::Open)
543 .targets(TargetSet::all(&workspace, false))
544 .only_deps(true)
545 .sync()
546 .await
547 .unwrap();
548 let tree = workspace.tree(&config).unwrap();
549 assert_eq!(
550 tree.lockfile()
551 .unwrap()
552 .entrypoint(&name)
553 .unwrap()
554 .version()
555 .to_string(),
556 "0.7.1-1"
557 );
558
559 toml.write_str(
560 r#"
561 package = "versionchange"
562 version = "0.1.0"
563 lua = ">=5.1"
564
565 [build]
566 type = "builtin"
567
568 [dependencies]
569 argparse = "==0.7.2-1"
570 "#,
571 )
572 .unwrap();
573
574 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
575 Sync::new(&workspace, &config)
576 .mode(SyncMode::Open)
577 .targets(TargetSet::all(&workspace, false))
578 .only_deps(true)
579 .sync()
580 .await
581 .unwrap();
582 let tree = workspace.tree(&config).unwrap();
583 assert_eq!(
584 tree.lockfile()
585 .unwrap()
586 .entrypoint(&name)
587 .unwrap()
588 .version()
589 .to_string(),
590 "0.7.2-1"
591 );
592 }
593}