use std::{
fs::{create_dir_all, File, OpenOptions},
io::Seek,
path::{Path, PathBuf},
process::Command,
};
use anyhow::{bail, Context};
use itertools::Itertools;
use lazy_static::lazy_static;
use crate::model::{Config, GitSource, Idl, Source};
lazy_static! {
pub static ref DEFAULT_DIR: PathBuf = std::path::Path::new(
&std::env::var("OUT_DIR")
.expect("OUT_DIR is not set, maybe you are calling volo-build outside build.rs?")
)
.join("idl");
}
pub const DEFAULT_CONFIG_FILE: &str = "volo.yml";
pub fn open_config_file<P: AsRef<Path>>(conf_file_name: P) -> std::io::Result<File> {
ensure_file(conf_file_name.as_ref())
}
pub fn ensure_cache_path() -> std::io::Result<()> {
ensure_path(&DEFAULT_DIR)
}
pub fn read_config_from_file(f: &File) -> Result<Config, serde_yaml::Error> {
if f.metadata().unwrap().len() != 0 {
serde_yaml::from_reader(f)
} else {
Ok(Config::new())
}
}
pub fn ensure_path(s: &Path) -> std::io::Result<()> {
create_dir_all(s)
}
pub fn ensure_file(filename: &Path) -> std::io::Result<File> {
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(filename)
}
const PILOTA_CREATED_FILE_NAME: &str = "pilota_crated";
pub fn download_files_from_git(task: Task) -> anyhow::Result<()> {
ensure_path(&task.dir)?;
if task.dir.join(PILOTA_CREATED_FILE_NAME).exists() {
return Ok(());
}
git_archive(&task.repo, &task.lock, &task.dir)?;
Ok(())
}
pub struct LocalIdl {
pub path: PathBuf,
pub includes: Vec<PathBuf>,
pub touch: Vec<String>,
pub keep_unknown_fields: bool,
}
pub fn get_or_download_idl(idl: Idl, target_dir: impl AsRef<Path>) -> anyhow::Result<LocalIdl> {
let (path, includes) = if let Source::Git(GitSource {
ref repo, ref lock, ..
}) = idl.source
{
let lock = lock.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"please exec 'volo idl update' or specify the lock for {}",
repo
)
})?;
let dir = target_dir
.as_ref()
.join(get_git_path(repo.as_str())?)
.join(lock);
let task = Task::new(
vec![idl.path.to_string_lossy().to_string()],
dir.clone(),
repo.clone(),
lock.to_string(),
);
download_files_from_git(task).with_context(|| format!("download repo {repo}"))?;
(
dir.join(strip_slash_prefix(idl.path.as_path())),
idl.includes
.unwrap_or_default()
.into_iter()
.map(|v| dir.join(v))
.collect_vec(),
)
} else {
(idl.path.to_path_buf(), idl.includes.unwrap_or_default())
};
Ok(LocalIdl {
path,
includes,
touch: idl.touch,
keep_unknown_fields: idl.keep_unknown_fields,
})
}
fn run_command(command: &mut Command) -> anyhow::Result<()> {
command.status().map_err(anyhow::Error::from).and_then(|s| {
if s.success() {
Ok(())
} else {
bail!("run {:?} failed, exit status: {:?}", command, s)
}
})
}
pub fn git_archive(repo: &str, revision: &str, dir: &Path) -> anyhow::Result<()> {
run_command(Command::new("git").arg("init").current_dir(dir))?;
run_command(
Command::new("git")
.arg("remote")
.arg("add")
.arg("origin")
.arg(repo)
.current_dir(dir),
)?;
run_command(
Command::new("git")
.arg("fetch")
.arg("origin")
.arg(revision)
.arg("--depth=1")
.current_dir(dir),
)?;
run_command(
Command::new("git")
.arg("reset")
.arg("--hard")
.arg(revision)
.current_dir(dir),
)?;
std::fs::write(dir.join(PILOTA_CREATED_FILE_NAME), "")?;
Ok(())
}
pub fn get_git_path(git: &str) -> anyhow::Result<PathBuf> {
let g = git.trim_end_matches(".git");
let s1 = g.split(':');
let s11 = s1.clone();
match s11.count() {
1 => Ok(PathBuf::from(g)), 2 => {
if g.starts_with("https") {
return Ok(PathBuf::from(g.trim_start_matches("https://"))); }
let s1vec: Vec<&str> = s1.collect();
let s2: Vec<&str> = s1vec[0].split('@').collect();
let mut res = String::new();
if s2.len() == 1 {
res.push_str(s2[0]);
} else {
res.push_str(s2[1]);
}
res.push('/');
res.push_str(s1vec[1]);
Ok(PathBuf::from(res))
}
_ => Err(anyhow::format_err!("git format error: {}", git)),
}
}
pub struct Task {
_files: Vec<String>,
dir: PathBuf,
repo: String,
lock: String,
}
impl Task {
pub fn new(files: Vec<String>, dir: PathBuf, repo: String, lock: String) -> Task {
Task {
_files: files,
dir,
repo,
lock,
}
}
}
pub fn get_repo_latest_commit_id(repo: &str, r#ref: &str) -> anyhow::Result<String> {
let commit_list = unsafe {
String::from_utf8_unchecked(
Command::new("git")
.arg("ls-remote")
.arg(repo)
.arg(r#ref)
.output()
.unwrap()
.stdout,
)
};
let commit_list: Vec<_> = commit_list
.split('\n')
.filter_map(|s| {
let v: Vec<_> = s.split('\t').collect();
(v.len() == 2).then_some(v)
})
.collect();
match commit_list.len() {
0 => {
bail!(
"get latest commit of {}:{} failed, please check the {} of {}",
repo,
r#ref,
r#ref,
repo
);
}
1 => {}
_ => {
let possibilities = commit_list
.iter()
.map(|x| x[1])
.collect::<Vec<_>>()
.join("\n");
bail!(
"get latest commit of {}:{} failed because of multiple refs, please choose one \
of: \n{}",
repo,
r#ref,
possibilities,
);
}
}
let commit_id = commit_list[0][0];
Ok(commit_id.into())
}
pub fn with_config<F, R>(func: F) -> anyhow::Result<R>
where
F: FnOnce(&mut Config) -> anyhow::Result<R>,
{
let mut f = open_config_file(DEFAULT_CONFIG_FILE).context("open config file")?;
let mut config = read_config_from_file(&f).context("read config file")?;
let r = func(&mut config)?;
f.rewind()?;
serde_yaml::to_writer(&mut f, &config).context("write back config file")?;
let len = f.stream_position()?;
f.set_len(len)?;
Ok(r)
}
pub fn git_repo_init(path: &Path) -> anyhow::Result<()> {
fn in_git_repo(path: &Path) -> bool {
if let Ok(repo) = git2::Repository::discover(path) {
if repo.workdir().map_or(false, |workdir| workdir == path) {
true
} else {
!repo.is_path_ignored(path).unwrap_or(false)
}
} else {
false
}
}
if !in_git_repo(path) {
git2::Repository::init(path)?;
}
Ok(())
}
pub fn strip_slash_prefix(p: &Path) -> PathBuf {
if p.starts_with("/") {
p.strip_prefix("/").unwrap().to_path_buf()
} else {
p.to_path_buf()
}
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::{tempdir, NamedTempFile};
use super::*;
#[test]
fn test_ensure_path() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("existing_dir");
fs::create_dir_all(&path).unwrap();
assert!(ensure_path(&path).is_ok());
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("new_dir");
assert!(ensure_path(&path).is_ok());
assert!(fs::metadata(&path).unwrap().is_dir());
}
#[test]
fn test_ensure_file() {
let result = tempdir().unwrap();
let binding = result.path().join("non_existing_file.txt");
let filename1 = binding.as_path();
match ensure_file(filename1) {
Ok(file) => {
assert!(file.metadata().is_ok());
}
Err(err) => {
eprintln!("Error: {}", err);
panic!("Failed to create new file");
}
}
let file1 = NamedTempFile::new().unwrap();
let filename2 = file1.path();
match ensure_file(filename2) {
Ok(file) => {
assert!(file.metadata().is_ok());
}
Err(err) => {
eprintln!("Error: {}", err);
panic!("Failed to append to existing file");
}
}
}
}