Build the p2updatesite using Bazel (#38)

To do so we download directly the Eclipse platform to use its
tooling to build the metadata of the p2 site.

We should ultimately take all deps for the plugin from the Eclipse
platform instead of downloading it twice.
diff --git a/com.google.devtools.bazel.e4b.feature/BUILD b/com.google.devtools.bazel.e4b.feature/BUILD
index 70d2162..51ce1ca 100644
--- a/com.google.devtools.bazel.e4b.feature/BUILD
+++ b/com.google.devtools.bazel.e4b.feature/BUILD
@@ -12,4 +12,5 @@
     sites = {"Bazel": "https://bazel.build"},
     url = "https://github.com/bazelbuild/e4b",
     version = "0.0.3.qualifier",
+    visibility = ["//visibility:public"],
 )
diff --git a/p2updatesite/BUILD b/p2updatesite/BUILD
new file mode 100644
index 0000000..3fa0a06
--- /dev/null
+++ b/p2updatesite/BUILD
@@ -0,0 +1,9 @@
+load("//tools/build_defs:eclipse.bzl", "eclipse_p2updatesite")
+
+eclipse_p2updatesite(
+    name = "p2updatesite",
+    description = "Eclipse plugin for Bazel",
+    eclipse_features = ["//com.google.devtools.bazel.e4b.feature"],
+    label = "Eclipse 4 Bazel",
+    url = "https://bazelbuild.github.io/e4b",
+)
diff --git a/tools/build_defs/BUILD b/tools/build_defs/BUILD
index 07c8d68..00dc44c 100644
--- a/tools/build_defs/BUILD
+++ b/tools/build_defs/BUILD
@@ -5,3 +5,9 @@
     srcs = ["feature_builder.py"],
     deps = ["@com_google_python_gflags//:gflags"],
 )
+
+py_binary(
+    name = "site_builder",
+    srcs = ["site_builder.py"],
+    deps = ["@com_google_python_gflags//:gflags"],
+)
diff --git a/tools/build_defs/eclipse.bzl b/tools/build_defs/eclipse.bzl
index dee905f..ff890fa 100644
--- a/tools/build_defs/eclipse.bzl
+++ b/tools/build_defs/eclipse.bzl
@@ -15,8 +15,13 @@
 # TODO(dmarting): mirror those jars.
 # TODO(dmarting): Provide checksums for those files.
 _EQUINOX_MIRROR_URL="http://download.eclipse.org/eclipse/updates/"
-_ECLIPSE_VERSION="4.5/R-4.5.2-201602121500"
-_DOWNLOAD_URL = _EQUINOX_MIRROR_URL + "/" + _ECLIPSE_VERSION + "/plugins/%s_%s.jar"
+_ECLIPSE_VERSION="4.5.2-201602121500"
+_DOWNLOAD_URL = "%s/%s/R-%s/plugins/%s_%s.jar" % (
+    _EQUINOX_MIRROR_URL,
+    ".".join(_ECLIPSE_VERSION.split(".", 3)[0:2]),
+    _ECLIPSE_VERSION,
+    "%s",
+    "%s")
 
 # TODO(dmarting): make this configurable?
 _DECLARED_DEPS = [
@@ -69,13 +74,19 @@
 }
 
 
+def _load_eclipse_dep(plugin, version):
+  native.http_file(
+    name = plugin.replace(".", "_"),
+    url = _DOWNLOAD_URL % (plugin, version),
+  )
+
+load("//tools/build_defs:eclipse_platform.bzl", "eclipse_platform")
+
 def load_eclipse_deps():
   """Load dependencies of the Eclipse plugin."""
   for plugin in _ECLIPSE_PLUGIN_DEPS:
-    native.http_file(
-      name = plugin.replace(".", "_"),
-      url = _DOWNLOAD_URL % (plugin, _ECLIPSE_PLUGIN_DEPS[plugin]),
-    )
+    _load_eclipse_dep(plugin, _ECLIPSE_PLUGIN_DEPS[plugin])
+  eclipse_platform(name="org_eclipse_equinox", version=_ECLIPSE_VERSION)
 
 
 def eclipse_plugin(name, version, bundle_name, activator=None,
@@ -182,6 +193,14 @@
                    ctx.outputs.out.path,
                    "feature.xml=" + feature_xml.path],
   )
