1use std::collections::{BTreeMap, HashSet};
2use std::error::Error;
3use std::fmt::Display;
4use std::io::{self, Write};
5use std::marker::PhantomData;
6use std::ops::{Deref, DerefMut};
7use std::{collections::HashMap, fs::File, io::ErrorKind, path::PathBuf};
8
9use itertools::Itertools;
10
11use miette::Diagnostic;
12use serde::{de, Deserialize, Serialize, Serializer};
13use sha2::{Digest, Sha256};
14use ssri::Integrity;
15use strum_macros::EnumIter;
16use thiserror::Error;
17use url::Url;
18
19use crate::config::tree::RockLayoutConfig;
20use crate::fs;
21use crate::package::{
22 PackageName, PackageReq, PackageSpec, PackageVersion, PackageVersionReq,
23 PackageVersionReqError, RemotePackageTypeFilterSpec,
24};
25use crate::remote_package_source::RemotePackageSource;
26use crate::rockspec::lua_dependency::LuaDependencySpec;
27use crate::rockspec::RockBinaries;
28use crate::tree::{InstallTree, Tree};
29
30const LOCKFILE_VERSION_STR: &str = "1.0.0";
31
32#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord, Default)]
33pub enum PinnedState {
34 #[default]
36 Unpinned,
37 Pinned,
39}
40
41impl Display for PinnedState {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match &self {
44 PinnedState::Unpinned => "unpinned".fmt(f),
45 PinnedState::Pinned => "pinned".fmt(f),
46 }
47 }
48}
49
50impl From<bool> for PinnedState {
51 fn from(value: bool) -> Self {
52 if value {
53 Self::Pinned
54 } else {
55 Self::Unpinned
56 }
57 }
58}
59
60impl PinnedState {
61 pub fn as_bool(&self) -> bool {
62 match self {
63 Self::Unpinned => false,
64 Self::Pinned => true,
65 }
66 }
67}
68
69impl Serialize for PinnedState {
70 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
71 where
72 S: serde::Serializer,
73 {
74 serializer.serialize_bool(self.as_bool())
75 }
76}
77
78impl<'de> Deserialize<'de> for PinnedState {
79 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
80 where
81 D: serde::Deserializer<'de>,
82 {
83 Ok(match bool::deserialize(deserializer)? {
84 false => Self::Unpinned,
85 true => Self::Pinned,
86 })
87 }
88}
89
90#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord, Default)]
91pub enum OptState {
92 #[default]
94 Required,
95 Optional,
97}
98
99impl OptState {
100 pub(crate) fn as_bool(&self) -> bool {
101 match self {
102 Self::Required => false,
103 Self::Optional => true,
104 }
105 }
106}
107
108impl From<bool> for OptState {
109 fn from(value: bool) -> Self {
110 if value {
111 Self::Optional
112 } else {
113 Self::Required
114 }
115 }
116}
117
118impl Display for OptState {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 match &self {
121 OptState::Required => "required".fmt(f),
122 OptState::Optional => "optional".fmt(f),
123 }
124 }
125}
126
127impl Serialize for OptState {
128 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
129 where
130 S: serde::Serializer,
131 {
132 serializer.serialize_bool(self.as_bool())
133 }
134}
135
136impl<'de> Deserialize<'de> for OptState {
137 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
138 where
139 D: serde::Deserializer<'de>,
140 {
141 Ok(match bool::deserialize(deserializer)? {
142 false => Self::Required,
143 true => Self::Optional,
144 })
145 }
146}
147
148#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
149pub(crate) struct LocalPackageSpec {
150 pub name: PackageName,
151 pub version: PackageVersion,
152 pub pinned: PinnedState,
153 pub opt: OptState,
154 pub dependencies: Vec<LocalPackageId>,
155 pub constraint: Option<String>,
157 pub binaries: RockBinaries,
158}
159
160#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Clone)]
167pub struct LocalPackageId(String);
168
169impl LocalPackageId {
170 pub fn new(
171 name: &PackageName,
172 version: &PackageVersion,
173 pinned: PinnedState,
174 opt: OptState,
175 constraint: LockConstraint,
176 ) -> Self {
177 let mut hasher = Sha256::new();
178
179 hasher.update(format!(
180 "{}{}{}{}{}",
181 name,
182 version,
183 pinned.as_bool(),
184 opt.as_bool(),
185 match constraint {
186 LockConstraint::Unconstrained => String::default(),
187 LockConstraint::Constrained(version_req) => version_req.to_string(),
188 },
189 ));
190
191 Self(hex::encode(hasher.finalize()))
192 }
193
194 pub unsafe fn from_unchecked(str: String) -> Self {
202 Self(str)
203 }
204
205 pub fn into_string(self) -> String {
206 self.0
207 }
208}
209
210impl Display for LocalPackageId {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 self.0.fmt(f)
213 }
214}
215
216impl LocalPackageSpec {
217 pub fn new(
218 name: &PackageName,
219 version: &PackageVersion,
220 constraint: LockConstraint,
221 dependencies: Vec<LocalPackageId>,
222 pinned: &PinnedState,
223 opt: &OptState,
224 binaries: RockBinaries,
225 ) -> Self {
226 Self {
227 name: name.clone(),
228 version: version.clone(),
229 pinned: *pinned,
230 opt: *opt,
231 dependencies,
232 constraint: match constraint {
233 LockConstraint::Unconstrained => None,
234 LockConstraint::Constrained(version_req) => Some(version_req.to_string()),
235 },
236 binaries,
237 }
238 }
239
240 pub fn id(&self) -> LocalPackageId {
241 LocalPackageId::new(
242 self.name(),
243 self.version(),
244 self.pinned,
245 self.opt,
246 match &self.constraint {
247 None => LockConstraint::Unconstrained,
248 Some(_) => self.constraint(),
249 },
250 )
251 }
252
253 pub fn constraint(&self) -> LockConstraint {
254 unsafe { LockConstraint::try_from(&self.constraint).unwrap_unchecked() }
256 }
257
258 pub fn name(&self) -> &PackageName {
259 &self.name
260 }
261
262 pub fn version(&self) -> &PackageVersion {
263 &self.version
264 }
265
266 pub fn pinned(&self) -> PinnedState {
267 self.pinned
268 }
269
270 pub fn opt(&self) -> OptState {
271 self.opt
272 }
273
274 pub fn dependencies(&self) -> Vec<&LocalPackageId> {
275 self.dependencies.iter().collect()
276 }
277
278 pub fn binaries(&self) -> Vec<&PathBuf> {
279 self.binaries.iter().collect()
280 }
281
282 pub fn to_package(&self) -> PackageSpec {
283 PackageSpec::new(self.name.clone(), self.version.clone())
284 }
285
286 pub fn into_package_req(self) -> PackageReq {
287 PackageSpec::new(self.name, self.version).into_package_req()
288 }
289}
290
291#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
292#[serde(rename_all = "lowercase", tag = "type")]
293pub(crate) enum RemotePackageSourceUrl {
294 Git {
295 url: String,
296 #[serde(rename = "ref")]
297 checkout_ref: String,
298 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
301 submodules: bool,
302 }, Url {
304 #[serde(deserialize_with = "deserialize_url", serialize_with = "serialize_url")]
305 url: Url,
306 },
307 File {
308 path: PathBuf,
309 },
310}
311
312impl Display for RemotePackageSourceUrl {
313 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 match self {
315 RemotePackageSourceUrl::Git {
316 url, checkout_ref, ..
317 } => format!("{url}@{checkout_ref}").fmt(f),
318 RemotePackageSourceUrl::Url { url } => url.fmt(f),
319 RemotePackageSourceUrl::File { path } => format!("{}", path.display()).fmt(f),
320 }
321 }
322}
323
324#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
328pub struct LocalPackage {
329 pub(crate) spec: LocalPackageSpec,
330 pub(crate) source: RemotePackageSource,
331 pub(crate) source_url: Option<RemotePackageSourceUrl>,
332 hashes: LocalPackageHashes,
333}
334
335impl LocalPackage {
336 pub fn into_package_spec(self) -> PackageSpec {
337 PackageSpec::new(self.spec.name, self.spec.version)
338 }
339
340 pub fn as_package_spec(&self) -> PackageSpec {
341 PackageSpec::new(self.spec.name.clone(), self.spec.version.clone())
342 }
343}
344
345#[derive(Debug, Serialize, Deserialize, Clone)]
346struct LocalPackageIntermediate {
347 name: PackageName,
348 version: PackageVersion,
349 pinned: PinnedState,
350 opt: OptState,
351 dependencies: Vec<LocalPackageId>,
352 constraint: Option<String>,
353 binaries: RockBinaries,
354 source: RemotePackageSource,
355 source_url: Option<RemotePackageSourceUrl>,
356 hashes: LocalPackageHashes,
357}
358
359impl TryFrom<LocalPackageIntermediate> for LocalPackage {
360 type Error = LockConstraintParseError;
361
362 fn try_from(value: LocalPackageIntermediate) -> Result<Self, Self::Error> {
363 let constraint = LockConstraint::try_from(&value.constraint)?;
364 Ok(Self {
365 spec: LocalPackageSpec::new(
366 &value.name,
367 &value.version,
368 constraint,
369 value.dependencies,
370 &value.pinned,
371 &value.opt,
372 value.binaries,
373 ),
374 source: value.source,
375 source_url: value.source_url,
376 hashes: value.hashes,
377 })
378 }
379}
380
381impl From<&LocalPackage> for LocalPackageIntermediate {
382 fn from(value: &LocalPackage) -> Self {
383 Self {
384 name: value.spec.name.clone(),
385 version: value.spec.version.clone(),
386 pinned: value.spec.pinned,
387 opt: value.spec.opt,
388 dependencies: value.spec.dependencies.clone(),
389 constraint: value.spec.constraint.clone(),
390 binaries: value.spec.binaries.clone(),
391 source: value.source.clone(),
392 source_url: value.source_url.clone(),
393 hashes: value.hashes.clone(),
394 }
395 }
396}
397
398impl<'de> Deserialize<'de> for LocalPackage {
399 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
400 where
401 D: serde::Deserializer<'de>,
402 {
403 LocalPackage::try_from(LocalPackageIntermediate::deserialize(deserializer)?)
404 .map_err(de::Error::custom)
405 }
406}
407
408impl Serialize for LocalPackage {
409 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
410 where
411 S: serde::Serializer,
412 {
413 LocalPackageIntermediate::from(self).serialize(serializer)
414 }
415}
416
417impl LocalPackage {
418 pub(crate) fn from(
419 package: &PackageSpec,
420 constraint: LockConstraint,
421 binaries: RockBinaries,
422 source: RemotePackageSource,
423 source_url: Option<RemotePackageSourceUrl>,
424 hashes: LocalPackageHashes,
425 ) -> Self {
426 Self {
427 spec: LocalPackageSpec::new(
428 package.name(),
429 package.version(),
430 constraint,
431 Vec::default(),
432 &PinnedState::Unpinned,
433 &OptState::Required,
434 binaries,
435 ),
436 source,
437 source_url,
438 hashes,
439 }
440 }
441
442 pub fn id(&self) -> LocalPackageId {
443 self.spec.id()
444 }
445
446 pub fn name(&self) -> &PackageName {
447 self.spec.name()
448 }
449
450 pub fn version(&self) -> &PackageVersion {
451 self.spec.version()
452 }
453
454 pub fn pinned(&self) -> PinnedState {
455 self.spec.pinned()
456 }
457
458 pub fn opt(&self) -> OptState {
459 self.spec.opt()
460 }
461
462 pub(crate) fn source(&self) -> &RemotePackageSource {
463 &self.source
464 }
465
466 pub fn dependencies(&self) -> Vec<&LocalPackageId> {
467 self.spec.dependencies()
468 }
469
470 pub fn constraint(&self) -> LockConstraint {
471 self.spec.constraint()
472 }
473
474 pub fn hashes(&self) -> &LocalPackageHashes {
475 &self.hashes
476 }
477
478 pub fn to_package(&self) -> PackageSpec {
479 self.spec.to_package()
480 }
481
482 pub fn into_package_req(self) -> PackageReq {
483 self.spec.into_package_req()
484 }
485}
486
487#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Hash)]
488pub struct LocalPackageHashes {
489 pub rockspec: Integrity,
490 pub source: Integrity,
491}
492
493impl Ord for LocalPackageHashes {
494 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
495 let a = (self.rockspec.to_hex().1, self.source.to_hex().1);
496 let b = (other.rockspec.to_hex().1, other.source.to_hex().1);
497 a.cmp(&b)
498 }
499}
500
501impl PartialOrd for LocalPackageHashes {
502 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
503 Some(self.cmp(other))
504 }
505}
506
507#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
508pub enum LockConstraint {
509 #[default]
510 Unconstrained,
511 Constrained(PackageVersionReq),
512}
513
514impl<'de> Deserialize<'de> for LockConstraint {
515 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
516 where
517 D: serde::Deserializer<'de>,
518 {
519 let str = String::deserialize(deserializer)?;
520 match str.as_str() {
521 "*" => Ok(LockConstraint::Unconstrained),
522 _ => Ok(LockConstraint::Constrained(
523 str.parse().map_err(serde::de::Error::custom)?,
524 )),
525 }
526 }
527}
528
529impl LockConstraint {
530 pub fn to_string_opt(&self) -> Option<String> {
531 match self {
532 LockConstraint::Unconstrained => None,
533 LockConstraint::Constrained(req) => Some(req.to_string()),
534 }
535 }
536
537 fn matches_version_req(&self, req: &PackageVersionReq) -> bool {
538 match self {
539 LockConstraint::Unconstrained => req.is_any(),
540 LockConstraint::Constrained(package_version_req) => package_version_req == req,
541 }
542 }
543}
544
545impl From<PackageVersionReq> for LockConstraint {
546 fn from(value: PackageVersionReq) -> Self {
547 if value.is_any() {
548 Self::Unconstrained
549 } else {
550 Self::Constrained(value)
551 }
552 }
553}
554
555#[derive(Error, Debug, Diagnostic)]
556#[non_exhaustive]
557pub enum LockConstraintParseError {
558 #[error("Invalid constraint in LuaPackage")]
559 #[diagnostic(forward(0))]
560 LockConstraintParseError(#[from] PackageVersionReqError),
561}
562
563impl TryFrom<&Option<String>> for LockConstraint {
564 type Error = LockConstraintParseError;
565
566 fn try_from(constraint: &Option<String>) -> Result<Self, Self::Error> {
567 match constraint {
568 Some(constraint) => {
569 let package_version_req = constraint.parse()?;
570 Ok(LockConstraint::Constrained(package_version_req))
571 }
572 None => Ok(LockConstraint::Unconstrained),
573 }
574 }
575}
576
577pub trait LockfilePermissions {}
578#[derive(Clone)]
579pub struct ReadOnly;
580#[derive(Clone)]
581pub struct ReadWrite;
582
583impl LockfilePermissions for ReadOnly {}
584impl LockfilePermissions for ReadWrite {}
585
586#[derive(Clone, Debug, Serialize, Deserialize, Default)]
587pub(crate) struct LocalPackageLock {
588 rocks: BTreeMap<LocalPackageId, LocalPackage>,
591
592 #[serde(serialize_with = "serialize_sorted_package_ids")]
593 entrypoints: Vec<LocalPackageId>,
594}
595
596impl LocalPackageLock {
597 fn get(&self, id: &LocalPackageId) -> Option<&LocalPackage> {
598 self.rocks.get(id)
599 }
600
601 fn is_empty(&self) -> bool {
602 self.rocks.is_empty()
603 }
604
605 pub(crate) fn rocks(&self) -> &BTreeMap<LocalPackageId, LocalPackage> {
606 &self.rocks
607 }
608
609 fn is_entrypoint(&self, package: &LocalPackageId) -> bool {
610 self.entrypoints.contains(package)
611 }
612
613 fn is_dependency(&self, package: &LocalPackageId) -> bool {
614 self.rocks
615 .values()
616 .flat_map(|rock| rock.dependencies())
617 .any(|dep_id| dep_id == package)
618 }
619
620 fn list(&self) -> HashMap<PackageName, Vec<LocalPackage>> {
621 self.rocks()
622 .values()
623 .cloned()
624 .map(|locked_rock| (locked_rock.name().clone(), locked_rock))
625 .into_group_map()
626 }
627
628 fn remove(&mut self, target: &LocalPackage) {
629 self.remove_by_id(&target.id())
630 }
631
632 fn remove_by_id(&mut self, target: &LocalPackageId) {
633 self.rocks.remove(target);
634 self.entrypoints.retain(|x| x != target);
635 }
636
637 pub(crate) fn has_rock(
638 &self,
639 req: &PackageReq,
640 filter: Option<RemotePackageTypeFilterSpec>,
641 ) -> Option<LocalPackage> {
642 self.list()
643 .get(req.name())
644 .map(|packages| {
645 packages
646 .iter()
647 .filter(|package| match &filter {
648 Some(filter_spec) => match package.source {
649 RemotePackageSource::LuarocksRockspec(_) => filter_spec.rockspec,
650 RemotePackageSource::LuarocksSrcRock(_) => filter_spec.src,
651 RemotePackageSource::LuarocksBinaryRock(_) => filter_spec.binary,
652 RemotePackageSource::RockspecContent(_) => true,
653 RemotePackageSource::Local => true,
654 #[cfg(test)]
655 RemotePackageSource::Test => unimplemented!(),
656 },
657 None => true,
658 })
659 .rev()
660 .find(|package| req.version_req().matches(package.version()))
661 })?
662 .cloned()
663 }
664
665 fn has_rock_with_equal_constraint(&self, req: &LuaDependencySpec) -> Option<LocalPackage> {
666 self.list()
667 .get(req.name())
668 .map(|packages| {
669 packages
670 .iter()
671 .rev()
672 .find(|package| package.constraint().matches_version_req(req.version_req()))
673 })?
674 .cloned()
675 }
676
677 pub(crate) fn package_sync_spec(
684 &self,
685 packages: &[LuaDependencySpec],
686 strategy: &SyncStrategy<'_>,
687 ) -> PackageSyncSpec {
688 let pkg_dir_exists = |pkg: &LocalPackage| match strategy {
689 SyncStrategy::LockfileOnly => true,
690 SyncStrategy::EnsureInstalled(tree) => tree.root_for(pkg).is_dir(),
691 };
692
693 let entrypoints_to_keep: HashSet<LocalPackage> = self
694 .entrypoints
695 .iter()
696 .filter_map(|id| self.get(id))
697 .filter(|local_pkg| {
698 packages.iter().any(|req| {
699 local_pkg
700 .constraint()
701 .matches_version_req(req.version_req())
702 })
703 })
704 .cloned()
705 .collect();
706
707 let packages_to_keep: HashSet<&LocalPackage> = entrypoints_to_keep
708 .iter()
709 .flat_map(|local_pkg| self.get_all_dependencies(&local_pkg.id()))
710 .collect();
711
712 let to_add = packages
713 .iter()
714 .filter(|pkg| {
715 self.has_rock_with_equal_constraint(pkg)
716 .map(|local_pkg| !pkg_dir_exists(&local_pkg))
717 .unwrap_or(true)
718 })
719 .cloned()
720 .collect_vec();
721
722 let to_remove = self
723 .rocks()
724 .values()
725 .filter(|pkg| !packages_to_keep.contains(*pkg))
726 .cloned()
727 .collect_vec();
728
729 PackageSyncSpec { to_add, to_remove }
730 }
731
732 fn get_all_dependencies(&self, id: &LocalPackageId) -> HashSet<&LocalPackage> {
734 let mut packages = HashSet::new();
735 if let Some(local_pkg) = self.get(id) {
736 packages.insert(local_pkg);
737 packages.extend(
738 local_pkg
739 .dependencies()
740 .iter()
741 .flat_map(|id| self.get_all_dependencies(id)),
742 );
743 }
744 packages
745 }
746}
747
748#[derive(Clone, Debug, Serialize, Deserialize)]
750pub struct Lockfile<P: LockfilePermissions> {
751 #[serde(skip)]
752 filepath: PathBuf,
753 #[serde(skip)]
754 _marker: PhantomData<P>,
755 version: String,
757 #[serde(flatten)]
758 lock: LocalPackageLock,
759 #[serde(default, skip_serializing_if = "RockLayoutConfig::is_default")]
760 pub(crate) entrypoint_layout: RockLayoutConfig,
761}
762
763#[derive(EnumIter, Debug, PartialEq, Eq)]
764pub enum LocalPackageLockType {
765 Regular,
766 Test,
767 Build,
768}
769
770#[derive(Clone, Debug, Serialize, Deserialize)]
772pub struct WorkspaceLockfile<P: LockfilePermissions> {
773 #[serde(skip)]
774 filepath: PathBuf,
775 #[serde(skip)]
776 _marker: PhantomData<P>,
777 version: String,
778 #[serde(default, skip_serializing_if = "LocalPackageLock::is_empty")]
779 dependencies: LocalPackageLock,
780 #[serde(default, skip_serializing_if = "LocalPackageLock::is_empty")]
781 test_dependencies: LocalPackageLock,
782 #[serde(default, skip_serializing_if = "LocalPackageLock::is_empty")]
783 build_dependencies: LocalPackageLock,
784}
785
786#[derive(Error, Debug, Diagnostic)]
787#[non_exhaustive]
788pub enum LockfileError {
789 #[error(transparent)]
790 #[diagnostic(transparent)]
791 Fs(#[from] fs::FsError),
792 #[error("error parsing lockfile from JSON")]
793 ParseJson(#[source] serde_json::Error),
794 #[error("error writing lockfile to JSON")]
795 WriteJson(#[source] serde_json::Error),
796 #[error("attempt load to a lockfile that does not match the expected rock layout.")]
797 MismatchedRockLayout,
798}
799
800#[derive(Error, Debug, Diagnostic)]
801#[non_exhaustive]
802pub enum LockfileIntegrityError {
803 #[error(
804 r#"rockspec integrity mismatch.
805- source: {src}
806- expected: {expected}
807- got: {got}
808"#
809 )]
810 #[diagnostic(help(
811 r#"the maintainer may have force-uploaded a different rockspec.
812check the rockspec source, then rerun the command with `--no-lock` to update the hash.
813"#
814 ))]
815 RockspecIntegrityMismatch {
816 src: String,
817 expected: Integrity,
818 got: Integrity,
819 },
820 #[error(
821 r#"source integrity mismatch.
822- source: {src}
823- expected: {expected}
824- got: {got}"#
825 )]
826 #[diagnostic(help(
827 r#"the maintainer may have force-uploaded a different source or moved a tag.
828check the source URL, then rerun the command with `lx --no-lock` to update the hash.
829"#
830 ))]
831 SourceIntegrityMismatch {
832 src: String,
833 expected: Integrity,
834 got: Integrity,
835 },
836 #[error("package {0} version {1} with pinned state {2} and constraint {3} not found in the lockfile.")]
837 PackageNotFound(PackageName, PackageVersion, PinnedState, String),
838}
839
840#[derive(Error, Debug, Diagnostic)]
841#[non_exhaustive]
842#[error("error flushing the lockfile ({})", .filepath.display())]
843#[diagnostic(help("make sure the parent directory is writeable"))]
844pub struct FlushLockfileError {
845 filepath: PathBuf,
846 source: io::Error,
847}
848
849#[derive(Debug, Default)]
851pub(crate) struct PackageSyncSpec {
852 pub to_add: Vec<LuaDependencySpec>,
853 pub to_remove: Vec<LocalPackage>,
854}
855
856pub(crate) enum SyncStrategy<'a> {
858 LockfileOnly,
860 EnsureInstalled(&'a Tree),
863}
864
865impl<P: LockfilePermissions> Lockfile<P> {
866 pub fn version(&self) -> &String {
867 &self.version
868 }
869
870 pub fn rocks(&self) -> &BTreeMap<LocalPackageId, LocalPackage> {
871 self.lock.rocks()
872 }
873
874 pub fn is_dependency(&self, package: &LocalPackageId) -> bool {
875 self.lock.is_dependency(package)
876 }
877
878 pub fn is_entrypoint(&self, package: &LocalPackageId) -> bool {
879 self.lock.is_entrypoint(package)
880 }
881
882 pub fn entry_type(&self, package: &LocalPackageId) -> bool {
883 self.lock.is_entrypoint(package)
884 }
885
886 pub(crate) fn local_pkg_lock(&self) -> &LocalPackageLock {
887 &self.lock
888 }
889
890 pub fn get(&self, id: &LocalPackageId) -> Option<&LocalPackage> {
891 self.lock.get(id)
892 }
893
894 pub unsafe fn get_unchecked(&self, id: &LocalPackageId) -> &LocalPackage {
900 self.lock.get(id).unwrap_unchecked()
901 }
902
903 pub(crate) fn list(&self) -> HashMap<PackageName, Vec<LocalPackage>> {
904 self.lock.list()
905 }
906
907 pub(crate) fn has_rock(
908 &self,
909 req: &PackageReq,
910 filter: Option<RemotePackageTypeFilterSpec>,
911 ) -> Option<LocalPackage> {
912 self.lock.has_rock(req, filter)
913 }
914
915 pub(crate) fn find_rocks(&self, req: &PackageReq) -> Vec<LocalPackageId> {
917 match self.list().get(req.name()) {
918 Some(packages) => packages
919 .iter()
920 .rev()
921 .filter(|package| req.version_req().matches(package.version()))
922 .map(|package| package.id())
923 .collect_vec(),
924 None => Vec::default(),
925 }
926 }
927
928 pub(crate) fn validate_integrity(
930 &self,
931 expected_package: &LocalPackage,
932 ) -> Result<(), LockfileIntegrityError> {
933 match self.list().get(expected_package.name()) {
936 None => Err(integrity_err_not_found(expected_package)),
937 Some(rocks) => match rocks
938 .iter()
939 .find(|rock| rock.version() == expected_package.version())
940 {
941 None => Err(integrity_err_not_found(expected_package)),
942 Some(installed_package) => {
943 if expected_package
944 .hashes
945 .rockspec
946 .matches(&installed_package.hashes.rockspec)
947 .is_none()
948 {
949 return Err(LockfileIntegrityError::RockspecIntegrityMismatch {
950 src: expected_package.source.to_string(),
951 expected: expected_package.hashes.rockspec.clone(),
952 got: installed_package.hashes.rockspec.clone(),
953 });
954 }
955 if expected_package
956 .hashes
957 .source
958 .matches(&installed_package.hashes.source)
959 .is_none()
960 {
961 return Err(LockfileIntegrityError::SourceIntegrityMismatch {
962 src: expected_package
963 .source_url
964 .as_ref()
965 .map(|url| url.to_string())
966 .unwrap_or(expected_package.source.to_string()),
967 expected: expected_package.hashes.source.clone(),
968 got: installed_package.hashes.source.clone(),
969 });
970 }
971 Ok(())
972 }
973 },
974 }
975 }
976
977 fn flush(&self) -> Result<(), FlushLockfileError> {
978 let content = serde_json::to_string_pretty(&self).map_err(|err| FlushLockfileError {
979 filepath: self.filepath.to_path_buf(),
980 source: io::Error::other(err),
981 })?;
982
983 fs::sync::write(&self.filepath, content).map_err(|err| FlushLockfileError {
984 filepath: self.filepath.to_path_buf(),
985 source: io::Error::other(err),
986 })
987 }
988}
989
990impl<P: LockfilePermissions> WorkspaceLockfile<P> {
991 pub(crate) fn rocks(
992 &self,
993 deps: &LocalPackageLockType,
994 ) -> &BTreeMap<LocalPackageId, LocalPackage> {
995 match deps {
996 LocalPackageLockType::Regular => self.dependencies.rocks(),
997 LocalPackageLockType::Test => self.test_dependencies.rocks(),
998 LocalPackageLockType::Build => self.build_dependencies.rocks(),
999 }
1000 }
1001
1002 pub(crate) fn get(
1003 &self,
1004 id: &LocalPackageId,
1005 deps: &LocalPackageLockType,
1006 ) -> Option<&LocalPackage> {
1007 match deps {
1008 LocalPackageLockType::Regular => self.dependencies.get(id),
1009 LocalPackageLockType::Test => self.test_dependencies.get(id),
1010 LocalPackageLockType::Build => self.build_dependencies.get(id),
1011 }
1012 }
1013
1014 pub(crate) fn is_entrypoint(
1015 &self,
1016 package: &LocalPackageId,
1017 deps: &LocalPackageLockType,
1018 ) -> bool {
1019 match deps {
1020 LocalPackageLockType::Regular => self.dependencies.is_entrypoint(package),
1021 LocalPackageLockType::Test => self.test_dependencies.is_entrypoint(package),
1022 LocalPackageLockType::Build => self.build_dependencies.is_entrypoint(package),
1023 }
1024 }
1025
1026 pub(crate) fn package_sync_spec(
1027 &self,
1028 packages: &[LuaDependencySpec],
1029 deps: &LocalPackageLockType,
1030 strategy: &SyncStrategy<'_>,
1031 ) -> PackageSyncSpec {
1032 match deps {
1033 LocalPackageLockType::Regular => {
1034 self.dependencies.package_sync_spec(packages, strategy)
1035 }
1036 LocalPackageLockType::Test => {
1037 self.test_dependencies.package_sync_spec(packages, strategy)
1038 }
1039 LocalPackageLockType::Build => self
1040 .build_dependencies
1041 .package_sync_spec(packages, strategy),
1042 }
1043 }
1044
1045 pub(crate) fn local_pkg_lock(&self, deps: &LocalPackageLockType) -> &LocalPackageLock {
1046 match deps {
1047 LocalPackageLockType::Regular => &self.dependencies,
1048 LocalPackageLockType::Test => &self.test_dependencies,
1049 LocalPackageLockType::Build => &self.build_dependencies,
1050 }
1051 }
1052
1053 fn flush(&self) -> io::Result<()> {
1054 let content = serde_json::to_string_pretty(&self)?;
1055
1056 fs::sync::write(&self.filepath, content).map_err(io::Error::other)?;
1057
1058 Ok(())
1059 }
1060}
1061
1062impl Lockfile<ReadOnly> {
1063 #[tracing::instrument(level = "trace")]
1065 pub(crate) fn new(
1066 filepath: PathBuf,
1067 rock_layout: RockLayoutConfig,
1068 ) -> Result<Lockfile<ReadOnly>, LockfileError> {
1069 match File::options().create_new(true).write(true).open(&filepath) {
1071 Ok(mut file) => {
1072 let empty_lockfile: Lockfile<ReadOnly> = Lockfile {
1073 filepath: filepath.clone(),
1074 _marker: PhantomData,
1075 version: LOCKFILE_VERSION_STR.into(),
1076 lock: LocalPackageLock::default(),
1077 entrypoint_layout: rock_layout.clone(),
1078 };
1079 let json_str =
1080 serde_json::to_string(&empty_lockfile).map_err(LockfileError::WriteJson)?;
1081 write!(file, "{json_str}").map_err(|source| fs::FsError::Write {
1082 path: filepath.to_path_buf(),
1083 source,
1084 })?;
1085 }
1086 Err(err) if err.kind() == ErrorKind::AlreadyExists => {}
1087 Err(source) => {
1088 return Err(LockfileError::Fs(fs::FsError::FileOpen {
1089 path: filepath.to_path_buf(),
1090 source,
1091 }))
1092 }
1093 }
1094
1095 Self::load(filepath, Some(&rock_layout))
1096 }
1097
1098 #[tracing::instrument(level = "trace")]
1101 pub fn load(
1102 filepath: PathBuf,
1103 expected_rock_layout: Option<&RockLayoutConfig>,
1104 ) -> Result<Lockfile<ReadOnly>, LockfileError> {
1105 let content = fs::sync::read_to_string(&filepath)?;
1106 let mut lockfile: Lockfile<ReadOnly> =
1107 serde_json::from_str(&content).map_err(LockfileError::ParseJson)?;
1108 lockfile.filepath = filepath;
1109 if let Some(expected_rock_layout) = expected_rock_layout {
1110 if &lockfile.entrypoint_layout != expected_rock_layout {
1111 return Err(LockfileError::MismatchedRockLayout);
1112 }
1113 }
1114 Ok(lockfile)
1115 }
1116
1117 pub(crate) fn into_temporary(self) -> Lockfile<ReadWrite> {
1119 Lockfile::<ReadWrite> {
1120 _marker: PhantomData,
1121 filepath: self.filepath,
1122 version: self.version,
1123 lock: self.lock,
1124 entrypoint_layout: self.entrypoint_layout,
1125 }
1126 }
1127
1128 pub fn write_guard(self) -> LockfileGuard {
1131 LockfileGuard(self.into_temporary())
1132 }
1133
1134 #[tracing::instrument(level = "trace", skip_all)]
1137 pub fn map_then_flush<T, F, E>(self, cb: F) -> Result<T, FlushLockfileError>
1138 where
1139 F: FnOnce(&mut Lockfile<ReadWrite>) -> Result<T, E>,
1140 E: Error,
1141 E: From<io::Error>,
1142 E: Into<Box<dyn Error + Send + Sync>>,
1143 {
1144 let mut writeable_lockfile = self.into_temporary();
1145
1146 let result = cb(&mut writeable_lockfile).map_err(|err| FlushLockfileError {
1147 filepath: writeable_lockfile.filepath.to_path_buf(),
1148 source: io::Error::other(err),
1149 })?;
1150
1151 writeable_lockfile.flush()?;
1152
1153 Ok(result)
1154 }
1155
1156 }
1174
1175impl WorkspaceLockfile<ReadOnly> {
1176 #[tracing::instrument(level = "trace")]
1178 pub fn new(filepath: PathBuf) -> Result<WorkspaceLockfile<ReadOnly>, LockfileError> {
1179 match File::options().create_new(true).write(true).open(&filepath) {
1181 Ok(mut file) => {
1182 let empty_lockfile: WorkspaceLockfile<ReadOnly> = WorkspaceLockfile {
1183 filepath: filepath.clone(),
1184 _marker: PhantomData,
1185 version: LOCKFILE_VERSION_STR.into(),
1186 dependencies: LocalPackageLock::default(),
1187 test_dependencies: LocalPackageLock::default(),
1188 build_dependencies: LocalPackageLock::default(),
1189 };
1190 let json_str =
1191 serde_json::to_string(&empty_lockfile).map_err(LockfileError::WriteJson)?;
1192 write!(file, "{json_str}").map_err(|source| fs::FsError::Write {
1193 path: filepath.to_path_buf(),
1194 source,
1195 })?;
1196 }
1197 Err(err) if err.kind() == ErrorKind::AlreadyExists => {}
1198 Err(source) => {
1199 return Err(LockfileError::Fs(fs::FsError::FileOpen {
1200 path: filepath.to_path_buf(),
1201 source,
1202 }))
1203 }
1204 }
1205
1206 Self::load(filepath)
1207 }
1208
1209 #[tracing::instrument(level = "trace")]
1211 pub fn load(filepath: PathBuf) -> Result<WorkspaceLockfile<ReadOnly>, LockfileError> {
1212 let content = fs::sync::read_to_string(&filepath)?;
1213 let mut lockfile: WorkspaceLockfile<ReadOnly> =
1214 serde_json::from_str(&content).map_err(LockfileError::ParseJson)?;
1215
1216 lockfile.filepath = filepath;
1217
1218 Ok(lockfile)
1219 }
1220
1221 fn into_temporary(self) -> WorkspaceLockfile<ReadWrite> {
1223 WorkspaceLockfile::<ReadWrite> {
1224 _marker: PhantomData,
1225 filepath: self.filepath,
1226 version: self.version,
1227 dependencies: self.dependencies,
1228 test_dependencies: self.test_dependencies,
1229 build_dependencies: self.build_dependencies,
1230 }
1231 }
1232
1233 pub fn write_guard(self) -> ProjectLockfileGuard {
1236 ProjectLockfileGuard(self.into_temporary())
1237 }
1238}
1239
1240impl Lockfile<ReadWrite> {
1241 pub(crate) fn add_entrypoint(&mut self, rock: &LocalPackage) {
1242 self.add(rock);
1243 let id = rock.id().clone();
1244 if !self.lock.entrypoints.contains(&id) {
1245 self.lock.entrypoints.push(id)
1246 }
1247 }
1248
1249 pub(crate) fn remove_entrypoint(&mut self, rock: &LocalPackage) {
1250 if let Some(index) = self
1251 .lock
1252 .entrypoints
1253 .iter()
1254 .position(|pkg_id| *pkg_id == rock.id())
1255 {
1256 self.lock.entrypoints.remove(index);
1257 }
1258 }
1259
1260 fn add(&mut self, rock: &LocalPackage) {
1261 self.lock
1264 .rocks
1265 .entry(rock.id())
1266 .or_insert_with(|| rock.clone());
1267 }
1268
1269 pub(crate) fn add_dependency(&mut self, target: &LocalPackage, dependency: &LocalPackage) {
1271 self.lock
1272 .rocks
1273 .entry(target.id())
1274 .and_modify(|rock| {
1275 let id = dependency.id();
1276 if !rock.spec.dependencies.contains(&id) {
1277 rock.spec.dependencies.push(id);
1278 }
1279 })
1280 .or_insert_with(|| {
1281 let id = dependency.id();
1282 let mut target = target.clone();
1283 if !target.spec.dependencies.contains(&id) {
1284 target.spec.dependencies.push(id);
1285 }
1286 target
1287 });
1288 self.add(dependency);
1289 }
1290
1291 pub(crate) fn remove(&mut self, target: &LocalPackage) {
1292 self.lock.remove(target)
1293 }
1294
1295 pub(crate) fn remove_by_id(&mut self, target: &LocalPackageId) {
1296 self.lock.remove_by_id(target)
1297 }
1298
1299 pub(crate) fn sync(&mut self, lock: &LocalPackageLock) {
1300 self.lock = lock.clone();
1301 }
1302
1303 }
1305
1306impl WorkspaceLockfile<ReadWrite> {
1307 pub(crate) fn remove(&mut self, target: &LocalPackage, deps: &LocalPackageLockType) {
1308 match deps {
1309 LocalPackageLockType::Regular => self.dependencies.remove(target),
1310 LocalPackageLockType::Test => self.test_dependencies.remove(target),
1311 LocalPackageLockType::Build => self.build_dependencies.remove(target),
1312 }
1313 }
1314
1315 pub(crate) fn sync(&mut self, lock: &LocalPackageLock, deps: &LocalPackageLockType) {
1316 match deps {
1317 LocalPackageLockType::Regular => {
1318 self.dependencies = lock.clone();
1319 }
1320 LocalPackageLockType::Test => {
1321 self.test_dependencies = lock.clone();
1322 }
1323 LocalPackageLockType::Build => {
1324 self.build_dependencies = lock.clone();
1325 }
1326 }
1327 }
1328}
1329
1330pub struct LockfileGuard(Lockfile<ReadWrite>);
1332
1333pub struct ProjectLockfileGuard(WorkspaceLockfile<ReadWrite>);
1334
1335impl Serialize for LockfileGuard {
1336 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1337 where
1338 S: serde::Serializer,
1339 {
1340 self.0.serialize(serializer)
1341 }
1342}
1343
1344impl Serialize for ProjectLockfileGuard {
1345 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1346 where
1347 S: serde::Serializer,
1348 {
1349 self.0.serialize(serializer)
1350 }
1351}
1352
1353impl<'de> Deserialize<'de> for LockfileGuard {
1354 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1355 where
1356 D: serde::Deserializer<'de>,
1357 {
1358 Ok(LockfileGuard(Lockfile::<ReadWrite>::deserialize(
1359 deserializer,
1360 )?))
1361 }
1362}
1363
1364impl<'de> Deserialize<'de> for ProjectLockfileGuard {
1365 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1366 where
1367 D: serde::Deserializer<'de>,
1368 {
1369 Ok(ProjectLockfileGuard(
1370 WorkspaceLockfile::<ReadWrite>::deserialize(deserializer)?,
1371 ))
1372 }
1373}
1374
1375impl Deref for LockfileGuard {
1376 type Target = Lockfile<ReadWrite>;
1377
1378 fn deref(&self) -> &Self::Target {
1379 &self.0
1380 }
1381}
1382
1383impl Deref for ProjectLockfileGuard {
1384 type Target = WorkspaceLockfile<ReadWrite>;
1385
1386 fn deref(&self) -> &Self::Target {
1387 &self.0
1388 }
1389}
1390
1391impl DerefMut for LockfileGuard {
1392 fn deref_mut(&mut self) -> &mut Self::Target {
1393 &mut self.0
1394 }
1395}
1396
1397impl DerefMut for ProjectLockfileGuard {
1398 fn deref_mut(&mut self) -> &mut Self::Target {
1399 &mut self.0
1400 }
1401}
1402
1403impl Drop for LockfileGuard {
1404 fn drop(&mut self) {
1405 let _ = self.flush();
1406 }
1407}
1408
1409impl Drop for ProjectLockfileGuard {
1410 fn drop(&mut self) {
1411 let _ = self.flush();
1412 }
1413}
1414
1415fn serialize_sorted_package_ids<S>(
1416 package_ids: &[LocalPackageId],
1417 serializer: S,
1418) -> Result<S::Ok, S::Error>
1419where
1420 S: Serializer,
1421{
1422 package_ids
1423 .iter()
1424 .sorted()
1425 .collect_vec()
1426 .serialize(serializer)
1427}
1428
1429fn integrity_err_not_found(package: &LocalPackage) -> LockfileIntegrityError {
1430 LockfileIntegrityError::PackageNotFound(
1431 package.name().clone(),
1432 package.version().clone(),
1433 package.spec.pinned,
1434 package
1435 .spec
1436 .constraint
1437 .clone()
1438 .unwrap_or("UNCONSTRAINED".into()),
1439 )
1440}
1441
1442fn deserialize_url<'de, D>(deserializer: D) -> Result<Url, D::Error>
1443where
1444 D: serde::Deserializer<'de>,
1445{
1446 let s = String::deserialize(deserializer)?;
1447 Url::parse(&s).map_err(serde::de::Error::custom)
1448}
1449
1450fn serialize_url<S>(url: &Url, serializer: S) -> Result<S::Ok, S::Error>
1451where
1452 S: Serializer,
1453{
1454 url.as_str().serialize(serializer)
1455}
1456
1457#[cfg(test)]
1458mod tests {
1459 use super::*;
1460 use std::{fs::remove_file, path::PathBuf};
1461
1462 use assert_fs::fixture::PathCopy;
1463 use insta::{assert_json_snapshot, sorted_redaction};
1464
1465 use crate::{config::ConfigBuilder, lua_version::LuaVersion, package::PackageSpec};
1466
1467 #[test]
1468 fn parse_lockfile() {
1469 let temp = assert_fs::TempDir::new().unwrap();
1470 temp.copy_from(
1471 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree"),
1472 &["**"],
1473 )
1474 .unwrap();
1475
1476 let config = ConfigBuilder::new()
1477 .unwrap()
1478 .user_tree(Some(temp.to_path_buf()))
1479 .build()
1480 .unwrap();
1481 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
1482 let lockfile = tree.lockfile().unwrap();
1483
1484 assert_json_snapshot!(lockfile, { ".**" => sorted_redaction() });
1485 }
1486
1487 #[test]
1488 fn add_rocks() {
1489 let temp = assert_fs::TempDir::new().unwrap();
1490 temp.copy_from(
1491 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree"),
1492 &["**"],
1493 )
1494 .unwrap();
1495
1496 let mock_hashes = LocalPackageHashes {
1497 rockspec: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
1498 .parse()
1499 .unwrap(),
1500 source: "sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="
1501 .parse()
1502 .unwrap(),
1503 };
1504
1505 let config = ConfigBuilder::new()
1506 .unwrap()
1507 .user_tree(Some(temp.to_path_buf()))
1508 .build()
1509 .unwrap();
1510 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
1511 let mut lockfile = tree.lockfile().unwrap().write_guard();
1512
1513 let test_package = PackageSpec::parse("test1".to_string(), "0.1.0".to_string()).unwrap();
1514 let test_local_package = LocalPackage::from(
1515 &test_package,
1516 crate::lockfile::LockConstraint::Unconstrained,
1517 RockBinaries::default(),
1518 RemotePackageSource::Test,
1519 None,
1520 mock_hashes.clone(),
1521 );
1522 lockfile.add_entrypoint(&test_local_package);
1523
1524 let test_dep_package =
1525 PackageSpec::parse("test2".to_string(), "0.1.0".to_string()).unwrap();
1526 let mut test_local_dep_package = LocalPackage::from(
1527 &test_dep_package,
1528 crate::lockfile::LockConstraint::Constrained(">= 1.0.0".parse().unwrap()),
1529 RockBinaries::default(),
1530 RemotePackageSource::Test,
1531 None,
1532 mock_hashes.clone(),
1533 );
1534 test_local_dep_package.spec.pinned = PinnedState::Pinned;
1535 lockfile.add_dependency(&test_local_package, &test_local_dep_package);
1536
1537 assert_json_snapshot!(lockfile, { ".**" => sorted_redaction() });
1538 }
1539
1540 #[test]
1541 fn parse_nonexistent_lockfile() {
1542 let tree_path =
1543 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree");
1544
1545 let temp = assert_fs::TempDir::new().unwrap();
1546 temp.copy_from(&tree_path, &["**"]).unwrap();
1547
1548 remove_file(temp.join("5.1/lux.lock")).unwrap();
1549
1550 let config = ConfigBuilder::new()
1551 .unwrap()
1552 .user_tree(Some(temp.to_path_buf()))
1553 .build()
1554 .unwrap();
1555 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
1556
1557 let _ = tree.lockfile().unwrap().write_guard(); }
1559
1560 fn get_test_lockfile() -> Lockfile<ReadOnly> {
1561 let sample_tree = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1562 .join("resources/test/sample-tree/5.1/lux.lock");
1563 Lockfile::new(sample_tree, RockLayoutConfig::default()).unwrap()
1564 }
1565
1566 #[test]
1567 fn test_sync_spec() {
1568 let lockfile = get_test_lockfile();
1569 let packages = vec![
1570 PackageReq::parse("neorg@8.8.1-1").unwrap().into(),
1571 PackageReq::parse("lua-cjson@2.1.0").unwrap().into(),
1572 PackageReq::parse("nonexistent").unwrap().into(),
1573 ];
1574
1575 let sync_spec = lockfile
1576 .lock
1577 .package_sync_spec(&packages, &SyncStrategy::LockfileOnly);
1578
1579 assert_eq!(sync_spec.to_add.len(), 1);
1580
1581 assert!(!sync_spec
1583 .to_remove
1584 .iter()
1585 .any(|pkg| pkg.name().to_string() == "nvim-nio"
1586 && pkg.constraint()
1587 == LockConstraint::Constrained(">=1.7.0, <1.8.0".parse().unwrap())));
1588 assert!(!sync_spec
1589 .to_remove
1590 .iter()
1591 .any(|pkg| pkg.name().to_string() == "lua-utils.nvim"
1592 && pkg.constraint() == LockConstraint::Constrained("=1.0.2".parse().unwrap())));
1593 assert!(!sync_spec
1594 .to_remove
1595 .iter()
1596 .any(|pkg| pkg.name().to_string() == "plenary.nvim"
1597 && pkg.constraint() == LockConstraint::Constrained("=0.1.4".parse().unwrap())));
1598 assert!(!sync_spec
1599 .to_remove
1600 .iter()
1601 .any(|pkg| pkg.name().to_string() == "nui.nvim"
1602 && pkg.constraint() == LockConstraint::Constrained("=0.3.0".parse().unwrap())));
1603 assert!(!sync_spec
1604 .to_remove
1605 .iter()
1606 .any(|pkg| pkg.name().to_string() == "pathlib.nvim"
1607 && pkg.constraint()
1608 == LockConstraint::Constrained(">=2.2.0, <2.3.0".parse().unwrap())));
1609 }
1610
1611 #[test]
1612 fn test_sync_spec_remove() {
1613 let lockfile = get_test_lockfile();
1614 let packages = vec![
1615 PackageReq::parse("lua-cjson@2.1.0").unwrap().into(),
1616 PackageReq::parse("nonexistent").unwrap().into(),
1617 ];
1618
1619 let sync_spec = lockfile
1620 .lock
1621 .package_sync_spec(&packages, &SyncStrategy::LockfileOnly);
1622
1623 assert_eq!(sync_spec.to_add.len(), 1);
1624
1625 assert!(sync_spec
1629 .to_remove
1630 .iter()
1631 .any(|pkg| pkg.name().to_string() == "neorg"
1632 && pkg.version() == &"8.8.1-1".parse().unwrap()));
1633 assert!(sync_spec
1634 .to_remove
1635 .iter()
1636 .any(|pkg| pkg.name().to_string() == "nvim-nio"
1637 && pkg.constraint()
1638 == LockConstraint::Constrained(">=1.7.0, <1.8.0".parse().unwrap())));
1639 assert!(sync_spec
1640 .to_remove
1641 .iter()
1642 .any(|pkg| pkg.name().to_string() == "lua-utils.nvim"
1643 && pkg.constraint() == LockConstraint::Constrained("=1.0.2".parse().unwrap())));
1644 assert!(sync_spec
1645 .to_remove
1646 .iter()
1647 .any(|pkg| pkg.name().to_string() == "plenary.nvim"
1648 && pkg.constraint() == LockConstraint::Constrained("=0.1.4".parse().unwrap())));
1649 assert!(sync_spec
1650 .to_remove
1651 .iter()
1652 .any(|pkg| pkg.name().to_string() == "nui.nvim"
1653 && pkg.constraint() == LockConstraint::Constrained("=0.3.0".parse().unwrap())));
1654 assert!(sync_spec
1655 .to_remove
1656 .iter()
1657 .any(|pkg| pkg.name().to_string() == "pathlib.nvim"
1658 && pkg.constraint()
1659 == LockConstraint::Constrained(">=2.2.0, <2.3.0".parse().unwrap())));
1660 }
1661
1662 #[test]
1663 fn test_sync_spec_empty() {
1664 let lockfile = get_test_lockfile();
1665 let packages = vec![];
1666 let sync_spec = lockfile
1667 .lock
1668 .package_sync_spec(&packages, &SyncStrategy::LockfileOnly);
1669
1670 assert!(sync_spec.to_add.is_empty());
1672 assert_eq!(sync_spec.to_remove.len(), lockfile.rocks().len());
1673 }
1674
1675 #[test]
1676 fn test_sync_spec_different_constraints() {
1677 let lockfile = get_test_lockfile();
1678 let packages = vec![PackageReq::parse("nvim-nio>=2.0.0").unwrap().into()];
1679 let sync_spec = lockfile
1680 .lock
1681 .package_sync_spec(&packages, &SyncStrategy::LockfileOnly);
1682
1683 let expected: PackageVersionReq = ">=2.0.0".parse().unwrap();
1684 assert!(sync_spec
1685 .to_add
1686 .iter()
1687 .any(|req| req.name().to_string() == "nvim-nio" && req.version_req() == &expected));
1688
1689 assert!(sync_spec
1690 .to_remove
1691 .iter()
1692 .any(|pkg| pkg.name().to_string() == "nvim-nio"));
1693 }
1694
1695 #[test]
1696 fn test_sync_spec_ensure_installed() {
1697 let temp = assert_fs::TempDir::new().unwrap();
1698 temp.copy_from(
1699 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-tree"),
1700 &["**"],
1701 )
1702 .unwrap();
1703
1704 let config = ConfigBuilder::new()
1705 .unwrap()
1706 .user_tree(Some(temp.to_path_buf()))
1707 .build()
1708 .unwrap();
1709 let tree = config.user_tree(LuaVersion::Lua51).unwrap();
1710 let lockfile = tree.lockfile().unwrap();
1711
1712 let packages: Vec<LuaDependencySpec> = vec![
1713 PackageReq::parse("neorg@8.8.1-1").unwrap().into(),
1714 PackageReq::parse("lua-cjson@2.1.0").unwrap().into(),
1716 PackageReq::parse("nonexistent").unwrap().into(),
1718 ];
1719
1720 let sync_spec = lockfile
1722 .lock
1723 .package_sync_spec(&packages, &SyncStrategy::EnsureInstalled(&tree));
1724
1725 assert!(!sync_spec
1726 .to_add
1727 .iter()
1728 .any(|req| req.name().to_string() == "neorg"));
1729
1730 assert!(sync_spec
1731 .to_add
1732 .iter()
1733 .any(|req| req.name().to_string() == "lua-cjson"));
1734
1735 assert!(sync_spec
1736 .to_add
1737 .iter()
1738 .any(|req| req.name().to_string() == "nonexistent"));
1739 }
1740}