Files
pkh/src/new/templates/makefile.rs
T
vhaudiquet ffac4d6b57 new: port the shell, empty, makefile and go templates to manifests
Move the four templates' static file bodies into .tpl files under
data/templates/<id>/, referenced by their manifests' files: lists —
the shell skeleton script (executable, {command}-named) with its
skeleton-only debian/install mapping, the empty template's stub README,
the makefile hello.c/Makefile skeleton with its skeleton-only install
mapping, and go's go.mod/main.go skeleton (the go directive of go.mod
stays a literal: nothing about it is answer-derived).

The empty template ends up hookless — zero Rust, its registry entry
points at no hooks — and src/new/templates/empty.rs is deleted. The
shell and go hooks shrink to their probes (plus go's {go_import_path}
context value); the makefile hooks keep only the existing-tree hint
probing the packaged Makefile for a phony install: target, since that
heuristic reads the tree and cannot be data.
2026-09-18 14:58:13 +02:00

180 lines
6.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. Everything is manifest data
//! (`data/templates/makefile/manifest.yml`: the `hello.c`/`Makefile`
//! skeleton and the skeleton-only `debian/install` mapping) except the
//! hint logged here for an existing tree, which probes its Makefile for a
//! phony `install:` target to tell whether `dh_auto_install` will run
//! `make install` (a skeleton's target is known by construction, so
//! skeletons need no code).
use std::path::Path;
use super::TemplateHooks;
use crate::new::options::{NewOptions, SourceDir};
/// The logic half of the makefile template.
pub struct Hooks;
/// The makefile template's hooks, registered in the template registry.
pub static HOOKS: Hooks = Hooks;
impl TemplateHooks for Hooks {
/// No files of its own: the manifest carries the skeleton-only
/// `debian/install` mapping. When packaging an existing tree, probe
/// its Makefile for a phony `install:` target and say which install
/// step `dh_auto_install` will take (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_files(&self, opts: &NewOptions) -> Vec<super::OutputFile> {
if matches!(opts.source_dir, SourceDir::Skeleton) {
return Vec::new();
}
let dir = super::source_dir_of(opts);
if let Some(dir) = dir.as_deref()
&& 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()
}
}
/// 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, TemplateId};
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(),
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 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 (manifest data): hello.c + Makefile with all/install/clean
// targets, 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 (the 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());
}
}