#!/usr/bin/env python3

import argparse
import os
import subprocess

parser = argparse.ArgumentParser(description='Produce a dep file from ninja.')
parser.add_argument(
    '--build-dir',
    help='The build directory.',
    required=True)
parser.add_argument(
    '--base-dir',
    help='The directory for which dependencies are rewriten.',
    required=True)
parser.add_argument('--ninja', help='The ninja executable to use.')
parser.add_argument(
    'base_target',
    help="The target from the base's perspective.")
parser.add_argument(
    'targets', nargs='+',
    help='The target for which dependencies are extracted.')
parser.add_argument(
    '--extra-deps', nargs='+',
    help='Extra dependencies.')

args = parser.parse_args()
build_dir = os.path.abspath(args.build_dir)
base_dir = os.path.abspath(args.base_dir)
ninja = args.ninja
base_target = args.base_target
targets = args.targets
extra_deps = args.extra_deps

# Make sure we operate in the right folder.
os.chdir(build_dir)

if ninja is None:
    ninja = subprocess.check_output(['command', '-v', 'ninja'])[:-1]

# Construct the set of all targets
all_targets = set()
doto_targets = set()
for t in subprocess.check_output([ninja, '-t', 'targets', 'all']).splitlines():
    t, r = t.split(b':')
    all_targets.add(t)
    if r[:13] == b' C_COMPILER__' or r[:15] == b' CXX_COMPILER__':
        doto_targets.add(t)


def parse_ninja_query(query):
    deps = dict()
    lines = query.splitlines()

    while len(lines):
        line = lines.pop(0)
        if line[0] == ord(' '):
            continue

        # We have a new target
        target = line.split(b':')[0]
        assert lines.pop(0)[:8] == b'  input:'

        inputs = set()
        while True:
            i = lines.pop(0)
            if i[:4] != b'    ':
                break

            '''
            ninja has 3 types of input:
              1. Explicit dependencies, no prefix;
              2. Implicit dependencies, | prefix.
              3. Order only dependencies, || prefix.

            Order only dependency do not require the target to be rebuilt
            and so we ignore them.
            '''
            i = i[4:]
            if i[0] == ord('|'):
                if i[1] == ord('|'):
                    # We reached the order only dependencies.
                    break
                i = i[2:]

            inputs.add(i)

        deps[target] = inputs

    return deps


def extract_deps(workset):
    # Recursively extract the dependencies of the target.
    deps = dict()
    while len(workset) > 0:
        query = subprocess.check_output([ninja, '-t', 'query'] + list(workset))
        target_deps = parse_ninja_query(query)
        deps.update(target_deps)

        workset = set()
        for d in target_deps.values():
            workset.update(t for t in d if t in all_targets and t not in deps)

    # Extract build time dependencies.
    bt_targets = [t for t in deps if t in doto_targets]
    if len(bt_targets) == 0:
        return deps

    ndeps = subprocess.check_output(
        [ninja, '-t', 'deps'] + bt_targets,
        stderr=subprocess.DEVNULL)

    lines = ndeps.splitlines()
    while len(lines) > 0:
        line = lines.pop(0)
        t, m = line.split(b':')
        if m == b' deps not found':
            continue

        inputs = set()
        while True:
            i = lines.pop(0)
            if i == b'':
                break

            assert i[:4] == b'    '
            inputs.add(i[4:])

        deps[t] = inputs

    return deps


base_dir = base_dir.encode()


def rebase_deps(deps):
    rebased = dict()
    cache = dict()

    def rebase(path):
        if path in cache:
            return cache[path]

        abspath = os.path.abspath(path)
        newpath = path if path == abspath else os.path.relpath(
            abspath, base_dir)
        cache[path] = newpath
        return newpath

    for t, s in deps.items():
        rebased[rebase(t)] = set(rebase(d) for d in s)

    return rebased


deps = extract_deps(set(targets))
deps = rebase_deps(deps)


def dump(deps):
    for t, d in deps.items():
        if len(d) == 0:
            continue

        str = t.decode() + ": \\\n  "
        str += " \\\n  ".join(sorted(map((lambda x: x.decode()), d)))

        print(str)


# Collapse everything under the base target.
basedeps = set() if extra_deps is None else set(d.encode() for d in extra_deps)
for d in deps.values():
    basedeps.update(d)

base_target = base_target.encode()
basedeps.discard(base_target)

dump({base_target: basedeps})

[((7, 9, 7, 78), 'argparse.ArgumentParser', 'argparse.ArgumentParser', (), '', False, 'import argparse\n'), ((28, 12, 28, 43), 'os.path.abspath', 'os.path.abspath', ({(28, 28, 28, 42): 'args.build_dir'}, {}), '(args.build_dir)', False, 'import os\n'), ((29, 11, 29, 41), 'os.path.abspath', 'os.path.abspath', ({(29, 27, 29, 40): 'args.base_dir'}, {}), '(args.base_dir)', False, 'import os\n'), ((36, 0, 36, 19), 'os.chdir', 'os.chdir', ({(36, 9, 36, 18): 'build_dir'}, {}), '(build_dir)', False, 'import os\n'), ((110, 12, 112, 34), 'subprocess.check_output', 'subprocess.check_output', (), '', False, 'import subprocess\n'), ((39, 12, 39, 63), 'subprocess.check_output', 'subprocess.check_output', ({(39, 36, 39, 62): "['command', '-v', 'ninja']"}, {}), "(['command', '-v', 'ninja'])", False, 'import subprocess\n'), ((44, 9, 44, 65), 'subprocess.check_output', 'subprocess.check_output', ({(44, 33, 44, 64): "[ninja, '-t', 'targets', 'all']"}, {}), "([ninja, '-t', 'targets', 'all'])", False, 'import subprocess\n'), ((146, 18, 146, 39), 'os.path.abspath', 'os.path.abspath', ({(146, 34, 146, 38): 'path'}, {}), '(path)', False, 'import os\n'), ((147, 47, 148, 30), 'os.path.relpath', 'os.path.relpath', ({(148, 12, 148, 19): 'abspath', (148, 21, 148, 29): 'base_dir'}, {}), '(abspath, base_dir)', False, 'import os\n')]