Files
pkh/src/new/templates/autotools.rs
T

251 lines
8.4 KiB
Rust

//! The `autotools` template: a C project with a `configure.ac` built through
//! debhelper's auto-detection (dh runs `autoreconf` itself when it finds
//! `configure.ac`, debhelper ≥ 10 — no override needed).
use std::path::Path;
use regex::Regex;
use super::meson::hello_c;
use super::{OutputFile, ProbeResult, Template, source_dir_of};
use crate::new::options::{NewOptions, TemplateId};
/// C/C++ with Autotools (`configure.ac`).
pub struct Autotools;
impl Template for Autotools {
fn id(&self) -> TemplateId {
TemplateId::Autotools
}
/// A minimal `configure.ac`, the matching `Makefile.am` and `hello.c`.
/// The first source build runs `autoreconf` (integrated in the dh
/// sequence), so no generated configure script is committed.
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> {
vec![
OutputFile::new(
"configure.ac",
format!(
"AC_INIT([{name}], [{version}])\n\
AM_INIT_AUTOMAKE([foreign])\n\
AC_PROG_CC\n\
AC_CONFIG_FILES([Makefile])\n\
AC_OUTPUT\n",
name = opts.name,
version = opts.upstream_version,
),
),
OutputFile::new(
"Makefile.am",
format!(
"bin_PROGRAMS = {command}\n\
{command}_SOURCES = hello.c\n",
command = opts.command,
),
),
hello_c(opts),
]
}
/// No extra debian/ files: plain `dh $@` auto-detects `configure.ac`.
fn debian(&self, _opts: &NewOptions) -> Vec<OutputFile> {
Vec::new()
}
fn build_depends(&self, opts: &NewOptions) -> Vec<String> {
let mut deps = vec![
"autoconf".to_string(),
"automake".to_string(),
"libtool".to_string(),
];
if uses_gettext(opts) {
deps.push("gettext".to_string());
}
deps
}
fn architecture(&self, _opts: &NewOptions) -> &'static str {
"any"
}
/// Package name and version from the `AC_INIT` macro of `configure.ac`.
fn probe(&self, dir: &Path) -> Option<ProbeResult> {
let content = std::fs::read_to_string(dir.join("configure.ac")).ok()?;
static AC_INIT_REGEX: std::sync::OnceLock<Regex> = std::sync::OnceLock::new();
// AC_INIT([name], [version]) — the autoconf quotes are optional.
let regex = AC_INIT_REGEX.get_or_init(|| {
Regex::new(
r"AC_INIT\s*\(\s*(?:\[([^\]]*)\]|([^,\s\[]+))\s*,\s*(?:\[([^\]]*)\]|([^,\s\[]+))",
)
.unwrap()
});
let caps = regex.captures(&content)?;
let value = |bracketed: usize, bare: usize| {
caps.get(bracketed)
.or_else(|| caps.get(bare))
.map(|v| v.as_str().trim().to_string())
.filter(|v| !v.is_empty())
};
let name = value(1, 2)?;
let version = value(3, 4);
Some(ProbeResult {
name: Some(name),
version,
..Default::default()
})
}
}
/// Whether the packaged `configure.ac` sets up GNU gettext (`AM_GNU_GETTEXT`
/// macro): those builds need the `gettext` package. Only meaningful when
/// packaging an existing tree (the generated skeleton carries no gettext).
fn uses_gettext(opts: &NewOptions) -> bool {
source_dir_of(opts).is_some_and(|dir| {
std::fs::read_to_string(dir.join("configure.ac"))
.is_ok_and(|content| content.contains("AM_GNU_GETTEXT"))
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::new::options::{License, SourceDir};
use tempfile::tempdir;
fn opts() -> NewOptions {
NewOptions {
name: "mytool".into(),
template: TemplateId::Autotools,
source_dir: SourceDir::Skeleton,
upstream_version: "0.1.0".into(),
revision: 1,
summary: "A tool".into(),
long_description: "A tool".into(),
homepage: None,
license: License::Mit,
command: "mytool".into(),
maintainer: ("Jane".into(), "jane@example.com".into()),
dist: "ubuntu".into(),
series: "resolute".into(),
release: false,
depends: Vec::new(),
source_format: crate::new::options::SourceFormat::Quilt,
orig: Some(crate::new::options::OrigOrigin::Snapshot),
git: true,
autopkgtest: false,
pkg_config: false,
watch: None,
}
}
#[test]
fn autotools_template_shape() {
let o = opts();
let template = super::super::get(TemplateId::Autotools).unwrap();
assert_eq!(template.architecture(&o), "any");
assert_eq!(
template.build_depends(&o),
vec![
"autoconf".to_string(),
"automake".to_string(),
"libtool".to_string()
]
);
assert_eq!(template.rules_dh_line(), "dh $@");
assert!(template.rules_extra(&o).is_empty());
assert!(template.debian(&o).is_empty());
let skeleton = template.skeleton(&o);
let configure = skeleton
.iter()
.find(|f| f.path == "configure.ac")
.expect("configure.ac skeleton");
assert!(
configure
.contents
.starts_with("AC_INIT([mytool], [0.1.0])\n")
);
let makefile_am = skeleton
.iter()
.find(|f| f.path == "Makefile.am")
.expect("Makefile.am skeleton");
assert!(makefile_am.contents.contains("bin_PROGRAMS = mytool"));
assert!(makefile_am.contents.contains("mytool_SOURCES = hello.c"));
assert!(skeleton.iter().any(|f| f.path == "hello.c"));
}
#[test]
fn autotools_probe_reads_ac_init() {
let template = super::super::get(TemplateId::Autotools).unwrap();
// Bracketed form (the generated skeleton's own shape).
let dir = tempdir().unwrap();
std::fs::write(
dir.path().join("configure.ac"),
"AC_INIT([mytool], [0.1.0])\nAM_INIT_AUTOMAKE([foreign])\n",
)
.unwrap();
let probe = template.probe(dir.path()).expect("probe result");
assert_eq!(probe.name.as_deref(), Some("mytool"));
assert_eq!(probe.version.as_deref(), Some("0.1.0"));
// Bare form with a bug-report address as the third argument.
let dir = tempdir().unwrap();
std::fs::write(
dir.path().join("configure.ac"),
"AC_INIT(mytool, 1.2.3, bugs@example.com)\n",
)
.unwrap();
let probe = template.probe(dir.path()).expect("probe result");
assert_eq!(probe.name.as_deref(), Some("mytool"));
assert_eq!(probe.version.as_deref(), Some("1.2.3"));
// Name with spaces inside the brackets.
let dir = tempdir().unwrap();
std::fs::write(
dir.path().join("configure.ac"),
"AC_INIT([My Tool], [2.0])\n",
)
.unwrap();
let probe = template.probe(dir.path()).expect("probe result");
assert_eq!(probe.name.as_deref(), Some("My Tool"));
// No configure.ac: silent None.
let dir = tempdir().unwrap();
assert!(template.probe(dir.path()).is_none());
}
#[test]
fn gettext_detected_in_configure_ac() {
// Skeleton: the generated configure.ac has no gettext.
let o = opts();
assert!(!uses_gettext(&o));
assert!(
!super::super::get(TemplateId::Autotools)
.unwrap()
.build_depends(&o)
.contains(&"gettext".to_string())
);
// Existing tree with AM_GNU_GETTEXT: gettext joins Build-Depends.
let dir = tempdir().unwrap();
std::fs::write(
dir.path().join("configure.ac"),
"AC_INIT([mytool], [0.1.0])\nAM_GNU_GETTEXT([external])\n",
)
.unwrap();
let o = NewOptions {
source_dir: SourceDir::Path(dir.path().to_path_buf()),
..opts()
};
assert!(uses_gettext(&o));
assert!(
super::super::get(TemplateId::Autotools)
.unwrap()
.build_depends(&o)
.contains(&"gettext".to_string())
);
}
}