Skip to main content

veryl_metadata/
metadata.rs

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