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