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