1use crate::build::{Build, Target};
2use crate::build_info::BuildInfo;
3use crate::doc::Doc;
4use crate::format::Format;
5use crate::git::Git;
6use crate::lint::Lint;
7use crate::lockfile::Lockfile;
8use crate::project::Project;
9use crate::pubfile::{Pubfile, Release};
10use crate::publish::Publish;
11use crate::synth::Synth;
12use crate::test::Test;
13use crate::{FilelistType, MetadataError, SourceMapTarget};
14use log::{debug, info, warn};
15use once_cell::sync::Lazy;
16use regex::Regex;
17use semver::VersionReq;
18use serde::{Deserialize, Serialize};
19use spdx::Expression;
20use std::collections::HashMap;
21use std::env;
22use std::fmt;
23use std::fs;
24use std::path::{Path, PathBuf};
25use std::str::FromStr;
26use std::time::SystemTime;
27use url::Url;
28use veryl_path::{PathSet, ignore_already_exists};
29
30#[derive(Clone, Copy, Debug)]
31pub enum BumpKind {
32 Major,
33 Minor,
34 Patch,
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize)]
38#[serde(deny_unknown_fields)]
39pub struct Metadata {
40 pub project: Project,
41 #[serde(default)]
42 pub build: Build,
43 #[serde(default)]
44 pub format: Format,
45 #[serde(default)]
46 pub lint: Lint,
47 #[serde(default)]
48 pub publish: Publish,
49 #[serde(default)]
50 pub doc: Doc,
51 #[serde(default)]
52 pub test: Test,
53 #[serde(default)]
54 pub synth: Synth,
55 #[serde(default)]
56 pub dependencies: HashMap<String, Dependency>,
57 #[serde(skip)]
58 pub metadata_path: PathBuf,
59 #[serde(skip)]
60 pub pubfile_path: PathBuf,
61 #[serde(skip)]
62 pub pubfile: Pubfile,
63 #[serde(skip)]
64 pub lockfile_path: PathBuf,
65 #[serde(skip)]
66 pub lockfile: Lockfile,
67 #[serde(skip)]
68 pub build_info: BuildInfo,
69}
70
71#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)]
72#[serde(untagged)]
73pub enum UrlPath {
74 Url(Url),
75 Path(PathBuf),
76}
77
78impl fmt::Display for UrlPath {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 UrlPath::Url(x) => x.fmt(f),
82 UrlPath::Path(x) => {
83 let text = x.to_string_lossy();
84 text.fmt(f)
85 }
86 }
87 }
88}
89
90static VALID_PROJECT_NAME: Lazy<Regex> =
91 Lazy::new(|| Regex::new(r"^[a-zA-Z_][0-9a-zA-Z_]*$").unwrap());
92
93fn check_project_name(name: &str) -> Result<(), MetadataError> {
94 if !VALID_PROJECT_NAME.is_match(name) {
95 return Err(MetadataError::InvalidProjectName(name.to_string()));
96 }
97 if name.starts_with("__") {
98 return Err(MetadataError::ReservedProjectName(name.to_string()));
99 }
100 Ok(())
101}
102
103impl Metadata {
104 pub fn search_from_current() -> Result<PathBuf, MetadataError> {
105 Metadata::search_from(
106 env::current_dir().map_err(|x| MetadataError::file_io(x, &PathBuf::from(".")))?,
107 )
108 }
109
110 pub fn search_from<T: AsRef<Path>>(from: T) -> Result<PathBuf, MetadataError> {
111 for path in from.as_ref().ancestors() {
112 let path = path.join("Veryl.toml");
113 if path.is_file() {
114 return Ok(path);
115 }
116 }
117
118 Err(MetadataError::FileNotFound)
119 }
120
121 pub fn load<T: AsRef<Path>>(path: T) -> Result<Self, MetadataError> {
122 let path = path
123 .as_ref()
124 .canonicalize()
125 .map_err(|x| MetadataError::file_io(x, path.as_ref()))?;
126 let text = fs::read_to_string(&path).map_err(|x| MetadataError::file_io(x, &path))?;
127 let mut metadata: Metadata = Self::from_str(&text)?;
128 metadata.metadata_path.clone_from(&path);
129 metadata.pubfile_path = path.with_file_name("Veryl.pub");
130 metadata.lockfile_path = path.with_file_name("Veryl.lock");
131 metadata.check()?;
132
133 if metadata.pubfile_path.exists() {
134 metadata.pubfile = Pubfile::load(&metadata.pubfile_path)?;
135 }
136
137 let dot_build = metadata.project_dot_build_path();
138 if !dot_build.exists() {
139 ignore_already_exists(fs::create_dir(&dot_build))
140 .map_err(|x| MetadataError::file_io(x, &dot_build))?;
141 }
142
143 let build_info = metadata.project_build_info_path();
144 if build_info.exists() {
145 if let Ok(info) = BuildInfo::load(&build_info) {
146 metadata.build_info = info;
147 } else {
148 info!("Discarded incompatible .build/info.toml");
150 }
151 }
152
153 debug!(
154 "Loaded metadata ({})",
155 metadata.metadata_path.to_string_lossy()
156 );
157 Ok(metadata)
158 }
159
160 pub fn publish(&mut self) -> Result<(), MetadataError> {
161 let prj_path = self.project_path();
162 let git = Git::open(&prj_path)?;
163 if !git.is_clean()? {
164 return Err(MetadataError::ModifiedProject(prj_path.to_path_buf()));
165 }
166
167 let version = self
168 .project
169 .version
170 .clone()
171 .ok_or(MetadataError::MissingVersion)?;
172
173 for release in &self.pubfile.releases {
174 if release.version == version {
175 return Err(MetadataError::PublishedVersion(version));
176 }
177 }
178
179 let revision = git.get_revision()?;
180
181 info!("Publishing release ({version} @ {revision})");
182
183 let release = Release { version, revision };
184
185 self.pubfile.releases.push(release);
186
187 self.pubfile.save(&self.pubfile_path)?;
188 info!("Writing metadata ({})", self.pubfile_path.to_string_lossy());
189
190 if self.publish.publish_commit {
191 git.add(&self.pubfile_path)?;
192 git.commit(&self.publish.publish_commit_message)?;
193 info!(
194 "Committing metadata ({})",
195 self.pubfile_path.to_string_lossy()
196 );
197 }
198
199 Ok(())
200 }
201
202 pub fn check(&self) -> Result<(), MetadataError> {
203 check_project_name(&self.project.name)?;
204
205 if let Some(ref license) = self.project.license {
206 let _ = Expression::parse(license)?;
207 }
208
209 Ok(())
210 }
211
212 pub fn bump_version(&mut self, kind: BumpKind) -> Result<(), MetadataError> {
213 let prj_path = self.project_path();
214 let git = Git::open(&prj_path)?;
215
216 let current_version = self
217 .project
218 .version
219 .as_ref()
220 .ok_or(MetadataError::MissingVersion)?;
221
222 let mut bumped_version = current_version.clone();
223
224 match kind {
225 BumpKind::Major => {
226 bumped_version.major += 1;
227 bumped_version.minor = 0;
228 bumped_version.patch = 0;
229 }
230 BumpKind::Minor => {
231 bumped_version.minor += 1;
232 bumped_version.patch = 0;
233 }
234 BumpKind::Patch => bumped_version.patch += 1,
235 }
236 info!(
237 "Bumping version ({} -> {})",
238 current_version, bumped_version
239 );
240
241 self.project.version = Some(bumped_version.clone());
242
243 let toml = fs::read_to_string(&self.metadata_path)
244 .map_err(|x| MetadataError::file_io(x, &self.metadata_path))?;
245 let re = Regex::new(r#"version\s+=\s+"([^"]*)""#).unwrap();
246 let caps = re
247 .captures(&toml)
248 .expect("safely unwrap because metadata is valid");
249 let bumped_field = caps[0].replace(&caps[1], &bumped_version.to_string());
250 let bumped_toml = re.replace(&toml, bumped_field);
251 fs::write(&self.metadata_path, bumped_toml.as_bytes())
252 .map_err(|x| MetadataError::file_io(x, &self.metadata_path))?;
253 info!(
254 "Updating version field ({})",
255 self.metadata_path.to_string_lossy()
256 );
257
258 if self.publish.bump_commit {
259 git.add(&self.metadata_path)?;
260 git.commit(&self.publish.bump_commit_message)?;
261 info!(
262 "Committing metadata ({})",
263 self.metadata_path.to_string_lossy()
264 );
265 }
266
267 Ok(())
268 }
269
270 pub fn update_lockfile(&mut self) -> Result<(), MetadataError> {
271 let modified = if self.lockfile_path.exists() {
272 let mut lockfile = Lockfile::load(self)?;
273 let modified = lockfile.update(self, false)?;
274 self.lockfile = lockfile;
275 modified
276 } else {
277 self.lockfile = Lockfile::new(self)?;
278 true
279 };
280 if modified {
281 self.lockfile.save(&self.lockfile_path)?;
282 }
283 Ok(())
284 }
285
286 pub fn save_build_info(&mut self) -> Result<(), MetadataError> {
287 let build_info = self.project_build_info_path();
288 self.build_info.save(&build_info)
289 }
290
291 pub fn add_generated_file(&mut self, path: PathBuf) {
292 self.build_info
293 .generated_files
294 .insert(path, SystemTime::now());
295 }
296
297 pub fn paths<T: AsRef<Path>>(
298 &mut self,
299 files: &[T],
300 symlink: bool,
301 include_dependencies: bool,
302 ) -> Result<Vec<PathSet>, MetadataError> {
303 let sources = if self.build.source.iter().count() > 0 {
304 warn!(
305 "[Veryl.toml] \"source\" field is deprecated. Replace it with \"sources\" field."
306 );
307 vec![self.build.source.clone()]
308 } else {
309 self.build.sources.clone()
310 };
311
312 let base = self.project_path();
313 let mut ret = Vec::new();
314
315 let canonical_files = if files.is_empty() {
319 None
320 } else {
321 let mut v = Vec::new();
322 for file in files {
323 v.push(
324 fs::canonicalize(file.as_ref())
325 .map_err(|x| MetadataError::file_io(x, file.as_ref()))?,
326 );
327 }
328 Some(v)
329 };
330 let mut explicit_routed = canonical_files.as_ref().map(|v| vec![false; v.len()]);
331
332 for source in &sources {
333 let src_base = base.join(source);
334
335 let src_files = if let Some(cf) = canonical_files.as_ref() {
336 let mut ret = Vec::new();
339 for (i, path) in cf.iter().enumerate() {
340 if path.starts_with(&src_base) {
341 ret.push(path.clone());
342 if let Some(ref mut flags) = explicit_routed {
343 flags[i] = true;
344 }
345 }
346 }
347 ret
348 } else {
349 veryl_path::gather_files_with_extension(&src_base, "veryl", symlink)?
350 };
351
352 for src in src_files {
353 let Ok(src_relative) = src.strip_prefix(&src_base) else {
354 return Err(MetadataError::InvalidSourceLocation(src));
355 };
356 let dst = match self.build.target {
357 Target::Source => src.with_extension("sv"),
358 Target::Directory { ref path } => {
359 base.join(path.join(src_relative.with_extension("sv")))
360 }
361 Target::Bundle { .. } => base.join(
362 PathBuf::from("target").join(src.with_extension("sv").file_name().unwrap()),
363 ),
364 };
365 let map = match &self.build.sourcemap_target {
366 SourceMapTarget::Directory { path } => {
367 if let Target::Directory { .. } = self.build.target {
368 base.join(path.join(src_relative.with_extension("sv.map")))
369 } else {
370 let dst = dst.strip_prefix(&base).unwrap();
371 base.join(path.join(dst.with_extension("sv.map")))
372 }
373 }
374 _ => {
375 let mut map = dst.clone();
376 map.set_extension("sv.map");
377 map
378 }
379 };
380 ret.push(PathSet {
381 prj: self.project.name.clone(),
382 src: src.to_path_buf(),
383 dst,
384 map,
385 });
386 }
387 }
388
389 if let (Some(cf), Some(flags)) = (canonical_files.as_ref(), explicit_routed.as_ref())
392 && let Some(pos) = flags.iter().position(|f| !f)
393 {
394 return Err(MetadataError::InvalidSourceLocation(cf[pos].clone()));
395 }
396
397 let base_dst = self.project_dependencies_path();
398 if !base_dst.exists() {
399 ignore_already_exists(fs::create_dir(&base_dst))
400 .map_err(|x| MetadataError::file_io(x, &base_dst))?;
401 }
402
403 if include_dependencies {
404 if !self.build.exclude_std {
405 veryl_std::expand()?;
406 ret.append(&mut veryl_std::paths(&base_dst)?);
407 }
408
409 self.update_lockfile()?;
410
411 let mut deps = self.lockfile.paths(&base_dst)?;
412 ret.append(&mut deps);
413 }
414
415 Ok(ret)
416 }
417
418 pub fn create_default_toml(name: &str) -> Result<String, MetadataError> {
419 check_project_name(name)?;
420
421 Ok(format!(
422 r###"[project]
423name = "{name}"
424version = "0.1.0"
425[build]
426sources = ["src"]
427target = {{type = "directory", path = "target"}}"###
428 ))
429 }
430
431 pub fn create_default(name: &str) -> Result<Metadata, MetadataError> {
432 let metadata: Metadata = toml::from_str(&Self::create_default_toml(name)?)?;
433 Ok(metadata)
434 }
435
436 pub fn create_default_gitignore() -> &'static str {
437 r#"# Build output
438.build/
439/target
440/dependencies
441*.f
442
443# Verilator
444obj_dir/
445"#
446 }
447
448 pub fn project_path(&self) -> PathBuf {
449 self.metadata_path.parent().unwrap().to_path_buf()
450 }
451
452 pub fn project_dependencies_path(&self) -> PathBuf {
453 self.project_path().join("dependencies")
454 }
455
456 pub fn project_dot_build_path(&self) -> PathBuf {
457 self.project_path().join(".build")
458 }
459
460 pub fn project_build_info_path(&self) -> PathBuf {
461 self.project_dot_build_path().join("info.toml")
462 }
463
464 pub fn filelist_path(&self) -> PathBuf {
465 let filelist_name = match self.build.filelist_type {
466 FilelistType::Absolute => format!("{}.f", self.project.name),
467 FilelistType::Relative => format!("{}.f", self.project.name),
468 FilelistType::Flgen => format!("{}.list.rb", self.project.name),
469 };
470
471 self.metadata_path.with_file_name(filelist_name)
472 }
473
474 pub fn doc_path(&self) -> PathBuf {
475 self.metadata_path.parent().unwrap().join(&self.doc.path)
476 }
477}
478
479impl FromStr for Metadata {
480 type Err = MetadataError;
481
482 fn from_str(s: &str) -> Result<Self, Self::Err> {
483 let metadata: Metadata = toml::from_str(s)?;
484 Ok(metadata)
485 }
486}
487
488#[derive(Clone, Debug, Serialize, Deserialize)]
489#[serde(untagged)]
490#[serde(deny_unknown_fields)]
491pub enum Dependency {
492 Version(VersionReq),
493 Entry(DependencyEntry),
494}
495
496#[derive(Clone, Debug, Serialize, Deserialize)]
497#[serde(deny_unknown_fields)]
498pub struct DependencyEntry {
499 pub version: Option<VersionReq>,
500 pub git: Option<UrlPath>,
501 pub github: Option<String>,
502 pub project: Option<String>,
503 pub path: Option<PathBuf>,
504}