From 5b16b19f83e2efd023509cdfbdf2979567af478d Mon Sep 17 00:00:00 2001 From: Damien Martin-Guillerez Date: Mon, 29 Jun 2015 16:22:20 +0000 Subject: [PATCH] Add basic support for building Java AppEngine WAR This support is based on kchodorow@'s genrule to build the Bazel's dashboard AppEngine and was tested with it. -- MOS_MIGRATED_REVID=97135934 --- tools/build_rules/appengine/BUILD | 7 + tools/build_rules/appengine/README.md | 202 +++++++++++++++ tools/build_rules/appengine/appengine.BUILD | 34 +++ .../build_rules/appengine/appengine.WORKSPACE | 46 ++++ tools/build_rules/appengine/appengine.bzl | 230 ++++++++++++++++++ 5 files changed, 519 insertions(+) create mode 100644 tools/build_rules/appengine/BUILD create mode 100644 tools/build_rules/appengine/README.md create mode 100644 tools/build_rules/appengine/appengine.BUILD create mode 100644 tools/build_rules/appengine/appengine.WORKSPACE create mode 100644 tools/build_rules/appengine/appengine.bzl diff --git a/tools/build_rules/appengine/BUILD b/tools/build_rules/appengine/BUILD new file mode 100644 index 00000000000000..5f9aff437d7f43 --- /dev/null +++ b/tools/build_rules/appengine/BUILD @@ -0,0 +1,7 @@ +# A target to ensure the servlet-api is not linked in the webapp. +java_library( + name = "javax.servlet.api", + neverlink = 1, + visibility = ["//visibility:public"], + exports = ["@javax-servlet-api//jar:jar"], +) diff --git a/tools/build_rules/appengine/README.md b/tools/build_rules/appengine/README.md new file mode 100644 index 00000000000000..5090f36b2cdb84 --- /dev/null +++ b/tools/build_rules/appengine/README.md @@ -0,0 +1,202 @@ +# Java AppEngine Rules for Bazel + +## Overview + +These build rules are used for building +[Java AppEngine](https://cloud.google.com/appengine/docs/java/) +application with Bazel. It does not aim at general WebApplication +support but can be easily modified to handle a standard WebApplication. + +* [Setup](#setup) +* [Basic Example](#basic-example) +* [Build Rule Reference](#reference) + * [`appengine_war`](#appengine_war) + * [`java_war`](#java_war) + + +## Setup + +To be able to use the Java AppEngine rules, you must make the AppEngine SDK +available to Bazel. The easiest way to do so is by copying the content of +`appengine.WORKSPACE` to your workspace file. + + +## Basic Example + +Suppose you have the following directory structure for a simple AppEngine +application: + +``` +[workspace]/ + WORKSPACE + hello_app/ + BUILD + java/my/webapp/ + TestServlet.java + webapp/ + index.html + webapp/WEB-INF + web.xml + appengine-web.xml +``` + +Then, to build your webapp, your `hello_app/BUILD` can look like: + +```python +load("/tools/build_rules/appengine/appengine", "appengine_war") + +java_library( + name = "mylib", + srcs = ["java/my/webapp/TestServlet.java"], + deps = [ + "//external:appengine/java/api", + "//external:javax/servlet/api", + ], +) + +appengine_war( + name = "myapp", + jars = [":mylib"], + data = glob(["webapp/**"]), + data_path = "webapp", +) +``` + +For simplicity, you can use the `java_war` rule to build an app from source. +Your `hello_app/BUILD` file would then look like: + +```python +load("/tools/build_rules/appengine/appengine", "java_war") + +java_war( + name = "myapp", + srcs = ["java/my/webapp/TestServlet.java"], + data = glob(["webapp/**"]), + data_path = "webapp", + deps = [ + "//external:appengine/java/api", + "//external:javax/servlet/api", + ], +) +``` + +You can then build the application with `bazel build //hello_app:myapp` and +run in it a development server with `bazel run //hello_app:myapp`. This will +bind a test server on port 8080. If you wish to select another port, +simply append the `--port=12345` to the command-line. + + +## Build Rule Reference [reference] + + +### `appengine_war` + +`appengine_war(name, jars, data, data_path)` + + + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeDescription
name + Name, required +

A unique name for this rule.

+
jars + List of labels, required +

+ List of JAR files that will be uncompressed as the code for the + Web Application. +

+

+ If it is a `java_library` or a `java_import`, the + JAR from the runtime classpath will be added in the `lib` directory + of the Web Application. +

+
data + List of files, optional +

List of files used by the Web Application at runtime.

+

+ This attribute can be used to specify the list of resources to + be included into the WAR file. +

+
data_path + String, optional +

Root path of the data.

+

+ The directory structure from the data is preserved inside the + WebApplication but a prefix path determined by `data_path` + is removed from the the directory structure. This path can + be absolute from the workspace root if starting with a `/` or + relative to the rule's directory. It is set to `.` by default. +

+
+ + +### `java_war` + +`java_war(name, data, data_path, **kwargs)` + + + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeDescription
name + Name, required +

A unique name for this rule.

