command_alias.bzl•8.69 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.
load("@prelude//os_lookup:defs.bzl", "Os", "OsLookup", "ScriptLanguage")
load("@prelude//utils:arglike.bzl", "ArgLike") # @unused: Used as a type
def command_alias_impl(ctx: AnalysisContext):
target_os = ctx.attrs._target_os_type[OsLookup]
if target_os.os == Os("fat_mac_linux") and len(ctx.attrs.platform_exe) > 0:
base = {
"Darwin": _get_os_base(ctx, Os("macos")),
"Linux": _get_os_base(ctx, Os("linux")),
}
else:
base = _get_os_base(ctx, target_os.os)
output = command_alias(
ctx,
"__command_alias_trampoline",
target_os,
base,
cmd_args(ctx.attrs.args),
ctx.attrs.env,
)
default_info = DefaultInfo(
default_output = output.output.default_outputs[0],
other_outputs = list(output.output.other_outputs) + ctx.attrs.resources,
)
# FIXME(JakobDegen): We should not accept `platform_exe` as meaning `run_using_single_arg`, but
# there are things that depend on that
#
# FIXME(JakobDegen): It's easy to end up depending on either one of these behaviors. Life would
# probably be easier if we just always went the `output.cmd` route
if output.maybe_directly_runnable == None or \
ctx.attrs.run_using_single_arg or \
(len(ctx.attrs.platform_exe) > 0 and target_os.script == ScriptLanguage("sh")):
run_info = RunInfo(args = output.cmd)
else:
run_info = RunInfo(args = output.maybe_directly_runnable)
run_info_with_resources = RunInfo(
args = cmd_args(run_info, hidden = ctx.attrs.resources),
)
return [default_info, run_info_with_resources]
def _get_os_base(ctx: AnalysisContext, os: Os) -> RunInfo:
exe = ctx.attrs.platform_exe.get(os.value)
if exe == None:
exe = ctx.attrs.exe
if exe == None:
return RunInfo()
if isinstance(exe, Artifact):
return RunInfo(args = cmd_args(exe))
run_info = exe.get(RunInfo)
if run_info == None:
run_info = RunInfo(
args = exe[DefaultInfo].default_outputs,
)
return run_info
CommandAliasOutput = record(
# A batch or sh script representing the command alias
#
# `DefaultInfo` instead of `Artifact` because the `other_outputs` will usually need to be
# included as hidden somewhere
output = DefaultInfo,
# The output wrapped into a `cmd_args` in the obvious way
cmd = cmd_args,
# For some values of the arguments to `command_alias` it is possible to represent the
# command_alias entirely within a `cmd_args`, without needing to write out a script. If that is
# the case, those args are made available here.
maybe_directly_runnable = cmd_args | None,
)
def command_alias(
ctx: AnalysisContext,
# The path at which to write the output to, without an extension - that will be added
path: str,
# The target where this script should be able to run (this may actually be your exec platform)
target_os: OsLookup,
# Either the `RunInfo` to use, or in the case of a fat platform, the choice of `RunInfo`
# depending on `uname`
base: RunInfo | dict[str, RunInfo],
args: cmd_args,
env: dict[str, ArgLike]) -> CommandAliasOutput:
if target_os.script == ScriptLanguage("sh"):
trampoline, hidden = _command_alias_write_trampoline_unix(ctx, path + ".sh", base, args, env)
elif target_os.script == ScriptLanguage("bat"):
trampoline, hidden = _command_alias_write_trampoline_windows(ctx, path + ".bat", base, args, env)
else:
fail("Unsupported script language: {}".format(target_os.script))
if len(env) > 0 or isinstance(base, dict):
maybe_directly_runnable = None
else:
maybe_directly_runnable = cmd_args(base.args, args)
return CommandAliasOutput(
output = DefaultInfo(
default_output = trampoline,
other_outputs = [hidden],
),
cmd = cmd_args(trampoline, hidden = hidden),
maybe_directly_runnable = maybe_directly_runnable,
)
def _command_alias_write_trampoline_unix(
ctx: AnalysisContext,
path: str,
base: RunInfo | dict[str, RunInfo],
args: cmd_args,
env: dict[str, ArgLike]) -> (Artifact, cmd_args):
trampoline_args = cmd_args()
trampoline_args.add("#!/usr/bin/env bash")
trampoline_args.add("set -euo pipefail")
if isinstance(base, dict):
trampoline_args.add('case "$(uname)" in')
for uname, run_info in base.items():
trampoline_args.add(" {})".format(uname))
_add_args_declaration_to_trampoline_args(trampoline_args, run_info, args)
trampoline_args.add(" ;;")
trampoline_args.add("esac")
else:
_add_args_declaration_to_trampoline_args(trampoline_args, base, args)
# We can't use cwd relative paths (since we don't know the cwd when this script is run) and so
# we instead use paths relative to the script itself. However, we can't just naively stick a
# `$(...)` into `absolute_prefix`, since we must also shell quote paths, and that expression
# would be shell quoted.
#
# Instead, we use `BUCK_COMMAND_ALIAS_ABSOLUTE_PREFIX/`, verbatim, as an absolute prefix on the
# cmd_args, and then replace that with the actual path of the script at runtime
trampoline_args.add(
"""
BASE=$(cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P)
R_ARGS=()
for arg in "${ARGS[@]}"; do
R_ARGS+=("${arg//BUCK_COMMAND_ALIAS_ABSOLUTE_PREFIX/$BASE}")
done
""",
)
for (k, v) in env.items():
# TODO(akozhevnikov): maybe check environment variable is not conflicting with pre-existing one
trampoline_args.add(cmd_args("export ", k, "=", cmd_args(v, quote = "shell"), delimiter = ""))
trampoline_args.add(cmd_args("export ", k, '="${', k, '//BUCK_COMMAND_ALIAS_ABSOLUTE_PREFIX/$BASE}"', delimiter = ""))
trampoline_args.add('exec "${R_ARGS[@]}" "$@"')
trampoline = ctx.actions.declare_output(path)
trampoline_args = cmd_args(
trampoline_args,
relative_to = (trampoline, 1),
absolute_prefix = "BUCK_COMMAND_ALIAS_ABSOLUTE_PREFIX/",
)
ctx.actions.write(
trampoline.as_output(),
trampoline_args,
allow_args = True,
is_executable = True,
)
return trampoline, trampoline_args
def _command_alias_write_trampoline_windows(
ctx: AnalysisContext,
path: str,
base: RunInfo,
args: cmd_args,
env: dict[str, ArgLike]) -> (Artifact, cmd_args):
trampoline_args = cmd_args()
trampoline_args.add("@echo off")
if "close_stdin" in ctx.attrs.labels:
# Avoids waiting for input on the "Terminate batch job (Y/N)?" prompt.
# The prompt itself is unavoidable, but we can avoid having to wait for input.
# This will call the same trampoline batch file with stdin disabled
trampoline_args.add("if not defined STDIN_CLOSED (set STDIN_CLOSED=1 & CALL <NUL %0 %* & GOTO :EOF)")
# Set BUCK_COMMAND_ALIAS_ABSOLUTE to the drive and full path of the script being created here
# We use this below to prefix any artifacts being referenced in the script
trampoline_args.add("set BUCK_COMMAND_ALIAS_ABSOLUTE=%~dp0")
# Handle envs
for (k, v) in env.items():
# TODO(akozhevnikov): maybe check environment variable is not conflicting with pre-existing one
trampoline_args.add(cmd_args(["set ", k, "=", v], delimiter = ""))
# FIXME(JakobDegen): This should be batch quoting, not shell quoting
cmd = cmd_args(cmd_args(base.args, args, quote = "shell"), "%*", delimiter = " ")
trampoline_args.add(cmd)
trampoline = ctx.actions.declare_output(path)
trampoline_args = cmd_args(
trampoline_args,
relative_to = (trampoline, 1),
absolute_prefix = "%BUCK_COMMAND_ALIAS_ABSOLUTE%/",
)
ctx.actions.write(
trampoline.as_output(),
trampoline_args,
allow_args = True,
is_executable = True,
)
return trampoline, trampoline_args
def _add_args_declaration_to_trampoline_args(trampoline_args: cmd_args, base: RunInfo, args: cmd_args):
trampoline_args.add("ARGS=(")
trampoline_args.add(cmd_args(base.args, args, quote = "shell"))
trampoline_args.add(")")