# Copyright 2023 The MediaPipe Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Bazel build rules for Halide."""

load("@bazel_skylib//lib:collections.bzl", "collections")
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "use_cpp_toolchain")

def halide_language_copts():
    _common_opts = [
        "-fPIC",
        "-frtti",
        "-Wno-conversion",
        "-Wno-sign-compare",
    ]
    _posix_opts = [
        "$(STACK_FRAME_UNLIMITED)",
        "-fno-exceptions",
        "-funwind-tables",
        "-fvisibility-inlines-hidden",
    ]
    _msvc_opts = [
        "-D_CRT_SECURE_NO_WARNINGS",
        "/MD",
    ]
    return _common_opts + select({
        "//conditions:default": _posix_opts,
        "@mediapipe//mediapipe:windows": _msvc_opts,
    })

def halide_language_linkopts():
    _linux_opts = [
        "-ldl",
        "-lpthread",
        "-lz",
        "-rdynamic",
    ]
    _osx_opts = [
        "-lz",
        "-Wl,-stack_size",
        "-Wl,1000000",
    ]
    _msvc_opts = []
    return select({
        "//conditions:default": _linux_opts,
        "@mediapipe//mediapipe:macos": _osx_opts,
        "@mediapipe//mediapipe:windows": _msvc_opts,
    })

def halide_runtime_linkopts():
    """ Return the linkopts needed when linking against halide_library_runtime.

    Returns:
        List to be used for linkopts.
    """
    _posix_opts = [
        "-ldl",
        "-lpthread",
    ]
    _android_opts = [
        "-llog",
    ]
    _msvc_opts = []

    return select({
        "//conditions:default": _posix_opts,
        "@mediapipe//mediapipe:android": _android_opts,
        "@mediapipe//mediapipe:windows": _msvc_opts,
    })

# Map of halide-target-base -> config_settings
_HALIDE_TARGET_CONFIG_SETTINGS_MAP = {
    # Android
    "arm-32-android": ["@halide//:halide_config_android_arm"],
    "arm-64-android": ["@halide//:halide_config_android_arm64"],
    "x86-32-android": ["@halide//:halide_config_android_x86_32"],
    "x86-64-android": ["@halide//:halide_config_android_x86_64"],
    # iOS
    "arm-32-ios": ["@halide//:halide_config_ios_arm"],
    "arm-64-ios": ["@halide//:halide_config_ios_arm64"],
    # OSX (or iOS simulator)
    "x86-32-osx": ["@halide//:halide_config_macos_x86_32", "@halide//:halide_config_ios_x86_32"],
    "x86-64-osx": ["@halide//:halide_config_macos_x86_64", "@halide//:halide_config_ios_x86_64"],
    "arm-64-osx": ["@halide//:halide_config_macos_arm64"],
    # Windows
    "x86-64-windows": ["@halide//:halide_config_windows_x86_64"],
    # Linux
    "x86-64-linux": ["@halide//:halide_config_linux_x86_64"],
    # deliberately nothing here using //conditions:default
}

_HALIDE_TARGET_MAP_DEFAULT = {
    "x86-64-linux": [
        "x86-64-linux-sse41-avx-avx2-fma",
        "x86-64-linux-sse41",
        "x86-64-linux",
    ],
    "x86-64-osx": [
        "x86-64-osx-sse41-avx-avx2-fma",
        "x86-64-osx-sse41",
        "x86-64-osx",
    ],
    "x86-64-windows": [
        "x86-64-windows-sse41-avx-avx2-fma",
        "x86-64-windows-sse41",
        "x86-64-windows",
    ],
}

def halide_library_default_target_map():
    return _HALIDE_TARGET_MAP_DEFAULT

