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