//! 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 { 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 { Vec::new() } fn build_depends(&self, opts: &NewOptions) -> Vec { 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 { let content = std::fs::read_to_string(dir.join("configure.ac")).ok()?; static AC_INIT_REGEX: std::sync::OnceLock = 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()) ); } }