mirror of
https://github.com/xcat2/xcat-dep.git
synced 2026-09-12 12:36:23 +00:00
fix(xcat-dep): merge master into the EL matrix build after the Genesis release landed
PR #64 landed on master and rewrote the same regions of mockbuild-all.pl this branch rewrites: it moved the shared helpers into lib/XCAT/BuildUtils.pm (sh_quote -> shell_quote, print_step, require_command, run_command), added the --genesis-release publication path to deploy_target, and added assert_build_progress. Left unmerged, this branch cannot land, and merging it naively would drop the Genesis release packages from the deployed cell. Resolution: - Imports: MockBuildUtils (this branch) and XCAT::BuildUtils / XCAT::GenesisRelease (master) coexist. print_step is imported only from MockBuildUtils -- the two implementations are identical, and importing both would redefine it. The local require_command is dropped in favour of the imported one. - deploy_target: master's --genesis-release steps (remove_genesis_packages and verify_genesis_release_packages) now run inside this branch's staged, atomic cell, before sign_and_index_repo, because rpmsign rewrites the very bytes the release checksums cover. Files are staged with master's mode-preserving publish_file. - assert_build_progress is kept, and is called from both the serial and the parallel path before the failed step ids are returned. The zero-tolerance manifest gate (verify_target_repo) still runs afterwards, so a run whose builders all failed now says exactly that instead of naming missing packages. - assert_required_deps and have_rpm are gone: verify_target_repo, which asserts the manifest, the pinned versions and every rpm signature, replaces them. t/genesis_openembedded_consumer.t needed three adaptations to run against this branch at all, none of them optional: - Its runs use a synthetic target, and this branch builds and gates every target against <repo-root>/packages-manifest.conf, where a target with no section is fatal. Each run now writes that section into its scratch repo root. - They passed --skip-xcat, which this branch removed (Getopt::Long silently prefix-matched it to --skip-xcat-dep) and which was what let buildrpms.pl be absent. They pass --skip-genesis instead: that is the only step needing it now. - --skip-build collects from this target's own build-results tree here, where it used to collect from the legacy build-output/list* directories. The "a run that built nothing" fixture had left an rpm in build-results, which is now genuinely collected, so that case leaves the tree empty and a new case covers --skip-build keeping (and collecting) the results it reads. Full suite green as root: 238 tests on xcat-master (rome01, EL10) and 244 on xcat-master-ub, which additionally runs the APT consumer cases. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
name: Genesis OpenEmbedded packages
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
package-tests:
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
XCAT_GENESIS_CI: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install build tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
apt-utils createrepo-c dpkg-dev gnupg \
|
||||
libfile-slurper-perl \
|
||||
libparallel-forkmanager-perl libperl-critic-perl rpm shellcheck
|
||||
|
||||
- name: Run static checks
|
||||
run: |
|
||||
perl -c genesis-openembedded/build
|
||||
perl -c genesis-openembedded/package
|
||||
perl -c genesis-openembedded/verify-release
|
||||
perl -Ilib -c lib/XCAT/BuildUtils.pm
|
||||
perl -Ilib -c lib/XCAT/GenesisRelease.pm
|
||||
perl -c mockbuild-all.pl
|
||||
rpmspec -P \
|
||||
-D 'genesis_arch x86_64' \
|
||||
-D 'version 2.19.0' \
|
||||
-D 'release snap202608210726' \
|
||||
genesis-openembedded/rpm/xCAT-genesis-openembedded.spec >/dev/null
|
||||
perlcritic \
|
||||
genesis-openembedded/build \
|
||||
genesis-openembedded/package \
|
||||
genesis-openembedded/verify-release \
|
||||
lib/XCAT/BuildUtils.pm \
|
||||
lib/XCAT/GenesisRelease.pm \
|
||||
mockbuild-all.pl \
|
||||
t/build_utils.t \
|
||||
t/genesis_openembedded_release.t \
|
||||
t/genesis_openembedded_consumer.t \
|
||||
t/lib/XCAT/GenesisReleaseTest.pm
|
||||
bash -n build-apt-repo.sh
|
||||
shellcheck build-apt-repo.sh
|
||||
|
||||
- name: Run package tests
|
||||
run: |
|
||||
prove -v t/build_utils.t
|
||||
prove -v -It/lib t/genesis_openembedded_release.t
|
||||
sudo -E prove -v -It/lib t/genesis_openembedded_consumer.t
|
||||
@@ -1,3 +1,4 @@
|
||||
Gitepoch
|
||||
Gitinfo
|
||||
Release
|
||||
build-output/
|
||||
|
||||
@@ -112,6 +112,11 @@ Use these flags to skip specific operations:
|
||||
- Skips `createrepo --update`.
|
||||
- `--skip-tarball`
|
||||
- Skips tarball creation for both binary and SRPM repos.
|
||||
- `--skip-genesis`
|
||||
- Skips the existing per-EL Genesis image build.
|
||||
- `--genesis-release <PATH>`
|
||||
- Adds a verified OpenEmbedded Genesis RPM release alongside the existing
|
||||
per-EL Genesis packages.
|
||||
- `--scrub-all-chroots`
|
||||
- Runs `mock -r <TARGET> --scrub=all` before build and collection.
|
||||
- `--keep-buildroots`
|
||||
@@ -138,7 +143,9 @@ Use these flags to skip specific operations:
|
||||
Install baseline tooling:
|
||||
|
||||
```bash
|
||||
dnf -y install perl perl-Parallel-ForkManager mock createrepo tar rpm-build rpmdevtools dnf-plugins-core wget git
|
||||
dnf -y install perl perl-File-Slurper perl-IPC-Cmd \
|
||||
perl-Parallel-ForkManager mock createrepo tar rpm-build rpmdevtools \
|
||||
dnf-plugins-core wget git
|
||||
```
|
||||
|
||||
If you will build the `xCAT-genesis-base` package (that is, you will **not** use `--skip-genesis`), install xCAT build dependencies:
|
||||
@@ -202,6 +209,39 @@ Notes:
|
||||
`<XCAT_SOURCE>/buildrpms.pl`).
|
||||
- `<RUN_ID>` is optional; when omitted it is timestamp-based.
|
||||
|
||||
# Add an OpenEmbedded Genesis Release
|
||||
|
||||
Build the Genesis package release first, following
|
||||
[`genesis-openembedded/README.md`](genesis-openembedded/README.md). Then pass the
|
||||
result to the regular repository build:
|
||||
|
||||
```bash
|
||||
cd <REPO_ROOT>
|
||||
perl ./mockbuild-all.pl \
|
||||
--repo-root <REPO_ROOT> \
|
||||
--xcat-source <XCAT_SOURCE> \
|
||||
--genesis-release /path/to/xcat-genesis-release
|
||||
```
|
||||
|
||||
The release is checked before any package is collected. Its RPMs and SRPMs are
|
||||
copied into each generated EL repository. Stale OpenEmbedded Genesis packages
|
||||
are removed from the output first, while the existing per-EL Genesis package
|
||||
remains available.
|
||||
|
||||
Repository publication requires a release containing every supported Genesis
|
||||
architecture. The packages are `noarch`, and every management-node repository
|
||||
receives the full set of target images.
|
||||
|
||||
The release checksums cover the unsigned input packages. If repository signing
|
||||
is enabled, `rpmsign` changes the deployed RPM bytes after collection.
|
||||
|
||||
The OpenEmbedded packages use their own names and install under
|
||||
`/opt/xcat/share/xcat/netboot/genesis-openembedded/`. Publishing them does not
|
||||
replace the Genesis packages used by current xcat-core releases. Activation is
|
||||
a separate xcat-core change.
|
||||
|
||||
Omit `--genesis-release` to keep using the existing Genesis builder.
|
||||
|
||||
# Common Build Modes
|
||||
|
||||
xcat-dep repo:
|
||||
|
||||
+125
-2
@@ -4,7 +4,8 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
if [ -z "${SOURCE_DATE_EPOCH:-}" ] && [ -f "$SCRIPT_DIR/Gitepoch" ]; then
|
||||
export SOURCE_DATE_EPOCH=$(cat "$SCRIPT_DIR/Gitepoch")
|
||||
SOURCE_DATE_EPOCH=$(cat "$SCRIPT_DIR/Gitepoch")
|
||||
export SOURCE_DATE_EPOCH
|
||||
fi
|
||||
|
||||
REPO_ROOT="$SCRIPT_DIR"
|
||||
@@ -12,6 +13,22 @@ APT_DIR=""
|
||||
GPG_KEY_ID="xcat@megware.com"
|
||||
SKIP_SIGN=0
|
||||
DRY_RUN=0
|
||||
GENESIS_RELEASE=""
|
||||
GENESIS_CHECKSUMS=""
|
||||
GENESIS_VERIFIER=""
|
||||
FORCE_UNLOCK=0
|
||||
HELD_LOCK=""
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "$GENESIS_CHECKSUMS" ]]; then
|
||||
rm -f -- "$GENESIS_CHECKSUMS"
|
||||
fi
|
||||
if [[ -n "$HELD_LOCK" ]]; then
|
||||
rm -f -- "$HELD_LOCK/owner"
|
||||
rmdir -- "$HELD_LOCK" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
declare -A CODENAME_MAP=(
|
||||
[ubuntu22.04]=jammy
|
||||
@@ -44,7 +61,9 @@ Options:
|
||||
--apt-dir PATH APT output directory (default: <repo-root>/repos/apt)
|
||||
--gpg-key-id ID GPG key ID for signing (default: xcat@megware.com)
|
||||
--skip-sign Skip GPG signing (for testing)
|
||||
--genesis-release PATH Add a verified OpenEmbedded Genesis DEB release to each selected suite
|
||||
--dry-run Print planned actions without executing
|
||||
--force-unlock Remove a stale <apt-dir>/.lock before acquiring it
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
@@ -70,7 +89,9 @@ while [[ $# -gt 0 ]]; do
|
||||
--apt-dir) APT_DIR="$2"; shift 2 ;;
|
||||
--gpg-key-id) GPG_KEY_ID="$2"; shift 2 ;;
|
||||
--skip-sign) SKIP_SIGN=1; shift ;;
|
||||
--genesis-release) GENESIS_RELEASE="$2"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--force-unlock) FORCE_UNLOCK=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-*) die "Unknown option: $1" ;;
|
||||
*) SELECTED_VERS+=("$1"); SUBSET=1; shift ;;
|
||||
@@ -80,6 +101,37 @@ done
|
||||
REPO_ROOT="$(cd "$REPO_ROOT" && pwd)"
|
||||
APT_DIR="${APT_DIR:-$REPO_ROOT/repos/apt}"
|
||||
|
||||
# Everything from the pool wipe to the signature is one transaction over a shared tree, and
|
||||
# the packages are verified inside it: a second writer between the verification and
|
||||
# apt-ftparchive would be indexed and signed unchecked. Hold the tree for the whole run, with
|
||||
# an atomic mkdir (reliable over NFS, unlike flock) like mockbuild-all.pl does for its output.
|
||||
acquire_apt_lock() {
|
||||
local lock="$APT_DIR/.lock"
|
||||
[[ $DRY_RUN -eq 0 ]] || return 0
|
||||
mkdir -p -- "$APT_DIR"
|
||||
if [[ $FORCE_UNLOCK -eq 1 && -d "$lock" ]]; then
|
||||
echo "force-unlock: removing stale lock $lock"
|
||||
rm -f -- "$lock/owner"
|
||||
rmdir -- "$lock" 2>/dev/null || true
|
||||
fi
|
||||
if mkdir -- "$lock" 2>/dev/null; then
|
||||
HELD_LOCK="$lock"
|
||||
printf 'host=%s\npid=%s\nepoch=%s\n' "$(uname -n)" "$$" "$(date +%s)" > "$lock/owner"
|
||||
return 0
|
||||
fi
|
||||
[[ -d "$lock" ]] || die "Cannot create lock $lock"
|
||||
die "APT directory $APT_DIR is locked ($lock): $(tr '\n' ' ' < "$lock/owner" 2>/dev/null)
|
||||
another build-apt-repo.sh run owns it; use a different --apt-dir or --force-unlock if stale."
|
||||
}
|
||||
acquire_apt_lock
|
||||
|
||||
if [[ -n "$GENESIS_RELEASE" ]]; then
|
||||
[[ -d "$GENESIS_RELEASE" ]] || die "Genesis release directory not found: $GENESIS_RELEASE"
|
||||
GENESIS_RELEASE="$(cd "$GENESIS_RELEASE" && pwd)"
|
||||
GENESIS_VERIFIER="$SCRIPT_DIR/genesis-openembedded/verify-release"
|
||||
[[ -x "$GENESIS_VERIFIER" ]] || die "Genesis release verifier not found: $GENESIS_VERIFIER"
|
||||
fi
|
||||
|
||||
# Default to all known versions when no DIST arg was given; otherwise validate
|
||||
# each requested version against CODENAME_MAP.
|
||||
if [[ ${#SELECTED_VERS[@]} -eq 0 ]]; then
|
||||
@@ -98,6 +150,18 @@ command -v apt-ftparchive >/dev/null 2>&1 \
|
||||
|| die "apt-ftparchive not found. Install: sudo apt-get install apt-utils"
|
||||
command -v gpg >/dev/null 2>&1 \
|
||||
|| die "gpg not found. Install: sudo apt-get install gnupg"
|
||||
if [[ -n "$GENESIS_RELEASE" ]]; then
|
||||
command -v dpkg-deb >/dev/null 2>&1 \
|
||||
|| die "dpkg-deb not found. Install: sudo apt-get install dpkg"
|
||||
command -v cmp >/dev/null 2>&1 \
|
||||
|| die "cmp not found. Install: sudo apt-get install diffutils"
|
||||
GENESIS_CHECKSUMS=$(mktemp "${TMPDIR:-/tmp}/xcat-genesis-checksums.XXXXXX")
|
||||
cp -- "$GENESIS_RELEASE/SHA256SUMS" "$GENESIS_CHECKSUMS"
|
||||
"$GENESIS_VERIFIER" --complete --format deb "$GENESIS_RELEASE"
|
||||
cmp -s "$GENESIS_CHECKSUMS" "$GENESIS_RELEASE/SHA256SUMS" \
|
||||
|| die "Genesis release changed during verification"
|
||||
echo "Genesis release: $GENESIS_RELEASE"
|
||||
fi
|
||||
|
||||
if [[ $SKIP_SIGN -eq 0 ]]; then
|
||||
if ! gpg --list-secret-keys "$GPG_KEY_ID" >/dev/null 2>&1; then
|
||||
@@ -144,6 +208,38 @@ done
|
||||
|
||||
step "Populating pool"
|
||||
|
||||
copy_deb() {
|
||||
local source="$1"
|
||||
local destination
|
||||
destination="$2/$(basename "$source")"
|
||||
if [[ -e "$destination" ]]; then
|
||||
cmp -s "$source" "$destination" \
|
||||
|| die "Package collision with different content: $destination"
|
||||
return
|
||||
fi
|
||||
ln "$source" "$destination" 2>/dev/null || cp "$source" "$destination"
|
||||
}
|
||||
|
||||
copy_genesis_deb() {
|
||||
local source="$1"
|
||||
local directory="$2"
|
||||
local name destination relative
|
||||
name=$(basename "$source")
|
||||
destination="$directory/$name"
|
||||
relative="deb/$name"
|
||||
# Every selected suite receives the whole release, so a plain copy spends hundreds of
|
||||
# megabytes per suite. --reflink=auto lets a filesystem that can share extents
|
||||
# copy-on-write avoid that, while still giving the pool a file of its own: a link would
|
||||
# leave the published package and the verified release sharing one inode, where a write
|
||||
# through either path changes what the other holds.
|
||||
cp --reflink=auto -- "$source" "$destination" 2>/dev/null \
|
||||
|| cp -- "$source" "$destination"
|
||||
"$GENESIS_VERIFIER" \
|
||||
--checksum-file "$GENESIS_CHECKSUMS" \
|
||||
--relative-file "$relative" \
|
||||
--copied-file "$destination"
|
||||
}
|
||||
|
||||
for ver in "${SELECTED_VERS[@]}"; do
|
||||
codename="${CODENAME_MAP[$ver]}"
|
||||
src="$APT_DIR/$ver"
|
||||
@@ -151,11 +247,38 @@ for ver in "${SELECTED_VERS[@]}"; do
|
||||
echo "$ver -> pool/main/$codename/"
|
||||
if [[ $DRY_RUN -eq 0 ]]; then
|
||||
for deb in "$src"/*.deb; do
|
||||
ln "$deb" "$dst/" 2>/dev/null || cp "$deb" "$dst/"
|
||||
if [[ -n "$GENESIS_RELEASE" && ${deb##*/} == xcat-genesis-openembedded-*.deb ]]; then
|
||||
continue
|
||||
fi
|
||||
copy_deb "$deb" "$dst"
|
||||
done
|
||||
if [[ -n "$GENESIS_RELEASE" ]]; then
|
||||
for deb in "$GENESIS_RELEASE"/deb/*.deb; do
|
||||
copy_genesis_deb "$deb" "$dst"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# The pool is indexed and the metadata is signed from what is on disk now, not from what
|
||||
# was copied earlier, so check the packages again here: anything that changed between the
|
||||
# copy and this point would otherwise be published and signed as verified.
|
||||
if [[ -n "$GENESIS_RELEASE" && $DRY_RUN -eq 0 ]]; then
|
||||
step "Re-verifying pooled Genesis packages"
|
||||
for ver in "${SELECTED_VERS[@]}"; do
|
||||
codename="${CODENAME_MAP[$ver]}"
|
||||
for deb in "$GENESIS_RELEASE"/deb/*.deb; do
|
||||
name=$(basename "$deb")
|
||||
pooled="$APT_DIR/pool/main/$codename/$name"
|
||||
"$GENESIS_VERIFIER" \
|
||||
--checksum-file "$GENESIS_CHECKSUMS" \
|
||||
--relative-file "deb/$name" \
|
||||
--copied-file "$pooled"
|
||||
echo "Re-verified pooled Genesis package: $pooled"
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
step "Generating Packages indexes"
|
||||
|
||||
for ver in "${SELECTED_VERS[@]}"; do
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# OpenEmbedded Genesis packages
|
||||
|
||||
xCAT builds the Genesis image in `xcat-core`. This directory turns those build
|
||||
outputs into packages and feeds them into the xcat-dep repositories.
|
||||
|
||||
Run the release builder on a host that can build the OpenEmbedded layer. The
|
||||
xcat-core checkout must be clean. `--xcat-ref` checks that the checkout points
|
||||
to the intended commit; it does not change the checkout for you.
|
||||
|
||||
The packaging scripts use `File::Slurper` and `IPC::Cmd`. Install
|
||||
`libfile-slurper-perl` on Ubuntu. On EL, install `perl-File-Slurper` and
|
||||
`perl-IPC-Cmd` from EPEL and AppStream.
|
||||
|
||||
```bash
|
||||
./genesis-openembedded/build \
|
||||
--xcat-source /path/to/xcat-core \
|
||||
--xcat-ref <tag-or-commit> \
|
||||
--all \
|
||||
--work-dir /path/to/oe-work \
|
||||
--output-dir /path/to/xcat-genesis-release
|
||||
```
|
||||
|
||||
The default format is `all`, which produces RPM, SRPM, and DEB packages. Use
|
||||
`--format rpm` or `--format deb` when only one package family is needed. The
|
||||
supported image architectures are `x86`, `x86_64`, `ppc64`, `ppc64le`,
|
||||
`armv7hf`, `aarch64`, and `riscv64`.
|
||||
|
||||
Use `--architecture` for development builds. Repository publication requires a
|
||||
complete release built with `--all`.
|
||||
|
||||
Each package installs one exact-architecture export under
|
||||
`/opt/xcat/share/xcat/netboot/genesis-openembedded/<architecture>/`. The package
|
||||
and install namespaces are separate from the old Genesis packages, so both
|
||||
generations can be published and installed without replacing one another. The
|
||||
packages are `noarch` or `all` because they are installed on the management
|
||||
node, not run on the target node.
|
||||
|
||||
`--work-dir` keeps the OpenEmbedded downloads and build state between release
|
||||
builds. Without it, the builder uses a temporary directory and removes it when
|
||||
the command finishes.
|
||||
|
||||
The release directory contains:
|
||||
|
||||
- `rpm/`, `srpm/`, and `deb/` package directories
|
||||
- `release.manifest`, including the xcat-core commit
|
||||
- `SHA256SUMS`
|
||||
|
||||
Validate the directory before publishing it:
|
||||
|
||||
```bash
|
||||
./genesis-openembedded/verify-release --complete /path/to/xcat-genesis-release
|
||||
```
|
||||
|
||||
The checksum file detects incomplete or changed output. It does not authenticate
|
||||
the release, so only accept a directory produced by a trusted build host.
|
||||
|
||||
`verify-release` also checks the identity of each package, including a fixed
|
||||
build host and a build time taken from the source epoch. Those are reproduced
|
||||
by rpm 4.14.3, 4.16.1.3, 4.19.1.1 and 6.0.2, so an EL8 or later builder -- and
|
||||
a current Fedora one -- produces a release the verifier accepts.
|
||||
|
||||
Pass that same directory to the repository builders:
|
||||
|
||||
```bash
|
||||
perl ./mockbuild-all.pl \
|
||||
--genesis-release /path/to/xcat-genesis-release \
|
||||
[other build options]
|
||||
|
||||
./build-apt-repo.sh \
|
||||
--genesis-release /path/to/xcat-genesis-release \
|
||||
[DIST ...]
|
||||
```
|
||||
|
||||
The RPM builder keeps its old per-EL Genesis build and adds the new packages.
|
||||
The APT builder does the same for each selected suite. Both consumers require
|
||||
all seven architectures and verify package identities and checksums before
|
||||
collecting packages. Every management-node repository receives every target
|
||||
image so it can provision nodes of another architecture.
|
||||
|
||||
Without `--genesis-release`, both builders keep their existing behavior. The
|
||||
new packages do not provide, replace, or obsolete the old package names. A
|
||||
separate xcat-core change will select the OpenEmbedded package and install
|
||||
namespace after the repositories carry it. Generated images and packages
|
||||
belong in release storage, not in Git.
|
||||
|
||||
Run the package tests on a Linux builder with RPM, DEB, and repository tools:
|
||||
|
||||
```bash
|
||||
prove t/build_utils.t
|
||||
prove -It/lib t/genesis_openembedded_release.t
|
||||
sudo -E prove -It/lib t/genesis_openembedded_consumer.t
|
||||
```
|
||||
Executable
+205
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Cwd qw(abs_path);
|
||||
use File::Basename qw(basename dirname);
|
||||
use File::Copy qw(copy);
|
||||
use File::Path qw(make_path);
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin;
|
||||
use Getopt::Long qw(GetOptions);
|
||||
use POSIX qw(strftime);
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use XCAT::BuildUtils qw(
|
||||
capture_command
|
||||
digest_manifest
|
||||
read_first_line
|
||||
relative_files
|
||||
run_command
|
||||
write_binary
|
||||
);
|
||||
use XCAT::GenesisRelease qw(
|
||||
architectures
|
||||
deb_package_name
|
||||
rpm_package_name
|
||||
validate_architecture
|
||||
validate_release
|
||||
);
|
||||
|
||||
my $repo_root = abs_path("$FindBin::Bin/..");
|
||||
my $xcat_source = "$repo_root/../xcat-core";
|
||||
my $xcat_ref = '';
|
||||
my $output_dir = '';
|
||||
my $work_dir = '';
|
||||
my $format = 'all';
|
||||
my $all = 0;
|
||||
my @requested_architectures;
|
||||
|
||||
GetOptions(
|
||||
'xcat-source=s' => \$xcat_source,
|
||||
'xcat-ref=s' => \$xcat_ref,
|
||||
'output-dir=s' => \$output_dir,
|
||||
'work-dir=s' => \$work_dir,
|
||||
'format=s' => \$format,
|
||||
'architecture=s@' => \@requested_architectures,
|
||||
'all!' => \$all,
|
||||
) or die usage();
|
||||
|
||||
die "Unsupported package format: $format\n"
|
||||
unless $format eq 'all' || $format eq 'rpm' || $format eq 'deb';
|
||||
die "Use either --all or --architecture\n" if $all && @requested_architectures;
|
||||
@requested_architectures = architectures() if $all;
|
||||
@requested_architectures = ('x86_64') unless @requested_architectures;
|
||||
|
||||
my %seen;
|
||||
@requested_architectures = grep { !$seen{$_}++ } @requested_architectures;
|
||||
validate_architecture($_) for @requested_architectures;
|
||||
my %requested = map { $_ => 1 } @requested_architectures;
|
||||
@requested_architectures = grep { $requested{$_} } architectures();
|
||||
umask(0022);
|
||||
|
||||
$xcat_source = abs_path($xcat_source) or die "Cannot resolve xcat-core source\n";
|
||||
for my $path (qw(Version xCAT-genesis-builder/oe/build xCAT-genesis-builder/oe/export)) {
|
||||
die "xcat-core source is missing $path\n" unless -f "$xcat_source/$path";
|
||||
}
|
||||
die "xcat-core checkout is not clean\n"
|
||||
if capture_command('git', '-C', $xcat_source, 'status', '--porcelain') ne '';
|
||||
|
||||
my $revision = capture_command('git', '-C', $xcat_source, 'rev-parse', 'HEAD');
|
||||
die "Invalid xcat-core revision: $revision\n" unless $revision =~ /^[0-9a-f]{40}$/;
|
||||
if ($xcat_ref ne '') {
|
||||
die "Invalid xcat-core ref: $xcat_ref\n" unless $xcat_ref =~ /^[A-Za-z0-9][A-Za-z0-9._\/-]*$/;
|
||||
my $expected = capture_command(
|
||||
'git', '-C', $xcat_source, 'rev-parse', '--verify', "$xcat_ref^{commit}",
|
||||
);
|
||||
die "xcat-core HEAD $revision does not match $xcat_ref ($expected)\n"
|
||||
unless $revision eq $expected;
|
||||
}
|
||||
|
||||
my $version = read_first_line("$xcat_source/Version");
|
||||
die "Invalid xCAT version: $version\n" unless $version =~ /^\d+(?:\.\d+){1,3}$/;
|
||||
my $source_date_epoch = capture_command(
|
||||
'git', '-C', $xcat_source, 'show', '-s', '--format=%ct', 'HEAD',
|
||||
);
|
||||
die "Invalid xcat-core commit time\n" unless $source_date_epoch =~ /^\d+$/;
|
||||
my $release = strftime('snap%Y%m%d%H%M', gmtime($source_date_epoch));
|
||||
|
||||
if ($output_dir eq '') {
|
||||
$output_dir = "$repo_root/build-output/genesis-openembedded/$version-$release";
|
||||
}
|
||||
my $output_parent = dirname($output_dir);
|
||||
make_path($output_parent) unless -d $output_parent;
|
||||
$output_parent = abs_path($output_parent) or die "Cannot resolve output parent\n";
|
||||
$output_dir = "$output_parent/" . basename($output_dir);
|
||||
die "Output already exists: $output_dir\n" if -e $output_dir || -l $output_dir;
|
||||
|
||||
my $work = tempdir('xcat-genesis-release.XXXXXX', TMPDIR => 1, CLEANUP => 1);
|
||||
my $staging = tempdir('.xcat-genesis-release.XXXXXX', DIR => $output_parent, CLEANUP => 1);
|
||||
my $oe_work;
|
||||
if ($work_dir ne '') {
|
||||
make_path($work_dir) unless -d $work_dir;
|
||||
$work_dir = abs_path($work_dir) or die "Cannot resolve work directory\n";
|
||||
$oe_work = "$work_dir/openembedded";
|
||||
} else {
|
||||
$oe_work = "$work/openembedded";
|
||||
}
|
||||
my $oe_tmp = "$oe_work/build/tmp";
|
||||
local $ENV{XCAT_GENESIS_WORK_DIR} = $oe_work;
|
||||
{
|
||||
local $ENV{TMPDIR} = $oe_tmp;
|
||||
make_path($oe_tmp);
|
||||
run_command("$xcat_source/xCAT-genesis-builder/oe/build", @requested_architectures);
|
||||
}
|
||||
my $effective_deploy = "$oe_tmp/deploy";
|
||||
die "Invalid OpenEmbedded deploy directory: $effective_deploy\n"
|
||||
unless -d $effective_deploy && !-l $effective_deploy;
|
||||
|
||||
for my $architecture (@requested_architectures) {
|
||||
my $export = "$work/exports/$architecture";
|
||||
make_path(dirname($export));
|
||||
run_command(
|
||||
"$xcat_source/xCAT-genesis-builder/oe/export",
|
||||
$architecture, $effective_deploy, $export,
|
||||
);
|
||||
|
||||
my $packages = "$work/packages/$architecture";
|
||||
run_command(
|
||||
"$FindBin::Bin/package",
|
||||
'--architecture', $architecture,
|
||||
'--export-dir', $export,
|
||||
'--output-dir', $packages,
|
||||
'--version', $version,
|
||||
'--release', $release,
|
||||
'--revision', $revision,
|
||||
'--source-date-epoch', $source_date_epoch,
|
||||
'--format', $format,
|
||||
);
|
||||
_collect_packages($packages, $staging, $architecture);
|
||||
}
|
||||
|
||||
my @formats = $format eq 'all' ? qw(deb rpm) : ($format);
|
||||
write_binary(
|
||||
"$staging/release.manifest",
|
||||
"format=xcat-genesis-packages\n"
|
||||
. "version=1\n"
|
||||
. "xcat_version=$version\n"
|
||||
. "xcat_release=$release\n"
|
||||
. "xcat_revision=$revision\n"
|
||||
. "source_date_epoch=$source_date_epoch\n"
|
||||
. "architectures=" . join(',', @requested_architectures) . "\n"
|
||||
. "formats=" . join(',', @formats) . "\n",
|
||||
);
|
||||
my @release_files = grep { $_ ne 'SHA256SUMS' } relative_files($staging);
|
||||
write_binary(
|
||||
"$staging/SHA256SUMS",
|
||||
digest_manifest($staging, 'sha256', @release_files),
|
||||
);
|
||||
validate_release($staging);
|
||||
my @verify_args = ('--format', $format);
|
||||
push(@verify_args, '--complete') if $all;
|
||||
run_command("$FindBin::Bin/verify-release", @verify_args, $staging);
|
||||
|
||||
chmod(0755, $staging) or die "Cannot make release directory readable: $!\n";
|
||||
rename($staging, $output_dir) or die "Cannot publish $output_dir: $!\n";
|
||||
print "Built Genesis package release in $output_dir\n";
|
||||
|
||||
sub _collect_packages {
|
||||
my ($source, $destination, $architecture) = @_;
|
||||
if ($format eq 'all' || $format eq 'rpm') {
|
||||
my $name = rpm_package_name($architecture);
|
||||
_collect_one(
|
||||
"$source/rpm/$name-$version-$release.noarch.rpm",
|
||||
"$destination/rpm",
|
||||
);
|
||||
_collect_one(
|
||||
"$source/srpm/$name-$version-$release.src.rpm",
|
||||
"$destination/srpm",
|
||||
);
|
||||
}
|
||||
if ($format eq 'all' || $format eq 'deb') {
|
||||
my $name = deb_package_name($architecture);
|
||||
_collect_one(
|
||||
"$source/deb/${name}_${version}-${release}_all.deb",
|
||||
"$destination/deb",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
sub _collect_one {
|
||||
my ($source, $directory) = @_;
|
||||
die "Missing package artifact: $source\n" unless -f $source && !-l $source;
|
||||
make_path($directory) unless -d $directory;
|
||||
my $destination = "$directory/" . basename($source);
|
||||
die "Duplicate package artifact: $destination\n" if -e $destination || -l $destination;
|
||||
copy($source, $destination) or die "Cannot collect $source: $!\n";
|
||||
}
|
||||
|
||||
sub usage {
|
||||
return <<'USAGE';
|
||||
Usage: build [--xcat-source DIR] [--xcat-ref REF] [--output-dir DIR]
|
||||
[--work-dir DIR]
|
||||
[--format all|rpm|deb] [--architecture ARCH ... | --all]
|
||||
USAGE
|
||||
}
|
||||
Executable
+246
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Cwd qw(abs_path);
|
||||
use File::Basename qw(basename dirname);
|
||||
use File::Copy qw(copy);
|
||||
use File::Path qw(make_path remove_tree);
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin;
|
||||
use Getopt::Long qw(GetOptions);
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use XCAT::BuildUtils qw(
|
||||
capture_command
|
||||
digest_manifest
|
||||
relative_files
|
||||
require_command
|
||||
run_command
|
||||
write_binary
|
||||
);
|
||||
use XCAT::GenesisRelease qw(
|
||||
deb_package_name
|
||||
rpm_package_name
|
||||
validate_architecture
|
||||
validate_export
|
||||
);
|
||||
|
||||
my ($architecture, $export_dir, $output_dir, $version, $release, $revision);
|
||||
my $source_date_epoch;
|
||||
my $format = 'all';
|
||||
my $rpm_top;
|
||||
|
||||
END {
|
||||
eval { remove_tree($rpm_top) }
|
||||
if defined($rpm_top) && -d $rpm_top && !-l $rpm_top;
|
||||
}
|
||||
|
||||
GetOptions(
|
||||
'architecture=s' => \$architecture,
|
||||
'export-dir=s' => \$export_dir,
|
||||
'output-dir=s' => \$output_dir,
|
||||
'version=s' => \$version,
|
||||
'release=s' => \$release,
|
||||
'revision=s' => \$revision,
|
||||
'source-date-epoch=i' => \$source_date_epoch,
|
||||
'format=s' => \$format,
|
||||
) or die usage();
|
||||
|
||||
for my $required (
|
||||
[ architecture => $architecture ],
|
||||
[ 'export-dir' => $export_dir ],
|
||||
[ 'output-dir' => $output_dir ],
|
||||
[ version => $version ],
|
||||
[ release => $release ],
|
||||
[ revision => $revision ],
|
||||
[ 'source-date-epoch' => $source_date_epoch ],
|
||||
)
|
||||
{
|
||||
die "Missing --$required->[0]\n" unless defined($required->[1]) && $required->[1] ne '';
|
||||
}
|
||||
|
||||
validate_architecture($architecture);
|
||||
die "Invalid version: $version\n" unless $version =~ /^\d+(?:\.\d+){1,3}$/;
|
||||
die "Invalid release: $release\n" unless $release =~ /^[A-Za-z0-9][A-Za-z0-9.+~]*$/;
|
||||
die "Invalid xcat-core revision: $revision\n" unless $revision =~ /^[0-9a-f]{40}$/;
|
||||
die "Invalid source date epoch\n" unless $source_date_epoch =~ /^\d+$/;
|
||||
die "Unsupported package format: $format\n"
|
||||
unless $format eq 'all' || $format eq 'rpm' || $format eq 'deb';
|
||||
umask(0022);
|
||||
|
||||
$export_dir = abs_path($export_dir) or die "Cannot resolve export directory\n";
|
||||
validate_export($export_dir, $architecture);
|
||||
|
||||
my $output_parent = dirname($output_dir);
|
||||
make_path($output_parent) unless -d $output_parent;
|
||||
$output_parent = abs_path($output_parent) or die "Cannot resolve output parent\n";
|
||||
$output_dir = "$output_parent/" . basename($output_dir);
|
||||
die "Output already exists: $output_dir\n" if -e $output_dir || -l $output_dir;
|
||||
chdir($output_parent) or die "Cannot use output parent $output_parent: $!\n";
|
||||
|
||||
my $work = tempdir('xcat-genesis-package.XXXXXX', TMPDIR => 1, CLEANUP => 1);
|
||||
my $staging = tempdir('.xcat-genesis-output.XXXXXX', DIR => $output_parent, CLEANUP => 1);
|
||||
my $rpm_name = rpm_package_name($architecture);
|
||||
my $deb_name = deb_package_name($architecture);
|
||||
my $source_name = "$rpm_name-$version";
|
||||
my $source_root = "$work/$source_name";
|
||||
make_path("$source_root/image");
|
||||
|
||||
for my $entry (_flat_files($export_dir)) {
|
||||
copy("$export_dir/$entry", "$source_root/image/$entry")
|
||||
or die "Cannot copy export file $entry: $!\n";
|
||||
}
|
||||
write_binary("$source_root/xcat-core-revision", "$revision\n");
|
||||
|
||||
local $ENV{SOURCE_DATE_EPOCH} = $source_date_epoch;
|
||||
|
||||
if ($format eq 'all' || $format eq 'rpm') {
|
||||
require_command('rpmbuild');
|
||||
_build_rpm($work, $staging, $source_root, $source_name, $rpm_name);
|
||||
}
|
||||
if ($format eq 'all' || $format eq 'deb') {
|
||||
require_command('dpkg-deb');
|
||||
_build_deb($work, $staging, $source_root, $deb_name);
|
||||
}
|
||||
|
||||
chmod(0755, $staging) or die "Cannot set output mode: $!\n";
|
||||
rename($staging, $output_dir) or die "Cannot publish $output_dir: $!\n";
|
||||
print "Packaged Genesis $architecture in $output_dir\n";
|
||||
|
||||
sub _build_rpm {
|
||||
my ($work, $staging, $source_root, $source_name, $package_name) = @_;
|
||||
_require_gnu_tar();
|
||||
require_command('gzip');
|
||||
$rpm_top = tempdir('xcat-genesis-rpmbuild.XXXXXX', DIR => '/var/tmp', CLEANUP => 0);
|
||||
make_path(map { "$rpm_top/$_" } qw(BUILD BUILDROOT RPMS SOURCES SPECS SRPMS));
|
||||
make_path("$staging/rpm", "$staging/srpm");
|
||||
|
||||
my $archive = "$rpm_top/SOURCES/$source_name.tar.gz";
|
||||
run_command(
|
||||
'tar', '--sort=name', '--owner=0', '--group=0', '--numeric-owner',
|
||||
"--mtime=\@$source_date_epoch", '--use-compress-program=gzip -n',
|
||||
'-cf', $archive, '-C', dirname($source_root), basename($source_root),
|
||||
);
|
||||
my $spec = "$FindBin::Bin/rpm/xCAT-genesis-openembedded.spec";
|
||||
copy($spec, "$rpm_top/SPECS/xCAT-genesis-openembedded.spec")
|
||||
or die "Cannot stage RPM spec: $!\n";
|
||||
run_command(
|
||||
'rpmbuild', '-ba', '--nodeps',
|
||||
'--define', "_topdir $rpm_top",
|
||||
'--define', "genesis_arch $architecture",
|
||||
'--define', "version $version",
|
||||
'--define', "release $release",
|
||||
# SOURCE_DATE_EPOCH is exported above; rpm turns it into the build time only
|
||||
# when asked, and takes the build host from _buildhost. Both are honoured from
|
||||
# rpm 4.14 through 6, which is what makes the identity verify-release expects
|
||||
# reproducible on every builder.
|
||||
'--define', '_buildhost xcat-release',
|
||||
'--define', 'use_source_date_epoch_as_buildtime 1',
|
||||
'--define', 'clamp_mtime_to_source_date_epoch 1',
|
||||
'--define', '_binary_payload w9.gzdio',
|
||||
'--define', '_source_payload w9.gzdio',
|
||||
"$rpm_top/SPECS/xCAT-genesis-openembedded.spec",
|
||||
);
|
||||
|
||||
my $binary = "$rpm_top/RPMS/noarch/$package_name-$version-$release.noarch.rpm";
|
||||
my $source = "$rpm_top/SRPMS/$package_name-$version-$release.src.rpm";
|
||||
die "RPM build did not produce $binary\n" unless -f $binary;
|
||||
die "RPM build did not produce $source\n" unless -f $source;
|
||||
copy($binary, "$staging/rpm/" . basename($binary)) or die "Cannot collect RPM: $!\n";
|
||||
copy($source, "$staging/srpm/" . basename($source)) or die "Cannot collect SRPM: $!\n";
|
||||
remove_tree($rpm_top);
|
||||
undef($rpm_top);
|
||||
}
|
||||
|
||||
sub _build_deb {
|
||||
my ($work, $staging, $source_root, $package_name) = @_;
|
||||
my $root = "$work/deb-root";
|
||||
my $image = "$root/opt/xcat/share/xcat/netboot/genesis-openembedded/$architecture";
|
||||
my $doc = "$root/usr/share/doc/$package_name";
|
||||
make_path("$root/DEBIAN", $image, $doc, "$staging/deb");
|
||||
|
||||
for my $entry (_flat_files("$source_root/image")) {
|
||||
copy("$source_root/image/$entry", "$image/$entry")
|
||||
or die "Cannot stage DEB image file $entry: $!\n";
|
||||
chmod(0644, "$image/$entry") or die "Cannot set mode on $entry: $!\n";
|
||||
}
|
||||
copy("$source_root/xcat-core-revision", "$doc/xcat-core-revision")
|
||||
or die "Cannot stage DEB revision: $!\n";
|
||||
chmod(0644, "$doc/xcat-core-revision") or die "Cannot set revision mode: $!\n";
|
||||
|
||||
my $installed_kib = _tree_bytes($root);
|
||||
$installed_kib = int(($installed_kib + 1023) / 1024);
|
||||
write_binary(
|
||||
"$root/DEBIAN/control",
|
||||
"Package: $package_name\n"
|
||||
. "Version: $version-$release\n"
|
||||
. "Section: admin\nPriority: optional\nArchitecture: all\n"
|
||||
. "Installed-Size: $installed_kib\n"
|
||||
. "Maintainer: xCAT <xcat-user\@lists.sourceforge.net>\n"
|
||||
. "Description: xCAT Genesis netboot image\n"
|
||||
. " The OpenEmbedded Genesis image used for node discovery, inventory,\n"
|
||||
. " and service actions.\n",
|
||||
);
|
||||
_write_deb_md5sums($root);
|
||||
_normalize_mtime($root, $source_date_epoch);
|
||||
|
||||
my $deb = "$staging/deb/${package_name}_${version}-${release}_all.deb";
|
||||
run_command('dpkg-deb', '--root-owner-group', '-Zgzip', '-z9', '--build', $root, $deb);
|
||||
die "DEB build did not produce $deb\n" unless -f $deb;
|
||||
}
|
||||
|
||||
sub _write_deb_md5sums {
|
||||
my ($root) = @_;
|
||||
my @files = grep { $_ !~ m{\ADEBIAN/} } relative_files($root);
|
||||
write_binary(
|
||||
"$root/DEBIAN/md5sums",
|
||||
digest_manifest($root, 'md5', @files),
|
||||
);
|
||||
}
|
||||
|
||||
sub _normalize_mtime {
|
||||
my ($root, $epoch) = @_;
|
||||
require File::Find;
|
||||
File::Find::find(
|
||||
{
|
||||
no_chdir => 1,
|
||||
wanted => sub {
|
||||
utime($epoch, $epoch, $File::Find::name)
|
||||
or die "Cannot set timestamp on $File::Find::name: $!\n";
|
||||
},
|
||||
},
|
||||
$root,
|
||||
);
|
||||
}
|
||||
|
||||
sub _flat_files {
|
||||
my ($directory) = @_;
|
||||
my @entries = relative_files($directory);
|
||||
for my $entry (@entries) {
|
||||
die "Expected a flat directory: $directory\n" if $entry =~ m{/};
|
||||
}
|
||||
return @entries;
|
||||
}
|
||||
|
||||
sub _tree_bytes {
|
||||
my ($directory) = @_;
|
||||
my $bytes = 0;
|
||||
$bytes += -s "$directory/$_" for relative_files($directory);
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
sub _require_gnu_tar {
|
||||
require_command('tar');
|
||||
my $version_line = capture_command('tar', '--version');
|
||||
die "GNU tar is required to build Genesis source packages\n"
|
||||
unless $version_line =~ /^tar \(GNU tar\)/;
|
||||
}
|
||||
|
||||
sub usage {
|
||||
return <<'USAGE';
|
||||
Usage: package --architecture ARCH --export-dir DIR --output-dir DIR
|
||||
--version VERSION --release RELEASE --revision COMMIT
|
||||
--source-date-epoch EPOCH [--format all|rpm|deb]
|
||||
USAGE
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
%global genesis_arch %{?genesis_arch}%{!?genesis_arch:x86_64}
|
||||
|
||||
Name: xCAT-genesis-openembedded-%{genesis_arch}
|
||||
Version: %{?version}%{!?version:0}
|
||||
Release: %{?release}%{!?release:1}
|
||||
Summary: xCAT OpenEmbedded Genesis netboot image
|
||||
License: Various
|
||||
URL: https://xcat.org/
|
||||
Source0: %{name}-%{version}.tar.gz
|
||||
BuildArch: noarch
|
||||
AutoReqProv: no
|
||||
BuildRequires: gzip
|
||||
BuildRequires: tar
|
||||
|
||||
%description
|
||||
The OpenEmbedded Genesis image used by xCAT for discovery, inventory, and
|
||||
service actions before a node boots its installed operating system.
|
||||
|
||||
%prep
|
||||
rm -rf %{name}-%{version}
|
||||
tar -xzf "$RPM_SOURCE_DIR/%{name}-%{version}.tar.gz"
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
cd %{name}-%{version}
|
||||
rm -rf "$RPM_BUILD_ROOT"
|
||||
install -d -m 0755 "$RPM_BUILD_ROOT/opt/xcat/share/xcat/netboot/genesis-openembedded/%{genesis_arch}"
|
||||
install -m 0644 image/* "$RPM_BUILD_ROOT/opt/xcat/share/xcat/netboot/genesis-openembedded/%{genesis_arch}/"
|
||||
# Keep this path stable across RPM build hosts. %%{_docdir} differs on SUSE.
|
||||
install -d -m 0755 "$RPM_BUILD_ROOT/usr/share/doc/%{name}"
|
||||
install -m 0644 xcat-core-revision "$RPM_BUILD_ROOT/usr/share/doc/%{name}/"
|
||||
|
||||
%files
|
||||
%defattr(-,root,root,-)
|
||||
%dir /opt/xcat
|
||||
%dir /opt/xcat/share
|
||||
%dir /opt/xcat/share/xcat
|
||||
%dir /opt/xcat/share/xcat/netboot
|
||||
%dir /opt/xcat/share/xcat/netboot/genesis-openembedded
|
||||
/opt/xcat/share/xcat/netboot/genesis-openembedded/%{genesis_arch}
|
||||
%dir /usr/share/doc/%{name}
|
||||
%doc /usr/share/doc/%{name}/xcat-core-revision
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Cwd qw(abs_path);
|
||||
use FindBin;
|
||||
use Getopt::Long qw(GetOptions);
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use XCAT::BuildUtils qw(capture_command require_command);
|
||||
use XCAT::GenesisRelease qw(
|
||||
deb_package_name
|
||||
read_checksum_manifest
|
||||
rpm_package_name
|
||||
validate_complete_release
|
||||
validate_release
|
||||
verify_release_file
|
||||
);
|
||||
|
||||
my $format = 'all';
|
||||
my $complete = 0;
|
||||
my ($checksum_file, $relative_file, $copied_file);
|
||||
GetOptions(
|
||||
'format=s' => \$format,
|
||||
'complete!' => \$complete,
|
||||
'checksum-file=s' => \$checksum_file,
|
||||
'relative-file=s' => \$relative_file,
|
||||
'copied-file=s' => \$copied_file,
|
||||
) or die usage();
|
||||
|
||||
my $copy_mode = grep { defined($_) }
|
||||
($checksum_file, $relative_file, $copied_file);
|
||||
if ($copy_mode) {
|
||||
die usage()
|
||||
unless defined($checksum_file)
|
||||
&& defined($relative_file)
|
||||
&& defined($copied_file)
|
||||
&& !$complete
|
||||
&& $format eq 'all'
|
||||
&& !@ARGV;
|
||||
$checksum_file = abs_path($checksum_file)
|
||||
or die "Cannot resolve checksum file\n";
|
||||
$copied_file = abs_path($copied_file)
|
||||
or die "Cannot resolve copied file\n";
|
||||
my $checksums = read_checksum_manifest($checksum_file);
|
||||
verify_release_file($checksums, $relative_file, $copied_file);
|
||||
print "Verified copied Genesis package: $copied_file\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
die usage() unless @ARGV == 1;
|
||||
die "Unsupported verification format: $format\n"
|
||||
unless $format eq 'all' || $format eq 'rpm' || $format eq 'deb';
|
||||
|
||||
my $directory = abs_path($ARGV[0]) or die "Cannot resolve release directory\n";
|
||||
my $manifest = $complete
|
||||
? validate_complete_release($directory)
|
||||
: validate_release($directory);
|
||||
my %available = map { $_ => 1 } split(/,/, $manifest->{formats});
|
||||
my @formats = $format eq 'all' ? qw(rpm deb) : ($format);
|
||||
for my $required (@formats) {
|
||||
die "Release does not contain $required packages\n" unless $available{$required};
|
||||
}
|
||||
|
||||
my @architectures = split(/,/, $manifest->{architectures});
|
||||
for my $architecture (@architectures) {
|
||||
if (grep { $_ eq 'rpm' } @formats) {
|
||||
_verify_rpm($directory, $manifest, $architecture);
|
||||
}
|
||||
if (grep { $_ eq 'deb' } @formats) {
|
||||
_verify_deb($directory, $manifest, $architecture);
|
||||
}
|
||||
}
|
||||
|
||||
print "Verified Genesis package release: $directory\n";
|
||||
|
||||
sub _verify_rpm {
|
||||
my ($directory, $manifest, $architecture) = @_;
|
||||
require_command('rpm');
|
||||
my $name = rpm_package_name($architecture);
|
||||
my $source_name = "$name-$manifest->{xcat_version}-$manifest->{xcat_release}.src.rpm";
|
||||
my @packages = (
|
||||
[ "$directory/rpm/$name-$manifest->{xcat_version}-$manifest->{xcat_release}.noarch.rpm",
|
||||
'noarch', $source_name ],
|
||||
[ "$directory/srpm/$source_name", 'noarch', '(none)' ],
|
||||
);
|
||||
for my $package (@packages) {
|
||||
my ($path, $expected_arch, $expected_source) = @{$package};
|
||||
my $metadata = capture_command(
|
||||
'rpm', '-qp', '--qf',
|
||||
"%{NAME}\t%{VERSION}\t%{RELEASE}\t%{ARCH}\t%{EPOCHNUM}\t%{SOURCERPM}"
|
||||
. "\t%{BUILDHOST}\t%{BUILDTIME}\n",
|
||||
$path,
|
||||
);
|
||||
my $expected = join(
|
||||
"\t", $name, $manifest->{xcat_version}, $manifest->{xcat_release},
|
||||
$expected_arch, '0', $expected_source, 'xcat-release',
|
||||
$manifest->{source_date_epoch},
|
||||
);
|
||||
die "Unexpected RPM identity for $path: $metadata\n" unless $metadata eq $expected;
|
||||
for my $relationship (qw(conflicts obsoletes)) {
|
||||
my $value = capture_command('rpm', '-qp', "--$relationship", $path);
|
||||
die "Unexpected RPM $relationship for $path: $value\n" if $value ne '';
|
||||
}
|
||||
my $provides = capture_command('rpm', '-qp', '--provides', $path);
|
||||
die "Legacy RPM relationship in $path: $provides\n"
|
||||
if $provides =~ /xCAT-genesis-base/;
|
||||
}
|
||||
}
|
||||
|
||||
sub _verify_deb {
|
||||
my ($directory, $manifest, $architecture) = @_;
|
||||
require_command('dpkg-deb');
|
||||
my $name = deb_package_name($architecture);
|
||||
my $path = "$directory/deb/${name}_$manifest->{xcat_version}-$manifest->{xcat_release}_all.deb";
|
||||
my $metadata = join(
|
||||
"\n",
|
||||
map { capture_command('dpkg-deb', '-f', $path, $_) }
|
||||
qw(Package Version Architecture),
|
||||
);
|
||||
my $expected = join(
|
||||
"\n", $name, "$manifest->{xcat_version}-$manifest->{xcat_release}",
|
||||
'all',
|
||||
);
|
||||
die "Unexpected DEB identity for $path: $metadata\n" unless $metadata eq $expected;
|
||||
for my $field (qw(Breaks Conflicts Provides Replaces)) {
|
||||
my $value = capture_command('dpkg-deb', '-f', $path, $field);
|
||||
die "Unexpected DEB $field for $path: $value\n" if $value ne '';
|
||||
}
|
||||
}
|
||||
|
||||
sub usage {
|
||||
return <<'USAGE';
|
||||
Usage: verify-release [--format all|rpm|deb] [--complete] RELEASE_DIRECTORY
|
||||
verify-release --checksum-file SHA256SUMS --relative-file PATH
|
||||
--copied-file FILE
|
||||
USAGE
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package XCAT::BuildUtils;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Digest::MD5 ();
|
||||
use Digest::SHA ();
|
||||
use Exporter qw(import);
|
||||
use File::Find qw(find);
|
||||
use File::Slurper qw(read_binary write_binary);
|
||||
use File::Spec;
|
||||
use IPC::Cmd qw(can_run);
|
||||
|
||||
our @EXPORT_OK = qw(
|
||||
capture_command
|
||||
command_exists
|
||||
digest_file
|
||||
digest_manifest
|
||||
display_quote
|
||||
every_step_failed
|
||||
hashes_equal
|
||||
print_step
|
||||
read_binary
|
||||
read_first_line
|
||||
read_lines
|
||||
relative_files
|
||||
require_command
|
||||
run_command
|
||||
shell_quote
|
||||
write_binary
|
||||
);
|
||||
|
||||
sub command_exists {
|
||||
my ($command) = @_;
|
||||
return defined(can_run($command));
|
||||
}
|
||||
|
||||
sub require_command {
|
||||
my ($command) = @_;
|
||||
return can_run($command)
|
||||
// die "Required command not found: $command\n";
|
||||
}
|
||||
|
||||
sub capture_command {
|
||||
my (@command) = @_;
|
||||
open(my $fh, '-|', @command) or die "Cannot run $command[0]: $!\n";
|
||||
local $/;
|
||||
my $output = <$fh> // '';
|
||||
close($fh) or die "Command failed: $command[0]\n";
|
||||
$output =~ s/\s+\z//;
|
||||
return $output;
|
||||
}
|
||||
|
||||
sub run_command {
|
||||
my (@command) = @_;
|
||||
print '+ ', join(' ', map { display_quote($_) } @command), "\n";
|
||||
my $status = system(@command);
|
||||
return 1 if $status == 0;
|
||||
|
||||
my $exit = $status == -1
|
||||
? 255
|
||||
: ($status & 127) ? 128 + ($status & 127) : $status >> 8;
|
||||
die "Command failed (rc=$exit): "
|
||||
. join(' ', map { display_quote($_) } @command) . "\n";
|
||||
}
|
||||
|
||||
sub display_quote {
|
||||
my ($value) = @_;
|
||||
return $value if $value =~ /^[A-Za-z0-9_.,+\/:=@~-]+$/;
|
||||
return shell_quote($value);
|
||||
}
|
||||
|
||||
sub shell_quote {
|
||||
my ($value) = @_;
|
||||
$value = '' unless defined($value);
|
||||
$value =~ s/'/'"'"'/g;
|
||||
return "'$value'";
|
||||
}
|
||||
|
||||
sub print_step {
|
||||
my ($message) = @_;
|
||||
print "\n== $message ==\n";
|
||||
}
|
||||
|
||||
sub read_lines {
|
||||
my ($path) = @_;
|
||||
my $content = read_binary($path);
|
||||
return () if $content eq '';
|
||||
|
||||
my @lines = split(/\n/, $content, -1);
|
||||
pop(@lines) if @lines && $lines[-1] eq '';
|
||||
s/\r\z// for @lines;
|
||||
return @lines;
|
||||
}
|
||||
|
||||
sub read_first_line {
|
||||
my ($path) = @_;
|
||||
my @lines = read_lines($path);
|
||||
die "Empty file: $path\n" unless @lines;
|
||||
return $lines[0];
|
||||
}
|
||||
|
||||
sub relative_files {
|
||||
my ($root) = @_;
|
||||
die "Invalid directory: $root\n" unless -d $root && !-l $root;
|
||||
|
||||
my $absolute = File::Spec->rel2abs($root);
|
||||
my @files;
|
||||
find(
|
||||
{
|
||||
no_chdir => 1,
|
||||
wanted => sub {
|
||||
my $path = $File::Find::name;
|
||||
return if $path eq $absolute;
|
||||
die "Symbolic links are not allowed: $path\n" if -l $path;
|
||||
return if -d $path;
|
||||
die "Non-regular entry: $path\n" unless -f $path;
|
||||
my $relative = File::Spec->abs2rel($path, $absolute);
|
||||
$relative =~ tr{\\}{/};
|
||||
push(@files, $relative);
|
||||
},
|
||||
},
|
||||
$absolute,
|
||||
);
|
||||
my @sorted = sort @files;
|
||||
return @sorted;
|
||||
}
|
||||
|
||||
sub digest_file {
|
||||
my ($path, $algorithm) = @_;
|
||||
$algorithm //= 'sha256';
|
||||
|
||||
my $digest = $algorithm eq 'sha256' ? Digest::SHA->new(256)
|
||||
: $algorithm eq 'md5' ? Digest::MD5->new
|
||||
: die "Unsupported digest algorithm: $algorithm\n";
|
||||
open(my $fh, '<:raw', $path) or die "Cannot read $path: $!\n";
|
||||
my $value = $digest->addfile($fh)->hexdigest;
|
||||
close($fh) or die "Cannot close $path: $!\n";
|
||||
return $value;
|
||||
}
|
||||
|
||||
sub digest_manifest {
|
||||
my ($root, $algorithm, @files) = @_;
|
||||
return join(
|
||||
'',
|
||||
map { digest_file("$root/$_", $algorithm) . " $_\n" }
|
||||
sort @files,
|
||||
);
|
||||
}
|
||||
|
||||
# True when a set of steps was attempted and none of them survived. Callers tolerate
|
||||
# individual failures; losing every step means the builder itself did not work.
|
||||
sub every_step_failed {
|
||||
my ($attempted, $failures) = @_;
|
||||
return 0 unless $attempted;
|
||||
return $failures >= $attempted ? 1 : 0;
|
||||
}
|
||||
|
||||
sub hashes_equal {
|
||||
my ($left, $right) = @_;
|
||||
return 0 unless keys(%{$left}) == keys(%{$right});
|
||||
for my $name (keys %{$left}) {
|
||||
return 0 unless exists($right->{$name})
|
||||
&& $left->{$name} eq $right->{$name};
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -0,0 +1,247 @@
|
||||
package XCAT::GenesisRelease;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Exporter qw(import);
|
||||
use XCAT::BuildUtils qw(digest_file read_lines relative_files);
|
||||
|
||||
our @EXPORT_OK = qw(
|
||||
architectures
|
||||
deb_package_name
|
||||
read_checksum_manifest
|
||||
read_release_manifest
|
||||
rpm_package_name
|
||||
validated_release_checksums
|
||||
validate_architecture
|
||||
validate_complete_release
|
||||
validate_export
|
||||
validate_release
|
||||
verify_release_file
|
||||
);
|
||||
|
||||
my @ARCHITECTURES = qw(x86 x86_64 ppc64 ppc64le armv7hf aarch64 riscv64);
|
||||
my %ARCHITECTURE = map { $_ => 1 } @ARCHITECTURES;
|
||||
|
||||
sub architectures {
|
||||
return @ARCHITECTURES;
|
||||
}
|
||||
|
||||
sub validate_architecture {
|
||||
my ($architecture) = @_;
|
||||
die "Unsupported Genesis architecture: $architecture\n"
|
||||
unless defined($architecture) && $ARCHITECTURE{$architecture};
|
||||
return $architecture;
|
||||
}
|
||||
|
||||
sub rpm_package_name {
|
||||
my ($architecture) = @_;
|
||||
validate_architecture($architecture);
|
||||
return "xCAT-genesis-openembedded-$architecture";
|
||||
}
|
||||
|
||||
sub deb_package_name {
|
||||
my ($architecture) = @_;
|
||||
validate_architecture($architecture);
|
||||
$architecture =~ tr/_/-/;
|
||||
return "xcat-genesis-openembedded-$architecture";
|
||||
}
|
||||
|
||||
sub _read_key_values {
|
||||
my ($path, $allowed) = @_;
|
||||
my %values;
|
||||
for my $line (read_lines($path)) {
|
||||
die "Invalid manifest entry in $path: $line\n"
|
||||
unless $line =~ /^([a-z][a-z0-9_]*)=([A-Za-z0-9][A-Za-z0-9.,_+~-]*)$/;
|
||||
my ($key, $value) = ($1, $2);
|
||||
die "Unknown manifest key in $path: $key\n" unless $allowed->{$key};
|
||||
die "Duplicate manifest key in $path: $key\n" if exists($values{$key});
|
||||
$values{$key} = $value;
|
||||
}
|
||||
return \%values;
|
||||
}
|
||||
|
||||
sub read_checksum_manifest {
|
||||
my ($path) = @_;
|
||||
my %checksums;
|
||||
for my $line (read_lines($path)) {
|
||||
die "Invalid checksum entry in $path: $line\n"
|
||||
unless $line =~ /^([0-9a-f]{64}) ([A-Za-z0-9][A-Za-z0-9._\/+~-]*)$/;
|
||||
my ($digest, $name) = ($1, $2);
|
||||
die "Unsafe checksum path in $path: $name\n"
|
||||
if $name =~ m{(?:\A|/)\.\.(?:/|\z)} || $name =~ m{//};
|
||||
die "Duplicate checksum entry in $path: $name\n"
|
||||
if exists($checksums{$name});
|
||||
$checksums{$name} = $digest;
|
||||
}
|
||||
return \%checksums;
|
||||
}
|
||||
|
||||
sub _verify_checksums {
|
||||
my ($root) = @_;
|
||||
my @files = grep { $_ ne 'SHA256SUMS' } relative_files($root);
|
||||
my $checksums = read_checksum_manifest("$root/SHA256SUMS");
|
||||
|
||||
my %files = map { $_ => 1 } @files;
|
||||
for my $name (@files) {
|
||||
die "Missing checksum for $name\n" unless exists($checksums->{$name});
|
||||
my $digest = digest_file("$root/$name", 'sha256');
|
||||
die "Checksum mismatch for $name\n" unless $digest eq $checksums->{$name};
|
||||
}
|
||||
for my $name (keys %{$checksums}) {
|
||||
die "Checksum names a missing file: $name\n" unless $files{$name};
|
||||
}
|
||||
return $checksums;
|
||||
}
|
||||
|
||||
sub validate_export {
|
||||
my ($directory, $architecture) = @_;
|
||||
validate_architecture($architecture);
|
||||
die "Invalid Genesis export: $directory\n" unless -d $directory && !-l $directory;
|
||||
|
||||
my %required = map { $_ => 1 } qw(
|
||||
SHA256SUMS
|
||||
image.manifest
|
||||
image.spdx.json
|
||||
image.vex.json
|
||||
initramfs.cpio.gz
|
||||
kernel
|
||||
license.manifest
|
||||
xcat-genesis.manifest
|
||||
);
|
||||
$required{'fw_jump.elf'} = 1 if $architecture eq 'riscv64';
|
||||
|
||||
my @files = relative_files($directory);
|
||||
my %files = map { $_ => 1 } @files;
|
||||
for my $name (sort keys %required) {
|
||||
die "Genesis export is missing $name\n" unless $files{$name};
|
||||
}
|
||||
for my $name (@files) {
|
||||
die "Unexpected Genesis export file: $name\n" unless $required{$name};
|
||||
}
|
||||
|
||||
my $manifest = _read_key_values(
|
||||
"$directory/xcat-genesis.manifest",
|
||||
{ map { $_ => 1 } qw(format version architecture) },
|
||||
);
|
||||
die "Unsupported Genesis export format\n"
|
||||
unless ($manifest->{format} // '') eq 'xcat-genesis';
|
||||
die "Unsupported Genesis export version\n"
|
||||
unless ($manifest->{version} // '') eq '1';
|
||||
die "Genesis export architecture mismatch\n"
|
||||
unless ($manifest->{architecture} // '') eq $architecture;
|
||||
|
||||
_verify_checksums($directory);
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub read_release_manifest {
|
||||
my ($directory) = @_;
|
||||
my $values = _read_key_values(
|
||||
"$directory/release.manifest",
|
||||
{
|
||||
map { $_ => 1 } qw(
|
||||
format version xcat_version xcat_release xcat_revision
|
||||
source_date_epoch architectures formats
|
||||
)
|
||||
},
|
||||
);
|
||||
for my $key (qw(format version xcat_version xcat_release xcat_revision source_date_epoch architectures formats)) {
|
||||
die "Release manifest is missing $key\n" unless exists($values->{$key});
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
sub _validate_release {
|
||||
my ($directory) = @_;
|
||||
die "Invalid Genesis package release: $directory\n"
|
||||
unless -d $directory && !-l $directory;
|
||||
|
||||
my $manifest = read_release_manifest($directory);
|
||||
die "Unsupported Genesis package release format\n"
|
||||
unless $manifest->{format} eq 'xcat-genesis-packages';
|
||||
die "Unsupported Genesis package release version\n"
|
||||
unless $manifest->{version} eq '1';
|
||||
die "Invalid xCAT version in release manifest\n"
|
||||
unless $manifest->{xcat_version} =~ /^\d+(?:\.\d+){1,3}$/;
|
||||
die "Invalid xCAT release in release manifest\n"
|
||||
unless $manifest->{xcat_release} =~ /^[A-Za-z0-9][A-Za-z0-9.+~]*$/;
|
||||
die "Invalid xCAT revision in release manifest\n"
|
||||
unless $manifest->{xcat_revision} =~ /^[0-9a-f]{40}$/;
|
||||
die "Invalid source epoch in release manifest\n"
|
||||
unless $manifest->{source_date_epoch} =~ /^\d+$/;
|
||||
|
||||
my @architectures = split(/,/, $manifest->{architectures});
|
||||
my %seen_arch;
|
||||
for my $architecture (@architectures) {
|
||||
validate_architecture($architecture);
|
||||
die "Duplicate release architecture: $architecture\n" if $seen_arch{$architecture}++;
|
||||
}
|
||||
die "Release manifest has no architectures\n" unless @architectures;
|
||||
|
||||
my @formats = split(/,/, $manifest->{formats});
|
||||
my %seen_format;
|
||||
for my $format (@formats) {
|
||||
die "Unsupported package format: $format\n" unless $format eq 'rpm' || $format eq 'deb';
|
||||
die "Duplicate package format: $format\n" if $seen_format{$format}++;
|
||||
}
|
||||
die "Release manifest has no package formats\n" unless @formats;
|
||||
|
||||
my $checksums = _verify_checksums($directory);
|
||||
|
||||
my %expected = (
|
||||
'release.manifest' => 1,
|
||||
);
|
||||
for my $architecture (@architectures) {
|
||||
my $rpm = rpm_package_name($architecture);
|
||||
my $deb = deb_package_name($architecture);
|
||||
if ($seen_format{rpm}) {
|
||||
$expected{"rpm/$rpm-$manifest->{xcat_version}-$manifest->{xcat_release}.noarch.rpm"} = 1;
|
||||
$expected{"srpm/$rpm-$manifest->{xcat_version}-$manifest->{xcat_release}.src.rpm"} = 1;
|
||||
}
|
||||
if ($seen_format{deb}) {
|
||||
$expected{"deb/${deb}_$manifest->{xcat_version}-$manifest->{xcat_release}_all.deb"} = 1;
|
||||
}
|
||||
}
|
||||
for my $file (grep { $_ ne 'SHA256SUMS' } relative_files($directory)) {
|
||||
die "Unexpected Genesis release artifact: $file\n" unless $expected{$file};
|
||||
delete($expected{$file});
|
||||
}
|
||||
die "Genesis release is missing: " . join(', ', sort keys %expected) . "\n"
|
||||
if %expected;
|
||||
return ($manifest, $checksums);
|
||||
}
|
||||
|
||||
sub validate_release {
|
||||
my ($directory) = @_;
|
||||
my ($manifest) = _validate_release($directory);
|
||||
return $manifest;
|
||||
}
|
||||
|
||||
sub validate_complete_release {
|
||||
my ($directory) = @_;
|
||||
my $manifest = validate_release($directory);
|
||||
my %present = map { $_ => 1 } split(/,/, $manifest->{architectures});
|
||||
my @missing = grep { !$present{$_} } @ARCHITECTURES;
|
||||
die "Genesis release is missing supported architectures: @missing\n" if @missing;
|
||||
return $manifest;
|
||||
}
|
||||
|
||||
sub validated_release_checksums {
|
||||
my ($directory) = @_;
|
||||
my (undef, $checksums) = _validate_release($directory);
|
||||
return $checksums;
|
||||
}
|
||||
|
||||
sub verify_release_file {
|
||||
my ($checksums, $relative, $path) = @_;
|
||||
die "Missing verified checksum for $relative\n"
|
||||
unless ref($checksums) eq 'HASH' && exists($checksums->{$relative});
|
||||
die "Invalid collected release file: $path\n" unless -f $path && !-l $path;
|
||||
my $digest = digest_file($path, 'sha256');
|
||||
die "Collected release file checksum mismatch: $path\n"
|
||||
unless $digest eq $checksums->{$relative};
|
||||
return 1;
|
||||
}
|
||||
|
||||
1;
|
||||
+257
-48
@@ -7,18 +7,34 @@ use Cwd qw(abs_path cwd);
|
||||
use File::Basename qw(dirname basename);
|
||||
use File::Copy qw(copy);
|
||||
use File::Find qw(find);
|
||||
use File::Glob qw(bsd_glob);
|
||||
use File::Path qw(make_path remove_tree);
|
||||
use File::Temp qw(tempdir);
|
||||
use File::Temp qw(tempdir tempfile);
|
||||
use Getopt::Long qw(GetOptions);
|
||||
use Parallel::ForkManager;
|
||||
use POSIX qw(strftime);
|
||||
use FindBin qw($RealBin);
|
||||
use lib $RealBin;
|
||||
use lib $RealBin, "$RealBin/lib";
|
||||
use MockBuildUtils qw(sh_quote print_step version_matches required_pkgs
|
||||
read_manifest verify_repo_packages verify_repo_signature verify_rpm_signatures
|
||||
rpm_version rpm_release rpm_sigmd5 restamp_release_line
|
||||
cross_copy_genesis finalize_xcat_dep bump_dep_release_suffix
|
||||
build_mock_uniqueext rpmkeys_checksig_problem);
|
||||
# print_step and sh_quote come from MockBuildUtils above; XCAT::BuildUtils carries the same
|
||||
# print_step, so it is deliberately NOT imported here (one definition, no redefinition warning).
|
||||
use XCAT::BuildUtils qw(
|
||||
capture_command
|
||||
every_step_failed
|
||||
hashes_equal
|
||||
read_lines
|
||||
require_command
|
||||
run_command
|
||||
shell_quote
|
||||
);
|
||||
use XCAT::GenesisRelease qw(
|
||||
validated_release_checksums
|
||||
verify_release_file
|
||||
);
|
||||
|
||||
# --- Mount-namespace isolation: guard the host cgroup against mock teardown propagation ----------
|
||||
# mock mounts /sys/fs/cgroup into every build chroot. On these systemd build hosts every mount is
|
||||
@@ -88,6 +104,8 @@ my $skip_perl = 0;
|
||||
my $skip_genesis = 0;
|
||||
my $skip_createrepo = 0;
|
||||
my $skip_tarball = 0;
|
||||
my $genesis_release = '';
|
||||
my $genesis_release_checksums;
|
||||
my $scrub_all_chroots = 0;
|
||||
my $keep_buildroots = 0; # keep per-step mock chroots after build (default: --scrub=chroot each)
|
||||
my $dry_run = 0;
|
||||
@@ -141,6 +159,7 @@ GetOptions(
|
||||
'skip-genesis!' => \$skip_genesis,
|
||||
'skip-createrepo!' => \$skip_createrepo,
|
||||
'skip-tarball!' => \$skip_tarball,
|
||||
'genesis-release=s' => \$genesis_release,
|
||||
'scrub-all-chroots!' => \$scrub_all_chroots,
|
||||
'keep-buildroots!' => \$keep_buildroots,
|
||||
'collect-dir=s@' => \@extra_collect_dirs,
|
||||
@@ -161,7 +180,8 @@ $repo_root = abs_path($repo_root);
|
||||
my $SOURCE_DATE_EPOCH;
|
||||
$SOURCE_DATE_EPOCH = $build_timestamp if defined $build_timestamp;
|
||||
if (!$SOURCE_DATE_EPOCH && -f "$repo_root/Gitepoch") {
|
||||
$SOURCE_DATE_EPOCH = slurp_chomp("$repo_root/Gitepoch");
|
||||
my @gitepoch = read_lines("$repo_root/Gitepoch");
|
||||
$SOURCE_DATE_EPOCH = $gitepoch[0] // '';
|
||||
}
|
||||
unless ($SOURCE_DATE_EPOCH && $SOURCE_DATE_EPOCH =~ /^\d+$/) {
|
||||
$SOURCE_DATE_EPOCH = `git -C \Q$repo_root\E log -1 --format=%ct HEAD 2>/dev/null`;
|
||||
@@ -281,7 +301,7 @@ acquire_output_lock($output_base, $force_unlock);
|
||||
|
||||
$xcat_src = resolve_xcat_source($xcat_src, $repo_root);
|
||||
|
||||
my $arch = capture('uname -m');
|
||||
my $arch = capture_command('uname', '-m');
|
||||
my %os = read_os_release('/etc/os-release');
|
||||
my $os_id = $os{ID} // '';
|
||||
my $version_id = $os{VERSION_ID} // '';
|
||||
@@ -298,6 +318,25 @@ require_command('mock') if $scrub_all_chroots;
|
||||
require_command('rpmsign') if $gpg_sign;
|
||||
require_command('gpg') if $gpg_sign;
|
||||
|
||||
if ($genesis_release ne '') {
|
||||
$genesis_release = abs_path($genesis_release)
|
||||
or die "Cannot resolve --genesis-release directory\n";
|
||||
die "Genesis release directory not found: $genesis_release\n"
|
||||
unless -d $genesis_release;
|
||||
my $verifier = "$script_dir/genesis-openembedded/verify-release";
|
||||
die "Genesis release verifier not found: $verifier\n" unless -x $verifier;
|
||||
# Checksum, verify, checksum again. The verifier reads the tree it validates, so a
|
||||
# release rewritten together with its SHA256SUMS while the verifier runs would satisfy
|
||||
# both the verifier and any single pass taken afterwards; comparing the pass taken
|
||||
# before with the one taken after is what closes that window.
|
||||
my $checksums_before = validated_release_checksums($genesis_release);
|
||||
run_command($^X, $verifier, '--complete', '--format', 'rpm', $genesis_release);
|
||||
my $checksums_after = validated_release_checksums($genesis_release);
|
||||
die "Genesis release changed during verification\n"
|
||||
unless hashes_equal($checksums_before, $checksums_after);
|
||||
$genesis_release_checksums = $checksums_before;
|
||||
}
|
||||
|
||||
# An explicit --target builds just that target; otherwise build the current host
|
||||
# arch across rh8/rh9/rh10 into a deployable per-EL xcat-dep repo. This script builds
|
||||
# ONLY the host arch (uname -m) -- the other arch is produced on its own build host.
|
||||
@@ -317,6 +356,7 @@ print "lock: $output_base/.lock (held)\n";
|
||||
print "gpg_sign: $gpg_sign\n";
|
||||
print "gpg_key_name: $gpg_key_name\n" if $gpg_sign;
|
||||
print "gpg_home: " . ($gpg_home ne '' ? $gpg_home : '(default keyring)') . "\n" if $gpg_sign;
|
||||
print "genesis_release: " . ($genesis_release || '(legacy builder)') . "\n";
|
||||
|
||||
# Build (and deploy) EL targets concurrently. Each target is fully isolated -- distinct run_id
|
||||
# (target-folded), mock --uniqueext, /tmp work dir, xcat_src/dist/<target>, and deploy dir
|
||||
@@ -327,7 +367,7 @@ $tgt_workers = scalar(@build_targets) if $tgt_workers > scalar(@build_targets);
|
||||
# mock build already gets a unique --uniqueext (separate chroot), so the only limit needed is
|
||||
# hardware: total concurrent builds across all targets stays <= $cap (default host nproc). The
|
||||
# per-target build-step concurrency is therefore the cap divided across the active targets.
|
||||
my $cap = $max_parallel > 0 ? $max_parallel : (capture('nproc') || 4);
|
||||
my $cap = $max_parallel > 0 ? $max_parallel : (capture_command('nproc') || 4);
|
||||
my $per_target_builds = defined($parallel_builds) ? $parallel_builds : int($cap / $tgt_workers);
|
||||
$per_target_builds = 1 if $per_target_builds < 1;
|
||||
print "parallel_targets: " . ($parallel_targets > 0 ? $parallel_targets : "auto($tgt_workers)") . "\n";
|
||||
@@ -425,6 +465,20 @@ die "Missing perl builder script: $perl_builder\n"
|
||||
|
||||
if (!$dry_run) {
|
||||
make_path($build_root, $log_root, $repo_dir, $srpm_repo_dir);
|
||||
# The staging repositories hold what THIS invocation produces. A reused --run-id, or a
|
||||
# rerun after a failed invocation, otherwise leaves an earlier run's packages in them,
|
||||
# where collection never sees them, createrepo indexes them and deploy_target publishes
|
||||
# them as this run's output.
|
||||
reset_staging_repo($repo_dir);
|
||||
reset_staging_repo($srpm_repo_dir);
|
||||
# Same for the builder results when this run is going to build them: a reused --run-id
|
||||
# otherwise leaves a previous run's packages there, and a step that fails this time is
|
||||
# collected from the last time it succeeded. --skip-build deliberately collects earlier
|
||||
# output, from the repository-level build-output tree, and must keep what is there.
|
||||
if (!$skip_build) {
|
||||
remove_tree($build_root);
|
||||
make_path($build_root);
|
||||
}
|
||||
}
|
||||
|
||||
print_step("Configuration");
|
||||
@@ -458,7 +512,7 @@ my @collect_roots;
|
||||
if ($scrub_all_chroots) {
|
||||
run_step(
|
||||
step => "Scrub all chroots for target $target",
|
||||
cmd => "mock -r " . sh_quote($target) . " --scrub=all",
|
||||
cmd => "mock -r " . shell_quote($target) . " --scrub=all",
|
||||
log => "$log_root/scrub-all-chroots.log",
|
||||
);
|
||||
}
|
||||
@@ -476,13 +530,13 @@ if (!$skip_build) {
|
||||
my $step_log = "$log_root/$name";
|
||||
my $step_uniqueext = build_mock_uniqueext($run_id, ++$build_step_seq, $name);
|
||||
my $cmd = join(' ',
|
||||
'perl', sh_quote($script),
|
||||
'--mock-cfg', sh_quote($target),
|
||||
'--mock-uniqueext', sh_quote($step_uniqueext),
|
||||
'--result-dir', sh_quote($step_result),
|
||||
'--log-dir', sh_quote($step_log),
|
||||
'perl', shell_quote($script),
|
||||
'--mock-cfg', shell_quote($target),
|
||||
'--mock-uniqueext', shell_quote($step_uniqueext),
|
||||
'--result-dir', shell_quote($step_result),
|
||||
'--log-dir', shell_quote($step_log),
|
||||
# host-local, run-scoped work dir so /tmp doesn't collide between runs
|
||||
'--work-dir', sh_quote("/tmp/mockbuild-all-$run_id/$name"),
|
||||
'--work-dir', shell_quote("/tmp/mockbuild-all-$run_id/$name"),
|
||||
'--build-timestamp', $SOURCE_DATE_EPOCH,
|
||||
# goconserver generates its spec at build time (from an upstream clone), so the
|
||||
# in-tree spec Release bump above cannot reach it. Hand the CD suffix down so its
|
||||
@@ -571,11 +625,11 @@ if (!$skip_build) {
|
||||
my $cmd = "mkdir -p $mktree && $genesis_scrub && HOME=" . sh_quote($genesis_home) . ' ' . join(' ',
|
||||
'perl', sh_quote("$xcat_src/buildrpms.pl"),
|
||||
'--package', 'xCAT-genesis-base',
|
||||
'--target', sh_quote($target),
|
||||
'--target', shell_quote($target),
|
||||
'--nproc', int($nproc),
|
||||
'--force',
|
||||
'--verbose',
|
||||
'--xcat_dep_path', sh_quote($repo_root),
|
||||
'--xcat_dep_path', shell_quote($repo_root),
|
||||
);
|
||||
push @build_steps, {
|
||||
id => 'genesis',
|
||||
@@ -654,6 +708,11 @@ push @collect_roots, @extra_collect_dirs;
|
||||
@collect_roots = uniq(@collect_roots);
|
||||
my @srpm_collect_roots = uniq(@collect_roots);
|
||||
|
||||
if ($genesis_release && !$dry_run) {
|
||||
remove_genesis_packages($repo_dir, 0);
|
||||
remove_genesis_packages($srpm_repo_dir, 1);
|
||||
}
|
||||
|
||||
print_step('Collect RPM artifacts');
|
||||
print "collection roots:\n";
|
||||
print " $_\n" for @collect_roots;
|
||||
@@ -664,15 +723,26 @@ my ($copied, $skipped_src, $missing_roots) = collect_rpms(
|
||||
dry_run => $dry_run,
|
||||
);
|
||||
|
||||
# Assert on what this run BUILT, before the Genesis release is added: the release is
|
||||
# installed from a verified directory rather than built here, so counting it first would
|
||||
# let a run whose builders all failed reach createrepo and the deployable tree, and fail
|
||||
# much later in the repo gate (verify_target_repo), naming missing packages instead of the
|
||||
# failed builds.
|
||||
if (!$dry_run && $copied == 0) {
|
||||
die "No binary RPMs were collected. Check build logs and collection roots.\n";
|
||||
}
|
||||
|
||||
if ($genesis_release) {
|
||||
$copied += $dry_run
|
||||
? preview_genesis_release_packages('rpm', $repo_dir)
|
||||
: install_genesis_release_packages('rpm', $repo_dir);
|
||||
}
|
||||
|
||||
# Ensure the OS-dependent xCAT-genesis-base rpm (built by the genesis step above)
|
||||
# lands in the dep repo -- pull it individually out of the xcat-core dist tree (the
|
||||
# rest of that tree, the full xCAT core, is built + published by the xcat-core pipeline).
|
||||
if (!$skip_genesis && !$dry_run) {
|
||||
for my $g (glob("$xcat_rpms_dir/xCAT-genesis-base-*.rpm")) {
|
||||
for my $g (bsd_glob("$xcat_rpms_dir/xCAT-genesis-base-*.rpm")) {
|
||||
next if $g =~ /\.src\.rpm$/;
|
||||
copy($g, "$repo_dir/" . basename($g))
|
||||
or die "Failed to copy genesis-base $g -> $repo_dir: $!\n";
|
||||
@@ -715,6 +785,15 @@ if (!$dry_run && $copied_srpms == 0) {
|
||||
print "WARN: No source RPMs were collected. SRPM repo and tarball may be empty.\n";
|
||||
}
|
||||
|
||||
if ($genesis_release) {
|
||||
$copied_srpms += $dry_run
|
||||
? preview_genesis_release_packages('srpm', $srpm_repo_dir)
|
||||
: install_genesis_release_packages('srpm', $srpm_repo_dir);
|
||||
}
|
||||
|
||||
assert_genesis_release_copied($repo_dir, $srpm_repo_dir)
|
||||
if $genesis_release && !$dry_run;
|
||||
|
||||
if (!$skip_createrepo) {
|
||||
run_step(
|
||||
step => 'Run createrepo',
|
||||
@@ -732,8 +811,8 @@ if (!$skip_tarball) {
|
||||
my $cmd = join(' ',
|
||||
'tar', '--sort=name', '--owner=0', '--group=0',
|
||||
"--mtime=\@$SOURCE_DATE_EPOCH",
|
||||
'-C', sh_quote($run_root),
|
||||
'-czf', sh_quote($tarball),
|
||||
'-C', shell_quote($run_root),
|
||||
'-czf', shell_quote($tarball),
|
||||
'repo'
|
||||
);
|
||||
run_step(
|
||||
@@ -744,8 +823,8 @@ if (!$skip_tarball) {
|
||||
my $srpm_cmd = join(' ',
|
||||
'tar', '--sort=name', '--owner=0', '--group=0',
|
||||
"--mtime=\@$SOURCE_DATE_EPOCH",
|
||||
'-C', sh_quote($run_root),
|
||||
'-czf', sh_quote($srpm_tarball),
|
||||
'-C', shell_quote($run_root),
|
||||
'-czf', shell_quote($srpm_tarball),
|
||||
'repo-src'
|
||||
);
|
||||
run_step(
|
||||
@@ -818,10 +897,18 @@ sub deploy_target {
|
||||
remove_tree($stage) if -d $stage;
|
||||
make_path($stage);
|
||||
my $ok = eval {
|
||||
for my $rpm (glob("$src/*.rpm")) {
|
||||
for my $rpm (bsd_glob("$src/*.rpm")) {
|
||||
next if $rpm =~ /\.src\.rpm$/;
|
||||
copy($rpm, "$stage/" . basename($rpm))
|
||||
or die "Failed to copy $rpm -> $stage: $!\n";
|
||||
publish_file($rpm, "$stage/" . basename($rpm));
|
||||
}
|
||||
# --genesis-release: drop any stale OpenEmbedded Genesis rpm the collection carried over,
|
||||
# then verify the ones from THIS release still match their checksums. Both run on the STAGE
|
||||
# and BEFORE sign_and_index_repo, because rpmsign rewrites the rpm bytes the release
|
||||
# checksums cover.
|
||||
if ($genesis_release) {
|
||||
my @keep = map { basename($_) } genesis_release_files('rpm');
|
||||
remove_genesis_packages($stage, 0, \@keep);
|
||||
verify_genesis_release_packages('rpm', $stage);
|
||||
}
|
||||
sign_and_index_repo($stage);
|
||||
write_dep_repo_metadata($stage, $rel);
|
||||
@@ -853,23 +940,50 @@ sub deploy_target {
|
||||
}
|
||||
remove_tree($old) if -d $old;
|
||||
|
||||
my $n = scalar(grep { !/\.src\.rpm$/ } glob("$dest/*.rpm"));
|
||||
my $n = scalar(grep { !/\.src\.rpm$/ } bsd_glob("$dest/*.rpm"));
|
||||
print "Deployed rh$rel/$arch: $n rpms\n";
|
||||
}
|
||||
|
||||
sub publish_file {
|
||||
my ($source, $destination) = @_;
|
||||
my ($temporary_fh, $temporary) = tempfile(
|
||||
'.xcat-deploy.XXXXXX',
|
||||
DIR => dirname($destination),
|
||||
UNLINK => 0,
|
||||
);
|
||||
close($temporary_fh) or die "Cannot close deployment staging file: $!\n";
|
||||
|
||||
my $mode = (stat($source))[2];
|
||||
die "Cannot read mode from $source: $!\n" unless defined($mode);
|
||||
my $published = eval {
|
||||
copy($source, $temporary)
|
||||
or die "Failed to stage $source -> $temporary: $!\n";
|
||||
chmod($mode & 0x0fff, $temporary)
|
||||
or die "Failed to set mode on $temporary: $!\n";
|
||||
rename($temporary, $destination)
|
||||
or die "Failed to publish $temporary -> $destination: $!\n";
|
||||
1;
|
||||
};
|
||||
return if $published;
|
||||
|
||||
my $error = $@ || "Failed to publish $source\n";
|
||||
unlink($temporary) if -e $temporary || -l $temporary;
|
||||
die $error;
|
||||
}
|
||||
|
||||
# createrepo_c command with upstream-matching, deterministic metadata. The tool's
|
||||
# defaults emit primary/filelists/other as *.xml.zst plus *.sqlite.bz2 (--database),
|
||||
# exactly the upstream shape; --set-timestamp-to-revision pins repomd to SOURCE_DATE_EPOCH.
|
||||
sub createrepo_c_cmd {
|
||||
my ($dir) = @_;
|
||||
return 'createrepo_c --update --database '
|
||||
. '--revision ' . sh_quote($SOURCE_DATE_EPOCH) . ' --set-timestamp-to-revision '
|
||||
. sh_quote($dir);
|
||||
. '--revision ' . shell_quote($SOURCE_DATE_EPOCH) . ' --set-timestamp-to-revision '
|
||||
. shell_quote($dir);
|
||||
}
|
||||
|
||||
sub sign_and_index_repo {
|
||||
my ($dir) = @_;
|
||||
my @rpms = grep { !/\.src\.rpm$/ } glob("$dir/*.rpm");
|
||||
my @rpms = grep { !/\.src\.rpm$/ } bsd_glob("$dir/*.rpm");
|
||||
if ($gpg_sign && @rpms) {
|
||||
local $ENV{GNUPGHOME} = $gpg_home if $gpg_home;
|
||||
run_simple("rpmsign --define " . sh_quote("%_gpg_name $gpg_key_name") . " --addsign "
|
||||
@@ -1014,9 +1128,11 @@ Options:
|
||||
--skip-build Skip all build steps and only collect/create repo/tarballs
|
||||
--skip-xcat-dep Skip xcat-dep mockbuild.pl package steps
|
||||
--skip-perl Skip perl package build step
|
||||
--skip-genesis Skip the xCAT-genesis-base build step
|
||||
--skip-genesis Skip the existing per-EL Genesis image build
|
||||
--skip-createrepo Skip createrepo
|
||||
--skip-tarball Skip binary/SRPM tarball creation
|
||||
--genesis-release PATH Add a verified OpenEmbedded Genesis RPM release alongside the
|
||||
existing per-EL Genesis packages
|
||||
--scrub-all-chroots Run mock -r <target> --scrub=all before build/collect
|
||||
--collect-dir PATH Additional directory to scan recursively for RPMs (repeatable)
|
||||
--dry-run Print planned commands without executing
|
||||
@@ -1033,12 +1149,6 @@ Notes:
|
||||
USAGE
|
||||
}
|
||||
|
||||
|
||||
sub require_command {
|
||||
my ($cmd) = @_;
|
||||
run_simple("command -v " . sh_quote($cmd) . " >/dev/null 2>&1");
|
||||
}
|
||||
|
||||
sub run_simple {
|
||||
my ($cmd) = @_;
|
||||
my $rc = system($cmd);
|
||||
@@ -1048,18 +1158,6 @@ sub run_simple {
|
||||
}
|
||||
}
|
||||
|
||||
sub capture {
|
||||
my ($cmd) = @_;
|
||||
my $out = `$cmd`;
|
||||
my $rc = $?;
|
||||
if ($rc != 0) {
|
||||
my $exit = $rc == -1 ? 255 : ($rc >> 8);
|
||||
die "Command failed (rc=$exit): $cmd\n$out\n";
|
||||
}
|
||||
chomp $out;
|
||||
return $out;
|
||||
}
|
||||
|
||||
sub run_step {
|
||||
my (%args) = @_;
|
||||
my $step = $args{step} // 'Run command';
|
||||
@@ -1080,12 +1178,12 @@ sub run_step {
|
||||
|
||||
my $full_cmd = $cmd;
|
||||
if ($cwd) {
|
||||
$full_cmd = "cd " . sh_quote($cwd) . " && $cmd";
|
||||
$full_cmd = "cd " . shell_quote($cwd) . " && $cmd";
|
||||
}
|
||||
if ($log) {
|
||||
my $log_dir = dirname($log);
|
||||
make_path($log_dir) if !-d $log_dir;
|
||||
$full_cmd .= " > " . sh_quote($log) . " 2>&1";
|
||||
$full_cmd .= " > " . shell_quote($log) . " 2>&1";
|
||||
}
|
||||
|
||||
my $rc = system($full_cmd);
|
||||
@@ -1140,6 +1238,7 @@ sub run_build_steps_parallel {
|
||||
warn "ERROR: build step failed: $step->{step}\n" . ($@ // '');
|
||||
push @failed, (defined($step->{id}) && $step->{id} ne '' ? $step->{id} : $step->{step});
|
||||
}
|
||||
assert_build_progress(scalar(@{$steps}), scalar(@failed));
|
||||
return @failed;
|
||||
}
|
||||
|
||||
@@ -1197,9 +1296,22 @@ sub run_build_steps_parallel {
|
||||
}
|
||||
warn "ERROR: build step(s) failed:\n " . join("\n ", @lines) . "\n";
|
||||
}
|
||||
|
||||
assert_build_progress(scalar(@{$steps}), scalar(keys %failed));
|
||||
return sort keys %failed;
|
||||
}
|
||||
|
||||
# The caller enforces zero tolerance per required package (verify_target_repo). ALL steps
|
||||
# failing is a different thing: the builder itself is unusable (no mock, a broken chroot, no
|
||||
# network), this invocation produced nothing, and every package the run would go on to publish
|
||||
# would come from somewhere other than this build -- say that, instead of naming the missing
|
||||
# packages later.
|
||||
sub assert_build_progress {
|
||||
my ($attempted, $failures) = @_;
|
||||
return unless every_step_failed($attempted, $failures);
|
||||
die "FATAL: every build step failed ($failures/$attempted). Check the build logs.\n";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1449,6 +1561,99 @@ sub derive_target_from_repo_path {
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub reset_staging_repo {
|
||||
my ($directory) = @_;
|
||||
return unless -d $directory;
|
||||
opendir(my $dh, $directory) or die "Cannot read $directory: $!\n";
|
||||
my @stale = grep { /\.rpm\z/ } readdir($dh);
|
||||
closedir($dh) or die "Cannot close $directory: $!\n";
|
||||
return unless @stale;
|
||||
print "Removing " . scalar(@stale) . " package(s) left in $directory by an earlier run\n";
|
||||
for my $name (@stale) {
|
||||
unlink("$directory/$name")
|
||||
or die "Cannot remove stale package $directory/$name: $!\n";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sub remove_genesis_packages {
|
||||
my ($directory, $source, $keep_names) = @_;
|
||||
return unless -d $directory;
|
||||
my %keep = map { $_ => 1 } @{ $keep_names // [] };
|
||||
opendir(my $dh, $directory) or die "Cannot read $directory: $!\n";
|
||||
my @names = grep { /^xCAT-genesis-openembedded-.*\.rpm\z/ } readdir($dh);
|
||||
closedir($dh) or die "Cannot close $directory: $!\n";
|
||||
for my $name (@names) {
|
||||
next if $keep{$name};
|
||||
my $path = "$directory/$name";
|
||||
next if $source && $path !~ /\.src\.rpm\z/;
|
||||
next if !$source && $path =~ /\.src\.rpm\z/;
|
||||
unlink($path) or die "Cannot remove stale Genesis package $path: $!\n";
|
||||
}
|
||||
}
|
||||
|
||||
sub install_genesis_release_packages {
|
||||
my ($prefix, $destination_root) = @_;
|
||||
remove_genesis_packages($destination_root, $prefix eq 'srpm');
|
||||
|
||||
my $copied = 0;
|
||||
for my $relative (genesis_release_files($prefix)) {
|
||||
my $source = "$genesis_release/$relative";
|
||||
my $destination = "$destination_root/" . basename($relative);
|
||||
copy($source, $destination)
|
||||
or die "Cannot install Genesis release package $source: $!\n";
|
||||
verify_release_file($genesis_release_checksums, $relative, $destination);
|
||||
$copied++;
|
||||
}
|
||||
die "Genesis release has no $prefix packages\n" unless $copied;
|
||||
return $copied;
|
||||
}
|
||||
|
||||
# Dry runs copy nothing, but they must still report what a real run would publish:
|
||||
# collect_rpms and collect_srpms drop every xCAT-genesis-openembedded package whenever
|
||||
# --genesis-release is given, so without this preview a dry run describes a repository
|
||||
# with no Genesis packages at all while the real run installs the whole set.
|
||||
sub preview_genesis_release_packages {
|
||||
my ($prefix, $destination_root) = @_;
|
||||
my @files = genesis_release_files($prefix);
|
||||
die "Genesis release has no $prefix packages\n" unless @files;
|
||||
for my $relative (@files) {
|
||||
print "DRY-RUN install Genesis release package: $genesis_release/$relative"
|
||||
. " -> $destination_root/" . basename($relative) . "\n";
|
||||
}
|
||||
return scalar(@files);
|
||||
}
|
||||
|
||||
sub genesis_release_files {
|
||||
my ($prefix) = @_;
|
||||
my @files = sort grep {
|
||||
/^\Q$prefix\E\/xCAT-genesis-openembedded-[^\/]+\.rpm\z/
|
||||
} keys %{$genesis_release_checksums};
|
||||
return @files;
|
||||
}
|
||||
|
||||
sub verify_genesis_release_packages {
|
||||
my ($prefix, $destination_root) = @_;
|
||||
my @files = genesis_release_files($prefix);
|
||||
die "Genesis release has no $prefix packages\n" unless @files;
|
||||
for my $relative (@files) {
|
||||
my $destination = "$destination_root/" . basename($relative);
|
||||
verify_release_file($genesis_release_checksums, $relative, $destination);
|
||||
}
|
||||
return scalar(@files);
|
||||
}
|
||||
|
||||
sub assert_genesis_release_copied {
|
||||
my ($binary_directory, $source_directory) = @_;
|
||||
for my $entry (
|
||||
[ 'rpm', $binary_directory ],
|
||||
[ 'srpm', $source_directory ],
|
||||
) {
|
||||
my ($prefix, $destination_root) = @{$entry};
|
||||
verify_genesis_release_packages($prefix, $destination_root);
|
||||
}
|
||||
}
|
||||
|
||||
sub collect_rpms {
|
||||
my (%args) = @_;
|
||||
my $roots = $args{roots} // [];
|
||||
@@ -1483,6 +1688,8 @@ sub collect_rpms {
|
||||
next;
|
||||
}
|
||||
my $base = basename($rpm);
|
||||
next if $genesis_release
|
||||
&& $base =~ /^xCAT-genesis-openembedded-/;
|
||||
next if $seen{$base}++;
|
||||
if ($is_dry) {
|
||||
print "DRY-RUN copy: $rpm -> $dest/$base\n";
|
||||
@@ -1532,6 +1739,8 @@ sub collect_srpms {
|
||||
next;
|
||||
}
|
||||
my $base = basename($rpm);
|
||||
next if $genesis_release
|
||||
&& $base =~ /^xCAT-genesis-openembedded-/;
|
||||
next if $seen{$base}++;
|
||||
if ($is_dry) {
|
||||
print "DRY-RUN copy source: $rpm -> $dest/$base\n";
|
||||
@@ -1600,7 +1809,7 @@ sub acquire_output_lock {
|
||||
if (mkdir $lock) {
|
||||
$HELD_LOCK = $lock;
|
||||
$LOCK_OWNER_PID = $$;
|
||||
my $host = capture('uname -n') || 'unknown';
|
||||
my $host = capture_command('uname', '-n') || 'unknown';
|
||||
if (open my $fh, '>', "$lock/owner") {
|
||||
print {$fh} "host=$host\npid=$$\nepoch=" . time() . "\n";
|
||||
close $fh;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use File::Path qw(make_path);
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin;
|
||||
use Test::More;
|
||||
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use XCAT::BuildUtils qw(
|
||||
capture_command
|
||||
command_exists
|
||||
digest_file
|
||||
digest_manifest
|
||||
display_quote
|
||||
every_step_failed
|
||||
hashes_equal
|
||||
read_binary
|
||||
read_first_line
|
||||
read_lines
|
||||
relative_files
|
||||
require_command
|
||||
run_command
|
||||
shell_quote
|
||||
write_binary
|
||||
);
|
||||
|
||||
my $tmp = tempdir(CLEANUP => 1);
|
||||
write_binary("$tmp/alpha", "abc");
|
||||
make_path("$tmp/nested");
|
||||
write_binary("$tmp/nested/lines", "first\r\nsecond\n");
|
||||
|
||||
is(read_binary("$tmp/alpha"), 'abc', 'binary files round trip');
|
||||
is_deeply(
|
||||
[ read_lines("$tmp/nested/lines") ],
|
||||
[ 'first', 'second' ],
|
||||
'line reader accepts CRLF and LF',
|
||||
);
|
||||
is(read_first_line("$tmp/nested/lines"), 'first', 'first line is returned');
|
||||
is_deeply(
|
||||
[ relative_files($tmp) ],
|
||||
[ 'alpha', 'nested/lines' ],
|
||||
'regular files are sorted and relative',
|
||||
);
|
||||
|
||||
my $sha256 = 'ba7816bf8f01cfea414140de5dae2223'
|
||||
. 'b00361a396177a9cb410ff61f20015ad';
|
||||
is(digest_file("$tmp/alpha", 'sha256'), $sha256, 'SHA-256 matches a known value');
|
||||
is(digest_file("$tmp/alpha", 'md5'), '900150983cd24fb0d6963f7d28e17f72',
|
||||
'MD5 matches a known value');
|
||||
is(
|
||||
digest_manifest($tmp, 'sha256', 'alpha'),
|
||||
"$sha256 alpha\n",
|
||||
'digest manifest uses the release format',
|
||||
);
|
||||
|
||||
ok(command_exists($^X), 'current Perl interpreter is executable');
|
||||
is(capture_command($^X, '-e', 'print "captured\\n"'), 'captured',
|
||||
'command output is captured without a shell');
|
||||
ok(run_command($^X, '-e', 'exit 0'), 'successful command returns true');
|
||||
|
||||
eval { run_command($^X, '-e', 'exit 7'); 1 };
|
||||
like($@, qr/Command failed \(rc=7\)/, 'command failure reports its exit status');
|
||||
eval { require_command("xcat-missing-command-$$"); 1 };
|
||||
like($@, qr/Required command not found/, 'missing command is rejected');
|
||||
eval { digest_file("$tmp/alpha", 'unknown'); 1 };
|
||||
like($@, qr/Unsupported digest algorithm/, 'unknown digest is rejected');
|
||||
|
||||
is(display_quote('plain/value'), 'plain/value', 'simple display value is unquoted');
|
||||
is(display_quote('two words'), q{'two words'}, 'display value with spaces is quoted');
|
||||
is(shell_quote("it's"), q{'it'"'"'s'}, 'shell quote escapes apostrophes');
|
||||
ok(every_step_failed(6, 6), 'every attempted step failing is reported');
|
||||
ok(!every_step_failed(6, 5), 'one surviving step is not a total failure');
|
||||
ok(!every_step_failed(0, 0), 'attempting no steps is not a failure');
|
||||
|
||||
ok(hashes_equal({ a => 1 }, { a => 1 }), 'equal hashes match');
|
||||
ok(!hashes_equal({ a => 1 }, { a => 2 }), 'different hashes do not match');
|
||||
|
||||
my $link = "$tmp/link";
|
||||
if (symlink('alpha', $link)) {
|
||||
eval { relative_files($tmp); 1 };
|
||||
like($@, qr/Symbolic links are not allowed/, 'directory walk rejects symlinks');
|
||||
} else {
|
||||
fail('test filesystem supports symbolic links');
|
||||
}
|
||||
|
||||
done_testing();
|
||||
@@ -0,0 +1,588 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Cwd qw(abs_path);
|
||||
use File::Basename qw(basename);
|
||||
use File::Copy qw(copy);
|
||||
use File::Path qw(make_path);
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin;
|
||||
use Test::More;
|
||||
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/lib";
|
||||
use XCAT::BuildUtils qw(
|
||||
capture_command
|
||||
command_exists
|
||||
digest_file
|
||||
read_binary
|
||||
write_binary
|
||||
);
|
||||
use XCAT::GenesisRelease qw(
|
||||
architectures
|
||||
deb_package_name
|
||||
rpm_package_name
|
||||
);
|
||||
use XCAT::GenesisReleaseTest qw(
|
||||
make_export
|
||||
run_capture
|
||||
write_forkmanager_stub
|
||||
write_checksums
|
||||
write_release_manifest
|
||||
);
|
||||
|
||||
my $repo_root = abs_path("$FindBin::Bin/..");
|
||||
my $packager = "$repo_root/genesis-openembedded/package";
|
||||
my $rpm_consumer = "$repo_root/mockbuild-all.pl";
|
||||
my $deb_consumer = "$repo_root/build-apt-repo.sh";
|
||||
my $revision = 'b' x 40;
|
||||
my $version = '2.19.0';
|
||||
my $release = 'snap202608210726';
|
||||
my $epoch = 1787293573;
|
||||
my $tmp = tempdir(CLEANUP => 1);
|
||||
|
||||
if ($ENV{XCAT_GENESIS_CI}) {
|
||||
BAIL_OUT('CI requires Linux root') unless $^O eq 'linux' && $> == 0;
|
||||
for my $command (qw(apt-ftparchive bash createrepo_c dpkg-deb gpg rpm rpmbuild)) {
|
||||
BAIL_OUT("CI requires $command") unless command_exists($command);
|
||||
}
|
||||
}
|
||||
|
||||
SKIP: {
|
||||
skip 'RPM repository tools require a root Linux builder', 30
|
||||
unless $^O eq 'linux'
|
||||
&& $> == 0
|
||||
&& command_exists('rpmbuild')
|
||||
&& command_exists('rpm')
|
||||
&& command_exists('createrepo_c');
|
||||
test_rpm_consumer();
|
||||
test_legacy_rpm_consumer();
|
||||
test_partial_rpm_release();
|
||||
test_failed_build_release();
|
||||
test_skip_build_collects_results();
|
||||
test_dry_run_release();
|
||||
}
|
||||
|
||||
SKIP: {
|
||||
skip 'APT repository tools are not installed', 17
|
||||
unless $^O eq 'linux'
|
||||
&& command_exists('bash')
|
||||
&& command_exists('dpkg-deb')
|
||||
&& command_exists('apt-ftparchive')
|
||||
&& command_exists('gpg');
|
||||
test_deb_consumer();
|
||||
test_legacy_deb_consumer();
|
||||
test_partial_deb_release();
|
||||
test_apt_lock();
|
||||
}
|
||||
|
||||
done_testing();
|
||||
|
||||
# mockbuild-all.pl builds and gates each target against <repo-root>/packages-manifest.conf, and a
|
||||
# target with no section there is fatal. These fixtures use a synthetic target, and their dependency
|
||||
# packages are copies of one rpm -- so the section names the header name those copies actually carry.
|
||||
# The per-package manifest gate itself is covered by t/mockbuild-all.t; what these runs exercise is
|
||||
# the Genesis release path. The runs pass --skip-genesis rather than the removed --skip-xcat: this
|
||||
# script no longer builds the xCAT core, so xcat-core's buildrpms.pl is required only for the
|
||||
# per-EL xCAT-genesis-base build.
|
||||
sub write_target_manifest {
|
||||
my ($root, $target) = @_;
|
||||
make_path($root);
|
||||
write_binary(
|
||||
"$root/packages-manifest.conf",
|
||||
"[$target]\n" . rpm_package_name(capture_command('uname', '-m')) . "=*\n",
|
||||
);
|
||||
}
|
||||
|
||||
sub test_rpm_consumer {
|
||||
my $release_root = make_package_release("$tmp/rpm", 'rpm');
|
||||
my $package = "xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
|
||||
my $source_package = "xCAT-genesis-openembedded-x86_64-$version-$release.src.rpm";
|
||||
my $output = "$tmp/rpm output";
|
||||
my $target = 'test+epel-10-' . capture_command('uname', '-m');
|
||||
my $run = "$target-consumer";
|
||||
my $run_repo = "$output/mockbuild-all/$run/repo/" . capture_command('uname', '-m');
|
||||
my $source_repo = "$output/mockbuild-all/$run/repo-src";
|
||||
my $deploy_repo = "$output/xcat-dep/rh10/" . capture_command('uname', '-m');
|
||||
make_path($run_repo, $source_repo, $deploy_repo);
|
||||
write_binary("$run_repo/xCAT-genesis-openembedded-stale.noarch.rpm", 'stale');
|
||||
write_binary("$source_repo/xCAT-genesis-openembedded-stale.src.rpm", 'stale');
|
||||
write_binary("$deploy_repo/xCAT-genesis-openembedded-stale.noarch.rpm", 'stale');
|
||||
|
||||
my $dependencies = "$tmp/rpm-dependencies";
|
||||
my $scratch_repo_root = "$tmp/rpm-repo-root";
|
||||
make_rpm_dependencies($dependencies, "$release_root/rpm/$package");
|
||||
write_target_manifest($scratch_repo_root, $target);
|
||||
write_binary(
|
||||
"$dependencies/xCAT-genesis-openembedded-x86_64-$version-old.noarch.rpm",
|
||||
'stale OpenEmbedded RPM',
|
||||
);
|
||||
write_binary(
|
||||
"$dependencies/xCAT-genesis-openembedded-x86_64-$version-old.src.rpm",
|
||||
'stale OpenEmbedded SRPM',
|
||||
);
|
||||
|
||||
my @perl_lib;
|
||||
push(@perl_lib, write_forkmanager_stub("$tmp/perl-stub"))
|
||||
unless eval { require Parallel::ForkManager; 1 };
|
||||
push(@perl_lib, $ENV{PERL5LIB})
|
||||
if defined($ENV{PERL5LIB}) && $ENV{PERL5LIB} ne '';
|
||||
local $ENV{PERL5LIB} = join(':', @perl_lib);
|
||||
my $log = "$tmp/rpm-consumer.log";
|
||||
my $status = run_capture(
|
||||
$log,
|
||||
$^X, $rpm_consumer,
|
||||
'--repo-root', $scratch_repo_root,
|
||||
'--output', $output,
|
||||
'--target', $target,
|
||||
'--run-id', 'consumer',
|
||||
'--build-timestamp', $epoch,
|
||||
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
||||
'--skip-createrepo', '--skip-tarball',
|
||||
'--genesis-release', $release_root,
|
||||
'--collect-dir', $dependencies,
|
||||
);
|
||||
|
||||
is($status, 0, 'RPM repository accepts a verified Genesis release');
|
||||
is(digest_file("$deploy_repo/$package"), digest_file("$release_root/rpm/$package"),
|
||||
'deployed RPM matches the release');
|
||||
is(
|
||||
sprintf('%04o', (stat("$deploy_repo/$package"))[2] & 0x0fff),
|
||||
sprintf('%04o', (stat("$release_root/rpm/$package"))[2] & 0x0fff),
|
||||
'deployed RPM keeps the release file mode',
|
||||
);
|
||||
opendir(my $deploy_dh, $deploy_repo) or die $!;
|
||||
my @staging_files = grep { /^\.xcat-deploy\./ } readdir($deploy_dh);
|
||||
closedir($deploy_dh) or die $!;
|
||||
is_deeply(\@staging_files, [], 'RPM deployment leaves no staging files');
|
||||
ok(!-e "$run_repo/xCAT-genesis-openembedded-stale.noarch.rpm",
|
||||
'stale run RPM is removed');
|
||||
ok(!-e "$source_repo/xCAT-genesis-openembedded-stale.src.rpm",
|
||||
'stale source RPM is removed');
|
||||
ok(!-e "$deploy_repo/xCAT-genesis-openembedded-stale.noarch.rpm",
|
||||
'stale deployed RPM is removed');
|
||||
is(digest_file("$source_repo/$source_package"),
|
||||
digest_file("$release_root/srpm/$source_package"),
|
||||
'source RPM matches the release');
|
||||
ok(-f "$deploy_repo/repodata/repomd.xml", 'RPM repository metadata is generated');
|
||||
ok(-f "$deploy_repo/xCAT-genesis-base-x86_64-1.noarch.rpm",
|
||||
'legacy Genesis package remains available');
|
||||
ok(!-e "$run_repo/xCAT-genesis-openembedded-x86_64-$version-old.noarch.rpm",
|
||||
'stale OpenEmbedded RPM is not collected');
|
||||
ok(!-e "$source_repo/xCAT-genesis-openembedded-x86_64-$version-old.src.rpm",
|
||||
'stale OpenEmbedded SRPM is not collected');
|
||||
like(read_binary("$output/mockbuild-all/$run/summary.txt"), qr/^copied_rpms=15$/m,
|
||||
'release RPM is counted alongside required dependencies');
|
||||
}
|
||||
|
||||
sub test_deb_consumer {
|
||||
my $release_root = make_package_release("$tmp/deb", 'deb');
|
||||
my $package = "xcat-genesis-openembedded-x86-64_${version}-${release}_all.deb";
|
||||
my $apt_root = "$tmp/apt";
|
||||
my $input = "$apt_root/ubuntu24.04";
|
||||
make_path($input, "$apt_root/pool/main/noble");
|
||||
make_legacy_deb("$tmp/dummy-deb", "$input/xcat-genesis-base-amd64_1_all.deb");
|
||||
write_binary("$input/xcat-genesis-openembedded-stale.deb", 'stale');
|
||||
write_binary("$apt_root/pool/main/noble/xcat-genesis-openembedded-old.deb", 'stale');
|
||||
|
||||
local $ENV{SOURCE_DATE_EPOCH} = $epoch;
|
||||
my $log = "$tmp/deb-consumer.log";
|
||||
my $status = run_capture(
|
||||
$log,
|
||||
'bash', $deb_consumer,
|
||||
'--repo-root', $repo_root,
|
||||
'--apt-dir', $apt_root,
|
||||
'--skip-sign',
|
||||
'--genesis-release', $release_root,
|
||||
'ubuntu24.04',
|
||||
);
|
||||
my $pool_package = "$apt_root/pool/main/noble/$package";
|
||||
my $amd64 = "$apt_root/dists/noble/main/binary-amd64/Packages";
|
||||
my $ppc64el = "$apt_root/dists/noble/main/binary-ppc64el/Packages";
|
||||
|
||||
is($status, 0, 'APT repository accepts a verified Genesis release');
|
||||
like(read_binary($log), qr/Verified copied Genesis package:/,
|
||||
'APT repository uses the shared copied-package verifier');
|
||||
is(digest_file($pool_package), digest_file("$release_root/deb/$package"),
|
||||
'pooled DEB matches the release');
|
||||
ok(!-e "$apt_root/pool/main/noble/xcat-genesis-openembedded-old.deb",
|
||||
'stale pooled DEB is removed');
|
||||
like(read_binary($amd64), qr/^Package: xcat-genesis-openembedded-x86-64$/m,
|
||||
'all-architecture DEB is indexed for amd64');
|
||||
like(read_binary($ppc64el), qr/^Package: xcat-genesis-openembedded-x86-64$/m,
|
||||
'all-architecture DEB is indexed for ppc64el');
|
||||
ok(-f "$apt_root/dists/noble/Release", 'APT Release metadata is generated');
|
||||
like(read_binary($amd64), qr/^Package: xcat-genesis-base-amd64$/m,
|
||||
'legacy Genesis DEB remains available');
|
||||
ok(!-e "$apt_root/pool/main/noble/xcat-genesis-openembedded-stale.deb",
|
||||
'stale OpenEmbedded DEB is not collected');
|
||||
# The published package must be a file of its own: sharing an inode with the release
|
||||
# would make a later write through either path change what the other one holds.
|
||||
my @pooled = stat($pool_package);
|
||||
my @released = stat("$release_root/deb/$package");
|
||||
isnt("$pooled[0]:$pooled[1]", "$released[0]:$released[1]",
|
||||
'pooled DEB is published independently of the release file');
|
||||
like(read_binary($log), qr/^Re-verified pooled Genesis package: \Q$pool_package\E$/m,
|
||||
'pooled DEB is verified again before the indexes are generated');
|
||||
|
||||
my $collision = "$tmp/apt-collision";
|
||||
make_path("$collision/ubuntu24.04");
|
||||
write_binary("$collision/ubuntu24.04/$package", 'different');
|
||||
my $collision_log = "$tmp/deb-collision.log";
|
||||
my $collision_status = run_capture(
|
||||
$collision_log,
|
||||
'bash', $deb_consumer,
|
||||
'--repo-root', $repo_root,
|
||||
'--apt-dir', $collision,
|
||||
'--skip-sign',
|
||||
'--genesis-release', $release_root,
|
||||
'ubuntu24.04',
|
||||
);
|
||||
is($collision_status, 0, 'verified release replaces a stale source package');
|
||||
is(digest_file("$collision/pool/main/noble/$package"),
|
||||
digest_file("$release_root/deb/$package"),
|
||||
'pooled package still matches the verified release');
|
||||
}
|
||||
|
||||
sub test_legacy_rpm_consumer {
|
||||
my $release_root = make_package_release("$tmp/rpm-legacy", 'rpm', 'x86_64');
|
||||
my $package = "xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
|
||||
my $dependencies = "$tmp/rpm-legacy-dependencies";
|
||||
my $output = "$tmp/rpm-legacy-output";
|
||||
my $scratch_repo_root = "$tmp/rpm-legacy-repo-root";
|
||||
my $target = 'test+epel-10-' . capture_command('uname', '-m');
|
||||
my $deployed = "$output/xcat-dep/rh10/" . capture_command('uname', '-m');
|
||||
make_rpm_dependencies($dependencies, "$release_root/rpm/$package");
|
||||
write_target_manifest($scratch_repo_root, $target);
|
||||
write_binary("$scratch_repo_root/Gitepoch", '');
|
||||
|
||||
my @perl_lib;
|
||||
push(@perl_lib, write_forkmanager_stub("$tmp/perl-legacy-stub"))
|
||||
unless eval { require Parallel::ForkManager; 1 };
|
||||
push(@perl_lib, $ENV{PERL5LIB})
|
||||
if defined($ENV{PERL5LIB}) && $ENV{PERL5LIB} ne '';
|
||||
local $ENV{PERL5LIB} = join(':', @perl_lib);
|
||||
|
||||
my $log = "$tmp/rpm-legacy-consumer.log";
|
||||
my $status = run_capture(
|
||||
$log,
|
||||
$^X, $rpm_consumer,
|
||||
'--repo-root', $scratch_repo_root,
|
||||
'--output', $output,
|
||||
'--target', $target,
|
||||
'--run-id', 'legacy',
|
||||
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
||||
'--skip-createrepo', '--skip-tarball',
|
||||
'--collect-dir', $dependencies,
|
||||
);
|
||||
|
||||
is($status, 0, 'RPM legacy repository accepts an empty Gitepoch');
|
||||
ok(-f "$deployed/xCAT-genesis-base-x86_64-1.noarch.rpm",
|
||||
'RPM legacy path still deploys the existing Genesis package');
|
||||
is_deeply(
|
||||
[ glob("$deployed/xCAT-genesis-openembedded-*.rpm") ],
|
||||
[],
|
||||
'RPM legacy path does not add OpenEmbedded packages',
|
||||
);
|
||||
}
|
||||
|
||||
sub test_legacy_deb_consumer {
|
||||
my $apt_root = "$tmp/apt-legacy";
|
||||
my $input = "$apt_root/ubuntu24.04";
|
||||
make_path($input);
|
||||
make_legacy_deb(
|
||||
"$tmp/legacy-dummy-deb",
|
||||
"$input/xcat-genesis-base-amd64_1_all.deb",
|
||||
);
|
||||
|
||||
local $ENV{SOURCE_DATE_EPOCH} = $epoch;
|
||||
my $log = "$tmp/deb-legacy-consumer.log";
|
||||
my $status = run_capture(
|
||||
$log,
|
||||
'bash', $deb_consumer,
|
||||
'--repo-root', $repo_root,
|
||||
'--apt-dir', $apt_root,
|
||||
'--skip-sign',
|
||||
'ubuntu24.04',
|
||||
);
|
||||
my $packages = "$apt_root/dists/noble/main/binary-amd64/Packages";
|
||||
|
||||
is($status, 0, 'APT repository keeps working without a Genesis release');
|
||||
like(read_binary($packages), qr/^Package: xcat-genesis-base-amd64$/m,
|
||||
'APT legacy path still indexes the existing Genesis package');
|
||||
unlike(read_binary($packages), qr/^Package: xcat-genesis-openembedded-/m,
|
||||
'APT legacy path does not add OpenEmbedded packages');
|
||||
}
|
||||
|
||||
sub test_partial_rpm_release {
|
||||
my $release_root = make_package_release("$tmp/rpm-partial", 'rpm', 'x86_64');
|
||||
my $output = "$tmp/partial-output";
|
||||
my $target = 'test+epel-10-' . capture_command('uname', '-m');
|
||||
my $deployed = "$output/xcat-dep/rh10/" . capture_command('uname', '-m');
|
||||
my $existing = "$deployed/xCAT-genesis-base-existing.noarch.rpm";
|
||||
make_path($deployed);
|
||||
write_binary($existing, 'existing release');
|
||||
|
||||
my $log = "$tmp/rpm-partial.log";
|
||||
my $status = run_capture(
|
||||
$log,
|
||||
$^X, $rpm_consumer,
|
||||
'--repo-root', $repo_root,
|
||||
'--output', $output,
|
||||
'--target', $target,
|
||||
'--run-id', 'partial',
|
||||
'--build-timestamp', $epoch,
|
||||
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
||||
'--skip-createrepo', '--skip-tarball',
|
||||
'--genesis-release', $release_root,
|
||||
);
|
||||
|
||||
isnt($status, 0, 'RPM repository rejects a partial Genesis release');
|
||||
like(read_binary($log), qr/Genesis release is missing supported architectures/,
|
||||
'RPM partial-release failure names the missing architectures');
|
||||
ok(-f $existing, 'partial release does not remove the deployed package');
|
||||
}
|
||||
|
||||
sub test_partial_deb_release {
|
||||
my $release_root = make_package_release("$tmp/deb-partial", 'deb', 'x86_64');
|
||||
my $apt_root = "$tmp/partial-apt";
|
||||
my $pool = "$apt_root/pool/main/noble";
|
||||
my $existing = "$pool/xcat-genesis-base-existing.deb";
|
||||
make_path($pool);
|
||||
write_binary($existing, 'existing release');
|
||||
|
||||
my $log = "$tmp/deb-partial.log";
|
||||
my $status = run_capture(
|
||||
$log,
|
||||
'bash', $deb_consumer,
|
||||
'--repo-root', $repo_root,
|
||||
'--apt-dir', $apt_root,
|
||||
'--skip-sign',
|
||||
'--genesis-release', $release_root,
|
||||
'ubuntu24.04',
|
||||
);
|
||||
|
||||
isnt($status, 0, 'APT repository rejects a partial Genesis release');
|
||||
like(read_binary($log), qr/Genesis release is missing supported architectures/,
|
||||
'APT partial-release failure names the missing architectures');
|
||||
ok(-f $existing, 'partial DEB release does not remove the deployed package');
|
||||
}
|
||||
|
||||
sub test_failed_build_release {
|
||||
my $release_root = make_package_release("$tmp/rpm-empty", 'rpm');
|
||||
my $package = "xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
|
||||
my $output = "$tmp/empty-output";
|
||||
my $target = 'test+epel-10-' . capture_command('uname', '-m');
|
||||
my $deployed = "$output/xcat-dep/rh10/" . capture_command('uname', '-m');
|
||||
my $scratch_repo_root = "$tmp/rpm-empty-root";
|
||||
my $collected = "$tmp/rpm-empty-collect";
|
||||
my $run_repo = "$output/mockbuild-all/$target-empty/repo/" . capture_command('uname', '-m');
|
||||
my $stale = "$run_repo/ipmitool-xcat-0-stale.noarch.rpm";
|
||||
make_path($deployed, $scratch_repo_root, $collected, $run_repo);
|
||||
write_target_manifest($scratch_repo_root, $target);
|
||||
write_binary($stale, 'package left by an earlier run');
|
||||
|
||||
my $log = "$tmp/rpm-empty.log";
|
||||
my $status = run_capture(
|
||||
$log,
|
||||
$^X, $rpm_consumer,
|
||||
'--repo-root', $scratch_repo_root,
|
||||
'--output', $output,
|
||||
'--target', $target,
|
||||
'--run-id', 'empty',
|
||||
'--build-timestamp', $epoch,
|
||||
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
||||
'--skip-createrepo', '--skip-tarball',
|
||||
'--genesis-release', $release_root,
|
||||
'--collect-dir', $collected,
|
||||
);
|
||||
|
||||
isnt($status, 0, 'a run that built nothing fails even with a Genesis release');
|
||||
like(read_binary($log), qr/No binary RPMs were collected/,
|
||||
'the failure names the empty collection, not the missing dependencies');
|
||||
ok(!-e "$deployed/$package",
|
||||
'a run that built nothing publishes no release package');
|
||||
ok(!-e $stale,
|
||||
'a package left by an earlier run is cleared from the staging repository');
|
||||
}
|
||||
|
||||
# --skip-build collects THIS target's previously built artifacts out of its own build-results tree,
|
||||
# so a run that skips building must not destroy the tree it collects from. (Where this test came
|
||||
# from the --skip-build roots were the legacy build-output/list* directories and build-results was
|
||||
# only ever kept, never collected -- which is why the empty-collection case above can no longer
|
||||
# leave an rpm there.)
|
||||
sub test_skip_build_collects_results {
|
||||
my $release_root = make_package_release("$tmp/rpm-kept", 'rpm');
|
||||
my $package = "xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
|
||||
my $output = "$tmp/kept-output";
|
||||
my $target = 'test+epel-10-' . capture_command('uname', '-m');
|
||||
my $scratch_repo_root = "$tmp/rpm-kept-root";
|
||||
my $results = "$output/mockbuild-all/$target-kept/build-results/ipmitool-xcat";
|
||||
my $kept = "$results/ipmitool-xcat-1.noarch.rpm";
|
||||
my $deployed = "$output/xcat-dep/rh10/" . capture_command('uname', '-m');
|
||||
make_path($results);
|
||||
copy("$release_root/rpm/$package", $kept) or die $!;
|
||||
write_target_manifest($scratch_repo_root, $target);
|
||||
|
||||
my $log = "$tmp/rpm-kept.log";
|
||||
my $status = run_capture(
|
||||
$log,
|
||||
$^X, $rpm_consumer,
|
||||
'--repo-root', $scratch_repo_root,
|
||||
'--output', $output,
|
||||
'--target', $target,
|
||||
'--run-id', 'kept',
|
||||
'--build-timestamp', $epoch,
|
||||
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
||||
'--skip-createrepo', '--skip-tarball',
|
||||
);
|
||||
|
||||
is($status, 0, 'a run that skips building collects its own build results');
|
||||
ok(-e $kept, 'the build results it collects from are kept');
|
||||
ok(-e "$deployed/" . basename($kept), 'the collected package reaches the deployed repo');
|
||||
}
|
||||
|
||||
sub test_dry_run_release {
|
||||
my $release_root = make_package_release("$tmp/rpm-dry", 'rpm');
|
||||
my $package = "xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
|
||||
my $output = "$tmp/dry-output";
|
||||
my $target = 'test+epel-10-' . capture_command('uname', '-m');
|
||||
my $run_repo = "$output/mockbuild-all/$target-dry/repo/" . capture_command('uname', '-m');
|
||||
my $dependencies = "$tmp/rpm-dry-dependencies";
|
||||
my $scratch_repo_root = "$tmp/rpm-dry-root";
|
||||
make_rpm_dependencies($dependencies, "$release_root/rpm/$package");
|
||||
write_target_manifest($scratch_repo_root, $target);
|
||||
|
||||
my $log = "$tmp/rpm-dry.log";
|
||||
my $status = run_capture(
|
||||
$log,
|
||||
$^X, $rpm_consumer,
|
||||
'--repo-root', $scratch_repo_root,
|
||||
'--output', $output,
|
||||
'--target', $target,
|
||||
'--run-id', 'dry',
|
||||
'--build-timestamp', $epoch,
|
||||
'--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl',
|
||||
'--skip-createrepo', '--skip-tarball',
|
||||
'--genesis-release', $release_root,
|
||||
'--collect-dir', $dependencies,
|
||||
'--dry-run',
|
||||
);
|
||||
my $printed = read_binary($log);
|
||||
|
||||
is($status, 0, 'a dry run accepts a verified Genesis release');
|
||||
like($printed, qr/^DRY-RUN install Genesis release package: .*\Q$package\E$/m,
|
||||
'the dry run reports the release packages it would install');
|
||||
like($printed, qr/^Collected binary RPMs: 15$/m,
|
||||
'the dry run counts the release packages a real run installs');
|
||||
ok(!-e "$run_repo/$package", 'the dry run installs nothing');
|
||||
}
|
||||
|
||||
sub test_apt_lock {
|
||||
my $apt_root = "$tmp/apt-lock";
|
||||
my $input = "$apt_root/ubuntu24.04";
|
||||
make_path($input, "$apt_root/.lock");
|
||||
make_legacy_deb("$tmp/lock-deb", "$input/xcat-genesis-base-amd64_1_all.deb");
|
||||
|
||||
my $locked_log = "$tmp/deb-locked.log";
|
||||
my $locked_status = run_capture(
|
||||
$locked_log,
|
||||
'bash', $deb_consumer,
|
||||
'--repo-root', $repo_root,
|
||||
'--apt-dir', $apt_root,
|
||||
'--skip-sign',
|
||||
'ubuntu24.04',
|
||||
);
|
||||
isnt($locked_status, 0, 'a locked APT directory is not published into');
|
||||
like(read_binary($locked_log), qr/\Q$apt_root\E is locked/,
|
||||
'the refusal names the directory another run owns');
|
||||
|
||||
my $forced_log = "$tmp/deb-forced.log";
|
||||
my $forced_status = run_capture(
|
||||
$forced_log,
|
||||
'bash', $deb_consumer,
|
||||
'--repo-root', $repo_root,
|
||||
'--apt-dir', $apt_root,
|
||||
'--skip-sign',
|
||||
'--force-unlock',
|
||||
'ubuntu24.04',
|
||||
);
|
||||
is($forced_status, 0, '--force-unlock takes over a stale lock');
|
||||
ok(!-d "$apt_root/.lock", 'the lock is released when the run finishes');
|
||||
}
|
||||
|
||||
sub make_package_release {
|
||||
my ($root, $format, @requested_architectures) = @_;
|
||||
@requested_architectures = architectures() unless @requested_architectures;
|
||||
my $release_root = "$root/release";
|
||||
make_path($release_root);
|
||||
for my $architecture (@requested_architectures) {
|
||||
my $export = make_export("$root/exports/$architecture", $architecture);
|
||||
my $packages = "$root/packages/$architecture";
|
||||
die "Cannot package test release for $architecture\n"
|
||||
if run_capture(
|
||||
"$root/package-$architecture.log",
|
||||
$packager,
|
||||
'--architecture', $architecture,
|
||||
'--export-dir', $export,
|
||||
'--output-dir', $packages,
|
||||
'--version', $version,
|
||||
'--release', $release,
|
||||
'--revision', $revision,
|
||||
'--source-date-epoch', $epoch,
|
||||
'--format', $format,
|
||||
);
|
||||
if ($format eq 'rpm') {
|
||||
my $name = rpm_package_name($architecture);
|
||||
make_path("$release_root/rpm", "$release_root/srpm");
|
||||
copy(
|
||||
"$packages/rpm/$name-$version-$release.noarch.rpm",
|
||||
"$release_root/rpm/$name-$version-$release.noarch.rpm",
|
||||
) or die $!;
|
||||
copy(
|
||||
"$packages/srpm/$name-$version-$release.src.rpm",
|
||||
"$release_root/srpm/$name-$version-$release.src.rpm",
|
||||
) or die $!;
|
||||
} else {
|
||||
my $name = deb_package_name($architecture);
|
||||
make_path("$release_root/deb");
|
||||
copy(
|
||||
"$packages/deb/${name}_${version}-${release}_all.deb",
|
||||
"$release_root/deb/${name}_${version}-${release}_all.deb",
|
||||
) or die $!;
|
||||
}
|
||||
}
|
||||
write_release_manifest(
|
||||
$release_root, $version, $release, $revision, $epoch,
|
||||
join(',', @requested_architectures), $format,
|
||||
);
|
||||
write_checksums($release_root);
|
||||
return $release_root;
|
||||
}
|
||||
|
||||
sub make_rpm_dependencies {
|
||||
my ($directory, $package) = @_;
|
||||
make_path($directory);
|
||||
for my $name (qw(
|
||||
ipmitool-xcat syslinux-xcat grub2-xcat xnba-undi
|
||||
perl-IO-Stty perl-HTTP-Async perl-Net-HTTPS-NB
|
||||
xCAT-genesis-base-x86_64
|
||||
)) {
|
||||
copy($package, "$directory/$name-1.noarch.rpm") or die $!;
|
||||
}
|
||||
}
|
||||
|
||||
sub make_legacy_deb {
|
||||
my ($root, $output) = @_;
|
||||
make_path("$root/DEBIAN");
|
||||
write_binary(
|
||||
"$root/DEBIAN/control",
|
||||
"Package: xcat-genesis-base-amd64\nVersion: 1\nArchitecture: all\n"
|
||||
. "Maintainer: xCAT <xcat-user\@lists.sourceforge.net>\n"
|
||||
. "Description: repository test package\n",
|
||||
);
|
||||
die "Cannot build legacy test DEB\n"
|
||||
if run_capture(
|
||||
"$root.log", 'dpkg-deb', '--root-owner-group', '--build', $root, $output,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Cwd qw(abs_path);
|
||||
use File::Copy qw(copy);
|
||||
use File::Path qw(make_path remove_tree);
|
||||
use File::Temp qw(tempdir);
|
||||
use FindBin;
|
||||
use Test::More;
|
||||
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use lib "$FindBin::Bin/lib";
|
||||
use XCAT::BuildUtils qw(
|
||||
command_exists
|
||||
digest_file
|
||||
read_binary
|
||||
write_binary
|
||||
);
|
||||
use XCAT::GenesisRelease qw(
|
||||
architectures
|
||||
deb_package_name
|
||||
rpm_package_name
|
||||
validated_release_checksums
|
||||
validate_architecture
|
||||
validate_complete_release
|
||||
validate_export
|
||||
validate_release
|
||||
verify_release_file
|
||||
);
|
||||
use XCAT::GenesisReleaseTest qw(
|
||||
copy_tree
|
||||
dies_like
|
||||
make_export
|
||||
run_capture
|
||||
write_checksums
|
||||
write_release_manifest
|
||||
);
|
||||
|
||||
my $repo_root = abs_path("$FindBin::Bin/..");
|
||||
my $packager = "$repo_root/genesis-openembedded/package";
|
||||
my $builder = "$repo_root/genesis-openembedded/build";
|
||||
my $verifier = "$repo_root/genesis-openembedded/verify-release";
|
||||
my $revision = 'a' x 40;
|
||||
my $version = '2.19.0';
|
||||
my $release = 'snap202608210726';
|
||||
my $epoch = 1787293573;
|
||||
|
||||
if ($ENV{XCAT_GENESIS_CI}) {
|
||||
for my $command (qw(git dpkg-deb rpm rpmbuild tar)) {
|
||||
BAIL_OUT("CI requires $command") unless command_exists($command);
|
||||
}
|
||||
}
|
||||
|
||||
is_deeply(
|
||||
[ architectures() ],
|
||||
[ qw(x86 x86_64 ppc64 ppc64le armv7hf aarch64 riscv64) ],
|
||||
'supported architectures keep their exact xCAT names',
|
||||
);
|
||||
is(rpm_package_name('ppc64le'), 'xCAT-genesis-openembedded-ppc64le',
|
||||
'RPM package keeps ppc64le distinct');
|
||||
is(deb_package_name('x86_64'), 'xcat-genesis-openembedded-x86-64',
|
||||
'DEB package uses a legal spelling of x86_64');
|
||||
dies_like(sub { validate_architecture('ppc') }, qr/Unsupported Genesis architecture/,
|
||||
'legacy ppc alias is rejected');
|
||||
|
||||
my $tmp = tempdir(CLEANUP => 1);
|
||||
my $export = make_export("$tmp/export", 'x86_64');
|
||||
ok(validate_export($export, 'x86_64'), 'valid export passes');
|
||||
dies_like(sub { validate_export($export, 'ppc64le') }, qr/architecture mismatch/,
|
||||
'wrong architecture fails');
|
||||
|
||||
my $missing = make_export("$tmp/missing", 'x86_64');
|
||||
unlink("$missing/image.vex.json") or die $!;
|
||||
write_checksums($missing);
|
||||
dies_like(sub { validate_export($missing, 'x86_64') }, qr/missing image\.vex\.json/,
|
||||
'missing release evidence fails');
|
||||
|
||||
my $corrupt = make_export("$tmp/corrupt", 'x86_64');
|
||||
write_binary("$corrupt/kernel", 'changed');
|
||||
dies_like(sub { validate_export($corrupt, 'x86_64') }, qr/Checksum mismatch for kernel/,
|
||||
'corrupt payload fails');
|
||||
|
||||
my $unexpected = make_export("$tmp/unexpected", 'x86_64');
|
||||
write_binary("$unexpected/extra", 'not part of the export');
|
||||
write_checksums($unexpected);
|
||||
dies_like(sub { validate_export($unexpected, 'x86_64') }, qr/Unexpected Genesis export file/,
|
||||
'unlisted export files fail');
|
||||
|
||||
my $linked = make_export("$tmp/linked", 'x86_64');
|
||||
unlink("$linked/kernel") or die $!;
|
||||
symlink('initramfs.cpio.gz', "$linked/kernel") or die $!;
|
||||
dies_like(sub { validate_export($linked, 'x86_64') }, qr/Symbolic links are not allowed/,
|
||||
'export symlinks fail');
|
||||
|
||||
my $riscv = make_export("$tmp/riscv", 'riscv64');
|
||||
ok(-f "$riscv/fw_jump.elf", 'RISC-V export carries firmware');
|
||||
ok(validate_export($riscv, 'riscv64'), 'RISC-V export passes');
|
||||
|
||||
my $release_dir = "$tmp/release";
|
||||
make_path("$release_dir/rpm", "$release_dir/srpm", "$release_dir/deb");
|
||||
for my $architecture (qw(x86_64 ppc64le)) {
|
||||
my $rpm = rpm_package_name($architecture);
|
||||
my $deb = deb_package_name($architecture);
|
||||
write_binary("$release_dir/rpm/$rpm-$version-$release.noarch.rpm", "rpm $architecture");
|
||||
write_binary("$release_dir/srpm/$rpm-$version-$release.src.rpm", "srpm $architecture");
|
||||
write_binary("$release_dir/deb/${deb}_${version}-${release}_all.deb", "deb $architecture");
|
||||
}
|
||||
write_release_manifest(
|
||||
$release_dir, $version, $release, $revision, $epoch,
|
||||
'x86_64,ppc64le', 'deb,rpm',
|
||||
);
|
||||
write_checksums($release_dir);
|
||||
my $manifest = validate_release($release_dir);
|
||||
is($manifest->{xcat_revision}, $revision, 'release records xcat-core revision');
|
||||
my $qualified_release = "$tmp/qualified-release";
|
||||
my $qualified_release_name = '1+deb~1';
|
||||
my $qualified_deb = deb_package_name('x86_64');
|
||||
make_path("$qualified_release/deb");
|
||||
write_binary(
|
||||
"$qualified_release/deb/${qualified_deb}_${version}-${qualified_release_name}_all.deb",
|
||||
'deb x86_64',
|
||||
);
|
||||
write_release_manifest(
|
||||
$qualified_release, $version, $qualified_release_name, $revision, $epoch,
|
||||
'x86_64', 'deb',
|
||||
);
|
||||
write_checksums($qualified_release);
|
||||
ok(validate_release($qualified_release), 'package filenames accept valid release qualifiers');
|
||||
my $checksum_reads = 0;
|
||||
my $verified_checksums;
|
||||
{
|
||||
no warnings 'redefine';
|
||||
my $reader = \&XCAT::GenesisRelease::read_checksum_manifest;
|
||||
local *XCAT::GenesisRelease::read_checksum_manifest = sub {
|
||||
$checksum_reads++;
|
||||
return $reader->(@_);
|
||||
};
|
||||
$verified_checksums = validated_release_checksums($release_dir);
|
||||
}
|
||||
is($checksum_reads, 1, 'validated checksums use the verified manifest read');
|
||||
my $verified_relative = "rpm/xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
|
||||
my $verified_copy = "$tmp/verified-copy.rpm";
|
||||
copy("$release_dir/$verified_relative", $verified_copy) or die $!;
|
||||
ok(verify_release_file($verified_checksums, $verified_relative, $verified_copy),
|
||||
'collected package matches the verified release');
|
||||
write_binary($verified_copy, 'changed after verification');
|
||||
dies_like(
|
||||
sub { verify_release_file($verified_checksums, $verified_relative, $verified_copy) },
|
||||
qr/Collected release file checksum mismatch/,
|
||||
'release changes after verification are rejected',
|
||||
);
|
||||
copy("$release_dir/$verified_relative", $verified_copy) or die $!;
|
||||
my $copied_file_log = "$tmp/copied-file.log";
|
||||
is(
|
||||
run_capture(
|
||||
$copied_file_log,
|
||||
$verifier,
|
||||
'--checksum-file', "$release_dir/SHA256SUMS",
|
||||
'--relative-file', $verified_relative,
|
||||
'--copied-file', $verified_copy,
|
||||
),
|
||||
0,
|
||||
'verifier accepts a copied release file',
|
||||
);
|
||||
write_binary($verified_copy, 'changed after verification');
|
||||
isnt(
|
||||
run_capture(
|
||||
$copied_file_log,
|
||||
$verifier,
|
||||
'--checksum-file', "$release_dir/SHA256SUMS",
|
||||
'--relative-file', $verified_relative,
|
||||
'--copied-file', $verified_copy,
|
||||
),
|
||||
0,
|
||||
'verifier rejects a changed copied file',
|
||||
);
|
||||
like(
|
||||
read_binary($copied_file_log),
|
||||
qr/Collected release file checksum mismatch/,
|
||||
'copied-file failure names the checksum mismatch',
|
||||
);
|
||||
dies_like(
|
||||
sub { validate_complete_release($release_dir) },
|
||||
qr/Genesis release is missing supported architectures/,
|
||||
'partial release cannot be published',
|
||||
);
|
||||
|
||||
my $complete_release = "$tmp/complete-release";
|
||||
make_path("$complete_release/rpm", "$complete_release/srpm", "$complete_release/deb");
|
||||
for my $architecture (architectures()) {
|
||||
my $rpm = rpm_package_name($architecture);
|
||||
my $deb = deb_package_name($architecture);
|
||||
write_binary("$complete_release/rpm/$rpm-$version-$release.noarch.rpm", "rpm $architecture");
|
||||
write_binary("$complete_release/srpm/$rpm-$version-$release.src.rpm", "srpm $architecture");
|
||||
write_binary("$complete_release/deb/${deb}_${version}-${release}_all.deb", "deb $architecture");
|
||||
}
|
||||
write_release_manifest(
|
||||
$complete_release, $version, $release, $revision, $epoch,
|
||||
join(',', architectures()), 'deb,rpm',
|
||||
);
|
||||
write_checksums($complete_release);
|
||||
ok(validate_complete_release($complete_release), 'complete release can be published');
|
||||
|
||||
my $deb_only_release = "$tmp/deb-only-release";
|
||||
make_path("$deb_only_release/deb");
|
||||
for my $architecture (architectures()) {
|
||||
my $deb = deb_package_name($architecture);
|
||||
write_binary("$deb_only_release/deb/${deb}_${version}-${release}_all.deb", "deb $architecture");
|
||||
}
|
||||
write_release_manifest(
|
||||
$deb_only_release, $version, $release, $revision, $epoch,
|
||||
join(',', architectures()), 'deb',
|
||||
);
|
||||
write_checksums($deb_only_release);
|
||||
my $verify_all_log = "$tmp/verify-all.log";
|
||||
isnt(run_capture($verify_all_log, $verifier, $deb_only_release), 0,
|
||||
'all-format verification rejects a single-format release');
|
||||
like(read_binary($verify_all_log), qr/Release does not contain rpm packages/,
|
||||
'all-format failure names the missing format');
|
||||
|
||||
my $bad_release = "$tmp/bad-release";
|
||||
copy_tree($release_dir, $bad_release);
|
||||
write_binary("$bad_release/rpm/stale.rpm", 'stale');
|
||||
write_checksums($bad_release);
|
||||
dies_like(sub { validate_release($bad_release) }, qr/Unexpected Genesis release artifact/,
|
||||
'stale package fails');
|
||||
|
||||
my $missing_release = "$tmp/missing-release";
|
||||
copy_tree($release_dir, $missing_release);
|
||||
unlink("$missing_release/rpm/xCAT-genesis-openembedded-ppc64le-$version-$release.noarch.rpm") or die $!;
|
||||
write_checksums($missing_release);
|
||||
dies_like(sub { validate_release($missing_release) }, qr/Genesis release is missing/,
|
||||
'incomplete architecture set fails');
|
||||
|
||||
SKIP: {
|
||||
skip 'git is not installed', 2 unless command_exists('git');
|
||||
my $source = "$tmp/dirty-xcat-core";
|
||||
make_path("$source/xCAT-genesis-builder/oe");
|
||||
write_binary("$source/Version", "$version\n");
|
||||
write_binary("$source/xCAT-genesis-builder/oe/build", "#!/bin/sh\nexit 99\n");
|
||||
write_binary("$source/xCAT-genesis-builder/oe/export", "#!/bin/sh\nexit 99\n");
|
||||
for my $command (
|
||||
[ 'git', '-C', $source, 'init', '-q' ],
|
||||
[ 'git', '-C', $source, 'add', '.' ],
|
||||
[ 'git', '-C', $source, '-c', 'user.name=xCAT test',
|
||||
'-c', 'user.email=xcat-test@example.invalid', 'commit', '-qm', 'fixture' ],
|
||||
) {
|
||||
die "Cannot prepare test repository\n"
|
||||
if run_capture("$tmp/git-fixture.log", @{$command});
|
||||
}
|
||||
write_binary("$source/untracked", "not part of the commit\n");
|
||||
my $log = "$tmp/dirty-source.log";
|
||||
isnt(
|
||||
run_capture(
|
||||
$log, $builder, '--xcat-source', $source,
|
||||
'--output-dir', "$tmp/dirty-output",
|
||||
),
|
||||
0,
|
||||
'release builder rejects untracked source files',
|
||||
);
|
||||
like(read_binary($log), qr/xcat-core checkout is not clean/,
|
||||
'dirty checkout failure is explicit');
|
||||
}
|
||||
|
||||
SKIP: {
|
||||
skip 'rpmbuild and rpm are not installed', 18
|
||||
unless command_exists('rpmbuild') && command_exists('rpm');
|
||||
exercise_packager('rpm');
|
||||
}
|
||||
|
||||
SKIP: {
|
||||
skip 'rpmbuild and rpm are not installed', 2
|
||||
unless command_exists('rpmbuild') && command_exists('rpm');
|
||||
skip 'root can traverse an unsearchable working directory', 2 if $> == 0;
|
||||
exercise_packager_from_unsearchable_cwd();
|
||||
}
|
||||
|
||||
SKIP: {
|
||||
skip 'dpkg-deb is not installed', 12 unless command_exists('dpkg-deb');
|
||||
exercise_packager('deb');
|
||||
}
|
||||
|
||||
SKIP: {
|
||||
skip 'git and dpkg-deb are not installed', 6
|
||||
unless command_exists('git') && command_exists('dpkg-deb');
|
||||
exercise_builder_tmpdir();
|
||||
}
|
||||
|
||||
done_testing();
|
||||
|
||||
sub exercise_packager {
|
||||
my ($format) = @_;
|
||||
my $first = "$tmp/$format-first";
|
||||
my $second = "$tmp/$format-second";
|
||||
my $package_revision = sprintf('%032x%08x', time, $$);
|
||||
my @command = (
|
||||
$packager,
|
||||
'--architecture', 'x86_64',
|
||||
'--export-dir', $export,
|
||||
'--version', $version,
|
||||
'--release', $release,
|
||||
'--revision', $package_revision,
|
||||
'--source-date-epoch', $epoch,
|
||||
'--format', $format,
|
||||
);
|
||||
my ($legacy_rpm_top, $legacy_rpm_work);
|
||||
if ($format eq 'rpm') {
|
||||
$legacy_rpm_top =
|
||||
"/var/tmp/xcat-genesis-rpmbuild-$package_revision-x86_64-$version-$release";
|
||||
$legacy_rpm_work = "$legacy_rpm_top/BUILD/stale";
|
||||
make_path("$legacy_rpm_top/BUILD");
|
||||
write_binary($legacy_rpm_work, 'stale work');
|
||||
}
|
||||
|
||||
my $original_umask = umask(0022);
|
||||
is(system(@command, '--output-dir', $first), 0,
|
||||
"$format package builds with umask 0022");
|
||||
umask(0002);
|
||||
is(system(@command, '--output-dir', $second), 0,
|
||||
"$format package rebuilds with umask 0002");
|
||||
umask($original_umask);
|
||||
is(sprintf('%04o', (stat($first))[2] & 0x0fff), '0755',
|
||||
"$format output is readable and searchable");
|
||||
is(sprintf('%04o', (stat($second))[2] & 0x0fff), '0755',
|
||||
"$format rebuilt output is readable and searchable");
|
||||
if (defined($legacy_rpm_work)) {
|
||||
ok(-e $legacy_rpm_work, 'RPM package ignores the legacy fixed work path');
|
||||
remove_tree($legacy_rpm_top);
|
||||
}
|
||||
|
||||
my ($relative, $source_relative);
|
||||
if ($format eq 'rpm') {
|
||||
$relative = "rpm/xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm";
|
||||
$source_relative = "srpm/xCAT-genesis-openembedded-x86_64-$version-$release.src.rpm";
|
||||
} else {
|
||||
$relative = "deb/xcat-genesis-openembedded-x86-64_${version}-${release}_all.deb";
|
||||
}
|
||||
ok(-f "$first/$relative", "$format binary exists");
|
||||
is(digest_file("$first/$relative"), digest_file("$second/$relative"),
|
||||
"$format binary is reproducible across umasks");
|
||||
if ($format eq 'rpm') {
|
||||
ok(-f "$first/$source_relative", 'source RPM exists');
|
||||
is(digest_file("$first/$source_relative"), digest_file("$second/$source_relative"),
|
||||
'source RPM is reproducible across umasks');
|
||||
}
|
||||
|
||||
my $release_root = "$tmp/$format-release";
|
||||
make_path($release_root);
|
||||
copy_tree($first, $release_root);
|
||||
write_release_manifest(
|
||||
$release_root, $version, $release, $package_revision, $epoch, 'x86_64', $format,
|
||||
);
|
||||
write_checksums($release_root);
|
||||
ok(validate_release($release_root), "$format release layout passes");
|
||||
is(system($verifier, '--format', $format, $release_root), 0,
|
||||
"$format package metadata passes");
|
||||
my $contents_log = "$tmp/$format-contents.log";
|
||||
if ($format eq 'rpm') {
|
||||
is(run_capture($contents_log, 'rpm', '-qpl', "$first/$relative"), 0,
|
||||
'RPM payload can be listed');
|
||||
like(read_binary($contents_log),
|
||||
qr{/opt/xcat/share/xcat/netboot/genesis-openembedded/x86_64/kernel},
|
||||
'RPM uses the OpenEmbedded staging namespace');
|
||||
like(read_binary($contents_log),
|
||||
qr{^/usr/share/doc/xCAT-genesis-openembedded-x86_64/?$}m,
|
||||
'RPM owns its documentation directory');
|
||||
my $ownership_log = "$tmp/rpm-ownership.log";
|
||||
is(
|
||||
run_capture(
|
||||
$ownership_log, 'rpm', '-qp',
|
||||
'--qf', '[%{FILEUSERNAME}:%{FILEGROUPNAME}\n]',
|
||||
"$first/$relative",
|
||||
),
|
||||
0,
|
||||
'RPM file ownership can be read',
|
||||
);
|
||||
is_deeply(
|
||||
[ grep { $_ ne 'root:root' } split(/\n/, read_binary($ownership_log)) ],
|
||||
[],
|
||||
'RPM owns every payload path as root',
|
||||
);
|
||||
is(run_capture($contents_log, 'rpm', '-qp', '--scripts', "$first/$relative"), 0,
|
||||
'RPM script metadata can be read');
|
||||
is(read_binary($contents_log), '', 'RPM has no package scripts');
|
||||
} else {
|
||||
is(run_capture($contents_log, 'dpkg-deb', '-c', "$first/$relative"), 0,
|
||||
'DEB payload can be listed');
|
||||
like(read_binary($contents_log),
|
||||
qr{/opt/xcat/share/xcat/netboot/genesis-openembedded/x86_64/kernel},
|
||||
'DEB uses the OpenEmbedded staging namespace');
|
||||
my $control = "$tmp/deb-control";
|
||||
make_path($control);
|
||||
is(run_capture($contents_log, 'dpkg-deb', '-e', "$first/$relative", $control), 0,
|
||||
'DEB control files can be extracted');
|
||||
opendir(my $control_dh, $control) or die $!;
|
||||
my @control_files = sort grep { $_ ne '.' && $_ ne '..' } readdir($control_dh);
|
||||
closedir($control_dh) or die $!;
|
||||
is_deeply(\@control_files, [ qw(control md5sums) ],
|
||||
'DEB has no maintainer scripts or triggers');
|
||||
}
|
||||
}
|
||||
|
||||
sub exercise_packager_from_unsearchable_cwd {
|
||||
my $cwd = "$tmp/unsearchable-cwd";
|
||||
my $output = "$tmp/cwd-independent-rpm";
|
||||
my $log = "$tmp/cwd-independent-rpm.log";
|
||||
make_path($cwd);
|
||||
|
||||
my $pid = fork();
|
||||
die "Cannot fork: $!\n" unless defined($pid);
|
||||
if ($pid == 0) {
|
||||
chdir($cwd) or die "Cannot enter test directory: $!\n";
|
||||
chmod(0000, $cwd) or die "Cannot restrict test directory: $!\n";
|
||||
open(STDOUT, '>:raw', $log) or die $!;
|
||||
open(STDERR, '>&', STDOUT) or die $!;
|
||||
exec(
|
||||
$packager,
|
||||
'--architecture', 'x86_64',
|
||||
'--export-dir', $export,
|
||||
'--output-dir', $output,
|
||||
'--version', $version,
|
||||
'--release', $release,
|
||||
'--revision', $revision,
|
||||
'--source-date-epoch', $epoch,
|
||||
'--format', 'rpm',
|
||||
) or die "Cannot run packager: $!\n";
|
||||
}
|
||||
waitpid($pid, 0);
|
||||
my $status = $? >> 8;
|
||||
chmod(0700, $cwd) or die "Cannot restore test directory: $!\n";
|
||||
|
||||
is($status, 0, 'RPM package ignores an inaccessible inherited working directory');
|
||||
ok(
|
||||
-f "$output/rpm/xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm",
|
||||
'RPM package is published from an inaccessible inherited working directory',
|
||||
);
|
||||
}
|
||||
|
||||
sub exercise_builder_tmpdir {
|
||||
my $source = "$tmp/tmpdir-xcat-core";
|
||||
my $oe = "$source/xCAT-genesis-builder/oe";
|
||||
make_path($oe);
|
||||
write_binary("$source/Version", "$version\n");
|
||||
write_binary(
|
||||
"$oe/build",
|
||||
<<'BUILD',
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
expected=$XCAT_GENESIS_WORK_DIR/build/tmp
|
||||
[ "${TMPDIR:-}" = "$expected" ] || exit 41
|
||||
mkdir -p "$TMPDIR/deploy"
|
||||
BUILD
|
||||
);
|
||||
write_binary(
|
||||
"$oe/export",
|
||||
<<'EXPORT',
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
architecture=$1
|
||||
deploy=$2
|
||||
output=$3
|
||||
[ "$deploy" = "$XCAT_GENESIS_WORK_DIR/build/tmp/deploy" ] || exit 42
|
||||
mkdir -p "$output"
|
||||
printf '%s\n' kernel >"$output/kernel"
|
||||
printf '%s\n' initramfs >"$output/initramfs.cpio.gz"
|
||||
printf '%s\n' packages >"$output/image.manifest"
|
||||
printf '%s\n' '{}' >"$output/image.spdx.json"
|
||||
printf '%s\n' '{}' >"$output/image.vex.json"
|
||||
printf '%s\n' licenses >"$output/license.manifest"
|
||||
printf 'format=xcat-genesis\nversion=1\narchitecture=%s\n' "$architecture" \
|
||||
>"$output/xcat-genesis.manifest"
|
||||
(
|
||||
cd "$output"
|
||||
sha256sum -- * >SHA256SUMS
|
||||
)
|
||||
EXPORT
|
||||
);
|
||||
chmod(0755, "$oe/build", "$oe/export") or die $!;
|
||||
for my $command (
|
||||
[ 'git', '-C', $source, 'init', '-q' ],
|
||||
[ 'git', '-C', $source, 'add', '.' ],
|
||||
[ 'git', '-C', $source, '-c', 'user.name=xCAT test',
|
||||
'-c', 'user.email=xcat-test@example.invalid', 'commit', '-qm', 'fixture' ],
|
||||
) {
|
||||
die "Cannot prepare test repository\n"
|
||||
if run_capture("$tmp/tmpdir-git.log", @{$command});
|
||||
}
|
||||
|
||||
my $ambient_tmp = "$tmp/ambient-tmp";
|
||||
my $persistent_work = "$tmp/persistent-work";
|
||||
my $output = "$tmp/tmpdir-release";
|
||||
my $log = "$tmp/tmpdir-builder.log";
|
||||
make_path($ambient_tmp);
|
||||
my $status;
|
||||
{
|
||||
local $ENV{TMPDIR} = $ambient_tmp;
|
||||
$status = run_capture(
|
||||
$log, $builder, '--xcat-source', $source,
|
||||
'--output-dir', $output, '--work-dir', $persistent_work,
|
||||
'--format', 'deb',
|
||||
);
|
||||
}
|
||||
is($status, 0, 'release builder isolates the OpenEmbedded tmpdir');
|
||||
unlike(read_binary($log), qr/Invalid OpenEmbedded deploy directory/,
|
||||
'release builder finds the configured deploy directory');
|
||||
ok(-d "$persistent_work/openembedded/build/tmp",
|
||||
'release builder preserves the requested work directory');
|
||||
is((stat($output))[2] & oct('07777'), oct('0755'),
|
||||
'release directory is readable by other users');
|
||||
my $built = validate_release($output);
|
||||
is($built->{architectures}, 'x86_64', 'isolated build keeps the target architecture');
|
||||
is($built->{formats}, 'deb', 'isolated build keeps the requested format');
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package XCAT::GenesisReleaseTest;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Exporter qw(import);
|
||||
use File::Basename qw(dirname);
|
||||
use File::Copy qw(copy);
|
||||
use File::Path qw(make_path);
|
||||
use Test::More ();
|
||||
use XCAT::BuildUtils qw(
|
||||
digest_manifest
|
||||
relative_files
|
||||
write_binary
|
||||
);
|
||||
|
||||
our @EXPORT_OK = qw(
|
||||
copy_tree
|
||||
dies_like
|
||||
make_export
|
||||
run_capture
|
||||
write_forkmanager_stub
|
||||
write_checksums
|
||||
write_release_manifest
|
||||
);
|
||||
|
||||
sub make_export {
|
||||
my ($directory, $architecture) = @_;
|
||||
make_path($directory);
|
||||
my %content = (
|
||||
'kernel' => 'kernel',
|
||||
'initramfs.cpio.gz' => 'initramfs',
|
||||
'image.manifest' => 'packages',
|
||||
'image.spdx.json' => '{}',
|
||||
'image.vex.json' => '{}',
|
||||
'license.manifest' => 'licenses',
|
||||
'xcat-genesis.manifest' =>
|
||||
"format=xcat-genesis\nversion=1\narchitecture=$architecture\n",
|
||||
);
|
||||
$content{'fw_jump.elf'} = 'firmware' if $architecture eq 'riscv64';
|
||||
write_binary("$directory/$_", $content{$_}) for sort keys %content;
|
||||
write_checksums($directory);
|
||||
return $directory;
|
||||
}
|
||||
|
||||
sub write_release_manifest {
|
||||
my ($directory, $xcat_version, $xcat_release, $revision, $epoch,
|
||||
$architectures, $formats) = @_;
|
||||
write_binary(
|
||||
"$directory/release.manifest",
|
||||
"format=xcat-genesis-packages\n"
|
||||
. "version=1\n"
|
||||
. "xcat_version=$xcat_version\n"
|
||||
. "xcat_release=$xcat_release\n"
|
||||
. "xcat_revision=$revision\n"
|
||||
. "source_date_epoch=$epoch\n"
|
||||
. "architectures=$architectures\n"
|
||||
. "formats=$formats\n",
|
||||
);
|
||||
}
|
||||
|
||||
sub write_checksums {
|
||||
my ($directory) = @_;
|
||||
unlink("$directory/SHA256SUMS") if -e "$directory/SHA256SUMS";
|
||||
my @files = relative_files($directory);
|
||||
write_binary(
|
||||
"$directory/SHA256SUMS",
|
||||
digest_manifest($directory, 'sha256', @files),
|
||||
);
|
||||
}
|
||||
|
||||
sub copy_tree {
|
||||
my ($source, $destination) = @_;
|
||||
make_path($destination);
|
||||
for my $relative (relative_files($source)) {
|
||||
my $target = "$destination/$relative";
|
||||
make_path(dirname($target));
|
||||
copy("$source/$relative", $target) or die $!;
|
||||
}
|
||||
}
|
||||
|
||||
sub write_forkmanager_stub {
|
||||
my ($root) = @_;
|
||||
my $stub = "$root/Parallel/ForkManager.pm";
|
||||
make_path("$root/Parallel");
|
||||
write_binary(
|
||||
$stub,
|
||||
"package Parallel::ForkManager;\n"
|
||||
. "sub new { bless {}, shift }\n"
|
||||
. "sub run_on_finish { \$_[0]->{callback} = \$_[1] }\n"
|
||||
. "sub start { 0 }\n"
|
||||
. "sub finish { my (\$self, \$exit) = \@_; "
|
||||
. "\$self->{callback}->(\$\$, \$exit, undef, 0, 0) if \$self->{callback}; 0 }\n"
|
||||
. "sub wait_all_children { 0 }\n1;\n",
|
||||
);
|
||||
return $root;
|
||||
}
|
||||
|
||||
sub run_capture {
|
||||
my ($log, @command) = @_;
|
||||
my $pid = fork();
|
||||
die "Cannot fork: $!\n" unless defined $pid;
|
||||
if ($pid == 0) {
|
||||
open(STDOUT, '>:raw', $log) or die $!;
|
||||
open(STDERR, '>&', STDOUT) or die $!;
|
||||
exec(@command) or die "Cannot run $command[0]: $!\n";
|
||||
}
|
||||
waitpid($pid, 0);
|
||||
return 255 if $? == -1;
|
||||
return 128 + ($? & 127) if $? & 127;
|
||||
return $? >> 8;
|
||||
}
|
||||
|
||||
sub dies_like {
|
||||
my ($code, $pattern, $name) = @_;
|
||||
my $error = '';
|
||||
eval { $code->(); 1 } or $error = $@;
|
||||
Test::More::like($error, $pattern, $name);
|
||||
}
|
||||
|
||||
1;
|
||||
Reference in New Issue
Block a user