| # 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 could have a desired | |
| set of toolchains (i.e., a C compiler, C libraries, java, python, zip, and | |
| other tools) installed. | |
| - Optionally, install more debian packages in the base container (any packages | |
| that might be needed by Bazel not installed in your container). | |
| - Optionally, install a given Bazel version on the container. | |
| - Extend the container to install sources for a project. | |
| - Run a bazel command to build one or more targets from | |
| remote repositories, inside the container. | |
| - Copy toolchain configs (outputs of remote repo targets) produced | |
| from the execution of Bazel inside the container to the host. | |
| Example: | |
| docker_toolchain_autoconfig( | |
| name = "my-autoconfig-rule", | |
| base = "@my_image//image:image.tar", | |
| bazel_version = "0.10.0", | |
| config_repos = ["local_config_cc", "<some_other_skylark_repo>"], | |
| git_repo = "https://github.com/some_git_repo", | |
| env = { | |
| ... Dictionary of env variables to configure Bazel properly | |
| for the container, see environments.bzl for examples. | |
| }, | |
| packages = [ | |
| "package_1", | |
| "package_2=version", | |
| ], | |
| # Any additional debian repos and keys needed to install packages above, | |
| # not needed if no packages are installed. | |
| additional_repos = [ | |
| "deb http://deb.debian.org/debian jessie-backports main", | |
| ], | |
| keys = [ | |
| "@some_gpg//file", | |
| ], | |
| ) | |
| Add to your WORKSPACE file the following: | |
| load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") | |
| http_archive( | |
| name = "bazel_toolchains", | |
| urls = [ | |
| "https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/archive/<latest_release>.tar.gz", | |
| "https://github.com/bazelbuild/bazel-toolchains/archive/<latest_release>.tar.gz", | |
| ], | |
| strip_prefix = "bazel-toolchains-<latest_commit>", | |
| sha256 = "<sha256>", | |
| ) | |
| load( | |
| "@bazel_toolchains//repositories:repositories.bzl", | |
| bazel_toolchains_repositories = "repositories", | |
| ) | |
| bazel_toolchains_repositories() | |
| load( | |
| "@io_bazel_rules_docker//container:container.bzl", | |
| container_repositories = "repositories", | |
| "container_pull", | |
| ) | |
| container_repositories() | |
| # Pulls the my_image used as base for example above | |
| container_pull( | |
| name = "my_image", | |
| digest = "sha256:<sha256>", | |
| registry = "<registry>", | |
| repository = "<repo>", | |
| ) | |
| # GPG file used by example above | |
| http_file( | |
| name = "some_gpg", | |
| sha256 = "<sha256>", | |
| url = "<URL>", | |
| ) | |
| For values of <latest_release> and other placeholders above, please see | |
| the WORKSPACE file in this repo. | |
| To use the rule run: | |
| bazel build //<location_of_rule>:my-autoconfig-rule | |
| Once rule finishes running the file my-autoconfig-rule_output.tar | |
| will be created with all toolchain configs generated by | |
| "local_config_cc" and "<some_other_skylark_repo>". | |
| Known issues: | |
| - 'name' of rule must conform to docker image naming standards | |
| - Rule cannot be placed in the BUILD file at the root of a project | |
| """ | |
| load( | |
| "@io_bazel_rules_docker//container:container.bzl", | |
| _container = "container", | |
| ) | |
| load("@base_images_docker//util:run.bzl", _extract = "extract") | |
| load("@bazel_toolchains//rules/container:docker_toolchains.bzl", "toolchain_container") | |
| # 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" | |
| # 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" | |
| name = ctx.attr.name | |
| # 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) | |
| if ctx.attr.repo_pkg_tar: | |
| # if package tar was used then the command should expand it | |
| repo_dir = bazel_config_dir + "/" + project_repo_dir | |
| clone_repo_cmd = ("mkdir %s && tar -xf /%s -C %s " % | |
| (repo_dir, ctx.file.repo_pkg_tar.basename, 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 and Bazel RC version is specified, install that version. | |
| bazel_url = "https://releases.bazel.build/" + ctx.attr.bazel_version | |
| if ctx.attr.bazel_rc_version: | |
| bazel_url += ("/rc" + ctx.attr.bazel_rc_version + | |
| "/bazel-" + ctx.attr.bazel_version + "rc" + | |
| ctx.attr.bazel_rc_version) | |
| else: | |
| bazel_url += "/release/bazel-" + ctx.attr.bazel_version | |
| bazel_url += "-installer-linux-x86_64.sh" | |
| 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 a tar at the root | |
| # of the container. | |
| 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 + " " + "/") | |
| copy_cmd.append("tar -cf /outputs.tar /" + " /".join(ctx.attr.config_repos)) | |
| 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_flags = "" | |
| if not ctx.attr.incompatible_changes_off: | |
| bazel_flags += " --all_incompatible_changes" | |
| bazel_cmd += " && bazel build " + bazel_flags + " " + 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 | |
| install_sh = ctx.new_file(name + "_install.sh") | |
| ctx.actions.write( | |
| output = install_sh, | |
| content = "\n ".join([ | |
| "set -ex", | |
| ctx.attr.setup_cmd, | |
| install_bazel_cmd, | |
| "echo === Cloning / expand 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, | |
| ]), | |
| ) | |
| # Include the repo_pkg_tar if needed | |
| files = [install_sh] + ctx.files._installers | |
| if ctx.attr.repo_pkg_tar: | |
| files += [ctx.file.repo_pkg_tar] | |
| image_tar = ctx.new_file(name + ".tar") | |
| # TODO(nlopezgi): fix upsream issue that output_executable is required | |
| load_image_sh_file = ctx.new_file(name + "load.sh") | |
| _container.image.implementation( | |
| ctx, | |
| files = files, | |
| output_executable = load_image_sh_file, | |
| output_tarball = image_tar, | |
| workdir = bazel_config_dir, | |
| ) | |
| _extract.implementation( | |
| ctx, | |
| name = ctx.attr.name + "_extract", | |
| image = image_tar, | |
| commands = ["/" + ctx.attr.name + "_install.sh"], | |
| extract_file = "/outputs.tar", | |
| output_file = ctx.outputs.output_tar, | |
| ) | |
| docker_toolchain_autoconfig_ = rule( | |
| attrs = _container.image.attrs + { | |
| "config_repos": attr.string_list(default = ["local_config_cc"]), | |
| "use_default_project": attr.bool(default = False), | |
| "git_repo": attr.string(), | |
| "repo_pkg_tar": attr.label(allow_files = tar_filetype, single_file = True), | |
| "bazel_version": attr.string(), | |
| "bazel_rc_version": attr.string(), | |
| "use_bazel_head": attr.bool(default = False), | |
| "setup_cmd": attr.string(default = "cd ."), | |
| "packages": attr.string_list(), | |
| "additional_repos": attr.string_list(), | |
| "keys": attr.string_list(), | |
| "incompatible_changes_off": attr.bool(default = False), | |
| "test": attr.bool(default = True), | |
| "_installers": attr.label(default = ":bazel_installers", allow_files = True), | |
| # TODO(nlopezgi): fix upstream attr declaration that is missing repo name | |
| "_extract_tpl": attr.label( | |
| default = Label("@base_images_docker//util:extract.sh.tpl"), | |
| allow_files = True, | |
| single_file = True, | |
| ), | |
| "_image_id_extractor": attr.label( | |
| default = "@io_bazel_rules_docker//contrib:extract_image_id.py", | |
| allow_files = True, | |
| single_file = True, | |
| ), | |
| }, | |
| outputs = _container.image.outputs + { | |
| "output_tar": "%{name}_outputs.tar", | |
| }, | |
| 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", | |
| # 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", | |
| ] | |
| 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. | |
| Args: | |
| **kwargs: | |
| Required Args | |
| name: A unique name for this rule. | |
| base: Docker image base - optionally with all tools pre-installed for | |
| which a configuration will be generated. Packages can also be installed | |
| by listing them in the 'packages' attriute. | |
| 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. | |
| packages: list of packages to fetch and install in the base image. | |
| additional_repos: list of additional debian package repos to use, | |
| in sources.list format. | |
| keys: list of additional gpg keys to use while downloading packages. | |
| incompatible_changes_off: If True Bazel will run without the | |
| all_incompatible_changes flag. Default False. | |
| test: a boolean which specifies whether a test target for this | |
| docker_toolchain_autoconfig will be added. | |
| If True, a test target with name {name}_test will be added. | |
| The test will build this docker_toolchain_autoconfig target, run the | |
| output script, and check the toolchain configs for the c++ auto | |
| generated config exist. | |
| """ | |
| 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.") | |
| packages_is_empty = "packages" not in kwargs or kwargs["packages"] == [] | |
| if packages_is_empty and "additional_repos" in kwargs: | |
| fail("'additional_repos' can only be specified when 'packages' is not empty.") | |
| if packages_is_empty and "keys" in kwargs: | |
| fail("'keys' can only be specified when 'packages' is not empty.") | |
| # 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", | |
| ] | |
| # Do not install packags if 'packages' is not specified or is an ampty list. | |
| if not packages_is_empty: | |
| # "additional_repos" and "keys" are optional for docker_toolchain_autoconfig, | |
| # but required for toolchain_container". Use empty lists as placeholder. | |
| if "additional_repos" not in kwargs: | |
| kwargs["additional_repos"] = [] | |
| if "keys" not in kwargs: | |
| kwargs["keys"] = [] | |
| # Install packages in the base image. | |
| toolchain_container( | |
| name = kwargs["name"] + "_image", | |
| base = kwargs["base"], | |
| packages = kwargs["packages"], | |
| additional_repos = kwargs["additional_repos"], | |
| keys = kwargs["keys"], | |
| ) | |
| # Use the image with packages installed as the new base for autoconfiguring. | |
| kwargs["base"] = ":" + kwargs["name"] + "_image.tar" | |
| if "test" in kwargs and kwargs["test"] == True: | |
| # Create a test target for the current docker_toolchain_autoconfig target, | |
| # which builds this docker_toolchain_autoconfig target, runs the output | |
| # script, and checks the toolchain configs for the c++ auto generated config | |
| # exist. | |
| native.sh_test( | |
| name = kwargs["name"] + "_test", | |
| size = "medium", | |
| timeout = "long", | |
| srcs = ["@bazel_toolchains//tests/config:autoconfig_test.sh"], | |
| data = [":" + kwargs["name"] + "_outputs.tar"], | |
| ) | |
| docker_toolchain_autoconfig_(**kwargs) |