Skip to content

Commit 25b662b

Browse files
committed
cmd-fetch: Derive kofnlus lockfiles from rpm-ostree
Let's use our lockfiles to get hermeto download a set of matching RPMs. This is done through injecting `includepkg` filters to the repofiles then feeding that to `rpm-lockfile-prototype`. The overview is: we parse the manifest for a package list and enabled repos. For each package, we take the expected EVRA in the rpm-ostree lockfile and inject it in the `includepkg` filter in the repos files. Finally we feed that package list and the custom repo definition to rpm-ostree-prototype. The result is a hermeto/cachi2 lockfile we will commit to the repo. This `--konflux` flag would then be added to the Jenkins bump-lockfiles job to update both lockfiles in the same commit. This will allow hermetic builds in Konflux. I also added a sanity check at the end that iterate over both lockfiles and compare all versions string to make sure we have exact matches all around. See also coreos/fedora-coreos-config#3644 for the inital implementation.
1 parent e9034ee commit 25b662b

File tree

3 files changed

+266
-1
lines changed

3 files changed

+266
-1
lines changed

src/cmd-fetch

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ 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+
--konflux Generate hermeto lockfile for Konflux. Will only do something when called
43+
alongside --update-lockfile
4244
4345
EOF
4446
}
@@ -50,8 +52,9 @@ IGNORE_COSA_OVERRIDES_ARG=--ignore-cosa-overrides
5052
DRY_RUN=
5153
FORCE_ARG=
5254
STRICT=
55+
KONFLUX=
5356
rc=0
54-
options=$(getopt --options h --longoptions help,update-lockfile,dry-run,with-cosa-overrides,write-lockfile-to:,strict,force,autolock: -- "$@") || rc=$?
57+
options=$(getopt --options h --longoptions help,update-lockfile,dry-run,with-cosa-overrides,write-lockfile-to:,strict,force,autolock:,konflux -- "$@") || rc=$?
5558
[ $rc -eq 0 ] || {
5659
print_help
5760
exit 1
@@ -87,6 +90,9 @@ while true; do
8790
shift;
8891
AUTOLOCK_VERSION=$1
8992
;;
93+
--konflux)
94+
KONFLUX=1
95+
;;
9096
--)
9197
shift
9298
break
@@ -175,4 +181,16 @@ if [ -n "${UPDATE_LOCKFILE}" ]; then
175181
# cd back to workdir in case OUTPUT_LOCKFILE is relative
176182
(cd "${workdir}" && mv -f "${tmprepo}/tmp/manifest-lock.json" "${outfile}")
177183
echo "Wrote out lockfile ${outfile}"
184+
185+
if [ -n "${KONFLUX}" ]; then
186+
echo "Generating hermeto input file..."
187+
/usr/lib/coreos-assembler/konflux-rpm-lockfile generate "${manifest}" --context "${configdir}" --output "${tmprepo}/tmp/rpm.in.${arch}.yaml"
188+
rpm-lockfile-prototype "${tmprepo}/tmp/rpm.in.${arch}.yaml" --outfile "${tmprepo}/tmp/rpms.lock.${arch}.yaml"
189+
# Sanity check the generated hermeto lockfile
190+
echo "Sanity check consistency between konflux and rpm-ostree lockfiles..."
191+
/usr/lib/coreos-assembler/konflux-rpm-lockfile compare "${configdir}/manifest-lock.${arch}.json" "${tmprepo}/tmp/rpms.lock.${arch}.yaml"
192+
(cd "${workdir}" && mv -f "${tmprepo}/tmp/rpms.lock.${arch}.yaml" "konflux-rpms-lock.${arch}.yaml")
193+
echo "Wrote out hermeto lockfile: /tmp/rpms.lock.${arch}.yaml
194+
195+
fi
178196
fi

