use anda_config::{Docker, DockerImage, Manifest, Project, RpmBuild};
use clap_verbosity_flag::log::LevelFilter;
use color_eyre::{eyre::eyre, Result, Section};
use nix::{sys::signal, unistd::Pid};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, io::IsTerminal, os::fd::FromRawFd, path::Path};
use tokio::process::Command;
use tracing::{debug, info};
lazy_static::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: [String; 2] = ["x86_64".to_owned(), "aarch64".to_owned()];
}
pub static STOP: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
#[derive(Copy, Clone, Debug)]
pub enum ConsoleOut {
Stdout,
Stderr,
}
#[derive(Debug, Clone, Serialize, Deserialize, Ord, Eq, PartialEq, PartialOrd)]
pub struct BuildEntry {
pub pkg: String,
pub arch: String,
pub labels: BTreeMap<String, String>,
}
pub fn fetch_build_entries(config: Manifest) -> Vec<BuildEntry> {
let changed_files = get_changed_files(Path::new(".")).unwrap_or_default();
let changed_dirs: std::collections::HashSet<_> = changed_files
.iter()
.map(|f| f.trim_end_matches(|x| x != '/').trim_end_matches('/'))
.collect();
let suffix = config.config.strip_suffix.clone().unwrap_or_default();
let mut entries = Vec::new();
for (mut name, project) in config.project {
let dir = name.trim_end_matches(&suffix);
if !changed_dirs.contains(dir) {
continue;
}
if let Some(rpm) = project.rpm {
if rpm.enable_scm.unwrap_or(false) {
entries.extend(DEFAULT_ARCHES.iter().map(|arch| BuildEntry {
pkg: std::mem::take(&mut name),
arch: arch.clone(),
labels: project.labels.clone(),
}));
continue;
}
}
entries.extend(
project
.arches
.unwrap_or_else(|| DEFAULT_ARCHES.to_vec())
.into_iter()
.map(|arch| BuildEntry { pkg: name.clone(), arch, labels: project.labels.clone() }),
);
}
entries
}
#[async_trait::async_trait]
pub trait CommandLog {
async fn log(&mut self) -> Result<()>;
}
#[async_trait::async_trait]
impl CommandLog for Command {
#[allow(clippy::arithmetic_side_effects)]
#[tracing::instrument]
async fn log(&mut self) -> Result<()> {
debug!("running command");
if !std::io::stdout().is_terminal() {
tracing::warn!("stdout is not a terminal, output may get a bit more verbose!");
}
let process = self.as_std().get_program();
let mut winsize = nix::libc::winsize { ws_row: 0, ws_col: 0, ws_xpixel: 0, ws_ypixel: 0 };
assert_eq!(
unsafe {
nix::libc::ioctl(nix::libc::STDOUT_FILENO, nix::libc::TIOCGWINSZ, &raw mut winsize)
},
0,
"ioctl failed"
);
winsize.ws_col -= u16::try_from(process.len()).expect("process name too long");
winsize.ws_col -= 3;
let pt_stdout = crate::pt::PseudoTerminal::new();
pt_stdout.set_winsize(winsize);
let stdout = unsafe { std::process::Stdio::from_raw_fd(pt_stdout.slave()) };
let pt_stderr = crate::pt::PseudoTerminal::new();
pt_stderr.set_winsize(winsize);
let stderr = unsafe { std::process::Stdio::from_raw_fd(pt_stderr.slave()) };
let mut c = Self::new(process);
c.args(self.as_std().get_args())
.stdin(std::process::Stdio::null())
.stdout(stdout)
.stderr(stderr);
trace!(?c, "Running command");
let mut output = c.spawn().map_err(|e| {
eyre!("Cannot run {process:?}")
.wrap_err(e)
.suggestion("You might need to install the program via a package manager.")
})?;
let process = process.to_os_string();
let process2 = process.clone();
for task in [
tokio::task::spawn_blocking(move || {
pt_stdout.print_log(&process, ConsoleOut::Stdout);
Ok(())
}),
tokio::task::spawn_blocking(move || {
pt_stderr.print_log(&process2, ConsoleOut::Stderr);
Ok(())
}),
tokio::spawn(async move {
let res = tokio::select! {
_ = tokio::signal::ctrl_c() => {
info!("Received ctrl-c, sending sigint to child process");
#[allow(clippy::cast_possible_wrap)]
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}"))
}
}
};
STOP.store(true, std::sync::atomic::Ordering::Relaxed);
res
}),
] {
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_owned());
true
},
None,
None,
None,
)
.ok()?;
trace!("changed files: {changed_files:?}");
Some(changed_files)
}
pub fn get_date() -> String {
let now: chrono::DateTime<chrono::Utc> = chrono::Utc::now();
now.format("%Y%m%d").to_string()
}
use promptly::prompt_default;
use tracing::trace;
pub fn init(path: &Path, yes: bool) -> Result<()> {
if !path.exists() {
std::fs::create_dir_all(path)?;
}
let mut config = Manifest { project: BTreeMap::new(), config: anda_config::Config::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() {
continue;
}
match path.extension().unwrap_or_default().as_encoded_bytes() {
b"spec" => {
debug!("Found spec file: {}", path.display());
if yes
|| prompt_default(
format!("Add spec file `{}` to manifest?", path.display()),
true,
)?
{
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_owned(), project);
}
}
b"dockerfile" => add_dockerfile_to_manifest(yes, path, &mut config)?,
_ if path.file_name().is_some_and(|f| f.eq("Dockerfile")) => {
add_dockerfile_to_manifest(yes, path, &mut config)?;
}
_ => {}
}
}
println!("{}", anda_config::config::to_string(&config)?);
Ok(())
}
fn add_dockerfile_to_manifest(
yes: bool,
path: &Path,
config: &mut Manifest,
) -> Result<(), color_eyre::eyre::Error> {
let add_oci =
yes || prompt_default(format!("Add Dockerfile `{}` to manifest?", path.display()), true)?;
if add_oci {
let mut docker = Docker::default();
let image =
DockerImage { dockerfile: Some(path.display().to_string()), ..Default::default() };
let image_name = "docker-1".to_owned();
docker.image.insert(image_name, image);
let project = Project { docker: Some(docker), ..Default::default() };
config.project.insert("docker".to_owned(), project);
}
Ok(())
}
pub const fn convert_filter(filter: LevelFilter) -> tracing_subscriber::filter::LevelFilter {
match filter {
LevelFilter::Off => tracing_subscriber::filter::LevelFilter::OFF,
LevelFilter::Error => tracing_subscriber::filter::LevelFilter::ERROR,
LevelFilter::Warn => tracing_subscriber::filter::LevelFilter::WARN,
LevelFilter::Info => tracing_subscriber::filter::LevelFilter::INFO,
LevelFilter::Debug => tracing_subscriber::filter::LevelFilter::DEBUG,
LevelFilter::Trace => tracing_subscriber::filter::LevelFilter::TRACE,
}
}
#[macro_export]
macro_rules! cmd {
(@ $cmd:ident [[$expr:expr]]) => { $cmd.args($expr); };
(@ $cmd:ident $tt:tt) => { $cmd.arg(cmd!(# $tt)); };
(# [$expr:literal $($arg:expr),*]) => { format!($expr, $($arg),*) };
(# {{$expr:expr}}) => { format!("{}", $expr) };
(# $expr:expr) => { &$expr };
(# $expr:literal) => { $expr };
(stdout $cmd:literal $($t:tt)+) => {{
#[allow(unused_braces)]
let cmd = cmd!($cmd $($t)+).output()?;
String::from_utf8_lossy(&cmd.stdout)
}};
($cmd:literal $($t:tt)*) => {{
#[allow(unused_braces)]
let mut cmd = std::process::Command::new($cmd);
$(
cmd!(@ cmd $t);
)*
cmd
}};
($cmd:block $($t:tt)*) => {{
#[allow(unused_braces)]
let mut cmd = std::process::Command::new(cmd!(# $cmd));
$(
cmd!(@ cmd $t);
)*
cmd
}};
(?$cmd:tt $($t:tt)*) => {{
#[allow(unused_braces)]
$crate::util::cmd(cmd!($cmd $($t)*), &[Box::new($cmd), $(Box::new(cmd!(# $t))),*])
}};
}
#[inline]
pub fn cmd<const N: usize>(
mut cmd: std::process::Command,
cmd_arr: &[Box<dyn std::fmt::Display>; N],
) -> color_eyre::Result<()> {
use color_eyre::Help;
use itertools::Itertools;
let cmd_str = cmd_arr.iter().join(" ");
tracing::trace!("Running command: `{cmd_str}`");
let status = cmd.status()?;
Err(match (status, status.code()) {
_ if status.success() => return Ok(()),
(_, Some(rc)) => color_eyre::Report::msg("Command exited")
.warning(lazy_format::lazy_format!("Status code: {rc}"))
.with_note(|| format!("Command: `{cmd_str}`"))
.note(lazy_format::lazy_format!("Status: {status}")),
_ => color_eyre::Report::msg("Script terminated unexpectedly")
.note(lazy_format::lazy_format!("Status: {status}")),
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_head() {
println!("{:?}", get_changed_files(Path::new(".")));
}
#[test]
fn test_entries() {
let config = anda_config::load_from_file(&PathBuf::from("anda.hcl"));
fetch_build_entries(config.unwrap());
}
}