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