<?php

function cli_error(string $message): void
{
    $stderr = fopen('php://stderr', 'w');
    if ($stderr !== false) {
        fwrite($stderr, $message);
        fclose($stderr);
        return;
    }
    echo $message;
}

function usage(string $argv0): void
{
    cli_error("Usage: {$argv0} <input.php> [support.php ...] [--target-name <name>] [--target-kind executable|library] [--phpef-runtime-embed true|false] [--phpef-runtime-natives list|*] [--external-source <php-file>] [--external-stub <stub.php>] [--extern-function <vala-name>] [--auto-init-function <vala-name>] [--stacktrace-section] [--no-debug-section] [--no-roundtrip] [--no-valair-optimizer-typing-zval2static] [--no-valair-optimizer-library-embed2native] [--no-valair-optimizer-fastpath-runtime] [--no-valair-optimizer-precompute]\n");
}

function pipeline_output_base(string $inputFile, string $targetName): string
{
    if ($targetName !== '') {
        return $targetName;
    }
    return source_base($inputFile) ?: 'phpef-output';
}

function source_base(string $source): string
{
    $base = basename($source);
    if (str_ends_with($base, '.php')) {
        $base = substr($base, 0, -4);
    }
    $base = preg_replace('/[^A-Za-z0-9_]+/', '_', $base);
    $base = trim((string) $base, '_');
    return $base !== '' ? $base : 'source';
}

function source_symbol_prefix(string $source): string
{
    $normalized = str_replace('\\', '/', $source);
    $normalized = ltrim($normalized, './');
    $prefix = preg_replace('/[^A-Za-z0-9_]+/', '_', $normalized);
    $prefix = trim((string) $prefix, '_');
    if ($prefix === '') {
        $prefix = 'source';
    }
    if (preg_match('/^[A-Za-z_]/', $prefix) !== 1) {
        $prefix = '_' . $prefix;
    }
    return $prefix;
}

function pipeline_library_init_function(string $targetName): string
{
    return 'phpef_library_' . source_symbol_prefix($targetName) . '_init';
}

if ($argc < 2) {
    usage($argv[0]);
    exit(2);
}

$inputFiles = [];
$externalFiles = [];
$externalStubFiles = [];
$targetName = '';
$targetKind = 'executable';
$debugSection = true;
$stacktraceSection = false;
$roundtrip = true;
$valairOptimizerTypingZval2Static = true;
$valairOptimizerLibraryEmbed2Native = true;
$valairOptimizerFastPathRuntime = true;
$valairOptimizerPrecompute = true;
$phpefRuntimeEmbed = true;
$phpefRuntimeNatives = '*';
$externFunctions = [];
$autoInitFunctions = [];

