Skip to content

Commit 3e7b1d3

Browse files
committed
cmd-diff: Add new ability to generate rpm diffs from commitmeta.json
This adds two new arguments for generating rpm-ostree db diff compatible output by just leveraging the information in the commitmeta.json files in each build's directory. This is a much more lightweight way to get diffs rather than having to download and import the entire ociarchive, which we are now having to do in the build-with-buildah world where the rpm information isn't available to us in the OSTree commit object like it was in the past. This was assisted by Google/Gemini and got me 75% of the way there. The prompt provided: ``` Look at the files old-commitmeta.json and new-commitmeta.json and generate a two functions in src/cmd-diff that will produce similar diff output. The first function will produce human readable diff output and should be in the form as shown in the human-diff.txt file. The second function should produce JSON output and be in the format as shown in the json-diff.txt file. The commitmeta.json files will exist in the build directory for a build (so when diffing two builds you'll need to look at the commitmeta.json for the first build and then the commitmeta.json for the second build and then produce the diff output based on the differences in those two files. ``` Assisted-By: <google/gemini-2.5-pro>
1 parent 228e18f commit 3e7b1d3

File tree

1 file changed

+157
-1
lines changed

1 file changed

+157
-1
lines changed

src/cmd-diff

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import argparse
44
import os
55
import shutil
6+
import json
67
import subprocess
78
import sys
89
import tempfile
910
import time
11+
import rpm
1012
from multiprocessing import Process
1113

1214
from dataclasses import dataclass
@@ -24,11 +26,78 @@ class DiffBuildTarget:
2426
id: str
2527
dir: str
2628
meta: dict
29+
commitmeta: dict
2730

2831
@staticmethod
2932
def from_build(builds, build, arch):
3033
return DiffBuildTarget(build, builds.get_build_dir(build, arch),
31-
builds.get_build_meta(build, arch))
34+
builds.get_build_meta(build, arch),
35+
builds.get_build_commitmeta(build, arch))
36+
37+
38+
class PackageDiffType(IntEnum):
39+
ADD = 0
40+
REMOVE = 1
41+
UPGRADE = 2
42+
DOWNGRADE = 3
43+
44+
45+
@dataclass
46+
class Package:
47+
name: str
48+
epoch: int
49+
v: str # Version
50+
r: str # Release
51+
a: str # Architecture
52+
53+
def evr(self):
54+
epoch_string = "" if self.epoch == "0" else f"{self.epoch}:"
55+
return f"{epoch_string}{self.v}-{self.r}"
56+
57+
# Compare versions
58+
# rc > 0 -> Newer; rc == 0 -> Same; rc < 0 -> Older
59+
@staticmethod
60+
def vercmp(from_pkg, to_pkg):
61+
# pylint: disable=E1101
62+
return rpm.labelCompare((to_pkg.epoch, to_pkg.v, to_pkg.r),
63+
(from_pkg.epoch, from_pkg.v, from_pkg.r))
64+
65+
# Return a data sctructure representing a diff entry from the
66+
# rpm-ostree db diff --json output.
67+
# "pkgdiff" : [
68+
# [
69+
# "NetworkManager",
70+
# 2,
71+
# {
72+
# "PreviousPackage" : [
73+
# "NetworkManager",
74+
# "1:1.52.0-5.el9_6",
75+
# "x86_64"
76+
# ],
77+
# "NewPackage" : [
78+
# "NetworkManager",
79+
# "1:1.52.0-7.el9_6",
80+
# "x86_64"
81+
# ]
82+
# }
83+
# ],
84+
@staticmethod
85+
def to_json_diff_entry(difftype: PackageDiffType, from_pkg, to_pkg):
86+
name = from_pkg.name if from_pkg else to_pkg.name
87+
change = ""
88+
match difftype:
89+
case PackageDiffType.ADD:
90+
change = {"NewPackage": [to_pkg.name, to_pkg.evr(), to_pkg.a]}
91+
case PackageDiffType.REMOVE:
92+
change = {"PreviousPackage": [from_pkg.name, from_pkg.evr(), from_pkg.a]}
93+
case PackageDiffType.UPGRADE | PackageDiffType.DOWNGRADE:
94+
change = {
95+
"PreviousPackage": [from_pkg.name, from_pkg.evr(), from_pkg.a],
96+
"NewPackage": [to_pkg.name, to_pkg.evr(), to_pkg.a]
97+
}
98+
case _:
99+
raise Exception(f"Invalid PackageDiffType: difftype")
100+
return [name, difftype, change]
32101

33102

34103
class OSTreeImport(IntEnum):
@@ -167,6 +236,89 @@ def diff_rpms_rpm_ostree(diff_from, diff_to, json):
167236
runcmd(cmd)
168237

169238