# Alphabetizes the features part of the target to make sure they always match no
# matter the concatenation order of the target string pieces.
def _canonicalize_target(halide_target):
    if halide_target == "host":
        return halide_target
    if "," in halide_target:
        fail("Multitarget may not be specified here")
    tokens = halide_target.split("-")
    if len(tokens) < 3:
        fail("Illegal target: %s" % halide_target)

    # rejoin the tokens with the features sorted
    return "-".join(tokens[0:3] + sorted(tokens[3:]))

# Converts comma and dash separators to underscore and alphabetizes
# the features part of the target to make sure they always match no
# matter the concatenation order of the target string pieces.
def _halide_target_to_bazel_rule_name(multitarget):
    subtargets = multitarget.split(",")
    subtargets = [_canonicalize_target(st).replace("-", "_") for st in subtargets]
    return "_".join(subtargets)

# The second argument is True if there is a separate file generated
# for each subtarget of a multitarget output, False if not. The third
# argument is True if the output is a directory (vs. a single file).
# The fourth argument is a list of output group(s) that the files should
# be added to.

_is_multi = True
_is_single = False
_is_file = False

_output_extensions = {
    "assembly": ("s", _is_multi, _is_file, []),
    "bitcode": ("bc", _is_multi, _is_file, ["generated_bitcode"]),
    "c_header": ("h", _is_single, _is_file, ["generated_headers"]),
    "c_source": ("halide_generated.cpp", _is_multi, _is_file, []),
    "compiler_log": ("halide_compiler_log", _is_single, _is_file, ["generated_object", "generated_compiler_log"]),
    "cpp_stub": ("stub.h", _is_single, _is_file, []),
    "featurization": ("featurization", _is_multi, _is_file, []),
    "llvm_assembly": ("ll", _is_multi, _is_file, []),
    "object": ("o", _is_single, _is_file, ["generated_object"]),
    "python_extension": ("py.cpp", _is_single, _is_file, []),
    "registration": ("registration.cpp", _is_single, _is_file, ["generated_registration"]),
    "schedule": ("schedule.h", _is_single, _is_file, []),
    "static_library": ("a", _is_single, _is_file, ["generated_object"]),
    "stmt": ("stmt", _is_multi, _is_file, []),
    "stmt_html": ("stmt.html", _is_multi, _is_file, []),
}

def _add_output_file(f, fmt, output_files, output_dict, verbose_extra_outputs, verbose_output_paths):
    if fmt in verbose_extra_outputs:
        verbose_output_paths.append(f.path)
    output_files.append(f)
    if fmt in _output_extensions:
        for group in _output_extensions[fmt][3]:
            output_dict.setdefault(group, []).append(f)

HalideFunctionNameInfo = provider(fields = ["function_name"])
HalideGeneratorBinaryInfo = provider(fields = ["generator_binary"])
HalideGeneratorNameInfo = provider(fields = ["generator_name_"])
HalideGeneratorParamsInfo = provider(fields = ["generator_params"])
HalideLibraryNameInfo = provider(fields = ["library_name"])
HalideTargetFeaturesInfo = provider(fields = ["target_features"])

def _gengen_closure_impl(ctx):
    return [
        HalideGeneratorBinaryInfo(generator_binary = ctx.attr.generator_binary),
        HalideGeneratorNameInfo(generator_name_ = ctx.attr.generator_name_),
    ]

_gengen_closure = rule(
    implementation = _gengen_closure_impl,
    attrs = {
        "generator_binary": attr.label(
            executable = True,
            allow_files = True,
            mandatory = True,
            cfg = "exec",
        ),
        # "generator_name" is apparently reserved by Bazel for attrs in rules
        "generator_name_": attr.string(mandatory = True),
    },
    provides = [HalideGeneratorBinaryInfo, HalideGeneratorNameInfo],
)

