blob: bbe3875b082bb45ad6e27852db10b262a49c14f7 [file] [log] [blame] [edit]
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
visibility("public")
_RUNFILES_HEADERS = """#!/bin/bash
# --- begin runfiles.bash initialization v3 ---
# Copy-pasted from the Bazel Bash runfiles library v3.
set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \\
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \\
source "$0.runfiles/$f" 2>/dev/null || \\
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \\
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \\
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
# --- end runfiles.bash initialization v3 ---
set -e
set -uo pipefail
"""
BASH_RUNFILES_ATTRS = dict(
_bash_runfiles = attr.label(default = "@bazel_tools//tools/bash/runfiles"),
)
BASH_RUNFILES_ATTR = attr.label(default = "@bazel_tools//tools/bash/runfiles")
def runfiles_path(ctx, file):
"""Returns a path suitable for use with the rlocation function."""
path = file.short_path
if path.startswith("../"):
# The path is not to one in the root repo. Eg, it might be @portage//...
return path.removeprefix("../")
else:
return "%s/%s" % (ctx.workspace_name, path)
def bash_rlocation(ctx, file):
"""Returns code that will generate the path of a file in bash."""
return "$(rlocation '%s')" % runfiles_path(ctx, file)
def _generate_bash_script(
ctx,
out,
content,
runfiles = None,
data = []):
ctx.actions.write(out, _RUNFILES_HEADERS + content, is_executable = True)
runfiles = runfiles or ctx.runfiles()
extra_runfiles = [ctx.attr._bash_runfiles[DefaultInfo].default_runfiles]
for target in data:
extra_runfiles.append(target[DefaultInfo].default_runfiles)
extra_runfiles.append(ctx.runfiles(files = target[DefaultInfo].files.to_list()))
return DefaultInfo(
files = depset([out]),
runfiles = runfiles.merge_all(extra_runfiles),
executable = out,
)
def _sh_runfiles_impl(ctx):
# The file is likely named name + .sh already.
out = ctx.actions.declare_file(ctx.label.name + "_generated.sh")
return _generate_bash_script(
ctx,
out,
content = "source %s" % bash_rlocation(ctx, ctx.file.src),
runfiles = ctx.runfiles(files = [ctx.file.src]),
data = ctx.attr.data,
)
_COMMON_ATTRS = dict(
_bash_runfiles = attr.label(default = "@bazel_tools//tools/bash/runfiles"),
data = attr.label_list(allow_files = True),
)
_SH_WITH_RUNFILES_ATTRS = dict(
doc = """Same as sh_binary/test, but it imports runfiles for you so you can directly call rlocation.""",
implementation = _sh_runfiles_impl,
attrs = dict(
src = attr.label(mandatory = True, allow_single_file = True),
**_COMMON_ATTRS
),
)
sh_runfiles_binary = rule(executable = True, **_SH_WITH_RUNFILES_ATTRS)
sh_runfiles_test = rule(test = True, **_SH_WITH_RUNFILES_ATTRS)
_WRITE_TO_FILE = """#!/bin/bash -e
dst="$1"
shift
echo "$@" > ${dst}
"""
def wrap_binary_with_args(ctx, out, binary, args, content_prefix = "", runfiles = None, data = []):
"""Generates a binary that runs another binary with some arguments.
Args:
out: (File) The executable to generate.
binary: (Target or File) The executable to wrap.
args: (List[str] or Args) The arguments to run it with.
content_prefix: (Optional[str]) Any code that should run before exec'ing.
runfiles: (Optional[runfiles]) Any files required to run your binary.
data: List[Target] Any deps you depend on.
Returns:
A DefaultInfo that should be able to run the binary.
"""
if type(binary) == "Target":
binary_files = binary[DefaultInfo].files.to_list()
if len(binary_files) != 1:
fail("There must be exactly one executable (got %s)" % binary_files)
exe_runfiles = binary[DefaultInfo].default_runfiles
runfiles = exe_runfiles if runfiles == None else runfiles.merge(exe_runfiles)
else:
binary_files = [binary]
if type(args) == "Args":
# You can't read args in bazel rules. So instead we write the args to a
# file and read from that file at runtime.
basename = out.basename.rsplit(".", 1)[0]
# We could define a separate executable target, but that would mean that
# users would need to add something like this attribute to their rule:
# _write_to_file = attr.label(default=Label("//bazel/bash:write_to_file"))
write_to_file = ctx.actions.declare_file(basename + "_write_to_file.sh")
ctx.actions.write(write_to_file, _WRITE_TO_FILE, is_executable = True)
args_file = ctx.actions.declare_file(basename + "_args.txt")
ctx.actions.run(
outputs = [args_file],
executable = write_to_file,
arguments = [args_file.path, args],
)
runfiles = runfiles.merge(ctx.runfiles(files = [args_file]))
args = "$(cat %s)" % bash_rlocation(ctx, args_file)
else:
new_args = []
for arg in args:
if type(arg) == "File":
new_args.append('"%s"' % (bash_rlocation(ctx, arg)))
elif type(arg) == "string":
new_args.append("'%s'" % (arg))
else:
fail("Unknown type '%s' for arg '%s'" % (type(arg), arg))
args = " ".join(new_args)
return _generate_bash_script(
ctx,
out,
content = '{content_prefix}\n\nexec "{binary}" {args} "$@"'.format(
content_prefix = content_prefix,
binary = bash_rlocation(ctx, binary_files[0]),
args = args,
),
data = data,
runfiles = runfiles,
)
def _custom_args_binary_impl(ctx):
if not ctx.attr.binary_args:
fail("The binary_args attribute is required. If you used args, please instead use binary_args. Args attribute is reserved by bazel.")
out = ctx.actions.declare_file(ctx.label.name + ".sh")
return wrap_binary_with_args(
ctx,
out = out,
binary = ctx.attr.binary,
args = ctx.attr.binary_args,
data = ctx.attr.data,
)
_CUSTOM_ARGS_ATTRS = dict(
doc = """Generates a binary that runs another binary with a custom set of args.""",
implementation = _custom_args_binary_impl,
attrs = dict(
binary = attr.label(executable = True, mandatory = True, cfg = "exec"),
binary_args = attr.string_list(),
**_COMMON_ATTRS
),
)
custom_args_binary = rule(executable = True, **_CUSTOM_ARGS_ATTRS)
custom_args_test = rule(test = True, **_CUSTOM_ARGS_ATTRS)