for ($i = 1; $i < $argc; $i++) {
    $arg = $argv[$i];
    if ($arg === '--target-name') {
        $i++;
        if ($i >= $argc || $argv[$i] === '') {
            cli_error("Missing value for --target-name\n");
            exit(2);
        }
        $targetName = $argv[$i];
        continue;
    }
    if ($arg === '--target-kind') {
        $i++;
        if ($i >= $argc || $argv[$i] === '') {
            cli_error("Missing value for --target-kind\n");
            exit(2);
        }
        $targetKind = $argv[$i];
        continue;
    }
    if ($arg === '--external-source') {
        $i++;
        if ($i >= $argc || $argv[$i] === '') {
            cli_error("Missing value for --external-source\n");
            exit(2);
        }
        $externalFiles[] = $argv[$i];
        continue;
    }
    if ($arg === '--external-stub') {
        $i++;
        if ($i >= $argc || $argv[$i] === '') {
            cli_error("Missing value for --external-stub\n");
            exit(2);
        }
        $externalStubFiles[] = $argv[$i];
        continue;
    }
    if ($arg === '--phpef-runtime-embed') {
        $i++;
        if ($i >= $argc || $argv[$i] === '') {
            cli_error("Missing value for --phpef-runtime-embed\n");
            exit(2);
        }
        $value = strtolower($argv[$i]);
        if (in_array($value, ['1', 'true', 'yes', 'on'], true)) {
            $phpefRuntimeEmbed = true;
        } elseif (in_array($value, ['0', 'false', 'no', 'off'], true)) {
            $phpefRuntimeEmbed = false;
        } else {
            cli_error("Invalid boolean for --phpef-runtime-embed: {$argv[$i]}\n");
            exit(2);
        }
        continue;
    }
    if ($arg === '--phpef-runtime-natives') {
        $i++;
        if ($i >= $argc) {
            cli_error("Missing value for --phpef-runtime-natives\n");
            exit(2);
        }
        $phpefRuntimeNatives = trim($argv[$i]);
        continue;
    }
    if ($arg === '--extern-function') {
        $i++;
        if ($i >= $argc || $argv[$i] === '') {
            cli_error("Missing value for --extern-function\n");
            exit(2);
        }
        $externFunctions[] = $argv[$i];
        continue;
    }
    if ($arg === '--auto-init-function') {
        $i++;
        if ($i >= $argc || $argv[$i] === '') {
            cli_error("Missing value for --auto-init-function\n");
            exit(2);
        }
        $autoInitFunctions[] = $argv[$i];
        continue;
    }
    if ($arg === '--stacktrace-section') {
        $stacktraceSection = true;
        continue;
    }
    if ($arg === '--no-debug-section') {
        $debugSection = false;
        continue;
    }
    if ($arg === '--no-roundtrip') {
        $roundtrip = false;
        continue;
    }
    if ($arg === '--no-valair-optimizer-typing-zval2static') {
        $valairOptimizerTypingZval2Static = false;
        continue;
    }
    if ($arg === '--no-valair-optimizer-library-embed2native') {
        $valairOptimizerLibraryEmbed2Native = false;
        continue;
    }
    if ($arg === '--no-valair-optimizer-fastpath-runtime') {
        $valairOptimizerFastPathRuntime = false;
        continue;
    }
    if ($arg === '--no-valair-optimizer-precompute') {
        $valairOptimizerPrecompute = false;
        continue;
    }
    if (str_starts_with($arg, '--')) {
        cli_error("Unknown option: {$arg}\n");
        exit(2);
    }
    $inputFiles[] = $arg;
}

if ($inputFiles === []) {
    usage($argv[0]);
    exit(2);
}
if (!in_array($targetKind, ['executable', 'library'], true)) {
    cli_error("Unsupported --target-kind {$targetKind}; supported kinds are executable and library\n");
    exit(2);
}

$runtimeDependencies = ['phpef-runtime-core'];
$runtimePackages = ['phpef-runtime-core'];
if ($phpefRuntimeEmbed) {
    $runtimeDependencies[] = 'phpef-runtime-embed';
    $runtimePackages[] = 'phpef-runtime-embed';
}
$runtimePackages[] = '@NATIVE_RUNTIME_PACKAGES@';
$allowedNativeRuntimeList = array_values(array_filter(array_map('trim', explode(',', $phpefRuntimeNatives)), static fn($name) => $name !== ''));
$allowedNativeRuntimePackages = [];
if ($phpefRuntimeNatives === '*') {
    $allowedNativeRuntimePackages[] = '*';
} else {
    foreach ($allowedNativeRuntimeList as $name) {
        $allowedNativeRuntimePackages[] = str_starts_with($name, 'phpef-runtime-native-') ? $name : 'phpef-runtime-native-' . $name;
    }
}

$entryInput = $inputFiles[0];
$entryBase = pipeline_output_base($entryInput, $targetName);
$targetName = $targetName !== '' ? $targetName : $entryBase;
$isMultiSource = count($inputFiles) > 1;

