blob: 8590e480006ba73753cfda5ac9bc7ba7ed52073c [file] [log] [blame]
#!/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())