def _halide_library_instance_impl(ctx):
    generator_binary = ctx.attr.generator_closure[HalideGeneratorBinaryInfo].generator_binary if ctx.attr.generator_closure else ""
    generator_name = ctx.attr.generator_closure[HalideGeneratorNameInfo].generator_name_ if ctx.attr.generator_closure else ""
    return [
        HalideFunctionNameInfo(function_name = ctx.attr.function_name),
        HalideGeneratorBinaryInfo(generator_binary = generator_binary),
        HalideGeneratorNameInfo(generator_name_ = generator_name),
        HalideGeneratorParamsInfo(generator_params = ctx.attr.generator_params),
        HalideLibraryNameInfo(library_name = ctx.attr.library_name),
        HalideTargetFeaturesInfo(target_features = ctx.attr.target_features),
    ]

_halide_library_instance = rule(
    implementation = _halide_library_instance_impl,
    attrs = {
        "function_name": attr.string(),
        "generator_closure": attr.label(
            cfg = "exec",
            providers = [HalideGeneratorBinaryInfo, HalideGeneratorNameInfo],
        ),
        "generator_params": attr.string_list(),
        "library_name": attr.string(),
        "target_features": attr.string_list(),
    },
    provides = [
        HalideFunctionNameInfo,
        HalideGeneratorBinaryInfo,
        HalideGeneratorNameInfo,
        HalideGeneratorParamsInfo,
        HalideLibraryNameInfo,
        HalideTargetFeaturesInfo,
    ],
)

