Merge branch 'stable-2.14' into stable-2.15

* stable-2.14:
  Fix deletion of Sonar Scanner folder
  Automate SonarQube analysis for plugins built with bazel

Change-Id: I115920d26ef63350e3308c82d092ca97e4b20c76
diff --git a/tools/sonar/BUILD b/tools/sonar/BUILD
new file mode 100644
index 0000000..30222ca
--- /dev/null
+++ b/tools/sonar/BUILD
@@ -0,0 +1,9 @@
+package(
+    default_visibility = ["//visibility:public"],
+)
+
+py_binary(
+    name = "sonar",
+    srcs = ["sonar.py"],
+    main = "sonar.py",
+)
diff --git a/tools/sonar/gen_sonar_project_properties.py b/tools/sonar/gen_sonar_project_properties.py
new file mode 100755
index 0000000..266f92a
--- /dev/null
+++ b/tools/sonar/gen_sonar_project_properties.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+# 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.
+
+from __future__ import print_function
+from optparse import OptionParser
+from os import path, walk
+from subprocess import check_output
+import re
+
+
+def guess_maven_group_id(plugin_name, plugin_dir):
+  current_dir = None
+  for dir_path, dirs, files in walk(path.join(plugin_dir, 'src', 'main', 'java')):
+    if len(files) > 0 or len(dirs) > 1 or path.basename(dir_path) == plugin_name:
+      break
+    current_dir = dir_path
+
+  if current_dir is None:
+    return str(hash(plugin_name))
+
+  group_id = []
+  while not path.basename(current_dir) == 'java':
+    group_id.append(path.basename(current_dir))
+    current_dir = path.dirname(current_dir)
+  return '.'.join(reversed(group_id))
+
+
+def get_plugin_version(plugin_dir):
+  version = '1.0'
+  version_file_path = path.join(plugin_dir, 'VERSION')
+  if path.exists(version_file_path):
+    try:
+      with open(version_file_path, "r") as version_file:
+        data = re.sub(r"\s+", '', version_file.read())
+    except Exception as err:
+      print('error reading plugin version: %s' % err)
+    else:
+      match = re.search(r"PLUGIN_VERSION='(.*?)'", data)
+      if match:
+        version = match.group(1)
+  elif path.exists(path.join(plugin_dir, '.git')):
+    version = check_output(['git', 'describe', '--always', 'HEAD'],
+                           cwd=plugin_dir).decode('utf-8')
+  return version
+
+
+def generate_project_properties(plugin_name, plugin_dir, classes_dir,
+                                output):
+  try:
+    with open(output, 'w') as fd:
+      print("""\
+sonar.projectKey=%s
+sonar.projectName=%s
+sonar.projectVersion=%s
+
+sonar.language=java
+sonar.sources=%s
+sonar.tests=%s
+sonar.sourceEncoding=UTF-8
+sonar.java.source=1.8
+sonar.java.binaries=%s\
+""" % (guess_maven_group_id(plugin_name, plugin_dir) + ":" + plugin_name,
+       plugin_name,
+       get_plugin_version(plugin_dir),
+       path.join(plugin_dir, 'src', 'main', 'java'),
+       path.join(plugin_dir, 'src', 'test', 'java'),
+       classes_dir),file=fd)
+  except Exception as err:
+      print('error writing project properties file: %s' % err)
+
+
+if __name__ == '__main__':
+  opts = OptionParser()
+  opts.add_option('-n', help='plugin name')
+  opts.add_option('-c', help='classes directory')
+  opts.add_option('-t', help='test report directory')
+  opts.add_option('-o', help='output file', default='sonar-project.properties')
+  args, _ = opts.parse_args()
+
+  plugin_dir = check_output(['bazel', 'info', 'workspace']).decode('utf-8').strip()
+  generate_project_properties(args.n, plugin_dir, path.abspath(args.c),
+                              path.abspath(args.t), args.o)
diff --git a/tools/sonar/sonar.py b/tools/sonar/sonar.py
new file mode 100755
index 0000000..18c5c22
--- /dev/null
+++ b/tools/sonar/sonar.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+# 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.
+
+
+# This script runs a Sonarqube analysis for a Gerrit plugin and uploads the
+# results to the local Sonarqube instance, similar to what `mvn sonar:sonar`
+# would do.
+#
+# It will build the plugin, generate sonar-project.properties file and then
+# call sonar-scanner (sonar-scanner must be installed and available in the
+# path).
+#
+# This script must be called from the root folder of a gerrit plugin supporting
+# standalone bazel build:
+#
+# ./bazlets/tools/sonar.py
+#
+
+from __future__ import print_function
+from os import path
+import re
+from shutil import rmtree
+from tempfile import mkdtemp
+from subprocess import check_call, check_output, CalledProcessError
+from zipfile import ZipFile
+
+from gen_sonar_project_properties import generate_project_properties
+
+
+def get_plugin_name():
+  target_names = check_output(['bazel', 'query', 'kind(java_binary, //...)']).decode('utf-8')
+  return re.search('(?<=//:)(.*)(?=__non_stamped)', target_names).group(1)
+
+
+plugin_dir = check_output(['bazel', 'info', 'workspace']).decode('utf-8').strip()
+plugin_name = get_plugin_name()
+temp_dir = mkdtemp()
+try:
+  try:
+    check_call(['bazel', 'build', '//:' + plugin_name])
+  except CalledProcessError as err:
+    exit(1)
+
+  classes_dir = path.join(temp_dir, 'classes')
+  with ZipFile(path.join(plugin_dir, 'bazel-genfiles', plugin_name + '.jar'),
+               "r") as z:
+    z.extractall(classes_dir)
+
+  sonar_project_properties = path.join(temp_dir, 'sonar-project.properties')
+
+  generate_project_properties(plugin_name, plugin_dir, classes_dir,
+                              sonar_project_properties)
+  try:
+    check_call(['sonar-scanner',
+                '-Dproject.settings=' + sonar_project_properties, ])
+  except CalledProcessError as err:
+    exit(1)
+finally:
+  rmtree(path.join(plugin_dir, '.scannerwork'), ignore_errors=True)
+  rmtree(temp_dir, ignore_errors=True)
+