Skip to content

Commit 442cd35

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 442cd35

File tree

3 files changed

+268
-2
lines changed

3 files changed

+268
-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) 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: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
#!/usr/bin/python
2+
3+
import argparse
4+
import json
5+
import os
6+
import re
7+
import sys
8+
import subprocess
9+
import yaml
10+
11+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
12+
from cosalib.cmdlib import get_locked_nevras, get_basearch
13+
14+
15+
def format_packages_with_repoid(pkgs, repos):
16+
"""
17+
Takes a list of package URLs and repos and returns a list
18+
of package dictionaries with repoids.
19+
"""
20+
packages = []
21+
local_repos = list(repos)
22+
if "fedora-coreos-pool" in local_repos:
23+
local_repos.remove("fedora-coreos-pool")
24+
25+
if not local_repos:
26+
if pkgs:
27+
print("Error: No repos available to associate with packages.", file=sys.stderr)
28+
sys.exit(1)
29+
return []
30+
31+
# trick to make sure we have at least an entry for each repoid
32+
# this way hermeto will create all the matching repo definitions
33+
# and rpm-ostree will find all the expected repos.
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 = 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+
print(f"Unexpected arch in override file : {arch}")
91+
92+
# Reconstruct the arches list
93+
base_lockfile['arches'] = list(base_arches.values())
94+
return base_lockfile
95+
96+
97+
def query_packages_location(locks, repoquery_args):
98+
"""
99+
Resolves packages URLs for a given architecture.
100+
"""
101+
pkg_urls = []
102+
if not locks:
103+
return pkg_urls
104+
105+
locked_nevras = [f'{k}-{v}' for (k, v) in locks.items()]
106+
cmd = ['dnf', 'repoquery'] + locked_nevras + repoquery_args
107+
result = subprocess.check_output(cmd, text=True)
108+
109+
processed_urls = {}
110+
for line in result.strip().split('\n'):
111+
# ignore empty lines
112+
if not line:
113+
continue
114+
parts = line.split(' ', 1)
115+
# If more than 1 URL is returned we pick the first one
116+
if len(parts) == 2:
117+
name, url = parts
118+
if name not in processed_urls:
119+
processed_urls[name] = url
120+
pkg_urls = list(processed_urls.values())
121+
# sanity check all the packages got resolved
122+
if len(pkg_urls) < len(locked_nevras):
123+
print("Some packages from the lockfile could not be resolved. The rpm-ostree lockfile is probably out of date.")
124+
for name in locks.keys():
125+
if name not in processed_urls:
126+
print(f"could not resolve package {name}")
127+
sys.exit(1)
128+
129+
return pkg_urls
130+
131+
132+
def generate_lockfile(contextdir, manifest, output_path, arches):
133+
"""
134+
Generates the cachi2/hermeto RPM lock file.
135+
"""
136+
if not arches:
137+
arches_to_resolve = [get_basearch()]
138+
elif 'all' in arches:
139+
arches_to_resolve = ['x86_64', 'aarch64', 's390x', 'ppc64le']
140+
else:
141+
arches_to_resolve = arches
142+
143+
if os.path.exists(manifest):
144+
with open(manifest, 'r', encoding="utf8") as f:
145+
manifest_data = json.load(f)
146+
else:
147+
print(f"flattened manifest not found at {manifest}")
148+
sys.exit(1)
149+
150+
repos = manifest_data.get('repos', [])
151+
repos += manifest_data.get('lockfile-repos', [])
152+
repofiles = [file for file in os.listdir(contextdir) if file.endswith('.repo')]
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+
repoquery_args += ['--forcearch', arch, '--arch', arch, '--arch', 'noarch']
172+
pkg_urls = query_packages_location(locks, repoquery_args)
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="utf8") as f:
190+
yaml.safe_dump(lockfile, f, default_flow_style=False)
191+
print(f"Successfully generated lockfile at {output_path}")
192+
except IOError as e:
193+
print(f"\u274c Error: Could not write to final output file '{output_path}'. Reason: {e}")
194+
sys.exit(1)
195+
196+
197+
if __name__ == "__main__":
198+
parser = argparse.ArgumentParser(
199+
description="Generate hermeto lock files."
200+
)
201+
202+
parser.add_argument(
203+
'manifest',
204+
help='Path to the flattened rpm-ostree manifest (e.g., tmp/manifest.json)'
205+
)
206+
207+
parser.add_argument(
208+
'--context',
209+
default='.',
210+
help="Path to the directory containing repofiles and lockfiles. (default: '.')"
211+
)
212+
213+
parser.add_argument(
214+
'--output',
215+
default='./rpms.lock.yaml',
216+
help="Path for the hermeto lockfile. (default: './rpms.lock.yaml')"
217+
)
218+
219+
parser.add_argument(
220+
'--arch',
221+
action='append',
222+
choices=['x86_64', 'aarch64', 's390x', 'ppc64le', 'all'],
223+
help="The architecture to resolve. Can be specified multiple times. 'all' resolves all architectures."
224+
)
225+
226+
args = parser.parse_args()
227+
228+
manifest_abs_path = os.path.abspath(args.manifest)
229+
generate_lockfile(args.context, manifest_abs_path, args.output, args.arch)

0 commit comments

Comments
 (0)