def _gengen_impl(ctx):
    if _has_dupes(ctx.attr.requested_outputs):
        fail("Duplicate values in outputs: " + str(ctx.attr.requested_outputs))

    function_name = ctx.attr.function_name[HalideFunctionNameInfo].function_name if ctx.attr.function_name else ""
    generator_binary = ctx.attr.generator_binary[HalideGeneratorBinaryInfo].generator_binary if ctx.attr.generator_binary else ""
    generator_name_ = ctx.attr.generator_name_[HalideGeneratorNameInfo].generator_name_ if ctx.attr.generator_name_ else ""
    generator_params = ctx.attr.generator_params[HalideGeneratorParamsInfo].generator_params if ctx.attr.generator_params else []
    library_name = ctx.attr.library_name[HalideLibraryNameInfo].library_name if ctx.attr.library_name else ""
    target_features = ctx.attr.target_features[HalideTargetFeaturesInfo].target_features if ctx.attr.target_features else []

    for gp in generator_params:
        if " " in gp:
            fail("%s: Entries in generator_params must not contain spaces." % library_name)

    # Escape backslashes and double quotes.
    generator_params = [gp.replace("\\", '\\\\"').replace('"', '\\"') for gp in generator_params]

    execution_requirements = {}

    # --- Calculate the output type(s) we're going to produce (and which ones should be verbose)
    quiet_extra_outputs = []
    verbose_extra_outputs = []
    if ctx.attr.consider_halide_extra_outputs:
        if "halide_extra_outputs" in ctx.var:
            verbose_extra_outputs = ctx.var.get("halide_extra_outputs", "").split(",")
        if "halide_extra_outputs_quiet" in ctx.var:
            quiet_extra_outputs = ctx.var.get("halide_extra_outputs_quiet", "").split(",")
    requested_outputs = sorted(collections.uniq(ctx.attr.requested_outputs +
                                                verbose_extra_outputs +
                                                quiet_extra_outputs))

    # --- Assemble halide_target, adding extra features if necessary
    base_target = ctx.attr.halide_base_target
    if "," in base_target:
        fail("halide_base_target should never be a multitarget")
    if len(base_target.split("-")) != 3:
        fail("halide_base_target should have exactly 3 components")

    target_features = target_features + ctx.var.get("halide_target_features", "").split(",")

    if "no_runtime" in target_features:
        fail("Specifying 'no_runtime' in halide_target_features is not supported; " +
             "please add 'add_halide_runtime_deps = False' to the halide_library() rule instead.")

    for san in ["asan", "msan", "tsan"]:
        if san in target_features:
            fail("halide_library doesn't support '%s' in halide_target_features; please build with --config=%s instead." % (san, san))

    # Append the features common to everything.
    target_features.append("c_plus_plus_name_mangling")
    target_features.append("no_runtime")

    # Make it all neat and tidy.
    target_features = sorted(collections.uniq(target_features))

    # Get the multitarget list (if any) from halide_target_map
    halide_targets = ctx.attr.halide_target_map.get(base_target, [base_target])

    # Add the extra features to all of them
    halide_targets = _add_features_to_all(halide_targets, target_features)

    leaf_name = ctx.attr.filename.split("/")[-1]

    output_files = []
    output_dict = {}
    verbose_output_paths = []
    inputs = []

    env = {
        "HL_DEBUG_CODEGEN": str(ctx.var.get("halide_debug_codegen", 0)),
        # --define halide_llvm_args=-time-passes is a typical usage
        "HL_LLVM_ARGS": str(ctx.var.get("halide_llvm_args", "")),
    }

    be_very_quiet = ctx.var.get("halide_experimental_quiet", False)  # I'm hunting wabbit...

    # --- Calculate the final set of output files
    for fmt in requested_outputs:
        if fmt not in _output_extensions:
            fail("Unknown Halide output '%s'; known outputs are %s" %
                 (fmt, sorted(_output_extensions.keys())))
        ext, is_multiple, is_dir, _ = _output_extensions[fmt]

        # Special-case Windows file extensions
        if "windows" in halide_targets[-1]:
            if ext == "o":
                ext = "obj"
            if ext == "a":
                ext = "lib"
        if is_multiple and len(halide_targets) > 1:
            for h in halide_targets:
                suffix = _canonicalize_target(h)
                name = "%s-%s.%s" % (ctx.attr.filename, suffix, ext)
                f = ctx.actions.declare_directory(name) if is_dir else ctx.actions.declare_file(name)
                _add_output_file(f, fmt, output_files, output_dict, verbose_extra_outputs, verbose_output_paths)
        else:
            name = "%s.%s" % (ctx.attr.filename, ext)
            f = ctx.actions.declare_directory(name) if is_dir else ctx.actions.declare_file(name)
            _add_output_file(f, fmt, output_files, output_dict, verbose_extra_outputs, verbose_output_paths)

    # --- Progress message(s), including log info about any 'extra' files being output due to --define halide_extra_output
    progress_message = "Executing generator %s with target (%s) args (%s)." % (
        generator_name_,
        ",".join(halide_targets),
        " ".join(generator_params),
    )

    for f in output_files:
        if any([f.path.endswith(suf) for suf in [".h", ".a", ".o", ".lib", ".registration.cpp", ".bc", ".halide_compiler_log"]]):
            continue

        # If an extra output was specified via --define halide_extra_outputs=foo on the command line,
        # add to the progress message (so that it is ephemeral and doesn't clog stdout).
        #
        # (Trailing space is intentional since Starlark will append a period to the end,
        # making copy-n-paste harder than it might otherwise be...)
        if not be_very_quiet:
            extra_msg = "Emitting extra Halide output: %s " % f.path
            progress_message += "\n" + extra_msg
            if f.path in verbose_output_paths:
                # buildifier: disable=print
                print(extra_msg)

    # --- Construct the arguments list for the Generator
    arguments = ctx.actions.args()
    arguments.add("-o", output_files[0].dirname)
    if ctx.attr.generate_runtime:
        arguments.add("-r", leaf_name)
        if len(halide_targets) > 1:
            fail("Only one halide_target allowed when using generate_runtime")
        if function_name:
            fail("halide_function_name not allowed when using generate_runtime")
    else:
        arguments.add("-g", generator_name_)
        arguments.add("-n", leaf_name)
        if function_name:
            arguments.add("-f", function_name)

    if requested_outputs:
        arguments.add_joined("-e", requested_outputs, join_with = ",")

    # Can't use add_joined(), as it will insert a space after target=
    arguments.add("target=%s" % (",".join(halide_targets)))
    if generator_params:
        for p in generator_params:
            for s in ["target"]:
                if p.startswith("%s=" % s):
                    fail("You cannot specify %s in the generator_params parameter in bazel." % s)
        arguments.add_all(generator_params)

    show_gen_arg = ctx.var.get("halide_show_generator_command", "")

    # If it's an exact match of a fully qualified path, show just that one.
    # If it's * or "all", match everything.
    if library_name and show_gen_arg in [library_name, "all", "*"] and not ctx.attr.generate_runtime:
        # The 'Args' object can be printed, but can't be usefully converted to a string, or iterated,
        # so we'll reproduce the logic here. We'll also take the opportunity to add or augment
        # some args to be more useful to whoever runs it (eg, add `-v=1`, add some output files).
        sg_args = ["-v", "1"]
        sg_args += ["-o", "/tmp"]
        sg_args += ["-g", generator_name_]
        sg_args += ["-n", leaf_name]
        if function_name:
            sg_args += ["-f", function_name]
        if requested_outputs:
            # Ensure that several commonly-useful output are added
            ro = sorted(collections.uniq(requested_outputs + ["stmt", "assembly", "llvm_assembly"]))
            sg_args += ["-e", ",".join(ro)]
        sg_args.append("target=%s" % (",".join(halide_targets)))

        if generator_params:
            sg_args += generator_params

        # buildifier: disable=print
        print(
            "\n\nTo locally run the Generator for",
            library_name,
            "use the command:\n\n",
            "bazel run -c opt",
            generator_binary.label,
            "--",
            " ".join(sg_args),
            "\n\n",
        )

    # Finally... run the Generator.
    ctx.actions.run(
        execution_requirements = execution_requirements,
        arguments = [arguments],
        env = env,
        executable = generator_binary.files_to_run.executable,
        mnemonic = "ExecuteHalideGenerator",
        inputs = depset(direct = inputs),
        outputs = output_files,
        progress_message = progress_message,
        exec_group = "generator",
    )

    return [
        DefaultInfo(files = depset(direct = output_files)),
        OutputGroupInfo(**output_dict),
    ]