$sources = [];
$usedBases = [];
foreach ($inputFiles as $index => $source) {
    $base = $index === 0 ? $entryBase : source_base($source);
    $originalBase = $base;
    $suffix = 2;
    while (isset($usedBases[$base])) {
        $base = $originalBase . '_' . $suffix;
        $suffix++;
    }
    $usedBases[$base] = true;
    $symbolPrefix = source_symbol_prefix($source);
    $sources[] = [
        'index' => $index,
        'source' => $source,
        'base' => $base,
        'symbolPrefix' => $symbolPrefix,
        'activation' => 'phpef_file_' . $symbolPrefix,
        'phpast' => $base . '.phpast',
        'phpir' => $base . '.phpir',
        'valair' => $base . '.valair.1',
        'valairAfterTypingZval2Static' => $base . '.valair.2.afteropt.typing-zval2static',
        'valairAfterFastPathRuntime' => $base . '.valair.3.afteropt.fastpath-runtime',
        'valairAfterLibraryEmbed2Native' => $base . '.valair.4.afteropt.library-embed2native',
        'valairAfterTypingZval2StaticFinal' => $base . '.valair.5.afteropt.typing-zval2static-final',
        'valairAfterPrecompute' => $base . '.valair.6.afteropt.precompute',
        'vala' => $base . '.vala',
        'c' => $base . '.c',
        'mappedC' => $base . '.mapped.c',
        'debugJson' => $base . '.phpef-debug.json',
        'stacktraceMap' => $base . '.phpef-stacktrace.map',
        'roundtripPhp' => $base . '.orig.php',
    ];
}

$externalSources = [];
foreach (array_values(array_unique(array_merge($externalFiles, $externalStubFiles))) as $externalIndex => $source) {
    $base = 'external_' . source_base($source);
    $originalBase = $base;
    $suffix = 2;
    while (isset($usedBases[$base])) {
        $base = $originalBase . '_' . $suffix;
        $suffix++;
    }
    $usedBases[$base] = true;
    $symbolPrefix = source_symbol_prefix($source);
    $externalSources[] = [
        'index' => count($sources) + $externalIndex,
        'external' => true,
        'externalStub' => in_array($source, $externalStubFiles, true),
        'source' => $source,
        'base' => $base,
        'symbolPrefix' => $symbolPrefix,
        'activation' => 'phpef_file_' . $symbolPrefix,
        'phpast' => $base . '.phpast',
        'phpir' => $base . '.phpir',
        'valair' => $base . '.valair.1',
        'valairAfterTypingZval2Static' => $base . '.valair.2.afteropt.typing-zval2static',
        'valairAfterFastPathRuntime' => $base . '.valair.3.afteropt.fastpath-runtime',
        'valairAfterLibraryEmbed2Native' => $base . '.valair.4.afteropt.library-embed2native',
        'valairAfterTypingZval2StaticFinal' => $base . '.valair.5.afteropt.typing-zval2static-final',
        'valairAfterPrecompute' => $base . '.valair.6.afteropt.precompute',
        'vala' => $base . '.vala',
        'c' => $base . '.c',
        'mappedC' => $base . '.mapped.c',
        'debugJson' => $base . '.phpef-debug.json',
        'stacktraceMap' => $base . '.phpef-stacktrace.map',
        'roundtripPhp' => $base . '.orig.php',
    ];
}
$allSources = array_merge($sources, $externalSources);

$steps = [];
foreach ($allSources as $source) {
    $steps[] = [
        'id' => $source['base'] . '.php2ast',
        'role' => 'php_to_ast',
        'kind' => 'custom_target',
        'tool' => 'phpefc-php2ast',
        'argv' => ['@INPUT@', '@OUTPUT@'],
        'inputs' => [$source['source']],
        'outputs' => [$source['phpast']],
    ];
}

foreach ($allSources as $source) {
    $inputs = [$source['phpast']];
    $inputSources = [$source['source']];
    foreach ($allSources as $other) {
        if ($other['index'] !== $source['index']) {
            $inputs[] = $other['phpast'];
            $inputSources[] = $other['source'];
        }
    }
    $argv = ['@INPUT0@', '@OUTPUT@'];
    for ($i = 1; $i < count($inputs); $i++) {
        $inputSource = (string) ($inputSources[$i] ?? '');
        $argv[] = in_array($inputSource, $externalStubFiles, true) ? '--external-stub' : '--external-phpast';
        $argv[] = '@INPUT' . $i . '@';
    }
    $steps[] = [
        'id' => $source['base'] . '.phpast2phpir',
        'role' => 'ast_to_phpir',
        'kind' => 'custom_target',
        'tool' => 'phpefc-phpast2phpir',
        'argv' => $argv,
        'inputs' => $inputs,
        'outputs' => [$source['phpir']],
    ];
}

