Merge branch 'stable-2.15' into stable-2.16

* stable-2.15:
  download_file.py: Synchronize curl options with gerrit core

Change-Id: Ic3af6e838ff726a317f390b416a88f6af8756a27
diff --git a/gerrit_api.bzl b/gerrit_api.bzl
index 3536a50..3c62f95 100644
--- a/gerrit_api.bzl
+++ b/gerrit_api.bzl
@@ -5,7 +5,7 @@
 gerrit_api is rule for fetching Gerrit plugin API using Bazel.
 """
 
-VER = "2.15.14"
+VER = "2.16.9"
 
 def gerrit_api():
     bouncycastle_repos()
@@ -13,18 +13,18 @@
     maven_jar(
         name = "gerrit_plugin_api",
         artifact = "com.google.gerrit:gerrit-plugin-api:" + VER,
-        sha1 = "72b062a380b23c9b8186d4d69c5c75cd9495de9c",
+        sha1 = "f650c16c8fdc4a7d76663f0bd720fe3055c0cbe1",
     )
     maven_jar(
         name = "gerrit_plugin_gwtui",
         artifact = "com.google.gerrit:gerrit-plugin-gwtui:" + VER,
-        sha1 = "799ad200d4482b78ebabd815de5be62e111cd8be",
+        sha1 = "33516d850b4906e069046add77037a96e27e26ae",
         exclude = ["com/google/gwt/*"],
     )
     maven_jar(
         name = "gerrit_acceptance_framework",
         artifact = "com.google.gerrit:gerrit-acceptance-framework:" + VER,
-        sha1 = "bb8c4bc73ab4e0d539cdf0e7fb11707621797cdb",
+        sha1 = "cd48eb229a72b4e8af4e975366af570ff0c8fc5a",
     )
     native.bind(
         name = "gerrit-plugin-api",
diff --git a/gerrit_api_maven_local.bzl b/gerrit_api_maven_local.bzl
index 3769cbb..8c4640d 100644
--- a/gerrit_api_maven_local.bzl
+++ b/gerrit_api_maven_local.bzl
@@ -5,7 +5,7 @@
 gerrit_api is rule for fetching Gerrit plugin API using Bazel.
 """
 
-VER = "2.15.2-SNAPSHOT"
+VER = "2.16.5-SNAPSHOT"
 
 def gerrit_api_maven_local():
     bouncycastle_repos()