_gengen = rule(
    implementation = _gengen_impl,
    attrs = {
        "consider_halide_extra_outputs": attr.bool(),
        "filename": attr.string(),
        "generate_runtime": attr.bool(default = False),
        "generator_binary": attr.label(
            cfg = "exec",
            providers = [HalideGeneratorBinaryInfo],
        ),
        # "generator_name" is apparently reserved by Bazel for attrs in rules
        "generator_name_": attr.label(
            cfg = "exec",
            providers = [HalideGeneratorNameInfo],
        ),
        "halide_base_target": attr.string(),
        "function_name": attr.label(
            cfg = "target",
            providers = [
                HalideFunctionNameInfo,
            ],
        ),
        "generator_params": attr.label(
            cfg = "target",
            providers = [
                HalideGeneratorParamsInfo,
            ],
        ),
        "library_name": attr.label(
            cfg = "target",
            providers = [
                HalideLibraryNameInfo,
            ],
        ),
        "target_features": attr.label(
            cfg = "target",
            providers = [
                HalideTargetFeaturesInfo,
            ],
        ),
        "halide_target_map": attr.string_list_dict(),
        "requested_outputs": attr.string_list(),
        "_cc_toolchain": attr.label(default = "@bazel_tools//tools/cpp:current_cc_toolchain"),
    },
    fragments = ["cpp"],
    output_to_genfiles = True,
    toolchains = use_cpp_toolchain(),
    exec_groups = {
        "generator": exec_group(),
    },
)

def _add_target_features(target, features):
    if "," in target:
        fail("Cannot use multitarget here")
    new_target = target.split("-")
    for f in features:
        if f and f not in new_target:
            new_target.append(f)
    return "-".join(new_target)