src/cosalib/cmdlib.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,3 +575,52 @@ 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_treefile(manifest_path, deriving=False):
581+
"""
582+
Parses an rpm-ostree manifest using 'rpm-ostree compose tree'.
583+
If deriving is True, it ensures that the treefile represents only the
584+
CoreOS bits and doesn't recurse into fedora-bootc.
585+
"""
586+
with tempfile.NamedTemporaryFile(suffix='.json', mode='w') as tmp_manifest:
587+
json.dump({
588+
"variables": {
589+
"deriving": deriving
590+
},
591+
"include": manifest_path
592+
}, tmp_manifest)
593+
tmp_manifest.flush()
594+
data = subprocess.check_output(['rpm-ostree', 'compose', 'tree',
595+
'--print-only', tmp_manifest.name])
596+
return json.loads(data)
597+
598+
599+
def get_locked_nevras(srcdir):
600+
"""
601+
Gathers all locked packages from the manifest-lock files.
602+
The return format can be a dictionary of {pkgname: evr} or a list
603+
of strings in the format 'pkgname-evr'.
604+
For example:
605+
- as_strings=False: {'rpm-ostree': '2024.4-1.fc40'}
606+
- as_strings=True: ['rpm-ostree-2024.4-1.fc40']
607+
"""
608+
arch = get_basearch()
609+
lockfile_path = os.path.join(srcdir, f"manifest-lock.{arch}.json")
610+
overrides_path = os.path.join(srcdir, "manifest-lock.overrides.yaml")
611+
overrides_arch_path = os.path.join(srcdir, f"manifest-lock.overrides.{arch}.json")
612+
613+
locks = {}
614+
for path in [lockfile_path, overrides_path, overrides_arch_path]:
615+
if os.path.exists(path):
616+
with open(path) as f:
617+
if path.endswith('.yaml'):
618+
data = yaml.safe_load(f)
619+
else:
620+
data = json.load(f)
621+
# this essentially re-implements the merge semantics of rpm-ostree
622+
locks.update({pkgname: v.get('evra') or v.get('evr')
623+
for (pkgname, v) in data['packages'].items()})
624+
625+
return locks
626+

