Skip to content

Commit 526d1fb

Browse files
committed
konflux: Decouple lockfile generation and merging
Refactor `konflux-rpm-lockfile` to support parallel, per-arch builds. The script is now split into `generate` and `merge` subcommands. `cmd-fetch` now calls `generate` to create arch-specific lockfile (e.g., `x86_64.rpms.lock.yaml`). It also look for an existing lockfile and call generate in that case. The `merge` subcommand combines these files in one, while injecting overrides. The override mechanism allow developpers to add extra RPMs needed in the build, e.g. in `buildroot-prep` [1] The pipeline will call merge after running `generate` on each builder. [1] coreos/fedora-coreos-config@fb167ed See: #4298 (comment)
1 parent d014342 commit 526d1fb

File tree

2 files changed

+122
-54
lines changed

2 files changed

+122
-54
lines changed

src/cmd-fetch

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Usage: coreos-assembler fetch --help
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`
4242
--konflux Generate hermeto lockfile for Konflux derived from the rpm-ostree lockfiles.
43+
Auto enabled if `rpms.lock.yaml` is found in the config directory.
4344
EOF
4445
}
4546

@@ -109,6 +110,7 @@ fi
109110

110111
prepare_build
111112

113+
112114
lock_args=
113115
extra_args=
114116

@@ -181,9 +183,10 @@ if [ -n "${UPDATE_LOCKFILE}" ]; then
181183
echo "Wrote out lockfile ${outfile}"
182184
fi
183185

184-
if [ -n "${KONFLUX}" ]; then
186+
KONFLUX_LOCKFILE=rpms.lock.yaml
187+
if [ -n "${KONFLUX}" ] || [ -f "${configdir}/${KONFLUX_LOCKFILE}" ]; then
185188
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+
/usr/lib/coreos-assembler/konflux-rpm-lockfile generate "${flattened_manifest}" --context "${configdir}" --output "${tmprepo}/tmp/${arch}.${KONFLUX_LOCKFILE}"
190+
mv -f "${tmprepo}/tmp/${arch}.${KONFLUX_LOCKFILE}" "${configdir}/${arch}.${KONFLUX_LOCKFILE}"
191+
echo "Wrote out hermeto (konflux) lockfile: ${configdir}/${arch}.${KONFLUX_LOCKFILE}"
189192
fi

src/konflux-rpm-lockfile

Lines changed: 115 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,8 @@ def format_packages_with_repoid(pkgs, repos):
3232
# This is needed because rpm-ostree uses the full list of repos to
3333
# resolve packages and errors out if a repository is missing.
3434
repo_numbers = len(local_repos)
35-
i = 0
36-
for pkg in pkgs:
35+
for i, pkg in enumerate(pkgs):
3736
packages.append({"url": pkg, "repoid": local_repos[i % repo_numbers]})
38-
i += 1
3937
return packages
4038

4139

@@ -62,33 +60,38 @@ def write_hermeto_lockfile(arch_packages, repos):
6260
return lockfile
6361

6462

65-
def merge_lockfiles(base_lockfile, override_lockfile):
63+
def merge_lockfiles(base_lockfile, next_lockfile, override=False):
6664
"""
67-
Merges an override lockfile into a base lockfile.
65+
Merges a lockfile into a base lockfile.
66+
67+
If is_override is True, it will only add packages to existing
68+
architectures. Otherwise, it will add new architectures.
6869
"""
69-
if not override_lockfile:
70+
if not next_lockfile:
7071
return base_lockfile
7172

7273
# Create a dictionary for base arches for easy lookup
7374
base_arches = {arch['arch']: arch for arch in base_lockfile.get('arches', [])}
7475

75-
override = override_lockfile.get('arches', [])
76-
if not override:
76+
next_arches_list = next_lockfile.get('arches', [])
77+
if not next_arches_list:
7778
return base_lockfile
7879

79-
for override_entry in override:
80-
# override_entry is a dict like {'arch': x86_64','packages': [...]}
81-
if not isinstance(override_entry, dict):
80+
for next_arch_entry in next_arches_list:
81+
if not isinstance(next_arch_entry, dict):
82+
continue
83+
arch = next_arch_entry.get('arch', None)
84+
if not arch:
8285
continue
83-
arch = override_entry.get('arch', None)
84-
override_packages = override_entry.get('packages', [])
86+
87+
next_packages = next_arch_entry.get('packages', [])
8588
if arch in base_arches:
86-
# Merge packages
89+
# Arch exists, merge packages
8790
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
91+
base_packages += next_packages
92+
elif not override:
93+
# Arch is new and this is not an override, so add it
94+
base_arches[arch] = next_arch_entry
9295

9396
# Reconstruct the arches list
9497
base_lockfile['arches'] = list(base_arches.values())
@@ -119,21 +122,24 @@ def query_packages_location(locks, repoquery_args):
119122
if name not in processed_urls:
120123
processed_urls[name] = url
121124
pkg_urls = list(processed_urls.values())
122-
# sanity check all the packages got resolved
123-
if len(pkg_urls) < len(locked_nevras):
125+
# sanity check all the locked packages got resolved
126+
if len(pkg_urls) != len(locked_nevras):
124127
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}")
128128
sys.exit(1)
129129

130+
print(f"Done. Resolved location for {len(pkg_urls)} packages.")
130131
return pkg_urls
131132

132133

133-
def generate_lockfile(contextdir, manifest, output_path, arches):
134+
def generate_main(args):
134135
"""
135136
Generates the cachi2/hermeto RPM lock file.
136137
"""
138+
contextdir = args.context
139+
manifest = os.path.abspath(args.manifest)
140+
output_path = args.output
141+
arches = args.arch
142+
137143
if not arches:
138144
arches_to_resolve = [get_basearch()]
139145
elif 'all' in arches:
@@ -151,7 +157,7 @@ def generate_lockfile(contextdir, manifest, output_path, arches):
151157
repos = manifest_data.get('repos', [])
152158
repos += manifest_data.get('lockfile-repos', [])
153159

154-
repoquery_args = ["--queryformat", "%{name} %{location}\n", "--disablerepo=*", "--refresh"]
160+
repoquery_args = ["--queryformat", "%{name} %{location}\n", "--disablerepo=*", "--refresh", "--quiet"]
155161
# Tell dnf to load repos files from $contextdir
156162
repoquery_args.extend([f"--setopt=reposdir={contextdir}"])
157163

@@ -165,64 +171,123 @@ def generate_lockfile(contextdir, manifest, output_path, arches):
165171
print(f"This tool derive the konflux lockfile from rpm-ostree lockfiles. No manifest-lock exist for {arch} in {contextdir}")
166172
sys.exit(1)
167173
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)
174+
arch_args = []
175+
if arch is not get_basearch():
176+
# append noarch as well because otherwise those packages get excluded from results
177+
# We use --forcearch here because otherwise dnf still respect the system basearch
178+
# we have to specify both --arch and --forcearch to get both result for $arch and $noarch
179+
arch_args = ['--forcearch', arch, '--arch', arch, '--arch', 'noarch']
180+
pkg_urls = query_packages_location(locks, repoquery_args + arch_args)
173181
packages.append({'arch': arch, 'packages': pkg_urls})
174182

175183
lockfile = write_hermeto_lockfile(packages, repos)
176184

177-
override_path = os.path.join(contextdir, 'konflux-lockfile-override.yaml')
178-
if os.path.exists(override_path):
185+
try:
186+
with open(output_path, 'w', encoding='utf-8') as f:
187+
yaml.safe_dump(lockfile, f, default_flow_style=False)
188+
except IOError as e:
189+
print(f"\u274c Error: Could not write to output file '{output_path}'. Reason: {e}")
190+
sys.exit(1)
191+
192+
193+
def merge_main(args):
194+
"""
195+
Merges multiple lockfiles into one, optionally applying an override file.
196+
"""
197+
if not args.input:
198+
print("Error: at least one input file is required for merging.", file=sys.stderr)
199+
sys.exit(1)
200+
201+
try:
202+
with open(args.input[0], 'r', encoding='utf-8') as f:
203+
base_lockfile = yaml.safe_load(f)
204+
except (IOError, yaml.YAMLError) as e:
205+
print(f"Error reading base lockfile {args.input[0]}: {e}", file=sys.stderr)
206+
sys.exit(1)
207+
208+
for subsequent_file in args.input[1:]:
179209
try:
180-
with open(override_path, 'r', encoding="utf8") as f:
210+
with open(subsequent_file, 'r', encoding='utf-8') as f:
211+
next_lockfile = yaml.safe_load(f)
212+
base_lockfile = merge_lockfiles(base_lockfile, next_lockfile)
213+
except (IOError, yaml.YAMLError) as e:
214+
print(f"Error reading or merging {subsequent_file}: {e}", file=sys.stderr)
215+
sys.exit(1)
216+
217+
if os.path.exists(args.override):
218+
try:
219+
with open(args.override, 'r', encoding="utf8") as f:
181220
override_data = yaml.safe_load(f)
182-
print(f"Merging override from {override_path}")
183-
lockfile = merge_lockfiles(lockfile, override_data)
221+
print(f"Merging override from {args.override}")
222+
base_lockfile = merge_lockfiles(base_lockfile, override_data, override=True)
184223
except (IOError, yaml.YAMLError) as e:
185-
print(f"\u274c Error: Could not read or parse override file '{override_path}'. Reason: {e}")
224+
print(f"Error reading or parsing override file '{args.override}': {e}", file=sys.stderr)
186225
sys.exit(1)
187226

188227
try:
189-
with open(output_path, 'w', encoding='utf-8') as f:
190-
yaml.safe_dump(lockfile, f, default_flow_style=False)
228+
with open(args.output, 'w', encoding='utf-8') as f:
229+
yaml.safe_dump(base_lockfile, f, default_flow_style=False)
230+
print(f"Successfully merged lockfiles to {args.output}")
191231
except IOError as e:
192-
print(f"\u274c Error: Could not write to output file '{output_path}'. Reason: {e}")
232+
print(f"Error writing to output file '{args.output}': {e}", file=sys.stderr)
193233
sys.exit(1)
194234

195235

196236
if __name__ == "__main__":
197237
parser = argparse.ArgumentParser(
198-
description="Generate hermeto lock files."
238+
description="Generate and merge hermeto lock files."
199239
)
240+
subparsers = parser.add_subparsers(dest='command', required=True)
200241

201-
parser.add_argument(
242+
# GENERATE command
243+
parser_generate = subparsers.add_parser(
244+
'generate',
245+
help='Resolve RPMs and generate a lockfile for one or more architectures.'
246+
)
247+
parser_generate.add_argument(
202248
'manifest',
203249
help='Path to the flattened rpm-ostree manifest (e.g., tmp/manifest.json)'
204250
)
205-
206-
parser.add_argument(
251+
parser_generate.add_argument(
207252
'--context',
208253
default='.',
209254
help="Path to the directory containing repofiles and lockfiles. (default: '.')"
210255
)
211-
212-
parser.add_argument(
256+
parser_generate.add_argument(
213257
'--output',
214258
default='./rpms.lock.yaml',
215259
help="Path for the hermeto lockfile. (default: './rpms.lock.yaml')"
216260
)
217-
218-
parser.add_argument(
261+
parser_generate.add_argument(
219262
'--arch',
220263
action='append',
221264
choices=['x86_64', 'aarch64', 's390x', 'ppc64le', 'all'],
222265
help="The architecture to resolve. Can be specified multiple times. 'all' resolves all architectures."
223266
)
267+
parser_generate.set_defaults(func=generate_main)
224268

225-
args = parser.parse_args()
269+
# MERGE command
270+
parser_merge = subparsers.add_parser(
271+
'merge',
272+
help='Merge multiple architecture-specific lockfiles into a single file.'
273+
)
274+
parser_merge.add_argument(
275+
'--input',
276+
nargs='+',
277+
required=True,
278+
help='One or more input lockfiles to merge.'
279+
)
280+
parser_merge.add_argument(
281+
'--output',
282+
default='./rpms.lock.yaml',
283+
help="Path for the merged lockfile. (default: './rpms.lock.yaml')"
284+
)
285+
parser_merge.add_argument(
286+
'--override',
287+
default='konflux-lockfile-override.yaml',
288+
help="Path to an override file. (default: 'konflux-lockfile-override.yaml')"
289+
)
290+
parser_merge.set_defaults(func=merge_main)
226291

227-
manifest_abs_path = os.path.abspath(args.manifest)
228-
generate_lockfile(args.context, manifest_abs_path, args.output, args.arch)
292+
args = parser.parse_args()
293+
args.func(args)

0 commit comments

Comments
 (0)