33import argparse
44import os
55import shutil
6+ import json
67import subprocess
78import sys
89import tempfile
910import time
11+ import rpm
1012from multiprocessing import Process
1113
1214from dataclasses import dataclass
@@ -24,11 +26,79 @@ 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+ return rpm .labelCompare (
62+ (to_pkg .epoch , to_pkg .v , to_pkg .r ),
63+ (from_pkg .epoch , from_pkg .v , from_pkg .r )
64+ )
65+
66+ # Return a data sctructure representing a diff entry from the
67+ # rpm-ostree db diff --json output.
68+ # "pkgdiff" : [
69+ # [
70+ # "NetworkManager",
71+ # 2,
72+ # {
73+ # "PreviousPackage" : [
74+ # "NetworkManager",
75+ # "1:1.52.0-5.el9_6",
76+ # "x86_64"
77+ # ],
78+ # "NewPackage" : [
79+ # "NetworkManager",
80+ # "1:1.52.0-7.el9_6",
81+ # "x86_64"
82+ # ]
83+ # }
84+ # ],
85+ @staticmethod
86+ def to_json_diff_entry (difftype : PackageDiffType , from_pkg , to_pkg ):
87+ name = from_pkg .name if from_pkg else to_pkg .name
88+ change = ""
89+ match difftype :
90+ case PackageDiffType .ADD :
91+ change = {"NewPackage" : [to_pkg .name , to_pkg .evr (), to_pkg .a ]}
92+ case PackageDiffType .REMOVE :
93+ change = {"PreviousPackage" : [from_pkg .name , from_pkg .evr (), from_pkg .a ]}
94+ case PackageDiffType .UPGRADE | PackageDiffType .DOWNGRADE :
95+ change = {
96+ "PreviousPackage" : [from_pkg .name , from_pkg .evr (), from_pkg .a ],
97+ "NewPackage" : [to_pkg .name , to_pkg .evr (), to_pkg .a ]
98+ }
99+ case _:
100+ raise
101+ return [name , difftype , change ]
32102
33103
34104class OSTreeImport (IntEnum ):
@@ -167,6 +237,89 @@ def diff_rpms_rpm_ostree(diff_from, diff_to, json):
167237 runcmd (cmd )
168238
169239
240+ def diff_rpms_commitmeta_json (diff_from , diff_to ):
241+ diff_rpms_commitmeta (diff_from , diff_to , json_output = True )
242+
243+
244+ def diff_rpms_commitmeta_no_json (diff_from , diff_to ):
245+ diff_rpms_commitmeta (diff_from , diff_to , json_output = False )
246+
247+
248+ def diff_rpms_commitmeta (diff_from , diff_to , json_output = False ):
249+ """
250+ Diff the commitmeta.json files between two builds.
251+ """
252+ from_meta = diff_from .meta
253+ to_meta = diff_to .meta
254+ from_commitmeta = diff_from .commitmeta
255+ to_commitmeta = diff_to .commitmeta
256+
257+ # Gather info about upgraded, downgraded, added, removed RPMS
258+ from_rpms = {pkg [0 ]: Package (* pkg ) for pkg in from_commitmeta .get ('rpmostree.rpmdb.pkglist' , [])}
259+ to_rpms = {pkg [0 ]: Package (* pkg ) for pkg in to_commitmeta .get ('rpmostree.rpmdb.pkglist' , [])}
260+
261+ added_rpms = set (to_rpms .keys ()) - set (from_rpms .keys ())
262+ removed_rpms = set (from_rpms .keys ()) - set (to_rpms .keys ())
263+ common_rpms = set (from_rpms .keys ()) & set (to_rpms .keys ())
264+
265+ upgraded_rpms = []
266+ downgraded_rpms = []
267+ for name in common_rpms :
268+ from_pkg = from_rpms [name ]
269+ to_pkg = to_rpms [name ]
270+ rc = Package .vercmp (from_pkg , to_pkg )
271+ if rc > 0 :
272+ upgraded_rpms .append ((name , from_pkg , to_pkg ))
273+ elif rc == 0 :
274+ pass # They are equal versions
275+ elif rc < 0 :
276+ downgraded_rpms .append ((name , from_pkg , to_pkg ))
277+
278+ if json_output :
279+ diff = {
280+ "ostree-commit-from" : from_meta .get ('ostree-commit' ),
281+ "ostree-commit-to" : to_meta .get ('ostree-commit' ),
282+ "pkgdiff" : [],
283+ "advisories" : []
284+ }
285+ for name in sorted (added_rpms ):
286+ diff ['pkgdiff' ].append (Package .to_json_diff_entry (PackageDiffType .ADD , from_pkg = None , to_pkg = to_rpms [name ]))
287+ for name in sorted (removed_rpms ):
288+ diff ['pkgdiff' ].append (Package .to_json_diff_entry (PackageDiffType .REMOVE , from_pkg = from_rpms [name ], to_pkg = None ))
289+ for name , from_pkg , to_pkg in sorted (upgraded_rpms ):
290+ diff ['pkgdiff' ].append (Package .to_json_diff_entry (PackageDiffType .UPGRADE , from_pkg = from_pkg , to_pkg = to_pkg ))
291+ for name , from_pkg , to_pkg in sorted (downgraded_rpms ):
292+ diff ['pkgdiff' ].append (Package .to_json_diff_entry (PackageDiffType .DOWNGRADE , from_pkg = from_pkg , to_pkg = to_pkg ))
293+
294+ # Only add advisory info for the JSON output
295+ from_advisories = {adv [0 ]: adv for adv in from_commitmeta .get ('rpmostree.advisories' , [])}
296+ to_advisories = {adv [0 ]: adv for adv in to_commitmeta .get ('rpmostree.advisories' , [])}
297+ added_advisories = set (to_advisories .keys ()) - set (from_advisories .keys ())
298+ for name in added_advisories :
299+ diff ['advisories' ].append (to_advisories [name ])
300+ # Dump the JSON to stdout
301+ print (json .dumps (diff , indent = 2 ))
302+ else :
303+ print (f"ostree diff commit from: { diff_from .id } ({ from_meta .get ('ostree-commit' )} )" )
304+ print (f"ostree diff commit to: { diff_to .id } ({ to_meta .get ('ostree-commit' )} )" )
305+ if upgraded_rpms :
306+ print ("Upgraded:" )
307+ for name , from_pkg , to_pkg in sorted (upgraded_rpms ):
308+ print (f" { name } { from_pkg .evr ()} -> { to_pkg .evr ()} " )
309+ if downgraded_rpms :
310+ print ("Downgraded:" )
311+ for name , from_pkg , to_pkg in sorted (downgraded_rpms ):
312+ print (f" { name } { from_pkg .evr ()} -> { to_pkg .evr ()} " )
313+ if removed_rpms :
314+ print ("Removed:" )
315+ for name in sorted (removed_rpms ):
316+ print (f" { name } -{ from_rpms [name ].evr ()} " )
317+ if added_rpms :
318+ print ("Added:" )
319+ for name in sorted (added_rpms ):
320+ print (f" { name } -{ to_rpms [name ].evr ()} " )
321+
322+
170323def diff_ostree_ls (diff_from , diff_to ):
171324 commit_from = diff_from .meta ['ostree-commit' ]
172325 commit_to = diff_to .meta ['ostree-commit' ]
@@ -476,6 +629,10 @@ DIFFERS = [
476629 needs_ostree = OSTreeImport .FULL , function = diff_rpms_rpm_ostree_no_json ),
477630 Differ ("rpms-json" , "Diff RPMs, output JSON" ,
478631 needs_ostree = OSTreeImport .FULL , function = diff_rpms_rpm_ostree_json ),
632+ Differ ("rpms-commitmeta" , "Diff rpms from commitmeta.json" ,
633+ needs_ostree = OSTreeImport .NO , function = diff_rpms_commitmeta_no_json ),
634+ Differ ("rpms-json-commitmeta" , "Diff RPMS & Advisories from commitmeta.json, output JSON" ,
635+ needs_ostree = OSTreeImport .NO , function = diff_rpms_commitmeta_json ),
479636 Differ ("source-control" , "Diff config and COSA input commits" ,
480637 needs_ostree = OSTreeImport .NO , function = diff_source_control ),
481638 Differ ("ostree-ls" , "Diff OSTree contents using 'ostree diff'" ,
0 commit comments