use std::path::PathBuf;
use std::process::Command;
use crate::preprocessor::PPConfig;
#[derive(Debug)]
pub struct PerlConfig {
pub include_paths: Vec<PathBuf>,
pub defines: Vec<(String, Option<String>)>,
pub build_mode: PerlBuildMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PerlBuildMode {
Threaded,
NonThreaded,
}
impl PerlBuildMode {
pub fn detect_from_perl_config() -> Result<Self, PerlConfigError> {
let usethreads = get_config_value("usethreads")?;
if usethreads == "define" {
Ok(PerlBuildMode::Threaded)
} else {
Ok(PerlBuildMode::NonThreaded)
}
}
pub fn is_threaded(self) -> bool {
matches!(self, Self::Threaded)
}
}
#[derive(Debug)]
pub enum PerlConfigError {
CommandFailed(String),
ConfigNotFound(String),
ParseError(String),
}
impl std::fmt::Display for PerlConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PerlConfigError::CommandFailed(msg) => write!(f, "perl command failed: {}", msg),
PerlConfigError::ConfigNotFound(key) => write!(f, "Config key not found: {}", key),
PerlConfigError::ParseError(msg) => write!(f, "parse error: {}", msg),
}
}
}
impl std::error::Error for PerlConfigError {}
fn get_config_value(key: &str) -> Result<String, PerlConfigError> {
let output = Command::new("perl")
.args(["-MConfig", "-le", &format!("print $Config{{{}}}", key)])
.output()
.map_err(|e| PerlConfigError::CommandFailed(e.to_string()))?;
if !output.status.success() {
return Err(PerlConfigError::CommandFailed(
String::from_utf8_lossy(&output.stderr).to_string(),
));
}
let value = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(value)
}
fn parse_cppsymbols(symbols: &str) -> Vec<(String, Option<String>)> {
let mut result = Vec::new();
let mut current = String::new();
let mut chars = symbols.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(&next) = chars.peek() {
current.push(c);
current.push(next);
chars.next();
}
} else if c == ' ' || c == '\t' {
if !current.is_empty() {
result.push(parse_single_define(¤t));
current.clear();
}
} else {
current.push(c);
}
}
if !current.is_empty() {
result.push(parse_single_define(¤t));
}
result
}
fn parse_single_define(s: &str) -> (String, Option<String>) {
if let Some(pos) = s.find('=') {
let (name, value) = s.split_at(pos);
let unescaped_value = value[1..].replace("\\ ", " ");
(name.to_string(), Some(unescaped_value))
} else {
(s.to_string(), None)
}
}
fn parse_incpth(incpth: &str) -> Vec<PathBuf> {
incpth
.split_whitespace()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.collect()
}
fn get_ccopts_defines() -> Result<Vec<(String, Option<String>)>, PerlConfigError> {
let output = Command::new("perl")
.args(["-MExtUtils::Embed", "-e", "print ccopts"])
.output()
.map_err(|e| PerlConfigError::CommandFailed(e.to_string()))?;
if !output.status.success() {
return Err(PerlConfigError::CommandFailed(
String::from_utf8_lossy(&output.stderr).to_string(),
));
}
let ccopts = String::from_utf8_lossy(&output.stdout);
let mut defines = Vec::new();
for part in ccopts.split_whitespace() {
if let Some(def) = part.strip_prefix("-D") {
defines.push(parse_single_define(def));
}
}
Ok(defines)
}
pub fn get_default_target_dir() -> Result<PathBuf, PerlConfigError> {
let archlib = get_config_value("archlib")?;
if archlib.is_empty() {
return Err(PerlConfigError::ConfigNotFound("archlib".to_string()));
}
Ok(PathBuf::from(&archlib).join("CORE"))
}
pub fn get_perl_version() -> Result<(u32, u32), PerlConfigError> {
let version = get_config_value("version")?;
if version.is_empty() {
return Err(PerlConfigError::ConfigNotFound("version".to_string()));
}
let parts: Vec<&str> = version.split('.').collect();
if parts.len() < 2 {
return Err(PerlConfigError::ParseError(format!(
"invalid version format: {}",
version
)));
}
let major = parts[0].parse::<u32>().map_err(|_| {
PerlConfigError::ParseError(format!("invalid major version: {}", parts[0]))
})?;
let minor = parts[1].parse::<u32>().map_err(|_| {
PerlConfigError::ParseError(format!("invalid minor version: {}", parts[1]))
})?;
Ok((major, minor))
}
pub fn get_perl_config() -> Result<PerlConfig, PerlConfigError> {
let incpth = get_config_value("incpth")?;
let mut include_paths = parse_incpth(&incpth);
let archlib = get_config_value("archlib")?;
if !archlib.is_empty() {
let core_path = PathBuf::from(&archlib).join("CORE");
if core_path.exists() {
include_paths.push(core_path);
}
}
let cppsymbols = get_config_value("cppsymbols")?;
let mut defines = parse_cppsymbols(&cppsymbols);
if let Ok(ccopts_defines) = get_ccopts_defines() {
for (name, value) in ccopts_defines {
if let Some(pos) = defines.iter().position(|(n, _)| n == &name) {
defines[pos] = (name, value);
} else {
defines.push((name, value));
}
}
}
defines.push(("PERL_CORE".to_string(), None));
if std::env::var("DEBUG_PERL_CONFIG").is_ok() {
eprintln!("[perl_config] include_paths: {:?}", include_paths);
eprintln!("[perl_config] defines count: {}", defines.len());
for (name, value) in &defines {
if name.contains("x86") || name.contains("LP64") {
eprintln!("[perl_config] {} = {:?}", name, value);
}
}
}
let build_mode = PerlBuildMode::detect_from_perl_config()?;
Ok(PerlConfig {
include_paths,
defines,
build_mode,
})
}
pub fn build_pp_config_for_perl() -> Result<PPConfig, PerlConfigError> {
let perl_cfg = get_perl_config()?;
let target_dir = get_default_target_dir().ok();
Ok(PPConfig {
include_paths: perl_cfg.include_paths,
predefined: perl_cfg.defines,
debug_pp: false,
target_dir,
emit_markers: false,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_single_define() {
assert_eq!(
parse_single_define("FOO"),
("FOO".to_string(), None)
);
assert_eq!(
parse_single_define("FOO=1"),
("FOO".to_string(), Some("1".to_string()))
);
assert_eq!(
parse_single_define("__GNUC__=15"),
("__GNUC__".to_string(), Some("15".to_string()))
);
}
#[test]
fn test_parse_cppsymbols_simple() {
let symbols = "FOO=1 BAR=2 BAZ";
let result = parse_cppsymbols(symbols);
assert_eq!(result.len(), 3);
assert_eq!(result[0], ("FOO".to_string(), Some("1".to_string())));
assert_eq!(result[1], ("BAR".to_string(), Some("2".to_string())));
assert_eq!(result[2], ("BAZ".to_string(), None));
}
#[test]
fn test_parse_cppsymbols_with_escape() {
let symbols = r#"__VERSION__="15.1.1\ 20250521" FOO=1"#;
let result = parse_cppsymbols(symbols);
assert_eq!(result.len(), 2);
assert_eq!(
result[0],
("__VERSION__".to_string(), Some(r#""15.1.1 20250521""#.to_string()))
);
assert_eq!(result[1], ("FOO".to_string(), Some("1".to_string())));
}
#[test]
fn test_parse_incpth() {
let incpth = "/usr/lib/gcc/x86_64-redhat-linux/15/include /usr/local/include /usr/include";
let result = parse_incpth(incpth);
assert_eq!(result.len(), 3);
assert_eq!(result[0], PathBuf::from("/usr/lib/gcc/x86_64-redhat-linux/15/include"));
assert_eq!(result[1], PathBuf::from("/usr/local/include"));
assert_eq!(result[2], PathBuf::from("/usr/include"));
}
#[test]
fn test_parse_version() {
fn parse_version(version: &str) -> Option<(u32, u32)> {
let parts: Vec<&str> = version.split('.').collect();
if parts.len() < 2 {
return None;
}
let major = parts[0].parse::<u32>().ok()?;
let minor = parts[1].parse::<u32>().ok()?;
Some((major, minor))
}
assert_eq!(parse_version("5.40.0"), Some((5, 40)));
assert_eq!(parse_version("5.38.2"), Some((5, 38)));
assert_eq!(parse_version("5.10.1"), Some((5, 10)));
assert_eq!(parse_version("5.8"), Some((5, 8)));
assert_eq!(parse_version("invalid"), None);
}
}