239+
def diff_rpms_commitmeta_json(diff_from, diff_to):
240+
diff_rpms_commitmeta(diff_from, diff_to, json_output=True)
241+
242+
243+
def diff_rpms_commitmeta_no_json(diff_from, diff_to):
244+
diff_rpms_commitmeta(diff_from, diff_to, json_output=False)
245+
246+
247+
def diff_rpms_commitmeta(diff_from, diff_to, json_output=False):
248+
"""
249+
Diff the commitmeta.json files between two builds.
250+
"""
251+
from_meta = diff_from.meta
252+
to_meta = diff_to.meta
253+
from_commitmeta = diff_from.commitmeta
254+
to_commitmeta = diff_to.commitmeta
255+
256+
# Gather info about upgraded, downgraded, added, removed RPMS
257+
from_rpms = {pkg[0]: Package(*pkg) for pkg in from_commitmeta.get('rpmostree.rpmdb.pkglist', [])}
258+
to_rpms = {pkg[0]: Package(*pkg) for pkg in to_commitmeta.get('rpmostree.rpmdb.pkglist', [])}
259+
260+
added_rpms = set(to_rpms.keys()) - set(from_rpms.keys())
261+
removed_rpms = set(from_rpms.keys()) - set(to_rpms.keys())
262+
common_rpms = set(from_rpms.keys()) & set(to_rpms.keys())
263+
264+
upgraded_rpms = []
265+
downgraded_rpms = []
266+
for name in common_rpms:
267+
from_pkg = from_rpms[name]
268+
to_pkg = to_rpms[name]
269+
rc = Package.vercmp(from_pkg, to_pkg)
270+
if rc > 0:
271+
upgraded_rpms.append((name, from_pkg, to_pkg))
272+
elif rc == 0:
273+
pass # They are equal versions
274+
elif rc < 0:
275+
downgraded_rpms.append((name, from_pkg, to_pkg))
276+
277+
if json_output:
278+
diff = {
279+
"ostree-commit-from": from_meta.get('ostree-commit'),
280+
"ostree-commit-to": to_meta.get('ostree-commit'),
281+
"pkgdiff": [],
282+
"advisories": []
283+
}
284+
for name in sorted(added_rpms):
285+
diff['pkgdiff'].append(Package.to_json_diff_entry(PackageDiffType.ADD, from_pkg=None, to_pkg=to_rpms[name]))
286+
for name in sorted(removed_rpms):
287+
diff['pkgdiff'].append(Package.to_json_diff_entry(PackageDiffType.REMOVE, from_pkg=from_rpms[name], to_pkg=None))
288+
for name, from_pkg, to_pkg in sorted(upgraded_rpms):
289+
diff['pkgdiff'].append(Package.to_json_diff_entry(PackageDiffType.UPGRADE, from_pkg=from_pkg, to_pkg=to_pkg))
290+
for name, from_pkg, to_pkg in sorted(downgraded_rpms):
291+
diff['pkgdiff'].append(Package.to_json_diff_entry(PackageDiffType.DOWNGRADE, from_pkg=from_pkg, to_pkg=to_pkg))
292+
293+
# Only add advisory info for the JSON output
294+
from_advisories = {adv[0]: adv for adv in from_commitmeta.get('rpmostree.advisories', [])}
295+
to_advisories = {adv[0]: adv for adv in to_commitmeta.get('rpmostree.advisories', [])}
296+
added_advisories = set(to_advisories.keys()) - set(from_advisories.keys())
297+
for name in added_advisories:
298+
diff['advisories'].append(to_advisories[name])
299+
# Dump the JSON to stdout
300+
print(json.dumps(diff, indent=2))
301+
else:
302+
print(f"ostree diff commit from: {diff_from.id} ({from_meta.get('ostree-commit')})")
303+
print(f"ostree diff commit to: {diff_to.id} ({to_meta.get('ostree-commit')})")
304+
if upgraded_rpms:
305+
print("Upgraded:")
306+
for name, from_pkg, to_pkg in sorted(upgraded_rpms):
307+
print(f" {name} {from_pkg.evr()} -> {to_pkg.evr()}")
308+
if downgraded_rpms:
309+
print("Downgraded:")
310+
for name, from_pkg, to_pkg in sorted(downgraded_rpms):
311+
print(f" {name} {from_pkg.evr()} -> {to_pkg.evr()}")
312+
if removed_rpms:
313+
print("Removed:")
314+
for name in sorted(removed_rpms):
315+
print(f" {name}-{from_rpms[name].evr()}")
316+
if added_rpms:
317+
print("Added:")
318+
for name in sorted(added_rpms):
319+
print(f" {name}-{to_rpms[name].evr()}")
320+
321+
170322
def diff_ostree_ls(diff_from, diff_to):
171323
commit_from = diff_from.meta['ostree-commit']
172324
commit_to = diff_to.meta['ostree-commit']
@@ -476,6 +628,10 @@ DIFFERS = [
476628
needs_ostree=OSTreeImport.FULL, function=diff_rpms_rpm_ostree_no_json),
477629
Differ("rpms-json", "Diff RPMs, output JSON",
478630
needs_ostree=OSTreeImport.FULL, function=diff_rpms_rpm_ostree_json),
631+
Differ("rpms-commitmeta", "Diff rpms from commitmeta.json",
632+
needs_ostree=OSTreeImport.NO, function=diff_rpms_commitmeta_no_json),
633+
Differ("rpms-json-commitmeta", "Diff RPMS & Advisories from commitmeta.json, output JSON",
634+
needs_ostree=OSTreeImport.NO, function=diff_rpms_commitmeta_json),
479635
Differ("source-control", "Diff config and COSA input commits",
480636
needs_ostree=OSTreeImport.NO, function=diff_source_control),
481637
Differ("ostree-ls", "Diff OSTree contents using 'ostree diff'",

0 commit comments

Comments
 (0)