# This file is licensed under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

"""Repository rules to configure the terminfo used by LLVM.

Most users should pick one of the explicit rules to configure their use of terminfo
with LLVM:
- `llvm_terminfo_system` will detect and link against a terminfo-implementing
  system library (non-hermetically).
- 'llvm_terminfo_disable` will disable terminfo completely.

If you would like to make your build configurable, you can use
`llvm_terminfo_from_env`. By default, this will disable terminfo, but will
inspect the environment variable (most easily set with a `--repo_env` flag to
the Bazel invocation) `BAZEL_LLVM_TERMINFO_STRATEGY`. If it is set to
`system` then it will behave the same as `llvm_terminfo_system`. Any other
setting will disable terminfo the same as not setting it at all.
"""

def _llvm_terminfo_disable_impl(repository_ctx):
    repository_ctx.template(
        "BUILD",
        repository_ctx.attr._disable_build_template,
        executable = False,
    )

_terminfo_disable_attrs = {
    "_disable_build_template": attr.label(
        default = Label("//deps_impl:terminfo_disable.BUILD"),
        allow_single_file = True,
    ),
}

llvm_terminfo_disable = repository_rule(
    implementation = _llvm_terminfo_disable_impl,
    attrs = _terminfo_disable_attrs,
)

def _find_c_compiler(repository_ctx):
    """Returns the path to a plausible C compiler.

    This routine will only reliably work on roughly POSIX-y systems as it
    ultimately falls back on the `cc` binary. Fortunately, the thing we are
    trying to use it for (detecting if a trivial source file can compile and
    link against a particular library) requires very little.
    """
    cc = repository_ctx.which(repository_ctx.os.environ.get("CC"))
    if not cc:
        # Look for Clang, GCC, and the POSIX / UNIX specified C compiler
        # binaries.
        for compiler in ["clang", "gcc", "c99", "c89", "cc"]:
            cc = repository_ctx.which(compiler)
            if cc:
                break
    return cc

def _try_link(repository_ctx, cc, source, linker_flags):
    """Returns `True` if able to link the source with the linker flag.

    Given a source file that contains references to library routines, this
    will check that when linked with the provided linker flag, those
    references are successfully resolved. This routine assumes a generally
    POSIX-y and GCC-ish compiler and environment and shouldn't be expected to
    work outside of that.
    """
    cmd = [
        cc,
        # Force discard the linked executable.
        "-o",
        "/dev/null",
        # Leave language detection to the compiler.
        source,
    ]

    # The linker flag must be valid for a compiler invocation of the link step,
    # so just append them to the command.
    cmd += linker_flags
    exec_result = repository_ctx.execute(cmd, timeout = 20)
    return exec_result.return_code == 0

def _llvm_terminfo_system_impl(repository_ctx):
    # LLVM doesn't need terminfo support on Windows, so just disable it.
    if repository_ctx.os.name.lower().find("windows") != -1:
        _llvm_terminfo_disable_impl(repository_ctx)
        return

    if len(repository_ctx.attr.system_linkopts) > 0:
        linkopts = repository_ctx.attr.system_linkopts
    else:
        required = repository_ctx.attr.system_required

        # Find a C compiler we can use to detect viable linkopts on this system.
        cc = _find_c_compiler(repository_ctx)
        if not cc:
            if required:
                fail("Failed  to find a C compiler executable")
            else:
                _llvm_terminfo_disable_impl(repository_ctx)
                return

        # Get the source file we use to detect successful linking of terminfo.
        source = repository_ctx.path(repository_ctx.attr._terminfo_test_source)

        # Collect the candidate linkopts and wrap them into a list. Ideally,
        # these would be provided as lists, but Bazel doesn't currently
        # support that. See: https://github.com/bazelbuild/bazel/issues/12178
        linkopts_candidates = [[x] for x in repository_ctx.attr.candidate_system_linkopts]

        # For each candidate, try to use it to link our test source file.
        for linkopts_candidate in linkopts_candidates:
            if _try_link(repository_ctx, cc, source, linkopts_candidate):
                linkopts = linkopts_candidate
                break

        # If we never found a viable linkopts candidate, either error or disable
        # terminfo for LLVM.
        if not linkopts:
            if required:
                fail("Failed to detect which linkopt would successfully provide the " +
                     "necessary terminfo functionality")
            else:
                _llvm_terminfo_disable_impl(repository_ctx)
                return

    repository_ctx.template(
        "BUILD",
        repository_ctx.attr._system_build_template,
        substitutions = {
            "{TERMINFO_LINKOPTS}": str(linkopts),
        },
        executable = False,
    )

def _merge_attrs(attrs_list):
    attrs = {}
    for input_attrs in attrs_list:
        attrs.update(input_attrs)
    return attrs

_terminfo_system_attrs = _merge_attrs([_terminfo_disable_attrs, {
    "_system_build_template": attr.label(
        default = Label("//deps_impl:terminfo_system.BUILD"),
        allow_single_file = True,
    ),
    "_terminfo_test_source": attr.label(
        default = Label("//deps_impl:terminfo_test.c"),
        allow_single_file = True,
    ),
    "candidate_system_linkopts": attr.string_list(
        default = [
            "-lterminfo",
            "-ltinfo",
            "-lcurses",
            "-lncurses",
            "-lncursesw",
        ],
        doc = "Candidate linkopts to test and see if they can link " +
              "successfully.",
    ),
    "system_required": attr.bool(
        default = False,
        doc = "Require that one of the candidates is detected successfully on POSIX platforms where it is needed.",
    ),
    "system_linkopts": attr.string_list(
        default = [],
        doc = "If non-empty, a specific array of linkopts to use to " +
              "successfully link against the terminfo library. No " +
              "detection is performed if this option is provided, it " +
              "directly forces the use of these link options. No test is " +
              "run to determine if they are valid or work correctly either.",
    ),
}])

llvm_terminfo_system = repository_rule(
    implementation = _llvm_terminfo_system_impl,
    configure = True,
    local = True,
    attrs = _terminfo_system_attrs,
)

def _llvm_terminfo_from_env_impl(repository_ctx):
    terminfo_strategy = repository_ctx.os.environ.get("BAZEL_LLVM_TERMINFO_STRATEGY")
    if terminfo_strategy == "system":
        _llvm_terminfo_system_impl(repository_ctx)
    else:
        _llvm_terminfo_disable_impl(repository_ctx)

llvm_terminfo_from_env = repository_rule(
    implementation = _llvm_terminfo_from_env_impl,
    configure = True,
    local = True,
    attrs = _merge_attrs([_terminfo_disable_attrs, _terminfo_system_attrs]),
    environ = ["BAZEL_LLVM_TERMINFO_STRATEGY", "CC"],
)

@@ -1831,24 +1831,28 @@
   %22%22%22%0A    cc
+_env
  = repositor
@@ -1861,49 +1861,98 @@
 ctx.
-which(repository_ctx.os.environ.get(%22CC%22)
+os.environ.get(%22CC%22)%0A    cc = None%0A    if cc_env:%0A        cc = repository_ctx.which(cc_env
 )%0A  

Fix the terminfo detection to work without a `CC` env variable. (#37)