Skip to main content
Glama
genrule.bzl16.8 kB
# Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under both the MIT license found in the # LICENSE-MIT file in the root directory of this source tree and the Apache # License, Version 2.0 found in the LICENSE-APACHE file in the root directory # of this source tree. # Implementation of the `genrule` build rule. load("@prelude//:cache_mode.bzl", "CacheModeInfo") load("@prelude//:genrule_local_labels.bzl", "genrule_labels_require_local") load("@prelude//:genrule_prefer_local_labels.bzl", "genrule_labels_prefer_local") load("@prelude//:genrule_toolchain.bzl", "GenruleToolchainInfo") load("@prelude//:is_full_meta_repo.bzl", "is_full_meta_repo") load("@prelude//android:build_only_native_code.bzl", "is_build_only_native_code") load("@prelude//os_lookup:defs.bzl", "Os", "OsLookup") load("@prelude//utils:expect.bzl", "expect") load("@prelude//utils:utils.bzl", "flatten", "value_or") GENRULE_OUT_DIR = "out" # Currently, some rules require running from the project root, so provide an # opt-in list for those here. Longer-term, these should be ported to actual # rule implementations in v2, rather then using `genrule`s. _BUILD_ROOT_LABELS = set([ # The buck2 test suite "buck2_test_build_root", "antlir_macros", "rust_bindgen", "haskell_hsc", "cql_cxx_genrule", "clang-module", "cuda_build_root", "bundle_pch_genrule", # Compiles C++, and so need to run from build root "lpm_package", "haskell_dll", "fnlc_build", "udf_sql", "redex_genrule", # T148016945 "pxl", # T151533831 "app_modules_genrule", # produces JSON containing file paths that are read from the root dir. "android_langpack_strings", # produces JSON containing file paths that are read from the root dir. "windows_long_path_issue", # Windows: relative path length exceeds PATH_MAX, program cannot access file "flowtype_ota_safety_target", # produces JSON containing file paths that are project-relative "ctrlr_setting_paths", "llvm_buck_genrule", ]) # In Buck1 the SRCS environment variable is only set if the substring SRCS is on the command line. # That's a horrible heuristic, and doesn't account for users accessing $SRCS from a shell script. # But in some cases, $SRCS is so large it breaks the process limit, so have a label to opt in to # that behavior. _NO_SRCS_ENVIRONMENT_LABEL = "no_srcs_environment" _WINDOWS_ENV_SUBSTITUTIONS = [ # Replace $OUT and ${OUT} (regex("\\$(OUT\\b|\\{OUT\\})"), "%OUT%"), (regex("\\$(SRCDIR\\b|\\{SRCDIR\\})"), "%SRCDIR%"), (regex("\\$(SRCS\\b|\\{SRCS\\})"), "%SRCS%"), (regex("\\$(TMP\\b|\\{TMP\\})"), "%TMP%"), ] def _requires_build_root(ctx: AnalysisContext) -> bool: for label in ctx.attrs.labels: if label in _BUILD_ROOT_LABELS: return True return False def _requires_local(ctx: AnalysisContext) -> bool: return genrule_labels_require_local(ctx.attrs.labels) def _prefers_local(ctx: AnalysisContext) -> bool: return genrule_labels_prefer_local(ctx.attrs.labels) def _ignore_artifacts(ctx: AnalysisContext) -> bool: return "buck2_ignore_artifacts" in ctx.attrs.labels def _requires_no_srcs_environment(ctx: AnalysisContext) -> bool: return _NO_SRCS_ENVIRONMENT_LABEL in ctx.attrs.labels # We don't want to use cache mode in open source because the config keys that drive it aren't wired up _USE_CACHE_MODE = is_full_meta_repo() # Extra attributes required by every genrule based on genrule_impl def genrule_attributes() -> dict[str, Attr]: attributes = { "always_print_stderr": attrs.bool(default = False), "metadata_env_var": attrs.option(attrs.string(), default = None), "metadata_path": attrs.option(attrs.string(), default = None), "no_outputs_cleanup": attrs.bool(default = False), "remote_execution_dependencies": attrs.list(attrs.dict(key = attrs.string(), value = attrs.string()), default = []), "_build_only_native_code": attrs.default_only(attrs.bool(default = is_build_only_native_code())), "_genrule_toolchain": attrs.default_only(attrs.toolchain_dep(default = "toolchains//:genrule", providers = [GenruleToolchainInfo])), } if _USE_CACHE_MODE and not read_root_config("fb", "cache_mode") == None: attributes["_cache_mode"] = attrs.dep(default = read_root_config("fb", "cache_mode")) return attributes def _get_cache_mode(ctx: AnalysisContext) -> CacheModeInfo: if _USE_CACHE_MODE: return ctx.attrs._cache_mode[CacheModeInfo] else: return CacheModeInfo(allow_cache_uploads = False, cache_bust_genrules = False) def genrule_impl(ctx: AnalysisContext) -> list[Provider]: # Directories: # sh - sh file # src - sources files # out - where outputs go # `src` is the current directory # Buck1 uses `.` as output, but that won't work since # Buck2 clears the output directory before execution, and thus src/sh too. return process_genrule(ctx, ctx.attrs.out, ctx.attrs.outs) def _declare_output(ctx: AnalysisContext, path: str) -> Artifact: if path == ".": return ctx.actions.declare_output(GENRULE_OUT_DIR, dir = True) elif path.endswith("/"): return ctx.actions.declare_output(GENRULE_OUT_DIR, path[:-1], dir = True) else: return ctx.actions.declare_output(GENRULE_OUT_DIR, path) def _project_output(out: Artifact, path: str) -> Artifact: if path == ".": return out elif path.endswith("/"): return out.project(path[:-1], hide_prefix = True) else: return out.project(path, hide_prefix = True) def process_genrule( ctx: AnalysisContext, out_attr: [str, None], outs_attr: [dict, None], extra_env_vars: dict = {}, identifier: [str, None] = None, other_outputs: list[Artifact] = [], genrule_error_handler: [typing.Callable[[ActionErrorCtx], list[ActionSubError]], None] = None) -> list[Provider]: if (out_attr != None) and (outs_attr != None): fail("Only one of `out` and `outs` should be set. Got out=`%s`, outs=`%s`" % (repr(out_attr), repr(outs_attr))) local_only = _requires_local(ctx) prefer_local = _prefers_local(ctx) # NOTE: Eventually we shouldn't require local_only here, since we should be # fine with caching local fallbacks if necessary (or maybe that should be # disallowed as a matter of policy), but for now let's be safe. cacheable = value_or(ctx.attrs.cacheable, True) and (local_only or prefer_local) executable_outs = getattr(ctx.attrs, "executable_outs", None) # TODO(cjhopman): verify output paths are ".", "./", or forward-relative. if out_attr != None: out_artifact = _declare_output(ctx, out_attr) named_outputs = {} default_outputs = [out_artifact] expect(executable_outs == None, "`executable_outs` should not be set when `out` is set") elif outs_attr != None: out_artifact = ctx.actions.declare_output(GENRULE_OUT_DIR, dir = True) named_outputs = { name: [_project_output(out_artifact, path) for path in outputs] for (name, outputs) in outs_attr.items() } outs_names = outs_attr.keys() if executable_outs != None: for executable_out in executable_outs: expect(executable_out in outs_names, "Value in `executable_outs` {} is not in `outs`".format(executable_out)) default_outputs = [ _project_output(out_artifact, path) for path in (ctx.attrs.default_outs or []) ] if len(default_outputs) == 0: # We want building to force something to be built, so make sure it contains at least one artifact default_outputs = [out_artifact] else: fail("One of `out` or `outs` should be set. Got `%s`" % repr(ctx.attrs)) # Some custom rules use `process_genrule` but doesn't set this attribute. is_windows = hasattr(ctx.attrs, "_exec_os_type") and ctx.attrs._exec_os_type[OsLookup].os == Os("windows") if is_windows: path_sep = "\\" cmd = ctx.attrs.cmd_exe if ctx.attrs.cmd_exe != None else ctx.attrs.cmd if cmd == None: fail("One of `cmd` or `cmd_exe` should be set.") else: path_sep = "/" cmd = ctx.attrs.bash if ctx.attrs.bash != None else ctx.attrs.cmd if cmd == None: fail("One of `cmd` or `bash` should be set.") replace_regex = [] # For backwards compatibility with Buck1. if is_windows: for re, sub in _WINDOWS_ENV_SUBSTITUTIONS: replace_regex.append((re, sub)) for extra_env_var in extra_env_vars: replace_regex.append( (regex("\\$(%s\\b|\\{%s\\})" % (extra_env_var, extra_env_var)), "%%%s%%" % extra_env_var), ) cmd = cmd_args(cmd, ignore_artifacts = _ignore_artifacts(ctx), replace_regex = replace_regex) if type(ctx.attrs.srcs) == type([]): # FIXME: We should always use the short_path, but currently that is sometimes blank. # See fbcode//buck2/tests/targets/rules/genrule:genrule-dot-input for a test that exposes it. symlinks = {src.short_path: src for src in ctx.attrs.srcs} if len(symlinks) != len(ctx.attrs.srcs): for src in ctx.attrs.srcs: name = src.short_path if symlinks[name] != src: msg = "genrule srcs include duplicative name: `{}`. ".format(name) msg += "`{}` conflicts with `{}`".format(symlinks[name].owner, src.owner) fail(msg) else: symlinks = ctx.attrs.srcs srcs_artifact = ctx.actions.symlinked_dir("srcs" if not identifier else "{}-srcs".format(identifier), symlinks) if ctx.attrs.environment_expansion_separator: delimiter = ctx.attrs.environment_expansion_separator else: delimiter = " " # Setup environment variables. srcs = cmd_args(delimiter = delimiter) for symlink in symlinks: srcs.add(cmd_args(srcs_artifact, format = path_sep.join([".", "{}", symlink.replace("/", path_sep)]))) env_vars = { "GEN_DIR": "GEN_DIR_DEPRECATED", "OUT": out_artifact.as_output(), "SRCDIR": cmd_args(srcs_artifact, format = path_sep.join([".", "{}"])), "SRCS": srcs, } | {k: cmd_args(v) for k, v in getattr(ctx.attrs, "env", {}).items()} # RE will cache successful actions that don't produce the desired outptuts, # so if that happens and _then_ we add a local-only label, we'll get a # cache hit on the action that didn't produce the outputs and get the error # again (thus making the label useless). So, when a local-only label is # set, we make the action *different*. if local_only: env_vars["__BUCK2_LOCAL_ONLY_CACHE_BUSTER"] = "" # see comment above if prefer_local: env_vars["__BUCK2_PREFER_LOCAL_CACHE_BUSTER"] = "" # For now, when uploads are enabled, be safe and avoid sharing cache hits. cache_bust = _get_cache_mode(ctx).cache_bust_genrules if cacheable and cache_bust: env_vars["__BUCK2_ALLOW_CACHE_UPLOADS_CACHE_BUSTER"] = "" if _requires_no_srcs_environment(ctx): env_vars.pop("SRCS") for key, value in extra_env_vars.items(): env_vars[key] = value # Create required directories. if is_windows: script = [ cmd_args(srcs_artifact, format = "if not exist .\\{}\\..\\out mkdir .\\{}\\..\\out"), cmd_args("if NOT \"%TEMP%\" == \"\" set \"TMP=%TEMP%\""), ] script_extension = "bat" else: script = [ # Use a somewhat unique exit code so this can get retried on RE (T99656531). cmd_args(srcs_artifact, format = "mkdir -p ./{}/../out || exit 99"), cmd_args("export TMP=${TMPDIR:-/tmp}"), ] script_extension = "sh" # Actually define the operation, relative to where we changed to script.append(cmd) hidden = [] genrule_toolchain = ctx.attrs._genrule_toolchain[GenruleToolchainInfo] zip_scrubber = genrule_toolchain.zip_scrubber if not is_windows and zip_scrubber != None: zip_outputs = [output for output in default_outputs + flatten(named_outputs.values()) if output.extension == ".zip"] if zip_outputs: hidden.append(zip_scrubber) # Any outputs that are .zip files need to be "scrubbed" to ensure that they are deterministic. script = [ cmd_args("ORIGINAL_DIR_FOR_ZIP_SCRUBBING=$(pwd)"), ] + script + [ cmd_args('cd "$ORIGINAL_DIR_FOR_ZIP_SCRUBBING"'), ] + [ cmd_args(zip_scrubber, output, delimiter = " ", quote = "shell") for output in zip_outputs ] # Some rules need to run from the build root, but for everything else, `cd` # into the sandboxed source dir and relative all paths to that. if not _requires_build_root(ctx): srcs_dir = srcs_artifact if is_windows: rewrite_scratch_path = cmd_args( cmd_args(ctx.label.project_root, relative_to = srcs_artifact), format = 'set "BUCK_SCRATCH_PATH={}\\%BUCK_SCRATCH_PATH%"', ) else: srcs_dir = cmd_args(srcs_dir, quote = "shell") rewrite_scratch_path = cmd_args( cmd_args(ctx.label.project_root, quote = "shell", relative_to = srcs_artifact), format = "export BUCK_SCRATCH_PATH={}/$BUCK_SCRATCH_PATH", ) # Relativize all paths in the command to the sandbox dir. for script_cmd in script: script_cmd.relative_to(srcs_artifact) script = ( [ # Rewrite BUCK_SCRATCH_PATH rewrite_scratch_path, # Change to the directory that genrules expect. cmd_args(srcs_dir, format = "cd {}"), ] + script ) # Relative all paths in the env to the sandbox dir. env_vars = { key: cmd_args(value, relative_to = srcs_artifact) for key, value in env_vars.items() } if is_windows: # Should be in the beginning. # Odd, why is this a single string. How does it not end up getting quoted and being weird? script = [cmd_args("@echo off")] + script sh_script, _ = ctx.actions.write( "sh/genrule.{}".format(script_extension) if not identifier else "sh/{}-genrule.{}".format(identifier, script_extension), script, is_executable = True, allow_args = True, ) if is_windows: script_args = ["cmd.exe", "/v:off", "/c", sh_script] else: script_args = ["/usr/bin/env", "bash", "-e", sh_script] # Only set metadata arguments when they are non-null metadata_args = {} if ctx.attrs.metadata_env_var: metadata_args["metadata_env_var"] = ctx.attrs.metadata_env_var if ctx.attrs.metadata_path: metadata_args["metadata_path"] = ctx.attrs.metadata_path if ctx.attrs.remote_execution_dependencies: metadata_args["remote_execution_dependencies"] = ctx.attrs.remote_execution_dependencies category = "genrule" if ctx.attrs.type != None: # As of 09/2021, all genrule types were legal snake case if their dashes and periods were replaced with underscores. category += "_" + ctx.attrs.type.replace("-", "_").replace(".", "_") ctx.actions.run( cmd_args(script_args, hidden = [cmd, srcs_artifact, out_artifact.as_output()] + hidden), env = env_vars, local_only = local_only, prefer_local = prefer_local, weight = value_or(ctx.attrs.weight, 1), allow_cache_upload = cacheable, category = category, identifier = identifier, no_outputs_cleanup = ctx.attrs.no_outputs_cleanup, always_print_stderr = ctx.attrs.always_print_stderr, error_handler = genrule_error_handler, **metadata_args ) sub_targets = {} for (k, v) in named_outputs.items(): sub_target_providers = [DefaultInfo(default_outputs = v)] if executable_outs != None and k in executable_outs: sub_target_providers.append(RunInfo(args = cmd_args(v))) sub_targets[k] = sub_target_providers providers = [DefaultInfo( default_outputs = default_outputs, sub_targets = sub_targets, other_outputs = other_outputs, )] # The cxx_genrule also forwards here, and that doesn't have .executable, so use getattr if getattr(ctx.attrs, "executable", False): providers.append(RunInfo(args = cmd_args(default_outputs))) return providers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server