blob: 6cb0c444a1785c832b97d909a219554a5fca3dc0 [file] [log] [blame]
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Rules for generating toolchain configs for a Docker container.
Exposes the docker_autoconfigure rule that does the following:
- Receive a base container as main input. Base container should have a desired
set of toolchains (i.e., a C compiler, C libraries, etc) installed.
- Create a container from the base container which includes debs packages to
run Bazel.
- Extend the container to install sources for a project.
- Run a bazel command to build one or more remote repos inside the container.
- Copy toolchain configs produced from the execution of Bazel
inside the container to the host.
- Optionally copy outputs to a folder defined via build variables.
Example:
docker_toolchain_autoconfig(
name = "my-autoconfig-rule",
base = "@my_image//image:image.tar",
config_repos = ["local_config_cc", "<some_other_skylark_repo>"],
git_repo = "https://github.com/some_git_repo",
distro = "trusty", # "trusty", "xenial" and "jessie" are supported
install_tools = ["bazel"], # list of tools that are necessary to run
# autoconfig that will be installed in the container
env = {
... Dictionary of env variables to configure Bazel properly
for the container
},
)
To use the rule run:
bazel run --define=DOCKER_AUTOCONF_OUTPUT=<some_output_dir> //<location_of_rule>:my-autoconfig-rule
Once rule finishes running the file <some_output_dir>/my-autoconfig-rule.tar
will be created with all toolchain configs generated by
"local_config_cc" and "<some_other_skylark_repo>". If no value for
DOCKER_AUTOCONF_OUTPUT is passed, the resulting tar file is left in /tmp
"""
load(
"@io_bazel_rules_docker//container:container.bzl",
_container = "container",
)
load(
"@bazel_package_bundle//file:packages.bzl",
bazel_packages = "packages",
)
load(
"@jessie_package_bundle//file:packages.bzl",
jessie_packages = "packages",
)
load(
"@trusty_package_bundle//file:packages.bzl",
trusty_packages = "packages",
)
load(
"@xenial_package_bundle//file:packages.bzl",
xenial_packages = "packages",
)
load("@bazel_toolchains//skylib:packages.bzl",
"get_jessie_packages",
"get_trusty_packages",
"get_xenial_packages",
)
load(
"@bazel_toolchains//skylib:package_names.bzl",
"tool_names",
"jessie_tools",
"trusty_tools",
"xenial_tools",
)
# A couple of lists of tools that are commonly used.
# Use all_tools if your container has very little other than a base image
all_tools = [
"bazel",
"curl",
"git",
"gcc",
"java",
"python_dev",
"wget",
"zip",
]
# Use default_tools if your container has a C/C++ compiler and little else
default_tools = [
"bazel",
"curl",
"git",
"java",
"python_dev",
"wget",
"zip",
]
# External folder is set to be deprecated, lets keep it here for easy
# refactoring
# https://github.com/bazelbuild/bazel/issues/1262
_EXTERNAL_FOLDER_PREFIX = "external/"
# Name of the current workspace
_WORKSPACE_NAME = "bazel_toolchains"
_WORKSPACE_PREFIX = "@" + _WORKSPACE_NAME + "//"
# Default cc project to use if no git_repo is provided.
_DEFAULT_AUTOCONFIG_PROJECT_PKG_TAR = _WORKSPACE_PREFIX + "rules:cc-sample-project-tar"
# Build variable to define the location of the output tar produced by
# docker_toolchain_autoconfig
_DOCKER_AUTOCONF_OUTPUT = "DOCKER_AUTOCONF_OUTPUT"
# Filetype to restrict inputs
tar_filetype = [
".tar",
".tar.xz",
]
def _docker_toolchain_autoconfig_impl(ctx):
"""Implementation for the docker_toolchain_autoconfig rule.
Args:
ctx: context. See docker_toolchain_autoconfig below for details
of what this ctx must include
Returns:
null
"""
bazel_config_dir = "/bazel-config"
project_repo_dir = "project_src"
# Command to retrieve the project from github if requested.
clone_repo_cmd = "cd ."
if ctx.attr.git_repo:
clone_repo_cmd = ("cd " + bazel_config_dir + " && git clone " +
ctx.attr.git_repo + " " + project_repo_dir)
# Command to install custom Bazel version (if requested)
install_bazel_cmd = "cd ."
if ctx.attr.use_bazel_head:
# If use_bazel_head was requested, we clone the source code from github and compile
# it using the release version with "bazel build //src:bazel".
install_bazel_cmd = "/install_bazel_head.sh"
elif ctx.attr.bazel_version:
# If a specific Bazel or Bazel RC version is specified, install that version.
# We bootstrap our Bazel binary using "bazel build", and cannot use ./compile.sh as it generates
# cc binaries depending on incompatible dynamically linked libraries.
bazel_pkg_name = "bazel-" + ctx.attr.bazel_version
bazel_url = "https://releases.bazel.build/" + ctx.attr.bazel_version
if ctx.attr.bazel_rc_version:
bazel_pkg_name += "rc" + ctx.attr.bazel_rc_version
bazel_url += "/rc" + ctx.attr.bazel_rc_version
else:
bazel_url += "/release"
bazel_pkg_name += "-dist.zip"
bazel_url += "/" + bazel_pkg_name
install_bazel_cmd = "/install_bazel_version.sh " + bazel_url
# Command to recursively convert soft links to hard links in the config_repos
deref_symlinks_cmd = []
for config_repo in ctx.attr.config_repos:
symlinks_cmd = ("find $(bazel info output_base)/" +
_EXTERNAL_FOLDER_PREFIX + config_repo +
" -type l -exec bash -c 'ln -f \"$(readlink -m \"$0\")\" \"$0\"' {} \;")
deref_symlinks_cmd.append(symlinks_cmd)
deref_symlinks_cmd = " && ".join(deref_symlinks_cmd)
# Command to copy produced toolchain configs to outside of
# the containter (to the mount_point).
copy_cmd = []
for config_repo in ctx.attr.config_repos:
src_dir = "$(bazel info output_base)/" + _EXTERNAL_FOLDER_PREFIX + config_repo
copy_cmd.append("cp -dr " + src_dir + " " + bazel_config_dir + "/")
# We need to change the owner of the files we copied, so that they can
# be manipulated from outside the container.
copy_cmd.append("chown -R $USER_ID " + bazel_config_dir + "/" + config_repo)
output_copy_cmd = " && ".join(copy_cmd)
# Command to run autoconfigure targets.
bazel_cmd = "cd " + bazel_config_dir + "/" + project_repo_dir
if ctx.attr.use_default_project:
bazel_cmd += " && touch WORKSPACE && mv BUILD.sample BUILD"
# For each config repo we run the target @<config_repo>//...
bazel_targets = "@" + "//... @".join(ctx.attr.config_repos) + "//..."
bazel_cmd += " && bazel build " + bazel_targets
# Command to run to clean up after autoconfiguration.
# we start with "cd ." to make sure in case of failure everything after the
# ";" will be executed
clean_cmd = "cd . ; bazel clean"
if ctx.attr.use_default_project:
clean_cmd += " && rm WORKSPACE"
if ctx.attr.git_repo:
clean_cmd += " && cd " + bazel_config_dir + " && rm -drf " + project_repo_dir
# Full command to use for docker container
# TODO(xingao): Make sure the command exits with error right away if a sub
# command fails.
docker_cmd = [
"/bin/sh", "-c", " && ".join([
"set -ex",
ctx.attr.setup_cmd,
install_bazel_cmd,
"echo === Cloning project repo ===",
clone_repo_cmd,
"echo === Running Bazel autoconfigure command ===",
bazel_cmd,
"echo === Copying outputs ===",
deref_symlinks_cmd,
output_copy_cmd,
"echo === Cleaning up ===",
clean_cmd])]
# Expand contents of repo_pkg_tar
# (and remove them after we're done running the docker command).
# A dummy command that does nothing in case git_repo was used
expand_repo_cmd = "cd ."
remove_repo_cmd = "cd ."
if ctx.attr.repo_pkg_tar:
repo_pkg_tar = str(ctx.attr.repo_pkg_tar.label.name)
package_name = _EXTERNAL_FOLDER_PREFIX + _WORKSPACE_NAME + "/" + str(ctx.attr.repo_pkg_tar.label.package)
# Expand the tar file pointed by repo_pkg_tar
expand_repo_cmd = ("mkdir ./%s ; tar -xf %s/%s.tar -C ./%s" %
(project_repo_dir, package_name, repo_pkg_tar, project_repo_dir))
remove_repo_cmd = ("rm -drf ./%s" % project_repo_dir)
result = _container.image.implementation(ctx, cmd=docker_cmd, output=ctx.outputs.load_image)
# By default we copy the produced tar file to /tmp/
output_location = "/tmp/" + ctx.attr.name + ".tar"
if _DOCKER_AUTOCONF_OUTPUT in ctx.var:
output_location = ctx.var[_DOCKER_AUTOCONF_OUTPUT] + "/" + ctx.attr.name + ".tar"
# Create the script to load image and run it
ctx.actions.expand_template(
template = ctx.files.run_tpl[0],
substitutions ={
"%{EXPAND_REPO_CMD}": expand_repo_cmd,
"%{LOAD_IMAGE_SH}": ctx.outputs.load_image.short_path,
"%{IMAGE_NAME}": "bazel/" + ctx.label.package + ":" + ctx.label.name,
"%{RM_REPO_CMD}": remove_repo_cmd,
"%{CONFIG_REPOS}": " ".join(ctx.attr.config_repos),
"%{OUTPUT}": output_location,
},
output = ctx.outputs.executable,
is_executable = True
)
# add to the runfiles the script to load image and (if needed) the repo_pkg_tar file
runfiles = ctx.runfiles(files = result.runfiles.files.to_list() + [ctx.outputs.load_image])
if ctx.attr.repo_pkg_tar:
runfiles = ctx.runfiles(files = result.runfiles.files.to_list() +
[ctx.outputs.load_image] + ctx.files.repo_pkg_tar)
return struct(runfiles = runfiles,
files = result.files,
container_parts = result.container_parts)
docker_toolchain_autoconfig_ = rule(
attrs = _container.image.attrs + {
"distro": attr.string(),
"config_repos": attr.string_list(["local_config_cc"]),
"use_default_project": attr.bool(default = False),
"git_repo": attr.string(),
"install_tools": attr.string_list(),
"repo_pkg_tar": attr.label(allow_files = tar_filetype),
"bazel_version": attr.string(),
"bazel_rc_version": attr.string(),
"use_bazel_head": attr.bool(default = False),
"run_tpl": attr.label(allow_files = True),
"setup_cmd": attr.string(default = "cd ."),
},
executable = True,
outputs = _container.image.outputs + {
"load_image": "%{name}_load_image.sh",
},
implementation = _docker_toolchain_autoconfig_impl,
)
# Attributes below are expected in ctx, but should not be provided
# in the BUILD file.
reserved_attrs = [
"use_default_project",
"files",
"debs",
"repo_pkg_tar",
"run_tpl",
# all the attrs from docker_build we dont want users to set
"directory",
"tars",
"legacy_repository_naming",
"legacy_run_behavior",
"docker_run_flags",
"mode",
"symlinks",
"entrypoint",
"cmd",
"user",
"labels",
"ports",
"volumes",
"workdir",
"repository",
"label_files",
"label_file_strings",
"empty_files",
"build_layer",
"create_image_config",
"sha256",
"incremental_load_template",
"join_layers",
"extract_config",
]
# Attrs expected in the BUILD rule
required_attrs = [
"base",
"distro",
"install_tools",
]
def docker_toolchain_autoconfig(**kwargs):
"""Generate toolchain configs for a docker container.
This rule produces a tar file with toolchain configs produced from the
execution of targets in skylark remote repositories. Typically, this rule is
used to produce toolchain configs for the local_config_cc repository.
This repo (as well as others, depending on the project) contains generated
toolchain configs that Bazel uses to properly use a toolchain. For instance,
the local_config_cc repo generates a cc_toolchain rule.
The toolchain configs that this rule produces, can be used to, for
instance, use a remote execution service that runs actions inside docker
containers.
All the toolchain configs published in the bazel-toolchains
repo (https://github.com/bazelbuild/bazel-toolchains/) have been produced
using this rule.
This rule is implemented by extending the container_image rule in
https://github.com/bazelbuild/rules_docker. The rule installs debs packages
to run bazel (using the package manager rules offered by
https://github.com/GoogleCloudPlatform/distroless/tree/master/package_manager).
The rule creates the container with a command that pulls a repo from github,
and runs bazel build for a series of remote repos. Files generated in these
repos are copied to a mount point inside the Bazel output tree, and finally
copied to the /tmp directory or to the DOCKER_AUTOCONF_OUTPUT directory
if passed as build variable.
Args:
**kwargs:
Required Args
name: A unique name for this rule.
base: Docker image base - the container with all tools
pre-installed for which a configuration will be generated
distro: the base distro for the docker container
("jessie", "trusty", "xenial" are supported). If a different distro is used
you will need to extend this rule to add the correct packages.
install_tools: list of tools to install. The following tools
can be installed as part of this rule in the base container:
- bazel
- curl
- gcc
- git
- java
- python_dev
- wget
- zip
All tools are needed for proper execution of this rule, but
if the base container already has some of these tools installed you
should indicate so. For instance, to install all necessary tools except
git, gcc, python_dev and java use:
install_tools = ["bazel", "curl", "wget", "zip"]
Note: if container has a c/c++ compiler, do not request installation
of gcc.
Note: if the base container already has a tool installed and
installing of the tool is requested, behavior will be undefined
(e.g., an overriding package might break some other package that
depends on it in unexpected ways).
We also defined two macros for lists of tools that are commonly used.
Use all_tools if your container has very little other than a base image
all_tools = ["bazel", "curl", "git", "gcc", "java", "python_dev", "wget", "zip",]
Use default_tools if your container has a C/C++ compiler and little else
default_tools = ["bazel", "curl", "git", "java", "python_dev", "wget", "zip",]
Default Args:
config_repos: a list of remote repositories. Autoconfig will run targets in
each of these remote repositories and copy all contents to the mount
point.
env: Dictionary of env variables for Bazel / project specific autoconfigure
git_repo: A git repo with the sources for the project to be used for
autoconfigure. If no git_repo is passed, autoconfig will run with a
sample c++ project.
bazel_version: a specific version of Bazel used to generate toolchain
configs. Format: x.x.x
bazel_rc_version: a specific version of Bazel release candidate used to
generate toolchain configs. Input "2" if you would like to use rc2.
use_bazel_head = Download bazel head from github, compile it and use it
to run autoconfigure targets.
setup_cmd: a customized command that will run as the very first command
inside the docker container.
"""
for reserved in reserved_attrs:
if reserved in kwargs:
fail("reserved for internal use by docker_toolchain_autoconfig macro", attr=reserved)
for required in required_attrs:
if required not in kwargs:
fail("required for docker_toolchain_autoconfig", attr=required)
# Input validations
if "use_bazel_head" in kwargs and ("bazel_version" in kwargs or "bazel_rc_version" in kwargs):
fail ("Only one of use_bazel_head or a combination of bazel_version and" +
"bazel_rc_version can be set at a time.")
if kwargs["distro"] not in ["jessie", "trusty", "xenial"]:
fail("Only jessie, trusty and xenial distros are supported")
# If the git_repo was not provided, use the default autoconfig project
if "git_repo" not in kwargs:
kwargs["repo_pkg_tar"] = _DEFAULT_AUTOCONFIG_PROJECT_PKG_TAR
kwargs["use_default_project"] = True
kwargs["files"] = [
_WORKSPACE_PREFIX + "rules:install_bazel_head.sh",
_WORKSPACE_PREFIX + "rules:install_bazel_version.sh"
]
# Set up certificates if git or wget needs to be used
if tool_names.git in kwargs["install_tools"] or tool_names.wget in kwargs["install_tools"]:
if kwargs["distro"] == "jessie":
kwargs["tars"] = [_WORKSPACE_PREFIX + "rules:jessie_cacerts.tar"]
elif kwargs["distro"] == "trusty":
kwargs["tars"] = [_WORKSPACE_PREFIX + "rules:trusty_cacerts.tar"]
elif kwargs["distro"] == "xenial":
kwargs["tars"] = [_WORKSPACE_PREFIX + "rules:xenial_cacerts.tar"]
# Set symlinks and env vars for python / Java if installation was requested.
symlinks = {}
if tool_names.python_dev in kwargs["install_tools"]:
symlinks.update({"/usr/bin/python": "/usr/bin/python2.7"})
if tool_names.java in kwargs["install_tools"]:
symlinks.update({"/usr/bin/java": "/usr/lib/jvm/java-8-openjdk-amd64/bin/java"})
kwargs["symlinks"] = symlinks
if tool_names.java in kwargs["install_tools"]:
kwargs["env"].update({"JAVA_HOME": "/usr/lib/jvm/java-8-openjdk-amd64"})
# Set in "debs" the list with all packages we will install
debs = []
if kwargs["distro"] == "jessie":
debs = autoconf_jessie_packages(kwargs["install_tools"])
elif kwargs["distro"] == "trusty":
debs = autoconf_trusty_packages(kwargs["install_tools"])
elif kwargs["distro"] == "xenial":
debs = autoconf_xenial_packages(kwargs["install_tools"])
kwargs["debs"] = debs
# The template for the main script to execute for this rule, which produces
# the toolchain configs
kwargs["run_tpl"] = _WORKSPACE_PREFIX + "rules:docker_config.sh.tpl"
docker_toolchain_autoconfig_(**kwargs)
def autoconf_jessie_packages(install_tools):
"""Return the list of debian packages given the name of tools for Jessie.
Args:
install_tools: a list of tool names.
Returns:
a list of debian packages to install
"""
debs = []
for tool in install_tools:
if tool in jessie_tools().keys():
debs.extend(get_jessie_packages(jessie_tools()[tool]))
elif tool == tool_names.bazel:
debs.append(bazel_packages["bazel"])
else:
fail("Installation of %s was requested, but it is not supported for jessie distro" % tool)
# Remove duplicates using a depset
return depset(debs).to_list()
def autoconf_trusty_packages(install_tools):
"""Return the list of debian packages given the name of tools for Trusty.
Args:
install_tools: a list of tool names.
Returns:
a list of debian packages to install
"""
debs = []
for tool in install_tools:
if tool in trusty_tools().keys():
debs.extend(get_trusty_packages(trusty_tools()[tool]))
elif tool == tool_names.bazel:
debs.append(bazel_packages["bazel"])
else:
fail("Installation of %s was requested, but it is not supported for trusty distro" % tool)
# Remove duplicates using a depset
return depset(debs).to_list()
def autoconf_xenial_packages(install_tools):
"""Return the list of debian packages given the name of tools for Xenial.
Args:
install_tools: a list of tool names.
Returns:
a list of debian packages to install
"""
debs = []
for tool in install_tools:
if tool in xenial_tools().keys():
debs.extend(get_xenial_packages(xenial_tools()[tool]))
elif tool == tool_names.bazel:
debs.append(bazel_packages["bazel"])
else:
fail("Installation of %s was requested, but it is not supported for xenial distro" % tool)
# Remove duplicates using a depset
return depset(debs).to_list()