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