168 lines
7 KiB
Python
Executable file
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()
|