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