1use self::reconcile::SyncReport;
2use crate::package;
3use bon::Builder;
4use miette::Diagnostic;
5use nonempty::NonEmpty;
6use thiserror::Error;
7
8mod frozen;
9pub(crate) mod reconcile;
10
11use crate::{
12 build::BuildBehaviour,
13 config::Config,
14 drivers::install_packages::{self, InstallPackages},
15 lockfile::LockedPackageLockType,
16 operations::{
17 GenLuaRc, GenLuaRcError, GenLuauRc, GenLuauRcError, InstallProject, InstallProjectError,
18 PackageInstallSpec,
19 },
20 package_db::{self, PackageDB},
21 project::project_toml::LocalProjectTomlValidationError,
22 rockspec::Rockspec,
23 tree::{self, EntryType, InstallTree},
24 workspace::{self, TreeError, Workspace},
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<package::Name>,
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: package::Name, 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] TreeError),
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<package::Name>,
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(&package::Name::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 crate::package;
350 use std::path::PathBuf;
351
352 use assert_fs::prelude::{FileWriteStr, PathChild, PathCopy};
353
354 use crate::{
355 config::ConfigBuilder, lockfile::LockedPackageLockType, lua_version::LuaVersion,
356 package_db::PackageDB, tree::InstallTree, workspace::Workspace,
357 };
358
359 use super::{Sync, SyncMode, TargetSet};
360
361 #[tokio::test]
362 async fn installs_workspace_dependencies() {
363 let sample = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
364 .join("resources/test/sample-projects/dependencies/");
365 let temp = assert_fs::TempDir::new().unwrap();
366 temp.copy_from(sample, &["**"]).unwrap();
367
368 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
369 let config = ConfigBuilder::new()
370 .unwrap()
371 .lua_version(Some(LuaVersion::Lua51))
372 .build()
373 .unwrap();
374
375 Sync::new(&workspace, &config)
376 .mode(SyncMode::Open)
377 .targets(TargetSet::all(&workspace, false))
378 .only_deps(true)
379 .sync()
380 .await
381 .unwrap();
382
383 let lockfile = workspace.lockfile().unwrap();
384 let rocks = lockfile.rocks(&LockedPackageLockType::Regular);
385 assert!(rocks
386 .values()
387 .any(|pkg| pkg.name().to_string() == "lua-cjson"));
388 assert!(rocks
389 .values()
390 .any(|pkg| pkg.name().to_string() == "plenary.nvim"));
391 }
392
393 #[tokio::test]
394 async fn install_honors_locked_version() {
395 let temp = assert_fs::TempDir::new().unwrap();
396 let toml = temp.child("lux.toml");
397 toml.write_str(
398 r#"
399 package = "locktest"
400 version = "0.1.0"
401 lua = ">=5.1"
402
403 [build]
404 type = "builtin"
405
406 [dependencies]
407 argparse = "==0.7.1-1"
408 "#,
409 )
410 .unwrap();
411
412 let config = ConfigBuilder::new()
413 .unwrap()
414 .lua_version(Some(LuaVersion::Lua51))
415 .build()
416 .unwrap();
417 let name = package::Name::from("argparse");
418
419 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
420 Sync::new(&workspace, &config)
421 .mode(SyncMode::Open)
422 .targets(TargetSet::all(&workspace, false))
423 .only_deps(true)
424 .sync()
425 .await
426 .unwrap();
427 let tree = workspace.tree(&config).unwrap();
428 assert_eq!(
429 tree.lockfile()
430 .unwrap()
431 .entrypoint(&name)
432 .unwrap()
433 .version()
434 .to_string(),
435 "0.7.1-1"
436 );
437
438 toml.write_str(
439 r#"
440 package = "locktest"
441 version = "0.1.0"
442 lua = ">=5.1"
443
444 [build]
445 type = "builtin"
446
447 [dependencies]
448 argparse = ">=0.7.1"
449 "#,
450 )
451 .unwrap();
452 std::fs::remove_dir_all(tree.root()).unwrap();
453
454 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
455 Sync::new(&workspace, &config)
456 .mode(SyncMode::Open)
457 .targets(TargetSet::all(&workspace, false))
458 .only_deps(true)
459 .sync()
460 .await
461 .unwrap();
462 let tree = workspace.tree(&config).unwrap();
463 assert_eq!(
464 tree.lockfile()
465 .unwrap()
466 .entrypoint(&name)
467 .unwrap()
468 .version()
469 .to_string(),
470 "0.7.1-1"
471 );
472 }
473
474 #[tokio::test]
475 async fn install_without_lock_resolves_latest() {
476 let temp = assert_fs::TempDir::new().unwrap();
477 temp.child("lux.toml")
478 .write_str(
479 r#"
480 package = "locktest"
481 version = "0.1.0"
482 lua = ">=5.1"
483
484 [build]
485 type = "builtin"
486
487 [dependencies]
488 argparse = ">=0.7.1"
489 "#,
490 )
491 .unwrap();
492
493 let config = ConfigBuilder::new()
494 .unwrap()
495 .lua_version(Some(LuaVersion::Lua51))
496 .build()
497 .unwrap();
498 let name = package::Name::from("argparse");
499
500 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
501 Sync::new(&workspace, &config)
502 .mode(SyncMode::Open)
503 .targets(TargetSet::all(&workspace, false))
504 .only_deps(true)
505 .sync()
506 .await
507 .unwrap();
508 let tree = workspace.tree(&config).unwrap();
509 let installed = tree
510 .lockfile()
511 .unwrap()
512 .entrypoint(&name)
513 .unwrap()
514 .version()
515 .clone();
516 let latest = PackageDB::from_config(&config)
517 .await
518 .unwrap()
519 .latest_version(&name)
520 .await
521 .unwrap();
522 assert_eq!(installed, latest);
523 assert_ne!(installed.to_string(), "0.7.1-1");
524 }
525
526 #[tokio::test]
527 async fn replaces_dependency_on_version_change() {
528 let temp = assert_fs::TempDir::new().unwrap();
529 let toml = temp.child("lux.toml");
530 toml.write_str(
531 r#"
532 package = "versionchange"
533 version = "0.1.0"
534 lua = ">=5.1"
535
536 [build]
537 type = "builtin"
538
539 [dependencies]
540 argparse = "==0.7.1-1"
541 "#,
542 )
543 .unwrap();
544
545 let config = ConfigBuilder::new()
546 .unwrap()
547 .lua_version(Some(LuaVersion::Lua51))
548 .build()
549 .unwrap();
550 let name = package::Name::from("argparse");
551
552 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
553 Sync::new(&workspace, &config)
554 .mode(SyncMode::Open)
555 .targets(TargetSet::all(&workspace, false))
556 .only_deps(true)
557 .sync()
558 .await
559 .unwrap();
560 let tree = workspace.tree(&config).unwrap();
561 assert_eq!(
562 tree.lockfile()
563 .unwrap()
564 .entrypoint(&name)
565 .unwrap()
566 .version()
567 .to_string(),
568 "0.7.1-1"
569 );
570
571 toml.write_str(
572 r#"
573 package = "versionchange"
574 version = "0.1.0"
575 lua = ">=5.1"
576
577 [build]
578 type = "builtin"
579
580 [dependencies]
581 argparse = "==0.7.2-1"
582 "#,
583 )
584 .unwrap();
585
586 let workspace = Workspace::from_exact(temp.path()).unwrap().unwrap();
587 Sync::new(&workspace, &config)
588 .mode(SyncMode::Open)
589 .targets(TargetSet::all(&workspace, false))
590 .only_deps(true)
591 .sync()
592 .await
593 .unwrap();
594 let tree = workspace.tree(&config).unwrap();
595 assert_eq!(
596 tree.lockfile()
597 .unwrap()
598 .entrypoint(&name)
599 .unwrap()
600 .version()
601 .to_string(),
602 "0.7.2-1"
603 );
604 }
605}