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

219 lines
7.8 KiB
Rust

//! The `makefile` template: a generic project driven by a plain `Makefile`.
//!
//! debhelper's makefile buildsystem runs `make` for the build and
//! `make install DESTDIR=...` when the Makefile carries an `install:` target
//! (missing targets are skipped gracefully), so plain `dh $@` plumbing is
//! enough here.
use std::path::Path;
use super::{OutputFile, Template, source_dir_of};
use crate::new::options::{NewOptions, SourceDir, TemplateId};
/// Generic Makefile-based project.
pub struct Makefile;
impl Template for Makefile {
fn id(&self) -> TemplateId {
TemplateId::Makefile
}
/// A `hello.c` plus a `Makefile` with `all`/`install`/`clean` targets;
/// `install` honors `DESTDIR` and copies the binary to
/// `$(DESTDIR)/usr/bin/`.
fn skeleton(&self, opts: &NewOptions) -> Vec<OutputFile> {
vec![
super::meson::hello_c(opts),
OutputFile::new(
"Makefile",
format!(
"CC ?= cc\n\
CFLAGS ?= -O2 -Wall -Wextra\n\
PREFIX ?= /usr\n\
\n\
all: {command}\n\
\n\
{command}: hello.c\n\
\t$(CC) $(CFLAGS) -o $@ hello.c\n\
\n\
install: {command}\n\
\tinstall -Dm755 {command} $(DESTDIR)$(PREFIX)/bin/{command}\n\
\n\
clean:\n\
\trm -f {command}\n\
\n\
.PHONY: all install clean\n",
command = opts.command,
),
),
]
}
/// `debian/install` mapping the built binary into `/usr/bin`, generated
/// only when the packaged Makefile carries a phony `install:` target:
/// for skeletons that is known by construction; when packaging an
/// existing tree the Makefile is probed instead (no `debian/install` is
/// emitted there — the source-relative mapping of an unknown artifact is
/// only the project's to write, and `make install` already ran).
fn debian(&self, opts: &NewOptions) -> Vec<OutputFile> {
match &opts.source_dir {
SourceDir::Skeleton => {
vec![OutputFile::new(
"debian/install",
format!("{} usr/bin/{}\n", opts.command, opts.command),
)]
}
_ => {
if let Some(dir) = source_dir_of(opts)
&& phony_install_target(&dir.join("Makefile")).is_some()
{
log::info!(
"Makefile carries a phony 'install:' target: \
dh_auto_install will run 'make install DESTDIR=...'"
);
} else {
log::info!(
"Makefile has no phony 'install:' target: \
dh_auto_install will skip the install step; write a \
debian/install file to map build artifacts manually"
);
}
Vec::new()
}
}
}
fn build_depends(&self, _opts: &NewOptions) -> Vec<String> {
vec!["build-essential".to_string()]
}
fn architecture(&self, _opts: &NewOptions) -> &'static str {
"any"
}
}
/// The name of the phony `install:` target of the Makefile at `path`, when
/// there is one: an unindented `install:` rule whose name also appears in a
/// `.PHONY:` declaration. `None` when the file is missing or carries no such
/// target. (A deliberately minimal line-oriented heuristic, like the other
/// project-file readers of the templates.)
pub fn phony_install_target(path: &Path) -> Option<String> {
let content = std::fs::read_to_string(path).ok()?;
let mut has_install_rule = false;
let mut phony_mentions_install = false;
for line in content.lines() {
if let Some(phony) = line.strip_prefix(".PHONY:")
&& phony.split_whitespace().any(|target| target == "install")
{
phony_mentions_install = true;
}
// Target rules start at column 0; recipes are indented, and special
// targets, comments and directives are excluded by the prefix check.
if !line.starts_with(['\t', ' ', '.', '#']) && line.starts_with("install:") {
has_install_rule = true;
}
}
(has_install_rule && phony_mentions_install).then(|| "install".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::new::options::{License, SourceDir};
use tempfile::tempdir;
fn opts(source_dir: SourceDir) -> NewOptions {
NewOptions {
name: "mytool".into(),
template: TemplateId::Makefile,
source_dir,
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(),
native: false,
git: true,
autopkgtest: false,
pkg_config: false,
watch: None,
}
}
#[test]
fn makefile_template_shape() {
let o = opts(SourceDir::Skeleton);
let template = super::super::get(TemplateId::Makefile).unwrap();
assert_eq!(template.architecture(&o), "any");
assert_eq!(
template.build_depends(&o),
vec!["build-essential".to_string()]
);
assert_eq!(template.rules_dh_line(), "dh $@");
assert!(template.rules_extra(&o).is_empty());
// Skeleton: hello.c + Makefile with all/install/clean, and the
// phony install target maps to debian/install.
let skeleton = template.skeleton(&o);
assert!(skeleton.iter().any(|f| f.path == "hello.c"));
let makefile = skeleton
.iter()
.find(|f| f.path == "Makefile")
.expect("Makefile skeleton");
assert!(makefile.contents.contains("all: mytool\n"));
assert!(
makefile
.contents
.contains("install -Dm755 mytool $(DESTDIR)$(PREFIX)/bin/mytool")
);
assert!(makefile.contents.contains(".PHONY: all install clean"));
assert!(
makefile
.contents
.contains("\t$(CC) $(CFLAGS) -o $@ hello.c")
);
let debian = template.debian(&o);
assert_eq!(debian.len(), 1);
assert_eq!(debian[0].path, "debian/install");
assert_eq!(debian[0].contents, "mytool usr/bin/mytool\n");
// Existing tree: nothing is emitted (probe log only).
let o = opts(SourceDir::Here);
assert!(template.debian(&o).is_empty());
}
#[test]
fn phony_install_target_detection() {
let dir = tempdir().unwrap();
let path = dir.path().join("Makefile");
// Phony install target: detected.
std::fs::write(
&path,
"all:\n\t@echo\n\n.PHONY: all install clean\ninstall:\n\t@echo install\n",
)
.unwrap();
assert!(phony_install_target(&path).is_some());
// install: without .PHONY: not detected.
std::fs::write(&path, "all:\n\t@echo\ninstall:\n\t@echo install\n").unwrap();
assert!(phony_install_target(&path).is_none());
// .PHONY mentioning install but no install: rule: not detected.
std::fs::write(&path, ".PHONY: install\nall:\n\t@echo\n").unwrap();
assert!(phony_install_target(&path).is_none());
// Missing file.
assert!(phony_install_target(&dir.path().join("missing.mk")).is_none());
}
}