foreach ($allSources as $source) {
    $inputs = [$source['phpir']];
    foreach ($allSources as $other) {
        if ($other['index'] !== $source['index']) {
            $inputs[] = $other['phpir'];
        }
    }
    $argv = ['@INPUT0@', '@OUTPUT@'];
    for ($i = 1; $i < count($inputs); $i++) {
        $argv[] = '--external-phpir';
        $argv[] = '@INPUT' . $i . '@';
    }
    foreach ($allSources as $include) {
        foreach (array_values(array_unique([$include['source'], basename($include['source'])])) as $includeName) {
            $argv[] = '--include-function';
            $argv[] = $includeName;
            $argv[] = $include['activation'];
            $argv[] = '--source-function-prefix';
            $argv[] = $includeName;
            $argv[] = $include['symbolPrefix'];
        }
    }
    $argv[] = '--function-prefix';
    $argv[] = $source['symbolPrefix'];
    if ($targetKind === 'library') {
        $argv[] = '--user-function-abi-prefix';
        $argv[] = 'phpuserfunc_' . source_symbol_prefix($targetName);
    }
    $steps[] = [
        'id' => $source['base'] . '.phpir2valair',
        'role' => 'phpir_to_valair',
        'kind' => 'custom_target',
        'tool' => 'phpefc-phpir2valair',
        'argv' => $argv,
        'inputs' => $inputs,
        'outputs' => [$source['valair']],
    ];
}

foreach ($allSources as $source) {
    $currentValair = $source['valair'];
    if ($valairOptimizerTypingZval2Static) {
        $steps[] = [
            'id' => $source['base'] . '.valair-optimize-typing-zval2static',
            'role' => 'valair_optimize',
            'pass' => 'typing-zval2static',
            'kind' => 'custom_target',
            'tool' => 'phpefc-valair-optimize-typing',
            'argv' => ($valairOptimizerFastPathRuntime || $valairOptimizerLibraryEmbed2Native) ? ['--suppress-warning', '@INPUT@', '@OUTPUT@'] : ['@INPUT@', '@OUTPUT@'],
            'inputs' => [$currentValair],
            'outputs' => [$source['valairAfterTypingZval2Static']],
        ];
        $currentValair = $source['valairAfterTypingZval2Static'];
    }
    if ($valairOptimizerFastPathRuntime) {
        $fastPathInputs = [$currentValair];
        $fastPathArgv = ["@INPUT0@", "@OUTPUT@"];
        foreach ($sources as $other) {
            if ($other["index"] === $source["index"]) {
                continue;
            }
            $fastPathInputs[] = $other["valair"];
            $fastPathArgv[] = "--external-valair";
            $fastPathArgv[] = "@INPUT" . (count($fastPathInputs) - 1) . "@";
        }
        $steps[] = [
            "id" => $source["base"] . ".valair-optimize-fastpath-runtime",
            "role" => "valair_optimize",
            "pass" => "fastpath-runtime",
            "kind" => "custom_target",
            "tool" => "phpefc-valair-optimize-fastpath-runtime",
            "argv" => $fastPathArgv,
            "inputs" => $fastPathInputs,
            "outputs" => [$source["valairAfterFastPathRuntime"]],
        ];
        $currentValair = $source["valairAfterFastPathRuntime"];
    }
    if ($valairOptimizerLibraryEmbed2Native) {
        $steps[] = [
            'id' => $source['base'] . '.valair-optimize-library-embed2native',
            'role' => 'valair_optimize',
            'pass' => 'library-embed2native',
            'kind' => 'custom_target',
            'tool' => 'phpefc-valair-optimize-library',
            'argv' => ['--allowed-native-runtimes', $phpefRuntimeNatives, '@INPUT@', '@OUTPUT@'],
            'inputs' => [$currentValair],
            'outputs' => [$source['valairAfterLibraryEmbed2Native']],
        ];
        $currentValair = $source['valairAfterLibraryEmbed2Native'];
    }
    if ($valairOptimizerTypingZval2Static && ($valairOptimizerFastPathRuntime || $valairOptimizerLibraryEmbed2Native)) {
        $steps[] = [
            'id' => $source['base'] . '.valair-optimize-typing-zval2static-final',
            'role' => 'valair_optimize',
            'pass' => 'typing-zval2static',
            'kind' => 'custom_target',
            'tool' => 'phpefc-valair-optimize-typing',
            'argv' => ['@INPUT@', '@OUTPUT@'],
            'inputs' => [$currentValair],
            'outputs' => [$source['valairAfterTypingZval2StaticFinal']],
        ];
        $currentValair = $source['valairAfterTypingZval2StaticFinal'];
    }
    if ($valairOptimizerPrecompute) {
        $steps[] = [
            'id' => $source['base'] . '.valair-optimize-precompute',
            'role' => 'valair_optimize',
            'pass' => 'precompute',
            'kind' => 'custom_target',
            'tool' => 'phpefc-valair-optimize-precompute',
            'argv' => ['@INPUT@', '@OUTPUT@'],
            'inputs' => [$currentValair],
            'outputs' => [$source['valairAfterPrecompute']],
        ];
        $currentValair = $source['valairAfterPrecompute'];
    }
    if (!empty($source['external'])) {
        continue;
    }
    $argv = ['@INPUT@', '@OUTPUT@'];
    if ($targetKind === 'library' || $source['index'] !== 0) {
        $argv[] = '--no-main';
    }
    $argv[] = '--activation-function';
    $argv[] = $source['activation'];
    if (!$phpefRuntimeEmbed) {
        $argv[] = '--forbid-runtime-embed';
    }
    if ($targetKind === 'library' && $source['index'] === 0) {
        $argv[] = '--library-init-function';
        $argv[] = pipeline_library_init_function($targetName);
    }
    if ($targetKind === 'executable' && $source['index'] === 0) {
        foreach (array_values(array_unique($externFunctions)) as $externFunction) {
            $argv[] = '--extern-function';
            $argv[] = $externFunction;
        }
        foreach (array_values(array_unique($autoInitFunctions)) as $autoInitFunction) {
            $argv[] = '--auto-init-function';
            $argv[] = $autoInitFunction;
        }
    }
    $steps[] = [
        'id' => $source['base'] . '.valair2vala',
        'role' => 'valair_to_vala',
        'kind' => 'custom_target',
        'tool' => 'phpefc-valair2vala',
        'argv' => $argv,
        'inputs' => [$currentValair],
        'outputs' => [$source['vala']],
    ];
}

$valaInputs = array_map(static fn($source) => $source['vala'], $sources);
$needsResourceSections = true;
$needsObjcopy = $debugSection || $stacktraceSection || $needsResourceSections;
$targetIsLibrary = $targetKind === "library";
$rawExe = $needsObjcopy ? $entryBase . '.raw' : $targetName;
$finalArtifact = ($targetIsLibrary && $needsObjcopy) ? ("lib" . $targetName . ".so") : $targetName;
$entryDebugJson = $debugSection ? $sources[0]['debugJson'] : null;
$entryStacktraceMap = $stacktraceSection ? $sources[0]['stacktraceMap'] : null;

if ($debugSection) {
    $cOutputs = array_map(static fn($source) => $source['c'], $sources);
    $valacOutputs = $cOutputs;
    if ($targetIsLibrary) {
        $valacOutputs[] = $targetName . '.h';
        $valacOutputs[] = $targetName . '.vapi';
    }
    $steps[] = [
        'id' => $entryBase . '.valac-c',
        'role' => 'compile_vala_to_c',
        'kind' => 'custom_target',
        'tool' => 'valac',
        'packages' => $runtimePackages,
        'inputs' => $valaInputs,
        'outputs' => $valacOutputs,
        'header' => $targetIsLibrary ? ($targetName . '.h') : null,
        'vapi' => $targetIsLibrary ? ($targetName . '.vapi') : null,
    ];

    foreach ($sources as $source) {
        $steps[] = [
            'id' => $source['base'] . '.map-c-source-lines',
            'role' => 'rewrite_c_source_lines',
            'kind' => 'custom_target',
            'tool' => 'phpefc-c-add-source-lines',
            'argv' => ['@INPUT0@', '@OUTPUT0@', '@INPUT1@', '@OUTPUT1@'],
            'inputs' => [$source['c'], $source['vala']],
            'outputs' => [$source['mappedC'], $source['debugJson']],
        ];
    }

    $mappedInputs = array_map(static fn($source) => $source['mappedC'], $sources);
    $steps[] = [
        'id' => $entryBase . ($targetIsLibrary ? '.compile-c-library' : '.compile-c-executable'),
        'role' => $targetIsLibrary ? 'compile_c_library' : 'compile_c_executable',
        'kind' => $targetIsLibrary ? 'shared_library' : 'executable',
        'inputs' => $mappedInputs,
        'outputs' => [$rawExe],
        'dependencies' => $runtimeDependencies,
        'build_by_default' => false,
    ];

    $embedCheckStamp = null;
    if (!$phpefRuntimeEmbed) {
        $embedCheckStamp = $entryBase . '.no-runtime-embed.stamp';
        $steps[] = [
            'id' => $entryBase . '.verify-no-runtime-embed',
            'role' => 'verify_no_runtime_embed',
            'kind' => 'custom_target',
            'inputs' => [$rawExe],
            'outputs' => [$embedCheckStamp],
            'build_by_default' => true,
        ];
    }
    $sectionBaseExe = $entryBase . '.with-resources';
    $resourceIndex = $entryBase . '.phpef-resource-index.bin';
    $resourceDebugMap = $entryBase . '.phpef-resource-index.txt';
    $resourceInputs = array_merge([$rawExe], array_map(static fn($source) => $source['phpir'], $allSources));
    $resourceArgv = ['@INPUT0@', '@OUTPUT0@', '@OUTPUT1@', '@OUTPUT2@'];
    for ($i = 1; $i < count($resourceInputs); $i++) {
        $resourceArgv[] = '@INPUT' . $i . '@';
    }
    $steps[] = [
        'id' => $entryBase . '.add-resource-sections',
        'role' => 'add_resource_sections',
        'kind' => 'custom_target',
        'tool' => 'phpefc-add-resource-sections',
        'argv' => $resourceArgv,
        'inputs' => $resourceInputs,
        'outputs' => [$sectionBaseExe, $resourceIndex, $resourceDebugMap],
        'build_by_default' => false,
    ];

    if ($stacktraceSection) {
        $steps[] = [
            'id' => $entryBase . '.generate-stacktrace-map',
            'role' => 'generate_stacktrace_map',
            'kind' => 'custom_target',
            'tool' => 'phpefc-generate-stacktrace-map',
            'argv' => array_merge(['@INPUT0@', '@OUTPUT@'], array_map(static fn($i) => '@INPUT' . $i . '@', range(1, count($valaInputs)))),
            'inputs' => array_merge([$sectionBaseExe], $valaInputs),
            'outputs' => [$entryStacktraceMap],
        ];
    }

    $metadataInputs = [$sectionBaseExe];
    $metadataArgv = [];
    $metadataInputIndex = 1;
    if ($debugSection) {
        $metadataInputs[] = $entryDebugJson;
        $metadataArgv[] = '--add-section';
        $metadataArgv[] = '.debug_phpef=@INPUT' . $metadataInputIndex . '@';
        $metadataArgv[] = '--set-section-flags';
        $metadataArgv[] = '.debug_phpef=debug,readonly';
        $metadataInputIndex++;
    }
    if ($stacktraceSection) {
        $metadataInputs[] = $entryStacktraceMap;
        $metadataArgv[] = '--add-section';
        $metadataArgv[] = '.phpef_stacktrace=@INPUT' . $metadataInputIndex . '@';
        $metadataArgv[] = '--set-section-flags';
        $metadataArgv[] = '.phpef_stacktrace=readonly';
        $metadataInputIndex++;
    }
    if ($embedCheckStamp !== null) {
        $metadataInputs[] = $embedCheckStamp;
    }
    $metadataArgv[] = '@INPUT0@';
    $metadataArgv[] = '@OUTPUT@';
    $steps[] = [
        'id' => $entryBase . '.add-metadata-sections',
        'role' => 'add_metadata_sections',
        'kind' => 'custom_target',
        'tool' => 'objcopy',
        'argv' => $metadataArgv,
        'inputs' => $metadataInputs,
        'outputs' => [$finalArtifact],
        'build_by_default' => true,
    ];
} else {
    $steps[] = [
        'id' => $entryBase . ($targetIsLibrary ? '.compile-vala-library' : '.compile-vala-executable'),
        'role' => $targetIsLibrary ? 'compile_vala_library' : 'compile_vala_executable',
        'kind' => $targetIsLibrary ? 'shared_library' : 'executable',
        'packages' => $runtimePackages,
        'inputs' => $valaInputs,
        'outputs' => [$rawExe],
        'dependencies' => $runtimeDependencies,
        'build_by_default' => !$stacktraceSection,
    ];
    $embedCheckStamp = null;
    if (!$phpefRuntimeEmbed) {
        $embedCheckStamp = $entryBase . '.no-runtime-embed.stamp';
        $steps[] = [
            'id' => $entryBase . '.verify-no-runtime-embed',
            'role' => 'verify_no_runtime_embed',
            'kind' => 'custom_target',
            'inputs' => [$rawExe],
            'outputs' => [$embedCheckStamp],
            'build_by_default' => true,
        ];
    }
    $sectionBaseExe = $stacktraceSection ? ($entryBase . '.with-resources') : $finalArtifact;
    $resourceIndex = $entryBase . '.phpef-resource-index.bin';
    $resourceDebugMap = $entryBase . '.phpef-resource-index.txt';
    $resourceInputs = array_merge([$rawExe], array_map(static fn($source) => $source['phpir'], $allSources));
    $resourceArgv = ['@INPUT0@', '@OUTPUT0@', '@OUTPUT1@', '@OUTPUT2@'];
    for ($i = 1; $i < count($resourceInputs); $i++) {
        $resourceArgv[] = '@INPUT' . $i . '@';
    }
    $steps[] = [
        'id' => $entryBase . '.add-resource-sections',
        'role' => 'add_resource_sections',
        'kind' => 'custom_target',
        'tool' => 'phpefc-add-resource-sections',
        'argv' => $resourceArgv,
        'inputs' => $resourceInputs,
        'outputs' => [$sectionBaseExe, $resourceIndex, $resourceDebugMap],
        'build_by_default' => !$stacktraceSection,
    ];
    if ($stacktraceSection) {
        $steps[] = [
            'id' => $entryBase . '.generate-stacktrace-map',
            'role' => 'generate_stacktrace_map',
            'kind' => 'custom_target',
            'tool' => 'phpefc-generate-stacktrace-map',
            'argv' => array_merge(['@INPUT0@', '@OUTPUT@'], array_map(static fn($i) => '@INPUT' . $i . '@', range(1, count($valaInputs)))),
            'inputs' => array_merge([$sectionBaseExe], $valaInputs),
            'outputs' => [$entryStacktraceMap],
        ];
        $steps[] = [
            'id' => $entryBase . '.add-stacktrace-section',
            'role' => 'add_metadata_sections',
            'kind' => 'custom_target',
            'tool' => 'objcopy',
            'argv' => ['--add-section', '.phpef_stacktrace=@INPUT1@', '--set-section-flags', '.phpef_stacktrace=readonly', '@INPUT0@', '@OUTPUT@'],
            'inputs' => $embedCheckStamp === null ? [$sectionBaseExe, $entryStacktraceMap] : [$sectionBaseExe, $entryStacktraceMap, $embedCheckStamp],
            'outputs' => [$finalArtifact],
            'build_by_default' => true,
        ];
    }

}

