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::{self, InstallPackages},
14 lockfile::LockedPackageLockType,
15 operations::{
16 GenLuaRc, GenLuaRcError, GenLuauRc, GenLuauRcError, InstallProject, InstallProjectError,
17 PackageInstallSpec,
18 },
19 package::PackageName,
20 package_db::{self, PackageDB},
21 project::project_toml::LocalProjectTomlValidationError,
22 rockspec::Rockspec,
23 tree::{self, EntryType, InstallTree},
24 workspace::{self, Workspace, 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 Error {
63 #[error(transparent)]
64 #[diagnostic(transparent)]
65 InstallPackages(#[from] Box<install_packages::Error>),
66 #[error(transparent)]
67 #[diagnostic(transparent)]
68 InstallProject(Box<InstallProjectError>),
69 #[error(transparent)]
70 #[diagnostic(transparent)]
71 Workspace(#[from] workspace::Error),
72 #[error(transparent)]
73 #[diagnostic(transparent)]
74 WorkspaceTree(#[from] WorkspaceTreeError),
75 #[error(transparent)]
76 #[diagnostic(transparent)]
77 Tree(#[from] tree::Error),
78 #[error(transparent)]
79 #[diagnostic(transparent)]
80 Project(#[from] LocalProjectTomlValidationError),
81 #[error(transparent)]
82 #[diagnostic(transparent)]
83 PackageDB(#[from] package_db::Error),
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<install_packages::Error> for Error {
96 fn from(source: install_packages::Error) -> Self {
97 Self::InstallPackages(Box::new(source))
98 }
99}
100
101impl From<InstallProjectError> for Error {
102 fn from(source: InstallProjectError) -> Self {
103 Self::InstallProject(Box::new(source))
104 }
105}
106
107impl From<frozen::SyncError> for Error {
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, Error> {
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, Error> {
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>, Error> {
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 .pin(*dependency.pin())
328 .opt(*dependency.opt())
329 .maybe_source(dependency.source().clone())
330 .build(),
331 );
332 }
333 }
334 }
335 Ok(packages)
336}
337
338async fn sync_frozen(args: &Sync<'_>) -> Result<SyncReport, Error> {
339 let report = frozen::Sync::new(args.workspace, args.config)
340 .validate_integrity(args.validate_integrity.unwrap_or(true))
341 .test(args.targets.test)
342 .sync()
343 .await?;
344 Ok(report)
345}
346
347#[cfg(all(test, feature = "impure_tests"))]
348mod tests {
349 use std::path::PathBuf;
350
351 use assert_fs::prelude::{FileWriteStr, PathChild, PathCopy};
352
353 use crate::{
354 config::ConfigBuilder, lockfile::LockedPackageLockType, lua_version::LuaVersion,
355 package::PackageName, package_db::PackageDB, tree::InstallTree, workspace::Workspace,
356 };
357
358 use super::{Sync, SyncMode, TargetSet};
359
360 #[tokio::test]
361 async fn installs_workspace_dependencies() {
362 let sample = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
363 .join("resources/test/sample-projects/dependencies/");
364 let temp = assert_fs::TempDir::new().unwrap();
365 temp.copy_from(sample, &["**"]).unwrap();
366
367 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
368 let config = ConfigBuilder::new()
369 .unwrap()
370 .lua_version(Some(LuaVersion::Lua51))
371 .build()
372 .unwrap();
373
374 Sync::new(&workspace, &config)
375 .mode(SyncMode::Open)
376 .targets(TargetSet::all(&workspace, false))
377 .only_deps(true)
378 .sync()
379 .await
380 .unwrap();
381
382 let lockfile = workspace.lockfile().unwrap();
383 let rocks = lockfile.rocks(&LockedPackageLockType::Regular);
384 assert!(rocks
385 .values()
386 .any(|pkg| pkg.name().to_string() == "lua-cjson"));
387 assert!(rocks
388 .values()
389 .any(|pkg| pkg.name().to_string() == "plenary.nvim"));
390 }
391
392 #[tokio::test]
393 async fn install_honors_locked_version() {
394 let temp = assert_fs::TempDir::new().unwrap();
395 let toml = temp.child("lux.toml");
396 toml.write_str(
397 r#"
398 package = "locktest"
399 version = "0.1.0"
400 lua = ">=5.1"
401
402 [build]
403 type = "builtin"
404
405 [dependencies]
406 argparse = "==0.7.1-1"
407 "#,
408 )
409 .unwrap();
410
411 let config = ConfigBuilder::new()
412 .unwrap()
413 .lua_version(Some(LuaVersion::Lua51))
414 .build()
415 .unwrap();
416 let name = PackageName::from("argparse");
417
418 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
419 Sync::new(&workspace, &config)
420 .mode(SyncMode::Open)
421 .targets(TargetSet::all(&workspace, false))
422 .only_deps(true)
423 .sync()
424 .await
425 .unwrap();
426 let tree = workspace.tree(&config).unwrap();
427 assert_eq!(
428 tree.lockfile()
429 .unwrap()
430 .entrypoint(&name)
431 .unwrap()
432 .version()
433 .to_string(),
434 "0.7.1-1"
435 );
436
437 toml.write_str(
438 r#"
439 package = "locktest"
440 version = "0.1.0"
441 lua = ">=5.1"
442
443 [build]
444 type = "builtin"
445
446 [dependencies]
447 argparse = ">=0.7.1"
448 "#,
449 )
450 .unwrap();
451 std::fs::remove_dir_all(tree.root()).unwrap();
452
453 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
454 Sync::new(&workspace, &config)
455 .mode(SyncMode::Open)
456 .targets(TargetSet::all(&workspace, false))
457 .only_deps(true)
458 .sync()
459 .await
460 .unwrap();
461 let tree = workspace.tree(&config).unwrap();
462 assert_eq!(
463 tree.lockfile()
464 .unwrap()
465 .entrypoint(&name)
466 .unwrap()
467 .version()
468 .to_string(),
469 "0.7.1-1"
470 );
471 }
472
473 #[tokio::test]
474 async fn install_without_lock_resolves_latest() {
475 let temp = assert_fs::TempDir::new().unwrap();
476 temp.child("lux.toml")
477 .write_str(
478 r#"
479 package = "locktest"
480 version = "0.1.0"
481 lua = ">=5.1"
482
483 [build]
484 type = "builtin"
485
486 [dependencies]
487 argparse = ">=0.7.1"
488 "#,
489 )
490 .unwrap();
491
492 let config = ConfigBuilder::new()
493 .unwrap()
494 .lua_version(Some(LuaVersion::Lua51))
495 .build()
496 .unwrap();
497 let name = PackageName::from("argparse");
498
499 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
500 Sync::new(&workspace, &config)
501 .mode(SyncMode::Open)
502 .targets(TargetSet::all(&workspace, false))
503 .only_deps(true)
504 .sync()
505 .await
506 .unwrap();
507 let tree = workspace.tree(&config).unwrap();
508 let installed = tree
509 .lockfile()
510 .unwrap()
511 .entrypoint(&name)
512 .unwrap()
513 .version()
514 .clone();
515 let latest = PackageDB::from_config(&config)
516 .await
517 .unwrap()
518 .latest_version(&name)
519 .await
520 .unwrap();
521 assert_eq!(installed, latest);
522 assert_ne!(installed.to_string(), "0.7.1-1");
523 }
524
525 #[tokio::test]
526 async fn replaces_dependency_on_version_change() {
527 let temp = assert_fs::TempDir::new().unwrap();
528 let toml = temp.child("lux.toml");
529 toml.write_str(
530 r#"
531 package = "versionchange"
532 version = "0.1.0"
533 lua = ">=5.1"
534
535 [build]
536 type = "builtin"
537
538 [dependencies]
539 argparse = "==0.7.1-1"
540 "#,
541 )
542 .unwrap();
543
544 let config = ConfigBuilder::new()
545 .unwrap()
546 .lua_version(Some(LuaVersion::Lua51))
547 .build()
548 .unwrap();
549 let name = PackageName::from("argparse");
550
551 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
552 Sync::new(&workspace, &config)
553 .mode(SyncMode::Open)
554 .targets(TargetSet::all(&workspace, false))
555 .only_deps(true)
556 .sync()
557 .await
558 .unwrap();
559 let tree = workspace.tree(&config).unwrap();
560 assert_eq!(
561 tree.lockfile()
562 .unwrap()
563 .entrypoint(&name)
564 .unwrap()
565 .version()
566 .to_string(),
567 "0.7.1-1"
568 );
569
570 toml.write_str(
571 r#"
572 package = "versionchange"
573 version = "0.1.0"
574 lua = ">=5.1"
575
576 [build]
577 type = "builtin"
578
579 [dependencies]
580 argparse = "==0.7.2-1"
581 "#,
582 )
583 .unwrap();
584
585 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
586 Sync::new(&workspace, &config)
587 .mode(SyncMode::Open)
588 .targets(TargetSet::all(&workspace, false))
589 .only_deps(true)
590 .sync()
591 .await
592 .unwrap();
593 let tree = workspace.tree(&config).unwrap();
594 assert_eq!(
595 tree.lockfile()
596 .unwrap()
597 .entrypoint(&name)
598 .unwrap()
599 .version()
600 .to_string(),
601 "0.7.2-1"
602 );
603 }
604}