def _add_features_to_all(halide_targets, features):
    return [_canonicalize_target(_add_target_features(t, features)) for t in halide_targets]

def _has_dupes(some_list):
    clean = collections.uniq(some_list)
    return sorted(some_list) != sorted(clean)

# Target features which do not affect runtime compatibility.
_IRRELEVANT_FEATURES = collections.uniq([
    "arm_dot_prod",
    "arm_fp16",
    "c_plus_plus_name_mangling",
    "check_unsafe_promises",
    "embed_bitcode",
    "enable_llvm_loop_opt",
    "large_buffers",
    "no_asserts",
    "no_bounds_query",
    "profile",
    "strict_float",
    "sve",
    "sve2",
    "trace_loads",
    "trace_pipeline",
    "trace_realizations",
    "trace_stores",
    "user_context",
    "wasm_sat_float_to_int",
    "wasm_signext",
    "wasm_simd128",
])

def _discard_irrelevant_features(halide_target_features = []):
    return sorted(collections.uniq([f for f in halide_target_features if f not in _IRRELEVANT_FEATURES]))

def _halide_library_runtime_target_name(halide_target_features = []):
    return "_".join(["halide_library_runtime"] + _discard_irrelevant_features(halide_target_features))

def _define_halide_library_runtime(
        halide_target_features = [],
        compatible_with = []):
    target_name = _halide_library_runtime_target_name(halide_target_features)

    if not native.existing_rule("halide_library_runtime.generator"):
        halide_generator(
            name = "halide_library_runtime.generator",
            srcs = [],
            deps = [],
            visibility = ["//visibility:private"],
        )
    condition_deps = {}
    for base_target, cfgs in _HALIDE_TARGET_CONFIG_SETTINGS_MAP.items():
        target_features = _discard_irrelevant_features(halide_target_features)
        halide_target_name = _halide_target_to_bazel_rule_name(base_target)
        gengen_name = "%s_%s" % (halide_target_name, target_name)

        _halide_library_instance(
            name = "%s.library_instance" % gengen_name,
            compatible_with = compatible_with,
            function_name = "",
            generator_closure = ":halide_library_runtime.generator_closure",
            generator_params = [],
            library_name = "",
            target_features = target_features,
            visibility = ["//visibility:private"],
        )
        hl_instance = ":%s.library_instance" % gengen_name

        _gengen(
            name = gengen_name,
            compatible_with = compatible_with,
            filename = "%s/%s" % (halide_target_name, target_name),
            generate_runtime = True,
            generator_binary = hl_instance,
            generator_name_ = hl_instance,
            halide_base_target = base_target,
            requested_outputs = ["object"],
            tags = ["manual"],
            target_features = hl_instance,
            visibility = ["@halide//:__subpackages__"],
        )
        for cfg in cfgs:
            condition_deps[cfg] = [":%s" % gengen_name]

    deps = []
    native.cc_library(
        name = target_name,
        compatible_with = compatible_with,
        srcs = select(condition_deps),
        linkopts = halide_runtime_linkopts(),
        tags = ["manual"],
        deps = deps,
        visibility = ["//visibility:public"],
    )

    return target_name

def _standard_library_runtime_features():
    _standard_features = [
        [],
        ["cuda"],
        ["metal"],
        ["opencl"],
        ["openglcompute"],
        ["openglcompute", "egl"],
    ]
    return [f for f in _standard_features] + [f + ["debug"] for f in _standard_features]

def _standard_library_runtime_names():
    return collections.uniq([_halide_library_runtime_target_name(f) for f in _standard_library_runtime_features()])