if ($roundtrip && !$isMultiSource) {
    $source = $sources[0];
    $steps[] = [
        'id' => $entryBase . '.ast2php-roundtrip',
        'role' => 'ast_to_php_roundtrip',
        'kind' => 'custom_target',
        'tool' => 'phpefc-ast2php',
        'argv' => ['@INPUT@', '@OUTPUT@'],
        'inputs' => [$source['phpast']],
        'outputs' => [$source['roundtripPhp']],
    ];
}

$targets = [[
    'id' => $targetName,
    'kind' => $targetKind,
    'artifact' => $needsObjcopy ? $finalArtifact : $rawExe,
    'raw_artifact' => $rawExe,
    'debug_bundle' => $entryDebugJson,
    'stacktrace_map' => $entryStacktraceMap,
    'source' => $entryInput,
    'sources' => $inputFiles,
    'dependencies' => $runtimeDependencies,
    'build_by_default' => true,
]];

$tests = [];
if ($roundtrip && !$isMultiSource) {
    $tests[] = [
        'id' => $entryBase . '.roundtrip',
        'name' => $entryBase . ' roundtrip',
        'role' => 'roundtrip_compare',
        'tool' => 'phpefc-compare-tokenized-files',
        'args' => [$entryInput, $sources[0]['roundtripPhp']],
        'depends' => [$sources[0]['roundtripPhp']],
        'timeout' => 60,
    ];
}