+  return struct(
+      eclipse_feature=struct(
+          file=ctx.outputs.out,
+          id=ctx.label.name,
+          version=ctx.attr.version,
+          plugins=ctx.files.plugins
+      )
+  )
 
 
 eclipse_feature = rule(
@@ -209,20 +228,60 @@
 """Create an eclipse feature jar."""
 
 
-# TODO(dmarting): implement eclipse_p2updatesite.
-# An p2 site is a site which has the following layout:
-# /site.xml (see p2updatesite/site.xml)
-# /artifacts.jar
-#   jar that contains only one XML file called artifacts.xml.
-#   This file contains the list of artifacts available on that
-#   update site, and the mapping between the name of the artifact,
-#   and the file position.
-# /content.jar
-#   jar that contains only one XML file called content.xml.
-#   This XML file describe which feature are available in that
-#   update site and their description (so a client may read only
-#   that file to list the content of the repository to the user).
-# /plugins
-#   plugin1_v1.jar -> OSGi Bundle for eclipse
-# /features
-#   feature1_v1.jar -> feature jar
+def _eclipse_p2updatesite_impl(ctx):
+  feat_files = [f.eclipse_feature.file for f in ctx.attr.eclipse_features]
+  args = [
+    "--output=" + ctx.outputs.out.path,
+    "--java=" + ctx.executable._java.path,
+    "--eclipse_launcher=" + ctx.file._eclipse_launcher.path,
+    "--name=" + ctx.attr.label,
+    "--url=" + ctx.attr.url,
+    "--description=" + ctx.attr.description]
+
+  _plugins = {}
+  for f in ctx.attr.eclipse_features:
+    args.append("--feature=" + f.eclipse_feature.file.path)
+    args.append("--feature_id=" + f.eclipse_feature.id)
+    args.append("--feature_version=" + f.eclipse_feature.version)
+    for p in f.eclipse_feature.plugins:
+      if p.path not in _plugins:
+        _plugins[p.path] = p
+  plugins = [_plugins[p] for p in _plugins]
+
+  ctx.action(
+      outputs=[ctx.outputs.out],
+      inputs=[
+          ctx.executable._java,
+          ctx.file._eclipse_launcher,
+          ] + ctx.files._jdk + ctx.files._eclipse_platform + feat_files + plugins,
+      executable = ctx.executable._site_builder,
+      arguments = args + ["--bundle=" + p.path for p in plugins])
+
+
+eclipse_p2updatesite = rule(
+   implementation=_eclipse_p2updatesite_impl,
+   attrs = {
+       "label": attr.string(mandatory=True),
+       "description": attr.string(mandatory=True),
+       "url": attr.string(mandatory=True),
+       "eclipse_features": attr.label_list(providers=["eclipse_feature"]),
+       "_site_builder": attr.label(
+           default=Label("//tools/build_defs:site_builder"),
+           executable=True,
+           cfg="host"),
+       "_zipper": attr.label(
+           default=Label("@bazel_tools//tools/zip:zipper"),
+           executable=True,
+           cfg="host"),
+        "_java": attr.label(
+           default=Label("@bazel_tools//tools/jdk:java"),
+           executable=True,
+           cfg="host"),
+        "_jdk": attr.label(default=Label("@bazel_tools//tools/jdk:jdk")),
+        "_eclipse_launcher": attr.label(
+            default=Label("@org_eclipse_equinox//:launcher"),
+            allow_single_file=True),
+        "_eclipse_platform": attr.label(default=Label("@org_eclipse_equinox//:platform")),
+    },
+    outputs = {"out": "%{name}.zip"})
+"""Create an eclipse p2update site inside a ZIP file."""
diff --git a/tools/build_defs/eclipse_platform.bzl b/tools/build_defs/eclipse_platform.bzl
new file mode 100644
index 0000000..012e6f5
--- /dev/null
+++ b/tools/build_defs/eclipse_platform.bzl
@@ -0,0 +1,70 @@
+# 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.
+
+# The Eclipse website provides SHA-512 but Bazel only support SHA256.
+# Really Bazel should start supporting all "safe" checksum (and also
+# drop support for SHA-1).
+SHA256_SUM={
+    # TODO(dmarting): we only support 4.5.2 right now because we need to
+    # download all version of eclipse to provide those checksums...
+    "4.5.2": {
+        "macosx-cocoa-x86_64": "755f8a75075f6310a8d0453b5766a84aca2fcc687808341b7a657259230b490f",
+        "linux-gtk-x86_64": "87f82b0c13c245ee20928557dbc4435657d1e029f72d9135683c8d585c69ba8d"
+    }
+}
+
+def _get_file_url(version, platform, t):
+  drop = "drops"
+  if int(version.split(".", 1)[0]) >= 4:
+    drop = "drops4"
+  short_version = version.split("-", 1)[0]
+  sha256 = ""
+  if short_version in SHA256_SUM:
+    if platform in SHA256_SUM[short_version]:
+      sha256 = SHA256_SUM[short_version][platform]
+
+  filename = "eclipse-SDK-%s-%s.%s" % (short_version, platform, t)
+  file = "/eclipse/downloads/%s/R-%s/%s" % (
+      drop,
+      version,
+      filename)
+  return ("http://www.eclipse.org/downloads/download.php?file=" + file, sha256)
+
+
+def _eclipse_platform_impl(rctx):
+  version = rctx.attr.version
+  os_name = rctx.os.name.lower()
+  if os_name.startswith("mac os"):
+    platform = "macosx-cocoa-x86_64"
+    t = "tar.gz"
+  elif os_name.startswith("linux"):
+    platform = "linux-gtk-x86_64"
+    t = "tar.gz"
+  else:
+    fail("Cannot fetch Eclipse for platform %s" % rctx.os.name)
+  url, sha256 = _get_file_url(version, platform, t)
+  rctx.download_and_extract(url=url, type=t, sha256=sha256)
+  rctx.file("BUILD.bazel", """
+package(default_visibility = ["//visibility:public"])
+filegroup(name = "platform", srcs = glob(["**"], exclude = ["BUILD.bazel", "BUILD"]))
+filegroup(name = "launcher", srcs = glob(["**/plugins/org.eclipse.equinox.launcher_*.jar"]))
+""")
+
+
+eclipse_platform = repository_rule(
+  implementation = _eclipse_platform_impl,
+  attrs = {
+    "version": attr.string(mandatory=True),
+  }, local=False)
+"""A repository for downloading the good version eclipse depending on the platform.""" 
diff --git a/tools/build_defs/feature_builder.py b/tools/build_defs/feature_builder.py
index 0964ebc..c82f2c3 100644
--- a/tools/build_defs/feature_builder.py
+++ b/tools/build_defs/feature_builder.py
@@ -102,10 +102,10 @@
   _plugins(feature, FLAGS.plugin)
 
   # Pretty print the resulting tree
-  output = ElementTree.tostring(feature, 'utf-8')
+  output = ElementTree.tostring(feature, "utf-8")
   reparsed = minidom.parseString(output)
   with open(FLAGS.output, "w") as f:
-    f.write(reparsed.toprettyxml(indent="  "))
+    f.write(reparsed.toprettyxml(indent="  ", encoding="UTF-8"))
 
 if __name__ == "__main__":
   main(FLAGS(sys.argv))
diff --git a/tools/build_defs/site_builder.py b/tools/build_defs/site_builder.py
new file mode 100644
index 0000000..8ac830a
--- /dev/null
+++ b/tools/build_defs/site_builder.py
@@ -0,0 +1,170 @@
+# 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.
+"""This tool build a zipped Eclipse p2 update site from features and plugins."""
+
+import gflags
+import os
+import os.path
+import shutil
+import subprocess
+import sys
+import tempfile
+import zipfile
+
+from xml.etree import ElementTree
+from xml.dom import minidom
+
+gflags.DEFINE_string("output", None, "The output files, mandatory")
+gflags.MarkFlagAsRequired("output")
+
+gflags.DEFINE_multistring(
+    "feature_id",
+    [],
+    "Feature id to include in the site, should come "
+    "along with --feature and --feature_version.")
+gflags.DEFINE_multistring(
+    "feature",
+    [],
+    "Feature file to include in the site, should come "
+    "along with --feature_id and --feature_version")
+gflags.DEFINE_multistring(
+    "feature_version",
+    [],
+    "Version of a feature to include in the site, should "
+    "come along with --feature and --feature_id")
+
+gflags.DEFINE_multistring(
+    "bundle",
+    [],
+    "Bundle file to include in the sit")
+
+gflags.DEFINE_string(
+  "name",
+  None,
+  "The site name (i.e. short description), mandatory")
+gflags.MarkFlagAsRequired("name")
+
+gflags.DEFINE_string("url", None, "A URL for the site, mandatory")
+gflags.MarkFlagAsRequired("url")
+
+gflags.DEFINE_string(
+  "description", None, "Description of the site, mandatory")
+gflags.MarkFlagAsRequired("description")
+
+gflags.DEFINE_string(
+  "java",
+  "java",
+  "Path to java, optional")
+
+gflags.DEFINE_string(
+  "eclipse_launcher",
+  None,
+  "Path to the eclipse launcher, mandatory")
+gflags.MarkFlagAsRequired("eclipse_launcher")
+
+FLAGS=gflags.FLAGS
+
+def _features(parent):
+  if (len(FLAGS.feature) != len(FLAGS.feature_id)) or (
+          len(FLAGS.feature) != len(FLAGS.feature_version)):
+      raise Exception(
+          "Should provide the same number of "
+          "time --feature, --feature_id and "
+          "--feature_version")
+  for i in range(0, len(FLAGS.feature)):
+    p = ElementTree.SubElement(parent, "feature")
+    p.set("url", "feature/%s" % os.path.basename(FLAGS.feature[i]))
+    p.set("id", FLAGS.feature_id[i])
+    p.set("version", FLAGS.feature_version[i])
+
+
+def create_site_xml(tmp_dir):
+  site = ElementTree.Element("site")
+  description = ElementTree.SubElement(site, "description")
+  description.set("name", FLAGS.name)
+  description.set("url", FLAGS.url)
+  description.text = FLAGS.description
+  _features(site)
+
+  # Pretty print the resulting tree
+  output = ElementTree.tostring(site, "utf-8")
+  reparsed = minidom.parseString(output)
+  with open(os.path.join(tmp_dir, "site.xml"), "w") as f:
+    f.write(reparsed.toprettyxml(indent="  ", encoding="UTF-8"))
+
+
+def copy_artifacts(tmp_dir):
+  feature_dir = os.path.join(tmp_dir, "features")
+  bundle_dir = os.path.join(tmp_dir, "plugins")
+  os.mkdir(feature_dir)
+  os.mkdir(bundle_dir)
+  for f in FLAGS.feature:
+    shutil.copyfile(f, os.path.join(feature_dir, os.path.basename(f)))
+  for p in FLAGS.bundle:
+    shutil.copyfile(p, os.path.join(bundle_dir, os.path.basename(p)))
+
+
+def generate_metadata(tmp_dir):
+  tmp_dir2 = tempfile.mkdtemp()
+
+  args = [
+    FLAGS.java,
+    "-jar", FLAGS.eclipse_launcher,
+    "-application", "org.eclipse.equinox.p2.publisher.FeaturesAndBundlesPublisher",
+    "-metadataRepository", "file:/" + tmp_dir,
+    "-artifactRepository", "file:/" + tmp_dir,
+    "-configuration", tmp_dir2,
+    "-source", tmp_dir,
+    "-compress", "-publishArtifacts"]
+  process = subprocess.Popen(args, stdout=subprocess.PIPE)
+  stdout, _ = process.communicate()
+  if process.returncode:
+    sys.stdout.write(stdout)
+    for root, dirs, files in os.walk(tmp_dir2):
+      for f in files:
+        if f.endswith(".log"):
+          with open(os.path.join(root, f), "r") as fi:
+            sys.stderr.write("Log %s: %s\n" % (f, fi.read()))
+    shutil.rmtree(tmp_dir)
+    sys.exit(process.returncode)
+  shutil.rmtree(tmp_dir2)
+
+
+def _zipinfo(filename):
+  result = zipfile.ZipInfo(filename, (1980, 1, 1, 0, 0, 0))
+  result.external_attr = 0o644 << 16L
+  return result
+
+def zip_all(tmp_dir):
+  with zipfile.ZipFile(FLAGS.output, "w", zipfile.ZIP_DEFLATED) as zf:
+    for root, dirs, files in os.walk(tmp_dir):
+      reldir = os.path.relpath(root, tmp_dir)
+      if reldir == ".":
+        reldir = ""
+      for f in files:
+        with open(os.path.join(root, f), "r") as fi:
+          zf.writestr(_zipinfo(os.path.join(reldir, f)), fi.read())
+
+
+def main(unused_argv):
+  tmp_dir = tempfile.mkdtemp()
+  create_site_xml(tmp_dir)
+  copy_artifacts(tmp_dir)
+  generate_metadata(tmp_dir)
+  zip_all(tmp_dir)
+  shutil.rmtree(tmp_dir)
+
+
+if __name__ == "__main__":
+  main(FLAGS(sys.argv))