debcargo-conf/dev/chain_build.py
Fabian Grünbichler 7016620bb1 backport trybuild 1.0.91-1
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2024-07-30 12:16:33 +02:00

251 lines
6.7 KiB
Python
Executable File

#!/usr/bin/env python3
USAGE = 'Usage: dev/chain_build.py [!]<CRATE>[=<REALVER>] <CRATE2>[=<REALVER>] ...'
HELP = f'''
{USAGE}
Build a chain of packages, each having all previous packages as "extra
dependency deb" as in build.sh. It fails when one in the chain fails to build,
and picks up where it stopped next time by checking which packages have been
recently built.
If multiple instances of the same crate are given, the first versioned
(crate=ver) overrides others. This is to ease manually specifying versions
before autogenerated ones (e.g. debcargo build-order), otherwise the generated
first occurence would have been built.
Prefixing a crate with "!" forces it to be built.
This script needs python-apt to work.
This script expects to run at the root of the debcargo-conf repository.
Env vars:
- DISTRIBUTION
- CHROOT
- SBUILD_OPTS
- EXTRA_DEBS
Those env vars are passed to build.sh, read it for their descriptions.
- REPACKAGE: Use ./repackage.sh instead of ./update.sh to prepare the source package
'''
import re
from sys import argv, stdout
from subprocess import run
from os import getcwd, chdir, environ, makedirs
from os.path import basename, exists, join
try:
from apt.cache import Cache as AptCache
except:
print('This scripts depends on python-apt to work, apt install python3-apt and rerun')
exit(1)
aptc = AptCache()
def _todash(crate: str) -> str:
return crate.replace('_', '-')
def _print(*args):
if stdout.isatty():
print('\n\x1b[34;100m[chain_build]\x1b[;m', *args)
else:
print('[chain_build]', *args, '\n')
# this is actually faster than os.walk
def _find(pattern: str):
return run(f'ls {pattern}', shell=True, capture_output=True, text=True).stdout.strip().split('\n')
DCH_VER_RE = re.compile(r'\((.*?)\)')
def _get_dch_version(crate: str) -> str:
# normally we check if there is a match, but a valid d/changelog should
# always have one
return DCH_VER_RE.search(open(join('src', _todash(crate), 'debian', 'changelog')).readline()).group(1)
def find_built(specs: list[tuple[str, str]]) -> list[tuple[str, str, str]]:
# get all debs first, so we needn't walk again and again
chdir('build')
debs = _find('*.deb')
chdir('..')
built = []
_print('Conducting search in apt cache and build/ directory for existing debs')
for crate, ver in specs:
_crate = _todash(crate)
pkg_re = re.compile(f'librust-{_crate}(?:\+.*?)?-dev_{ver}')
if ver == '*':
try:
ver = _get_dch_version(crate)
except:
pass
pkg = aptc.get(f'librust-{_crate}-dev')
# if pkg is not None and pkg.candidate is not None and (ver == '*' or pkg.candidate.version.startswith(ver)):
# built.append((crate, pkg.candidate.version, 'apt'))
# continue
if ver == '*':
# version isn't specified, and d/changelog doesn't exist,
# means it's yet to be `./update.sh`d, move on
continue
for deb in debs:
if pkg_re.match(deb):
built.append((crate, deb, 'build'))
return built
COLL_LINE = 'collapse_features = true'
def collapse_features(crate: str):
f = open(join('src', _todash(crate), 'debian', 'debcargo.toml'), 'r+')
toml = f.read()
if COLL_LINE not in toml:
_print('writing collapse_features for', crate)
lines = toml.split('\n')
for i, line in enumerate(lines):
# avoid inserting at end ending up in [some.directive]
if line.startswith('['): #] to work around auto indent in my nvim
lines.insert(i, COLL_LINE)
lines.insert(i + 1, '')
f.seek(0)
f.write('\n'.join(lines))
f.close()
return True
f.write('\n')
f.write(COLL_LINE)
f.close()
return True
def build_one(crate: str, ver: str, prev_debs: list[str]):
env = environ.copy()
if ver != '*':
env['REALVER'] = ver
# prevent git from stopping us with a pager
env['GIT_PAGER'] = 'cat'
if 'REPACKAGE' in env:
run(('./repackage.sh', crate), env=env, check=True)
else:
# \n is for when update.sh stops for confirmation
run(('./update.sh', crate), env=env, input=b'\n', check=True)
# if not set before, rerun ./update.sh to enable it
if collapse_features(crate):
run(('./update.sh', crate), env=env, input=b'\n', check=True)
env['EXTRA_DEBS'] = ','.join(prev_debs)
chdir('build')
run(('./build.sh', crate), env=env, check=True)
chdir('..')
def parse_specs(specs: tuple[str]) -> list[tuple[str, str]]:
recorded = []
versions = {}
for spec in specs:
crate, ver = (spec.split('=') + ['*'])[:2]
# filter out 1.2.3+surplus-version-part
if '+' in ver:
ver = ver.split('+')[0]
if crate in recorded:
if versions.get(crate, '*') != '*':
continue
else:
recorded.append(crate)
versions[crate] = ver
return [(crate, versions[crate]) for crate in recorded]
def chain_build(specs):
specs = parse_specs(specs)
found = find_built(specs)
env = environ.copy()
extra_debs = env.get('EXTRA_DEBS')
built, debs = set(), set()
if found:
_print('Existing debs:')
for crate, deb_or_ver, kind in found:
built.add(crate)
if kind == 'build':
print(crate, deb_or_ver)
debs.add(deb_or_ver)
elif kind == 'apt':
print(crate, deb_or_ver, 'in apt repository')
_print('To be built:')
for crate, ver in specs:
if crate not in built:
if ver == '*':
try:
ver = _get_dch_version(crate)
except:
pass
print(crate, ver, 'FORCE BUILD' if crate[0] == '!' else '')
else:
built, debs = set(), set()
_print('No recently built packages')
if extra_debs:
_print('EXTRA_DEBS:')
for deb in extra_debs.split(' '):
print(deb)
debs.add(deb)
_print('Starting chain build, press any key to continue, Ctrl+C to abort')
input()
def try_build(crate, ver, debs):
try:
build_one(crate, ver, debs)
except Exception as e:
print(e)
_print(f'Failed to build crate {crate}. Please fix it then press any key to continue.')
input()
if basename(getcwd()) == 'build':
chdir('..')
try_build(crate, ver, debs)
for crate, ver in specs:
if crate in built:
continue
if crate[0] == '!':
crate = crate[1:]
_print('Start building', crate, 'version', ver, 'with previous debs', debs)
try_build(crate, ver, debs)
built.add(crate)
if ver == '*':
# used in a glob, so
ver = ''
_crate = _todash(crate)
pkg_re = re.compile(f'librust-{_crate}(?:\+.*?)?-dev_{ver}')
chdir('build')
all_debs = _find('*.deb')
chdir('..')
for deb in all_debs:
if pkg_re.match(deb):
debs.add(deb)
if __name__ == '__main__':
if len(argv) <= 2:
print(HELP)
exit()
cwd = getcwd()
if not (exists(join(cwd, 'repackage.sh')) and exists(join(cwd, 'build.sh'))):
_print('Please run this script at root of debcargo-conf')
exit(1)
# Make sure build directory is present
makedirs('build', exist_ok=True)
# flatten shell substituted args
i = 1
while i < len(argv):
if ' ' in argv[i]:
argv[i:] = list(filter(lambda a: a != '', argv[1].split(' '))) + argv[i + 1:]
i += 1
chain_build(argv[1:])