def halide_library_runtimes(compatible_with = []):
    # Note that we don't use all of these combinations
    # (and some are invalid), but that's ok.
    for cpu in ["arm", "arm64", "x86_32", "x86_64"]:
        for os in ["android", "linux", "windows", "ios", "macos"]:
            native.config_setting(
                name = "halide_config_%s_%s" % (os, cpu),
                constraint_values = [
                    "@platforms//os:%s" % os,
                    "@platforms//cpu:%s" % cpu,
                ],
                visibility = ["//visibility:public"],
            )

    unused = [
        _define_halide_library_runtime(f, compatible_with = compatible_with)
        for f in _standard_library_runtime_features()
    ]
    unused = unused  # unused variable

def halide_generator(
        name,
        srcs,
        compatible_with = [],
        copts = [],
        deps = [],
        generator_name = "",
        includes = [],
        tags = [],
        testonly = False,
        visibility = None):
    if not name.endswith(".generator"):
        fail("halide_generator rules must end in .generator")

    basename = name[:-10]  # strip ".generator" suffix
    if not generator_name:
        generator_name = basename

    # Note: This target is public, but should not be needed by the vast
    # majority of users. Unless you are writing a custom Bazel rule that
    # involves Halide generation, you most probably won't need to depend on
    # this rule.
    native.cc_binary(
        name = name,
        copts = copts + halide_language_copts(),
        linkopts = halide_language_linkopts(),
        compatible_with = compatible_with,
        srcs = srcs,
        deps = [
            "@halide//:gengen",
            "@halide//:language",
        ] + deps,
        tags = ["manual"] + tags,
        testonly = testonly,
        visibility = ["//visibility:public"],
    )

    _gengen_closure(
        name = "%s_closure" % name,
        generator_binary = name,
        generator_name_ = generator_name,
        compatible_with = compatible_with,
        testonly = testonly,
        visibility = ["//visibility:private"],
    )

# This rule exists to allow us to select() on halide_target_features.
def _select_halide_library_runtime_impl(ctx):
    f = ctx.attr.halide_target_features

    standard_runtimes = {t.label.name: t for t in ctx.attr._standard_runtimes}

    f = sorted(_discard_irrelevant_features(collections.uniq(f)))
    runtime_name = _halide_library_runtime_target_name(f)
    if runtime_name not in standard_runtimes:
        fail(("There is no Halide runtime available for the feature set combination %s. " +
              "Please use contact information from halide-lang.org to contact the Halide " +
              "team to add the right combination.") % str(f))

    return standard_runtimes[runtime_name][CcInfo]

_select_halide_library_runtime = rule(
    implementation = _select_halide_library_runtime_impl,
    attrs = {
        "halide_target_features": attr.string_list(),
        "_standard_runtimes": attr.label_list(
            default = ["@halide//:%s" % n for n in _standard_library_runtime_names()],
            providers = [CcInfo],
        ),
    },
    provides = [CcInfo],
)

