use super::{proces::{get_all_info, Proc},
Ansi};
use crate::ResumeKind;
use log::*;
use regex::Regex;
use serde::Deserialize;
use serde_json::from_reader;
use std::{fs::File,
io::{BufRead, BufReader, Read},
path::PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Pkg {
key: String,
pos: usize,
}
impl Pkg {
pub fn new(ebuild: &str, version: &str) -> Self {
Self { key: format!("{ebuild}-{version}"), pos: ebuild.len() + 1 }
}
fn try_new(key: &str) -> Option<Self> {
let mut pos = 0;
loop {
pos += key[pos..].find('-')?;
if pos > 0 && key.as_bytes().get(pos + 1)?.is_ascii_digit() {
return Some(Self { key: key.to_string(), pos: pos + 1 });
}
pos += 1;
}
}
pub fn ebuild(&self) -> &str {
&self.key[..(self.pos - 1)]
}
#[cfg(test)]
pub fn version(&self) -> &str {
&self.key[self.pos..]
}
pub fn ebuild_version(&self) -> &str {
&self.key
}
}
pub fn get_pretend<R: Read>(reader: R, filename: &str) -> Vec<Pkg> {
debug!("get_pretend input={}", filename);
let mut out = vec![];
let re = Regex::new("^\\[ebuild[^]]*\\] +([^ :\\n]+)").unwrap();
let mut buf = BufReader::new(reader);
let mut line = String::new();
loop {
match buf.read_line(&mut line) {
Ok(0) | Err(_) => break,
Ok(_) => {
if let Some(c) = re.captures(&line) {
out.push(Pkg::try_new(c.get(1).unwrap().as_str()).unwrap())
}
},
}
line.clear();
}
out
}
#[derive(Deserialize)]
struct Resume {
mergelist: Vec<Vec<String>>,
}
#[derive(Deserialize)]
struct Mtimedb {
resume: Option<Resume>,
resume_backup: Option<Resume>,
}
pub fn get_resume(kind: ResumeKind) -> Vec<Pkg> {
let r = get_resume_priv(kind, "/var/cache/edb/mtimedb").unwrap_or_default();
debug!("Loaded {kind:?} resume list: {r:?}");
r
}
fn get_resume_priv(kind: ResumeKind, file: &str) -> Option<Vec<Pkg>> {
if matches!(kind, ResumeKind::No) {
return Some(vec![]);
}
let reader = File::open(file).map_err(|e| warn!("Cannot open {file:?}: {e}")).ok()?;
let db: Mtimedb = from_reader(reader).map_err(|e| warn!("Cannot parse {file:?}: {e}")).ok()?;
let r = match kind {
ResumeKind::Either => db.resume.or(db.resume_backup)?,
ResumeKind::Main | ResumeKind::Auto => db.resume?,
ResumeKind::Backup => db.resume_backup?,
ResumeKind::No => unreachable!(),
};
Some(r.mergelist.iter().filter_map(|v| v.get(2).and_then(|s| Pkg::try_new(s))).collect())
}
pub fn get_buildlog(pkg: &Pkg, portdirs: &Vec<PathBuf>) -> Option<String> {
for portdir in portdirs {
let name = portdir.join("portage").join(pkg.ebuild_version()).join("temp/build.log");
if let Ok(file) = File::open(&name).map_err(|e| warn!("Cannot open {name:?}: {e}")) {
info!("Build log: {}", name.display());
return Some(read_buildlog(file, 50));
}
}
None
}
fn read_buildlog(file: File, max: usize) -> String {
let mut last = String::new();
for line in rev_lines::RevLines::new(BufReader::new(file)).map_while(Result::ok) {
if line.starts_with(">>>") {
let tag = line.split_ascii_whitespace().skip(1).take(2).collect::<Vec<_>>().join(" ");
return if last.is_empty() {
format!(" ({})", tag.trim_matches('.'))
} else {
format!(" ({}: {})", tag.trim_matches('.'), last)
};
}
if last.is_empty() {
let stripped = Ansi::strip(&line, max);
if stripped.chars().any(char::is_alphanumeric) {
last = stripped;
}
}
}
format!(" ({last})")
}
#[derive(Debug)]
pub struct EmergeInfo {
pub start: i64,
pub cmds: Vec<Proc>,
pub pkgs: Vec<Pkg>,
}
pub fn get_emerge(tmpdirs: &mut Vec<PathBuf>) -> EmergeInfo {
let mut res = EmergeInfo { start: i64::MAX, cmds: vec![], pkgs: vec![] };
let re_python = Regex::new("^[a-z/-]+python[0-9.]* [a-z/-]+python[0-9.]*/").unwrap();
for mut proc in get_all_info(&["emerge", "python"], tmpdirs) {
res.start = std::cmp::min(res.start, proc.start);
if proc.idx == 0 {
proc.cmdline = re_python.replace(&proc.cmdline, "").to_string();
res.cmds.push(proc);
} else if let Some(a) = proc.cmdline.find("sandbox [") {
if let Some(b) = proc.cmdline.find("] sandbox") {
if let Some(p) = Pkg::try_new(&proc.cmdline[(a + 9)..b]) {
res.pkgs.push(p);
}
}
}
}
trace!("{:?}", res);
res
}
#[cfg(test)]
mod tests {
use super::*;
fn check_pretend(file: &str, expect: &[(&str, &str)]) {
let mut n = 0;
for p in get_pretend(File::open(&format!("tests/{file}")).unwrap(), file) {
assert_eq!((p.ebuild(), p.version()), expect[n], "Mismatch for {file}:{n}");
n += 1;
}
}
#[test]
fn pretend_basic() {
let out = [("sys-devel/gcc", "6.4.0-r1"),
("sys-libs/readline", "7.0_p3"),
("app-portage/emlop", "0.1.0_p20180221"),
("app-shells/bash", "4.4_p12"),
("dev-db/postgresql", "10.3")];
check_pretend("emerge-p.basic.out", &out);
check_pretend("emerge-pv.basic.out", &out);
}
#[test]
fn pretend_blocker() {
let out = [("app-admin/syslog-ng", "3.13.2"), ("dev-lang/php", "7.1.13")];
check_pretend("emerge-p.blocker.out", &out);
}
fn check_resume(kind: ResumeKind, file: &str, expect: Option<&[(&str, &str)]>) {
let file = &format!("tests/{file}");
match expect {
Some(ex) => {
let mut n = 0;
for p in get_resume_priv(kind, file).unwrap() {
assert_eq!((p.ebuild(), p.version()), ex[n], "Mismatch for {file}:{n}");
n += 1;
}
},
None => assert_eq!(None, get_resume_priv(kind, file)),
}
}
#[test]
fn resume() {
let main = &[("dev-lang/rust", "1.65.0"), ("app-portage/emlop", "0.5.0")];
let bkp = &[("app-portage/dummybuild", "0.1.600"), ("app-portage/dummybuild", "0.1.60")];
check_resume(ResumeKind::Main, "mtimedb.ok", Some(main));
check_resume(ResumeKind::Backup, "mtimedb.ok", Some(bkp));
check_resume(ResumeKind::No, "mtimedb.ok", Some(&[]));
check_resume(ResumeKind::Either, "mtimedb.ok", Some(main));
check_resume(ResumeKind::Either, "mtimedb.backuponly", Some(bkp));
check_resume(ResumeKind::Either, "mtimedb.empty", Some(&[]));
check_resume(ResumeKind::Either, "mtimedb.noresume", None);
check_resume(ResumeKind::Either, "mtimedb.badjson", None);
}
#[test]
fn pkg_new() {
assert_eq!(Some(Pkg::new("foo", "1.2")), Pkg::try_new("foo-1.2"));
assert_eq!("foo", Pkg::new("foo", "1.2").ebuild());
assert_eq!("1.2", Pkg::new("foo", "1.2").version());
assert_eq!("foo-1.2", Pkg::new("foo", "1.2").ebuild_version());
}
#[test]
fn buildlog() {
for (file, lim, res) in
[("build.log.empty", 20, ""),
("build.log.notag", 50, "* Upstream: phil@riverbankcomputing.com pyqt@riv..."),
("build.log.onlytag", 30, "Unpacking source"),
("build.log.trim", 20, "Unpacking source: 102 | HTTP2W..."),
("build.log.short", 20, "Configuring source: done"),
("build.log.color", 100, "Unpacking source: 0:57.55 Compiling syn v1.0.99"),
("build.log.color", 15, "Unpacking source: 0:57.55 Comp...")]
{
let f = File::open(&format!("tests/{file}")).expect(&format!("can't open {file:?}"));
assert_eq!(format!(" ({res})"), read_buildlog(f, lim));
}
}
}