| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2023 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Compare the contents of an IDK to a golden file. |
| |
| This script reads the manifest.json file from an exported IDK directory and |
| compares its contents to one or more existing golden files. If the contents of |
| the IDK don't match the contents of the golden file, this returns an error |
| indicating the set of changes that should be acknowledged by updating the golden |
| file. |
| |
| Used by `sdk_final_manifest_golden` GN rules. |
| """ |
| |
| import argparse |
| import json |
| import os |
| import sys |
| from typing import Sequence, TypedDict |
| |
| HOST_TOOL_SCHEMES: Sequence[str] = [ |
| "host_tool", |
| "ffx_tool", |
| "companion_host_tool", |
| ] |
| |
| IdkPart = TypedDict("IdkPart", {"meta": str, "type": str}) |
| IdkManifest = TypedDict("IdkManifest", {"parts": Sequence[IdkPart]}) |
| |
| |
| def part_to_id(part: IdkPart) -> str: |
| """Get a string ID for a part of the IDK. |
| |
| Args: |
| - part: An entry from the `parts` field of the IDK manifest. |
| |
| Returns: |
| A string ID for that part, that's a bit more human-friendly than the |
| JSON object. |
| """ |
| path = part["meta"] |
| meta_type = part["type"] |
| if os.path.basename(path) == "meta.json": |
| path = os.path.dirname(path) |
| elif path.endswith("-meta.json"): |
| path = path[: -len("-meta.json")] |
| |
| if not part.get("stable", False): |
| path = f"{path} (unstable)" |
| |
| return f"{meta_type}://{path}" |
| |
| |
| def part_is_tool_for_other_cpu(part_id: str, host_cpu: str) -> bool: |
| """Returns True if a given part is a host tool for a different cpu.""" |
| for scheme in HOST_TOOL_SCHEMES: |
| if part_id.startswith(f"{scheme}://") and not part_id.startswith( |
| f"{scheme}://tools/{host_cpu}/" |
| ): |
| return True |
| return False |
| |
| |
| def remove_tools_for_other_cpus(part_ids: set[str], cpu: str) -> set[str]: |
| """Takes a set of IDs and returns a set excluding host tools built |
| for other CPU architectures.""" |
| return {id for id in part_ids if not part_is_tool_for_other_cpu(id, cpu)} |
| |
| |
| def main() -> int: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| "--manifest", help="Path to the IDK manifest", required=True |
| ) |
| parser.add_argument( |
| "--golden", help="Path to the golden file", required=True |
| ) |
| parser.add_argument( |
| "--inherit_golden", |
| help=( |
| "Inherit items from this golden file, if specified, and omit " |
| "any items from this file in the updated golden." |
| ), |
| required=False, |
| ) |
| parser.add_argument( |
| "--only_verify_host_tools_for_cpu", |
| help="If specified, ignore any host tools that aren't built for the named CPU arch.", |
| required=False, |
| ) |
| parser.add_argument( |
| "--updated_golden", |
| help=( |
| "Path where a new version of the golden file will be written. The " |
| "contents of this file will be meaningful iff " |
| "--only_verify_host_tools_for_cpu is not specified" |
| ), |
| required=True, |
| ) |
| parser.add_argument( |
| "--label", |
| help=( |
| "GN label that caused this script to be run. Makes errors easier " |
| "to reproduce." |
| ), |
| ) |
| |
| args = parser.parse_args() |
| |
| with open(args.manifest) as f: |
| manifest: IdkManifest = json.load(f) |
| manifest_ids = {part_to_id(part) for part in manifest["parts"]} |
| |
| with open(args.golden) as f: |
| golden_ids = {line.strip() for line in f} |
| |
| if args.inherit_golden: |
| with open(args.inherit_golden) as f: |
| inherit_golden_ids = {line.strip() for line in f} |
| else: |
| inherit_golden_ids = set() |
| |
| effective_golden_ids = golden_ids | inherit_golden_ids |
| |
| added_ids: set[str] = manifest_ids - effective_golden_ids |
| removed_ids: set[str] = effective_golden_ids - manifest_ids |
| |
| if args.only_verify_host_tools_for_cpu: |
| added_ids = remove_tools_for_other_cpus( |
| added_ids, args.only_verify_host_tools_for_cpu |
| ) |
| removed_ids = remove_tools_for_other_cpus( |
| removed_ids, args.only_verify_host_tools_for_cpu |
| ) |
| |
| # Write the file even if it's not useful as an updated golden, because |
| # GN expects it to be there. |
| with open(args.updated_golden, "w") as f: |
| print("Golden cannot be automatically updated.", file=f) |
| else: |
| with open(args.updated_golden, "w") as f: |
| for id in sorted(manifest_ids - inherit_golden_ids): |
| print(id, file=f) |
| |
| if added_ids: |
| print("Parts added to IDK:", file=sys.stderr) |
| for id in sorted(added_ids): |
| print(" - " + id, file=sys.stderr) |
| if removed_ids: |
| print("Parts removed from IDK:", file=sys.stderr) |
| for id in sorted(removed_ids): |
| print(" - " + id, file=sys.stderr) |
| if removed_ids or added_ids: |
| print("Error: IDK contents have changed!", file=sys.stderr) |
| if args.only_verify_host_tools_for_cpu: |
| print( |
| f"""\ |
| The manifest cannot be automatically updated when not cross compiling host tools. |
| |
| Please update the manifest manually - {args.golden} |
| |
| or run fx set with "--args sdk_cross_compile_host_tools=true" |
| """, |
| file=sys.stderr, |
| ) |
| else: |
| print( |
| f"""\ |
| Please acknowledge this change by running: |
| |
| cp {os.path.abspath(args.updated_golden)} {os.path.abspath(args.golden)} |
| """, |
| file=sys.stderr, |
| ) |
| |
| print( |
| f"""\ |
| Note: If you are seeing this on an automated build failure and are trying to |
| reproduce, ensure that |
| {args.label} |
| is in your GN graph. |
| """, |
| file=sys.stderr, |
| ) |
| return 1 |
| |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |