use std::cell::RefCell;
use std::collections::{HashMap, HashSet, VecDeque};
use std::io::{self, Read, Write};
use std::mem;
use camino::Utf8Path;
use indexmap::IndexSet;
use itertools::Itertools;
use nix::unistd::isatty;
use scallop::builtins::{ExecStatus, ScopedOptions};
use scallop::variables::{self, *};
use scallop::{functions, source, Error};
use strum::{AsRefStr, Display};
use sys_info::os_release;
use crate::atom::Atom;
use crate::eapi::{Eapi, Feature};
use crate::macros::{build_from_paths, extend_left};
use crate::pkgsh::builtins::Scope;
use crate::repo::{ebuild, Repository};
pub mod builtins;
mod install;
pub(crate) mod metadata;
pub(crate) mod phase;
pub(crate) mod test;
mod unescape;
mod utils;
use metadata::Key;
struct Stdin {
inner: io::Stdin,
fake: io::Cursor<Vec<u8>>,
}
impl Default for Stdin {
fn default() -> Self {
Self {
inner: io::stdin(),
fake: io::Cursor::new(vec![]),
}
}
}
#[cfg(test)]
macro_rules! write_stdin {
($($arg:tt)*) => {
crate::pkgsh::BUILD_DATA.with(|d| {
write!(d.borrow_mut().stdin.fake, $($arg)*).unwrap();
d.borrow_mut().stdin.fake.set_position(0);
})
}
}
#[cfg(test)]
use write_stdin;
struct Stdout {
inner: io::Stdout,
fake: io::Cursor<Vec<u8>>,
}
impl Default for Stdout {
fn default() -> Self {
Self {
inner: io::stdout(),
fake: io::Cursor::new(vec![]),
}
}
}
macro_rules! write_stdout {
($($arg:tt)*) => {
crate::pkgsh::BUILD_DATA.with(|d| {
write!(d.borrow_mut().stdout(), $($arg)*)?;
d.borrow_mut().stdout().flush()
})
}
}
use write_stdout;
#[cfg(test)]
macro_rules! get_stdout {
() => {
crate::pkgsh::BUILD_DATA.with(|d| {
let mut d = d.borrow_mut();
let output = std::str::from_utf8(d.stdout.fake.get_ref()).unwrap();
let output = String::from(output);
d.stdout.fake = std::io::Cursor::new(vec![]);
output
})
};
}
#[cfg(test)]
use get_stdout;
#[cfg(test)]
macro_rules! assert_stdout {
($expected:expr) => {
let output = crate::pkgsh::get_stdout!();
assert_eq!(output, $expected);
};
}
#[cfg(test)]
use assert_stdout;
struct Stderr {
inner: io::Stderr,
fake: io::Cursor<Vec<u8>>,
}
impl Default for Stderr {
fn default() -> Self {
Self {
inner: io::stderr(),
fake: io::Cursor::new(vec![]),
}
}
}
macro_rules! write_stderr {
($($arg:tt)*) => {
crate::pkgsh::BUILD_DATA.with(|d| {
write!(d.borrow_mut().stderr(), $($arg)*)?;
d.borrow_mut().stderr().flush()
})
}
}
use write_stderr;
#[cfg(test)]
macro_rules! get_stderr {
() => {
crate::pkgsh::BUILD_DATA.with(|d| {
let mut d = d.borrow_mut();
let output = std::str::from_utf8(d.stderr.fake.get_ref()).unwrap();
let output = String::from(output);
d.stderr.fake = std::io::Cursor::new(vec![]);
output
})
};
}
#[cfg(test)]
use get_stderr;
#[cfg(test)]
macro_rules! assert_stderr {
($expected:expr) => {
let output = crate::pkgsh::get_stderr!();
assert_eq!(output, $expected);
};
}
#[cfg(test)]
use assert_stderr;
#[derive(Default)]
struct BuildData<'a> {
eapi: &'static Eapi,
atom: Option<Atom>,
repo: Option<&'a ebuild::Repo>,
captured_io: bool,
stdin: Stdin,
stdout: Stdout,
stderr: Stderr,
env: HashMap<String, String>,
distfiles: Vec<String>,
user_patches: Vec<String>,
phase: Option<phase::Phase>,
scope: Scope,
user_patches_applied: bool,
desttree: String,
docdesttree: String,
exedesttree: String,
insdesttree: String,
insopts: Vec<String>,
diropts: Vec<String>,
exeopts: Vec<String>,
libopts: Vec<String>,
compress_include: HashSet<String>,
compress_exclude: HashSet<String>,
strip_include: HashSet<String>,
strip_exclude: HashSet<String>,
iuse_effective: HashSet<String>,
use_: HashSet<String>,
inherit: IndexSet<String>,
inherited: IndexSet<String>,
iuse: VecDeque<String>,
required_use: VecDeque<String>,
depend: VecDeque<String>,
rdepend: VecDeque<String>,
pdepend: VecDeque<String>,
bdepend: VecDeque<String>,
idepend: VecDeque<String>,
properties: VecDeque<String>,
restrict: VecDeque<String>,
}
impl BuildData<'_> {
fn new() -> Self {
Self {
captured_io: cfg!(test),
..Default::default()
}
}
fn update(atom: &Atom, repo: &ebuild::Repo) {
let r = unsafe { mem::transmute(repo) };
BUILD_DATA.with(|d| {
d.replace(BuildData {
atom: Some(atom.clone()),
repo: Some(r),
insopts: vec!["-m0644".to_string()],
libopts: vec!["-m0644".to_string()],
diropts: vec!["-m0755".to_string()],
exeopts: vec!["-m0755".to_string()],
desttree: "/usr".into(),
..BuildData::new()
})
});
}
fn set_vars(&mut self) -> scallop::Result<()> {
for (var, scopes) in self.eapi.env() {
if scopes.matches(self.scope) {
if self.env.get(var.as_ref()).is_none() {
let val = var.get(self);
self.env.insert(var.to_string(), val);
}
bind(var, self.env.get(var.as_ref()).unwrap(), None, None)?;
}
}
Ok(())
}
fn override_var(&self, var: BuildVariable, val: &str) -> scallop::Result<()> {
if let Some(scopes) = self.eapi.env().get(&var) {
if scopes.matches(self.scope) {
bind(var, val, None, None)?;
} else {
panic!("invalid scope {:?} for variable: {var}", self.scope);
}
}
Ok(())
}
fn stdin(&mut self) -> scallop::Result<&mut dyn Read> {
if !cfg!(test) && isatty(0).unwrap_or(false) {
return Err(Error::Base("no input available, stdin is a tty".into()));
}
match self.captured_io {
false => Ok(&mut self.stdin.inner),
true => Ok(&mut self.stdin.fake),
}
}
fn stdout(&mut self) -> &mut dyn Write {
match self.captured_io {
false => &mut self.stdout.inner,
true => &mut self.stdout.fake,
}
}
fn stderr(&mut self) -> &mut dyn Write {
match self.captured_io {
false => &mut self.stderr.inner,
true => &mut self.stderr.fake,
}
}
fn destdir(&self) -> &str {
self.env
.get("ED")
.unwrap_or_else(|| self.env.get("D").expect("undefined destdirs $ED and $D"))
}
fn install(&self) -> install::Install {
install::Install::new(self)
}
fn get_deque(&mut self, key: &Key) -> &mut VecDeque<String> {
match key {
Key::Iuse => &mut self.iuse,
Key::RequiredUse => &mut self.required_use,
Key::Depend => &mut self.depend,
Key::Rdepend => &mut self.rdepend,
Key::Pdepend => &mut self.pdepend,
Key::Bdepend => &mut self.bdepend,
Key::Idepend => &mut self.idepend,
Key::Properties => &mut self.properties,
Key::Restrict => &mut self.restrict,
_ => panic!("unknown field name: {key}"),
}
}
}
thread_local! {
static BUILD_DATA: RefCell<BuildData<'static>> = RefCell::new(BuildData::new())
}
#[cfg(feature = "init")]
#[ctor::ctor]
fn initialize() {
use crate::pkgsh::builtins::ALL_BUILTINS;
scallop::shell::init(true);
let builtins: Vec<_> = ALL_BUILTINS.values().map(|&b| b.into()).collect();
scallop::builtins::register(&builtins);
scallop::builtins::enable(&builtins).expect("failed enabling builtins");
}
#[allow(dead_code)]
fn run_phase(phase: phase::Phase) -> scallop::Result<ExecStatus> {
BUILD_DATA.with(|d| -> scallop::Result<ExecStatus> {
let eapi = d.borrow().eapi;
d.borrow_mut().phase = Some(phase);
d.borrow_mut().scope = Scope::Phase(phase);
d.borrow_mut().set_vars()?;
if let Some(mut func) = functions::find(format!("pre_{phase}")) {
func.execute(&[])?;
}
match functions::find(phase) {
Some(mut func) => func.execute(&[])?,
None => match eapi.phases().get(&phase) {
Some(phase) => phase.run()?,
None => return Err(Error::Base(format!("nonexistent phase: {phase}"))),
},
};
if let Some(mut func) = functions::find(format!("post_{phase}")) {
func.execute(&[])?;
}
d.borrow_mut().phase = None;
Ok(ExecStatus::Success)
})
}
fn source_ebuild(path: &Utf8Path) -> scallop::Result<()> {
if !path.exists() {
return Err(Error::Base(format!("nonexistent ebuild: {path:?}")));
}
BUILD_DATA.with(|d| -> scallop::Result<()> {
let eapi = d.borrow().eapi;
d.borrow_mut().scope = Scope::Global;
d.borrow_mut().set_vars()?;
let mut opts = ScopedOptions::default();
if eapi.has(Feature::GlobalFailglob) {
opts.enable(["failglob"])?;
}
source::file(path)?;
if eapi.has(Feature::RdependDefault) && variables::optional("RDEPEND").is_none() {
if let Some(depend) = variables::optional("DEPEND") {
bind("RDEPEND", depend, None, None)?;
}
}
if !d.borrow().inherited.is_empty() {
let mut d = d.borrow_mut();
for var in eapi.incremental_keys() {
let deque = d.get_deque(var);
if let Ok(data) = string_vec(var) {
extend_left!(deque, data.into_iter());
}
bind(var, deque.iter().join(" "), None, None)?;
}
}
Ok(())
})
}
#[derive(AsRefStr, Display, Debug, PartialEq, Eq, Hash, Copy, Clone)]
#[allow(non_camel_case_types)]
pub enum BuildVariable {
P,
PF,
PN,
CATEGORY,
PV,
PR,
PVR,
A,
AA,
FILESDIR,
DISTDIR,
WORKDIR,
S,
PORTDIR,
ECLASSDIR,
ROOT,
EROOT,
SYSROOT,
ESYSROOT,
BROOT,
T,
TMPDIR,
HOME,
EPREFIX,
D,
ED,
DESTTREE,
INSDESTTREE,
USE,
EBUILD_PHASE,
EBUILD_PHASE_FUNC,
KV,
MERGE_TYPE,
REPLACING_VERSIONS,
REPLACED_BY_VERSION,
}
impl BuildVariable {
fn get(&self, build: &BuildData) -> String {
use BuildVariable::*;
let a = build.atom.as_ref().expect("missing required atom field");
let v = a.version().expect("missing required versioned atom");
match self {
P => format!("{}-{}", a.package(), v.base()),
PF => format!("{}-{}", a.package(), PVR.get(build)),
PN => a.package().into(),
CATEGORY => a.category().into(),
PV => v.base().into(),
PR => format!("r{}", v.revision()),
PVR => match v.revision() == "0" {
true => v.base().into(),
false => v.into(),
},
FILESDIR => {
let path = build_from_paths!(
build.repo.unwrap().path(),
a.category(),
a.package(),
"files"
);
path.into_string()
}
PORTDIR => build.repo.unwrap().path().to_string(),
ECLASSDIR => build.repo.unwrap().path().join("eclass").into_string(),
EBUILD_PHASE => build.phase.expect("missing phase").short_name().to_string(),
EBUILD_PHASE_FUNC => build.phase.expect("missing phase").to_string(),
KV => os_release().expect("failed to get OS version"),
_ => "TODO".to_string(),
}
}
}
#[cfg(test)]
mod tests {
use crate::config::Config;
use super::*;
#[test]
fn source_ebuild_disables_external_cmds() {
let mut config = Config::default();
let (t, repo) = config.temp_repo("test", 0).unwrap();
let data = indoc::indoc! {r#"
DESCRIPTION="unknown command failure"
SLOT=0
ls /
"#};
let (path, cpv) = t.create_ebuild_raw("cat/pkg-1", data).unwrap();
BuildData::update(&cpv, &repo);
assert!(source_ebuild(&path).is_err());
let data = indoc::indoc! {r#"
DESCRIPTION="unknown command failure"
SLOT=0
/bin/ls /
"#};
let (path, cpv) = t.create_ebuild_raw("cat/pkg-2", data).unwrap();
BuildData::update(&cpv, &repo);
assert!(source_ebuild(&path).is_err());
}
}