src/konflux-rpm-lockfile

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/python
2+
3+
import argparse
4+
import json
5+
import os
6+
import re
7+
import sys
8+
import yaml
9+
10+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
11+
from cosalib.cmdlib import get_treefile, get_locked_nevras, get_basearch
12+
13+
14+
ARCH = get_basearch()
15+
16+
17+
def locks_mismatch(rpm_ostree_lock, hermeto_lock):
18+
"""
19+
Compares a JSON manifest lock with a YAML RPM lock for x86_64.
20+
21+
Args:
22+
rpm_ostree_lock (str): Path to the rpm-ostree JSON manifest lock file.
23+
hermeto_lock (str): Path to the hermeto YAML lock file.
24+
25+
Returns:
26+
bool: True if there are differences, False otherwise.
27+
"""
28+
with open(rpm_ostree_lock, 'r') as f:
29+
manifest_data = {name: details['evra'] for name, details in json.load(f).get('packages', {}).items()}
30+
31+
with open(hermeto_lock, 'r') as f:
32+
yaml_data = {pkg['name']: str(pkg['evr']) for arch in yaml.safe_load(f).get('arches', []) if arch.get('arch') == 'x86_64' for pkg in arch.get('packages', [])}
33+
34+
rpm_ostree_lock_set = set(manifest_data.keys())
35+
hermeto_lock_set = set(yaml_data.keys())
36+
37+
differences_found = False
38+
39+
if only_in_manifest := sorted(list(rpm_ostree_lock_set - hermeto_lock_set)):
40+
differences_found = True
41+
print("Packages only in rpm-ostree lockfile:")
42+
for pkg in only_in_manifest:
43+
print(f"- {pkg} ({manifest_data[pkg]})")
44+
45+
if only_in_yaml := sorted(list(hermeto_lock_set - rpm_ostree_lock_set)):
46+
differences_found = True
47+
print("\nPackages only in hermeto lockfile:")
48+
for pkg in only_in_yaml:
49+
print(f"- {pkg} ({yaml_data[pkg]})")
50+
51+
mismatches = []
52+
for pkg in sorted(list(rpm_ostree_lock_set.intersection(hermeto_lock_set))):
53+
manifest_evr = manifest_data[pkg].rsplit('.', 1)[0]
54+
if manifest_evr != yaml_data[pkg]:
55+
mismatches.append(f"- {pkg}:\n - rpm-ostree: {manifest_data[pkg]}\n - hermeto: {yaml_data[pkg]}")
56+
57+
if mismatches:
58+
differences_found = True
59+
print("\nVersion mismatches:")
60+
for mismatch in mismatches:
61+
print(mismatch)
62+
else:
63+
print(f"\u2705 No mismatches founds between {rpm_ostree_lock} and {hermeto_lock}")
64+
65+
return differences_found
66+
67+
68+
def filter_repofile(repos, locked_nevras, repo_path, output_dir):
69+
if not os.path.exists(repo_path):
70+
print(f"Error: {repo_path} not found. Cannot inject includepkg filter.")
71+
return
72+
73+
include_str = ','.join(locked_nevras)
74+
75+
with open(repo_path, 'r') as f:
76+
repofile = f.read()
77+
78+
# We use a regex that looks for [reo_name] on a line by itself,
79+
# possibly with whitespace.
80+
sections = re.split(r'^\s*(\[.+\])\s*', repofile, flags=re.MULTILINE)
81+
82+
new_content = sections[0] # part before any section
83+
keep = False
84+
85+
# sections will be [before, section1_name, section1_content, section2_name, section2_content, ...]
86+
for i in range(1, len(sections), 2):
87+
name = sections[i]
88+
# ignore repos that don't match the repos we want to use
89+
if name.strip("[]") in repos:
90+
repodef = sections[i+1]
91+
if 'includepkgs=' not in repodef:
92+
# We only keep the repo definition that we edited
93+
# to avoid accidentaly taking in other packages
94+
# from a repofile already having an includepkgs
95+
# directive.
96+
keep = True
97+
new_content += name + '\n'
98+
repodef += f"includepkgs={include_str}\n"
99+
new_content += repodef
100+
101+
filename = None
102+
if keep:
103+
filename = os.path.basename(repo_path.removesuffix(".repo"))
104+
filename = os.path.join(output_dir, f"{filename}-hermeto.repo")
105+
with open(filename, 'w') as f:
106+
f.write(new_content)
107+
print(f"Wrote filtered repo to: {filename}")
108+
109+
return keep, filename
110+
111+
112+
def build_rpm_lockfile_config(packages, repo_files):
113+
"""
114+
Augments package names in rpm_in_data with version numbers from locks.
115+
Populates contentOrigin and repofiles.
116+
"""
117+
# Initialize the structure for rpm_lockfile_input, similar to write_rpms_input_file
118+
# This ensures consistency whether it comes from a manifest or directly
119+
rpm_lockfile_config = {
120+
'contentOrigin': {
121+
'repofiles': repo_files
122+
},
123+
'installWeakDeps': False,
124+
'context': {
125+
'bare': True,
126+
},
127+
'packages': packages
128+
}
129+
130+
return rpm_lockfile_config
131+
132+
def generate_lockfile(contextdir, manifest, output_path):
133+
"""
134+
Generates the rpm-lockfile-prototype input file.
135+
"""
136+
manifest_data = get_treefile(manifest, deriving=False)
137+
repos = manifest_data.get('repos', [])
138+
repos += manifest_data.get('lockfile-repos', [])
139+
packages = manifest_data.get('packages', [])
140+
locks = get_locked_nevras(contextdir)
141+
repofiles = [file for file in os.listdir(contextdir) if file.endswith('.repo')]
142+
relevant_repofiles = []
143+
output_dir = os.path.dirname(output_path)
144+
145+
if locks:
146+
locked_nevras = [f'{k}-{v}' for (k, v) in locks.items()]
147+
for repofile in repofiles:
148+
keep, newrepo = filter_repofile(repos, locked_nevras, os.path.join(contextdir, repofile), output_dir)
149+
if keep:
150+
relevant_repofiles.append(newrepo)
151+
152+
augmented_rpm_in = build_rpm_lockfile_config(packages, relevant_repofiles)
153+
154+
try:
155+
with open(output_path, 'w') as f:
156+
yaml.safe_dump(augmented_rpm_in, f, default_flow_style=False)
157+
print(f"rpm-lockfile-prototype input config wrote to: {output_path}")
158+
except IOError as e:
159+
print(f"\u274c Error: Could not write to final output file '{output_path}'. Reason: {e}")
160+
sys.exit(1)
161+
162+
163+
if __name__ == "__main__":
164+
parser = argparse.ArgumentParser(
165+
description="Generate and compare rpm-lockfile-prototype input files."
166+
)
167+
subparsers = parser.add_subparsers(dest='command', required=True)
168+
169+
parser_generate = subparsers.add_parser('generate', help='Generate rpm-lockfile-prototype input file.')
170+
parser_generate.add_argument(
171+
'manifest',
172+
help='Path to the rpm-ostree manifest (e.g., fedora-coreos-config/manifest.yaml)'
173+
)
174+
175+
parser_generate.add_argument(
176+
'--context',
177+
default='.',
178+
help="Path to the directory containing repofiles and lockfiles. (default: '.')"
179+
)
180+
181+
parser_generate.add_argument(
182+
'--output',
183+
default='./rpms.in.yaml',
184+
help="Path for the final rpm-lockfile-protoype config file. (default: './rpms.in.yaml')"
185+
)
186+
187+
parser_compare = subparsers.add_parser('compare', help='Compare rpm-ostree JSON lockfile with hermeto RPM lock.')
188+
parser_compare.add_argument('rpmostree_lockfile', help='Path to the rpm-ostree manifest lock file (JSON).')
189+
parser_compare.add_argument('hermeto_lockfile', help='Path to the hermeto RPM lock file (YAML).')
190+
191+
args = parser.parse_args()
192+
193+
if args.command == 'generate':
194+
manifest_abs_path = os.path.abspath(args.manifest)
195+
generate_lockfile(args.context, manifest_abs_path, args.output)
196+
elif args.command == 'compare':
197+
if locks_mismatch(args.rpmostree_lockfile, args.hermeto_lockfile):
198+
sys.exit(1)

0 commit comments

Comments
 (0)