diff --git a/.containerignore b/.containerignore index ac57a186..857caec2 100644 --- a/.containerignore +++ b/.containerignore @@ -17,3 +17,7 @@ !/overlay.d/ !/overrides/ !/platforms.yaml + +# Useful for testing development versions. +!/bootc-base-imagectl +!/rpm-ostree diff --git a/build-rootfs b/build-rootfs index 3f8c7638..11493abf 100755 --- a/build-rootfs +++ b/build-rootfs @@ -55,12 +55,16 @@ def main(): overlays = gather_overlays(manifest) nodocs = (manifest.get('documentation') is False) + recommends = manifest.get('recommends') + # We generate the initramfs using dracut ourselves later after our + # CoreOS postprocess scripts have run. If this version of rpm-ostree + # supports it we'll tell it to not run dracut in the initial compose. + no_initramfs = True if no_initramfs_arg_supported() else False - if version != "": - dracut_tmpd = inject_dracut_version(manifest['mutate-os-release'], version) - overlays += [dracut_tmpd.name] - - build_rootfs(target_rootfs, manifest_path, packages, locked_nevras, overlays, repos, nodocs) + build_rootfs( + target_rootfs, manifest_path, packages, locked_nevras, + overlays, repos, nodocs, recommends, no_initramfs + ) inject_live(target_rootfs) inject_image_json(target_rootfs, manifest_path) @@ -68,14 +72,13 @@ def main(): inject_content_manifest(target_rootfs, manifest) if version != "": - overlays.remove(dracut_tmpd.name) - cleanup_dracut_version(target_rootfs, dracut_tmpd) inject_version_info(target_rootfs, manifest['mutate-os-release'], version) strict_mode = os.getenv('STRICT_MODE') if strict_mode == '1': verify_strict_mode(target_rootfs, locked_nevras) run_postprocess_scripts(target_rootfs, manifest) + run_dracut(target_rootfs) cleanup_extraneous_files(target_rootfs) calculate_inputhash(target_rootfs, overlays, manifest) @@ -111,7 +114,10 @@ def inject_yumrepos(): shutil.copy(repo, "/etc/yum.repos.d") -def build_rootfs(target_rootfs, manifest_path, packages, locked_nevras, overlays, repos, nodocs): +def build_rootfs( + target_rootfs, manifest_path, packages, locked_nevras, + overlays, repos, nodocs, recommends, no_initramfs +): passwd_group_dir = os.getenv('PASSWD_GROUP_DIR') if passwd_group_dir is not None: inject_passwd_group(os.path.join(SRCDIR, passwd_group_dir)) @@ -124,6 +130,12 @@ def build_rootfs(target_rootfs, manifest_path, packages, locked_nevras, overlays argsfile.write("--no-docs\n") # temporarily work around https://issues.redhat.com/browse/RHEL-97826 tmpd = workaround_rhel_97826(argsfile) + if recommends: + if not recommends_arg_supported(): + raise Exception(f"Need to set recommends: true but --recommends is unsupported") + argsfile.write("--recommends\n") + if no_initramfs: + argsfile.write("--no-initramfs\n") if repos and repo_arg_supported(): for repo in repos: argsfile.write(f"--repo={repo}\n") @@ -141,22 +153,47 @@ def build_rootfs(target_rootfs, manifest_path, packages, locked_nevras, overlays if nodocs and tmpd is not None: del tmpd +def get_bootc_base_imagectl_help(): + return subprocess.check_output(['/usr/libexec/bootc-base-imagectl', 'build-rootfs', '-h'], encoding='utf-8') + def repo_arg_supported(): # Detect if we have https://gitlab.com/fedora/bootc/base-images/-/merge_requests/248. # If not, then we can't use `--repo`. That's OK because that should only # happen on RHEL, where we don't have any default repos anyway and only rely on # the mounted secret repo file. - help = subprocess.check_output(['/usr/libexec/bootc-base-imagectl', 'build-rootfs', '-h'], encoding='utf-8') - return '--repo REPO' in help + return '--repo REPO' in get_bootc_base_imagectl_help() def lock_arg_supported(): # Detect if we have https://gitlab.com/fedora/bootc/base-images/-/merge_requests/279. # If not, then we can't use `--lock`. That should only happen in RHCOS, # where we only use this for autolocking and not base lockfile management. - help = subprocess.check_output(['/usr/libexec/bootc-base-imagectl', 'build-rootfs', '-h'], encoding='utf-8') - return '--lock NEVRA' in help + return '--lock NEVRA' in get_bootc_base_imagectl_help() + + +def recommends_arg_supported(): + # Detect if we have https://gitlab.com/fedora/bootc/base-images/-/merge_requests/314. + # If not, then we can't use `--recommends` and should error. + return '--recommends' in get_bootc_base_imagectl_help() + + +def no_initramfs_arg_supported(): + # Detect if we have # https://gitlab.com/fedora/bootc/base-images/-/merge_requests/320. + # If not, then we can't use `--no-initramfs`, but that's OK because it's just + # an optimization to prevent building the initramfs twice. + if not '--no-initramfs' in get_bootc_base_imagectl_help(): + return False + # Detect if we have https://github.com/coreos/rpm-ostree/commit/481fbb034292666578780bacfdbf3dae9d10e6c3 + # At the time of this writing it's unreleased in rpm-ostree but it + # should be in the next release (2025.13 or 2026.1). + out = subprocess.check_output(['rpm-ostree', '--version'], encoding='utf-8') + data = yaml.safe_load(out) + version_str = data['rpm-ostree']['Version'] + # ideally, we could use `packaging.version`, but that's not in centos-bootc + # but conveniently, Python list comparisons do the right thing here + version = [int(c) for c in version_str.split('.')] + return version >= [2025, 13] def workaround_rhel_97826(argsfile): @@ -217,6 +254,15 @@ def run_postprocess_scripts(rootfs, manifest): os.unlink(os.path.join(rootfs, name)) +def run_dracut(rootfs): + print(f"Running dracut to generate the initramfs", flush=True) + # https://docs.fedoraproject.org/en-US/bootc/initramfs/#_modifying_and_regenerating_the_initrd + kver = bwrap(rootfs, ['ls', '/usr/lib/modules'], capture=True).strip() + bwrap(rootfs, ['env', 'DRACUT_NO_XATTR=1', + 'dracut', '--verbose', '--force', '--reproducible', + '--no-hostonly', f"/usr/lib/modules/{kver}/initramfs.img", kver]) + + def prepare_local_rpm_overrides(rootfs): overrides_repo = os.path.join(SRCDIR, 'overrides/rpm') if not os.path.isdir(f'{overrides_repo}/repodata'): @@ -252,8 +298,9 @@ priority=1 # Could upstream this as e.g. `bootc-base-imagectl runroot /rootfs ` maybe? # But we'd need to carry it anyway at least for RHCOS 9.6. def bwrap(rootfs, args, capture=False): - args = ['bwrap', '--bind', f'{rootfs}', '/', '--dev', '/dev', '--proc', - '/proc', '--tmpfs', '/tmp', '--tmpfs', '/var', '--tmpfs', '/run', + args = ['bwrap', '--bind', f'{rootfs}', '/', '--dev', '/dev', + '--proc', '/proc', '--tmpfs', '/tmp', '--tmpfs', '/var', + '--tmpfs', '/var/tmp', '--tmpfs', '/run', '--bind', '/run/.containerenv', '/run/.containerenv', '--'] + args if capture: return subprocess.check_output(args, encoding='utf-8') @@ -310,8 +357,6 @@ def inject_version_info(rootfs, base_version, version): (k, v) = line.split('=', 1) os_release[k] = v - # The fields modified here match those in inject_dracut_version below. Keep - # them in sync. for key in ['VERSION', 'PRETTY_NAME']: os_release[key] = os_release[key].replace(base_version, version) os_release['OSTREE_VERSION'] = f"'{version}'" @@ -322,46 +367,6 @@ def inject_version_info(rootfs, base_version, version): f.write(f'{k}={v}\n') -# This dynamically generates a dracut module which doesn't actually install -# anything in the initrd. It just mutates the initrd-release installed by -# dracut-systemd in the same way we mutate os-release above. Normally, we'd -# only need inject_version_info above and dracut would create its initrd-release -# based on that. But injection happens after the rpm-ostree compose and we want -# to avoid regenerating the initramfs just for that. -def inject_dracut_version(base_version, version): - tmpd = tempfile.TemporaryDirectory() - # we use 99 here so we run last, i.e. after initrd-release exists - module_setup = os.path.join(tmpd.name, 'usr/lib/dracut/modules.d/99dracut-coreos-version/module-setup.sh') - os.makedirs(os.path.dirname(module_setup), exist_ok=True) - with open(module_setup, 'w', encoding='utf-8') as f: - # The fields modified here match those in inject_version_info above. - # Keep them in sync. - f.write(f''' -check() {{ return 0; }} -install() {{ - sed -i -E -e '/^(PRETTY_NAME|VERSION)=/ s/{base_version}/{version}/' $initdir/usr/lib/initrd-release - echo "OSTREE_VERSION='{version}'" >> $initdir/usr/lib/initrd-release - echo "IMAGE_VERSION='{version}'" >> $initdir/usr/lib/initrd-release - - # XXX SUPER HACK to stamp out the systemd-gpt-auto-generator in the - # initramfs. The rm of the file in a postprocess we do runs after - # the initramfs is generated so we need something like this - # delivered via a dracut module that gets into an overlay in the - # original rpm-ostree compose. - rm -v $initdir/usr/lib/systemd/system-generators/systemd-gpt-auto-generator -}} -''') - return tmpd - - -def cleanup_dracut_version(rootfs, tmpd): - # we can nuke this from the rootfs now; any dracut regeneration from this - # point on (e.g. in derived builds, or client side) will be able to see the - # os-release changes done by inject_version_info - shutil.rmtree(f'{rootfs}/usr/lib/dracut/modules.d/99dracut-coreos-version') - del tmpd - - # This re-implements cosa's overlay logic. def gather_overlays(manifest): overlays = [] @@ -463,7 +468,9 @@ def calculate_inputhash(rootfs, overlays, manifest): all_files = sorted(all_files) for file in all_files: with open(file, 'rb') as f: - h.update(hashlib.file_digest(f, 'sha256').digest()) + # When python3.11+ is the minimal version we can use hashlib.file_digest + # h.update(hashlib.file_digest(f, 'sha256').digest()) + h.update(hashlib.sha256(f.read()).digest()) has_x_bit = os.stat(f.fileno()).st_mode & 0o111 != 0 h.update(bytes([has_x_bit]))