diff --git a/CHANGELOG.md b/CHANGELOG.md index 18001c6..c96cb74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,39 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ### Added +- Added quiet argument for decreasing verbosity to ERROR only. ### Changed +- The following filtering category arguments have been renamed and converted to +boolean options, all defaulting to `True`: + - `--show-packages` to `--packages`(default) and `--no-packages` + - `--show-config` to `--kernel-config`(default) and `--no-kernel-config` + - `--show-packageconfig` to `--packageconfig`(default) and `--no-packageconfig` + - `--ignore-proprietary` to `--packages-proprietary`(default) and `--no-packages-proprietary` + +- Console output handling has been updated: + - **Human-readable** output is now redirected to **stderr** + - **Structured JSON** output is emitted on **stdout** to facilitate seamless + integration of the tool into automated pipelines and scripting workflows. + +- The JSON output file has been updated: + - The JSON output file is no longer generated by default. + - The `--output ` option have been renamed to `--json-output ` + - The `--json-output ` option must now be explicitly specified to enable JSON file generation. ### Fixed ### Removed +- Refocused the tool on producing complete and reliable spdx diff: + - Removing output filtering options: + - `--show-added` + - `--show-changed` + - `--show-removed` + - The default behaviour is to show the full spdx diff, following arguments are removed: + - `--full` + - `--summary` + ## [1.0.1] - 2026-02-12 ### Added diff --git a/INSTALL.md b/INSTALL.md index b971d7e..d190b91 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -46,14 +46,11 @@ After installation: # Show help spdx-diff --help -# Compare two SPDX files +# Compare two SPDX files, human-readable output on stderr and JSON output on stdout spdx-diff reference.json new.json -# Show summary only -spdx-diff reference.json new.json --summary - -# JSON output for automation -spdx-diff reference.json new.json --format json --output results.json +# With JSON file output generated +spdx-diff reference.json new.json --json-output results.json ``` See `README.md` for full documentation. diff --git a/README.md b/README.md index 6ee7133..b001dbf 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,11 @@ This tool compares two SPDX3 JSON documents and reports differences in: - Kernel configuration parameters (CONFIG_*) - PACKAGECONFIG entries per package -It produces both human-readable output (console) and a structured JSON diff file. +The application separates human-readable and machine-readable outputs to improve automation and pipeline integration. + +- **stderr** is used for human-readable (text) output intended for debugging. +- **stdout** always emits structured **JSON output**, making it suitable for consumption by scripts and CI/CD pipelines. +- When a JSON filename parameter is provided, the JSON result is also written to the specified file. Usage ----- @@ -21,26 +25,13 @@ Required arguments: - `new`: Path to the newer SPDX3 JSON file. Optional arguments: - - `--full`: For console output, always show section names (added, removed, - changed) even if there is no difference. - - `--output `: Save diff results to the given JSON file. - Default: `spdx_diff_.json` - - `--ignore-proprietary`: Ignore packages with LicenseRef-Proprietary. - - `--summary`: Show only summary statistics without detailed differences. - - `--format {text,json,both}`: Control output format: - - `text`: Console output only (no JSON file) - - `json`: JSON file only (silent mode for automation) - - `both`: Both console and JSON output (**default**) - -Output filtering - change type: - - `--show-added`: Show only added items. - - `--show-removed`: Show only removed items. - - `--show-changed`: Show only changed items. - -Output filtering - category: - - `--show-packages`: Show only package differences. - - `--show-config`: Show only kernel config differences. - - `--show-packageconfig`: Show only PACKAGECONFIG differences. + - `--json-output `: Save diff results to the given JSON file. + +Text output filtering - category : + - `--[no-]packages`: show|hide package differences. + - `--[no-]kernel-config`: show|hide kernel config differences. + - `--[no-]packageconfig`: show|hide PACKAGECONFIG differences. + - `--[no-]packages-proprietary`: show|hide packages with LicenseRef-Proprietary. Output ------ @@ -67,27 +58,6 @@ Symbols: - removed ~ changed -Summary Mode ------------- -When using --summary, the tool displays aggregate statistics: - -``` -SPDX-Diff Summary: - -Packages: - Added: 5 - Removed: 2 - Changed: 3 - -Kernel Config: - Added: 10 - Removed: 3 - Changed: 7 - -PACKAGECONFIG: - Features Added: 12 - Features Removed: 4 - Features Changed: 6 ``` JSON Diff File @@ -157,37 +127,22 @@ The script uses Python's logging module: Examples -------- -### Basic comparison with both console and JSON output: +### Basic comparison with both console(stderr) and JSON(stdout) output: ./spdx-diff reference.json new.json ### Full details with proprietary packages excluded: - ./spdx-diff reference.json new.json --ignore-proprietary --full - -### Quick summary check: - ./spdx-diff reference.json new.json --summary + ./spdx-diff reference.json new.json --no-packages-proprietary -### Silent mode for CI/CD (JSON output only): - ./spdx-diff reference.json new.json --format json --output results.json +### Console output for CI/CD: + ./spdx-diff reference.json new.json --quiet -### Console output only (no JSON file): - ./spdx-diff reference.json new.json --format text --full +### Console and JSON output with JSON file generated: + ./spdx-diff reference.json new.json --quiet --json-output result.json -### Show only changed packages: - ./spdx-diff reference.json new.json --show-packages --show-changed +### Exclude on console PACKAGECONFIG differences: + ./spdx-diff reference.json new.json --no-packageconfig -### Show only added packages: - ./spdx-diff reference.json new.json --show-packages --show-added - -### Show only kernel config changes: - ./spdx-diff reference.json new.json --show-config --show-changed - -### Show added and changed items across all categories: - ./spdx-diff reference.json new.json --show-added --show-changed - -### Show only PACKAGECONFIG differences: - ./spdx-diff reference.json new.json --show-packageconfig - -Console output example: +Console output(stderr) example: ``` Packages - Added: + libfoo: 2.0 diff --git a/src/spdx_diff/cli.py b/src/spdx_diff/cli.py index 74860ae..a274b3b 100644 --- a/src/spdx_diff/cli.py +++ b/src/spdx_diff/cli.py @@ -5,9 +5,10 @@ import logging import pathlib import re -from argparse import ArgumentParser, ArgumentTypeError +import sys +from argparse import ArgumentParser, ArgumentTypeError, BooleanOptionalAction from collections import defaultdict -from datetime import datetime, timezone +from contextlib import redirect_stdout from typing import Any from . import __version__ @@ -131,14 +132,14 @@ def normalize_package_name(name: str) -> str: match = re.search(kernel_version_pattern, name) return name[: match.start()] if match else name - def extract_spdx_data(self, ignore_proprietary: bool = False) -> None: + def extract_spdx_data(self, include_packages_proprietary: bool = True) -> None: """ Extract SPDX information (packages, kernel CONFIG, and PACKAGECONFIG). Extract SPDX package data, kernel CONFIG options, and PACKAGECONFIG entries from the SPDX JSON file. Kernel packages are automatically normalized. - :param ignore_proprietary: Whether to skip proprietary packages + :param include_packages_proprietary: Whether to skip proprietary packages """ build_count = 0 @@ -150,7 +151,10 @@ def extract_spdx_data(self, ignore_proprietary: bool = False) -> None: if not pkg_name or not version: continue - if ignore_proprietary and self.is_package_proprietary(item): + if ( + not include_packages_proprietary + and self.is_package_proprietary(item) + ): _logger.info("Ignoring proprietary package: %s", pkg_name) continue @@ -274,11 +278,6 @@ def print_diff( added: dict[str, Any], removed: dict[str, Any], changed: dict[str, Any], - *, - show_all: bool = False, - show_added: bool = True, - show_removed: bool = True, - show_changed: bool = True, ) -> None: """ Print differences between items. @@ -288,23 +287,19 @@ def print_diff( added: Added items removed: Removed items changed: Changed items - show_all: Whether to show even if empty - show_added: Whether to show added items - show_removed: Whether to show removed items - show_changed: Whether to show changed items """ - if show_added and (show_all or added): + if added: print(f"\n{title} - Added:") for k in sorted(added): print(f" + {k}" if isinstance(added, list) else f" + {k}: {added[k]}") - if show_removed and (show_all or removed): + if removed: print(f"\n{title} - Removed:") for k in sorted(removed): print(f" - {k}" if isinstance(removed, list) else f" - {k}: {removed[k]}") - if show_changed and changed and (show_all or changed): + if changed: print(f"\n{title} - Changed:") for k in sorted(changed): print(f" ~ {k}: {changed[k]['from']} -> {changed[k]['to']}") @@ -314,11 +309,6 @@ def print_packageconfig_diff( added: dict[str, dict[str, str]], removed: dict[str, dict[str, str]], changed: dict[str, dict[str, Any]], - *, - show_all: bool = False, - show_added: bool = True, - show_removed: bool = True, - show_changed: bool = True, ) -> None: """ Print PACKAGECONFIG differences. @@ -327,27 +317,23 @@ def print_packageconfig_diff( added: Added packages with their features removed: Removed packages with their features changed: Changed packages with feature differences - show_all: Whether to show even if empty - show_added: Whether to show added items - show_removed: Whether to show removed items - show_changed: Whether to show changed items """ - if show_added and (show_all or added): + if added: print("\nPACKAGECONFIG - Added Packages:") for pkg in sorted(added): print(f" + {pkg}:") for feature, value in sorted(added[pkg].items()): print(f" {feature}: {value}") - if show_removed and (show_all or removed): + if removed: print("\nPACKAGECONFIG - Removed Packages:") for pkg in sorted(removed): print(f" - {pkg}:") for feature, value in sorted(removed[pkg].items()): print(f" {feature}: {value}") - if show_changed and (show_all or changed): + if changed: print("\nPACKAGECONFIG - Changed Packages:") for pkg in sorted(changed): print(f" ~ {pkg}:") @@ -363,56 +349,6 @@ def print_packageconfig_diff( print(f" ~ {feature}: {change['from']} -> {change['to']}") -def print_summary( - pkg_diff: tuple[dict[str, Any], dict[str, Any], dict[str, Any]], - cfg_diff: tuple[dict[str, Any], dict[str, Any], dict[str, Any]], - pcfg_diff: tuple[ - dict[str, dict[str, str]], dict[str, dict[str, str]], dict[str, dict[str, Any]] - ], -) -> None: - """ - Print summary statistics of differences. - - Args: - pkg_diff: Package differences - cfg_diff: Kernel config differences - pcfg_diff: PACKAGECONFIG differences - - """ - print("\nSPDX-Diff Summary:\n") - - print("Packages:") - print(f" Added: {len(pkg_diff[0])}") - print(f" Removed: {len(pkg_diff[1])}") - print(f" Changed: {len(pkg_diff[2])}") - - print("\nKernel Config:") - print(f" Added: {len(cfg_diff[0])}") - print(f" Removed: {len(cfg_diff[1])}") - print(f" Changed: {len(cfg_diff[2])}") - - print("\nPACKAGECONFIG:") - print(f" Packages Added: {len(pcfg_diff[0])}") - print(f" Packages Removed: {len(pcfg_diff[1])}") - print(f" Packages Changed: {len(pcfg_diff[2])}") - - # Count total feature changes - total_features_added = sum(len(v.get("added", {})) for v in pcfg_diff[2].values()) - total_features_removed = sum( - len(v.get("removed", {})) for v in pcfg_diff[2].values() - ) - total_features_changed = sum( - len(v.get("changed", {})) for v in pcfg_diff[2].values() - ) - - if total_features_added or total_features_removed or total_features_changed: - print(f" Features Added: {total_features_added}") - print(f" Features Removed: {total_features_removed}") - print(f" Features Changed: {total_features_changed}") - - print() - - def write_diff_to_json( pkg_diff: tuple[dict[str, Any], dict[str, Any], dict[str, Any]], cfg_diff: tuple[dict[str, Any], dict[str, Any], dict[str, Any]], @@ -449,8 +385,15 @@ def write_diff_to_json( "changed": dict(sorted(pcfg_diff[2].items())), }, } - with output_file.open("w", encoding="utf-8") as f: - json.dump(delta, f, indent=2, ensure_ascii=False) + # Write the resulting SPDX diff JSON to stdout for piping + json.dump(delta, sys.stdout, indent=2, ensure_ascii=False) + sys.stdout.write("\n") + + # Write the resulting SPDX diff JSON to file + if output_file is not None: + _logger.info("Writing diff results to %s", output_file) + with output_file.open("w", encoding="utf-8") as f: + json.dump(delta, f, indent=2, ensure_ascii=False) def path_is_file(value: str) -> pathlib.Path: @@ -477,6 +420,12 @@ def main() -> None: default=0, help="Increase verbosity (-v for INFO, -vv for DEBUG)", ) + parser.add_argument( + "-q", + "--quiet", + action="store_true", + help="", + ) parser.add_argument( "reference", type=path_is_file, @@ -488,73 +437,47 @@ def main() -> None: help="New SPDX3 JSON file", ) parser.add_argument( - "--full", - action="store_true", - help="For console output, always show section names (added, removed, changed)", - ) - timestamp = datetime.now(tz=timezone.utc).astimezone().strftime("%Y%m%d-%H%M%S") - default_output = f"spdx_diff_{timestamp}.json" - parser.add_argument( - "--output", + "--json-output", "-o", metavar="PATH", type=pathlib.Path, - default=default_output, - help="Optional output file name (JSON)", - ) - parser.add_argument( - "--ignore-proprietary", - action="store_true", - help="Ignore packages with LicenseRef-Proprietary", - ) - parser.add_argument( - "--summary", - action="store_true", - help="Show only summary statistics without detailed differences", - ) - parser.add_argument( - "--format", - choices=["text", "json", "both"], - default="both", - help="Output format: text (console only), json (file only), or both (default)", + default=None, + help="JSON Output file name (default: none)", ) - # Output filtering options - parser.add_argument( - "--show-added", - action="store_true", - help="Show only added items", - ) - parser.add_argument( - "--show-removed", - action="store_true", - help="Show only removed items", - ) - parser.add_argument( - "--show-changed", - action="store_true", - help="Show only changed items", + # Output filtering category options + text_output_group = parser.add_argument_group("for text output") + text_output_group.add_argument( + "--kernel-config", + action=BooleanOptionalAction, + default=True, + help="show|hide kernel config differences (default: yes)", ) - parser.add_argument( - "--show-packages", - action="store_true", - help="Show only package differences", + text_output_group.add_argument( + "--packageconfig", + action=BooleanOptionalAction, + default=True, + help="show|hide PACKAGECONFIG differences (default: yes)", ) - parser.add_argument( - "--show-config", - action="store_true", - help="Show only kernel config differences", + text_output_group.add_argument( + "--packages", + action=BooleanOptionalAction, + default=True, + help="show|hide package differences (default: yes)", ) - parser.add_argument( - "--show-packageconfig", - action="store_true", - help="Show only PACKAGECONFIG differences", + text_output_group.add_argument( + "--packages-proprietary", + action=BooleanOptionalAction, + default=True, + help="show|hide packages with LicenseRef-Proprietary (default: yes)", ) args = parser.parse_args() log_level = logging.WARNING - if args.verbose >= 2: + if args.quiet: + log_level = logging.ERROR + elif args.verbose >= 2: log_level = logging.DEBUG elif args.verbose == 1: log_level = logging.INFO @@ -563,24 +486,17 @@ def main() -> None: # Determine what to show based on flags # If no specific show flags are set, show everything - show_all_change = not (args.show_added or args.show_removed or args.show_changed) - show_added = args.show_added or show_all_change - show_removed = args.show_removed or show_all_change - show_changed = args.show_changed or show_all_change - show_all_category = not ( - args.show_packages or args.show_config or args.show_packageconfig - ) - show_packages = args.show_packages or show_all_category - show_config = args.show_config or show_all_category - show_packageconfig = args.show_packageconfig or show_all_category + show_packages = args.packages + show_kernel_config = args.kernel_config + show_packageconfig = args.packageconfig try: sbom_ref = Spdx3Sbom(args.reference) - sbom_ref.extract_spdx_data(args.ignore_proprietary) + sbom_ref.extract_spdx_data(args.packages_proprietary) sbom_new = Spdx3Sbom(args.new) - sbom_new.extract_spdx_data(args.ignore_proprietary) + sbom_new.extract_spdx_data(args.packages_proprietary) except (ValueError, TypeError) as e: parser.error(str(e)) @@ -593,39 +509,25 @@ def main() -> None: pcfg_diff[2], ) - # Print summary or full output - if args.summary: - print_summary(pkg_diff, cfg_diff, pcfg_light_diff) - elif args.format in {"text", "both"}: + # Print human readable information on stderr + with redirect_stdout(sys.stderr): if show_packages: print_diff( "Packages", *pkg_diff, - show_all=args.full, - show_added=show_added, - show_removed=show_removed, - show_changed=show_changed, ) - if show_config: + if show_kernel_config: print_diff( "Kernel Config", *cfg_diff, - show_all=args.full, - show_added=show_added, - show_removed=show_removed, - show_changed=show_changed, ) if show_packageconfig: print_packageconfig_diff( *pcfg_light_diff, - show_all=args.full, - show_added=show_added, - show_removed=show_removed, - show_changed=show_changed, ) - if args.format in ["json", "both"]: - write_diff_to_json(pkg_diff, cfg_diff, pcfg_light_diff, args.output) + + write_diff_to_json(pkg_diff, cfg_diff, pcfg_light_diff, args.json_output) if __name__ == "__main__": diff --git a/tests/helper.py b/tests/helper.py index f71f72c..68834ec 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -102,9 +102,7 @@ def _run_spdx_diff_check( [ os.fspath(sbom_data_path.joinpath(sbom_ref).resolve(strict=True)), os.fspath(sbom_data_path.joinpath(sbom_new).resolve(strict=True)), - "--format", - "json", - "--output", + "--json-output", os.fspath(out_path), *extra_args, ], @@ -145,5 +143,5 @@ def run_spdx_diff_check( sbom_ref, sbom_new, exp_diff, - ["--ignore-proprietary", *extra_args], + ["--no-packages-proprietary", *extra_args], ) diff --git a/tests/test_package.py b/tests/test_package.py index 36ac913..5455838 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -61,7 +61,7 @@ def test_new_pkg_ign_proprietary( "reference-sbom.spdx.json", sbom_new_name, exp, - ["--ignore-proprietary"], + ["--no-packages-proprietary"], )