[projects/pld-builder.new] Yet another php rebuild script.
arekm
arekm at pld-linux.org
Mon Apr 6 14:50:31 CEST 2026
commit f4b716215ca529169fd5405da8f834f2a67a7e98
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date: Mon Apr 6 14:50:06 2026 +0200
Yet another php rebuild script.
client/php-rebuild.py | 467 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 467 insertions(+)
---
diff --git a/client/php-rebuild.py b/client/php-rebuild.py
new file mode 100755
index 0000000..9def1dd
--- /dev/null
+++ b/client/php-rebuild.py
@@ -0,0 +1,467 @@
+#!/usr/bin/python3
+
+"""Send PLD Linux build requests for PHP extensions across all supported PHP versions.
+
+Reads the spec from git (via git show) to determine php-devel version constraints,
+then generates make-request.sh commands with the appropriate php_suffix define.
+
+Default ref: auto-tag at master tip if it exists, otherwise master.
+Default is dry-run (prints commands). Use --run to execute.
+
+Examples:
+ php-rebuild.py php-pecl-apcu # from auto-tag or master
+ php-rebuild.py --run -t php-pecl-xdebug # execute test builds
+ php-rebuild.py php-pecl-redis:auto/th/php-pecl-redis-6.0.2-2 # specific tag
+ php-rebuild.py --php 8.0-8.4 php-pecl-redis # override version range
+ php-rebuild.py --autotag php-pecl-apcu # only first suffix
+ php-rebuild.py --autotag-continue php-pecl-apcu # remaining suffixes
+"""
+
+import os
+import re
+import shlex
+import subprocess
+import sys
+import time
+
+# Two-digit suffixes mapping to PHP versions: '52' = PHP 5.2, '85' = PHP 8.5
+ALL_SUFFIXES = [
+ '52', '53', '54', '55', '56',
+ '70', '71', '72', '73', '74',
+ '80', '81', '82', '83', '84', '85',
+]
+DEFAULT_MIN_PHP = (8, 4) # assumed when spec has no php-devel BuildRequires
+DIST = 'th'
+TAG_POLL_TIMEOUT = 240 # seconds to wait for builder to create auto-tag
+TAG_POLL_INTERVAL = 5 # seconds between polls
+
+# Our own CLI flags — defined once, opts defaults and parsing derived from these
+OUR_BOOL_FLAGS = {
+ '--run': 'run', '-t': 'test', '--test': 'test',
+ '-v': 'verbose', '--verbose': 'verbose',
+ '--autotag': 'autotag', '--autotag-continue': 'autotag_continue',
+}
+OUR_VALUE_FLAGS = {
+ '--php': 'php',
+ '--autotag-wait': 'autotag_wait',
+}
+
+# make-request.sh build mode flags — used to avoid doubling if user passes one
+BUILD_MODE_FLAGS = {'-t', '--test-build', '-r', '--ready-build'}
+
+# make-request.sh flags that consume the next argument — needed so the arg parser
+# doesn't mistake their values (e.g. "-D 'php_suffix 80'") for spec names
+MR_VALUE_FLAGS = {
+ '-b', '--builder', '-D', '--define', '-d', '--dist', '--distro',
+ '-c', '--command', '-C', '--post-command', '-p', '--priority',
+ '-w', '-s', '--skip', '--branch', '--with', '--without',
+ '--kernel', '--target', '-j', '--jobs', '-m', '-g', '--gpg-opts',
+ '-f', '--flag', '-cf', '--command-flags', '--config-file',
+ '-q', '--test-remove-pkg', '--remove-pkg', '--upgrade-pkg', '-Uhv',
+ '--requester',
+}
+
+# poldek globs match against full name-version-release, so php*-common-* is needed
+# to match e.g. php82-common-8.2.28-1
+PRE_COMMAND = 'poldek --cmd "uninstall php*-common-*" --noask; :'
+
+
+def suffix_to_version(s):
+ return (int(s[0]), int(s[1:]))
+
+
+def version_to_suffix(major, minor):
+ return f'{major}{minor}'
+
+
+def fmt_ver(v):
+ """Format version tuple as 'M.N' for display."""
+ return f'{v[0]}.{v[1]}'
+
+
+def run_cmd(cmd):
+ """Run command, return (ok, output). Output is stdout on success, stderr on failure."""
+ proc = subprocess.run(cmd, capture_output=True, text=True)
+ if proc.returncode == 0:
+ return True, proc.stdout.strip()
+ return False, proc.stderr.strip()
+
+
+def clean_pkgname(pkg):
+ """Split 'name[:ref]' into (name, spec, ref). ref is None if not specified."""
+ parts = pkg.split(':', 1)
+ spec = parts[0]
+ ref = parts[1] if len(parts) > 1 else None
+ if not spec.endswith('.spec'):
+ spec += '.spec'
+ name = spec[:-5].rsplit('/', 1)[-1]
+ return name, spec, ref
+
+
+def _match_remote_autotag(gitdir, head_hash):
+ """Check remote for an auto-tag pointing at the given commit hash."""
+ tag_prefix = f'refs/tags/auto/{DIST}/'
+ ok, output = run_cmd(['git', '-C', gitdir, 'ls-remote', '--tags', 'origin', f'{tag_prefix}*'])
+ if not ok or not output:
+ return None
+ for line in output.splitlines():
+ if '\t' not in line:
+ continue
+ commit, ref = line.split('\t', 1)
+ if commit == head_hash and ref.startswith(tag_prefix):
+ return ref.removeprefix('refs/tags/')
+ return None
+
+
+def find_autotag_remote(gitdir, branch):
+ """Find auto-tag at branch tip by checking the remote (source of truth).
+
+ Auto-tags (auto/th/*) are created exclusively by builders after a successful
+ build. Always checks remote — local tags may be stale or missing.
+ """
+ ok, head_hash = run_cmd(['git', '-C', gitdir, 'rev-parse', branch])
+ if not ok:
+ return None
+ return _match_remote_autotag(gitdir, head_hash)
+
+
+def wait_for_tag(gitdir, branch, timeout=TAG_POLL_TIMEOUT):
+ """Poll remote for auto-tag on branch's commit, return tag name or None.
+
+ After the first suffix build succeeds, the builder creates an auto-tag.
+ We poll git ls-remote (no fetch) to detect it. Prints a dot every poll.
+ Returns None on timeout — caller decides whether to abort or fall back.
+ """
+ ok, head_hash = run_cmd(['git', '-C', gitdir, 'rev-parse', branch])
+ if not ok:
+ return None
+
+ deadline = time.time() + timeout
+ while time.time() < deadline:
+ time.sleep(TAG_POLL_INTERVAL)
+ print('.', end='', flush=True)
+ tag = _match_remote_autotag(gitdir, head_hash)
+ if tag:
+ return tag
+ return None
+
+
+def git_show_file(gitdir, ref, filename):
+ """Read file content at a git ref without touching the working tree."""
+ ok, output = run_cmd(['git', '-C', gitdir, 'show', f'{ref}:{filename}'])
+ if not ok:
+ print(f'Error: cannot read {filename} at {ref}: {output}', file=sys.stderr)
+ return None
+ return output
+
+
+def parse_php_range_from_text(text):
+ """Parse PHP version range from raw spec text using regex.
+
+ Works on unexpanded spec text (straight from git), matching both literal
+ 'php-devel' and macro '%{php_name}-devel' forms in BuildRequires lines.
+ Strips epoch prefix (e.g. 4:8.0.0 -> 8.0).
+ """
+ min_php = None
+ max_php_excl = None
+ for line in text.splitlines():
+ if 'BuildRequires' not in line or '-devel' not in line:
+ continue
+ if 'php' not in line and 'php_name' not in line:
+ continue
+ m = re.search(r'(>=|<)\s*(?:\d+:)?(\d+)\.(\d+)', line)
+ if m:
+ ver = (int(m.group(2)), int(m.group(3)))
+ if m.group(1) == '>=':
+ min_php = ver
+ else:
+ max_php_excl = ver
+ return min_php, max_php_excl
+
+
+def parse_version(s):
+ """Parse 'M.N' string into (major, minor) tuple. Dies on bad input."""
+ parts = s.strip().split('.')
+ if len(parts) < 2:
+ print(f'Error: invalid PHP version {s!r}, expected M.N format', file=sys.stderr)
+ sys.exit(1)
+ return (int(parts[0]), int(parts[1]))
+
+
+def get_suffixes(min_php, max_php_excl, php_override=None):
+ """Return list of suffixes to build."""
+ if php_override:
+ if '-' in php_override:
+ lo_s, hi_s = php_override.split('-', 1)
+ lo, hi = parse_version(lo_s), parse_version(hi_s)
+ suffixes = [s for s in ALL_SUFFIXES if lo <= suffix_to_version(s) <= hi]
+ else:
+ # comma-separated list: "7.0,8.0,8.5" (also handles single "8.1")
+ suffixes = []
+ for v in php_override.split(','):
+ s = version_to_suffix(*parse_version(v))
+ if s in ALL_SUFFIXES:
+ suffixes.append(s)
+ else:
+ print(f'Warning: unknown PHP version {v.strip()}', file=sys.stderr)
+ # warn about versions outside spec's declared range
+ for s in suffixes:
+ v = suffix_to_version(s)
+ if v < min_php or (max_php_excl and v >= max_php_excl):
+ print(f'Warning: PHP {fmt_ver(v)} is outside spec declared range', file=sys.stderr)
+ return suffixes
+
+ return [s for s in ALL_SUFFIXES
+ if suffix_to_version(s) >= min_php
+ and (not max_php_excl or suffix_to_version(s) < max_php_excl)]
+
+
+def print_help():
+ print("""\
+Usage: php-rebuild.py [OPTIONS] [MAKE-REQUEST-OPTS] SPEC[:REF] [SPEC[:REF]...]
+
+Send build requests for PHP extensions across all supported PHP versions.
+Reads the spec from git (via git show) to determine which PHP versions the
+extension supports, then generates make-request.sh commands with the
+appropriate php_suffix define.
+
+Default is dry-run — commands are printed but not executed.
+
+Options:
+ --run Execute commands (default: dry-run, just print)
+ -t, --test Send test builds (pass -t to make-request.sh)
+ --php RANGE Override PHP version range
+ range: --php 8.0-8.5
+ list: --php 8.0,8.2,8.4
+ single: --php 8.4
+ --autotag Only send the first suffix build (to trigger auto-tag
+ creation). Skips if auto-tag already exists.
+ --autotag-continue Send builds for all suffixes except the first, using
+ the existing auto-tag. Use after --autotag succeeded.
+ --autotag-wait SECS Seconds to wait for auto-tag to appear (default: 240)
+ -v, --verbose Print extra info (ref used, PHP versions)
+ -h, --help Show this help
+
+Spec arguments:
+ SPEC Package name, e.g. php-pecl-apcu or php-pecl-apcu.spec
+ Default ref: auto-tag at master tip (remote check),
+ or master itself if no tag at tip.
+ SPEC:TAG Build from a specific auto-tag
+ SPEC:BRANCH Build from a branch tip
+
+Any unrecognized options are passed through to make-request.sh, e.g.:
+ -D 'with_foo 1' Define RPM macro
+ -b 'th-x86_64' Select specific builders
+
+Workflow:
+ 1. Full rebuild (auto-tag exists at branch tip):
+ php-rebuild.py --run php-pecl-apcu
+
+ 2. Full rebuild (no auto-tag, waits for builder to create one):
+ php-rebuild.py --run php-pecl-apcu
+
+ 3. Two-step rebuild (send first build, then continue after tag appears):
+ php-rebuild.py --run --autotag php-pecl-apcu
+ # ... wait for build to succeed ...
+ php-rebuild.py --run --autotag-continue php-pecl-apcu
+
+ 4. Resend from a specific tag:
+ php-rebuild.py --run php-pecl-redis:auto/th/php-pecl-redis-6.0.2-2
+
+ 5. Dry-run to preview commands:
+ php-rebuild.py php-pecl-xdebug
+
+ 6. Test build for specific PHP versions:
+ php-rebuild.py --run -t --php 8.2-8.4 php-pecl-redis""")
+
+
+def parse_args(argv):
+ """Two-pass arg parser. Returns (opts_dict, specs_list, passthrough_list).
+
+ Can't use argparse because we need to transparently pass unknown flags
+ (and their values) through to make-request.sh. We track which make-request.sh
+ flags take a value argument (MR_VALUE_FLAGS) so we don't mistake those values
+ for spec names.
+
+ Specs: non-flag args not consumed as values (e.g. 'php-pecl-apcu:master')
+ Passthrough: everything else -> forwarded to make-request.sh
+ """
+ opts = {v: False for v in OUR_BOOL_FLAGS.values()}
+ opts.update({v: None for v in OUR_VALUE_FLAGS.values()})
+ specs = []
+ passthrough = []
+
+ if '--help' in argv or '-h' in argv:
+ print_help()
+ sys.exit(0)
+
+ i = 0
+ while i < len(argv):
+ arg = argv[i]
+ if arg in OUR_BOOL_FLAGS:
+ opts[OUR_BOOL_FLAGS[arg]] = True
+ elif arg in OUR_VALUE_FLAGS and i + 1 < len(argv):
+ i += 1
+ opts[OUR_VALUE_FLAGS[arg]] = argv[i]
+ elif arg.startswith('-'):
+ passthrough.append(arg)
+ if arg in MR_VALUE_FLAGS and i + 1 < len(argv):
+ i += 1
+ passthrough.append(argv[i])
+ else:
+ specs.append(arg)
+ i += 1
+
+ opts['autotag_wait'] = int(opts['autotag_wait'] or TAG_POLL_TIMEOUT)
+
+ return opts, specs, passthrough
+
+
+def process_spec(spec_arg, opts, passthrough):
+ """Process one spec: find ref, read spec from git, determine PHP versions, send builds.
+
+ Ref resolution (when not specified by user):
+ 1. Auto-tag at master tip (remote check) — current version already built
+ 2. master itself — new version, needs first+wait+rest flow
+
+ Modes:
+ default all suffixes from resolved ref (wait for tag if needed)
+ --autotag first suffix only, to trigger auto-tag creation
+ --autotag-continue remaining suffixes from existing auto-tag
+ """
+ name, spec, ref = clean_pkgname(spec_arg)
+ gitdir = os.path.join(opts['rpmdir'], name)
+
+ if not os.path.isdir(gitdir):
+ print(f'Error: {gitdir} not found', file=sys.stderr)
+ return
+
+ # Determine what ref to build from.
+ # Auto-tag at branch tip = already built, rebuild all from it.
+ # No tag at tip = new build, first+wait+rest flow.
+ branch = ref or 'master'
+ tip_tag = None
+ if not ref:
+ tip_tag = find_autotag_remote(gitdir, branch)
+
+ if opts['verbose']:
+ print(f'{name}: branch {branch}, tip_tag {tip_tag or "none"}')
+
+ # Read spec from branch/ref (local) — remote auto-tags may not be fetched locally
+ spec_text = git_show_file(gitdir, branch, spec)
+ if not spec_text:
+ return
+
+ min_php, max_php_excl = parse_php_range_from_text(spec_text)
+ if min_php is None:
+ print(f'Warning: no php-devel dep in {name}, assuming >= {fmt_ver(DEFAULT_MIN_PHP)}',
+ file=sys.stderr)
+ min_php = DEFAULT_MIN_PHP
+
+ suffixes = get_suffixes(min_php, max_php_excl, opts['php'])
+ if not suffixes:
+ print(f'Error: no PHP versions to build for {name}', file=sys.stderr)
+ return
+
+ # Summary of what we're about to do
+ ver_list = ' '.join(fmt_ver(suffix_to_version(s)) for s in suffixes)
+ mode = 'test' if opts['test'] else 'ready'
+ if not opts['run']:
+ mode += ', dry-run'
+
+ if opts['autotag']:
+ if tip_tag:
+ plan = f'skip (auto-tag {tip_tag} already exists)'
+ else:
+ plan = f'autotag only: PHP {fmt_ver(suffix_to_version(suffixes[0]))} from {branch}'
+ elif opts['autotag_continue']:
+ plan = f'continue: PHP {ver_list} (skip first) from auto-tag'
+ elif tip_tag:
+ plan = f'rebuild all: PHP {ver_list} from {tip_tag}'
+ else:
+ plan = f'new build: PHP {ver_list} from {branch}, first+wait+rest'
+
+ print(f'{name}: {plan} [{mode}]')
+
+ # Avoid doubling build mode if user already passed -t/-r in passthrough
+ if any(a in BUILD_MODE_FLAGS for a in passthrough):
+ build_mode = []
+ else:
+ build_mode = ['-t'] if opts['test'] else ['-r']
+
+ def send(suffixes_to_send, send_ref):
+ """Build and show/execute make-request.sh commands for given suffixes."""
+ for suffix in suffixes_to_send:
+ cmd = ['make-request.sh'] + build_mode + [
+ '-D', f'php_suffix {suffix}',
+ '-c', PRE_COMMAND,
+ ] + passthrough + [f'{spec}:{send_ref}']
+ print(f'\nBuilding {name} PHP {fmt_ver(suffix_to_version(suffix))} from {send_ref}:')
+ print(shlex.join(cmd))
+ if opts['run']:
+ ok, out = run_cmd(cmd)
+ if not ok:
+ print(f'Error: build failed', file=sys.stderr)
+ if out:
+ print(out, file=sys.stderr)
+
+ if opts['autotag']:
+ if tip_tag:
+ return # plan summary already printed "skip"
+ else:
+ send(suffixes[:1], branch)
+ elif opts['autotag_continue']:
+ # re-check remote only if we didn't already find a tip tag during ref resolution
+ if not tip_tag:
+ tip_tag = find_autotag_remote(gitdir, branch)
+ if not tip_tag:
+ print(f'Error: no auto-tag for {name}, run --autotag first', file=sys.stderr)
+ return
+ send(suffixes[1:], tip_tag)
+ elif tip_tag:
+ # Auto-tag at branch tip — all suffixes build from the same tagged commit
+ send(suffixes, tip_tag)
+ else:
+ # No auto-tag at tip — new build: send first suffix from branch,
+ # wait for builder to create auto-tag, then send the rest from it
+ send(suffixes[:1], branch)
+
+ if len(suffixes) > 1:
+ new_tag = None
+ if opts['run']:
+ timeout = opts['autotag_wait']
+ print(f'Waiting up to {timeout}s for auto-tag: ', end='', flush=True)
+ t0 = time.time()
+ new_tag = wait_for_tag(gitdir, branch, timeout)
+ if new_tag:
+ print(f' Got it in {time.time() - t0:.1f}s.')
+ else:
+ print() # newline after dots
+ print(f'Error: no auto-tag after {timeout}s, aborting',
+ file=sys.stderr)
+ return
+ else:
+ print(f'# Would wait up to {opts["autotag_wait"]}s for auto-tag, then send remaining from it:')
+ send(suffixes[1:], '<auto-tag>')
+ return
+
+ send(suffixes[1:], new_tag)
+
+
+def main():
+ opts, specs, passthrough = parse_args(sys.argv[1:])
+ if not specs:
+ print('Usage: php-rebuild.py [--run] [-t] [--autotag|--autotag-continue] [--php RANGE] [make-request opts...] SPEC[:REF]...',
+ file=sys.stderr)
+ sys.exit(1)
+ ok, rpmdir = run_cmd(['rpm', '--eval', '%{_topdir}'])
+ if not ok:
+ print('Error: cannot determine RPM topdir', file=sys.stderr)
+ sys.exit(1)
+ opts['rpmdir'] = rpmdir
+ for spec_arg in specs:
+ process_spec(spec_arg, opts, passthrough)
+
+
+if __name__ == '__main__':
+ main()
================================================================
---- gitweb:
http://git.pld-linux.org/gitweb.cgi/projects/pld-builder.new.git/commitdiff/f4b716215ca529169fd5405da8f834f2a67a7e98
More information about the pld-cvs-commit
mailing list