$descriptor = [
    'format' => 'phpefc-build-pipeline-v0',
    'input' => [
        'file' => $entryInput,
        'files' => $inputFiles,
        'externalFiles' => $externalFiles,
        'language' => 'php',
    ],
    'options' => [
        'targetName' => $targetName,
        'targetKind' => $targetKind,
        'debugSourceLines' => $debugSection,
        'embedPhpefDebugSection' => $debugSection,
        'embedPhpefStacktraceSection' => $stacktraceSection,
        'roundtripTest' => $roundtrip && !$isMultiSource,
        'valairOptimizerTypingZval2Static' => $valairOptimizerTypingZval2Static,
        'valairOptimizerLibraryEmbed2Native' => $valairOptimizerLibraryEmbed2Native,
        'valairOptimizerFastPathRuntime' => $valairOptimizerFastPathRuntime,
        'valairOptimizerPrecompute' => $valairOptimizerPrecompute,
    ],
    'runtimeDependencies' => $runtimeDependencies,
    'nativeRuntimeDependencyPattern' => 'phpef-runtime-native-*',
    'runtimePolicy' => [
        'embed' => $phpefRuntimeEmbed,
        'nativeRuntimes' => $allowedNativeRuntimePackages,
        'nativeRuntimeNames' => $allowedNativeRuntimeList,
    ],
    'steps' => $steps,
    'targets' => $targets,
    'tests' => $tests,
    'artifacts' => [
        'phpast' => $sources[0]['phpast'],
        'phpir' => $sources[0]['phpir'],
        'valair' => $sources[0]['valair'],
        'valair_afteropt_typing_zval2static' => $valairOptimizerTypingZval2Static ? $sources[0]['valairAfterTypingZval2Static'] : null,
        'valair_afteropt_typing_zval2static_final' => ($valairOptimizerTypingZval2Static && ($valairOptimizerFastPathRuntime || $valairOptimizerLibraryEmbed2Native)) ? $sources[0]['valairAfterTypingZval2StaticFinal'] : null,
        'valair_afteropt_library_embed2native' => $valairOptimizerLibraryEmbed2Native ? $sources[0]['valairAfterLibraryEmbed2Native'] : null,
        'valair_afteropt_fastpath_runtime' => $valairOptimizerFastPathRuntime ? $sources[0]['valairAfterFastPathRuntime'] : null,
        'valair_afteropt_precompute' => $valairOptimizerPrecompute ? $sources[0]['valairAfterPrecompute'] : null,
        'vala' => $sources[0]['vala'],
        'c' => $sources[0]['c'],
        'mapped_c' => $sources[0]['mappedC'],
        'debug_bundle' => $entryDebugJson,
        'stacktrace_map' => $entryStacktraceMap,
        'raw_executable' => $rawExe,
        'executable' => $targetKind === 'executable' ? ($needsObjcopy ? $finalArtifact : $rawExe) : null,
        'library' => $targetKind === 'library' ? ($needsObjcopy ? $finalArtifact : $rawExe) : null,
    ],
];

$json = json_encode($descriptor, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
if ($json === false) {
    cli_error("Could not encode pipeline descriptor\n");
    exit(1);
}
echo $json;
