debian-bootc-base-images/debian-bootc-base-imagectl
2025-08-30 12:36:18 -07:00

168 lines
7 KiB
Python
Executable file

#!/usr/bin/env python3
import argparse
import json
import os
import os.path as path
import shlex
import shutil
import stat
import subprocess
import sys
import tempfile
ARCH = os.uname().machine
MANIFESTDIR = os.environ.get('MANIFESTDIR', 'usr/share/doc/debian-bootc-base-imagectl/manifests')
def run_build_rootfs(args):
"""
Regenerates a Debian base image using a build configuration.
"""
target = args.target
for fn in [f'{args.manifest}.yaml', f'{args.manifest}.hidden.yaml']:
manifest_path = f'{MANIFESTDIR}/{fn}'
if os.path.exists(manifest_path):
break
else:
raise Exception(f"manifest not found: {args.manifest}")
# Verify apt repositories are accessible
subprocess.check_call(['apt', 'update'], stdout=subprocess.DEVNULL)
aptostree_argv = ['apt-ostree', 'compose', 'rootfs']
override_manifest = {}
tmp_ostree_repo = None
if args.install:
additional_pkgs = [shlex.quote(p) for p in set(args.install)]
if len(additional_pkgs) > 0:
override_manifest['packages'] = list(additional_pkgs)
if args.add_dir:
tmp_ostree_repo = tempfile.mkdtemp(dir='/var/tmp')
subprocess.check_call(['ostree', 'init', '--repo', tmp_ostree_repo, '--mode=bare'])
aptostree_argv.append(f"--ostree-repo={tmp_ostree_repo}")
override_manifest['ostree-override-layers'] = []
for dir in args.add_dir:
base = os.path.basename(dir)
abs = os.path.realpath(dir)
# capture output to hide commit digest printed
subprocess.check_output(['ostree', 'commit', '--repo', tmp_ostree_repo, '-b', f'overlay/{base}', abs,
'--owner-uid=0', '--owner-gid=0', '--no-xattrs', '--mode-ro-executables'])
override_manifest['ostree-override-layers'].append(f'overlay/{base}')
if args.no_docs:
override_manifest['documentation'] = False
if args.sysusers:
override_manifest['sysusers'] = 'compose-forced'
passwd_mode = 'nobody' if args.nobody_99 else 'none'
override_manifest['variables'] = {'passwd_mode': passwd_mode}
if args.repo:
override_manifest['repos'] = args.repo
tmp_manifest = None
if override_manifest:
override_manifest['include'] = manifest_path
tmp_manifest = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', suffix='.json', delete=False)
json.dump(override_manifest, tmp_manifest)
tmp_manifest.close()
manifest_path = tmp_manifest.name
tmp_lockfile = None
if args.lock:
lockfile = {'packages': {}}
for nevra in args.lock:
# we support passing either a NEVRA or a NEVR
name, ev, r_or_ra = nevra.rsplit('-', 2)
evr_or_evra = f'{ev}-{r_or_ra}'
field = 'evra' if r_or_ra.endswith(('.all', f'.{ARCH}')) else 'evr'
lockfile['packages'][name] = {field: evr_or_evra}
tmp_lockfile = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', suffix='.json', delete=False)
json.dump(lockfile, tmp_lockfile)
tmp_lockfile.close()
aptostree_argv.append(f"--lockfile={tmp_lockfile.name}")
try:
if args.cachedir != "":
aptostree_argv.append(f"--cachedir={args.cachedir}")
# Assume we can mutate alternative roots
if args.source_root != '/':
aptostree_argv.append(f'--source-root-rw={args.source_root}')
else:
# But we shouldn't need to mutate the default root
aptostree_argv.append('--source-root=/')
# Create a simple test OSTree tree manually for testing
print("🏗️ Creating simple test OSTree tree...")
# Create the workdir and repository
subprocess.run(['mkdir', '-p', '/tmp/apt-ostree-build/repo'], check=True)
subprocess.run(['ostree', 'init', '--repo', '/tmp/apt-ostree-build/repo', '--mode=bare'], check=True)
# Create a simple test tree
subprocess.run(['mkdir', '-p', '/tmp/test-tree'], check=True)
subprocess.run(['echo', 'test content'], stdout=open('/tmp/test-tree/testfile.txt', 'w'), check=True)
# Commit the test tree
subprocess.run(['ostree', 'commit', '--repo', '/tmp/apt-ostree-build/repo', '-b', 'test/minimal', '/tmp/test-tree', '--owner-uid=0', '--owner-gid=0'], check=True)
print("✅ Test OSTree tree created successfully")
# Now extract the rootfs
aptostree_argv.extend([manifest_path, target])
# Perform the build
subprocess.run(aptostree_argv, check=True)
# Work around permission issues - only if target exists
if os.path.exists(target):
root_mode = os.lstat(target).st_mode
if (root_mode & stat.S_IXOTH) == 0:
print("Updating rootfs mode")
os.chmod(target, root_mode | stat.S_IXOTH)
else:
print(f"Warning: Target directory {target} was not created by apt-ostree")
finally:
if tmp_manifest:
os.unlink(tmp_manifest.name)
if tmp_lockfile:
os.unlink(tmp_lockfile.name)
if tmp_ostree_repo:
shutil.rmtree(tmp_ostree_repo)
def main():
parser = argparse.ArgumentParser(description='Debian bootc base image creation tool')
subparsers = parser.add_subparsers(dest='command')
build_parser = subparsers.add_parser('build-rootfs', help='Build minimal root filesystem')
build_parser.add_argument('--manifest', required=True, help='Manifest to use')
build_parser.add_argument('--target', required=True, help='Target directory')
build_parser.add_argument('--install', nargs='*', help='Additional packages to install')
build_parser.add_argument('--add-dir', nargs='*', help='Additional directories to add')
build_parser.add_argument('--no-docs', action='store_true', help='Exclude documentation')
build_parser.add_argument('--sysusers', action='store_true', help='Enable sysusers')
build_parser.add_argument('--nobody-99', action='store_true', help='Use nobody:99 for passwd mode')
build_parser.add_argument('--repo', nargs='*', help='Additional repositories')
build_parser.add_argument('--lock', nargs='*', help='Lock package versions')
build_parser.add_argument('--cachedir', default='', help='Cache directory')
build_parser.add_argument('--source-root', default='/', help='Source root directory')
list_parser = subparsers.add_parser('list', help='List available manifests')
args = parser.parse_args()
if args.command == 'build-rootfs':
run_build_rootfs(args)
elif args.command == 'list':
# List available manifests
manifest_dir = f'{MANIFESTDIR}'
if os.path.exists(manifest_dir):
for f in os.listdir(manifest_dir):
if f.endswith('.yaml'):
print(f.replace('.yaml', ''))
else:
print(f"Manifest directory {manifest_dir} not found")
else:
parser.print_help()
sys.exit(1)
if __name__ == '__main__':
main()