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