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