219 lines
7.8 KiB
Rust
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());
|
|
}
|
|
}
|