2
0
mirror of https://github.com/xcat2/xcat-dep.git synced 2026-09-12 12:36:23 +00:00

fix(build): stop concurrent per-arch builds racing on shared package sources

The elilo, ipmitool and syslinux builders each fetched their upstream
source and rewrote the tracked source tarball in place, inside the
package source directory that both arch builds share. When the two
per-arch builds run in parallel they were racing to fetch the source:
one build truncated and rewrote the tarball while the other read it, so
the reader got a truncated archive and failed intermittently with
"missing top-level tree" errors.

The correct, normalized source is already tracked in the repository and
is what mock consumes, so the fetch is redundant as well as unsafe. Drop
the download/normalize entirely and verify the tracked source read-only
(it exists and has the expected top-level tree). With no writer, the
shared source is only ever read, so parallel per-arch builds can no
longer race on it. Also removes the now-dead --source-url /
--skip-upstream-download options, the normalize helper, and the wget
dependency check.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
Daniel Hilst
2026-07-26 21:15:39 -03:00
parent 090aadf86e
commit c70e09ceeb
3 changed files with 53 additions and 136 deletions
+18 -70
View File
@@ -13,7 +13,6 @@ my $repo_root = abs_path("$script_dir/..");
my $pkg_dir = "$repo_root/elilo";
my $spec_file = "$pkg_dir/elilo-xcat.spec";
my $source_url = 'https://downloads.sourceforge.net/project/elilo/elilo/elilo-3.14/elilo-3.14-all.tar.gz';
my $source_file = '';
my $work_dir = '/tmp/elilo-xcat-mockbuild';
my $mock_cfg = '';
@@ -24,7 +23,6 @@ my $skip_install = 0;
my $build_timestamp;
GetOptions(
'source-url=s' => \$source_url,
'source-file=s' => \$source_file,
'work-dir=s' => \$work_dir,
'mock-cfg=s' => \$mock_cfg,
@@ -38,7 +36,7 @@ GetOptions(
die "Run as root (current uid=$>)\n" if $> != 0;
die "Missing spec file: $spec_file\n" if !-f $spec_file;
for my $bin (qw(wget mock rpmbuild rpm dnf file bash grep)) {
for my $bin (qw(mock rpmbuild rpm dnf file bash grep)) {
run("command -v " . sh_quote($bin) . " >/dev/null 2>&1");
}
@@ -80,7 +78,6 @@ print "result_dir: $result_dir\n";
print "log_dir: $log_dir\n";
print "mock_cfg: $mock_cfg\n";
print "mock_uniqueext: " . ($mock_uniqueext ne '' ? $mock_uniqueext : '(none)') . "\n";
print "source_url: $source_url\n";
print "source_file:$source_file\n";
print "skip_install: $skip_install\n";
@@ -90,28 +87,23 @@ make_path($log_dir);
print_step("Mock config check");
run("mock -r " . sh_quote($mock_cfg) . $mock_uniqueext_opt . " --print-root-path >/dev/null");
print_step("Prepare source archive");
# The elilo source tarball is tracked in the repo, already normalized to an elilo/ top-level
# tree. Re-downloading + normalizing rewrites $source_path IN PLACE -- and it lives in the
# shared (NFS) checkout that BOTH arch build hosts (x86 + ppc) build against at the same time,
# so the other host's concurrent elilo build can read it mid-rewrite and get a truncated
# archive (intermittent "missing elilo top-level tree" failures). Use the tracked copy
# read-only when it is already normalized; only fetch upstream if it is absent/unnormalized.
my $have_normalized = 0;
if (-f $source_path) {
my $top = capture(
"tar -tzf " . sh_quote($source_path) .
" 2>/dev/null | grep -E '^(\\./)?elilo/' | head -n1 || true"
);
$have_normalized = 1 if $top ne '';
}
if ($have_normalized) {
print "Using tracked normalized source archive (no upstream fetch, no shared write): $source_path\n";
} else {
run("wget --spider " . sh_quote($source_url));
run("wget -O " . sh_quote($source_path) . " " . sh_quote($source_url));
normalize_source_archive($source_path, $version, $work_dir);
}
print_step("Verify tracked source archive");
# Source0 (elilo-<ver>-source.tar.gz) is tracked in the repo, already normalized to an elilo/
# top-level tree, and consumed directly by mock (--sources $pkg_dir below). There is nothing to
# download: the old fetch re-derived this SAME tracked file and rewrote it IN PLACE. Because the
# checkout is on a shared (NFS) mount that BOTH arch build hosts (x86 + ppc) build against at the
# same time, that in-place rewrite raced the other host's concurrent elilo build -- it could read
# the file mid-write and get a truncated archive ("missing elilo top-level tree" failures). We now
# only READ the tracked file, so concurrent builds can never race on it. Fail loudly (do NOT
# silently re-fetch) if the checkout is missing/broken -- that is repo corruption, not a fetch miss.
die "Tracked elilo source missing: $source_path (incomplete checkout?)\n" if !-f $source_path;
my $top = capture(
"tar -tzf " . sh_quote($source_path) .
" 2>/dev/null | grep -E '^(\\./)?elilo/' | head -n1 || true"
);
die "Tracked elilo source is not normalized (no elilo/ top-level tree): $source_path\n"
if $top eq '';
print "Using tracked normalized source archive (read-only, no fetch, no shared write): $source_path\n";
print_step("Verify spec assets");
for my $asset (@spec_assets) {
@@ -253,7 +245,6 @@ exit 0;
sub usage {
return <<"USAGE";
Usage: $0 [options]
--source-url URL Upstream tarball URL (default: $source_url)
--source-file FILE Source filename stored in elilo/ (default: inferred from spec version)
--work-dir PATH Temporary work dir (default: $work_dir)
--mock-cfg NAME Mock config (default: <ID>+epel-10-<ARCH>)
@@ -302,49 +293,6 @@ sub parse_spec {
return ($version, @assets);
}
sub normalize_source_archive {
my ($archive, $version, $work_base) = @_;
my $has_elilo = capture(
"tar -tzf " . sh_quote($archive) .
" | grep -E '^(\\./)?elilo/' | head -n1 || true"
);
return if $has_elilo ne '';
my $nested = capture(
"tar -tzf " . sh_quote($archive) .
" | grep -E '^(\\./)?elilo-$version-source\\.tar\\.gz\$' | head -n1 || true"
);
die "Downloaded archive does not contain elilo source payload: $archive\n"
if $nested eq '';
my $normalize_dir = "$work_base/source-normalize";
remove_tree($normalize_dir) if -d $normalize_dir;
make_path($normalize_dir);
run(
"tar -xzf " . sh_quote($archive) .
" -C " . sh_quote($normalize_dir) .
" " . sh_quote($nested)
);
my $nested_rel = $nested;
$nested_rel =~ s{^\./}{};
my $nested_path = "$normalize_dir/$nested_rel";
die "Failed to extract nested source archive: $nested_path\n"
if !-f $nested_path;
copy($nested_path, $archive)
or die "Failed to normalize source archive $archive: $!\n";
my $recheck = capture(
"tar -tzf " . sh_quote($archive) .
" | grep -E '^(\\./)?elilo/' | head -n1 || true"
);
die "Normalized source archive still missing elilo top-level tree: $archive\n"
if $recheck eq '';
}
sub print_step {
my ($msg) = @_;
print "\n== $msg ==\n";
+18 -42
View File
@@ -3,7 +3,7 @@
use strict;
use warnings;
use Cwd qw(abs_path);
use File::Basename qw(dirname basename);
use File::Basename qw(dirname);
use File::Copy qw(copy);
use File::Path qw(make_path remove_tree);
use Getopt::Long qw(GetOptions);
@@ -13,7 +13,6 @@ my $repo_root = abs_path("$script_dir/..");
my $pkg_dir = "$repo_root/ipmitool";
my $spec_file = "$pkg_dir/ipmitool.spec";
my $source_url = 'https://github.com/ipmitool/ipmitool/archive/refs/tags/IPMITOOL_1_8_18.tar.gz';
my $source_file = '';
my $work_dir = '/tmp/ipmitool-xcat-mockbuild';
my $mock_cfg = '';
@@ -24,7 +23,6 @@ my $skip_install = 0;
my $build_timestamp;
GetOptions(
'source-url=s' => \$source_url,
'source-file=s' => \$source_file,
'work-dir=s' => \$work_dir,
'mock-cfg=s' => \$mock_cfg,
@@ -38,7 +36,7 @@ GetOptions(
die "Run as root (current uid=$>)\n" if $> != 0;
die "Missing spec file: $spec_file\n" if !-f $spec_file;
for my $bin (qw(wget mock rpmbuild rpm dnf ldd bash)) {
for my $bin (qw(mock rpmbuild rpm dnf ldd bash)) {
run("command -v " . sh_quote($bin) . " >/dev/null 2>&1");
}
@@ -80,7 +78,6 @@ print "result_dir: $result_dir\n";
print "log_dir: $log_dir\n";
print "mock_cfg: $mock_cfg\n";
print "mock_uniqueext: " . ($mock_uniqueext ne '' ? $mock_uniqueext : '(none)') . "\n";
print "source_url: $source_url\n";
print "source_file:$source_file\n";
print "skip_install: $skip_install\n";
print "SOURCE_DATE_EPOCH: $SOURCE_DATE_EPOCH\n";
@@ -91,10 +88,22 @@ make_path($log_dir);
print_step("Mock config check");
run("mock -r " . sh_quote($mock_cfg) . $mock_uniqueext_opt . " --print-root-path >/dev/null");
print_step("Download upstream source");
run("wget --spider " . sh_quote($source_url));
run("wget -O " . sh_quote($source_path) . " " . sh_quote($source_url));
normalize_source_archive($source_path, $version, $work_dir);
print_step("Verify tracked source archive");
# The ipmitool source (ipmitool-<ver>.tar.gz) is tracked in the repo, already normalized to the
# ipmitool-<ver>/ top-level that %setup -n expects, and consumed directly by mock (--sources
# $pkg_dir below). There is nothing to download: the old fetch re-derived this SAME tracked file
# and rewrote it IN PLACE, and the checkout is shared between the two arch build hosts building at
# once -- so the in-place rewrite raced the other host's concurrent ipmitool build, which could
# read the file mid-write and get a truncated archive. We only READ it now, so concurrent builds
# can never race on it. Fail loudly (do NOT silently re-fetch) if the checkout is missing/broken.
die "Tracked ipmitool source missing: $source_path (incomplete checkout?)\n" if !-f $source_path;
my $top = capture(
"tar -tzf " . sh_quote($source_path) .
" 2>/dev/null | grep -E '^(\\./)?ipmitool-$version/' | head -n1 || true"
);
die "Tracked ipmitool source is not the expected ipmitool-$version/ tree: $source_path\n"
if $top eq '';
print "Using tracked source archive (read-only, no fetch, no shared write): $source_path\n";
print_step("Verify spec assets");
for my $asset (@spec_assets) {
@@ -258,7 +267,6 @@ exit 0;
sub usage {
return <<"USAGE";
Usage: $0 [options]
--source-url URL Upstream tarball URL (default: $source_url)
--source-file FILE Source filename stored in ipmitool/ (default: inferred from spec version)
--work-dir PATH Temporary work dir (default: $work_dir)
--mock-cfg NAME Mock config (default: <ID>+epel-10-<ARCH>)
@@ -308,38 +316,6 @@ sub parse_spec {
return ($version, @assets);
}
sub normalize_source_archive {
my ($archive, $version, $work_base) = @_;
my $normalize_dir = "$work_base/source-normalize";
remove_tree($normalize_dir) if -d $normalize_dir;
make_path($normalize_dir);
run("tar -xzf " . sh_quote($archive) . " -C " . sh_quote($normalize_dir));
my @entries = grep { $_ !~ m{/\.\.?$} } glob("$normalize_dir/*");
die "Unexpected archive layout in $archive\n" if @entries != 1;
my $top_path = $entries[0];
die "Unexpected non-directory top-level entry in $archive: $top_path\n"
if !-d $top_path;
my $expected_top = "ipmitool-$version";
my $actual_top = basename($top_path);
if ($actual_top ne $expected_top) {
my $new_path = "$normalize_dir/$expected_top";
run("rm -rf " . sh_quote($new_path));
run("mv " . sh_quote($top_path) . " " . sh_quote($new_path));
}
# Repack using the expected top-level directory required by the spec.
run(
"tar --sort=name --owner=0 --group=0 --mtime=\@$SOURCE_DATE_EPOCH" .
" -C " . sh_quote($normalize_dir) .
" -czf " . sh_quote($archive) .
" " . sh_quote($expected_top)
);
}
sub print_step {
my ($msg) = @_;
print "\n== $msg ==\n";
+17 -24
View File
@@ -13,7 +13,6 @@ my $repo_root = abs_path("$script_dir/..");
my $pkg_dir = "$repo_root/syslinux";
my $spec_file = "$pkg_dir/syslinux-xcat.spec";
my $source_url = 'https://www.kernel.org/pub/linux/utils/boot/syslinux/syslinux-6.03.tar.xz';
my $source_file = '';
my $work_dir = '/tmp/syslinux-xcat-mockbuild';
my $mock_cfg = '';
@@ -21,11 +20,9 @@ my $mock_uniqueext = '';
my $result_dir = "$repo_root/build-output/list3/syslinux-xcat";
my $log_dir = "$repo_root/build-logs/list3/syslinux-xcat";
my $skip_install = 0;
my $skip_upstream_download = 0;
my $build_timestamp;
GetOptions(
'source-url=s' => \$source_url,
'source-file=s' => \$source_file,
'work-dir=s' => \$work_dir,
'mock-cfg=s' => \$mock_cfg,
@@ -33,14 +30,13 @@ GetOptions(
'result-dir=s' => \$result_dir,
'log-dir=s' => \$log_dir,
'skip-install!' => \$skip_install,
'skip-upstream-download!' => \$skip_upstream_download,
'build-timestamp=i' => \$build_timestamp,
) or die usage();
die "Run as root (current uid=$>)\n" if $> != 0;
die "Missing spec file: $spec_file\n" if !-f $spec_file;
for my $bin (qw(wget mock rpmbuild rpm dnf file bash grep cut)) {
for my $bin (qw(mock rpmbuild rpm dnf file bash grep)) {
run("command -v " . sh_quote($bin) . " >/dev/null 2>&1");
}
@@ -90,10 +86,8 @@ print "pkg_name: $pkg_name\n";
print "version: $version\n";
print "mock_cfg: $mock_cfg\n";
print "mock_uniqueext: " . ($mock_uniqueext ne '' ? $mock_uniqueext : '(none)') . "\n";
print "source_url: $source_url\n";
print "source_file:$source_file\n";
print "skip_install: $skip_install\n";
print "skip_upstream_download: $skip_upstream_download\n";
make_path($result_dir);
make_path($log_dir);
@@ -101,21 +95,22 @@ make_path($log_dir);
print_step("Mock config check");
run("mock -r " . sh_quote($mock_cfg) . $mock_uniqueext_opt . " --print-root-path >/dev/null");
if (!$skip_upstream_download) {
print_step("Download upstream source");
run("wget --spider " . sh_quote($source_url));
run("wget -O " . sh_quote($source_path) . " " . sh_quote($source_url));
my $sha = capture("sha256sum " . sh_quote($source_path) . " | cut -d ' ' -f1");
my $meta_file = "$log_dir/upstream-source.txt";
open my $mfh, '>', $meta_file or die "Cannot write $meta_file: $!\n";
print {$mfh} "url=$source_url\n";
print {$mfh} "file=$source_path\n";
print {$mfh} "sha256=$sha\n";
close $mfh;
print "Downloaded source: $source_path\n";
print "SHA256: $sha\n";
}
print_step("Verify tracked source archive");
# The syslinux source (syslinux-<ver>.tar.xz, Source0) is tracked in the repo, has the
# syslinux-<ver>/ top-level that %setup -n expects, and is consumed directly by mock (--sources
# $pkg_dir below). There is nothing to download: the old fetch re-downloaded this SAME tracked file
# and rewrote it IN PLACE, and the checkout is shared between the two arch build hosts building at
# once -- so the in-place rewrite raced the other host's concurrent syslinux build, which could
# read the file mid-write and get a truncated archive. We only READ it now, so concurrent builds
# can never race on it. Fail loudly (do NOT silently re-fetch) if the checkout is missing/broken.
die "Tracked syslinux source missing: $source_path (incomplete checkout?)\n" if !-f $source_path;
my $top = capture(
"tar -tf " . sh_quote($source_path) .
" 2>/dev/null | grep -E '^(\\./)?syslinux-' | head -n1 || true"
);
die "Tracked syslinux source is not a syslinux-*/ source tree: $source_path\n"
if $top eq '';
print "Using tracked source archive (read-only, no fetch, no shared write): $source_path\n";
print_step("Verify spec assets");
for my $asset (@all_assets) {
@@ -280,7 +275,6 @@ exit 0;
sub usage {
return <<"USAGE";
Usage: $0 [options]
--source-url URL Upstream tarball URL (default: $source_url)
--source-file FILE Source filename stored in syslinux/ (default: inferred from spec)
--work-dir PATH Temporary work dir (default: $work_dir)
--mock-cfg NAME Mock config (default: <ID>+epel-10-<ARCH>)
@@ -288,7 +282,6 @@ Usage: $0 [options]
--result-dir PATH Output RPM/SRPM directory (default: $result_dir)
--log-dir PATH Log directory (default: $log_dir)
--build-timestamp EPOCH SOURCE_DATE_EPOCH for deterministic builds
--skip-upstream-download Skip wget download step
--skip-install Skip dnf install + smoke tests
USAGE
}