+
data + List of labels, optional +

List of files used by the Web Application at runtime.

+

Passed to the appengine_war rule.

+
data_path + String, optional +

Root path of the data.

+

Passed to the appengine_war rule.

+
**kwargs + see java_library +

+ The other arguments of this rule will be passed to build a `java_library` + that will be passed in the `jar` arguments of a + appengine_war rule. +

+
diff --git a/tools/build_rules/appengine/appengine.BUILD b/tools/build_rules/appengine/appengine.BUILD new file mode 100644 index 00000000000000..76adcb31046a94 --- /dev/null +++ b/tools/build_rules/appengine/appengine.BUILD @@ -0,0 +1,34 @@ +# Copyright 2015 Google Inc. 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. + +# BUILD file to use the Java AppEngine SDK with a remote repository. +java_import( + name = "jars", + jars = glob(["**/*.jar"]), + visibility = ["//visibility:public"], +) + +java_import( + name = "api", + jars = ["appengine-java-sdk-1.9.23/lib/impl/appengine-api.jar"], + visibility = ["//visibility:public"], + neverlink = 1, +) + +filegroup( + name = "sdk", + srcs = glob(["appengine-java-sdk-1.9.23/**"]), + visibility = ["//visibility:public"], + path = "appengine-java-sdk-1.9.23", +) \ No newline at end of file diff --git a/tools/build_rules/appengine/appengine.WORKSPACE b/tools/build_rules/appengine/appengine.WORKSPACE new file mode 100644 index 00000000000000..7aa4ef8ea06a8d --- /dev/null +++ b/tools/build_rules/appengine/appengine.WORKSPACE @@ -0,0 +1,46 @@ +# Copyright 2015 Google Inc. 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. + +# WORKSPACE file example to download Java AppEngine dependencies. +new_http_archive( + name = "appengine-java", + url = "http://central.maven.org/maven2/com/google/appengine/appengine-java-sdk/1.9.23/appengine-java-sdk-1.9.23.zip", + sha256 = "05e667036e9ef4f999b829fc08f8e5395b33a5a3c30afa9919213088db2b2e89", + build_file = "tools/build_rules/appengine/appengine.BUILD", +) + +bind( + name = "appengine/java/sdk", + actual = "@appengine-java//:sdk", +) + +bind( + name = "appengine/java/api", + actual = "@appengine-java//:api", +) + +bind( + name = "appengine/java/jars", + actual = "@appengine-java//:jars", +) + +maven_jar( + name = "javax-servlet-api", + artifact = "javax.servlet:servlet-api:2.5", +) + +bind( + name = "javax/servlet/api", + actual = "//tools/build_rules/appengine:javax.servlet.api", +) diff --git a/tools/build_rules/appengine/appengine.bzl b/tools/build_rules/appengine/appengine.bzl new file mode 100644 index 00000000000000..5a1bae00bc6625 --- /dev/null +++ b/tools/build_rules/appengine/appengine.bzl @@ -0,0 +1,230 @@ +# Copyright 2015 Google Inc. 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. +"""Java AppEngine support for Bazel. + +For now, it only support bundling a WebApp and running locally. + +To create a WebApp for Google AppEngine, add the rules: +appengine_war( + name = "MyWebApp", + # Jars to use for the classpath in the webapp. + jars = ["//java/com/google/examples/mywebapp:java"], + # data to put in the webapp, the directory structure of the data set + # will be maintained. + data = ["//java/com/google/examples/mywebapp:data"], + # Data's root path, it will be considered as the root of the data files. + # If unspecified, the path to the current package will be used. The path is + # relative to current package or, relative to the workspace root if starting + # with a leading slash. + data_path = "/java/com/google/examples/mywebapp", +) + + +You can also make directly a single target for it with: + +java_war( + name = "MyWebApp", + srcs = glob(["**/*.java"]), + resources = ["..."], + data = ["..."], + data_path = "...", +) + +Resources will be put in the classpath whereas data will be bundled at the root +of the war file. This is strictly equivalent to (it is actually a convenience +macros that translate to that): + +java_library( + name = "libMyWebApp", + srcs = glob(["**/*.java"]), + resources = ["..."], +) + +appengine_war( + name = "MyWebApp", + jars = [":libMyWebApp"], + data = ["..."], + data_path = "...", +) + +""" + +jar_filetype = FileType([".jar"]) + +def _extract_jar(zipper, jar, output): + return [ + "mkdir -p %s" % output, + "(root=$(pwd);" + + ("cd %s && " % output) + + ("${root}/%s x ${root}/%s)\n" % (zipper.path, jar.path)) + ] + +def _add_file(in_file, output, path = None): + output_path = output + input_path = in_file.path + if path and input_path.startswith(path): + output_path += input_path[len(path):] + return [ + "mkdir -p $(dirname %s)" % output_path, + "ln -s $(pwd)/%s %s\n" % (input_path, output_path) + ] + +def _make_war(zipper, input_dir, output): + return [ + "(root=$(pwd);" + + ("cd %s &&" % input_dir) + + ("${root}/%s c ${root}/%s $(find .))" % (zipper.path, output.path)) + ] + +def _common_substring(str1, str2): + i = 0 + res = "" + for c in str1: + if str2[i] != c: + return res + res += c + i += 1 + return res + +def _short_path_dirname(path): + sp = path.short_path + return sp[0:len(sp)-len(path.basename)-1] + +def _war_impl(ctxt): + zipper = ctxt.file._zipper + + data_path = ctxt.attr.data_path + if not data_path: + data_path = _short_path_dirname(ctxt.outputs.war) + elif data_path[0] == "/": + data_path = data_path[1:] + else: # relative path + data_path = _short_path_dirname(ctxt.outputs.war) + "/" + data_path + + war = ctxt.outputs.war + build_output = war.path + ".build_output" + cmd = [ + "set -e;rm -rf " + build_output, + "mkdir -p " + build_output + ] + + inputs = ctxt.files.jars + [zipper] + for jar in ctxt.files.jars: + # Add the jar content to WEB-INF/classes + cmd += _extract_jar(zipper, jar, build_output + "/WEB-INF/classes") + # Add its runtime classpath to WEB-INF/lib + if hasattr(jar, "java"): + inputs += jar.java.transitive_runtime_deps + for run_jar in jar.java.transitive_runtime_deps: + cmd += _add_file(run_jar, build_output + "/WEB-INF/lib") + + inputs += ctxt.files.data + for res in ctxt.files.data: + # Add the data file + cmd += _add_file(res, build_output, path = data_path) + + cmd += _make_war(zipper, build_output, war) + + ctxt.action( + inputs = inputs, + outputs = [war], + mnemonic="WAR", + command="\n".join(cmd), + use_default_shell_env=True) + + executable = ctxt.outputs.executable + appengine_sdk = None + for f in ctxt.files._appengine_sdk: + if not appengine_sdk: + appengine_sdk = f.path + elif not file.path.startswith(appengine_sdk): + appengine_sdk = _common_substring(appengine_sdk, f.path) + + classpath = [ + "${JAVA_RUNFILES}/%s" % jar.short_path + for jar in ctxt.files._appengine_jars + ] + ctxt.file_action( + output = executable, + content = "\n".join([ + "#!/bin/bash", + "# autogenerated - do not edit.", + "case \"$0\" in", + "/*) self=\"$0\" ;;", + "*) self=\"$PWD/$0\";;", + "esac", + "", + "if [[ -z \"$JAVA_RUNFILES\" ]]; then", + " if [[ -e \"${self}.runfiles\" ]]; then", + " export JAVA_RUNFILES=\"${self}.runfiles\"", + " fi", + " if [[ -n \"$JAVA_RUNFILES\" ]]; then", + " export TEST_SRCDIR=${TEST_SRCDIR:-$JAVA_RUNFILES}", + " fi", + "fi", + "", + "tmp_dir=$(mktemp -d ${TMPDIR:-/tmp}/war.XXXXXXXX)", + "trap \"rm -rf ${tmp_dir}\" EXIT", + "root_path=$(pwd)", + "cd ${tmp_dir}", + "${JAVA_RUNFILES}/%s x ${JAVA_RUNFILES}/%s" % ( + ctxt.file._zipper.short_path, ctxt.outputs.war.short_path), + "cd ${root_path}", + "jvm_bin=${JAVA_RUNFILES}/%s" % (ctxt.file._java.short_path), + "if [[ ! -x ${jvm_bin} ]]; then", + " jvm_bin=$(which java)", + "fi", + "APP_ENGINE_ROOT=${JAVA_RUNFILES}/%s" % appengine_sdk, + "main_class=\"com.google.appengine.tools.development.DevAppServerMain\"", + "classpath=\"%s\"" % (":".join(classpath)), + "${jvm_bin} -Dappengine.sdk.root=${APP_ENGINE_ROOT} " + + "-cp \"${classpath}\" ${main_class} \"$@\" ${tmp_dir}", + "", + ]), + executable = True) + + runfiles = ctxt.runfiles(files = [war, executable] + + ctxt.files._appengine_sdk + + ctxt.files._appengine_jars + + [ctxt.file._java, ctxt.file._zipper]) + return struct(runfiles = runfiles) + +appengine_war = rule( + _war_impl, + executable = True, + attrs = { + "_java": attr.label( + default=Label("//tools/jdk:java"), + single_file=True), + "_zipper": attr.label( + default=Label("//third_party/ijar:zipper"), + single_file=True), + "_appengine_sdk": attr.label( + default=Label("//external:appengine/java/sdk")), + "_appengine_jars": attr.label( + default=Label("//external:appengine/java/jars")), + "jars": attr.label_list(allow_files=jar_filetype, mandatory=True), + "data": attr.label_list(allow_files=True), + "data_path": attr.string(), + }, + outputs = { + "war": "%{name}.war", + }) + +def java_war(name, data=[], data_path=None, **kwargs): + native.java_library(name = "lib%s" % name, **kwargs) + appengine_war(name = name, + jars = ["lib%s" % name], + data=data, + data_path=data_path)