Skip to content

Image mode (bootc): a require defined as a file path is silently skipped when not already present #5013

Description

@thrix-vault

Summary

In image mode (package manager: bootc), a test/plan requirement specified as a file path (e.g. require: /usr/bin/go) is silently dropped when the path is not already present on the booted image. tmt neither bakes the providing package into the derived container image nor reports an error — the prepare step reports success, and the test only fails later at runtime when it tries to use the missing dependency.

Requirements given as a package name or a capability work fine; only file-path requirements are affected, and only in image mode.

Reproducer

A plan that requires a file path which is not part of the base image, run against a bootc image-mode guest:

prepare:
  - how: install
    package:
      - /usr/bin/go      # provided by the (not-installed) golang package

Provisioned against Fedora-44-image-mode-x86_64 (is image mode: yes, package manager: bootc, bootc builder: dnf5), tmt 1.75.0.

Observed (from tmt run -dddvvv)

prepare task #2: requires on default-0
    package: 1 package requested
        /usr/bin/go
    cmd: rpm -q --whatprovides /usr/bin/go
    stderr: error: file /usr/bin/go: No such file or directory
Command returned '1' (failure).
No Containerfile directives to build container image, skipping build.
...
summary: 2 preparations applied      # <-- reported as success

The package is never installed. The test then fails:

inner wrapper: set -eo pipefail; rpm -qf /usr/bin/go
    cmd: rpm -qf /usr/bin/go
    stdout: error: file /usr/bin/go: No such file or directory
Command returned '1' (failure).

Compare with /usr/bin/flock in the same run, which is present — rpm -q --whatprovides /usr/bin/flock returns util-linux-core-..., so it is correctly treated as already installed.

Expected

The package providing /usr/bin/go (golang) should be installed into the derived image (added as a RUN dnf install … Containerfile directive), exactly as it would be for require: golang. If it genuinely cannot be provided, prepare should fail loudly, not silently succeed.

Root cause

Two compounding defects, both in tmt/package_managers/bootc.py.

1. Bootc.check_presence does not classify an absent file path as "absent"

Bootc.check_presence runs rpm -q --whatprovides <path> and parses the result. For an absent file path, rpm writes

error: file /usr/bin/go: No such file or directory

to stderr (with empty stdout) and exits 1. The parser only inspects stdout and only matches package … is not installed / no package provides …:

https://github.com/teemtee/tmt/blob/HEAD/tmt/package_managers/bootc.py#L212-L240

So the installable gets no entry in the returned presence dict.

Note this differs from Dnf.check_presence, which gained a third matcher for file paths in e99abfd (#3791):

https://github.com/teemtee/tmt/blob/HEAD/tmt/package_managers/dnf.py#L330-L336

…but even that matcher inspects stdout, while rpm writes the error: file … line to stderr — so it does not actually fire for absent paths either. (For the classic dnf path this is harmless: the install is a shell rpm -q --whatprovides X || dnf install -y X, so the || short-circuit installs X regardless of the Python-side presence parsing. In image mode the install is gated by the Python check_presence result, so the parsing bug becomes load-bearing.)

2. Bootc.install derives "missing" from the presence dict, masking #1

presence = self.check_presence(*installables)
missing_installables = {
    installable for installable, present in presence.items() if not present
}

https://github.com/teemtee/tmt/blob/HEAD/tmt/package_managers/bootc.py#L327-L341

Because it iterates presence.items() rather than the requested installables, any installable absent from the dict (which is exactly what #1 produces for a missing file path) is silently treated as not missing → nothing is installed → "No Containerfile directives to build container image, skipping build."

This regressed in #4131 (518e15b), which changed

installable for installable in installables if not presence[installable]

to

installable for installable, present in presence.items() if not present

The original form would have raised KeyError (a loud failure) for a missing file path; the new form silently drops it.

Proposed fix

Make Bootc.check_presence robust to the stderr/empty-stdout case, and make Bootc.install fail-safe by treating "unknown" as "missing":

  1. check_presence — search the combined stdout + stderr for per-installable absence markers, and default to present only when no absence marker is found, iterating the requested installables rather than relying on positional zip() alignment (which breaks as soon as one item errors to stderr while another prints to stdout):

    def check_presence(self, *installables):
        try:
            output = self.guest.execute(self.engine.check_presence(*installables))
            combined = f"{output.stdout or ''}\n{output.stderr or ''}"
        except RunError as exc:
            combined = f"{exc.stdout or ''}\n{exc.stderr or ''}"
    
        results: dict[Installable, bool] = {}
        for installable in installables:
            name = re.escape(str(installable))
            absent = (
                re.search(rf'package {name} is not installed', combined)
                or re.search(rf'no package provides {name}', combined)
                or re.search(rf'error: file {name}: No such file or directory', combined)
            )
            results[installable] = absent is None
        return results
  2. install — never silently drop a requested installable:

    missing_installables = {i for i in installables if not presence.get(i, False)}
  3. Add regression coverage for an absent file-path requirement in image mode (the existing check_presence/install_filesystempath tests only exercise file paths that are already present, e.g. /usr/bin/arch, /usr/bin/flock, so this case is currently untested). The same stdout-vs-stderr point should be considered for the Dnf.check_presence file-path matcher.

Environment

  • tmt: 1.75.0
  • guest: Fedora-44-image-mode-x86_64.qcow2 (quay.io/testing-farm/fedora-bootc:44), provisioned with how: virtual
  • package manager: bootc, bootc builder: dnf5, is image mode: yes

Generated-by: Claude Code

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

Status
backlog
Status
investigate

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions