1use crate::build::{Build, Target};
2use crate::build_info::BuildInfo;
3use crate::component::Component;
4use crate::doc::Doc;
5use crate::format::Format;
6use crate::git::Git;
7use crate::lint::Lint;
8use crate::lockfile::Lockfile;
9use crate::project::Project;
10use crate::pubfile::{Pubfile, Release};
11use crate::publish::Publish;
12use crate::synth::Synth;
13use crate::test::Test;
14use crate::{FilelistType, MetadataError, SourceMapTarget};
15use log::{debug, info, warn};
16use once_cell::sync::Lazy;
17use regex::Regex;
18use semver::VersionReq;
19use serde::{Deserialize, Serialize};
20use spdx::Expression;
21use std::collections::{BTreeMap, HashMap};
22use std::env;
23use std::fmt;
24use std::fs;
25use std::path::{Path, PathBuf};
26use std::str::FromStr;
27use std::time::SystemTime;
28use url::Url;
29use veryl_path::{PathSet, ignore_already_exists};
30
31#[derive(Clone, Copy, Debug)]
32pub enum BumpKind {
33 Major,
34 Minor,
35 Patch,
36}
37
38#[derive(Clone, Debug, Serialize, Deserialize)]
39#[serde(deny_unknown_fields)]
40pub struct Metadata {
41 pub project: Project,
42 #[serde(default)]
43 pub build: Build,
44 #[serde(default)]
45 pub format: Format,
46 #[serde(default)]
47 pub lint: Lint,
48 #[serde(default)]
49 pub publish: Publish,
50 #[serde(default)]
51 pub doc: Doc,
52 #[serde(default)]
53 pub test: Test,
54 #[serde(default)]
55 pub synth: Synth,
56 #[serde(default)]
57 pub properties: BTreeMap<String, ProjectProperty>,
58 #[serde(default)]
59 pub components: Vec<Component>,
60 #[serde(default)]
61 pub dependencies: HashMap<String, Dependency>,
62 #[serde(default)]
63 pub metadata: HashMap<String, toml::Value>,
64 #[serde(skip)]
65 pub metadata_path: PathBuf,
66 #[serde(skip)]
67 pub pubfile_path: PathBuf,
68 #[serde(skip)]
69 pub pubfile: Pubfile,
70 #[serde(skip)]
71 pub lockfile_path: PathBuf,
72 #[serde(skip)]
73 pub lockfile: Lockfile,
74 #[serde(skip)]
75 pub build_info: BuildInfo,
76 #[serde(skip)]
80 pub output_dir_override: Option<PathBuf>,
81}
82
83#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)]
84#[serde(untagged)]
85pub enum ProjectProperty {
86 Int(i64),
87 Bool(bool),
88}
89
90impl ProjectProperty {
91 pub fn is_compatible(&self, other: &ProjectProperty) -> bool {
92 matches!(
93 (self, other),
94 (ProjectProperty::Int(_), ProjectProperty::Int(_))
95 | (ProjectProperty::Bool(_), ProjectProperty::Bool(_))
96 )
97 }
98
99 pub fn type_name(&self) -> String {
100 match self {
101 ProjectProperty::Int(_) => "int".to_string(),
102 ProjectProperty::Bool(_) => "bool".to_string(),
103 }
104 }
105
106 pub fn value_string(&self) -> String {
107 match self {
108 ProjectProperty::Int(x) => x.to_string(),
109 ProjectProperty::Bool(x) => x.to_string(),
110 }
111 }
112
113 pub fn verilog_value_string(&self) -> String {
114 match self {
115 ProjectProperty::Int(x) => x.to_string(),
116 ProjectProperty::Bool(x) => (if *x { "1'b1" } else { "1'b0" }).to_string(),
117 }
118 }
119}
120
121#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)]
122#[serde(untagged)]
123pub enum UrlPath {
124 Url(Url),
125 Path(PathBuf),
126}
127
128impl fmt::Display for UrlPath {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 UrlPath::Url(x) => x.fmt(f),
132 UrlPath::Path(x) => {
133 let text = x.to_string_lossy();
134 text.fmt(f)
135 }
136 }
137 }
138}
139
140static VALID_PROJECT_NAME: Lazy<Regex> =
141 Lazy::new(|| Regex::new(r"^[a-zA-Z_][0-9a-zA-Z_]*$").unwrap());
142
143pub fn check_project_name(name: &str) -> Result<(), MetadataError> {
145 if !VALID_PROJECT_NAME.is_match(name) {
146 return Err(MetadataError::InvalidProjectName(name.to_string()));
147 }
148 if name.starts_with("__") {
149 return Err(MetadataError::ReservedProjectName(name.to_string()));
150 }
151 Ok(())
152}
153
154impl Metadata {
155 pub fn search_from_current() -> Result<PathBuf, MetadataError> {
156 Metadata::search_from(
157 env::current_dir().map_err(|x| MetadataError::file_io(x, &PathBuf::from(".")))?,
158 )
159 }
160
161 pub fn search_from<T: AsRef<Path>>(from: T) -> Result<PathBuf, MetadataError> {
162 for path in from.as_ref().ancestors() {
163 let path = path.join("Veryl.toml");
164 if path.is_file() {
165 return Ok(path);
166 }
167 }
168
169 Err(MetadataError::FileNotFound(from.as_ref().to_path_buf()))
170 }
171
172 pub fn load<T: AsRef<Path>>(path: T) -> Result<Self, MetadataError> {
173 let path = path
174 .as_ref()
175 .canonicalize()
176 .map_err(|x| MetadataError::file_io(x, path.as_ref()))?;
177 let text = fs::read_to_string(&path).map_err(|x| MetadataError::file_io(x, &path))?;
178 let mut metadata: Metadata = Self::from_str(&text)?;
179 metadata.metadata_path.clone_from(&path);
180 metadata.pubfile_path = path.with_file_name("Veryl.pub");
181 metadata.lockfile_path = path.with_file_name("Veryl.lock");
182 metadata.check()?;
183
184 if metadata.pubfile_path.exists() {
185 metadata.pubfile = Pubfile::load(&metadata.pubfile_path)?;
186 }
187
188 let dot_build = metadata.project_dot_build_path();
189 if !dot_build.exists() {
190 ignore_already_exists(fs::create_dir(&dot_build))
191 .map_err(|x| MetadataError::file_io(x, &dot_build))?;
192 }
193
194 let build_info = metadata.project_build_info_path();
195 if build_info.exists() {
196 if let Ok(info) = BuildInfo::load(&build_info) {
197 metadata.build_info = info;
198 } else {
199 info!("Discarded incompatible .build/info.toml");
201 }
202 }
203
204 debug!(
205 "Loaded metadata ({})",
206 metadata.metadata_path.to_string_lossy()
207 );
208 Ok(metadata)
209 }
210
211 pub fn publish(&mut self) -> Result<(), MetadataError> {
212 let prj_path = self.project_path();
213 let git = Git::open(&prj_path)?;
214 if !git.is_clean()? {
215 return Err(MetadataError::ModifiedProject(prj_path.to_path_buf()));
216 }
217
218 let version = self
219 .project
220 .version
221 .clone()
222 .ok_or(MetadataError::MissingVersion)?;
223
224 for release in &self.pubfile.releases {
225 if release.version == version {
226 return Err(MetadataError::PublishedVersion(version));
227 }
228 }
229
230 let revision = git.get_revision()?;
231
232 info!("Publishing release ({version} @ {revision})");
233
234 let release = Release { version, revision };
235
236 self.pubfile.releases.push(release);
237
238 self.pubfile.save(&self.pubfile_path)?;
239 info!("Writing metadata ({})", self.pubfile_path.to_string_lossy());
240
241 if self.publish.publish_commit {
242 git.add(&self.pubfile_path)?;
243 git.commit(&self.publish.publish_commit_message)?;
244 info!(
245 "Committing metadata ({})",
246 self.pubfile_path.to_string_lossy()
247 );
248 } else {
249 warn!(
250 "Please git add and commit Veryl.pub (set `publish_commit = true` in [publish] to automate this)"
251 );
252 }
253
254 Ok(())
255 }
256
257 pub fn check(&self) -> Result<(), MetadataError> {
258 check_project_name(&self.project.name)?;
259
260 if let Some(ref license) = self.project.license {
261 let _ = Expression::parse(license)?;
262 }
263
264 Ok(())
265 }
266
267 pub fn bump_version(&mut self, kind: BumpKind) -> Result<(), MetadataError> {
268 let prj_path = self.project_path();
269 let git = Git::open(&prj_path)?;
270
271 let current_version = self
272 .project
273 .version
274 .as_ref()
275 .ok_or(MetadataError::MissingVersion)?;
276
277 let mut bumped_version = current_version.clone();
278
279 match kind {
280 BumpKind::Major => {
281 bumped_version.major += 1;
282 bumped_version.minor = 0;
283 bumped_version.patch = 0;
284 }
285 BumpKind::Minor => {
286 bumped_version.minor += 1;
287 bumped_version.patch = 0;
288 }
289 BumpKind::Patch => bumped_version.patch += 1,
290 }
291 info!(
292 "Bumping version ({} -> {})",
293 current_version, bumped_version
294 );
295
296 self.project.version = Some(bumped_version.clone());
297
298 let toml = fs::read_to_string(&self.metadata_path)
299 .map_err(|x| MetadataError::file_io(x, &self.metadata_path))?;
300 let re = Regex::new(r#"version\s+=\s+"([^"]*)""#).unwrap();
301 let caps = re
302 .captures(&toml)
303 .expect("safely unwrap because metadata is valid");
304 let bumped_field = caps[0].replace(&caps[1], &bumped_version.to_string());
305 let bumped_toml = re.replace(&toml, bumped_field);
306 fs::write(&self.metadata_path, bumped_toml.as_bytes())
307 .map_err(|x| MetadataError::file_io(x, &self.metadata_path))?;
308 info!(
309 "Updating version field ({})",
310 self.metadata_path.to_string_lossy()
311 );
312
313 if self.publish.bump_commit {
314 git.add(&self.metadata_path)?;
315 git.commit(&self.publish.bump_commit_message)?;
316 info!(
317 "Committing metadata ({})",
318 self.metadata_path.to_string_lossy()
319 );
320 }
321
322 Ok(())
323 }
324
325 pub fn update_lockfile(&mut self) -> Result<(), MetadataError> {
326 let modified = if self.lockfile_path.exists() {
327 let mut lockfile = Lockfile::load(self)?;
328 let modified = lockfile.update(self, false)?;
329 self.lockfile = lockfile;
330 modified
331 } else {
332 self.lockfile = Lockfile::new(self)?;
333 true
334 };
335 if modified {
336 self.lockfile.save(&self.lockfile_path)?;
337 }
338 Ok(())
339 }
340
341 pub fn save_build_info(&mut self) -> Result<(), MetadataError> {
342 let build_info = self.project_build_info_path();
343 self.build_info.save(&build_info)
344 }
345
346 pub fn add_generated_file(&mut self, path: PathBuf) {
347 self.build_info
348 .generated_files
349 .insert(path, SystemTime::now());
350 }
351
352 pub fn paths<T: AsRef<Path>>(
353 &mut self,
354 files: &[T],
355 symlink: bool,
356 include_dependencies: bool,
357 ) -> Result<Vec<PathSet>, MetadataError> {
358 let sources = if self.build.source.iter().count() > 0 {
359 warn!(
360 "[Veryl.toml] \"source\" field is deprecated. Replace it with \"sources\" field."
361 );
362 vec![self.build.source.clone()]
363 } else {
364 self.build.sources.clone()
365 };
366
367 let base = self.project_path();
368 let out_base = self.output_dir();
371 let mut ret = Vec::new();
372
373 let canonical_files = if files.is_empty() {
377 None
378 } else {
379 let mut v = Vec::new();
380 for file in files {
381 v.push(
382 fs::canonicalize(file.as_ref())
383 .map_err(|x| MetadataError::file_io(x, file.as_ref()))?,
384 );
385 }
386 Some(v)
387 };
388 let mut explicit_routed = canonical_files.as_ref().map(|v| vec![false; v.len()]);
389
390 let examples_base = base.join("examples");
393 if let Some(source) = sources
394 .iter()
395 .find(|x| base.join(x).starts_with(&examples_base))
396 {
397 return Err(MetadataError::ReservedSourceDir(base.join(source)));
398 }
399
400 let mut source_dirs: Vec<(PathBuf, bool)> =
401 sources.iter().map(|x| (base.join(x), false)).collect();
402 if examples_base.exists() {
403 source_dirs.push((examples_base.clone(), true));
404 }
405
406 for (src_base, is_example) in source_dirs {
407 let src_files = if let Some(cf) = canonical_files.as_ref() {
408 let mut ret = Vec::new();
413 for (i, path) in cf.iter().enumerate() {
414 if path.starts_with(&src_base)
415 && (is_example || !path.starts_with(&examples_base))
416 {
417 ret.push(path.clone());
418 if let Some(ref mut flags) = explicit_routed {
419 flags[i] = true;
420 }
421 }
422 }
423 ret
424 } else {
425 let mut files =
426 veryl_path::gather_files_with_extension(&src_base, "veryl", symlink)?;
427 if !is_example {
428 files.retain(|x| !x.starts_with(&examples_base));
429 }
430 files
431 };
432
433 for src in src_files {
434 let Ok(src_relative) = src.strip_prefix(&src_base) else {
435 return Err(MetadataError::InvalidSourceLocation(src));
436 };
437 let dst = match self.build.target {
438 Target::Source => {
439 if self.output_dir_override.is_some() {
440 out_base.join(src_relative.with_extension("sv"))
443 } else {
444 src.with_extension("sv")
445 }
446 }
447 Target::Directory { ref path } => {
448 out_base.join(path.join(src_relative.with_extension("sv")))
449 }
450 Target::Bundle { .. } => out_base.join(
451 PathBuf::from("target").join(src.with_extension("sv").file_name().unwrap()),
452 ),
453 };
454 let map = match &self.build.sourcemap_target {
455 SourceMapTarget::Directory { path } => {
456 if let Target::Directory { .. } = self.build.target {
457 out_base.join(path.join(src_relative.with_extension("sv.map")))
458 } else {
459 let dst = dst.strip_prefix(&out_base).unwrap();
460 out_base.join(path.join(dst.with_extension("sv.map")))
461 }
462 }
463 _ => {
464 let mut map = dst.clone();
465 map.set_extension("sv.map");
466 map
467 }
468 };
469 ret.push(PathSet {
470 prj: self.project.name.clone(),
471 src: src.to_path_buf(),
472 dst,
473 map,
474 example: is_example,
475 });
476 }
477 }
478
479 if let (Some(cf), Some(flags)) = (canonical_files.as_ref(), explicit_routed.as_ref())
482 && let Some(pos) = flags.iter().position(|f| !f)
483 {
484 return Err(MetadataError::InvalidSourceLocation(cf[pos].clone()));
485 }
486
487 let base_dst = self.output_dependencies_path();
488 if !base_dst.exists() {
489 ignore_already_exists(fs::create_dir_all(&base_dst))
490 .map_err(|x| MetadataError::file_io(x, &base_dst))?;
491 }
492
493 if include_dependencies {
494 if !self.build.exclude_std {
495 veryl_std::expand()?;
496 ret.append(&mut veryl_std::paths(&base_dst)?);
497 }
498
499 self.update_lockfile()?;
500
501 let mut deps = self.lockfile.paths(&base_dst)?;
502 ret.append(&mut deps);
503 }
504
505 Ok(ret)
506 }
507
508 pub fn create_default_toml(name: &str) -> Result<String, MetadataError> {
509 check_project_name(name)?;
510
511 Ok(format!(
512 r###"[project]
513name = "{name}"
514version = "0.1.0"
515[build]
516sources = ["src"]
517target = {{type = "directory", path = "target"}}"###
518 ))
519 }
520
521 pub fn create_default(name: &str) -> Result<Metadata, MetadataError> {
522 let metadata: Metadata = toml::from_str(&Self::create_default_toml(name)?)?;
523 Ok(metadata)
524 }
525
526 pub fn create_default_gitignore() -> &'static str {
527 r#"# Build output
528.build/
529/target
530/dependencies
531*.f
532
533# Verilator
534obj_dir/
535"#
536 }
537
538 pub fn project_path(&self) -> PathBuf {
539 self.metadata_path.parent().unwrap().to_path_buf()
540 }
541
542 pub fn output_dir(&self) -> PathBuf {
543 self.output_dir_override
544 .clone()
545 .unwrap_or_else(|| self.project_path())
546 }
547
548 pub fn project_dependencies_path(&self) -> PathBuf {
549 self.project_path().join("dependencies")
550 }
551
552 pub fn output_dependencies_path(&self) -> PathBuf {
553 self.output_dir().join("dependencies")
554 }
555
556 pub fn project_dot_build_path(&self) -> PathBuf {
557 self.project_path().join(".build")
558 }
559
560 pub fn project_build_info_path(&self) -> PathBuf {
561 self.project_dot_build_path().join("info.toml")
562 }
563
564 pub fn filelist_path(&self) -> PathBuf {
565 let filelist_name = match self.build.filelist_type {
566 FilelistType::Absolute => format!("{}.f", self.project.name),
567 FilelistType::Relative => format!("{}.f", self.project.name),
568 FilelistType::Flgen => format!("{}.list.rb", self.project.name),
569 };
570
571 self.output_dir().join(filelist_name)
572 }
573
574 pub fn doc_path(&self) -> PathBuf {
575 self.metadata_path.parent().unwrap().join(&self.doc.path)
576 }
577
578 pub fn collect_dependency_components(
582 &self,
583 ) -> Result<Vec<crate::lockfile::DependencyComponents>, MetadataError> {
584 self.lockfile.collect_components()
585 }
586
587 pub fn collect_component_manifests(
594 &self,
595 ) -> Vec<(String, crate::component_manifest::ComponentManifest)> {
596 fn collect_project(
600 components: &[crate::Component],
601 root: &Path,
602 target_dir: &Path,
603 project: Option<&str>,
604 ret: &mut Vec<(String, crate::component_manifest::ComponentManifest)>,
605 ) {
606 let mut seen = std::collections::HashSet::new();
607 for def in components {
608 for (name, manifest) in def.collect_manifests(root, target_dir) {
609 if seen.insert(name.clone()) {
610 let key = match project {
611 Some(project) => format!("{project}::{name}"),
612 None => name,
613 };
614 ret.push((key, manifest));
615 } else {
616 let scope = project
617 .map(|p| format!(" of dependency `{p}`"))
618 .unwrap_or_default();
619 log::warn!(
620 "component `{name}` is exported by more than one [[components]] package{scope}; the first declaration wins"
621 );
622 }
623 }
624 }
625 }
626
627 let mut ret = vec![];
628 if self.metadata_path.as_os_str().is_empty() {
631 return ret;
632 }
633 let root = self.project_path();
634 let target_dir = root.join("target/veryl-components");
635 collect_project(&self.components, &root, &target_dir, None, &mut ret);
636 if let Ok(deps) = self.collect_dependency_components() {
637 for dep in &deps {
638 collect_project(
639 &dep.components,
640 &dep.root,
641 &dep.target_dir,
642 Some(&dep.project),
643 &mut ret,
644 );
645 }
646 }
647 ret
648 }
649}
650
651impl FromStr for Metadata {
652 type Err = MetadataError;
653
654 fn from_str(s: &str) -> Result<Self, Self::Err> {
655 let metadata: Metadata = toml::from_str(s)?;
656 Ok(metadata)
657 }
658}
659
660#[derive(Clone, Debug, Serialize, Deserialize)]
661#[serde(untagged)]
662#[serde(deny_unknown_fields)]
663pub enum Dependency {
664 Version(VersionReq),
665 Entry(Box<DependencyEntry>),
666}
667
668#[derive(Clone, Debug, Serialize, Deserialize)]
669#[serde(deny_unknown_fields)]
670pub struct DependencyEntry {
671 pub version: Option<VersionReq>,
672 pub git: Option<UrlPath>,
673 pub github: Option<String>,
674 pub project: Option<String>,
675 pub path: Option<PathBuf>,
676 #[serde(default)]
677 pub properties: HashMap<String, ProjectProperty>,
678}