215 lines
7.8 KiB
Rust
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}");
|
|
}
|
|
}
|