| #!/usr/bin/env python3.8 |
| # Copyright 2020 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. |
| '''Reads the contents of a manifest file generated by the build and verifies |
| that there are no collisions among destination paths. |
| ''' |
| |
| import argparse |
| import collections |
| import filecmp |
| import functools |
| import json |
| import sys |
| |
| Entry = collections.namedtuple('Entry', ['source', 'destination', 'label']) |
| |
| # List of destination paths for which conflicting entries are acceptable as long |
| # as the source files are identical. |
| # This is mostly use to soft-transition zbi contents. |
| DUPLICATION_EXCEPTIONS = [] |
| |
| |
| def expand(items): |
| '''Reads metadata produced by GN. |
| Expands and flattens file references found within that metadata. |
| See distribution_manifest.gni for a description of the metadata format. |
| Also returns a list of files opened. |
| ''' |
| entries = [] |
| opened_files = [] |
| for item in items: |
| if 'source' in item: |
| entries.append(item) |
| elif 'file' in item: |
| with open(item['file'], 'r') as data_file: |
| opened_files.append(item['file']) |
| data = json.load(data_file) |
| for entry in data: |
| entry['label'] = item['label'] |
| items.append(entry) |
| return [Entry(**e) for e in entries], opened_files |
| |
| |
| def sources_are_different(entries): |
| '''Returns true if the given entries do not all point to identical source |
| files.''' |
| sources = [entry.source for entry in entries] |
| return any(not filecmp.cmp(sources[0], f) for f in sources[1:]) |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| '--input', help='Path to original manifest', required=True) |
| parser.add_argument( |
| '--output', help='Path to the updated manifest', required=True) |
| parser.add_argument( |
| '--depfile', help='Path to GN style depfile', required=True) |
| args = parser.parse_args() |
| |
| with open(args.input, 'r') as input_file: |
| contents = json.load(input_file) |
| |
| entries, opened_files = expand(contents) |
| entries_by_dest = { |
| d: set(e for e in entries if e.destination == d) for d in set( |
| e.destination for e in entries) |
| } |
| with open(args.depfile, 'w+') as depfile: |
| depfile.write('%s: %s\n' % (args.output, ' '.join(opened_files))) |
| |
| # Filter entries for which is is ok to have duplication if the sources are |
| # the same. |
| for exception in DUPLICATION_EXCEPTIONS: |
| if not exception in entries_by_dest: |
| continue |
| duplicates = list(entries_by_dest[exception]) |
| if sources_are_different(duplicates): |
| # Treat this as a normal conflict. |
| continue |
| # This is an exception! Select the first entry as canon, and remove |
| # the others. |
| canon = duplicates[0] |
| entries_by_dest[exception] = set([canon]) |
| for entry in duplicates[1:]: |
| entries.remove(entry) |
| |
| conflicts = {d: e for d, e in entries_by_dest.items() if len(e) > 1} |
| # Only report a conflict if the source files differ. |
| # TODO(fxbug.dev/45680): remove this additional filtering when dependency trees are |
| # cleaned up and //build/package.gni has gone the way of the dodo. |
| conflicts = { |
| d: e |
| for d, e in conflicts.items() |
| if len(set(entry.source for entry in e)) >= 2 |
| } |
| if conflicts: |
| for destination in conflicts: |
| print('Conflicts for path ' + destination + ':') |
| for conflict in conflicts[destination]: |
| print(' - ' + conflict.source) |
| print(' from ' + conflict.label) |
| print('Error: conflicting distribution entries!') |
| return 1 |
| |
| with open(args.output, 'w') as output_file: |
| json.dump( |
| [e._asdict() for e in sorted(entries)], |
| output_file, |
| indent=2, |
| sort_keys=True, |
| separators=(',', ': ')) |
| |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |