Skip to content

Commit d014342

Browse files
committed
cmd-fetch: Derive konflux lockfiles from rpm-ostree
Let's use our lockfiles to get hermeto download a set of matching RPMs. This is done by simply getting the package URL with `dnf repoquery` using the full NEVRA from the rpm-ostree lockfile. This will allow hermetic builds in Konflux. I also added an override yaml file to allow merging packages to the lock this will allow injecting arbitrary package. This will help enabling workarounds such as coreos/fedora-coreos-config@fb167ed See also coreos/fedora-coreos-config#3644 for the inital implementation and discussion.
1 parent e9034ee commit d014342

File tree

3 files changed

+267
-2
lines changed

3 files changed

+267
-2
lines changed

src/cmd-fetch

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Usage: coreos-assembler fetch --help
3939
--write-lockfile-to=FILE Write updated base lockfile to separate file
4040
--with-cosa-overrides Don't ignore cosa overrides in `overrides/rpm`
4141
--autolock=VERSION If no base lockfile used, create one from any arch build of `VERSION`
42-
42+
--konflux Generate hermeto lockfile for Konflux derived from the rpm-ostree lockfiles.
4343
EOF
4444
}
4545

@@ -50,8 +50,9 @@ IGNORE_COSA_OVERRIDES_ARG=--ignore-cosa-overrides
5050
DRY_RUN=
5151
FORCE_ARG=
5252
STRICT=
53+
KONFLUX=
5354
rc=0
54-
options=$(getopt --options h --longoptions help,update-lockfile,dry-run,with-cosa-overrides,write-lockfile-to:,strict,force,autolock: -- "$@") || rc=$?
55+
options=$(getopt --options h --longoptions help,update-lockfile,dry-run,with-cosa-overrides,write-lockfile-to:,strict,force,autolock:,konflux -- "$@") || rc=$?
5556
[ $rc -eq 0 ] || {
5657
print_help
5758
exit 1
@@ -87,6 +88,9 @@ while true; do
8788
shift;
8889
AUTOLOCK_VERSION=$1
8990
;;
91+
--konflux)
92+
KONFLUX=1
93+
;;
9094
--)
9195
shift
9296
break
@@ -176,3 +180,10 @@ if [ -n "${UPDATE_LOCKFILE}" ]; then
176180
(cd "${workdir}" && mv -f "${tmprepo}/tmp/manifest-lock.json" "${outfile}")
177181
echo "Wrote out lockfile ${outfile}"
178182
fi
183+
184+
if [ -n "${KONFLUX}" ]; then
185+
echo "Generating hermeto lockfile..."
186+
/usr/lib/coreos-assembler/konflux-rpm-lockfile "${flattened_manifest}" --context "${configdir}" --output "${tmprepo}/tmp/rpms.lock.yaml" --arch all
187+
(cd "${workdir}" && mv -f "${tmprepo}/tmp/rpms.lock.yaml" "konflux-rpms-lock.yaml")
188+
echo "Wrote out hermeto lockfile: konflux-rpms-lock.yaml"
189+
fi

src/cosalib/cmdlib.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,3 +575,29 @@ def ensure_glob(pathname, **kwargs):
575575
def ncpu():
576576
'''Return the number of usable CPUs we have for parallelism.'''
577577
return int(subprocess.check_output(['kola', 'ncpu']))
578+
579+
580+
def get_locked_nevras(srcdir, arch=None):
581+
"""
582+
Gathers all locked packages from the manifest-lock files.
583+
The return format can be a dictionary of {pkgname: evr}
584+
"""
585+
if not arch:
586+
arch = get_basearch()
587+
lockfile_path = os.path.join(srcdir, f"manifest-lock.{arch}.json")
588+
overrides_path = os.path.join(srcdir, "manifest-lock.overrides.yaml")
589+
overrides_arch_path = os.path.join(srcdir, f"manifest-lock.overrides.{arch}.json")
590+
591+
locks = {}
592+
for path in [lockfile_path, overrides_path, overrides_arch_path]:
593+
if os.path.exists(path):
594+
with open(path, encoding='utf-8') as f:
595+
if path.endswith('.yaml'):
596+
data = yaml.safe_load(f)
597+
else:
598+
data = json.load(f)
599+
# this essentially re-implements the merge semantics of rpm-ostree
600+
locks.update({pkgname: v.get('evra') or v.get('evr')
601+
for (pkgname, v) in data['packages'].items()})
602+
603+
return locks

src/konflux-rpm-lockfile

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
#!/usr/bin/python
2+
3+
import argparse
4+
import json
5+
import os
6+
import sys
7+
import subprocess
8+
import yaml
9+
10+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
11+
from cosalib.cmdlib import get_locked_nevras, get_basearch
12+
13+
14+
def format_packages_with_repoid(pkgs, repos):
15+
"""
16+
Takes a list of package URLs and repos and returns a list
17+
of package dictionaries with repoids.
18+
"""
19+
packages = []
20+
local_repos = list(repos)
21+
if "fedora-coreos-pool" in local_repos:
22+
local_repos.remove("fedora-coreos-pool")
23+
24+
if not local_repos:
25+
if pkgs:
26+
print("Error: No repos to associate with packages.", file=sys.stderr)
27+
sys.exit(1)
28+
return []
29+
30+
# We want to ensure that hermeto creates repo definitions for every repository.
31+
# A round-robin assignment ensures each repo is mentioned at least once.
32+
# This is needed because rpm-ostree uses the full list of repos to
33+
# resolve packages and errors out if a repository is missing.
34+
repo_numbers = len(local_repos)
35+
i = 0
36+
for pkg in pkgs:
37+
packages.append({"url": pkg, "repoid": local_repos[i % repo_numbers]})
38+
i += 1
39+
return packages
40+
41+
42+
def write_hermeto_lockfile(arch_packages, repos):
43+
"""
44+
Writes the hermeto lockfile structure.
45+
"""
46+
arches = []
47+
for arch_data in arch_packages:
48+
arch = arch_data['arch']
49+
pkgs = arch_data['packages']
50+
formatted_packages = format_packages_with_repoid(pkgs, repos)
51+
arches.append({
52+
'arch': arch,
53+
'packages': formatted_packages
54+
})
55+
56+
lockfile = {
57+
'lockfileVersion': 1,
58+
"lockfileVendor": "redhat",
59+
"arches": arches
60+
}
61+
62+
return lockfile
63+
64+
65+
def merge_lockfiles(base_lockfile, override_lockfile):
66+
"""
67+
Merges an override lockfile into a base lockfile.
68+
"""
69+
if not override_lockfile:
70+
return base_lockfile
71+
72+
# Create a dictionary for base arches for easy lookup
73+
base_arches = {arch['arch']: arch for arch in base_lockfile.get('arches', [])}
74+
75+
override = override_lockfile.get('arches', [])
76+
if not override:
77+
return base_lockfile
78+
79+
for override_entry in override:
80+
# override_entry is a dict like {'arch': x86_64','packages': [...]}
81+
if not isinstance(override_entry, dict):
82+
continue
83+
arch = override_entry.get('arch', None)
84+
override_packages = override_entry.get('packages', [])
85+
if arch in base_arches:
86+
# Merge packages
87+
base_packages = base_arches[arch].get('packages', [])
88+
base_packages += override_packages
89+
else:
90+
# Add the arch from the override file
91+
base_arches[arch] = override_packages
92+
93+
# Reconstruct the arches list
94+
base_lockfile['arches'] = list(base_arches.values())
95+
return base_lockfile
96+
97+
98+
def query_packages_location(locks, repoquery_args):
99+
"""
100+
Resolves packages URLs for a given architecture.
101+
"""
102+
pkg_urls = []
103+
if not locks:
104+
return pkg_urls
105+
106+
locked_nevras = [f'{k}-{v}' for (k, v) in locks.items()]
107+
cmd = ['dnf', 'repoquery'] + locked_nevras + repoquery_args
108+
result = subprocess.check_output(cmd, text=True)
109+
110+
processed_urls = {}
111+
for line in result.strip().split('\n'):
112+
# ignore empty lines
113+
if not line:
114+
continue
115+
parts = line.split(' ', 1)
116+
# If more than 1 URL is returned we pick the first one
117+
if len(parts) == 2:
118+
name, url = parts
119+
if name not in processed_urls:
120+
processed_urls[name] = url
121+
pkg_urls = list(processed_urls.values())
122+
# sanity check all the packages got resolved
123+
if len(pkg_urls) < len(locked_nevras):
124+
print("Some packages from the lockfile could not be resolved. The rpm-ostree lockfile is probably out of date.")
125+
for name in locks.keys():
126+
if name not in processed_urls:
127+
print(f"could not resolve package {name}")
128+
sys.exit(1)
129+
130+
return pkg_urls
131+
132+
133+
def generate_lockfile(contextdir, manifest, output_path, arches):
134+
"""
135+
Generates the cachi2/hermeto RPM lock file.
136+
"""
137+
if not arches:
138+
arches_to_resolve = [get_basearch()]
139+
elif 'all' in arches:
140+
arches_to_resolve = ['x86_64', 'aarch64', 's390x', 'ppc64le']
141+
else:
142+
arches_to_resolve = arches
143+
144+
if os.path.exists(manifest):
145+
with open(manifest, 'r', encoding='utf-8') as f:
146+
manifest_data = json.load(f)
147+
else:
148+
print(f"flattened manifest not found at {manifest}")
149+
sys.exit(1)
150+
151+
repos = manifest_data.get('repos', [])
152+
repos += manifest_data.get('lockfile-repos', [])
153+
154+
repoquery_args = ["--queryformat", "%{name} %{location}\n", "--disablerepo=*", "--refresh"]
155+
# Tell dnf to load repos files from $contextdir
156+
repoquery_args.extend([f"--setopt=reposdir={contextdir}"])
157+
158+
for repoid in set(repos):
159+
repoquery_args.extend([f"--enablerepo={repoid}"])
160+
161+
packages = []
162+
for arch in arches_to_resolve:
163+
locks = get_locked_nevras(contextdir, arch)
164+
if not locks:
165+
print(f"This tool derive the konflux lockfile from rpm-ostree lockfiles. No manifest-lock exist for {arch} in {contextdir}")
166+
sys.exit(1)
167+
print(f"Resolving packages for {arch}...")
168+
# append noarch as well because otherwise tose packages get excluded from results
169+
# We use --forcearch here because otherwise dnf still respect the system basearch
170+
# we have to specify both --arch and --forcearch to get both result for $arch and $noarch
171+
args_arch = ['--forcearch', arch, '--arch', arch, '--arch', 'noarch']
172+
pkg_urls = query_packages_location(locks, repoquery_args + args_arch)
173+
packages.append({'arch': arch, 'packages': pkg_urls})
174+
175+
lockfile = write_hermeto_lockfile(packages, repos)
176+
177+
override_path = os.path.join(contextdir, 'konflux-lockfile-override.yaml')
178+
if os.path.exists(override_path):
179+
try:
180+
with open(override_path, 'r', encoding="utf8") as f:
181+
override_data = yaml.safe_load(f)
182+
print(f"Merging override from {override_path}")
183+
lockfile = merge_lockfiles(lockfile, override_data)
184+
except (IOError, yaml.YAMLError) as e:
185+
print(f"\u274c Error: Could not read or parse override file '{override_path}'. Reason: {e}")
186+
sys.exit(1)
187+
188+
try:
189+
with open(output_path, 'w', encoding='utf-8') as f:
190+
yaml.safe_dump(lockfile, f, default_flow_style=False)
191+
except IOError as e:
192+
print(f"\u274c Error: Could not write to output file '{output_path}'. Reason: {e}")
193+
sys.exit(1)
194+
195+
196+
if __name__ == "__main__":
197+
parser = argparse.ArgumentParser(
198+
description="Generate hermeto lock files."
199+
)
200+
201+
parser.add_argument(
202+
'manifest',
203+
help='Path to the flattened rpm-ostree manifest (e.g., tmp/manifest.json)'
204+
)
205+
206+
parser.add_argument(
207+
'--context',
208+
default='.',
209+
help="Path to the directory containing repofiles and lockfiles. (default: '.')"
210+
)
211+
212+
parser.add_argument(
213+
'--output',
214+
default='./rpms.lock.yaml',
215+
help="Path for the hermeto lockfile. (default: './rpms.lock.yaml')"
216+
)
217+
218+
parser.add_argument(
219+
'--arch',
220+
action='append',
221+
choices=['x86_64', 'aarch64', 's390x', 'ppc64le', 'all'],
222+
help="The architecture to resolve. Can be specified multiple times. 'all' resolves all architectures."
223+
)
224+
225+
args = parser.parse_args()
226+
227+
manifest_abs_path = os.path.abspath(args.manifest)
228+
generate_lockfile(args.context, manifest_abs_path, args.output, args.arch)

0 commit comments

Comments
 (0)