#!/usr/bin/env python3.8
# Copyright 2021 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.
"""Build packages.archive.tgz."""

import argparse
import filecmp
import json
import os
import subprocess
import sys

from typing import Dict, Set


def add_content_entry(
        content: Dict[str, str], dst: str, src: str, src_dir: str,
        depfile_inputs: Set[str]):
    """Add new entry to |content|, checking for duplicates."""
    # Make the source path relative, because the build still
    # puts absolute one in a small number of package manifests :-/
    src_path = os.path.relpath(os.path.join(src_dir, src))
    depfile_inputs.add(src_path)
    if dst in content:
        cur_src_path = content[dst]
        if cur_src_path != src_path and not filecmp.cmp(cur_src_path, src_path):
            raise ValueError(
                'Duplicate entries for destination %s:\n  %s\n  %s\n' %
                (dst, cur_src_path, src_path))
        return
    content[dst] = src_path


def main():
    parser = argparse.ArgumentParser(description=__doc__)

    parser.add_argument(
        '--files',
        metavar='FILESPEC',
        nargs='*',
        default=[],
        help=
        'Add one or more file to the archive, each FILESPEC should be DST=SRC')

    parser.add_argument(
        '--src-dir',
        default='',
        help='Specify source directory. Default is current one.')

    parser.add_argument(
        '--tuf-repo-name', required=True, help='Specify TUF repository name.')

    parser.add_argument(
        '--tarmaker',
        required=True,
        help='Path to the \'tarmaker\' host tool to use')

    parser.add_argument(
        '--tarmaker-manifest',
        required=True,
        help='Path to output tarmaker manifest file.')

    parser.add_argument(
        '--package-manifests-list',
        required=True,
        help='Path to file listing all package manifest files.')

    parser.add_argument(
        '--tuf-timestamp-json', required=True, help='Path to timestamp.json')

    parser.add_argument(
        '--tuf-targets-json',
        nargs='*',
        default=[],
        help='Path to targets.json file')

    parser.add_argument(
        '--output', required=True, help='Path to output .tar.gz archive.')

    parser.add_argument('--depfile', help='Path to output Ninja depfile.')

    args = parser.parse_args()

    # Map destination path -> source path
    content: Dict[str, str] = {}

    # Map merkle root string -> source path
    merkle_to_source: Dict[str, str] = {}

    # List of input paths for the depfile.
    depfile_inputs: Set[str] = set()

    # Parse the --files argument first.
    for filespec in args.files:
        dst, sep, src = filespec.strip().partition('=')
        if sep != '=':
            raise ValueError('Unexpected file spec: ' + filespec)
        add_content_entry(content, dst, src, args.src_dir, depfile_inputs)

    # Parse --package-manifests-list if provided, parse it to
    # populate the blobs directory in the archive.
    blobs_dst_dir = args.tuf_repo_name + '/repository/blobs'
    with open(args.package_manifests_list) as f:
        package_manifest_paths = f.read().splitlines()

    for package_manifest_path in package_manifest_paths:
        depfile_inputs.add(package_manifest_path)
        with open(package_manifest_path) as f:
            package_manifest = json.load(f)
        for blob_entry in package_manifest['blobs']:
            src = blob_entry['source_path']
            merkle = blob_entry['merkle']
            merkle_to_source[merkle] = src
            dst = os.path.join(blobs_dst_dir, merkle)
            add_content_entry(content, dst, src, args.src_dir, depfile_inputs)

    # Parse the timestamp.json file to extract the version of the
    # snapshot.json file to use, copy it to the archive, then parse it.
    depfile_inputs.add(args.tuf_timestamp_json)
    with open(args.tuf_timestamp_json) as f:
        timestamp_json = json.load(f)

    tuf_repo_dir = os.path.join(args.src_dir, args.tuf_repo_name, 'repository')
    add_content_entry(
        content, os.path.join(tuf_repo_dir, 'timestamp.json'),
        args.tuf_timestamp_json, '', depfile_inputs)

    # Install <version>.snapshot.json as is, then as 'snapshot.json'.
    snapshot_json_version = timestamp_json['signed']['meta']['snapshot.json'][
        'version']
    snapshot_json_file = os.path.join(
        tuf_repo_dir, '%d.snapshot.json' % snapshot_json_version)
    snapshot_json_src_path = os.path.join(args.src_dir, snapshot_json_file)
    add_content_entry(
        content, snapshot_json_file, snapshot_json_src_path, '', depfile_inputs)
    add_content_entry(
        content, os.path.join(tuf_repo_dir, 'snapshot.json'),
        snapshot_json_src_path, '', depfile_inputs)

    # Parse the snapshots.json file to grab the versions of root.json and
    # targets.json. Note that the root.json version is its hash.
    # Install <hash>.root.json as-is, then as root.json in the archive.
    # Install <version>.snapshot.json as-is, then as snapshot.json in the
    # archive.
    #
    # Note that <hash>.root.json is technically not needed, but this makes
    # it easier to verify that the archive matches the content of
    # out/default/amber-files/repository/ with a clean build.
    #
    with open(snapshot_json_src_path) as f:
        snapshot_json = json.load(f)

    root_json_hash = snapshot_json['signed']['meta']['root.json']['hashes'][
        'sha512']
    root_json_file = os.path.join(tuf_repo_dir, root_json_hash + '.root.json')
    root_json_src_path = os.path.join(args.src_dir, root_json_file)
    add_content_entry(
        content, root_json_file, root_json_src_path, '', depfile_inputs)
    add_content_entry(
        content, os.path.join(tuf_repo_dir, 'root.json'), root_json_src_path,
        '', depfile_inputs)

    targets_json_version = snapshot_json['signed']['meta']['targets.json'][
        'version']
    targets_json_file = os.path.join(
        tuf_repo_dir, '%d.targets.json' % targets_json_version)
    targets_json_src_path = os.path.join(args.src_dir, targets_json_file)
    add_content_entry(
        content, targets_json_file, targets_json_src_path, '', depfile_inputs)
    add_content_entry(
        content, os.path.join(tuf_repo_dir, 'targets.json'), targets_json_file,
        '', depfile_inputs)

    # Parse the targets.json file to populate the targets directory
    # in the archive.
    with open(targets_json_src_path) as f:
        targets_json = json.load(f)
    for target, entry in targets_json['signed']['targets'].items():
        hash = entry['hashes']['sha512']
        merkle = entry['custom']['merkle']
        # The target name is <dirname>/<basename> or just <basename>.
        # While the installation file name is
        # ${tuf_repo_name}/repository/targets/<dirname>/<hash>.<basename>
        # or ${tuf_repo_name}/repository/targets/<hash>.<basename> is there
        # is no <dirname> in the target name,
        dirname, basename = os.path.split(target)
        dst = args.tuf_repo_name + '/repository/targets'
        if dirname:
            dst = os.path.join(dst, dirname)
        dst = os.path.join(dst, f'{hash}.{basename}')
        src = merkle_to_source[merkle]
        add_content_entry(content, dst, src, args.src_dir, depfile_inputs)

    # Verify that all content source files are available, or print an error
    # message describing what's missing.
    missing_files = []
    for dst, src in content.items():
        if not os.path.exists(src):
            missing_files.append(src)

    if missing_files:
        print(
            'ERROR: Source files required to build the archive are missing!:\n',
            file=sys.stderr)
        for path in missing_files[:16]:
            print('  ' + path, file=sys.stderr)
        if len(missing_files > 16):
            print('  ...', file=sys.stderr)
        return 1

    # Generate a tarmaker manifest file listing how to construct the archive,
    # then call the tool to build it.
    with open(args.tarmaker_manifest, 'w') as tarmaker_manifest:
        for dst, src in sorted(content.items()):
            tarmaker_manifest.write('%s=%s\n' % (dst, src))

    cmd = [
        args.tarmaker, '-output', args.output, '-manifest',
        args.tarmaker_manifest
    ]
    subprocess.run(cmd)

    # Generate the depfile if needed
    if args.depfile:
        with open(args.depfile, 'w') as f:
            f.write(args.output + ':')
            for input in sorted(depfile_inputs):
                f.write(' ' + input)
            f.write('\n')

    return 0


if __name__ == '__main__':
    sys.exit(main())
