#!/usr/bin/env python3
# Copyright (c) 2020-2025 The Bitcoin developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.

import subprocess
import os
import argparse

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)

    inputs = set().union(*deps.values())

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

    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

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

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

    return inputs


deps = extract_deps(set(targets))

# Make paths absolute
deps = set(os.path.abspath(d) for d in deps)

# extra_deps and base_target are relative to base_dir rather than build_dir
os.chdir(base_dir)
if extra_deps is not None:
    deps.update(set(os.path.abspath(d).encode() for d in extra_deps))
base_target = os.path.abspath(base_target)

# Collapse everything under the base target.
deps.discard(base_target.encode())
out_string = base_target + ": \\\n  "
out_string += " \\\n  ".join(sorted(map((lambda x: x.decode()), deps)))
print(out_string)
