use crate::dependencies::BinaryDependency;
use crate::dependency::Dependency;
use crate::installer::{
install_missing_deps, Error as InstallerError, InstallationScope, Installer,
};
use crate::output::Output;
use crate::session::{which, Session};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, std::hash::Hash)]
pub enum DependencyCategory {
Universal,
Build,
Runtime,
Test,
Dev,
BuildExtra(String),
RuntimeExtra(String),
}
impl std::fmt::Display for DependencyCategory {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
DependencyCategory::Universal => write!(f, "universal"),
DependencyCategory::Build => write!(f, "build"),
DependencyCategory::Runtime => write!(f, "runtime"),
DependencyCategory::Test => write!(f, "test"),
DependencyCategory::Dev => write!(f, "dev"),
DependencyCategory::BuildExtra(s) => write!(f, "build-extra:{}", s),
DependencyCategory::RuntimeExtra(s) => write!(f, "runtime-extra:{}", s),
}
}
}
#[derive(Debug)]
pub enum Error {
NoBuildSystemDetected,
DependencyInstallError(InstallerError),
Error(crate::analyze::AnalyzedError),
IoError(std::io::Error),
Unimplemented,
Other(String),
}
impl From<InstallerError> for Error {
fn from(e: InstallerError) -> Self {
Error::DependencyInstallError(e)
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::IoError(e)
}
}
impl From<crate::analyze::AnalyzedError> for Error {
fn from(e: crate::analyze::AnalyzedError) -> Self {
Error::Error(e)
}
}
impl From<crate::session::Error> for Error {
fn from(e: crate::session::Error) -> Self {
match e {
crate::session::Error::CalledProcessError(e) => {
crate::analyze::AnalyzedError::Unidentified {
retcode: e.code().unwrap(),
lines: Vec::new(),
secondary: None,
}
.into()
}
crate::session::Error::IoError(e) => e.into(),
crate::session::Error::SetupFailure(_, _) => unreachable!(),
crate::session::Error::ImageError(_) => unreachable!(),
}
}
}
impl From<crate::fix_build::IterateBuildError<InstallerError>> for Error {
fn from(e: crate::fix_build::IterateBuildError<InstallerError>) -> Self {
match e {
crate::fix_build::IterateBuildError::FixerLimitReached(n) => {
Error::Other(format!("Fixer limit reached: {}", n))
}
crate::fix_build::IterateBuildError::Persistent(e) => {
crate::analyze::AnalyzedError::Detailed {
error: e,
retcode: 1,
}
.into()
}
crate::fix_build::IterateBuildError::Unidentified {
retcode,
lines,
secondary,
} => crate::analyze::AnalyzedError::Unidentified {
retcode,
lines,
secondary,
}
.into(),
crate::fix_build::IterateBuildError::Other(o) => o.into(),
}
}
}
impl From<crate::fix_build::IterateBuildError<Error>> for Error {
fn from(e: crate::fix_build::IterateBuildError<Error>) -> Self {
match e {
crate::fix_build::IterateBuildError::FixerLimitReached(n) => {
Error::Other(format!("Fixer limit reached: {}", n))
}
crate::fix_build::IterateBuildError::Persistent(e) => {
crate::analyze::AnalyzedError::Detailed {
error: e,
retcode: 1,
}
.into()
}
crate::fix_build::IterateBuildError::Unidentified {
retcode,
lines,
secondary,
} => crate::analyze::AnalyzedError::Unidentified {
retcode,
lines,
secondary,
}
.into(),
crate::fix_build::IterateBuildError::Other(o) => o,
}
}
}
impl From<Error> for crate::fix_build::InterimError<Error> {
fn from(e: Error) -> Self {
match e {
Error::Error(crate::analyze::AnalyzedError::Detailed { error, retcode: _ }) => {
crate::fix_build::InterimError::Recognized(error)
}
e => crate::fix_build::InterimError::Other(e),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::NoBuildSystemDetected => write!(f, "No build system detected"),
Error::DependencyInstallError(e) => write!(f, "Error installing dependency: {}", e),
Error::Error(e) => write!(f, "Error: {}", e),
Error::IoError(e) => write!(f, "IO Error: {}", e),
Error::Other(e) => write!(f, "Error: {}", e),
Error::Unimplemented => write!(f, "Unimplemented"),
}
}
}
impl std::error::Error for Error {}
#[derive(Debug, Clone)]
pub struct InstallTarget {
pub scope: InstallationScope,
pub prefix: Option<PathBuf>,
}
impl DependencyCategory {
pub fn all() -> [DependencyCategory; 5] {
[
DependencyCategory::Universal,
DependencyCategory::Build,
DependencyCategory::Runtime,
DependencyCategory::Test,
DependencyCategory::Dev,
]
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Action {
Clean,
Build,
Test,
Install,
}
pub fn guaranteed_which(
session: &dyn Session,
installer: &dyn Installer,
name: &str,
) -> Result<PathBuf, InstallerError> {
match which(session, name) {
Some(path) => Ok(PathBuf::from(path)),
None => {
installer.install(&BinaryDependency::new(name), InstallationScope::Global)?;
Ok(PathBuf::from(which(session, name).unwrap()))
}
}
}
pub trait BuildSystem: std::fmt::Debug {
fn name(&self) -> &str;
fn dist(
&self,
session: &dyn Session,
installer: &dyn Installer,
target_directory: &Path,
quiet: bool,
) -> Result<std::ffi::OsString, Error>;
fn install_declared_dependencies(
&self,
categories: &[DependencyCategory],
scopes: &[InstallationScope],
session: &dyn Session,
installer: &dyn Installer,
fixers: Option<&[&dyn crate::fix_build::BuildFixer<InstallerError>]>,
) -> Result<(), Error> {
let declared_deps = self.get_declared_dependencies(session, fixers)?;
let relevant: Vec<_> = declared_deps
.into_iter()
.filter(|(c, _d)| categories.contains(c))
.map(|(_, d)| d)
.collect();
log::debug!("Declared dependencies: {:?}", relevant);
let dep_refs: Vec<&dyn Dependency> = relevant.iter().map(|d| d.as_ref()).collect();
install_missing_deps(session, installer, scopes, &dep_refs)?;
Ok(())
}
fn test(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error>;
fn build(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error>;
fn clean(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error>;
fn install(
&self,
session: &dyn Session,
installer: &dyn Installer,
install_target: &InstallTarget,
) -> Result<(), Error>;
fn get_declared_dependencies(
&self,
session: &dyn Session,
fixers: Option<&[&dyn crate::fix_build::BuildFixer<InstallerError>]>,
) -> Result<Vec<(DependencyCategory, Box<dyn Dependency>)>, Error>;
fn get_declared_outputs(
&self,
session: &dyn Session,
fixers: Option<&[&dyn crate::fix_build::BuildFixer<InstallerError>]>,
) -> Result<Vec<Box<dyn Output>>, Error>;
fn as_any(&self) -> &dyn std::any::Any;
}
struct BuildSystemEntry {
name: &'static str,
probe: fn(&Path) -> Option<Box<dyn BuildSystem>>,
}
const BUILDSYSTEM_REGISTRY: &[BuildSystemEntry] = &[
BuildSystemEntry {
name: "pear",
probe: Pear::probe,
},
BuildSystemEntry {
name: "setup.py",
probe: crate::buildsystems::python::SetupPy::probe,
},
BuildSystemEntry {
name: "node",
probe: crate::buildsystems::node::Node::probe,
},
BuildSystemEntry {
name: "waf",
probe: crate::buildsystems::waf::Waf::probe,
},
BuildSystemEntry {
name: "gem",
probe: crate::buildsystems::ruby::Gem::probe,
},
BuildSystemEntry {
name: "meson",
probe: crate::buildsystems::meson::Meson::probe,
},
BuildSystemEntry {
name: "cargo",
probe: crate::buildsystems::rust::Cargo::probe,
},
BuildSystemEntry {
name: "cabal",
probe: crate::buildsystems::haskell::Cabal::probe,
},
BuildSystemEntry {
name: "gradle",
probe: crate::buildsystems::java::Gradle::probe,
},
BuildSystemEntry {
name: "maven",
probe: crate::buildsystems::java::Maven::probe,
},
BuildSystemEntry {
name: "distzilla",
probe: crate::buildsystems::perl::DistZilla::probe,
},
BuildSystemEntry {
name: "perl-build-tiny",
probe: crate::buildsystems::perl::PerlBuildTiny::probe,
},
BuildSystemEntry {
name: "go",
probe: crate::buildsystems::go::Golang::probe,
},
BuildSystemEntry {
name: "bazel",
probe: crate::buildsystems::bazel::Bazel::probe,
},
BuildSystemEntry {
name: "r",
probe: crate::buildsystems::r::R::probe,
},
BuildSystemEntry {
name: "octave",
probe: crate::buildsystems::octave::Octave::probe,
},
BuildSystemEntry {
name: "cmake",
probe: crate::buildsystems::make::CMake::probe,
},
BuildSystemEntry {
name: "gnome-shell-extension",
probe: crate::buildsystems::gnome::GnomeShellExtension::probe,
},
BuildSystemEntry {
name: "make",
probe: crate::buildsystems::make::Make::probe,
},
BuildSystemEntry {
name: "composer",
probe: Composer::probe,
},
BuildSystemEntry {
name: "runtests",
probe: RunTests::probe,
},
];
pub const PEAR_NAMESPACES: &[&str] = &[
"http://pear.php.net/dtd/package-2.0",
"http://pear.php.net/dtd/package-2.1",
];
#[derive(Debug)]
pub struct Pear(pub PathBuf);
impl Pear {
pub fn new(path: PathBuf) -> Self {
Self(path)
}
pub fn probe(path: &Path) -> Option<Box<dyn BuildSystem>> {
let package_xml_path = path.join("package.xml");
if !package_xml_path.exists() {
return None;
}
use xmltree::Element;
let root = Element::parse(std::fs::File::open(package_xml_path).unwrap()).unwrap();
if root
.namespace
.as_deref()
.and_then(|ns| PEAR_NAMESPACES.iter().find(|&n| *n == ns))
.is_none()
{
log::warn!(
"Namespace of package.xml is not recognized as a PEAR package: {:?}",
root.namespace
);
return None;
}
if root.name != "package" {
log::warn!("Root tag of package.xml is not <package>: {:?}", root.name);
return None;
}
log::debug!(
"Found package.xml with namespace {}, assuming pear package.",
root.namespace.as_ref().unwrap()
);
Some(Box::new(Self(PathBuf::from(path))))
}
}
impl BuildSystem for Pear {
fn name(&self) -> &str {
"pear"
}
fn dist(
&self,
session: &dyn Session,
installer: &dyn Installer,
target_directory: &Path,
quiet: bool,
) -> Result<std::ffi::OsString, Error> {
let dc = crate::dist_catcher::DistCatcher::new(vec![session.external_path(Path::new("."))]);
let pear = guaranteed_which(session, installer, "pear")?;
session
.command(vec![pear.to_str().unwrap(), "package"])
.quiet(quiet)
.run_detecting_problems()?;
Ok(dc.copy_single(target_directory).unwrap().unwrap())
}
fn test(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> {
let pear = guaranteed_which(session, installer, "pear")?;
session
.command(vec![pear.to_str().unwrap(), "run-tests"])
.run_detecting_problems()?;
Ok(())
}
fn build(&self, session: &dyn Session, installer: &dyn Installer) -> Result<(), Error> {
let pear = guaranteed_which(session, installer, "pear")?;
session
.command(vec![
pear.to_str().unwrap(),
"build",
self.0.to_str().unwrap(),
])
.run_detecting_problems()?;
Ok(())
}
fn clean(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> {
todo!()
}
fn install(
&self,
session: &dyn Session,
installer: &dyn Installer,
_install_target: &InstallTarget,
) -> Result<(), Error> {
let pear = guaranteed_which(session, installer, "pear")?;
session
.command(vec![
pear.to_str().unwrap(),
"install",
self.0.to_str().unwrap(),
])
.run_detecting_problems()?;
Ok(())
}
fn get_declared_dependencies(
&self,
_session: &dyn Session,
_fixers: Option<&[&dyn crate::fix_build::BuildFixer<InstallerError>]>,
) -> Result<Vec<(DependencyCategory, Box<dyn Dependency>)>, Error> {
let path = self.0.join("package.xml");
use xmltree::Element;
let root = Element::parse(std::fs::File::open(path).unwrap()).unwrap();
if root
.namespace
.as_deref()
.and_then(|ns| PEAR_NAMESPACES.iter().find(|&n| *n == ns))
.is_none()
{
log::warn!(
"Namespace of package.xml is not recognized as a PEAR package: {:?}",
root.namespace
);
return Ok(vec![]);
}
if root.name != "package" {
log::warn!("Root tag of package.xml is not <package>: {:?}", root.name);
return Ok(vec![]);
}
let dependencies_tag = root
.get_child("dependencies")
.ok_or_else(|| Error::Other("No <dependencies> tag found in <package>".to_owned()))?;
let required_tag = dependencies_tag
.get_child("required")
.ok_or_else(|| Error::Other("No <required> tag found in <dependencies>".to_owned()))?;
Ok(required_tag
.children
.iter()
.filter_map(|x| x.as_element())
.filter(|c| c.name.as_str() == "package")
.filter_map(
|package_tag| -> Option<(DependencyCategory, Box<dyn Dependency>)> {
let name = package_tag
.get_child("name")
.and_then(|n| n.get_text())
.unwrap()
.into_owned();
let min_version = package_tag
.get_child("min")
.and_then(|m| m.get_text())
.map(|s| s.into_owned());
let max_version = package_tag
.get_child("max")
.and_then(|m| m.get_text())
.map(|s| s.into_owned());
let channel = package_tag
.get_child("channel")
.and_then(|c| c.get_text())
.map(|s| s.into_owned());
Some((
DependencyCategory::Universal,
Box::new(crate::dependencies::php::PhpPackageDependency {
package: name,
channel,
min_version,
max_version,
}) as Box<dyn Dependency>,
))
},
)
.collect())
}
fn get_declared_outputs(
&self,
_session: &dyn Session,
_fixers: Option<&[&dyn crate::fix_build::BuildFixer<InstallerError>]>,
) -> Result<Vec<Box<dyn Output>>, Error> {
todo!()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
pub fn scan_buildsystems(path: &Path) -> Vec<(PathBuf, Box<dyn BuildSystem>)> {
let mut ret = vec![];
ret.extend(
detect_buildsystems(path)
.into_iter()
.map(|bs| (PathBuf::from(path), bs)),
);
if ret.is_empty() {
for entry in std::fs::read_dir(path).unwrap() {
let entry = entry.unwrap();
if entry.file_type().unwrap().is_dir() {
ret.extend(
detect_buildsystems(&entry.path())
.into_iter()
.map(|bs| (entry.path(), bs)),
);
}
}
}
ret
}
#[derive(Debug)]
pub struct Composer(pub PathBuf);
impl Composer {
pub fn new(path: PathBuf) -> Self {
Self(path)
}
pub fn probe(path: &Path) -> Option<Box<dyn BuildSystem>> {
if path.join("composer.json").exists() {
Some(Box::new(Self(path.into())))
} else {
None
}
}
}
impl BuildSystem for Composer {
fn name(&self) -> &str {
"composer"
}
fn dist(
&self,
_session: &dyn Session,
_installer: &dyn Installer,
_target_directory: &Path,
_quiet: bool,
) -> Result<std::ffi::OsString, Error> {
todo!()
}
fn test(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> {
todo!()
}
fn build(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> {
todo!()
}
fn clean(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> {
todo!()
}
fn install(
&self,
_session: &dyn Session,
_installer: &dyn Installer,
_install_target: &InstallTarget,
) -> Result<(), Error> {
todo!()
}
fn get_declared_dependencies(
&self,
_session: &dyn Session,
_fixers: Option<&[&dyn crate::fix_build::BuildFixer<InstallerError>]>,
) -> Result<Vec<(DependencyCategory, Box<dyn Dependency>)>, Error> {
todo!()
}
fn get_declared_outputs(
&self,
_session: &dyn Session,
_fixers: Option<&[&dyn crate::fix_build::BuildFixer<InstallerError>]>,
) -> Result<Vec<Box<dyn Output>>, Error> {
todo!()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[derive(Debug)]
pub struct RunTests(pub PathBuf);
impl RunTests {
pub fn new(path: PathBuf) -> Self {
Self(path)
}
pub fn probe(path: &Path) -> Option<Box<dyn BuildSystem>> {
if path.join("runtests.sh").exists() {
Some(Box::new(Self(path.into())))
} else {
None
}
}
}
impl BuildSystem for RunTests {
fn name(&self) -> &str {
"runtests"
}
fn dist(
&self,
_session: &dyn Session,
_installer: &dyn Installer,
_target_directory: &Path,
_quiet: bool,
) -> Result<std::ffi::OsString, Error> {
todo!()
}
fn test(&self, session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> {
let interpreter = crate::shebang::shebang_binary(&self.0.join("runtests.sh")).unwrap();
let argv = if interpreter.is_some() {
vec!["./runtests.sh"]
} else {
vec!["/bin/bash", "./runtests.sh"]
};
session.command(argv).run_detecting_problems()?;
Ok(())
}
fn build(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> {
todo!()
}
fn clean(&self, _session: &dyn Session, _installer: &dyn Installer) -> Result<(), Error> {
todo!()
}
fn install(
&self,
_session: &dyn Session,
_installer: &dyn Installer,
_install_target: &InstallTarget,
) -> Result<(), Error> {
todo!()
}
fn get_declared_dependencies(
&self,
_session: &dyn Session,
_fixers: Option<&[&dyn crate::fix_build::BuildFixer<InstallerError>]>,
) -> Result<Vec<(DependencyCategory, Box<dyn Dependency>)>, Error> {
todo!()
}
fn get_declared_outputs(
&self,
_session: &dyn Session,
_fixers: Option<&[&dyn crate::fix_build::BuildFixer<InstallerError>]>,
) -> Result<Vec<Box<dyn Output>>, Error> {
todo!()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
pub fn detect_buildsystems(path: &std::path::Path) -> Vec<Box<dyn BuildSystem>> {
if !path.exists() {
log::error!("Path does not exist: {:?}", path);
return vec![];
}
let path = path.canonicalize().unwrap();
let mut ret = vec![];
for entry in BUILDSYSTEM_REGISTRY {
let bs = (entry.probe)(&path);
if let Some(bs) = bs {
ret.push(bs);
}
}
ret
}
pub fn get_buildsystem(path: &Path) -> Option<(PathBuf, Box<dyn BuildSystem>)> {
scan_buildsystems(path).into_iter().next()
}
pub fn supported_buildsystem_names() -> Vec<&'static str> {
BUILDSYSTEM_REGISTRY
.iter()
.map(|entry| entry.name)
.collect()
}
pub fn buildsystem_by_name(name: &str, path: &Path) -> Option<Box<dyn BuildSystem>> {
BUILDSYSTEM_REGISTRY
.iter()
.find(|entry| entry.name == name)
.and_then(|entry| (entry.probe)(path))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::installer::NullInstaller;
use crate::session::plain::PlainSession;
#[test]
fn test_guaranteed_which() {
let session = PlainSession::new();
let installer = NullInstaller::new();
let _path = guaranteed_which(&session, &installer, "ls").unwrap();
}
#[test]
fn test_guaranteed_which_not_found() {
let session = PlainSession::new();
let installer = NullInstaller::new();
assert!(matches!(
guaranteed_which(&session, &installer, "this-does-not-exist").unwrap_err(),
InstallerError::UnknownDependencyFamily,
));
}
#[test]
fn test_supported_buildsystem_names() {
let names = supported_buildsystem_names();
assert!(!names.is_empty());
assert!(names.contains(&"cargo"));
assert!(names.contains(&"make"));
assert!(names.contains(&"meson"));
assert_eq!(names.len(), 21);
}
}