use crate::git::Git;
use crate::lockfile_compat;
use crate::metadata::{Dependency, Metadata, UrlPath};
use crate::metadata_error::MetadataError;
use crate::pubfile::{Pubfile, Release};
use log::info;
use pathdiff::diff_paths;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use url::Url;
use uuid::Uuid;
use veryl_path::{PathSet, ignore_already_exists};
use walkdir::WalkDir;
const LOCKFILE_VERSION: usize = 1;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Lockfile {
version: usize,
projects: Vec<Lock>,
#[serde(skip)]
pub lock_table: HashMap<UrlPath, Vec<Lock>>,
#[serde(skip)]
force_update: bool,
#[serde(skip)]
pub metadata_path: PathBuf,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Lock {
pub name: String,
pub source: LockSource,
pub dependencies: Vec<LockDependency>,
#[serde(skip)]
pub visible: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
pub enum LockSource {
Repository(Box<LockSourceRepository>),
Path(PathBuf),
}
impl LockSource {
pub fn to_url(&self) -> UrlPath {
match self {
LockSource::Repository(x) => x.url.clone(),
LockSource::Path(x) => UrlPath::Path(x.clone()),
}
}
pub fn get_version(&self) -> Option<&Version> {
match self {
LockSource::Repository(x) => Some(&x.version),
LockSource::Path(_) => None,
}
}
pub fn get_revision(&self) -> Option<&str> {
match self {
LockSource::Repository(x) => Some(&x.revision),
LockSource::Path(_) => None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(deny_unknown_fields)]
pub struct LockSourceRepository {
uuid: Uuid,
url: UrlPath,
path: PathBuf,
project: String,
version: Version,
revision: String,
r#override: Option<PathBuf>,
}
impl PartialOrd for LockSource {
fn partial_cmp(&self, other: &LockSource) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for LockSource {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(LockSource::Repository(x), LockSource::Repository(y)) => x
.url
.cmp(&y.url)
.then(x.project.cmp(&y.project))
.then(x.version.cmp(&y.version)),
(LockSource::Path(x), LockSource::Path(y)) => x.cmp(y),
(LockSource::Repository(_), LockSource::Path(_)) => std::cmp::Ordering::Less,
(LockSource::Path(_), LockSource::Repository(_)) => std::cmp::Ordering::Greater,
}
}
}
impl fmt::Display for LockSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut ret = String::new();
match self {
LockSource::Repository(x) => {
ret.push_str(&format!("{} : {} @ {}", x.project, x.url, x.version));
}
LockSource::Path(x) => {
ret.push_str(&format!("{}", x.to_string_lossy()));
}
}
ret.fmt(f)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LockDependency {
pub name: String,
pub source: LockSource,
}
impl Lockfile {
pub fn load(metadata: &Metadata) -> Result<Self, MetadataError> {
let path = metadata
.lockfile_path
.canonicalize()
.map_err(|x| MetadataError::file_io(x, &metadata.lockfile_path))?;
let text = fs::read_to_string(&path).map_err(|x| MetadataError::file_io(x, &path))?;
let mut ret = LockfileCompat::load(&text, &path, metadata)?;
ret.metadata_path = metadata.metadata_path.clone();
let mut locks = Vec::new();
locks.append(&mut ret.projects);
ret.lock_table.clear();
for lock in locks {
ret.lock_table
.entry(lock.source.to_url())
.and_modify(|x| x.push(lock.clone()))
.or_insert(vec![lock]);
}
ret.sort_table();
Ok(ret)
}
pub fn save<T: AsRef<Path>>(&mut self, path: T) -> Result<(), MetadataError> {
self.projects.clear();
for locks in self.lock_table.values() {
for lock in locks {
self.projects.push(lock.clone());
}
}
self.projects.sort_by(|x, y| x.source.cmp(&y.source));
let mut text = String::new();
text.push_str("# This file is automatically @generated by Veryl.\n");
text.push_str("# It is not intended for manual editing.\n");
text.push_str(&toml::to_string_pretty(&self)?);
fs::write(&path, text.as_bytes()).map_err(|x| MetadataError::file_io(x, path.as_ref()))?;
Ok(())
}
pub fn new(metadata: &Metadata) -> Result<Self, MetadataError> {
let mut ret = Lockfile {
version: LOCKFILE_VERSION,
metadata_path: metadata.metadata_path.clone(),
..Default::default()
};
let mut name_table = HashSet::new();
let mut src_table = HashMap::new();
let locks = ret.gen_locks(metadata, &mut name_table, &mut src_table, true, metadata)?;
for lock in locks {
info!("Adding dependency ({})", lock.source);
ret.lock_table
.entry(lock.source.to_url())
.and_modify(|x| x.push(lock.clone()))
.or_insert(vec![lock]);
}
ret.sort_table();
Ok(ret)
}
pub fn update(
&mut self,
metadata: &Metadata,
force_update: bool,
) -> Result<bool, MetadataError> {
self.force_update = force_update;
let mut name_table = HashSet::new();
let mut src_table = HashMap::new();
let locks = self.gen_locks(metadata, &mut name_table, &mut src_table, true, metadata)?;
let old_table = self.lock_table.clone();
self.lock_table.clear();
let mut modified = false;
for lock in &locks {
let add = if let Some(old_locks) = old_table.get(&lock.source.to_url()) {
!old_locks.iter().any(|x| x.source == lock.source)
} else {
true
};
if add {
info!("Adding dependency ({})", lock.source);
modified = true;
}
self.lock_table
.entry(lock.source.to_url())
.and_modify(|x| x.push(lock.clone()))
.or_insert(vec![lock.clone()]);
}
self.sort_table();
for old_locks in old_table.values() {
for old_lock in old_locks {
if !locks.iter().any(|x| x.source == old_lock.source) {
info!("Removing dependency ({})", old_lock.source);
modified = true;
}
}
}
Ok(modified)
}
pub fn paths(&self, base_dst: &Path) -> Result<Vec<PathSet>, MetadataError> {
let mut ret = Vec::new();
for locks in self.lock_table.values() {
for lock in locks {
let metadata = self.get_metadata(&lock.source)?;
let path = metadata.project_path();
for src in &veryl_path::gather_files_with_extension(&path, "veryl", false)? {
let Ok(rel) = src.strip_prefix(&path) else {
return Err(MetadataError::InvalidSourceLocation(src.clone()));
};
let mut dst = base_dst.join(&lock.name);
dst.push(rel);
dst.set_extension("sv");
let mut map = dst.clone();
map.set_extension("sv.map");
ret.push(PathSet {
prj: lock.name.clone(),
src: src.to_path_buf(),
dst,
map,
});
}
}
}
Ok(ret)
}
pub fn clear_cache(&self) -> Result<(), MetadataError> {
let lock_resolve = veryl_path::lock_dir("resolve")?;
let lock_dependencies = veryl_path::lock_dir("dependencies")?;
for locks in self.lock_table.values() {
for lock in locks {
if let LockSource::Repository(x) = &lock.source {
let resolve_path = Self::resolve_path(&x.url)?;
let dependency_path = Self::dependency_path(&x.url, &x.path, &x.revision)?;
if resolve_path.exists() {
fs::remove_dir_all(&resolve_path)
.map_err(|x| MetadataError::file_io(x, &resolve_path))?;
}
if dependency_path.exists() {
fs::remove_dir_all(&dependency_path)
.map_err(|x| MetadataError::file_io(x, &dependency_path))?;
}
}
}
}
veryl_path::unlock_dir(lock_resolve)?;
veryl_path::unlock_dir(lock_dependencies)?;
Ok(())
}
fn git_clone(&self, url: &UrlPath, path: &Path) -> Result<Git, MetadataError> {
let url = match url {
UrlPath::Url(x) => UrlPath::Url(x.clone()),
UrlPath::Path(x) => {
if x.is_relative() {
let path = self.metadata_path.parent().unwrap().join(x);
UrlPath::Path(path)
} else {
UrlPath::Path(x.clone())
}
}
};
Git::clone(&url, path)
}
fn sort_table(&mut self) {
for locks in self.lock_table.values_mut() {
locks.sort_by(|a, b| b.source.cmp(&a.source));
}
}
fn gen_uuid(url: &UrlPath, path: &Path, revision: &str) -> Result<Uuid, MetadataError> {
let mut url = url.to_string();
url.push_str(&path.to_string_lossy());
url.push_str(revision);
Ok(Uuid::new_v5(&Uuid::NAMESPACE_URL, url.as_bytes()))
}
fn gen_locks(
&mut self,
metadata: &Metadata,
name_table: &mut HashSet<String>,
src_table: &mut HashMap<LockSource, String>,
root: bool,
root_metadata: &Metadata,
) -> Result<Vec<Lock>, MetadataError> {
let mut ret = Vec::new();
let mut dependencies_metadata = Vec::new();
for (name, dep) in &metadata.dependencies {
let dependency = self.resolve_dependency(metadata, name, dep, root, root_metadata)?;
let metadata = self.get_metadata(&dependency.source)?;
let mut name = dependency.name.clone();
if name_table.contains(&name) {
if root {
return Err(MetadataError::NameConflict(name));
}
let mut suffix = 0;
loop {
let new_name = format!("{name}_{suffix}");
if !name_table.contains(&new_name) {
name = new_name;
break;
}
suffix += 1;
}
}
name_table.insert(name.clone());
let mut dependencies = Vec::new();
for (name, dep) in &metadata.dependencies {
let dependency =
self.resolve_dependency(&metadata, name, dep, root, root_metadata)?;
dependencies.push(dependency);
}
if let Some(x) = src_table.get(&dependency.source) {
if root {
return Err(MetadataError::InvalidDependency {
name: dependency.name.clone(),
cause: format!("it conflicts with {x}"),
});
}
} else {
let lock = Lock {
name: name.clone(),
source: dependency.source.clone(),
dependencies,
visible: root,
};
ret.push(lock);
src_table.insert(dependency.source.clone(), name.clone());
dependencies_metadata.push(metadata);
}
}
for metadata in dependencies_metadata {
let mut dependency_locks =
self.gen_locks(&metadata, name_table, src_table, false, root_metadata)?;
ret.append(&mut dependency_locks);
}
Ok(ret)
}
fn resolve_dependency(
&mut self,
metadata: &Metadata,
name: &str,
dep: &Dependency,
root: bool,
root_metadata: &Metadata,
) -> Result<LockDependency, MetadataError> {
Ok(match dep {
Dependency::Version(_) => {
unimplemented!();
}
Dependency::Entry(x) => {
let url = if let Some(git) = &x.git {
Some(git.clone())
} else if let Some(github) = &x.github {
let url = format!("https://github.com/{github}");
let url = Url::parse(&url).unwrap();
Some(UrlPath::Url(url))
} else {
None
};
let project = x.project.clone().unwrap_or(name.to_string());
let source = if let Some(url) = &url {
let Some(version) = &x.version else {
return Err(MetadataError::InvalidDependency {
name: name.to_string(),
cause: "version is not specified".to_string(),
});
};
let (release, path) = self.resolve_version(url, &project, version)?;
let uuid = Self::gen_uuid(url, &path, &release.revision)?;
let r#override = if root { x.path.clone() } else { None };
LockSource::Repository(Box::new(LockSourceRepository {
uuid,
url: url.clone(),
path,
project,
version: release.version,
revision: release.revision,
r#override,
}))
} else if let Some(path) = &x.path {
let path = if path.is_absolute() {
path.clone()
} else {
let base = root_metadata.project_path();
let path = base.join(metadata.project_path()).join(path);
if !path.exists() {
let project = x.project.clone().unwrap_or(name.to_string());
return Err(MetadataError::ProjectNotFound {
url: UrlPath::Path(path),
project,
});
}
diff_paths(path.canonicalize().unwrap(), base).unwrap()
};
LockSource::Path(path)
} else {
return Err(MetadataError::InvalidDependency {
name: name.to_string(),
cause: "[git|github|path] are not specified".to_string(),
});
};
LockDependency {
name: name.to_string(),
source,
}
}
})
}
fn resolve_version(
&mut self,
url: &UrlPath,
project: &str,
version_req: &VersionReq,
) -> Result<(Release, PathBuf), MetadataError> {
if let Some(release) = self.resolve_version_from_lockfile(url, project, version_req)? {
if self.force_update {
let latest = self.resolve_version_from_latest(url, project, version_req)?;
Ok(latest)
} else {
Ok(release)
}
} else {
let latest = self.resolve_version_from_latest(url, project, version_req)?;
Ok(latest)
}
}
fn resolve_version_from_lockfile(
&mut self,
url: &UrlPath,
project: &str,
version_req: &VersionReq,
) -> Result<Option<(Release, PathBuf)>, MetadataError> {
if let Some(locks) = self.lock_table.get_mut(url) {
for lock in locks {
if let LockSource::Repository(x) = &lock.source
&& x.project == project
&& version_req.matches(&x.version)
{
let release = Release {
version: x.version.clone(),
revision: x.revision.clone(),
};
let path = x.path.clone();
return Ok(Some((release, path)));
}
}
}
Ok(None)
}
fn resolve_path(url: &UrlPath) -> Result<PathBuf, MetadataError> {
let resolve_dir = veryl_path::cache_path().join("resolve");
let uuid = Self::gen_uuid(url, &PathBuf::new(), "")?;
Ok(resolve_dir.join(uuid.simple().encode_lower(&mut Uuid::encode_buffer())))
}
fn search_project(path: &Path, project: &str) -> Option<PathBuf> {
for entry in WalkDir::new(path).into_iter().flatten() {
if entry.file_name() == "Veryl.toml"
&& let Ok(metadata) = Metadata::load(entry.path())
&& metadata.project.name == project
{
let ret = entry.path();
let ret = ret.parent().unwrap().strip_prefix(path).unwrap();
return Some(ret.to_path_buf());
}
}
None
}
fn resolve_version_from_latest(
&mut self,
url: &UrlPath,
project: &str,
version_req: &VersionReq,
) -> Result<(Release, PathBuf), MetadataError> {
let resolve_dir = veryl_path::cache_path().join("resolve");
if !resolve_dir.exists() {
ignore_already_exists(fs::create_dir_all(&resolve_dir))
.map_err(|x| MetadataError::file_io(x, &resolve_dir))?;
}
let path = Self::resolve_path(url)?;
let lock = veryl_path::lock_dir("resolve")?;
let git = self.git_clone(url, &path)?;
git.fetch()?;
git.checkout(None)?;
veryl_path::unlock_dir(lock)?;
let Some(prj_path) = Self::search_project(&path, project) else {
return Err(MetadataError::ProjectNotFound {
url: url.clone(),
project: project.to_string(),
});
};
let toml = path.join(&prj_path).join("Veryl.pub");
let mut pubfile = Pubfile::load(toml)?;
pubfile.releases.sort_by(|a, b| b.version.cmp(&a.version));
for release in &pubfile.releases {
if version_req.matches(&release.version) {
return Ok((release.clone(), prj_path));
}
}
Err(MetadataError::VersionNotFound {
url: url.clone(),
version: version_req.to_string(),
})
}
fn dependency_path(
url: &UrlPath,
path: &Path,
revision: &str,
) -> Result<PathBuf, MetadataError> {
let dependencies_dir = veryl_path::cache_path().join("dependencies");
let uuid = Self::gen_uuid(url, path, revision)?;
Ok(dependencies_dir.join(uuid.simple().encode_lower(&mut Uuid::encode_buffer())))
}
fn get_metadata(&self, source: &LockSource) -> Result<Metadata, MetadataError> {
let path = match source {
LockSource::Path(x) => Some(x.clone()),
LockSource::Repository(x) => x.r#override.clone(),
};
let path_metadata = if let Some(x) = path {
let path = self.metadata_path.parent().unwrap().join(x);
let path = path.join("Veryl.toml");
if path.exists() {
Some(Metadata::load(path)?)
} else {
None
}
} else {
None
};
match source {
LockSource::Path(_) => {
if let Some(x) = path_metadata {
Ok(x)
} else {
Err(MetadataError::FileNotFound)
}
}
LockSource::Repository(x) => {
if let Some(x) = path_metadata {
Ok(x)
} else {
let dependencies_dir = veryl_path::cache_path().join("dependencies");
if !dependencies_dir.exists() {
ignore_already_exists(fs::create_dir_all(&dependencies_dir))
.map_err(|x| MetadataError::file_io(x, &dependencies_dir))?;
}
let path = Self::dependency_path(&x.url, &x.path, &x.revision)?;
let toml = path.join("Veryl.toml");
if !path.exists() {
let lock = veryl_path::lock_dir("dependencies")?;
let git = self.git_clone(&x.url, &path)?;
git.fetch()?;
git.checkout(Some(&x.revision))?;
veryl_path::unlock_dir(lock)?;
} else {
let git = Git::open(&path)?;
let ret = git.is_clean().is_ok_and(|x| x);
if !ret || !toml.exists() {
let lock = veryl_path::lock_dir("dependencies")?;
veryl_path::ignore_directory_not_empty(fs::remove_dir_all(&path))
.map_err(|x| MetadataError::file_io(x, &path))?;
let git = self.git_clone(&x.url, &path)?;
git.fetch()?;
git.checkout(Some(&x.revision))?;
veryl_path::unlock_dir(lock)?;
}
}
Metadata::load(toml)
}
}
}
}
}
impl FromStr for Lockfile {
type Err = MetadataError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let lockfile: Lockfile = toml::from_str(s)?;
Ok(lockfile)
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct LockfileCompat {
version: Option<usize>,
}
impl LockfileCompat {
pub fn load(
text: &str,
lockfile_path: &Path,
metadata: &Metadata,
) -> Result<Lockfile, MetadataError> {
let compat: LockfileCompat = toml::from_str(text)?;
let version = compat.version.unwrap_or(0);
let mut lockfile = match version {
0 => {
info!(
"Migrating lockfile to v1 ({})",
lockfile_path.to_string_lossy()
);
let lockfile: lockfile_compat::v0::Lockfile = toml::from_str(text)?;
let mut lockfile = Lockfile::from_v0(lockfile, &metadata.metadata_path)?;
lockfile.save(lockfile_path)?;
lockfile
}
1 => toml::from_str(text)?,
_ => unreachable!(),
};
for lock in lockfile.projects.iter_mut() {
lock.visible = metadata.dependencies.contains_key(&lock.name);
}
Ok(lockfile)
}
}
impl Lockfile {
fn set_project(
mut source: LockSource,
metadata_path: &Path,
) -> Result<LockSource, MetadataError> {
let lockfile = Lockfile {
metadata_path: metadata_path.to_path_buf(),
..Default::default()
};
let metadata = lockfile.get_metadata(&source)?;
if let LockSource::Repository(x) = &mut source {
x.project = metadata.project.name;
}
Ok(source)
}
pub fn from_v0(
x: lockfile_compat::v0::Lockfile,
metadata_path: &Path,
) -> Result<Self, MetadataError> {
let mut projects = Vec::new();
for lock in x.projects {
let mut dependencies = Vec::new();
for dep in lock.dependencies {
let uuid = Lockfile::gen_uuid(&dep.url, &PathBuf::new(), &dep.revision)?;
let source = LockSource::Repository(Box::new(LockSourceRepository {
uuid,
url: dep.url,
path: PathBuf::new(),
project: String::new(),
version: dep.version,
revision: dep.revision,
r#override: None,
}));
let source = Self::set_project(source, metadata_path)?;
let new_dep = LockDependency {
name: dep.name,
source,
};
dependencies.push(new_dep);
}
let uuid = Lockfile::gen_uuid(&lock.url, &PathBuf::new(), &lock.revision).unwrap();
let source = LockSource::Repository(Box::new(LockSourceRepository {
uuid,
url: lock.url,
path: PathBuf::new(),
project: String::new(),
version: lock.version,
revision: lock.revision,
r#override: lock.path,
}));
let source = Self::set_project(source, metadata_path)?;
let new_lock = Lock {
name: lock.name,
source,
dependencies,
visible: false,
};
projects.push(new_lock);
}
Ok(Lockfile {
version: LOCKFILE_VERSION,
projects,
..Default::default()
})
}
}