1use crate::git::Git;
2use crate::metadata::{Dependency, Metadata, UrlPath};
3use crate::metadata_error::MetadataError;
4use crate::pubfile::{Pubfile, Release};
5use crate::{ProjectProperty, lockfile_compat};
6use log::info;
7use pathdiff::diff_paths;
8use semver::{Version, VersionReq};
9use serde::{Deserialize, Serialize};
10use std::collections::{BTreeMap, HashMap, HashSet};
11use std::fmt;
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::str::FromStr;
15use url::Url;
16use uuid::Uuid;
17use veryl_path::{PathSet, ignore_already_exists};
18use walkdir::WalkDir;
19
20const LOCKFILE_VERSION: usize = 1;
21
22#[derive(Clone, Debug, Default, Serialize, Deserialize)]
23#[serde(deny_unknown_fields)]
24pub struct Lockfile {
25 version: usize,
26 projects: Vec<Lock>,
27 #[serde(skip)]
28 pub lock_table: HashMap<UrlPath, Vec<Lock>>,
29 #[serde(skip)]
30 force_update: bool,
31 #[serde(skip)]
32 pub metadata_path: PathBuf,
33}
34
35#[derive(Clone, Debug, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct Lock {
38 pub name: String,
39 pub source: LockSource,
40 #[serde(default)]
41 pub properties: BTreeMap<String, ProjectProperty>,
42 pub dependencies: Vec<LockDependency>,
43 #[serde(skip)]
44 pub visible: bool,
45}
46
47impl Lock {
48 pub fn uuid(&self) -> Uuid {
49 match &self.source {
50 LockSource::Path(path) => {
51 let url = self.source.to_url();
52 Lockfile::gen_uuid(&url, path, "", &self.properties)
53 }
54 LockSource::Repository(repo) => {
55 Lockfile::gen_uuid(&repo.url, &repo.path, &repo.revision, &self.properties)
56 }
57 }
58 }
59}
60
61#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
62#[serde(deny_unknown_fields)]
63#[serde(untagged)]
64pub enum LockSource {
65 Repository(Box<LockSourceRepository>),
66 Path(PathBuf),
67 }
70
71impl LockSource {
72 pub fn to_url(&self) -> UrlPath {
73 match self {
74 LockSource::Repository(x) => x.url.clone(),
75 LockSource::Path(x) => UrlPath::Path(x.clone()),
76 }
77 }
78
79 pub fn get_version(&self) -> Option<&Version> {
80 match self {
81 LockSource::Repository(x) => Some(&x.version),
82 LockSource::Path(_) => None,
83 }
84 }
85
86 pub fn get_revision(&self) -> Option<&str> {
87 match self {
88 LockSource::Repository(x) => Some(&x.revision),
89 LockSource::Path(_) => None,
90 }
91 }
92
93 pub fn project(&self) -> Option<&str> {
94 match self {
95 LockSource::Repository(x) => Some(&x.project),
96 LockSource::Path(_) => None,
97 }
98 }
99
100 pub fn local_path(&self, root_path: &Path) -> Result<PathBuf, MetadataError> {
101 match self {
102 LockSource::Repository(x) => x.local_path(),
103 LockSource::Path(path) => Ok(root_path.join(path)),
104 }
105 }
106}
107
108#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
109#[serde(deny_unknown_fields)]
110pub struct LockSourceRepository {
111 uuid: Uuid,
112 pub url: UrlPath,
113 pub path: PathBuf,
114 pub project: String,
115 pub version: Version,
116 pub revision: String,
117 r#override: Option<PathBuf>,
118}
119
120impl LockSourceRepository {
121 pub fn local_path(&self) -> Result<PathBuf, MetadataError> {
122 Ok(Lockfile::dependency_path(&self.url, &self.path, &self.revision)?.join(&self.path))
123 }
124}
125
126impl PartialOrd for LockSource {
127 fn partial_cmp(&self, other: &LockSource) -> Option<std::cmp::Ordering> {
128 Some(self.cmp(other))
129 }
130}
131
132impl Ord for LockSource {
133 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
134 match (self, other) {
135 (LockSource::Repository(x), LockSource::Repository(y)) => x
136 .url
137 .cmp(&y.url)
138 .then(x.project.cmp(&y.project))
139 .then(x.version.cmp(&y.version)),
140 (LockSource::Path(x), LockSource::Path(y)) => x.cmp(y),
141 (LockSource::Repository(_), LockSource::Path(_)) => std::cmp::Ordering::Less,
142 (LockSource::Path(_), LockSource::Repository(_)) => std::cmp::Ordering::Greater,
143 }
144 }
145}
146
147impl fmt::Display for LockSource {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 let mut ret = String::new();
150 match self {
151 LockSource::Repository(x) => {
152 ret.push_str(&format!("{} : {} @ {}", x.project, x.url, x.version));
153 }
154 LockSource::Path(x) => {
155 ret.push_str(&format!("{}", x.to_string_lossy()));
156 }
157 }
158 ret.fmt(f)
159 }
160}
161
162#[derive(Clone, Debug, Serialize, Deserialize)]
163#[serde(deny_unknown_fields)]
164pub struct LockDependency {
165 pub name: String,
166 pub source: LockSource,
167}
168
169#[derive(Clone, Debug)]
171pub struct DependencyComponents {
172 pub project: String,
175 pub root: PathBuf,
177 pub target_dir: PathBuf,
181 pub components: Vec<crate::Component>,
182}
183
184impl Lockfile {
185 pub fn projects(&self) -> Vec<&Lock> {
186 let mut ret = self
187 .lock_table
188 .values()
189 .flat_map(|locks| locks.iter())
190 .collect::<Vec<_>>();
191 ret.sort_by(|x, y| x.name.cmp(&y.name).then(x.source.cmp(&y.source)));
192 ret
193 }
194
195 pub fn load(metadata: &Metadata) -> Result<Self, MetadataError> {
196 let path = metadata
197 .lockfile_path
198 .canonicalize()
199 .map_err(|x| MetadataError::file_io(x, &metadata.lockfile_path))?;
200 let text = fs::read_to_string(&path).map_err(|x| MetadataError::file_io(x, &path))?;
201 let mut ret = LockfileCompat::load(&text, &path, metadata)?;
202 ret.metadata_path = metadata.metadata_path.clone();
203
204 let mut locks = Vec::new();
205 locks.append(&mut ret.projects);
206
207 ret.lock_table.clear();
208 for lock in locks {
209 ret.lock_table
210 .entry(lock.source.to_url())
211 .and_modify(|x| x.push(lock.clone()))
212 .or_insert(vec![lock]);
213 }
214 ret.sort_table();
215
216 Ok(ret)
217 }
218
219 pub fn save<T: AsRef<Path>>(&mut self, path: T) -> Result<(), MetadataError> {
220 self.projects.clear();
221 for locks in self.lock_table.values() {
222 for lock in locks {
223 self.projects.push(lock.clone());
224 }
225 }
226 self.projects.sort_by(|x, y| x.source.cmp(&y.source));
227
228 let mut text = String::new();
229 text.push_str("# This file is automatically @generated by Veryl.\n");
230 text.push_str("# It is not intended for manual editing.\n");
231 text.push_str(&toml::to_string_pretty(&self)?);
232
233 let path = path.as_ref();
234 veryl_path::atomic_write(path, text.as_bytes())
235 .map_err(|x| MetadataError::file_io(x, path))?;
236 Ok(())
237 }
238
239 pub fn new(metadata: &Metadata) -> Result<Self, MetadataError> {
240 let mut ret = Lockfile {
241 version: LOCKFILE_VERSION,
242 metadata_path: metadata.metadata_path.clone(),
243 ..Default::default()
244 };
245
246 let mut name_table = HashSet::new();
247 let mut src_table = HashMap::new();
248 let locks = ret.gen_locks(metadata, &mut name_table, &mut src_table, true, metadata)?;
249
250 for lock in locks {
251 info!("Adding dependency ({})", lock.source);
252 ret.lock_table
253 .entry(lock.source.to_url())
254 .and_modify(|x| x.push(lock.clone()))
255 .or_insert(vec![lock]);
256 }
257 ret.sort_table();
258
259 Ok(ret)
260 }
261
262 pub fn update(
263 &mut self,
264 metadata: &Metadata,
265 force_update: bool,
266 ) -> Result<bool, MetadataError> {
267 self.force_update = force_update;
268
269 let mut name_table = HashSet::new();
270 let mut src_table = HashMap::new();
271 let locks = self.gen_locks(metadata, &mut name_table, &mut src_table, true, metadata)?;
272
273 let old_table = self.lock_table.clone();
274 self.lock_table.clear();
275
276 let mut modified = false;
277
278 for lock in &locks {
279 let add = if let Some(old_locks) = old_table.get(&lock.source.to_url()) {
280 !old_locks.iter().any(|x| x.uuid() == lock.uuid())
281 } else {
282 true
283 };
284
285 if add {
286 info!("Adding dependency ({})", lock.source);
287 modified = true;
288 }
289
290 self.lock_table
291 .entry(lock.source.to_url())
292 .and_modify(|x| x.push(lock.clone()))
293 .or_insert(vec![lock.clone()]);
294 }
295 self.sort_table();
296
297 for old_locks in old_table.values() {
298 for old_lock in old_locks {
299 if !locks.iter().any(|x| x.uuid() == old_lock.uuid()) {
300 info!("Removing dependency ({})", old_lock.source);
301 modified = true;
302 }
303 }
304 }
305
306 Ok(modified)
307 }
308
309 pub fn paths(&self, base_dst: &Path) -> Result<Vec<PathSet>, MetadataError> {
310 let mut ret = Vec::new();
311
312 for locks in self.lock_table.values() {
313 for lock in locks {
314 let metadata = self.get_metadata(&lock.source)?;
315 let path = metadata.project_path();
316 let examples = path.join("examples");
318
319 for src in &veryl_path::gather_files_with_extension(&path, "veryl", false)? {
320 if src.starts_with(&examples) {
321 continue;
322 }
323 let Ok(rel) = src.strip_prefix(&path) else {
324 return Err(MetadataError::InvalidSourceLocation(src.clone()));
325 };
326 let mut dst = base_dst.join(&lock.name);
327 dst.push(rel);
328 dst.set_extension("sv");
329 let mut map = dst.clone();
330 map.set_extension("sv.map");
331 ret.push(PathSet {
332 prj: lock.name.clone(),
333 src: src.to_path_buf(),
334 dst,
335 map,
336 example: false,
337 });
338 }
339 }
340 }
341
342 Ok(ret)
343 }
344
345 pub fn collect_components(&self) -> Result<Vec<DependencyComponents>, MetadataError> {
349 let mut ret = Vec::new();
350
351 for locks in self.lock_table.values() {
352 for lock in locks {
353 if !lock.visible {
354 continue;
355 }
356 let metadata = self.get_metadata(&lock.source)?;
357 if metadata.components.is_empty() {
358 continue;
359 }
360 let root = metadata.project_path();
361 let target_dir = match &lock.source {
362 LockSource::Repository(x) => veryl_path::cache_path()
363 .join("components")
364 .join(x.uuid.simple().to_string()),
365 LockSource::Path(_) => root.join("target/veryl-components"),
368 };
369 ret.push(DependencyComponents {
370 project: lock.name.clone(),
371 root,
372 target_dir,
373 components: metadata.components.clone(),
374 });
375 }
376 }
377
378 ret.sort_by(|a, b| a.project.cmp(&b.project));
379 Ok(ret)
380 }
381
382 pub fn clear_cache(&self) -> Result<(), MetadataError> {
383 let lock_resolve = veryl_path::lock_dir("resolve")?;
384 let lock_dependencies = veryl_path::lock_dir("dependencies")?;
385
386 for locks in self.lock_table.values() {
387 for lock in locks {
388 if let LockSource::Repository(x) = &lock.source {
389 let resolve_path = Self::resolve_path(&x.url)?;
390 let dependency_path = Self::dependency_path(&x.url, &x.path, &x.revision)?;
391 if resolve_path.exists() {
392 fs::remove_dir_all(&resolve_path)
393 .map_err(|x| MetadataError::file_io(x, &resolve_path))?;
394 }
395 if dependency_path.exists() {
396 fs::remove_dir_all(&dependency_path)
397 .map_err(|x| MetadataError::file_io(x, &dependency_path))?;
398 }
399 }
400 }
401 }
402
403 veryl_path::unlock_dir(lock_resolve)?;
404 veryl_path::unlock_dir(lock_dependencies)?;
405
406 Ok(())
407 }
408
409 fn git_clone(&self, url: &UrlPath, path: &Path) -> Result<Git, MetadataError> {
410 let url = match url {
411 UrlPath::Url(x) => UrlPath::Url(x.clone()),
412 UrlPath::Path(x) => {
413 if x.is_relative() {
414 let path = self.metadata_path.parent().unwrap().join(x);
415 UrlPath::Path(path)
416 } else {
417 UrlPath::Path(x.clone())
418 }
419 }
420 };
421
422 Git::clone(&url, path)
423 }
424
425 fn sort_table(&mut self) {
426 for locks in self.lock_table.values_mut() {
427 locks.sort_by(|a, b| b.source.cmp(&a.source));
428 }
429 }
430
431 fn gen_uuid(
432 url: &UrlPath,
433 path: &Path,
434 revision: &str,
435 properties: &BTreeMap<String, ProjectProperty>,
436 ) -> Uuid {
437 let mut url = url.to_string();
438 url.push_str(&path.to_string_lossy());
439 url.push_str(revision);
440 for (name, value) in properties {
441 url.push_str(name);
442 url.push_str(&value.value_string());
443 }
444 Uuid::new_v5(&Uuid::NAMESPACE_URL, url.as_bytes())
445 }
446
447 fn gen_locks(
448 &mut self,
449 metadata: &Metadata,
450 name_table: &mut HashSet<String>,
451 src_table: &mut HashMap<Uuid, String>,
452 root: bool,
453 root_metadata: &Metadata,
454 ) -> Result<Vec<Lock>, MetadataError> {
455 let mut ret = Vec::new();
456
457 let mut dependencies_metadata = Vec::new();
459 for (name, dep) in &metadata.dependencies {
460 let dependency = self.resolve_dependency(metadata, name, dep, root, root_metadata)?;
461 let metadata = self.get_metadata(&dependency.source)?;
462 let mut name = dependency.name.clone();
463
464 if name_table.contains(&name) {
466 if root {
467 return Err(MetadataError::NameConflict(name));
468 }
469 let mut suffix = 0;
470 loop {
471 let new_name = format!("{name}_{suffix}");
472 if !name_table.contains(&new_name) {
473 name = new_name;
474 break;
475 }
476 suffix += 1;
477 }
478 }
479 name_table.insert(name.clone());
480
481 let mut properties: BTreeMap<_, _> = metadata.properties.clone().into_iter().collect();
482 if let Dependency::Entry(dep_entry) = dep {
483 for (prop_name, prop_value) in &dep_entry.properties {
484 let Some(value) = properties.get_mut(prop_name) else {
485 let err = MetadataError::UnknownProperty {
486 property: prop_name.clone(),
487 project: dependency.name.clone(),
488 };
489 return Err(err);
490 };
491
492 if !value.is_compatible(prop_value) {
493 let err = MetadataError::MismatchType {
494 name: prop_name.clone(),
495 expected: value.type_name(),
496 actual: prop_value.type_name(),
497 };
498 return Err(err);
499 }
500
501 *value = prop_value.clone();
502 }
503 }
504
505 let mut dependencies = Vec::new();
506 for (name, dep) in &metadata.dependencies {
507 let dependency =
508 self.resolve_dependency(&metadata, name, dep, root, root_metadata)?;
509 dependencies.push(dependency);
511 }
512
513 let lock = Lock {
514 name: name.clone(),
515 source: dependency.source.clone(),
516 properties,
517 dependencies,
518 visible: root,
519 };
520 let uuid = lock.uuid();
521 if let Some(x) = src_table.get(&uuid) {
522 if root {
523 return Err(MetadataError::InvalidDependency {
524 name: dependency.name.clone(),
525 cause: format!("it conflicts with {x}"),
526 });
527 }
528 } else {
529 ret.push(lock);
530 src_table.insert(uuid, name.clone());
531 dependencies_metadata.push(metadata);
532 }
533 }
534
535 for metadata in dependencies_metadata {
536 let mut dependency_locks =
537 self.gen_locks(&metadata, name_table, src_table, false, root_metadata)?;
538 ret.append(&mut dependency_locks);
539 }
540
541 Ok(ret)
542 }
543
544 fn resolve_dependency(
545 &mut self,
546 metadata: &Metadata,
547 name: &str,
548 dep: &Dependency,
549 root: bool,
550 root_metadata: &Metadata,
551 ) -> Result<LockDependency, MetadataError> {
552 Ok(match dep {
553 Dependency::Version(_) => {
554 return Err(MetadataError::InvalidDependency {
555 name: name.to_string(),
556 cause: "version-only dependencies are not supported yet; specify `git`, `github`, or `path`".to_string(),
557 });
558 }
559 Dependency::Entry(x) => {
560 let url = if let Some(git) = &x.git {
561 Some(git.clone())
562 } else if let Some(github) = &x.github {
563 let url = format!("https://github.com/{github}");
564 let url = Url::parse(&url).unwrap();
565 Some(UrlPath::Url(url))
566 } else {
567 None
568 };
569 let project = x.project.clone().unwrap_or(name.to_string());
570 let source = if let Some(url) = &url {
571 let Some(version) = &x.version else {
572 return Err(MetadataError::InvalidDependency {
573 name: name.to_string(),
574 cause: "version is not specified".to_string(),
575 });
576 };
577 let (release, path) = self.resolve_version(url, &project, version)?;
578 let uuid = Self::gen_uuid(url, &path, &release.revision, &BTreeMap::new());
579
580 let r#override = if root { x.path.clone() } else { None };
582
583 LockSource::Repository(Box::new(LockSourceRepository {
584 uuid,
585 url: url.clone(),
586 path,
587 project,
588 version: release.version,
589 revision: release.revision,
590 r#override,
591 }))
592 } else if let Some(path) = &x.path {
593 let path = if path.is_absolute() {
594 path.clone()
595 } else {
596 let base = root_metadata.project_path();
597 let path = base.join(metadata.project_path()).join(path);
598 if !path.exists() {
599 let project = x.project.clone().unwrap_or(name.to_string());
600 return Err(MetadataError::ProjectNotFound {
601 url: UrlPath::Path(path),
602 project,
603 });
604 }
605 diff_paths(path.canonicalize().unwrap(), base).unwrap()
606 };
607
608 LockSource::Path(path)
609 } else {
610 return Err(MetadataError::InvalidDependency {
611 name: name.to_string(),
612 cause: "[git|github|path] are not specified".to_string(),
613 });
614 };
615 LockDependency {
616 name: name.to_string(),
617 source,
618 }
619 }
620 })
621 }
622
623 fn resolve_version(
624 &mut self,
625 url: &UrlPath,
626 project: &str,
627 version_req: &VersionReq,
628 ) -> Result<(Release, PathBuf), MetadataError> {
629 if let Some(release) = self.resolve_version_from_lockfile(url, project, version_req)? {
630 if self.force_update {
631 let latest = self.resolve_version_from_latest(url, project, version_req)?;
632 Ok(latest)
633 } else {
634 Ok(release)
635 }
636 } else {
637 let latest = self.resolve_version_from_latest(url, project, version_req)?;
638 Ok(latest)
639 }
640 }
641
642 fn resolve_version_from_lockfile(
643 &mut self,
644 url: &UrlPath,
645 project: &str,
646 version_req: &VersionReq,
647 ) -> Result<Option<(Release, PathBuf)>, MetadataError> {
648 if let Some(locks) = self.lock_table.get_mut(url) {
649 for lock in locks {
650 if let LockSource::Repository(x) = &lock.source
651 && x.project == project
652 && version_req.matches(&x.version)
653 {
654 let release = Release {
655 version: x.version.clone(),
656 revision: x.revision.clone(),
657 };
658 let path = x.path.clone();
659 return Ok(Some((release, path)));
660 }
661 }
662 }
663 Ok(None)
664 }
665
666 fn resolve_path(url: &UrlPath) -> Result<PathBuf, MetadataError> {
667 let resolve_dir = veryl_path::cache_path().join("resolve");
668 let uuid = Self::gen_uuid(url, &PathBuf::new(), "", &BTreeMap::new());
669 Ok(resolve_dir.join(uuid.simple().encode_lower(&mut Uuid::encode_buffer())))
670 }
671
672 fn search_project(path: &Path, project: &str) -> Option<PathBuf> {
673 for entry in WalkDir::new(path).into_iter().flatten() {
674 if entry.file_name() == "Veryl.toml"
675 && let Ok(metadata) = Metadata::load(entry.path())
676 && metadata.project.name == project
677 {
678 let ret = entry.path();
679 let ret = ret.parent().unwrap().strip_prefix(path).unwrap();
680 return Some(ret.to_path_buf());
681 }
682 }
683 None
684 }
685
686 fn resolve_version_from_latest(
687 &mut self,
688 url: &UrlPath,
689 project: &str,
690 version_req: &VersionReq,
691 ) -> Result<(Release, PathBuf), MetadataError> {
692 let resolve_dir = veryl_path::cache_path().join("resolve");
693
694 if !resolve_dir.exists() {
695 ignore_already_exists(fs::create_dir_all(&resolve_dir))
696 .map_err(|x| MetadataError::file_io(x, &resolve_dir))?;
697 }
698
699 let path = Self::resolve_path(url)?;
700 let lock = veryl_path::lock_dir("resolve")?;
701 let git = self.git_clone(url, &path)?;
702 git.fetch()?;
703 git.checkout(None)?;
704 veryl_path::unlock_dir(lock)?;
705
706 let Some(prj_path) = Self::search_project(&path, project) else {
707 return Err(MetadataError::ProjectNotFound {
708 url: url.clone(),
709 project: project.to_string(),
710 });
711 };
712
713 let toml = path.join(&prj_path).join("Veryl.pub");
714 if !toml.exists() {
715 return Err(MetadataError::UnpublishedDependency {
716 name: project.to_string(),
717 });
718 }
719 let mut pubfile = Pubfile::load(toml)?;
720
721 pubfile.releases.sort_by(|a, b| b.version.cmp(&a.version));
722
723 for release in &pubfile.releases {
724 if version_req.matches(&release.version) {
725 return Ok((release.clone(), prj_path));
726 }
727 }
728
729 Err(MetadataError::VersionNotFound {
730 url: url.clone(),
731 version: version_req.to_string(),
732 })
733 }
734
735 fn dependency_path(
736 url: &UrlPath,
737 path: &Path,
738 revision: &str,
739 ) -> Result<PathBuf, MetadataError> {
740 let dependencies_dir = veryl_path::cache_path().join("dependencies");
741 let uuid = Self::gen_uuid(url, path, revision, &BTreeMap::new());
742 Ok(dependencies_dir.join(uuid.simple().encode_lower(&mut Uuid::encode_buffer())))
743 }
744
745 pub(crate) fn get_metadata(&self, source: &LockSource) -> Result<Metadata, MetadataError> {
746 let path = match source {
748 LockSource::Path(path) => Some(path.clone()),
749 LockSource::Repository(x) => x.r#override.clone(),
750 };
751 let mut searched_path = None;
752 let path_metadata = if let Some(x) = path {
753 let path = self.metadata_path.parent().unwrap().join(x);
754 let path = path.join("Veryl.toml");
755 searched_path = Some(path.clone());
756 if path.exists() {
757 Some(Metadata::load(path)?)
758 } else {
759 None
760 }
761 } else {
762 None
763 };
764
765 match source {
766 LockSource::Path(_) => {
767 if let Some(x) = path_metadata {
768 Ok(x)
769 } else {
770 Err(MetadataError::FileNotFound(
771 searched_path.unwrap_or_default(),
772 ))
773 }
774 }
775 LockSource::Repository(x) => {
776 if let Some(x) = path_metadata {
777 Ok(x)
778 } else {
779 let dependencies_dir = veryl_path::cache_path().join("dependencies");
780
781 if !dependencies_dir.exists() {
782 ignore_already_exists(fs::create_dir_all(&dependencies_dir))
783 .map_err(|x| MetadataError::file_io(x, &dependencies_dir))?;
784 }
785
786 let path = Self::dependency_path(&x.url, &x.path, &x.revision)?;
787 let toml = path.join(&x.path).join("Veryl.toml");
788
789 let lock = veryl_path::lock_dir("dependencies")?;
793 if !path.exists() {
794 let git = self.git_clone(&x.url, &path)?;
795 git.fetch()?;
796 git.checkout(Some(&x.revision))?;
797 } else {
798 let git = Git::open(&path)?;
799 let ret = git.is_clean().is_ok_and(|x| x);
800
801 if !ret || !toml.exists() {
803 veryl_path::ignore_directory_not_empty(fs::remove_dir_all(&path))
804 .map_err(|x| MetadataError::file_io(x, &path))?;
805 let git = self.git_clone(&x.url, &path)?;
806 git.fetch()?;
807 git.checkout(Some(&x.revision))?;
808 }
809 }
810 veryl_path::unlock_dir(lock)?;
811
812 Metadata::load(toml)
813 }
814 }
815 }
816 }
817}
818
819impl FromStr for Lockfile {
820 type Err = MetadataError;
821
822 fn from_str(s: &str) -> Result<Self, Self::Err> {
823 let lockfile: Lockfile = toml::from_str(s)?;
824 Ok(lockfile)
825 }
826}
827
828#[derive(Clone, Debug, Default, Serialize, Deserialize)]
829pub struct LockfileCompat {
830 version: Option<usize>,
831}
832
833impl LockfileCompat {
834 pub fn load(
835 text: &str,
836 lockfile_path: &Path,
837 metadata: &Metadata,
838 ) -> Result<Lockfile, MetadataError> {
839 let compat: LockfileCompat = toml::from_str(text)?;
840 let version = compat.version.unwrap_or(0);
841 let mut lockfile = match version {
842 0 => {
843 info!(
844 "Migrating lockfile to v1 ({})",
845 lockfile_path.to_string_lossy()
846 );
847 let lockfile: lockfile_compat::v0::Lockfile = toml::from_str(text)?;
848 let mut lockfile = Lockfile::from_v0(lockfile, &metadata.metadata_path)?;
849 lockfile.save(lockfile_path)?;
850 lockfile
851 }
852 1 => toml::from_str(text)?,
853 _ => unreachable!(),
854 };
855
856 for lock in lockfile.projects.iter_mut() {
857 lock.visible = metadata.dependencies.contains_key(&lock.name);
858 }
859
860 Ok(lockfile)
861 }
862}
863
864impl Lockfile {
865 fn set_project(
866 mut source: LockSource,
867 metadata_path: &Path,
868 ) -> Result<LockSource, MetadataError> {
869 let lockfile = Lockfile {
870 metadata_path: metadata_path.to_path_buf(),
871 ..Default::default()
872 };
873 let metadata = lockfile.get_metadata(&source)?;
874 if let LockSource::Repository(x) = &mut source {
875 x.project = metadata.project.name;
876 }
877 Ok(source)
878 }
879
880 pub fn from_v0(
881 x: lockfile_compat::v0::Lockfile,
882 metadata_path: &Path,
883 ) -> Result<Self, MetadataError> {
884 let mut projects = Vec::new();
885 for lock in x.projects {
886 let mut dependencies = Vec::new();
887 for dep in lock.dependencies {
888 let uuid =
889 Lockfile::gen_uuid(&dep.url, &PathBuf::new(), &dep.revision, &BTreeMap::new());
890 let source = LockSource::Repository(Box::new(LockSourceRepository {
891 uuid,
892 url: dep.url,
893 path: PathBuf::new(),
894 project: String::new(),
895 version: dep.version,
896 revision: dep.revision,
897 r#override: None,
898 }));
899 let source = Self::set_project(source, metadata_path)?;
900 let new_dep = LockDependency {
901 name: dep.name,
902 source,
903 };
904 dependencies.push(new_dep);
905 }
906
907 let uuid =
908 Lockfile::gen_uuid(&lock.url, &PathBuf::new(), &lock.revision, &BTreeMap::new());
909 let source = LockSource::Repository(Box::new(LockSourceRepository {
910 uuid,
911 url: lock.url,
912 path: PathBuf::new(),
913 project: String::new(),
914 version: lock.version,
915 revision: lock.revision,
916 r#override: lock.path,
917 }));
918 let source = Self::set_project(source, metadata_path)?;
919
920 let new_lock = Lock {
921 name: lock.name,
922 source,
923 properties: BTreeMap::new(),
924 dependencies,
925 visible: false,
926 };
927
928 projects.push(new_lock);
929 }
930
931 Ok(Lockfile {
932 version: LOCKFILE_VERSION,
933 projects,
934 ..Default::default()
935 })
936 }
937}