blob: 6410d311a871b10c7d6d996455a52ef64aee907a [file] [log] [blame]
# Copyright 2015 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.
# Some definition to setup jenkins and build the corresponding docker images
load("@bazel_tools//tools/build_defs/docker:docker.bzl", "docker_build")
load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar")
JENKINS_PORT = 80
JENKINS_HOST = "jenkins"
MAILS_SUBSTITUTIONS = {
"BAZEL_BUILD_RECIPIENT": "bazel-ci@googlegroups.com",
"BAZEL_RELEASE_RECIPIENT": "bazel-discuss+release@googlegroups.com",
"SENDER_EMAIL": "noreply@bazel.io",
}
def expand_template_impl(ctx):
"""Simply spawn the template-engine in a rule."""
variables = [
"--variable=%s=%s" % (k, ctx.attr.substitutions[k])
for k in ctx.attr.substitutions
]
imports = [
"--imports=%s=%s" % (ctx.attr.deps[i].label, ctx.files.deps[i].path)
for i in range(0, len(ctx.attr.deps))
]
ctx.action(
executable = ctx.executable._engine,
arguments = [
"--executable" if ctx.attr.executable else "--noexecutable",
"--template=%s" % ctx.file.template.path,
"--output=%s" % ctx.outputs.out.path,
] + variables + imports,
inputs = [ctx.file.template] + ctx.files.deps,
outputs = [ctx.outputs.out],
)
expand_template = rule(
attrs = {
"template": attr.label(
mandatory = True,
allow_files = True,
single_file = True,
),
"deps": attr.label_list(default = [], allow_files = True),
"substitutions": attr.string_dict(mandatory = True),
"out": attr.output(mandatory = True),
"executable": attr.bool(default = True),
"_engine": attr.label(
default = Label("//templating:template_engine"),
executable = True,
cfg="host"),
},
implementation = expand_template_impl,
)
def _dest_path(f, strip_prefixes):
"""Returns the short path of f, stripped of strip_prefix."""
for strip_prefix in strip_prefixes:
if f.short_path.startswith(strip_prefix):
return f.short_path[len(strip_prefix):]
return f.short_path
def _format_path(path_format, path):
dirsep = path.rfind("/")
dirname = path[:dirsep] if dirsep > 0 else ""
basename = path[dirsep+1:] if dirsep > 0 else path
extsep = basename.rfind(".")
extension = basename[extsep+1:] if extsep > 0 else ""
basename = basename[:extsep] if extsep > 0 else basename
flavor = ""
if basename.endswith("-staging"):
basename = basename[:-8]
flavor = "staging"
elif basename.endswith("-test"):
basename = basename[:-5]
flavor = "test"
return path_format.format(
path=path,
dirname=dirname,
basename=basename,
flavor=flavor,
extension=extension
)
def _append_inputs(args, inputs, f, path, path_format):
args.append("--file=%s=%s" % (
f.path,
_format_path(path_format, path)
))
inputs.append(f)
def _merge_files_impl(ctx):
"""Merge a list of config files in a tar ball with the correct layout."""
output = ctx.outputs.out
build_tar = ctx.executable._build_tar
inputs = []
args = [
"--output=" + output.path,
"--directory=" + ctx.attr.directory,
"--mode=0644",
]
variables = [
"--variable=%s=%s" % (k, ctx.attr.substitutions[k])
for k in ctx.attr.substitutions
]
for f in ctx.files.srcs:
path = _dest_path(f, ctx.attr.strip_prefixes)
if path.endswith(".tpl"):
path = path[:-4]
f2 = ctx.new_file(ctx.label.name + "/" + path)
ctx.action(
executable = ctx.executable._engine,
arguments = [
"--template=%s" % f.path,
"--output=%s" % f2.path,
"--noescape_xml",
] + variables,
inputs = [f],
outputs = [f2],
)
_append_inputs(args, inputs, f2, path, ctx.attr.path_format)
else:
_append_inputs(args, inputs, f, path, ctx.attr.path_format)
ctx.action(
executable = build_tar,
arguments = args,
inputs = inputs,
outputs = [output],
mnemonic="MergeFiles"
)
_merge_files = rule(
attrs = {
"srcs": attr.label_list(allow_files=True),
"template_extension": attr.string(default=".tpl"),
"directory": attr.string(default="/"),
"strip_prefixes": attr.string_list(default=[]),
"substitutions": attr.string_dict(default={}),
"path_format": attr.string(default="{path}"),
"_build_tar": attr.label(
default=Label("@bazel_tools//tools/build_defs/pkg:build_tar"),
cfg="host",
executable=True,
allow_files=True),
"_engine": attr.label(
cfg="host",
default = Label("//templating:template_engine"),
executable = True),
},
outputs = {"out": "%{name}.tar"},
implementation = _merge_files_impl,
)
def jenkins_job(name, config, substitutions = {}, deps = [],
project='bazel', org='bazelbuild', git_url=None, project_url=None,
platforms=[], test_platforms=["linux-x86_64"]):
"""Create a job configuration on Jenkins.
Args:
name: the name of the job to create
config: the configuration file for the job
substitutions: additional substitutions to pass to the template generation
deps: list of dependencies (templates included by the config file)
project: the project name on github
org: the project organization on github, default 'bazelbuild'
git_url: the URL to the git project, defaulted to the Github URL
project_url: the project url, defaulted to the Git URL
platforms: platforms on which to run that job, default None,
test_platforms: platforms on which to run that job when inside of a
dockerized test, by default only 'linux-x86_64'
"""
github_project = "%s/%s" % (org, project.lower())
github_url = "https://github.com/" + github_project
if not git_url:
git_url = github_url
if not project_url:
project_url = git_url
substitutions = substitutions + {
"GITHUB_URL": github_url,
"GIT_URL": git_url,
"GITHUB_PROJECT": github_project,
"PROJECT_URL": project_url,
"PLATFORMS": "\n".join(platforms),
} + MAILS_SUBSTITUTIONS
substitutions["SEND_EMAIL"] = "1"
expand_template(
name = name,
template = config,
out = "%s.xml" % name,
deps = deps,
substitutions = substitutions,
)
substitutions["SEND_EMAIL"] = "0"
expand_template(
name = name + "-staging",
template = config,
out = "%s-staging.xml" % name,
deps = deps,
substitutions = substitutions,
)
if test_platforms:
substitutions["PLATFORMS"] = "\n".join(test_platforms)
expand_template(
name = name + "-test",
template = config,
out = "%s-test.xml" % name,
deps = deps,
substitutions = substitutions,
)
def bazel_git_job(**kwargs):
"""Override bazel_github_job to test a project that is not on GitHub."""
kwargs["github_enabled"] = False
if not "git_url" in kwargs:
if not "project_url" in kwargs:
fail("Neither project_url nor git_url was specified")
kwargs["git_url"] = kwargs
bazel_github_job(**kwargs)
def bazel_github_job(name, platforms=[], branch="master", project=None, org="google",
project_url=None, workspace=".", configure=[], git_url=None,
bazel_versions=["HEAD", "latest"],
tests=["//..."], targets=["//..."], substitutions={},
windows_configure=[],
windows_tests=["//..."], windows_targets=["//..."],
windows_tests_msys=["//..."], windows_targets_msys=["//..."],
test_opts=["--test_output=errors", "--build_tests_only"],
test_tag_filters=["-noci", "-manual"],
build_opts=["--verbose_failures"],
test_platforms=["linux-x86_64"],
enable_trigger=True,
gerrit_project=None,
enabled=True,
pr_enabled=True,
github_enabled=True,
run_sequential=False,
sauce_enabled=False):
"""Create a generic github job configuration to build against Bazel head."""
if not project:
project = name
substitutions = substitutions + {
"WORKSPACE": workspace,
"PROJECT_NAME": project,
"BRANCH": branch,
"CONFIGURE": "\n".join(configure),
"WINDOWS_CONFIGURE": "\n".join(windows_configure),
"TEST_OPTS": " ".join(test_opts),
"TEST_TAG_FILTERS": ",".join(test_tag_filters),
"BUILD_OPTS": " ".join(build_opts),
"TESTS": " + ".join(tests),
"WINDOWS_TESTS": " ".join(windows_tests),
# TODO(pcloudy): remove *_MSYS attributes when we don't need MSYS anymore
"WINDOWS_TESTS_MSYS": " ".join(windows_tests_msys),
"BUILDS": " ".join(targets),
"WINDOWS_BUILDS": " ".join(windows_targets),
"WINDOWS_BUILDS_MSYS": " ".join(windows_targets_msys),
"BAZEL_VERSIONS": "\n".join(bazel_versions),
"disabled": str(not enabled).lower(),
"enable_trigger": str(enable_trigger and github_enabled).lower(),
"github": str(github_enabled),
"GERRIT_PROJECT": str(gerrit_project),
"RUN_SEQUENTIAL": str(run_sequential).lower(),
"SAUCE_ENABLED": str(sauce_enabled).lower(),
}
jenkins_job(
name = name,
config = "//jenkins:github-jobs.xml.tpl",
deps = [
"//jenkins:github-jobs.sh.tpl",
"//jenkins:github-jobs.bat.tpl",
"//jenkins:github-jobs.test-logs.sh.tpl",
"//jenkins:github-jobs.test-logs.bat.tpl",
],
substitutions=substitutions,
git_url=git_url,
project=project,
org=org,
project_url=project_url,
platforms=platforms,
test_platforms=test_platforms)
substitutions["BAZEL_VERSIONS"] = "\n".join([
v for v in bazel_versions if not v.startswith("HEAD")])
if pr_enabled:
jenkins_job(
name = "PR-" + name,
config = "//jenkins:github-jobs-PR.xml.tpl",
deps = [
"//jenkins:github-jobs.sh.tpl",
"//jenkins:github-jobs.bat.tpl",
"//jenkins:github-jobs.test-logs.sh.tpl",
"//jenkins:github-jobs.test-logs.bat.tpl",
],
substitutions=substitutions,
project=project,
org=org,
project_url=project_url,
platforms=platforms,
test_platforms=test_platforms)
if gerrit_project != None:
jenkins_job(
name = "Gerrit-" + name,
config = "//jenkins:github-jobs-Gerrit.xml.tpl",
deps = [
"//jenkins:github-jobs.sh.tpl",
"//jenkins:github-jobs.bat.tpl",
"//jenkins:github-jobs.test-logs.sh.tpl",
"//jenkins:github-jobs.test-logs.bat.tpl",
],
substitutions=substitutions,
project=project,
org=org,
project_url=project_url,
platforms=platforms,
test_platforms=test_platforms)
def jenkins_node(name, remote_fs = "/home/ci", num_executors = 1, mode = "NORMAL",
labels = [], docker_base = None, preference = 1,
visibility = None):
"""Create a node configuration on Jenkins, with possible docker image.
Args:
name: Name of the node on Jenkins.
remote_fs: path to the home of the Jenkins user.
num_executors: number of executors (i.e. concurrent build) this machine can have.
mode: NORMAL for "Utilize this node as much as possible"
EXCLUSIVE for "Only build jobs with label restrictions matching this node"
labels: list of Jenkins labels for this node (the node name is always added).
docker_base: base for the corresponding docker image to create if we should create one
(if docker_base is not specified, then a corresponding machine should be configured
to connect to the Jenkins master).
preference: A preference factor, if a node as a factor of 1 and another a factor of
4, then the second one will be scheduled 4 time more jobs than the first one.
visibility: rule visibility.
"""
native.genrule(
name = name,
cmd = """cat >$@ <<'EOF'
<?xml version='1.0' encoding='UTF-8'?>
<slave>
<name>%s</name>
<description></description>
<remoteFS>%s</remoteFS>
<numExecutors>%s</numExecutors>
<mode>%s</mode>
<retentionStrategy class="hudson.slaves.RetentionStrategy$$Always"/>
<launcher class="hudson.slaves.JNLPLauncher"/>
<label>%s</label>
<nodeProperties>
<jp.ikedam.jenkins.plugins.scoringloadbalancer.preferences.BuildPreferenceNodeProperty plugin="scoring-load-balancer@1.0.1">
<preference>%s</preference>
</jp.ikedam.jenkins.plugins.scoringloadbalancer.preferences.BuildPreferenceNodeProperty>
</nodeProperties>
</slave>
EOF
""" % (name, remote_fs, num_executors, mode, " ".join([name] + labels), preference),
outs = ["nodes/%s/config.xml" % name],
visibility = visibility,
)
if docker_base:
# Generate docker image startup script
expand_template(
name = name + ".docker-launcher",
out = name + ".docker-launcher.sh",
template = "slave_setup.sh",
substitutions = {
"NODE_NAME": name,
"HOME_FS": remote_fs,
"JENKINS_SERVER": "http://%s:%s" % (JENKINS_HOST, JENKINS_PORT),
},
executable = True,
)
# Generate docker image
docker_build(
name = name + ".docker",
base = docker_base,
volumes = [remote_fs],
files = [":%s.docker-launcher.sh" % name],
data_path = ".",
entrypoint = [
"/bin/bash",
"/%s.docker-launcher.sh" % name,
],
visibility = visibility,
)
def jenkins_build(name, plugins = None, base = "//jenkins/base", configs = [],
jobs = [], substitutions = {}, visibility = None, tars = []):
"""Build the docker image for the Jenkins instance."""
substitutions = substitutions + MAILS_SUBSTITUTIONS
# Expands config files in a tar ball
_merge_files(
name = "%s-configs" % name,
srcs = configs,
directory = "/usr/share/jenkins/ref",
strip_prefixes = [
"jenkins/config",
"jenkins",
],
substitutions = substitutions)
# Create the structures for jobs
_merge_files(
name = "%s-jobs" % name,
srcs = jobs,
path_format = "jobs/{basename}/config.xml",
directory = "/usr/share/jenkins/ref",
)
### FINAL IMAGE ###
docker_build(
name = name,
tars = [
":%s-jobs" % name,
":%s-configs" % name,
] + tars,
# Workaround no way to specify owner in pkg_tar
# TODO(dmarting): use https://cr.bazel.build/10255 when it hits a release.
user = "root",
entrypoint = [
"/bin/tini",
"--",
"/bin/bash",
"-c",
"[ -d /opt/lib ] && chown -R jenkins /opt/lib; su jenkins -c /usr/local/bin/jenkins.sh",
],
# End of workaround
base = base,
directory = "/",
visibility = visibility,
)