#!/usr/bin/env python3
# Copyright 2024 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Wrapper for Python scripts in toolchain-utils.

Python scripts here assume that they can import arbitrary modules. This is only
consistently possible if the root of toolchain-utils is on PYTHONPATH. The
simplest way to make that happen is to wrap the scripts.

py/bin/foo.py will be invoked _similarly to_ `PYTHONPATH=. ./foo.py`. If the
script has a `main` or `_main` function defined, it will be called with a
single argument: `sys.argv[1:]`.
"""

import importlib.util
import inspect
import os
from pathlib import Path
import sys
from typing import Any, Callable


def find_file_to_execute(argv0: str) -> Path:
    symlink_path = Path(os.getcwd(), argv0)
    symlink_parent = symlink_path.parent.resolve()
    me = (symlink_parent / symlink_path.name).resolve()
    toolchain_utils = me.parent
    relative_script_path = (
        symlink_parent.relative_to(toolchain_utils) / symlink_path.name
    )
    prefix = "py/bin/"
    relative_script_path_str = str(relative_script_path)
    if not relative_script_path_str.startswith(prefix):
        raise ValueError(
            f"Expected argv0 to be in {prefix} - it's {relative_script_path}"
        )
    target_script = relative_script_path_str[len(prefix) :]
    result = toolchain_utils / target_script
    if not result.exists():
        sys.exit(f"No script found at {target_script} - can't execute")
    return result


def main() -> None:
    main_file = find_file_to_execute(sys.argv[0])
    module_name = main_file.with_suffix("").name
    spec = importlib.util.spec_from_file_location(
        module_name,
        main_file,
    )
    if not spec:
        raise ValueError(f"Could not retrieve spec from module {module_name}")
    main_module = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = main_module
    if not spec.loader:
        raise ValueError(f"Spec for {module_name} does not have a loader")
    spec.loader.exec_module(main_module)

    # We have various `main` conventions to support here, unfortunately:
    # - Some return None; others return an exit code.
    # - Some take argv; others take no args.
    # - Some are called `main`, others are called `_main`.
    # - Some capitalize `Main`, others don't.
    # It'd be nice to make this more uniform, but it's easy enough to handle
    # all of these until that happens.
    main_fns = (
        "main",
        "Main",
        "_main",
        "_Main",
    )
    main_fn: Callable[..., Any]
    for f in main_fns:
        if my_fn := getattr(main_module, f, None):
            main_fn = my_fn
            break
    else:
        sys.exit(
            f"No function called any of {main_fns} declared in {main_file}."
        )
    if inspect.signature(main_fn).parameters:
        result = main_fn(sys.argv[1:])
    else:
        result = main_fn()
    if result:
        sys.exit(result)


if __name__ == "__main__":
    main()
