use std::{collections::BTreeMap, fs::read_to_string, path::Path};
use anda_config::{Docker, DockerImage, Manifest, Project, RpmBuild};
use async_trait::async_trait;
use cmd_lib::log;
use color_eyre::{eyre::eyre, Result};
use console::style;
use itertools::Itertools;
use lazy_static::lazy_static;
use log::{debug, info};
use nix::{sys::signal, unistd::Pid};
use regex::Regex;
use serde::{Deserialize, Serialize};
use tokio::{io::AsyncBufReadExt, process::Command};
lazy_static! {
static ref BUILDARCH_REGEX: Regex = Regex::new("BuildArch:\\s*(.+)").unwrap();
static ref EXCLUSIVEARCH_REGEX: Regex = Regex::new("ExclusiveArch:\\s*(.+)").unwrap();
static ref DEFAULT_ARCHES: Vec<String> = vec!["x86_64".to_string(), "aarch64".to_string()];
}
enum ConsoleOut {
Stdout,
Stderr,
}
#[derive(Debug, Clone, Serialize, Deserialize, Ord, Eq, PartialEq, PartialOrd)]
pub struct BuildEntry {
pub pkg: String,
pub arch: String,
}
pub fn fetch_build_entries(config: Manifest) -> Result<Vec<BuildEntry>> {
let changed_files = get_changed_files(Path::new(".")).unwrap_or_default();
let mut entries = Vec::new();
for (name, project) in config.project {
if !changed_files
.iter()
.filter_map(|file| Path::new(file).parent())
.any(|file| name.starts_with(file.to_str().unwrap()))
{
continue;
}
if let Some(rpm) = project.rpm {
if rpm.enable_scm.unwrap_or(false) {
for arch in DEFAULT_ARCHES.iter() {
entries.push(BuildEntry { pkg: name.clone(), arch: arch.clone() });
}
continue;
}
let mut arches: Vec<String> = Vec::new();
let mut build_arches: Vec<String> = Vec::new();
let spec = rpm.spec;
let spec_contents = read_to_string(spec)?;
for cap in BUILDARCH_REGEX.captures_iter(spec_contents.as_str()) {
build_arches.append(
&mut cap[1].split(' ').map(|arch| arch.to_string()).collect::<Vec<String>>(),
);
}
let mut exclusive_arches: Vec<String> = Vec::new();
for cap in EXCLUSIVEARCH_REGEX.captures_iter(spec_contents.as_str()) {
exclusive_arches.append(
&mut cap[1].split(' ').map(|arch| arch.to_string()).collect::<Vec<String>>(),
);
}
let combined_arches: Vec<String> =
build_arches.iter().chain(exclusive_arches.iter()).unique().cloned().collect();
if combined_arches.is_empty()
|| combined_arches.iter().any(|arch| arch.starts_with('%'))
{
arches = DEFAULT_ARCHES.clone();
} else if build_arches.len() == 1 && build_arches[0] == "noarch" {
let arch = DEFAULT_ARCHES
.iter()
.find(|arch| exclusive_arches.is_empty() || exclusive_arches.contains(arch))
.unwrap();
arches.push(arch.to_string());
} else {
arches = combined_arches.iter().filter(|&arch| arch != "noarch").cloned().collect();
}
for arch in arches {
entries.push(BuildEntry { pkg: name.clone(), arch });
}
}
}
Ok(entries)
}
#[async_trait]
pub trait CommandLog {
async fn log(&mut self) -> Result<()>;
}
#[async_trait]
impl CommandLog for Command {
async fn log(&mut self) -> Result<()> {
let process = self.as_std().get_program().to_owned().into_string().unwrap();
let args =
self.as_std().get_args().map(|a| a.to_str().unwrap()).collect::<Vec<&str>>().join(" ");
debug!("Running command: {process} {args}",);
let c = self.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped());
let mut output = c.spawn().unwrap();
fn print_log(process: &str, output: &str, out: ConsoleOut) {
let no_color = std::env::var("NO_COLOR").is_ok();
let process = {
if no_color {
style(process)
} else {
match out {
ConsoleOut::Stdout => style(process).cyan(),
ConsoleOut::Stderr => style(process).yellow(),
}
}
};
let formatter = format!("{process}\t| {output}");
println!("{formatter}");
}
let mut tasks = vec![];
let stdout = output.stdout.take().unwrap();
let stdout_reader = tokio::io::BufReader::new(stdout);
let mut stdout_lines = stdout_reader.lines();
let t = process.clone();
let stdout_handle = tokio::spawn(async move {
while let Some(line) = stdout_lines.next_line().await.unwrap() {
print_log(&t, &line, ConsoleOut::Stdout);
}
Ok(())
});
tasks.push(stdout_handle);
debug!("Streaming stderr");
let stderr = output.stderr.take().unwrap();
let stderr_reader = tokio::io::BufReader::new(stderr).lines();
let mut stderr_lines = stderr_reader;
debug!("stderr: {stderr_lines:?}");
let stderr_handle = tokio::spawn(async move {
while let Some(line) = stderr_lines.next_line().await.unwrap() {
print_log(&process, &line, ConsoleOut::Stderr);
}
Ok(())
});
tasks.push(stderr_handle);
let sigint_handle = tokio::spawn(async move {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
info!("Received ctrl-c, sending sigint to child process");
signal::kill(Pid::from_raw(output.id().unwrap() as i32), signal::Signal::SIGINT).unwrap();
eprintln!("Received ctrl-c, exiting");
Err(eyre!("Received ctrl-c, exiting"))
}
w = output.wait() => {
let status = w.unwrap();
if status.success() {
info!("Command exited successfully");
Ok(())
} else {
info!("Command exited with status: {status}");
Err(eyre!("Command exited with status: {status}"))
}
}
}
});
tasks.push(sigint_handle);
for task in tasks {
task.await??;
}
Ok(())
}
}
use git2::Repository;
pub fn get_commit_id_cwd() -> Option<String> {
let repo = Repository::open(".").ok()?;
let head = repo.head().ok()?;
let commit = head.peel_to_commit().ok()?;
let id = commit.id();
Some(id.to_string())
}
pub fn _get_commit_id(path: &str) -> Option<String> {
let repo = Repository::open(path).ok()?;
let head = repo.head().ok()?;
let commit = head.peel_to_commit().ok()?;
let id = commit.id();
Some(id.to_string())
}
pub fn get_changed_files(path: &Path) -> Option<Vec<String>> {
let repo = Repository::open(path).ok()?;
let head = repo.head().ok()?;
let commit = head.peel_to_commit().ok()?;
let parent = commit.parent(0).ok()?;
let diff = repo
.diff_tree_to_tree(Some(&parent.tree().ok()?), Some(&commit.tree().ok()?), None)
.ok()?;
let mut changed_files = vec![];
diff.foreach(
&mut |delta, _| {
changed_files.push(delta.new_file().path().unwrap().to_str().unwrap().to_string());
true
},
None,
None,
None,
)
.ok()?;
Some(changed_files)
}
#[test]
fn test_head() {
println!("{:?}", get_changed_files(Path::new(".")));
}
use chrono::prelude::*;
pub fn get_date() -> String {
let now: DateTime<Utc> = Utc::now();
now.format("%Y%m%d").to_string()
}
use promptly::prompt_default;
pub fn init(path: &Path, yes: bool) -> Result<()> {
if !path.exists() {
std::fs::create_dir(path)?;
}
let mut config = Manifest { project: BTreeMap::new(), config: Default::default() };
let walk = ignore::WalkBuilder::new(path).build();
for entry in walk {
let entry = entry?;
let path = entry.path().strip_prefix("./").unwrap();
if path.is_file() {
if path.extension().unwrap_or_default().eq("spec") {
{
debug!("Found spec file: {}", path.display());
let add_spec: bool = {
if yes {
true
} else {
prompt_default(
format!("Add spec file `{}` to manifest?", path.display()),
true,
)?
}
};
if add_spec {
let project_name = path.file_stem().unwrap().to_str().unwrap();
let project = Project {
rpm: Some(RpmBuild { spec: path.to_path_buf(), ..Default::default() }),
..Default::default()
};
config.project.insert(project_name.to_string(), project);
}
}
}
let mut counter = 0;
if path.extension().unwrap_or_default().eq("dockerfile")
|| path.file_name().unwrap_or_default().to_str().unwrap().eq("Dockerfile")
{
let add_oci: bool = {
if yes {
true
} else {
prompt_default(
format!("Add Dockerfile `{}` to manifest?", path.display()),
true,
)?
}
};
if add_oci {
let mut docker = Docker { ..Default::default() };
let image = DockerImage {
dockerfile: Some(path.display().to_string()),
..Default::default()
};
counter += 1;
let image_name = format!("docker-{counter}");
docker.image.insert(image_name, image);
let project = Project { docker: Some(docker), ..Default::default() };
config.project.insert("docker".to_string(), project);
}
}
}
}
println!("{}", anda_config::config::to_string(config)?);
Ok(())
}
pub(crate) fn convert_filter(filter: log::LevelFilter) -> tracing_subscriber::filter::LevelFilter {
match filter {
log::LevelFilter::Off => tracing_subscriber::filter::LevelFilter::OFF,
log::LevelFilter::Error => tracing_subscriber::filter::LevelFilter::ERROR,
log::LevelFilter::Warn => tracing_subscriber::filter::LevelFilter::WARN,
log::LevelFilter::Info => tracing_subscriber::filter::LevelFilter::INFO,
log::LevelFilter::Debug => tracing_subscriber::filter::LevelFilter::DEBUG,
log::LevelFilter::Trace => tracing_subscriber::filter::LevelFilter::TRACE,
}
}