| # 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() |