blob: 10bbce5550e6c953e70e12a6d49336c442bd0859 [file] [log] [blame] [edit]
# Copyright 2022 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
load("//bazel/portage/build_defs:common.bzl", "BinaryPackageInfo", "EbuildLibraryInfo", "OverlayInfo", "OverlaySetInfo", "SDKInfo", "compute_input_file_path", "relative_path_in_package", "single_binary_package_set_info")
load("//bazel/portage/build_defs:binary_package.bzl", "add_runtime_deps")
load("//bazel/portage/build_defs:install_groups.bzl", "calculate_install_groups")
load("//bazel/portage/build_defs:interface_lib.bzl", "add_interface_library_args", "generate_interface_libraries")
load("//bazel/portage/build_defs:package_contents.bzl", "generate_contents")
load("//bazel/transitions:primordial.bzl", "primordial_transition")
load("//bazel/bash:defs.bzl", "BASH_RUNFILES_ATTR", "wrap_binary_with_args")
load("@rules_pkg//pkg:providers.bzl", "PackageArtifactInfo")
load(":install_deps.bzl", "install_deps")
# The stage1 SDK will need to be built with ebuild_primordial.
# After that, they can use the ebuild rule.
# This ensures that we don't build the stage1 targets twice.
def maybe_primordial_rule(attrs, **kwargs):
return (
rule(attrs = attrs, **kwargs),
rule(cfg = primordial_transition, attrs = dict(
_allowlist_function_transition = attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
**attrs
), **kwargs),
)
# Attributes common to the `ebuild`/`ebuild_debug`/`ebuild_test` rule.
_EBUILD_COMMON_ATTRS = dict(
ebuild = attr.label(
mandatory = True,
allow_single_file = [".ebuild"],
),
overlay = attr.label(
mandatory = True,
providers = [OverlayInfo],
doc = """
The overlay this package belongs to.
""",
),
eclasses = attr.label_list(
providers = [PackageArtifactInfo],
doc = """
The eclasses this package inherits from (including transitive ones).
""",
),
category = attr.string(
mandatory = True,
doc = """
The category of this package.
""",
),
package_name = attr.string(
mandatory = True,
doc = """
The name of this package.
""",
),
version = attr.string(
mandatory = True,
doc = """
The version of this package.
""",
),
slot = attr.string(
mandatory = True,
doc = """
The slot the package is installed to.
""",
),
distfiles = attr.label_keyed_string_dict(
allow_files = True,
),
srcs = attr.label_list(
doc = "src files used by the ebuild",
allow_files = True,
),
git_trees = attr.label_list(
doc = """
The git tree objects listed in the CROS_WORKON_TREE variable.
""",
allow_empty = True,
allow_files = True,
),
use_flags = attr.string_list(
allow_empty = True,
doc = """
The USE flags used to build the package.
""",
),
inject_use_flags = attr.bool(
default = False,
doc = """
Inject the USE flags into the container as opposed to letting portage
compute them.
""",
),
files = attr.label_list(
allow_files = True,
),
provided_runtime_deps = attr.label_list(
doc = """
The runtime deps that this package depends provided by the SDK.
""",
providers = [BinaryPackageInfo],
),
runtime_deps = attr.label_list(
providers = [BinaryPackageInfo],
),
shared_lib_deps = attr.label_list(
doc = """
The shared libraries this target will link against.
""",
providers = [EbuildLibraryInfo],
),
allow_network_access = attr.bool(
default = False,
doc = """
Allows the build process to access the network. This should be True only
when the package explicitly requests network access, e.g.
RESTRICT=network-sandbox.
""",
),
board = attr.string(
doc = """
The target board name to build the package for. If unset, then the host
will be targeted.
""",
),
sdk = attr.label(
providers = [SDKInfo],
mandatory = True,
),
overlays = attr.label(
providers = [OverlaySetInfo],
mandatory = True,
),
_action_wrapper = attr.label(
executable = True,
cfg = "exec",
default = Label("//bazel/portage/bin/action_wrapper"),
),
_install_deps = attr.label(
executable = True,
cfg = "exec",
default = Label("//bazel/portage/bin/install_deps"),
),
_build_package = attr.label(
executable = True,
cfg = "exec",
default = Label("//bazel/portage/bin/build_package"),
),
)
# TODO(b/269558613): Fix all call sites to always use runfile paths and delete `for_test`.
def _compute_build_package_args(ctx, output_path, use_runfiles):
"""
Computes the arguments to run build_package.
These arguments should be passed to action_wrapper, not build_package. They
contain the path to the build_package executable itself, and also may start
with options to action_wrapper (e.g. --privileged).
This function can be called only from `ebuild`, `ebuild_debug`, and
`ebuild_test`. Particularly, the current rule must include
_EBUILD_COMMON_ATTRS in its attribute definition.
Args:
ctx: ctx: A context objected passed to the rule implementation.
output_path: Optional[str]: A file path where an output binary package
file is saved. If None, a binary package file is not saved.
use_runfiles: bool: Whether to refer to input file paths in relative to
execroot or runfiles directory. See compute_input_file_path for
details.
Returns:
(args, inputs) where:
args: Args: Arguments to pass to action_wrapper.
inputs: Depset[File]: Inputs to action_wrapper.
"""
args = ctx.actions.args()
direct_inputs = []
transitive_inputs = []
# Define formatting functions for Args.add_all. Avoid defining them in a
# loop to avoid memory bloat.
def format_file_arg(file):
return "--file=%s=%s" % (relative_path_in_package(file), compute_input_file_path(file, use_runfiles))
def format_layer_arg(file):
return "--layer=%s" % compute_input_file_path(file, use_runfiles)
def format_git_tree_arg(file):
return "--git-tree=%s" % compute_input_file_path(file, use_runfiles)
# Path to build_package
args.add(compute_input_file_path(ctx.executable._build_package, use_runfiles))
# Basic arguments
if ctx.attr.board:
args.add("--board=" + ctx.attr.board)
if output_path:
args.add("--output=" + output_path)
# We extract the <category>/<package>/<ebuild> from the file path.
relative_ebuild_path = "/".join(ctx.file.ebuild.path.rsplit("/", 3)[1:4])
ebuild_inside_path = "%s/%s" % (ctx.attr.overlay[OverlayInfo].path, relative_ebuild_path)
# --ebuild
args.add("--ebuild=%s=%s" % (ebuild_inside_path, compute_input_file_path(ctx.file.ebuild, use_runfiles)))
direct_inputs.append(ctx.file.ebuild)
# --file
for file in ctx.attr.files:
args.add_all(file.files, map_each = format_file_arg, allow_closure = True)
transitive_inputs.append(file.files)
# --distfile
for distfile, distfile_name in ctx.attr.distfiles.items():
files = distfile.files.to_list()
if len(files) != 1:
fail("cannot refer to multi-file rule in distfiles")
file = files[0]
args.add("--distfile=%s=%s" % (distfile_name, compute_input_file_path(file, use_runfiles)))
direct_inputs.append(file)
# --layer for SDK, overlays and eclasses
sdk = ctx.attr.sdk[SDKInfo]
overlays = ctx.attr.overlays[OverlaySetInfo]
layer_inputs = sdk.layers + overlays.layers
for eclass in ctx.attr.eclasses:
layer_inputs.extend(eclass.files.to_list())
args.add_all(layer_inputs, map_each = format_layer_arg, expand_directories = False, allow_closure = True)
direct_inputs.extend(layer_inputs)
# --layer for source code
for file in ctx.files.srcs:
args.add("--layer=%s" % compute_input_file_path(file, use_runfiles))
direct_inputs.append(file)
# --git-tree
args.add_all(ctx.files.git_trees, map_each = format_git_tree_arg, allow_closure = True)
direct_inputs.extend(ctx.files.git_trees)
# --allow-network-access
if ctx.attr.allow_network_access:
args.add("--allow-network-access")
# --use-flags
if ctx.attr.inject_use_flags:
args.add_joined("--use-flags", ctx.attr.use_flags, join_with = ",")
# Consume interface libraries.
interface_library_inputs = add_interface_library_args(
input_targets = ctx.attr.shared_lib_deps,
args = args,
use_runfiles = use_runfiles,
)
transitive_inputs.append(interface_library_inputs)
# Include runfiles in the inputs.
transitive_inputs.append(ctx.attr._action_wrapper[DefaultInfo].default_runfiles.files)
transitive_inputs.append(ctx.attr._build_package[DefaultInfo].default_runfiles.files)
inputs = depset(direct_inputs, transitive = transitive_inputs)
return args, inputs
def _download_prebuilt(ctx, prebuilt, output_binary_package_file):
if prebuilt.startswith("http://") or prebuilt.startswith("https://"):
executable = "wget"
args = [prebuilt, "-O", output_binary_package_file.path]
elif prebuilt.startswith("gs://"):
executable = Label("@chromite//:src").workspace_root + "/bin/gsutil"
args = ["cp", prebuilt, output_binary_package_file.path]
else:
executable = "cp"
args = [prebuilt, output_binary_package_file.path]
ctx.actions.run(
inputs = [],
outputs = [output_binary_package_file],
executable = executable,
arguments = args,
execution_requirements = {
"no-remote": "",
"no-sandbox": "",
"requires-network": "",
},
progress_message = "Downloading %s" % prebuilt,
)
def _get_basename(ctx):
src_basename = ctx.file.ebuild.basename.rsplit(".", 1)[0]
if ctx.attr.suffix:
src_basename += ctx.attr.suffix
return src_basename
def generate_ebuild_validation_action(ctx, binpkg):
src_basename = _get_basename(ctx.rule)
validation_file = ctx.actions.declare_file(src_basename + ".validation")
args = ctx.actions.args()
args.add_all([
"validate-package",
"--touch",
validation_file,
"--package",
binpkg,
"--report-only",
])
args.add_joined("--use-flags", ctx.rule.attr.use_flags, join_with = ",", omit_if_empty = False)
ctx.actions.run(
inputs = depset([binpkg]),
outputs = [validation_file],
executable = ctx.rule.executable._xpaktool,
arguments = [args],
mnemonic = "EbuildValidation",
progress_message = "Building %{label}",
)
return validation_file
def _ebuild_impl(ctx):
src_basename = _get_basename(ctx)
# Declare outputs.
output_binary_package_file = ctx.actions.declare_file(
src_basename + ".tbz2",
)
output_log_file = ctx.actions.declare_file(src_basename + ".log")
output_profile_file = ctx.actions.declare_file(
src_basename + ".profile.json",
)
# Compute arguments and inputs to run build_package.
args, inputs = _compute_build_package_args(ctx, output_path = output_binary_package_file.path, use_runfiles = False)
# Define the main action.
prebuilt = ctx.attr.prebuilt[BuildSettingInfo].value
if prebuilt:
_download_prebuilt(ctx, prebuilt, output_binary_package_file)
ctx.actions.write(output_log_file, "Downloaded from %s\n" % prebuilt)
ctx.actions.write(output_profile_file, "[]")
else:
ctx.actions.run(
inputs = inputs,
outputs = [
output_binary_package_file,
output_log_file,
output_profile_file,
],
executable = ctx.executable._action_wrapper,
tools = [ctx.executable._build_package],
arguments = [
"--log=" + output_log_file.path,
"--profile=" + output_profile_file.path,
args,
],
execution_requirements = {
# Disable sandbox to avoid creating a symlink forest.
# This does not affect hermeticity since ebuild runs in a container.
"no-sandbox": "",
# Send SIGTERM instead of SIGKILL on user interruption.
"supports-graceful-termination": "",
},
mnemonic = "Ebuild",
progress_message = "Building %{label}",
)
# Generate contents directories.
contents = generate_contents(
ctx = ctx,
binary_package = output_binary_package_file,
output_prefix = src_basename,
board = ctx.attr.board,
executable_action_wrapper = ctx.executable._action_wrapper,
executable_extract_package = ctx.executable._extract_package,
)
# Generate interface libraries.
interface_library_outputs, interface_library_providers = generate_interface_libraries(
ctx = ctx,
input_binary_package_file = output_binary_package_file,
output_base_dir = src_basename,
headers = ctx.attr.headers,
pkg_configs = ctx.attr.pkg_configs,
shared_libs = ctx.attr.shared_libs,
static_libs = ctx.attr.static_libs,
extract_interface_executable = ctx.executable._extract_interface,
action_wrapper_executable = ctx.executable._action_wrapper,
)
# Compute provider data.
direct_runtime_deps = [
target[BinaryPackageInfo]
for target in ctx.attr.runtime_deps
]
if not ctx.attr.has_hooks:
package_info = BinaryPackageInfo(
file = output_binary_package_file,
category = ctx.attr.category,
package_name = ctx.attr.package_name,
slot = ctx.attr.slot,
version = ctx.attr.version,
direct_runtime_deps = [],
layer = None,
)
install_layers = install_deps(
ctx = ctx,
output_prefix = "%s_layer" % (ctx.attr.name),
board = ctx.attr.board,
sdk = ctx.attr.sdk[SDKInfo],
overlays = ctx.attr.overlays[OverlaySetInfo],
install_set = depset([package_info]),
strategy = "slow",
executable_action_wrapper = ctx.executable._action_wrapper,
executable_install_deps = ctx.executable._install_deps,
# Pass an invalid value as this code path is incompatible with
# fast_install_packages.
executable_fast_install_packages = None,
progress_message = "Creating layer for %{label}",
)
if len(install_layers) != 1:
fail("Expected only one layer")
install_layer = install_layers[0]
else:
install_layer = None
package_info = BinaryPackageInfo(
file = output_binary_package_file,
contents = contents,
category = ctx.attr.category,
package_name = ctx.attr.package_name,
version = ctx.attr.version,
slot = ctx.attr.slot,
direct_runtime_deps = direct_runtime_deps,
layer = install_layer,
)
package_set_info = single_binary_package_set_info(package_info)
return [
DefaultInfo(files = depset(
[output_binary_package_file, output_log_file] +
interface_library_outputs,
)),
package_info,
package_set_info,
] + interface_library_providers
ebuild, ebuild_primordial = maybe_primordial_rule(
implementation = _ebuild_impl,
doc = "Builds a Portage binary package from an ebuild file.",
attrs = dict(
headers = attr.string_list(
allow_empty = True,
doc = """
The path inside the binpkg that contains the public C headers
exported by this library.
""",
),
pkg_configs = attr.string_list(
allow_empty = True,
doc = """
The path inside the binpkg that contains the pkg-config
(man 1 pkg-config) `pc` files exported by this package.
The `pc` is used to look up the CFLAGS and LDFLAGS required to link
to the library.
""",
),
shared_libs = attr.string_list(
allow_empty = True,
doc = """
The path inside the binpkg that contains shared object libraries.
""",
),
static_libs = attr.string_list(
allow_empty = True,
doc = """
The path inside the binpkg that contains static libraries.
""",
),
suffix = attr.string(
doc = """
Suffix to add to the output file. i.e., libcxx-17.0-r15<suffix>.tbz2
""",
),
prebuilt = attr.label(providers = [BuildSettingInfo]),
has_hooks = attr.bool(
doc = """
If true, the package uses the preinst and postinst hooks to modify
ROOT or /.
""",
),
_extract_package = attr.label(
executable = True,
cfg = "exec",
default = Label("//bazel/portage/bin/extract_package"),
),
_extract_interface = attr.label(
executable = True,
cfg = "exec",
default = Label("//bazel/portage/bin/extract_interface"),
),
_xpaktool = attr.label(
executable = True,
cfg = "exec",
default = Label("//bazel/portage/bin/xpaktool"),
),
**_EBUILD_COMMON_ATTRS
),
)
_DEBUG_SCRIPT = """
# Arguments passed in during build time are passed in relative to the execroot,
# which means all files passed in are relative paths starting with bazel-out/
# Thus, we cd to the directory in our working directory containing a bazel-out.
wd="$(pwd)"
cd "${wd%%/bazel-out/*}"
# The runfiles manifest file contains relative paths, which are evaluated
# relative to the working directory. Since we provide our own working directory,
# we need to use the RUNFILES_DIR instead.
export RUNFILES_DIR="${RUNFILES_MANIFEST_FILE%_manifest}"
unset RUNFILES_MANIFEST_FILE
"""
def _ebuild_debug_impl(ctx):
src_basename = ctx.file.ebuild.basename.rsplit(".", 1)[0]
# Declare outputs.
output_debug_script = ctx.actions.declare_file(src_basename + "_debug.sh")
# Compute arguments and inputs to run build_package.
# While we include all relevant input files in the wrapper script's
# runfiles, we embed execroot paths in the script, not runfiles paths, so
# that the debug invocation is closer to the real build execution.
args, inputs = _compute_build_package_args(ctx, output_path = None, use_runfiles = False)
# An interactive run will make --login default to after.
# The user can still explicitly set --login=before if they wish.
args.add("--interactive")
return wrap_binary_with_args(
ctx,
out = output_debug_script,
binary = ctx.executable._action_wrapper,
args = args,
content_prefix = _DEBUG_SCRIPT,
runfiles = ctx.runfiles(transitive_files = inputs),
)
# TODO(b/298889830): Remove this rule once chromite starts using install_list.
ebuild_debug, ebuild_debug_primordial = maybe_primordial_rule(
implementation = _ebuild_debug_impl,
executable = True,
doc = "Enters the ephemeral chroot to build a Portage binary package in.",
attrs = dict(
_bash_runfiles = BASH_RUNFILES_ATTR,
**_EBUILD_COMMON_ATTRS
),
)
_INSTALL_SCRIPT_HEADER = """#!/bin/bash
set -ue
if [[ ! -e /etc/cros_chroot_version ]]; then
echo "Cannot run outside the cros SDK chroot."
exit 1
fi
# Arguments passed in during build time are passed in relative to the execroot,
# which means all files passed in are relative paths starting with bazel-out/
# Thus, we cd to the directory in our working directory containing a bazel-out.
wd="$(pwd)"
cd "${wd%%/bazel-out/*}"
"""
def _ebuild_install_impl(ctx):
src_basename = ctx.file.ebuild.basename.rsplit(".", 1)[0]
# Generate script.
script_contents = _INSTALL_SCRIPT_HEADER
# Add script to copy binary packages to the PKGDIR.
for package in ctx.attr.packages:
info = package[BinaryPackageInfo]
dest_dir = "/build/%s/packages/%s/" % (ctx.attr.board, info.category)
dest_path = "%s/%s" % (dest_dir, info.file.basename)
script_contents += """
sudo mkdir -p "%s"
sudo cp "%s" "%s"
sudo chmod 644 "%s"
""" % (dest_dir, info.file.path, dest_path, dest_path)
# Add script to install binary packages.
install_groups = calculate_install_groups(
[package[BinaryPackageInfo] for package in ctx.attr.packages],
provided_packages = depset(),
use_layers = False,
)
for install_group in install_groups:
atoms = [
"=%s/%s" % (info.category, info.file.basename.rsplit(".", 1)[0])
for info in install_group
]
script_contents += "emerge-%s --usepkgonly --nodeps --jobs %s\n" % (
ctx.attr.board,
" ".join(atoms),
)
# Write script.
output_install_script = ctx.actions.declare_file(src_basename +
"_install.sh")
ctx.actions.write(
output_install_script,
script_contents,
is_executable = True,
)
runfiles = ctx.runfiles(files = [
package[BinaryPackageInfo].file
for package in ctx.attr.packages
])
return DefaultInfo(
executable = output_install_script,
runfiles = runfiles,
)
ebuild_install, ebuild_install_primordial = maybe_primordial_rule(
implementation = _ebuild_install_impl,
executable = True,
doc = "Installs the package to the environment.",
attrs = dict(
ebuild = attr.label(
mandatory = True,
allow_single_file = [".ebuild"],
),
category = attr.string(
mandatory = True,
doc = """
The category name of the package.
""",
),
board = attr.string(
mandatory = True,
doc = """
The target board name to build the package for.
""",
),
packages = attr.label_list(
providers = [BinaryPackageInfo],
),
),
)
def _ebuild_install_list_impl(ctx):
src_basename = ctx.file.ebuild.basename.rsplit(".", 1)[0]
packages = []
for package in ctx.attr.packages:
info = package[BinaryPackageInfo]
name = "%s/%s" % (info.category, info.file.basename)
path = info.file.path
deps = ["%s/%s" % (dep.category, dep.file.basename) for dep in info.direct_runtime_deps]
packages.append("""{
"name": "%s",
"path": "%s",
"deps": [%s]
}""" % (name, path, ",".join(["\"%s\"" % dep for dep in deps])))
contents = "[%s]" % ",".join(packages)
output = ctx.actions.declare_file(src_basename + "_install_list.json")
ctx.actions.write(
output,
contents,
)
runfiles = ctx.runfiles(files = [
package[BinaryPackageInfo].file
for package in ctx.attr.packages
])
return DefaultInfo(
files = depset([output]),
runfiles = runfiles,
)
ebuild_install_list, ebuild_install_list_primordial = maybe_primordial_rule(
implementation = _ebuild_install_list_impl,
doc = "Generates a JSON file which contains necessary info to install the package to the environment.",
attrs = dict(
ebuild = attr.label(
mandatory = True,
allow_single_file = [".ebuild"],
),
category = attr.string(
mandatory = True,
doc = """
The category name of the package.
""",
),
board = attr.string(
mandatory = True,
doc = """
The target board name to build the package for.
""",
),
packages = attr.label_list(
providers = [BinaryPackageInfo],
),
),
)
def _ebuild_test_impl(ctx):
src_basename = ctx.file.ebuild.basename.rsplit(".", 1)[0]
# Declare outputs.
output_runner_script = ctx.actions.declare_file(src_basename + "_test.sh")
# Compute arguments and inputs to run build_package.
args, inputs = _compute_build_package_args(ctx, output_path = None, use_runfiles = True)
args.add("--test")
return wrap_binary_with_args(
ctx,
out = output_runner_script,
binary = ctx.executable._action_wrapper,
args = args,
runfiles = ctx.runfiles(transitive_files = inputs),
)
ebuild_test, ebuild_primordial_test = maybe_primordial_rule(
implementation = _ebuild_test_impl,
doc = "Runs ebuild tests.",
attrs = dict(
_bash_runfiles = BASH_RUNFILES_ATTR,
**_EBUILD_COMMON_ATTRS
),
test = True,
)
def _ebuild_compare_package_test_impl(ctx):
if len(ctx.attr.packages) != 2:
fail("Expected two packages, got %d" % (len(ctx.attr.packages)))
inputs = [
package[BinaryPackageInfo].file
for package in ctx.attr.packages
]
args = ["compare-packages"]
for file in inputs:
args.append(file)
return wrap_binary_with_args(
ctx,
out = ctx.outputs.executable,
binary = ctx.attr._xpaktool,
args = args,
content_prefix = "export RUST_BACKTRACE=1",
runfiles = ctx.runfiles(transitive_files = depset(inputs)),
)
ebuild_compare_package_test, ebuild_compare_package_primordial_test = maybe_primordial_rule(
implementation = _ebuild_compare_package_test_impl,
doc = """
Compares two binary packages and ensures they are identical. This test is
helpful to ensure that ebuild outputs are hermetic.
Unfortunately this test can't guarantee that two hosts will produce the same
binary package. i.e., The `make -j <cores>` might get logged into the ebuild
environment file which is build machine specific.
""",
attrs = {
"packages": attr.label_list(
doc = "The two binary packages to compare.",
providers = [BinaryPackageInfo],
mandatory = True,
),
"_xpaktool": attr.label(
executable = True,
cfg = "exec",
default = Label("//bazel/portage/bin/xpaktool"),
),
"_bash_runfiles": BASH_RUNFILES_ATTR,
},
test = True,
)