def halide_library_from_generator(
        name,
        generator,
        add_halide_runtime_deps = True,
        compatible_with = [],
        deps = [],
        function_name = None,
        generator_params = [],
        halide_target_features = [],
        halide_target_map = halide_library_default_target_map(),
        includes = [],
        namespace = None,
        tags = [],
        testonly = False,
        visibility = None):
    if not function_name:
        function_name = name

    if namespace:
        function_name = "%s::%s" % (namespace, function_name)

    generator_closure = "%s_closure" % generator

    _halide_library_instance(
        name = "%s.library_instance" % name,
        compatible_with = compatible_with,
        function_name = function_name,
        generator_closure = generator_closure,
        generator_params = generator_params,
        library_name = "//%s:%s" % (native.package_name(), name),
        target_features = halide_target_features,
        testonly = testonly,
        visibility = ["//visibility:private"],
    )
    hl_instance = ":%s.library_instance" % name

    condition_deps = {}
    for base_target, cfgs in _HALIDE_TARGET_CONFIG_SETTINGS_MAP.items():
        base_target_name = _halide_target_to_bazel_rule_name(base_target)
        gengen_name = "%s_%s" % (base_target_name, name)
        _gengen(
            name = gengen_name,
            compatible_with = compatible_with,
            consider_halide_extra_outputs = True,
            filename = "%s/%s" % (base_target_name, name),
            function_name = hl_instance,
            generator_binary = generator_closure,
            generator_name_ = generator_closure,
            generator_params = hl_instance,
            halide_base_target = base_target,
            halide_target_map = halide_target_map,
            library_name = hl_instance,
            requested_outputs = ["static_library"],
            tags = ["manual"] + tags,
            target_features = hl_instance,
            testonly = testonly,
        )
        for cfg in cfgs:
            condition_deps[cfg] = [":%s" % gengen_name]

    # Use a canonical target to build CC, regardless of config detected
    cc_base_target = "x86-64-linux"

    for output, target_name in [
        ("c_header", "%s_h" % name),
        ("c_source", "%s_cc" % name),
    ]:
        _gengen(
            name = target_name,
            compatible_with = compatible_with,
            filename = name,
            function_name = hl_instance,
            generator_binary = generator_closure,
            generator_name_ = generator_closure,
            generator_params = hl_instance,
            halide_base_target = cc_base_target,
            library_name = hl_instance,
            requested_outputs = [output],
            tags = ["manual"] + tags,
            target_features = hl_instance,
            testonly = testonly,
        )

    _select_halide_library_runtime(
        name = "%s.halide_library_runtime_deps" % name,
        halide_target_features = halide_target_features,
        compatible_with = compatible_with,
        tags = tags,
        visibility = ["//visibility:private"],
    )

    native.filegroup(
        name = "%s_object" % name,
        srcs = select(condition_deps),
        output_group = "generated_object",
        visibility = ["//visibility:private"],
        compatible_with = compatible_with,
        tags = tags,
        testonly = testonly,
    )

    native.cc_library(
        name = name,
        srcs = ["%s_object" % name],
        hdrs = [
            ":%s_h" % name,
        ],
        deps = deps +
               ["@halide//:runtime"] +  # for HalideRuntime.h, etc
               ([":%s.halide_library_runtime_deps" % name] if add_halide_runtime_deps else []),  # for the runtime implementation
        defines = ["HALIDE_FUNCTION_ATTRS=HALIDE_MUST_USE_RESULT"],
        compatible_with = compatible_with,
        includes = includes,
        tags = tags,
        testonly = testonly,
        visibility = visibility,
        linkstatic = 1,
    )

    # Return the fully-qualified built target name.
    return "//%s:%s" % (native.package_name(), name)

def halide_library(
        name,
        srcs = [],
        add_halide_runtime_deps = True,
        copts = [],
        compatible_with = [],
        filter_deps = [],
        function_name = None,
        generator_params = [],
        generator_deps = [],
        generator_name = None,
        halide_target_features = [],
        halide_target_map = halide_library_default_target_map(),
        includes = [],
        namespace = None,
        tags = [],
        testonly = False,
        visibility = None):
    if not srcs and not generator_deps:
        fail("halide_library needs at least one of srcs or generator_deps to provide a generator")

    halide_generator(
        name = "%s.generator" % name,
        srcs = srcs,
        compatible_with = compatible_with,
        generator_name = generator_name,
        deps = generator_deps,
        includes = includes,
        copts = copts,
        tags = tags,
        testonly = testonly,
        visibility = visibility,
    )

    return halide_library_from_generator(
        name = name,
        generator = ":%s.generator" % name,
        add_halide_runtime_deps = add_halide_runtime_deps,
        compatible_with = compatible_with,
        deps = filter_deps,
        function_name = function_name,
        generator_params = generator_params,
        halide_target_features = halide_target_features,
        halide_target_map = halide_target_map,
        includes = includes,
        namespace = namespace,
        tags = tags,
        testonly = testonly,
        visibility = visibility,
    )
