Files
pkh/src/new/verify.rs
T

215 lines
7.8 KiB
Rust

//! Structural self-checks of a freshly scaffolded package tree (step 1 of
//! the spec's "try very hard" verification): every check is cheap, local
//! and re-parses the generated files with the same parsers the build
//! pipeline uses, so `pkh build` failures are caught at generation time
//! when they come from a pkh bug rather than from the tree.
use std::path::Path;
use crate::debian::{ControlInfo, DebianVersion};
/// Verify the structural sanity of the scaffolded tree at `tree`:
///
/// - `debian/rules` exists and carries the executable bit,
/// - `debian/control` parses as deb822 (source stanza + binary stanza),
/// - `debian/changelog` parses,
/// - `debian/source/format` is one of the known values,
/// - for quilt packages, the orig tarball exists next to the tree.
pub fn verify(tree: &Path) -> Result<(), Box<dyn std::error::Error>> {
use std::os::unix::fs::PermissionsExt;
// debian/rules: present + executable.
let rules_path = tree.join("debian/rules");
let rules_mode = std::fs::metadata(&rules_path)
.map_err(|e| format!("'{}' is missing: {e}", rules_path.display()))?
.permissions()
.mode();
if rules_mode & 0o111 == 0 {
return Err(format!(
"'{}' is not executable (pkh's build fixtures require the exec bit)",
rules_path.display()
)
.into());
}
// debian/control: re-parse with the real parser.
let control_path = tree.join("debian/control");
let control = ControlInfo::parse(&control_path)
.map_err(|e| format!("Generated '{}' does not parse: {e}", control_path.display()))?;
if control.binaries.is_empty() {
return Err(format!(
"Generated '{}' has no binary package stanza",
control_path.display()
)
.into());
}
// debian/changelog: re-parse with the real parser.
let changelog_path = tree.join("debian/changelog");
let (source, version, _distribution) =
crate::changelog::parse_changelog_header(&changelog_path).map_err(|e| {
format!(
"Generated '{}' does not parse: {e}",
changelog_path.display()
)
})?;
let parsed_version = DebianVersion::parse(&version)
.map_err(|e| format!("Generated changelog version '{version}' is invalid: {e}"))?;
// debian/source/format: one of the three known values.
let format_path = tree.join("debian/source/format");
let format = std::fs::read_to_string(&format_path)
.map_err(|e| format!("'{}' is missing: {e}", format_path.display()))?;
let format = format.trim();
if !super::debian::KNOWN_SOURCE_FORMATS.contains(&format) {
return Err(format!(
"'{}' carries the unknown source format '{format}'",
format_path.display()
)
.into());
}
// Quilt packages need their orig tarball next to the tree.
if format == super::debian::SOURCE_FORMAT_QUILT {
let uversion = parsed_version.upstream;
let tarball =
super::debian::orig_tarball_path(tree, &source, &uversion).ok_or_else(|| {
format!(
"cannot determine the parent directory of '{}'",
tree.display()
)
})?;
if !tarball.exists() {
return Err(format!(
"Quilt package without orig tarball: '{}' is missing. \
Re-run pkh new, or pass --native.",
tarball.display()
)
.into());
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::new::options::{License, NewOptions, SourceDir, TemplateId};
use tempfile::tempdir;
fn opts() -> NewOptions {
NewOptions {
name: "mytool".into(),
template: TemplateId::Shell,
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: "debian".into(),
series: "sid".into(),
release: false,
depends: Vec::new(),
native: false,
git: false,
}
}
/// Scaffold a complete tree with the given options (without git, so no
/// fixture below depends on the git binary).
fn scaffold_tree(dir: &Path, opts: &NewOptions) -> std::path::PathBuf {
let template = crate::new::templates::get(opts.template).unwrap();
let tree = dir.join("tree");
std::fs::create_dir_all(&tree).unwrap();
let mut files = crate::new::debian::files(opts, template);
files.extend(template.skeleton(opts));
files.extend(template.debian(opts));
crate::new::debian::write_files(&tree, &files).unwrap();
if !opts.native {
crate::new::debian::create_orig_tarball(&tree, &opts.name, &opts.upstream_version)
.unwrap();
}
tree
}
#[test]
fn verify_accepts_a_good_tree() {
let dir = tempdir().unwrap();
let tree = scaffold_tree(dir.path(), &opts());
verify(&tree).unwrap();
}
#[test]
fn verify_accepts_native_trees_without_tarball() {
let dir = tempdir().unwrap();
let tree = scaffold_tree(
dir.path(),
&NewOptions {
native: true,
..opts()
},
);
verify(&tree).unwrap();
}
#[test]
fn verify_names_the_broken_file() {
// Each case needs its own tempdir: scaffolding refuses to overwrite
// an existing orig tarball.
let o = opts();
// Missing rules.
let dir = tempdir().unwrap();
let tree = scaffold_tree(dir.path(), &o);
std::fs::remove_file(tree.join("debian/rules")).unwrap();
let err = verify(&tree).unwrap_err().to_string();
assert!(err.contains("debian/rules"), "{err}");
// Non-executable rules.
let dir = tempdir().unwrap();
let tree = scaffold_tree(dir.path(), &o);
let mut perms = std::fs::metadata(tree.join("debian/rules"))
.unwrap()
.permissions();
use std::os::unix::fs::PermissionsExt;
perms.set_mode(0o644);
std::fs::set_permissions(tree.join("debian/rules"), perms).unwrap();
let err = verify(&tree).unwrap_err().to_string();
assert!(err.contains("not executable"), "{err}");
// Broken control.
let dir = tempdir().unwrap();
let tree = scaffold_tree(dir.path(), &o);
std::fs::write(tree.join("debian/control"), "not: a\ncontrol\n").unwrap();
let err = verify(&tree).unwrap_err().to_string();
assert!(err.contains("debian/control"), "{err}");
// Broken changelog.
let dir = tempdir().unwrap();
let tree = scaffold_tree(dir.path(), &o);
std::fs::write(tree.join("debian/changelog"), "garbage\n").unwrap();
let err = verify(&tree).unwrap_err().to_string();
assert!(err.contains("debian/changelog"), "{err}");
// Unknown source format.
let dir = tempdir().unwrap();
let tree = scaffold_tree(dir.path(), &o);
std::fs::write(tree.join("debian/source/format"), "42.0 (quilt)\n").unwrap();
let err = verify(&tree).unwrap_err().to_string();
assert!(err.contains("debian/source/format"), "{err}");
// Missing orig tarball.
let dir = tempdir().unwrap();
let tree = scaffold_tree(dir.path(), &o);
std::fs::remove_file(dir.path().join("mytool_0.1.0.orig.tar.xz")).unwrap();
let err = verify(&tree).unwrap_err().to_string();
assert!(err.contains("orig tarball"), "{err}");
}
}