Skip to main content

veryl_metadata/
lockfile.rs

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    // TODO
68    // Registory
69}
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/// The `[[components]]` provided by a resolved dependency.
170#[derive(Clone, Debug)]
171pub struct DependencyComponents {
172    /// Project name as seen from the root project (`lock.name`, so a
173    /// dependency alias is reflected).
174    pub project: String,
175    /// Root directory of the dependency project (checkout cache or local path).
176    pub root: PathBuf,
177    /// Cargo target directory for building the components. Repository
178    /// dependencies use a revision-keyed shared cache so identical revisions
179    /// are built once across projects.
180    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                // Analyzed only for the build root (`Metadata::paths`).
317                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    /// Collects `[[components]]` declared by direct (visible) dependencies.
346    /// Transitive dependencies are skipped, matching the visibility of
347    /// dependency symbols.
348    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                    // A path dependency is a local project; share its own
366                    // component target directory.
367                    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        // breadth first search because root has top priority of name
458        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            // avoid name conflict by adding suffix
465            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                // project local name is not required to check name_table
510                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                    // Path override is disabled if it is not root
581                    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        // try to load from local path
747        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                    // Acquire the lock before checking path existence to prevent
790                    // race conditions where gix::prepare_clone creates an
791                    // incomplete directory that other threads may observe.
792                    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 the existing path is not git repository, cleanup and re-try
802                        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}