diff --git a/prelude/cxx/cmake.bzl b/prelude/cxx/cmake.bzl new file mode 100644 index 000000000000..1caa9ae85f01 --- /dev/null +++ b/prelude/cxx/cmake.bzl @@ -0,0 +1,116 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is dual-licensed under either the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree or the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. You may select, at your option, one of the +# above-listed licenses. + +load("@prelude//utils/value.bzl", "GenericValueInfo") +load("@prelude//python:toolchain.bzl", "PythonToolchainInfo") + +# Stores a variable which will be searched for in the cmake configure file +# template as well as a value to replace that variable with during substitution. +CMakeSubstitutionInfo = provider( + fields = { + "variable": provider_field(str), + "value": provider_field(typing.Any) + } +) + +def cmake_configure_file_impl(ctx: AnalysisContext) -> list[Provider]: + output_dir = ctx.actions.declare_output("out", dir = True) + output_file = output_dir.project(ctx.attrs.output if ctx.attrs.output else ctx.label.name) + + script_run_info = ctx.attrs.script[RunInfo] + args = [ + script_run_info, + "--input", ctx.attrs.template, + "--output", output_file.as_output(), + ] + hidden = [] + if ctx.attrs.strict: + args.append("--strict") + if ctx.attrs.at_sub: + args.append("--enable-at-replacements") + if ctx.attrs.var_sub: + args.append("--enable-var-replacements") + if ctx.attrs.escape_quotes: + args.append("--escape-quotes") + if ctx.attrs.copy_only: + args.append("--copy-only") + + substitution_dictionary = {} + + for sub in ctx.attrs.substitutions: + info = sub[CMakeSubstitutionInfo] + entry = { + "value": info.value, + "type": "embed" if isinstance(info.value, Artifact) else "subst" + } + + substitution_dictionary[info.variable] = entry + + config_json = ctx.actions.write_json("config.json", substitution_dictionary, with_inputs=True, pretty=True) + args.extend([ + "--substitution-file", + config_json + ]) + + ctx.actions.run( + cmd_args(args, hidden=hidden), + category = "cmake_configure_file", + identifier = ctx.attrs.template.short_path(), + ) + + sub_targets = {"outdir": [DefaultInfo(default_outputs=[output_dir])]} + return [ + DefaultInfo( + default_outputs = [output_file], + sub_targets = sub_targets + ) + ] + +def _cmake_substitution_impl_internal(name:str, variable:str|None, value) -> list[Provider]: + if variable == None: + variable = name + + + return [ + DefaultInfo(), + CMakeSubstitutionInfo( + variable = variable, + value = value + ) + ] + +def _get_integer_value(value:Dependency|None|int) -> int|None: + if isinstance(value, Dependency): + return _get_integer_value(value[GenericValueInfo].value) + + if isinstance(value, int): + return value + + if value == None: + return value + + fail(f"Unsupported value type {value} for integral argument") + return None + +def cmake_type_size_substitution_impl(ctx: AnalysisContext) -> list[Provider]: + variable = ctx.attrs.variable or ctx.attrs.name + + key = f"{variable}_CODE" + + size = _get_integer_value(ctx.attrs.size) + if size == None: + value = None + else: + value = f"#define {variable} {size}" + return _cmake_substitution_impl_internal(ctx.attrs.name, key, value) + +def cmake_substitution_impl(ctx: AnalysisContext) -> list[Provider]: + return _cmake_substitution_impl_internal(ctx.attrs.name, ctx.attrs.variable, ctx.attrs.value[GenericValueInfo].value) + +def cmake_immediate_substitution_impl(ctx: AnalysisContext) -> list[Provider]: + return _cmake_substitution_impl_internal(ctx.attrs.name, ctx.attrs.variable, ctx.attrs.value) diff --git a/prelude/cxx/tools/BUCK b/prelude/cxx/tools/BUCK index 71eac2495231..a087ae5a2799 100644 --- a/prelude/cxx/tools/BUCK +++ b/prelude/cxx/tools/BUCK @@ -18,6 +18,12 @@ prelude.python_bootstrap_binary( visibility = ["PUBLIC"], ) +prelude.python_bootstrap_binary( + name = "expand_cmake_template", + main = "expand_cmake_template.py", + visibility = ["PUBLIC"], +) + prelude.python_bootstrap_binary( name = "hmap_wrapper.py", main = "hmap_wrapper.py", diff --git a/prelude/cxx/tools/expand_cmake_template.py b/prelude/cxx/tools/expand_cmake_template.py new file mode 100644 index 000000000000..d1d9e1e84816 --- /dev/null +++ b/prelude/cxx/tools/expand_cmake_template.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is dual-licensed under either the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree or the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. You may select, at your option, one of the +# above-listed licenses. + +""" +A compatibility layer for cmake to buck2 migration that expands a template file with substitutions +Supports the following expansions: + - @substitution@ -> value + - #cmakedefine substitution -> #define substitution + - #cmakedefine01 substitution -> #define substitution 1 or #define substitution 0 +""" + +import argparse +import logging +from pathlib import Path + +import json +import re +import logging +import sys +from typing import Union + +at_replace = re.compile(r"(@\w+@)") +variable_replace = re.compile(r"(\${\w+})") +regex_cmakedefine = re.compile(r"#cmakedefine (\w+)(?:\s+(.+))?") +cmakedefine01 = re.compile(r"#cmakedefine01 (\w+)") + +substitutions_encountered_in_template = set() + + +def load_file(path:str) -> str: + with open(path) as fp: + return fp.read() + +def resolve_one_embedding(json_spec:dict) -> str: + result = "" + result += json_spec["prefix"] + file_contents = [ + json_spec["item_prefix"] + load_file(file) + json_spec["item_suffix"] for file in json_spec["files"] + ] + concatenated_contents = json_spec["delimiter"].join(file_contents) + if json_spec["trailing_delimiter"]: + concatenated_contents += json_spec["delimiter"] + result += concatenated_contents + + result += json_spec["suffix"] + return result + +def resolve_all_embeddings(substitutions:list[str]) -> dict[str, str]: + embedding_specs = {} + for spec_file in substitutions: + with open(spec_file) as fp: + spec = json.load(fp) + label = spec["label"] + embedding = resolve_one_embedding(spec) + embedding_specs[label] = embedding + + return embedding_specs + + + +def read_substitutions(substitution_file: str) -> dict[str, Union[int,str,bool,None]]: + substitutions = [] + with open(substitution_file) as f: + results = json.load(f) + + processed_dict = {} + for key, value in results.items(): + substitution_type = value["type"] + + if substitution_type == "subst": + substitution_value = value["value"] + elif substitution_type == "embed": + embedded_file = value["value"] + with open(embedded_file, "r") as fp: + substitution_value = fp.read() + else: + print(f"Unknown substitution type {value.type} for variable {key}") + sys.exit(1) + + print(f"substitution key {key} = {substitution_value}") + processed_dict[key] = substitution_value + return processed_dict + +def perform_substitutions( + input: list[str], + substitutions: dict[str, Union[int,str,bool,None]], + copy_only: bool, + at_replacements: bool, + var_replacements: bool) -> list[str]: + + formatted = [] + for line in input: + line = line.rstrip("\r\n") + + if not copy_only: + if at_replacements: + line = substitute_at_replace(line, substitutions) + + if var_replacements: + line = substitute_variable_replace(line, substitutions) + + line = substitute_cmakedefine(line, substitutions) + line = substitute_cmakedefine01(line, substitutions) + + formatted.append(line + "\n") + return formatted + +def get_substitution_value(key:str, substitutions: dict[str, Union[int, str, bool, None]], default=None): + substitutions_encountered_in_template.add(key) + return substitutions.get(key, default) + +def substitute_at_replace(line: str, substitutions: dict[str, Union[int, str, bool, None]]) -> str: + return re.sub(at_replace, lambda m: __at_replace_impl(m, substitutions), line) + +def __at_replace_impl(match: re.Match[str], substitutions: dict[str, Union[int, str, bool, None]]) -> str: + key = match.group(1).strip("@") + + value = get_substitution_value(key, substitutions, "") + if value: + return str(value) + return "" + + +def substitute_variable_replace(line: str, substitutions: dict[str, Union[int, str, bool, None]]) -> str: + return re.sub(variable_replace, lambda m: __variable_replace_impl(m, substitutions), line) + +def __variable_replace_impl(match: re.Match[str], substitutions: dict[str, Union[int, str, bool, None]]) -> str: + key = match.group(1) + key = key[2:-1] + + value = get_substitution_value(key, substitutions) + if value: + return str(value) + return "" + +def substitute_cmakedefine(line: str, substitutions: dict[str, Union[int, str, bool, None]]) -> str: + return re.sub(regex_cmakedefine, lambda m: __cmakedefine_impl(m, substitutions), line).rstrip() + +def _is_value_false_like(value:Union[str,int,bool,None]) -> bool: + if not value: + return True + if isinstance(value, str) and value.lower() in ['0', 'false', 'off', '']: + return True + return False + +def __cmakedefine_impl(match: re.Match[str], substitutions: dict[str, Union[int, str, bool, None]]) -> str: + groups = match.groups() + + # Support both of the following forms: + # #cmakedefine KEY + # #cmakedefine KEY VALUE + key = groups[0] + substitute_value = None + if len(groups) >= 2: + substitute_value = groups[1] + + template_value = get_substitution_value(key, substitutions) + + if _is_value_false_like(template_value): + return f"/* #undef {key} */" + + result = f"#define {key}" + if substitute_value: + result += f" {substitute_value}" + return result + +def substitute_cmakedefine01(line: str, substitutions: dict[str, Union[int, str, bool, None]]) -> str: + return re.sub(cmakedefine01, lambda m: __cmakedefine01_impl(m, substitutions), line) + +def __cmakedefine01_impl(match: re.Match[str], substitutions: dict[str, Union[int, str, bool, None]]) -> str: + key = match.group(1) + value = get_substitution_value(key, substitutions) + if _is_value_false_like(value): + return f"#define {key} 0" + else: + return f"#define {key} 1" + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-i", "--input", required=True) + parser.add_argument("-o", "--output", required=True) + parser.add_argument( + "--strict", + action='store_true', + required=False, + default=False, + help="Fail on missing & unused substitutions", + ) + + parser.add_argument( + "--substitution-file", + required=True, + default=None, + help="File containing key=value pairs to make in the template file", + ) + parser.add_argument( + "--enable-at-replacements", + required=False, + default=False, + action="store_true", + help="Perform @var@ substitution", + ) + parser.add_argument( + "--enable-var-replacements", + required=False, + default=False, + action="store_true", + help="Perform ${var} substitution", + ) + parser.add_argument( + "--copy-only", + required=False, + default=False, + action="store_true", + help="Only copy the input file to the output file", + ) + parser.add_argument( + "--log-level", + default="WARNING", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + help="Configure the logging level.", + ) + + args = parser.parse_args() + logging.getLogger().setLevel(args.log_level) + + with open(args.input) as f: + template = f.readlines() + + substitutions = read_substitutions(args.substitution_file) + + formatted = perform_substitutions( + template, + substitutions=substitutions, + at_replacements=args.enable_at_replacements, + var_replacements=args.enable_var_replacements, + copy_only=args.copy_only) + + if args.strict: + unused_substitutions = set(substitutions.keys()).difference(substitutions_encountered_in_template) + missing_substitutions = substitutions_encountered_in_template.difference(substitutions.keys()) + + fail = False + if unused_substitutions: + fail = True + print(f"Found {len(unused_substitutions)} unused substitutions:") + for s in unused_substitutions: + print(f" * {s}") + + if missing_substitutions: + fail = True + print(f"Encountered {len(missing_substitutions)} substitutions in the template file with no corresponding replacement given:") + for s in missing_substitutions: + print(f" * {s}") + + if fail: + sys.exit(1) + + if args.output is not None: + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + with open(output, "w") as f: + f.writelines(formatted) diff --git a/prelude/decls/core_rules.bzl b/prelude/decls/core_rules.bzl index f710f8af660b..e4ba67e070cd 100644 --- a/prelude/decls/core_rules.bzl +++ b/prelude/decls/core_rules.bzl @@ -14,6 +14,7 @@ load(":common.bzl", "OnDuplicateEntry", "buck", "prelude_rule", "validate_uri") load(":genrule_common.bzl", "genrule_common") load(":remote_common.bzl", "remote_common") +load("@prelude//utils:value.bzl", "GenericValueInfo") ExportFileDescriptionMode = ["reference", "copy"] @@ -494,6 +495,123 @@ filegroup = prelude_rule( ), ) + +generic_simple_value = prelude_rule( + name = "generic_simple_value", + docs = """ + A `generic_simple_value` rule encodes a single value of type boolean, int, or string. None can be specified to + indicate the non-existance of the value. This rule produces no outputs and serves only to carry values through + to other rule apis that depend on them. + """, + examples = """ + ``` + + generic_simple_value( + name="sizeof-socket_t", + value=select({ + "config//os:windows": select({ + "config//cpu:x86_64": 8, + "DEFAULT": 4 + }), + "DEFAULT": 4 + }) + ) + + ``` + """, + attrs = { + "value": attrs.option(attrs.one_of(attrs.bool(), attrs.int(), attrs.string()), default=None) + } +) + +generic_file_value = prelude_rule( + name = "generic_file_value", + docs = """ + A `generic_file_value` rule takes an artifact as input (for example a source file) and makes its content available + to other value-based rules as a dynamic input. + """, + examples = """ + ``` + + generic_file_value( + name="embedded-content", + file="embedded_content.h" + ) + + ``` + """, + attrs = { + "file": attrs.source() + } +) + +generic_value_mapping = prelude_rule( + name = "generic_value_mapping", + docs = """ + A `generic_value_mapping` rule takes a reference to a previously defined `generic_simple_value`, and maps its value + to a new value using a user-supplied lookup table. It is an error if the lookup table does not contain an entry for + the current value. + """, + examples = """ + ``` + ``` + """, + attrs = { + "original": attrs.dep(providers = [GenericValueInfo]), + "new": attrs.dict( + attrs.one_of(attrs.bool(), attrs.int(), attrs.string()), + attrs.option(attrs.one_of(attrs.bool(), attrs.int(), attrs.string()), default=None), + ) + } +) + +generic_value_join_list = prelude_rule( + name = "generic_value_join_list", + docs = """ + A 'generic_value_mapping` can combine the values of each of the input values and produce a new value that has all of + the input values concatenated together using user-specified delimiters, item prefixes, item suffixes, and global prefixes + and suffixes. If any of the inputs is a `generic_list_value`, it will be flattened (including recursively), and if any of + the values is a `generic_file_value` its content will be joined with the other items. The output of this rule is a dynamic + output artifact containing the joined content, which can then be used by other rules as if it were a generic_file_value itself. + """, + examples = """ + ``` + generic_simple_value(name="4", value=4) + generic_simple_value(name="true", value=True) + generic_file_value(name="input.txt", file="input.txt") + + + # Assuming the content of `input.txt` is "Hello world", this produces a generic value whose contents resolves to the string + # + # [(4), (True), (Hello World)] + # + generic_value_join_list( + name="joined", + values=[ + ":4", + ":true", + ":input.txt" + ], + item_prefix="(", + item_suffix=")", + prefix="[", + suffix="]", + delimiter=", " + ) + + ``` + """, + attrs = { + "values": attrs.list(attrs.dep(providers=[GenericValueInfo])), + "prefix": attrs.string(default=""), + "suffix": attrs.string(default=""), + "item_prefix": attrs.string(default=""), + "item_suffix": attrs.string(default=""), + "delimiter": attrs.string(default=","), + "trailing_delimiter": attrs.bool(default=False), + } +) + genrule = prelude_rule( name = "genrule", docs = """ @@ -1501,6 +1619,10 @@ core_rules = struct( export_file = export_file, external_test_runner = external_test_runner, filegroup = filegroup, + generic_simple_value = generic_simple_value, + generic_file_value = generic_file_value, + generic_value_mapping = generic_value_mapping, + generic_value_join_list = generic_value_join_list, genrule = genrule, http_archive = http_archive, http_file = http_file, diff --git a/prelude/decls/cxx_rules.bzl b/prelude/decls/cxx_rules.bzl index 4e9591a99ba2..1917c1575cbf 100644 --- a/prelude/decls/cxx_rules.bzl +++ b/prelude/decls/cxx_rules.bzl @@ -12,6 +12,7 @@ # well-formatted (and then delete this TODO) load("@prelude//apple:apple_common.bzl", "apple_common") +load("@prelude//cxx:cmake.bzl", "CMakeSubstitutionInfo") load("@prelude//cxx:cuda.bzl", "CudaCompileStyle") load("@prelude//cxx:headers.bzl", "CPrecompiledHeaderInfo") load("@prelude//cxx:link_groups_types.bzl", "LINK_GROUP_MAP_ATTR") @@ -21,6 +22,7 @@ load("@prelude//linking:execution_preference.bzl", "link_execution_preference_at load("@prelude//linking:link_info.bzl", "ArchiveContentsType", "LinkOrdering", "LinkStyle") load("@prelude//linking:types.bzl", "Linkage") load("@prelude//transitions:constraint_overrides.bzl", "constraint_overrides") +load("@prelude//utils:value.bzl", "GenericValueInfo") load(":common.bzl", "CxxRuntimeType", "CxxSourceType", "HeadersAsRawHeadersMode", "buck", "prelude_rule") load(":cxx_common.bzl", "cxx_common") load(":genrule_common.bzl", "genrule_common") @@ -81,6 +83,98 @@ PicType = ["pic", "pdc"] SharedLibraryInterfaceParamsType = ["disabled", "enabled", "defined_only"] +cmake_configure_file = prelude_rule( + name = "cmake_configure_file", + docs = """ + + Implements the behavior of CMake's `configure_file()` builtin. + + https://cmake.org/cmake/help/latest/command/configure_file.html + + It accepts a template file that adheres to the `configure_file` syntax specification and outputs a processed file after performing + the appropriate variable substitutions. + + It also adds some additional functionality beyond what is built into CMake's `configure_file()`. In particular, it adds two main + features: + + 1. It is by default "strict", meaning that if you pass in substitutions that are not found in the template file, the rule will fail. + Similarly, if there are variables in the template file that no user substitutions were passed in for, it will also fail. strict + mode is controlled by the `strict` rule attribute, and can be disabled. + + 2. It supports file embeddings. This is very difficult and cumbersome to do in CMake and is not supported out of the box. Here, we + support it through the `embeddings` argument, which can reference targets previously declared with the `cmake_embedding` rule. + + """, + examples = """ + ``` + + ``` + """, + attrs = { + "template": attrs.source(), + "substitutions": attrs.list(attrs.dep(providers=[CMakeSubstitutionInfo])), + "at_sub": attrs.bool(default=True), + "var_sub": attrs.bool(default=True), + "escape_quotes": attrs.bool(default=False), + "copy_only": attrs.bool(default=False), + "output": attrs.option(attrs.string(), default = None), + "strict": attrs.bool(default=True), + "script": attrs.default_only(attrs.exec_dep(default="prelude//cxx/tools:expand_cmake_template", providers=[RunInfo])) + } +) + +cmake_substitution = prelude_rule( + name = "cmake_substitution", + docs = """ + A `cmake_type_size_substitution()` is like `cmake_substitution()`, but it requires its value to be a target that was declared with the + `cctest_type_size()` rule (or any other rule that returns a `CcTestTypeSizeInfo` provider). It will result in the template variable + `${_CODE} being replaced with either `#define `, or not defind at all if the value is None. + """, + examples = """ + ``` + ``` + """, + attrs = { + "variable": attrs.option(attrs.string(), default=None), + "value": attrs.dep(providers=[GenericValueInfo]) + } +) + +cmake_immediate_substitution = prelude_rule( + name = "cmake_immediate_substitution", + docs = """ + A `cmake_immediate_substitution()` is like `cmake_substitution()`, but it requires its value to be a target that was declared with the + `cctest_type_size()` rule (or any other rule that returns a `CcTestTypeSizeInfo` provider). It will result in the template variable + `${_CODE} being replaced with either `#define `, or not defind at all if the value is None. + """, + examples = """ + ``` + ``` + """, + attrs = { + "variable": attrs.option(attrs.string(), default=None), + "value": attrs.option(attrs.one_of(attrs.string(), attrs.int(), attrs.bool()), default=None) + } +) + +cmake_type_size_substitution = prelude_rule( + name = "cmake_type_size_substitution", + docs = """ + A `cmake_type_size_substitution()` is like `cmake_substitution()`, but it requires its value to be a target that was declared with the + `cctest_type_size()` rule (or any other rule that returns a `CcTestTypeSizeInfo` provider). It will result in the template variable + `${_CODE} being replaced with either `#define `, or not defind at all if the value is None. + """, + examples = """ + ``` + ``` + """, + attrs = { + "variable": attrs.option(attrs.string(), default=None), + "size": attrs.option(attrs.dep(providers=[GenericValueInfo]), default=None), + } +) + + cxx_binary = prelude_rule( name = "cxx_binary", docs = """ @@ -1440,6 +1534,10 @@ llvm_link_bitcode = prelude_rule( ) cxx_rules = struct( + cmake_configure_file = cmake_configure_file, + cmake_type_size_substitution = cmake_type_size_substitution, + cmake_substitution = cmake_substitution, + cmake_immediate_substitution = cmake_immediate_substitution, cxx_binary = cxx_binary, cxx_genrule = cxx_genrule, cxx_library = cxx_library, @@ -1450,4 +1548,4 @@ cxx_rules = struct( prebuilt_cxx_library = prebuilt_cxx_library, prebuilt_cxx_library_group = prebuilt_cxx_library_group, llvm_link_bitcode = llvm_link_bitcode, -) +) \ No newline at end of file diff --git a/prelude/rules_impl.bzl b/prelude/rules_impl.bzl index 6434f14217b4..c4e944a74cd9 100644 --- a/prelude/rules_impl.bzl +++ b/prelude/rules_impl.bzl @@ -25,6 +25,8 @@ load("@prelude//configurations:rules.bzl", _config_extra_attributes = "extra_att load("@prelude//csharp:csharp.bzl", "csharp_library_impl", "prebuilt_dotnet_library_impl") load("@prelude//cxx:bitcode.bzl", "llvm_link_bitcode_impl") load("@prelude//cxx:cuda.bzl", "CudaCompileStyle") +load("@prelude//cxx:cmake.bzl", "cmake_configure_file_impl", "cmake_type_size_substitution_impl", "cmake_substitution_impl", "cmake_immediate_substitution_impl") +load("@prelude//utils:value.bzl", "generic_simple_value_impl", "generic_file_value_impl", "generic_list_value_impl", "generic_value_mapping_impl", "generic_value_join_list_impl") load("@prelude//cxx:cxx.bzl", "cxx_binary_impl", "cxx_library_impl", "cxx_precompiled_header_impl", "cxx_test_impl", "prebuilt_cxx_library_impl") load("@prelude//cxx:cxx_toolchain.bzl", "cxx_toolchain_extra_attributes", "cxx_toolchain_impl") load("@prelude//cxx:cxx_toolchain_types.bzl", "CxxPlatformInfo", "CxxToolchainInfo") @@ -178,12 +180,20 @@ extra_implemented_rules = struct( toolchain_alias = alias_impl, versioned_alias = versioned_alias_impl, worker_tool = worker_tool, + generic_simple_value = generic_simple_value_impl, + generic_file_value = generic_file_value_impl, + generic_value_mapping = generic_value_mapping_impl, + generic_value_join_list = generic_value_join_list_impl, #c# csharp_library = csharp_library_impl, prebuilt_dotnet_library = prebuilt_dotnet_library_impl, #c++ + cmake_configure_file = cmake_configure_file_impl, + cmake_type_size_substitution = cmake_type_size_substitution_impl, + cmake_substitution = cmake_substitution_impl, + cmake_immediate_substitution = cmake_immediate_substitution_impl, cxx_binary = cxx_binary_impl, cxx_test = cxx_test_impl, cxx_toolchain = cxx_toolchain_impl, @@ -579,4 +589,4 @@ toolchain_rule_names = [ "swift_macro_toolchain", "swift_toolchain", "toolchain_alias", -] +] \ No newline at end of file diff --git a/prelude/utils/value.bzl b/prelude/utils/value.bzl new file mode 100644 index 000000000000..d9330d0c59f6 --- /dev/null +++ b/prelude/utils/value.bzl @@ -0,0 +1,144 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is dual-licensed under either the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree or the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. You may select, at your option, one of the +# above-listed licenses. + +GenericValueInfo = provider( + fields = { + "value": provider_field(typing.Any) + } +) + +def _make_value(v) -> GenericValueInfo: + if isinstance(v, Dependency): + if GenericValueInfo in v: + return v[GenericValueInfo] + + if not isinstance(v, [int, bool, str, None, Artifact]): + actual = type(v) + fail(f"Unsupported type {actual} for generic simple value. Support types are [int, bool, str]") + return GenericValueInfo(value = v) + +def generic_simple_value_impl(ctx: AnalysisContext) -> list[Provider]: + return [ + DefaultInfo(), + _make_value(ctx.attrs.value) + ] + +def generic_list_value_impl(ctx: AnalysisContext) -> list[Provider]: + return [ + DefaultInfo(), + GenericValueInfo(value = [_make_value(v).value for v in ctx.attrs.values]) + ] + +def generic_file_value_impl(ctx: AnalysisContext) -> list[Provider]: + return [ + DefaultInfo(), + GenericValueInfo(value = ctx.attrs.file) + ] + +def generic_value_mapping_impl(ctx: AnalysisContext) -> list[Provider]: + original = ctx.attrs.original[GenericValueInfo].value + + if original not in ctx.attrs.new: + fail(f"Value {original} does not contain a mapping.") + + return [ + DefaultInfo(), + _make_value(ctx.attrs.new[original]) + ] + +def _collect_dynamic_inputs(outputs:list, inputs: list): + for v in inputs: + inner_value = v + if isinstance(inner_value, Dependency): + inner_value = v[GenericValueInfo].value + + if isinstance(inner_value, list): + _collect_dynamic_inputs(outputs, inner_value) + elif isinstance(inner_value, Artifact): + outputs.append(inner_value) + +def _get_artifact_content(artifacts, item:Artifact) -> str: + return artifacts[item].read_string() + +def _get_dependency_content(artifacts, item) -> str: + if isinstance(item, Dependency): + value = item[GenericValueInfo].value + else: + value = item + + if isinstance(value, Artifact): + return _get_artifact_content(artifacts, value) + elif isinstance(value, Dependency): + return _get_dependency_content(artifacts, value) + elif isinstance(value, GenericValueInfo): + return str(value.value) + else: + return str(value) + + +def _flatten(items:list) -> list: + result = [] + + for item in items: + if isinstance(item, list): + result += _flatten(item) + elif isinstance(item, Dependency): + result += _flatten(item[GenericValueInfo].value) + elif isinstance(item, GenericValueInfo): + result.append(item) + else: + result.append(GenericValueInfo(value=item)) + + return result + +def generic_value_join_list_impl(ctx: AnalysisContext) -> list[Provider]: + output = ctx.actions.declare_output("joined_content.txt") + + flattened_values = _flatten(ctx.attrs.values) + + def _impl(ctx, artifacts, outputs, flattened_values=flattened_values, output=output): + content = "" + if ctx.attrs.prefix: + content += ctx.attrs.prefix + + + count = len(flattened_values) + index = 0 + for value in flattened_values: + if ctx.attrs.item_prefix: + content += ctx.attrs.item_prefix + content += _get_dependency_content(artifacts, value) + if ctx.attrs.item_suffix: + content += ctx.attrs.item_suffix + + if index < (count-1) and ctx.attrs.delimiter: + content += ctx.attrs.delimiter + index += 1 + + if ctx.attrs.suffix: + content += ctx.attrs.suffix + + ctx.actions.write( + outputs[output], + content + ) + + dynamic = [] + _collect_dynamic_inputs(dynamic, flattened_values) + + ctx.actions.dynamic_output( + dynamic = dynamic, + inputs = [], + outputs = [output.as_output()], + f = _impl + ) + + return [ + DefaultInfo(default_outputs=[output]), + _make_value(output) + ]