1use crate::git::Git;
2use crate::lockfile_compat;
3use crate::metadata::{Dependency, Metadata, UrlPath};
4use crate::metadata_error::MetadataError;
5use crate::pubfile::{Pubfile, Release};
6use log::info;
7use pathdiff::diff_paths;
8use semver::{Version, VersionReq};
9use serde::{Deserialize, Serialize};
10use std::collections::{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 pub dependencies: Vec<LockDependency>,
41 #[serde(skip)]
42 pub visible: bool,
43}
44
45#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
46#[serde(deny_unknown_fields)]
47#[serde(untagged)]
48pub enum LockSource {
49 Repository(Box<LockSourceRepository>),
50 Path(PathBuf),
51 }
54
55impl LockSource {
56 pub fn to_url(&self) -> UrlPath {
57 match self {
58 LockSource::Repository(x) => x.url.clone(),
59 LockSource::Path(x) => UrlPath::Path(x.clone()),
60 }
61 }
62
63 pub fn get_version(&self) -> Option<&Version> {
64 match self {
65 LockSource::Repository(x) => Some(&x.version),
66 LockSource::Path(_) => None,
67 }
68 }
69
70 pub fn get_revision(&self) -> Option<&str> {
71 match self {
72 LockSource::Repository(x) => Some(&x.revision),
73 LockSource::Path(_) => None,
74 }
75 }
76}
77
78#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
79#[serde(deny_unknown_fields)]
80pub struct LockSourceRepository {
81 uuid: Uuid,
82 url: UrlPath,
83 path: PathBuf,
84 project: String,
85 version: Version,
86 revision: String,
87 r#override: Option<PathBuf>,
88}
89
90impl PartialOrd for LockSource {
91 fn partial_cmp(&self, other: &LockSource) -> Option<std::cmp::Ordering> {
92 Some(self.cmp(other))
93 }
94}
95
96impl Ord for LockSource {
97 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
98 match (self, other) {
99 (LockSource::Repository(x), LockSource::Repository(y)) => x
100 .url
101 .cmp(&y.url)
102 .then(x.project.cmp(&y.project))
103 .then(x.version.cmp(&y.version)),
104 (LockSource::Path(x), LockSource::Path(y)) => x.cmp(y),
105 (LockSource::Repository(_), LockSource::Path(_)) => std::cmp::Ordering::Less,
106 (LockSource::Path(_), LockSource::Repository(_)) => std::cmp::Ordering::Greater,
107 }
108 }
109}
110
111impl fmt::Display for LockSource {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 let mut ret = String::new();
114 match self {
115 LockSource::Repository(x) => {
116 ret.push_str(&format!("{} : {} @ {}", x.project, x.url, x.version));
117 }
118 LockSource::Path(x) => {
119 ret.push_str(&format!("{}", x.to_string_lossy()));
120 }
121 }
122 ret.fmt(f)
123 }
124}
125
126#[derive(Clone, Debug, Serialize, Deserialize)]
127#[serde(deny_unknown_fields)]
128pub struct LockDependency {
129 pub name: String,
130 pub source: LockSource,
131}
132
133impl Lockfile {
134 pub fn load(metadata: &Metadata) -> Result<Self, MetadataError> {
135 let path = metadata
136 .lockfile_path
137 .canonicalize()
138 .map_err(|x| MetadataError::file_io(x, &metadata.lockfile_path))?;
139 let text = fs::read_to_string(&path).map_err(|x| MetadataError::file_io(x, &path))?;
140 let mut ret = LockfileCompat::load(&text, &path, metadata)?;
141 ret.metadata_path = metadata.metadata_path.clone();
142
143 let mut locks = Vec::new();
144 locks.append(&mut ret.projects);
145
146 ret.lock_table.clear();
147 for lock in locks {
148 ret.lock_table
149 .entry(lock.source.to_url())
150 .and_modify(|x| x.push(lock.clone()))
151 .or_insert(vec![lock]);
152 }
153 ret.sort_table();
154
155 Ok(ret)
156 }
157
158 pub fn save<T: AsRef<Path>>(&mut self, path: T) -> Result<(), MetadataError> {
159 self.projects.clear();
160 for locks in self.lock_table.values() {
161 for lock in locks {
162 self.projects.push(lock.clone());
163 }
164 }
165 self.projects.sort_by(|x, y| x.source.cmp(&y.source));
166
167 let mut text = String::new();
168 text.push_str("# This file is automatically @generated by Veryl.\n");
169 text.push_str("# It is not intended for manual editing.\n");
170 text.push_str(&toml::to_string_pretty(&self)?);
171 fs::write(&path, text.as_bytes()).map_err(|x| MetadataError::file_io(x, path.as_ref()))?;
172 Ok(())
173 }
174
175 pub fn new(metadata: &Metadata) -> Result<Self, MetadataError> {
176 let mut ret = Lockfile {
177 version: LOCKFILE_VERSION,
178 metadata_path: metadata.metadata_path.clone(),
179 ..Default::default()
180 };
181
182 let mut name_table = HashSet::new();
183 let mut src_table = HashMap::new();
184 let locks = ret.gen_locks(metadata, &mut name_table, &mut src_table, true, metadata)?;
185
186 for lock in locks {
187 info!("Adding dependency ({})", lock.source);
188 ret.lock_table
189 .entry(lock.source.to_url())
190 .and_modify(|x| x.push(lock.clone()))
191 .or_insert(vec![lock]);
192 }
193 ret.sort_table();
194
195 Ok(ret)
196 }
197
198 pub fn update(
199 &mut self,
200 metadata: &Metadata,
201 force_update: bool,
202 ) -> Result<bool, MetadataError> {
203 self.force_update = force_update;
204
205 let mut name_table = HashSet::new();
206 let mut src_table = HashMap::new();
207 let locks = self.gen_locks(metadata, &mut name_table, &mut src_table, true, metadata)?;
208
209 let old_table = self.lock_table.clone();
210 self.lock_table.clear();
211
212 let mut modified = false;
213
214 for lock in &locks {
215 let add = if let Some(old_locks) = old_table.get(&lock.source.to_url()) {
216 !old_locks.iter().any(|x| x.source == lock.source)
217 } else {
218 true
219 };
220
221 if add {
222 info!("Adding dependency ({})", lock.source);
223 modified = true;
224 }
225
226 self.lock_table
227 .entry(lock.source.to_url())
228 .and_modify(|x| x.push(lock.clone()))
229 .or_insert(vec![lock.clone()]);
230 }
231 self.sort_table();
232
233 for old_locks in old_table.values() {
234 for old_lock in old_locks {
235 if !locks.iter().any(|x| x.source == old_lock.source) {
236 info!("Removing dependency ({})", old_lock.source);
237 modified = true;
238 }
239 }
240 }
241
242 Ok(modified)
243 }
244
245 pub fn paths(&self, base_dst: &Path) -> Result<Vec<PathSet>, MetadataError> {
246 let mut ret = Vec::new();
247
248 for locks in self.lock_table.values() {
249 for lock in locks {
250 let metadata = self.get_metadata(&lock.source)?;
251 let path = metadata.project_path();
252
253 for src in &veryl_path::gather_files_with_extension(&path, "veryl", false)? {
254 let Ok(rel) = src.strip_prefix(&path) else {
255 return Err(MetadataError::InvalidSourceLocation(src.clone()));
256 };
257 let mut dst = base_dst.join(&lock.name);
258 dst.push(rel);
259 dst.set_extension("sv");
260 let mut map = dst.clone();
261 map.set_extension("sv.map");
262 ret.push(PathSet {
263 prj: lock.name.clone(),
264 src: src.to_path_buf(),
265 dst,
266 map,
267 });
268 }
269 }
270 }
271
272 Ok(ret)
273 }
274
275 pub fn clear_cache(&self) -> Result<(), MetadataError> {
276 let lock_resolve = veryl_path::lock_dir("resolve")?;
277 let lock_dependencies = veryl_path::lock_dir("dependencies")?;
278
279 for locks in self.lock_table.values() {
280 for lock in locks {
281 if let LockSource::Repository(x) = &lock.source {
282 let resolve_path = Self::resolve_path(&x.url)?;
283 let dependency_path = Self::dependency_path(&x.url, &x.path, &x.revision)?;
284 if resolve_path.exists() {
285 fs::remove_dir_all(&resolve_path)
286 .map_err(|x| MetadataError::file_io(x, &resolve_path))?;
287 }
288 if dependency_path.exists() {
289 fs::remove_dir_all(&dependency_path)
290 .map_err(|x| MetadataError::file_io(x, &dependency_path))?;
291 }
292 }
293 }
294 }
295
296 veryl_path::unlock_dir(lock_resolve)?;
297 veryl_path::unlock_dir(lock_dependencies)?;
298
299 Ok(())
300 }
301
302 fn git_clone(&self, url: &UrlPath, path: &Path) -> Result<Git, MetadataError> {
303 let url = match url {
304 UrlPath::Url(x) => UrlPath::Url(x.clone()),
305 UrlPath::Path(x) => {
306 if x.is_relative() {
307 let path = self.metadata_path.parent().unwrap().join(x);
308 UrlPath::Path(path)
309 } else {
310 UrlPath::Path(x.clone())
311 }
312 }
313 };
314
315 Git::clone(&url, path)
316 }
317
318 fn sort_table(&mut self) {
319 for locks in self.lock_table.values_mut() {
320 locks.sort_by(|a, b| b.source.cmp(&a.source));
321 }
322 }
323
324 fn gen_uuid(url: &UrlPath, path: &Path, revision: &str) -> Result<Uuid, MetadataError> {
325 let mut url = url.to_string();
326 url.push_str(&path.to_string_lossy());
327 url.push_str(revision);
328 Ok(Uuid::new_v5(&Uuid::NAMESPACE_URL, url.as_bytes()))
329 }
330
331 fn gen_locks(
332 &mut self,
333 metadata: &Metadata,
334 name_table: &mut HashSet<String>,
335 src_table: &mut HashMap<LockSource, String>,
336 root: bool,
337 root_metadata: &Metadata,
338 ) -> Result<Vec<Lock>, MetadataError> {
339 let mut ret = Vec::new();
340
341 let mut dependencies_metadata = Vec::new();
343 for (name, dep) in &metadata.dependencies {
344 let dependency = self.resolve_dependency(metadata, name, dep, root, root_metadata)?;
345 let metadata = self.get_metadata(&dependency.source)?;
346 let mut name = dependency.name.clone();
347
348 if name_table.contains(&name) {
350 if root {
351 return Err(MetadataError::NameConflict(name));
352 }
353 let mut suffix = 0;
354 loop {
355 let new_name = format!("{name}_{suffix}");
356 if !name_table.contains(&new_name) {
357 name = new_name;
358 break;
359 }
360 suffix += 1;
361 }
362 }
363 name_table.insert(name.clone());
364
365 let mut dependencies = Vec::new();
366 for (name, dep) in &metadata.dependencies {
367 let dependency =
368 self.resolve_dependency(&metadata, name, dep, root, root_metadata)?;
369 dependencies.push(dependency);
371 }
372
373 if let Some(x) = src_table.get(&dependency.source) {
374 if root {
375 return Err(MetadataError::InvalidDependency {
376 name: dependency.name.clone(),
377 cause: format!("it conflicts with {x}"),
378 });
379 }
380 } else {
381 let lock = Lock {
382 name: name.clone(),
383 source: dependency.source.clone(),
384 dependencies,
385 visible: root,
386 };
387
388 ret.push(lock);
389 src_table.insert(dependency.source.clone(), name.clone());
390 dependencies_metadata.push(metadata);
391 }
392 }
393
394 for metadata in dependencies_metadata {
395 let mut dependency_locks =
396 self.gen_locks(&metadata, name_table, src_table, false, root_metadata)?;
397 ret.append(&mut dependency_locks);
398 }
399
400 Ok(ret)
401 }
402
403 fn resolve_dependency(
404 &mut self,
405 metadata: &Metadata,
406 name: &str,
407 dep: &Dependency,
408 root: bool,
409 root_metadata: &Metadata,
410 ) -> Result<LockDependency, MetadataError> {
411 Ok(match dep {
412 Dependency::Version(_) => {
413 unimplemented!();
414 }
415 Dependency::Entry(x) => {
416 let url = if let Some(git) = &x.git {
417 Some(git.clone())
418 } else if let Some(github) = &x.github {
419 let url = format!("https://github.com/{github}");
420 let url = Url::parse(&url).unwrap();
421 Some(UrlPath::Url(url))
422 } else {
423 None
424 };
425 let project = x.project.clone().unwrap_or(name.to_string());
426 let source = if let Some(url) = &url {
427 let Some(version) = &x.version else {
428 return Err(MetadataError::InvalidDependency {
429 name: name.to_string(),
430 cause: "version is not specified".to_string(),
431 });
432 };
433 let (release, path) = self.resolve_version(url, &project, version)?;
434 let uuid = Self::gen_uuid(url, &path, &release.revision)?;
435
436 let r#override = if root { x.path.clone() } else { None };
438
439 LockSource::Repository(Box::new(LockSourceRepository {
440 uuid,
441 url: url.clone(),
442 path,
443 project,
444 version: release.version,
445 revision: release.revision,
446 r#override,
447 }))
448 } else if let Some(path) = &x.path {
449 let path = if path.is_absolute() {
450 path.clone()
451 } else {
452 let base = root_metadata.project_path();
453 let path = base.join(metadata.project_path()).join(path);
454 if !path.exists() {
455 let project = x.project.clone().unwrap_or(name.to_string());
456 return Err(MetadataError::ProjectNotFound {
457 url: UrlPath::Path(path),
458 project,
459 });
460 }
461 diff_paths(path.canonicalize().unwrap(), base).unwrap()
462 };
463 LockSource::Path(path)
464 } else {
465 return Err(MetadataError::InvalidDependency {
466 name: name.to_string(),
467 cause: "[git|github|path] are not specified".to_string(),
468 });
469 };
470 LockDependency {
471 name: name.to_string(),
472 source,
473 }
474 }
475 })
476 }
477
478 fn resolve_version(
479 &mut self,
480 url: &UrlPath,
481 project: &str,
482 version_req: &VersionReq,
483 ) -> Result<(Release, PathBuf), MetadataError> {
484 if let Some(release) = self.resolve_version_from_lockfile(url, project, version_req)? {
485 if self.force_update {
486 let latest = self.resolve_version_from_latest(url, project, version_req)?;
487 Ok(latest)
488 } else {
489 Ok(release)
490 }
491 } else {
492 let latest = self.resolve_version_from_latest(url, project, version_req)?;
493 Ok(latest)
494 }
495 }
496
497 fn resolve_version_from_lockfile(
498 &mut self,
499 url: &UrlPath,
500 project: &str,
501 version_req: &VersionReq,
502 ) -> Result<Option<(Release, PathBuf)>, MetadataError> {
503 if let Some(locks) = self.lock_table.get_mut(url) {
504 for lock in locks {
505 if let LockSource::Repository(x) = &lock.source
506 && x.project == project
507 && version_req.matches(&x.version)
508 {
509 let release = Release {
510 version: x.version.clone(),
511 revision: x.revision.clone(),
512 };
513 let path = x.path.clone();
514 return Ok(Some((release, path)));
515 }
516 }
517 }
518 Ok(None)
519 }
520
521 fn resolve_path(url: &UrlPath) -> Result<PathBuf, MetadataError> {
522 let resolve_dir = veryl_path::cache_path().join("resolve");
523 let uuid = Self::gen_uuid(url, &PathBuf::new(), "")?;
524 Ok(resolve_dir.join(uuid.simple().encode_lower(&mut Uuid::encode_buffer())))
525 }
526
527 fn search_project(path: &Path, project: &str) -> Option<PathBuf> {
528 for entry in WalkDir::new(path).into_iter().flatten() {
529 if entry.file_name() == "Veryl.toml"
530 && let Ok(metadata) = Metadata::load(entry.path())
531 && metadata.project.name == project
532 {
533 let ret = entry.path();
534 let ret = ret.parent().unwrap().strip_prefix(path).unwrap();
535 return Some(ret.to_path_buf());
536 }
537 }
538 None
539 }
540
541 fn resolve_version_from_latest(
542 &mut self,
543 url: &UrlPath,
544 project: &str,
545 version_req: &VersionReq,
546 ) -> Result<(Release, PathBuf), MetadataError> {
547 let resolve_dir = veryl_path::cache_path().join("resolve");
548
549 if !resolve_dir.exists() {
550 ignore_already_exists(fs::create_dir_all(&resolve_dir))
551 .map_err(|x| MetadataError::file_io(x, &resolve_dir))?;
552 }
553
554 let path = Self::resolve_path(url)?;
555 let lock = veryl_path::lock_dir("resolve")?;
556 let git = self.git_clone(url, &path)?;
557 git.fetch()?;
558 git.checkout(None)?;
559 veryl_path::unlock_dir(lock)?;
560
561 let Some(prj_path) = Self::search_project(&path, project) else {
562 return Err(MetadataError::ProjectNotFound {
563 url: url.clone(),
564 project: project.to_string(),
565 });
566 };
567
568 let toml = path.join(&prj_path).join("Veryl.pub");
569 let mut pubfile = Pubfile::load(toml)?;
570
571 pubfile.releases.sort_by(|a, b| b.version.cmp(&a.version));
572
573 for release in &pubfile.releases {
574 if version_req.matches(&release.version) {
575 return Ok((release.clone(), prj_path));
576 }
577 }
578
579 Err(MetadataError::VersionNotFound {
580 url: url.clone(),
581 version: version_req.to_string(),
582 })
583 }
584
585 fn dependency_path(
586 url: &UrlPath,
587 path: &Path,
588 revision: &str,
589 ) -> Result<PathBuf, MetadataError> {
590 let dependencies_dir = veryl_path::cache_path().join("dependencies");
591 let uuid = Self::gen_uuid(url, path, revision)?;
592 Ok(dependencies_dir.join(uuid.simple().encode_lower(&mut Uuid::encode_buffer())))
593 }
594
595 fn get_metadata(&self, source: &LockSource) -> Result<Metadata, MetadataError> {
596 let path = match source {
598 LockSource::Path(x) => Some(x.clone()),
599 LockSource::Repository(x) => x.r#override.clone(),
600 };
601 let path_metadata = if let Some(x) = path {
602 let path = self.metadata_path.parent().unwrap().join(x);
603 let path = path.join("Veryl.toml");
604 if path.exists() {
605 Some(Metadata::load(path)?)
606 } else {
607 None
608 }
609 } else {
610 None
611 };
612
613 match source {
614 LockSource::Path(_) => {
615 if let Some(x) = path_metadata {
616 Ok(x)
617 } else {
618 Err(MetadataError::FileNotFound)
619 }
620 }
621 LockSource::Repository(x) => {
622 if let Some(x) = path_metadata {
623 Ok(x)
624 } else {
625 let dependencies_dir = veryl_path::cache_path().join("dependencies");
626
627 if !dependencies_dir.exists() {
628 ignore_already_exists(fs::create_dir_all(&dependencies_dir))
629 .map_err(|x| MetadataError::file_io(x, &dependencies_dir))?;
630 }
631
632 let path = Self::dependency_path(&x.url, &x.path, &x.revision)?;
633 let toml = path.join("Veryl.toml");
634
635 let lock = veryl_path::lock_dir("dependencies")?;
639 if !path.exists() {
640 let git = self.git_clone(&x.url, &path)?;
641 git.fetch()?;
642 git.checkout(Some(&x.revision))?;
643 } else {
644 let git = Git::open(&path)?;
645 let ret = git.is_clean().is_ok_and(|x| x);
646
647 if !ret || !toml.exists() {
649 veryl_path::ignore_directory_not_empty(fs::remove_dir_all(&path))
650 .map_err(|x| MetadataError::file_io(x, &path))?;
651 let git = self.git_clone(&x.url, &path)?;
652 git.fetch()?;
653 git.checkout(Some(&x.revision))?;
654 }
655 }
656 veryl_path::unlock_dir(lock)?;
657
658 Metadata::load(toml)
659 }
660 }
661 }
662 }
663}
664
665impl FromStr for Lockfile {
666 type Err = MetadataError;
667
668 fn from_str(s: &str) -> Result<Self, Self::Err> {
669 let lockfile: Lockfile = toml::from_str(s)?;
670 Ok(lockfile)
671 }
672}
673
674#[derive(Clone, Debug, Default, Serialize, Deserialize)]
675pub struct LockfileCompat {
676 version: Option<usize>,
677}
678
679impl LockfileCompat {
680 pub fn load(
681 text: &str,
682 lockfile_path: &Path,
683 metadata: &Metadata,
684 ) -> Result<Lockfile, MetadataError> {
685 let compat: LockfileCompat = toml::from_str(text)?;
686 let version = compat.version.unwrap_or(0);
687 let mut lockfile = match version {
688 0 => {
689 info!(
690 "Migrating lockfile to v1 ({})",
691 lockfile_path.to_string_lossy()
692 );
693 let lockfile: lockfile_compat::v0::Lockfile = toml::from_str(text)?;
694 let mut lockfile = Lockfile::from_v0(lockfile, &metadata.metadata_path)?;
695 lockfile.save(lockfile_path)?;
696 lockfile
697 }
698 1 => toml::from_str(text)?,
699 _ => unreachable!(),
700 };
701
702 for lock in lockfile.projects.iter_mut() {
703 lock.visible = metadata.dependencies.contains_key(&lock.name);
704 }
705
706 Ok(lockfile)
707 }
708}
709
710impl Lockfile {
711 fn set_project(
712 mut source: LockSource,
713 metadata_path: &Path,
714 ) -> Result<LockSource, MetadataError> {
715 let lockfile = Lockfile {
716 metadata_path: metadata_path.to_path_buf(),
717 ..Default::default()
718 };
719 let metadata = lockfile.get_metadata(&source)?;
720 if let LockSource::Repository(x) = &mut source {
721 x.project = metadata.project.name;
722 }
723 Ok(source)
724 }
725
726 pub fn from_v0(
727 x: lockfile_compat::v0::Lockfile,
728 metadata_path: &Path,
729 ) -> Result<Self, MetadataError> {
730 let mut projects = Vec::new();
731 for lock in x.projects {
732 let mut dependencies = Vec::new();
733 for dep in lock.dependencies {
734 let uuid = Lockfile::gen_uuid(&dep.url, &PathBuf::new(), &dep.revision)?;
735 let source = LockSource::Repository(Box::new(LockSourceRepository {
736 uuid,
737 url: dep.url,
738 path: PathBuf::new(),
739 project: String::new(),
740 version: dep.version,
741 revision: dep.revision,
742 r#override: None,
743 }));
744 let source = Self::set_project(source, metadata_path)?;
745 let new_dep = LockDependency {
746 name: dep.name,
747 source,
748 };
749 dependencies.push(new_dep);
750 }
751
752 let uuid = Lockfile::gen_uuid(&lock.url, &PathBuf::new(), &lock.revision).unwrap();
753 let source = LockSource::Repository(Box::new(LockSourceRepository {
754 uuid,
755 url: lock.url,
756 path: PathBuf::new(),
757 project: String::new(),
758 version: lock.version,
759 revision: lock.revision,
760 r#override: lock.path,
761 }));
762 let source = Self::set_project(source, metadata_path)?;
763
764 let new_lock = Lock {
765 name: lock.name,
766 source,
767 dependencies,
768 visible: false,
769 };
770
771 projects.push(new_lock);
772 }
773
774 Ok(Lockfile {
775 version: LOCKFILE_VERSION,
776 projects,
777 ..Default::default()
778 })
779 }
780}