diff --git a/gerrit_polymer.bzl b/gerrit_polymer.bzl
new file mode 100644
index 0000000..aaf1d4a
--- /dev/null
+++ b/gerrit_polymer.bzl
@@ -0,0 +1,18 @@
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
+
+def gerrit_polymer():
+    http_archive(
+        name = "io_bazel_rules_closure",
+        sha256 = "75c58680af5f7b938ce9fe2abe8ecd9d24c698d160c0b71a945bd100fa77632b",
+        strip_prefix = "rules_closure-10cb1a78bd6cc8927eb39c2644c0369934f4aed6",
+        urls = ["https://github.com/bazelbuild/rules_closure/archive/10cb1a78bd6cc8927eb39c2644c0369934f4aed6.tar.gz"],
+    )
+
+    # File is specific to Polymer and copied from the Closure Github -- should be
+    # synced any time there are major changes to Polymer.
+    # https://github.com/google/closure-compiler/blob/master/contrib/externs/polymer-1.0.js
+    http_file(
+        name = "polymer_closure",
+        sha256 = "5a589bdba674e1fec7188e9251c8624ebf2d4d969beb6635f9148f420d1e08b1",
+        urls = ["https://raw.githubusercontent.com/google/closure-compiler/775609aad61e14aef289ebec4bfc09ad88877f9e/contrib/externs/polymer-1.0.js"],
+    )
diff --git a/lib/BUILD b/lib/BUILD
new file mode 100644
index 0000000..00301d3
--- /dev/null
+++ b/lib/BUILD
@@ -0,0 +1 @@
+# Empty marker file, indicating this directory is a Bazel package.
diff --git a/lib/js/BUILD b/lib/js/BUILD
new file mode 100644
index 0000000..00301d3
--- /dev/null
+++ b/lib/js/BUILD
@@ -0,0 +1 @@
+# Empty marker file, indicating this directory is a Bazel package.
diff --git a/lib/js/externs/BUILD b/lib/js/externs/BUILD
new file mode 100644
index 0000000..fab3954
--- /dev/null
+++ b/lib/js/externs/BUILD
@@ -0,0 +1,25 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# 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.
+
+package(
+    default_visibility = ["//visibility:public"],
+)
+
+load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library")
+
+closure_js_library(
+    name = "plugin",
+    srcs = ["plugin.js"],
+    no_closure_library = True,
+)
diff --git a/lib/js/externs/plugin.js b/lib/js/externs/plugin.js
new file mode 100644
index 0000000..c88c724
--- /dev/null
+++ b/lib/js/externs/plugin.js
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * 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.
+ */
+
+/**
+ * @fileoverview Closure compiler externs for the Gerrit UI plugins.
+ * @externs
+ */
+
+/* eslint-disable no-var */
+
+var Gerrit = {};
+
+/**
+ * @param {!Function} callback
+ */
+Gerrit.install = function(callback) {};
diff --git a/lib/js/npm.bzl b/lib/js/npm.bzl
new file mode 100644
index 0000000..92f44bd
--- /dev/null
+++ b/lib/js/npm.bzl
@@ -0,0 +1,11 @@
+NPM_VERSIONS = {
+    "bower": "1.8.2",
+    "crisper": "2.0.2",
+    "polymer-bundler": "4.0.2",
+}
+
+NPM_SHA1S = {
+    "bower": "adf53529c8d4af02ef24fb8d5341c1419d33e2f7",
+    "crisper": "7183c58cea33632fb036c91cefd1b43e390d22a2",
+    "polymer-bundler": "6b296b6099ab5a0e93ca914cbe93e753f2395910",
+}
diff --git a/lib/polymer_externs/BUILD b/lib/polymer_externs/BUILD
new file mode 100644
index 0000000..9a45508
--- /dev/null
+++ b/lib/polymer_externs/BUILD
@@ -0,0 +1,32 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# 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.
+
+package(
+    default_visibility = ["//visibility:public"],
+)
+
+load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library")
+
+genrule(
+    name = "polymer_closure_renamed",
+    srcs = ["@polymer_closure//file"],
+    outs = ["polymer_closure_renamed.js"],
+    cmd = "cp $< $@",
+)
+
+closure_js_library(
+    name = "polymer_closure",
+    srcs = [":polymer_closure_renamed"],
+    no_closure_library = True,
+)
diff --git a/tools/javadoc.bzl b/tools/javadoc.bzl
index 204f569..2503594 100644
--- a/tools/javadoc.bzl
+++ b/tools/javadoc.bzl
@@ -31,7 +31,7 @@
         "rm -rf %s" % dir,
         "mkdir %s" % dir,
         " ".join([
-            ctx.file._javadoc.path,
+            "%s/bin/javadoc" % ctx.attr._jdk[java_common.JavaRuntimeInfo].java_home,
             "-Xdoclint:-missing",
             "-protected",
             "-encoding UTF-8",
@@ -62,13 +62,10 @@
         "libs": attr.label_list(allow_files = False),
         "pkgs": attr.string_list(),
         "title": attr.string(),
-        "_javadoc": attr.label(
-            default = Label("@local_jdk//:bin/javadoc"),
-            allow_single_file = True,
-        ),
         "_jdk": attr.label(
-            default = Label("@local_jdk//:jdk-default"),
+            default = Label("@bazel_tools//tools/jdk:current_java_runtime"),
             allow_files = True,
+            providers = [java_common.JavaRuntimeInfo],
         ),
     },
     outputs = {"zip": "%{name}.zip"},
diff --git a/tools/js.bzl b/tools/js.bzl
new file mode 100644
index 0000000..bbae345
--- /dev/null
+++ b/tools/js.bzl
@@ -0,0 +1,521 @@
+load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_binary", "closure_js_library")
+load("//lib/js:npm.bzl", "NPM_SHA1S", "NPM_VERSIONS")
+
+NPMJS = "NPMJS"
+
+GERRIT = "GERRIT:"
+
+def _npm_tarball(name):
+    return "%s@%s.npm_binary.tgz" % (name, NPM_VERSIONS[name])
+
+def _npm_binary_impl(ctx):
+    """rule to download a NPM archive."""
+    name = ctx.name
+    version = NPM_VERSIONS[name]
+    sha1 = NPM_SHA1S[name]
+
+    dir = "%s-%s" % (name, version)
+    filename = "%s.tgz" % dir
+    base = "%s@%s.npm_binary.tgz" % (name, version)
+    dest = ctx.path(base)
+    repository = ctx.attr.repository
+    if repository == GERRIT:
+        url = "http://gerrit-maven.storage.googleapis.com/npm-packages/%s" % filename
+    elif repository == NPMJS:
+        url = "http://registry.npmjs.org/%s/-/%s" % (name, filename)
+    else:
+        fail("repository %s not in {%s,%s}" % (repository, GERRIT, NPMJS))
+
+    python = ctx.which("python")
+    script = ctx.path(ctx.attr._download_script)
+
+    args = [python, script, "-o", dest, "-u", url, "-v", sha1]
+    out = ctx.execute(args)
+    if out.return_code:
+        fail("failed %s: %s" % (args, out.stderr))
+    ctx.file("BUILD", "package(default_visibility=['//visibility:public'])\nfilegroup(name='tarball', srcs=['%s'])" % base, False)
+
+npm_binary = repository_rule(
+    attrs = {
+        "repository": attr.string(default = NPMJS),
+        # Label resolves within repo of the .bzl file.
+        "_download_script": attr.label(default = Label("//tools:download_file.py")),
+    },
+    local = True,
+    implementation = _npm_binary_impl,
+)
+
+ComponentInfo = provider()
+
+# for use in repo rules.
+def _run_npm_binary_str(ctx, tarball, args):
+    python_bin = ctx.which("python")
+    return " ".join([
+        python_bin,
+        ctx.path(ctx.attr._run_npm),
+        ctx.path(tarball),
+    ] + args)
+
+def _bower_archive(ctx):
+    """Download a bower package."""
+    download_name = "%s__download_bower.zip" % ctx.name
+    renamed_name = "%s__renamed.zip" % ctx.name
+    version_name = "%s__version.json" % ctx.name
+
+    cmd = [
+        ctx.which("python"),
+        ctx.path(ctx.attr._download_bower),
+        "-b",
+        "%s" % _run_npm_binary_str(ctx, ctx.attr._bower_archive, []),
+        "-n",
+        ctx.name,
+        "-p",
+        ctx.attr.package,
+        "-v",
+        ctx.attr.version,
+        "-s",
+        ctx.attr.sha1,
+        "-o",
+        download_name,
+    ]
+
+    out = ctx.execute(cmd)
+    if out.return_code:
+        fail("failed %s: %s" % (" ".join(cmd), out.stderr))
+
+    _bash(ctx, " && ".join([
+        "TMP=$(mktemp -d || mktemp -d -t bazel-tmp)",
+        "TZ=UTC",
+        "export UTC",
+        "cd $TMP",
+        "mkdir bower_components",
+        "cd bower_components",
+        "unzip %s" % ctx.path(download_name),
+        "cd ..",
+        "find . -exec touch -t 198001010000 '{}' ';'",
+        "zip -Xr %s bower_components" % renamed_name,
+        "cd ..",
+        "rm -rf ${TMP}",
+    ]))
+
+    dep_version = ctx.attr.semver if ctx.attr.semver else ctx.attr.version
+    ctx.file(
+        version_name,
+        '"%s":"%s#%s"' % (ctx.name, ctx.attr.package, dep_version),
+    )
+    ctx.file(
+        "BUILD",
+        "\n".join([
+            "package(default_visibility=['//visibility:public'])",
+            "filegroup(name = 'zipfile', srcs = ['%s'], )" % download_name,
+            "filegroup(name = 'version_json', srcs = ['%s'], visibility=['//visibility:public'])" % version_name,
+        ]),
+        False,
+    )
+
+def _bash(ctx, cmd):
+    cmd_list = ["bash", "-c", cmd]
+    out = ctx.execute(cmd_list)
+    if out.return_code:
+        fail("failed %s: %s" % (" ".join(cmd_list), out.stderr))
+
+bower_archive = repository_rule(
+    _bower_archive,
+    attrs = {
+        "package": attr.string(mandatory = True),
+        "semver": attr.string(),
+        "sha1": attr.string(mandatory = True),
+        "version": attr.string(mandatory = True),
+        "_bower_archive": attr.label(default = Label("@bower//:%s" % _npm_tarball("bower"))),
+        "_download_bower": attr.label(default = Label("//tools/js:download_bower.py")),
+        "_run_npm": attr.label(default = Label("//tools/js:run_npm_binary.py")),
+    },
+)
+
+def _bower_component_impl(ctx):
+    transitive_zipfiles = depset(
+        direct = [ctx.file.zipfile],
+        transitive = [d[ComponentInfo].transitive_zipfiles for d in ctx.attr.deps],
+    )
+
+    transitive_licenses = depset(
+        direct = [ctx.file.license],
+        transitive = [d[ComponentInfo].transitive_licenses for d in ctx.attr.deps],
+    )
+
+    transitive_versions = depset(
+        direct = ctx.files.version_json,
+        transitive = [d[ComponentInfo].transitive_versions for d in ctx.attr.deps],
+    )
+
+    return [
+        ComponentInfo(
+            transitive_licenses = transitive_licenses,
+            transitive_versions = transitive_versions,
+            transitive_zipfiles = transitive_zipfiles,
+        ),
+    ]
+
+_common_attrs = {
+    "deps": attr.label_list(providers = [ComponentInfo]),
+}
+
+def _js_component(ctx):
+    dir = ctx.outputs.zip.path + ".dir"
+    name = ctx.outputs.zip.basename
+    if name.endswith(".zip"):
+        name = name[:-4]
+    dest = "%s/%s" % (dir, name)
+    cmd = " && ".join([
+        "TZ=UTC",
+        "export TZ",
+        "mkdir -p %s" % dest,
+        "cp %s %s/" % (" ".join([s.path for s in ctx.files.srcs]), dest),
+        "cd %s" % dir,
+        "find . -exec touch -t 198001010000 '{}' ';'",
+        "zip -Xqr ../%s *" % ctx.outputs.zip.basename,
+    ])
+
+    ctx.actions.run_shell(
+        inputs = ctx.files.srcs,
+        outputs = [ctx.outputs.zip],
+        command = cmd,
+        mnemonic = "GenBowerZip",
+    )
+
+    licenses = []
+    if ctx.file.license:
+        licenses.append(ctx.file.license)
+
+    return [
+        ComponentInfo(
+            transitive_licenses = depset(licenses),
+            transitive_versions = depset(),
+            transitive_zipfiles = list([ctx.outputs.zip]),
+        ),
+    ]
+
+js_component = rule(
+    _js_component,
+    attrs = dict(_common_attrs.items() + {
+        "srcs": attr.label_list(allow_files = [".js"]),
+        "license": attr.label(allow_single_file = True),
+    }.items()),
+    outputs = {
+        "zip": "%{name}.zip",
+    },
+)
+
+_bower_component = rule(
+    _bower_component_impl,
+    attrs = dict(_common_attrs.items() + {
+        "license": attr.label(allow_single_file = True),
+
+        # If set, define by hand, and don't regenerate this entry in bower2bazel.
+        "seed": attr.bool(default = False),
+        "version_json": attr.label(allow_files = [".json"]),
+        "zipfile": attr.label(allow_single_file = [".zip"]),
+    }.items()),
+)
+
+# TODO(hanwen): make license mandatory.
+def bower_component(name, license = None, **kwargs):
+    prefix = "//lib:LICENSE-"
+    if license and not license.startswith(prefix):
+        license = prefix + license
+    _bower_component(
+        name = name,
+        license = license,
+        zipfile = "@%s//:zipfile" % name,
+        version_json = "@%s//:version_json" % name,
+        **kwargs
+    )
+
+def _bower_component_bundle_impl(ctx):
+    """A bunch of bower components zipped up."""
+    zips = depset()
+    for d in ctx.attr.deps:
+        files = d[ComponentInfo].transitive_zipfiles
+
+        # TODO(davido): Make sure the field always contains a depset
+        if type(files) == "list":
+            files = depset(files)
+        zips = depset(transitive = [zips, files])
+
+    versions = depset(transitive = [d[ComponentInfo].transitive_versions for d in ctx.attr.deps])
+
+    licenses = depset(transitive = [d[ComponentInfo].transitive_versions for d in ctx.attr.deps])
+
+    out_zip = ctx.outputs.zip
+    out_versions = ctx.outputs.version_json
+
+    ctx.actions.run_shell(
+        inputs = zips.to_list(),
+        outputs = [out_zip],
+        command = " && ".join([
+            "p=$PWD",
+            "TZ=UTC",
+            "export TZ",
+            "rm -rf %s.dir" % out_zip.path,
+            "mkdir -p %s.dir/bower_components" % out_zip.path,
+            "cd %s.dir/bower_components" % out_zip.path,
+            "for z in %s; do unzip -q $p/$z ; done" % " ".join(sorted([z.path for z in zips.to_list()])),
+            "cd ..",
+            "find . -exec touch -t 198001010000 '{}' ';'",
+            "zip -Xqr $p/%s bower_components/*" % out_zip.path,
+        ]),
+        mnemonic = "BowerCombine",
+    )
+
+    ctx.actions.run_shell(
+        inputs = versions.to_list(),
+        outputs = [out_versions],
+        mnemonic = "BowerVersions",
+        command = "(echo '{' ; for j in  %s ; do cat $j; echo ',' ; done ; echo \\\"\\\":\\\"\\\"; echo '}') > %s" % (" ".join([v.path for v in versions.to_list()]), out_versions.path),
+    )
+
+    return [
+        ComponentInfo(
+            transitive_licenses = licenses,
+            transitive_versions = versions,
+            transitive_zipfiles = zips,
+        ),
+    ]
+
+bower_component_bundle = rule(
+    _bower_component_bundle_impl,
+    attrs = _common_attrs,
+    outputs = {
+        "version_json": "%{name}-versions.json",
+        "zip": "%{name}.zip",
+    },
+)
+
+def _bundle_impl(ctx):
+    """Groups a set of .html and .js together in a zip file.
+
+    Outputs:
+      NAME-versions.json:
+        a JSON file containing a PKG-NAME => PKG-NAME#VERSION mapping for the
+        transitive dependencies.
+    NAME.zip:
+      a zip file containing the transitive dependencies for this bundle.
+    """
+
+    # intermediate artifact if split is wanted.
+    if ctx.attr.split:
+        bundled = ctx.actions.declare_file(ctx.outputs.html.path + ".bundled.html")
+    else:
+        bundled = ctx.outputs.html
+    destdir = ctx.outputs.html.path + ".dir"
+    zips = [z for d in ctx.attr.deps for z in d[ComponentInfo].transitive_zipfiles.to_list()]
+
+    hermetic_npm_binary = " ".join([
+        "python",
+        "$p/" + ctx.file._run_npm.path,
+        "$p/" + ctx.file._bundler_archive.path,
+        "--inline-scripts",
+        "--inline-css",
+        "--strip-comments",
+        "--out-file",
+        "$p/" + bundled.path,
+        ctx.file.app.path,
+    ])
+
+    pkg_dir = ctx.attr.pkg.lstrip("/")
+    cmd = " && ".join([
+        # unpack dependencies.
+        "export PATH",
+        "p=$PWD",
+        "rm -rf %s" % destdir,
+        "mkdir -p %s/%s/bower_components" % (destdir, pkg_dir),
+        "for z in %s; do unzip -qd %s/%s/bower_components/ $z; done" % (
+            " ".join([z.path for z in zips]),
+            destdir,
+            pkg_dir,
+        ),
+        "tar -cf - %s | tar -C %s -xf -" % (" ".join([s.path for s in ctx.files.srcs]), destdir),
+        "cd %s" % destdir,
+        hermetic_npm_binary,
+    ])
+
+    # Node/NPM is not (yet) hermeticized, so we have to get the binary
+    # from the environment, and it may be under $HOME, so we can't run
+    # in the sandbox.
+    node_tweaks = dict(
+        execution_requirements = {"local": "1"},
+        use_default_shell_env = True,
+    )
+    ctx.actions.run_shell(
+        mnemonic = "Bundle",
+        inputs = [
+            ctx.file._run_npm,
+            ctx.file.app,
+            ctx.file._bundler_archive,
+        ] + list(zips) + ctx.files.srcs,
+        outputs = [bundled],
+        command = cmd,
+        **node_tweaks
+    )
+
+    if ctx.attr.split:
+        hermetic_npm_command = "export PATH && " + " ".join([
+            "python",
+            ctx.file._run_npm.path,
+            ctx.file._crisper_archive.path,
+            "--always-write-script",
+            "--source",
+            bundled.path,
+            "--html",
+            ctx.outputs.html.path,
+            "--js",
+            ctx.outputs.js.path,
+        ])
+
+        ctx.actions.run_shell(
+            mnemonic = "Crisper",
+            inputs = [
+                ctx.file._run_npm,
+                ctx.file.app,
+                ctx.file._crisper_archive,
+                bundled,
+            ],
+            outputs = [ctx.outputs.js, ctx.outputs.html],
+            command = hermetic_npm_command,
+            **node_tweaks
+        )
+
+def _bundle_output_func(name, split):
+    _ignore = [name]  # unused.
+    out = {"html": "%{name}.html"}
+    if split:
+        out["js"] = "%{name}.js"
+    return out
+
+_bundle_rule = rule(
+    _bundle_impl,
+    attrs = {
+        "srcs": attr.label_list(allow_files = [
+            ".js",
+            ".html",
+            ".txt",
+            ".css",
+            ".ico",
+        ]),
+        "app": attr.label(
+            mandatory = True,
+            allow_single_file = True,
+        ),
+        "pkg": attr.string(mandatory = True),
+        "split": attr.bool(default = True),
+        "deps": attr.label_list(providers = [ComponentInfo]),
+        "_bundler_archive": attr.label(
+            default = Label("@polymer-bundler//:%s" % _npm_tarball("polymer-bundler")),
+            allow_single_file = True,
+        ),
+        "_crisper_archive": attr.label(
+            default = Label("@crisper//:%s" % _npm_tarball("crisper")),
+            allow_single_file = True,
+        ),
+        "_run_npm": attr.label(
+            default = Label("//tools/js:run_npm_binary.py"),
+            allow_single_file = True,
+        ),
+    },
+    outputs = _bundle_output_func,
+)
+
+def bundle_assets(*args, **kwargs):
+    """Combine html, js, css files and optionally split into js and html bundles."""
+    _bundle_rule(pkg = native.package_name(), *args, **kwargs)
+
+def polygerrit_plugin(name, app, srcs = [], assets = None, **kwargs):
+    """Bundles plugin dependencies for deployment.
+
+    This rule bundles all Polymer elements and JS dependencies into .html and .js files.
+    Run-time dependencies (e.g. JS libraries loaded after plugin starts) should be provided using "assets" property.
+    Output of this rule is a FileSet with "${name}_fs", with deploy artifacts in "plugins/${name}/static".
+
+    Args:
+      name: String, plugin name.
+      app: String, the main or root source file.
+      assets: Fileset, additional files to be used by plugin in runtime, exported to "plugins/${name}/static".
+      srcs: Source files required for combining.
+    """
+
+    # Combines all .js and .html files into foo_combined.js and foo_combined.html
+    _bundle_rule(
+        name = name + "_combined",
+        app = app,
+        srcs = srcs if app in srcs else srcs + [app],
+        pkg = native.package_name(),
+        **kwargs
+    )
+
+    closure_js_binary(
+        name = name + "_bin",
+        compilation_level = "WHITESPACE_ONLY",
+        defs = [
+            "--polymer_version=1",
+            "--language_out=ECMASCRIPT6",
+            "--rewrite_polyfills=false",
+        ],
+        deps = [
+            name + "_closure_lib",
+        ],
+    )
+
+    closure_js_library(
+        name = name + "_closure_lib",
+        srcs = [name + "_combined.js"],
+        convention = "GOOGLE",
+        no_closure_library = True,
+        deps = [
+            "@com_googlesource_gerrit_bazlets//lib/polymer_externs:polymer_closure",
+            "@com_googlesource_gerrit_bazlets//lib/js/externs:plugin",
+        ],
+    )
+
+    native.genrule(
+        name = name + "_rename_html",
+        srcs = [name + "_combined.html"],
+        outs = [name + ".html"],
+        cmd = "sed 's/<script src=\"" + name + "_combined.js\"/<script src=\"" + name + ".js\"/g' $(SRCS) > $(OUTS)",
+        output_to_bindir = True,
+    )
+
+    native.genrule(
+        name = name + "_rename_js",
+        srcs = [name + "_bin.js"],
+        outs = [name + ".js"],
+        cmd = "cp $< $@",
+        output_to_bindir = True,
+    )
+
+    static_files = [
+        name + ".js",
+        name + ".html",
+    ]
+
+    if assets:
+        nested, direct = [], []
+        for x in assets:
+            target = nested if "/" in x else direct
+            target.append(x)
+
+        static_files += direct
+
+        if nested:
+            native.genrule(
+                name = name + "_copy_assets",
+                srcs = assets,
+                outs = [f.split("/")[-1] for f in nested],
+                cmd = "cp $(SRCS) $(@D)",
+                output_to_bindir = True,
+            )
+            static_files += [":" + name + "_copy_assets"]
+
+    native.filegroup(
+        name = name,
+        srcs = static_files,
+    )
diff --git a/tools/js/BUILD b/tools/js/BUILD
new file mode 100644
index 0000000..fedaf7f
--- /dev/null
+++ b/tools/js/BUILD
@@ -0,0 +1 @@
+exports_files(["run_npm_binary.py"])
diff --git a/tools/js/run_npm_binary.py b/tools/js/run_npm_binary.py
new file mode 100644
index 0000000..bdee5ab
--- /dev/null
+++ b/tools/js/run_npm_binary.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+# Copyright (C) 2015 The Android Open Source Project
+#
+# 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.
+
+from __future__ import print_function
+
+import atexit
+from distutils import spawn
+import hashlib
+import os
+import shutil
+import subprocess
+import sys
+import tarfile
+import tempfile
+
+
+def extract(path, outdir, bin):
+    if os.path.exists(os.path.join(outdir, bin)):
+        return  # Another process finished extracting, ignore.
+
+    # Use a temp directory adjacent to outdir so shutil.move can use the same
+    # device atomically.
+    tmpdir = tempfile.mkdtemp(dir=os.path.dirname(outdir))
+
+    def cleanup():
+        try:
+            shutil.rmtree(tmpdir)
+        except OSError:
+            pass  # Too late now
+    atexit.register(cleanup)
+
+    def extract_one(mem):
+        dest = os.path.join(outdir, mem.name)
+        tar.extract(mem, path=tmpdir)
+        try:
+            os.makedirs(os.path.dirname(dest))
+        except OSError:
+            pass  # Either exists, or will fail on the next line.
+        shutil.move(os.path.join(tmpdir, mem.name), dest)
+
+    with tarfile.open(path, 'r:gz') as tar:
+        for mem in tar.getmembers():
+            if mem.name != bin:
+                extract_one(mem)
+        # Extract bin last so other processes only short circuit when
+        # extraction is finished.
+        if bin in tar.getnames():
+            extract_one(tar.getmember(bin))
+
+
+def main(args):
+    path = args[0]
+    suffix = '.npm_binary.tgz'
+    tgz = os.path.basename(path)
+
+    parts = tgz[:-len(suffix)].split('@')
+
+    if not tgz.endswith(suffix) or len(parts) != 2:
+        print('usage: %s <path/to/npm_binary>' % sys.argv[0], file=sys.stderr)
+        return 1
+
+    name, _ = parts
+
+    # Avoid importing from gerrit because we don't want to depend on the right
+    # working directory
+    sha1 = hashlib.sha1(open(path, 'rb').read()).hexdigest()
+    outdir = '%s-%s' % (path[:-len(suffix)], sha1)
+    rel_bin = os.path.join('package', 'bin', name)
+    rel_lib_bin = os.path.join('package', 'lib', 'bin', name + '.js')
+    bin = os.path.join(outdir, rel_bin)
+    libbin = os.path.join(outdir, rel_lib_bin)
+    if not os.path.isfile(bin):
+        extract(path, outdir, rel_bin)
+
+    nodejs = spawn.find_executable('nodejs')
+    if nodejs:
+        # Debian installs Node.js as 'nodejs', due to a conflict with another
+        # package.
+        if not os.path.isfile(bin) and os.path.isfile(libbin):
+            subprocess.check_call([nodejs, libbin] + args[1:])
+        else:
+            subprocess.check_call([nodejs, bin] + args[1:])
+    elif not os.path.isfile(bin) and os.path.isfile(libbin):
+        subprocess.check_call([libbin] + args[1:])
+    else:
+        subprocess.check_call([bin] + args[1:])
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))