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