Initial checkin for external updater

Bug: 109748616
Test: https://android-review.googlesource.com/c/platform/external/kotlinc/+/699886
Change-Id: I1c28aa256bca6ee5be1ea15f295c5e0fa63526d1
diff --git a/github_archive_updater.py b/github_archive_updater.py
new file mode 100644
index 0000000..7f0bb77
--- /dev/null
+++ b/github_archive_updater.py
@@ -0,0 +1,111 @@
+# 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.
+"""Module to update packages from GitHub archive."""
+
+
+import json
+import re
+import shutil
+import urllib.request
+
+import archive_utils
+import fileutils
+import metadata_pb2    # pylint: disable=import-error
+import updater_utils
+
+GITHUB_URL_PATTERN = (r'^https:\/\/github.com\/([-\w]+)\/([-\w]+)\/' +
+                      r'(releases\/download\/|archive\/)')
+GITHUB_URL_RE = re.compile(GITHUB_URL_PATTERN)
+
+
+class GithubArchiveUpdater():
+    """Updater for archives from GitHub.
+
+    This updater supports release archives in GitHub. Version is determined by
+    release name in GitHub.
+    """
+
+    VERSION_FIELD = 'tag_name'
+
+    def __init__(self, url, proj_path, metadata):
+        self.proj_path = proj_path
+        self.metadata = metadata
+        self.old_url = url
+        self.owner = None
+        self.repo = None
+        self.data = None
+        self._parse_url(url)
+
+    def _parse_url(self, url):
+        if url.type != metadata_pb2.URL.ARCHIVE:
+            raise ValueError('Only archive url from Github is supported.')
+        match = GITHUB_URL_RE.match(url.value)
+        if match is None:
+            raise ValueError('Url format is not supported.')
+        try:
+            self.owner, self.repo = match.group(1, 2)
+        except IndexError:
+            raise ValueError('Url format is not supported.')
+
+    def get_latest_version(self):
+        """Checks upstream and returns the latest version name we found."""
+
+        url = 'https://api.github.com/repos/{}/{}/releases/latest'.format(
+            self.owner, self.repo)
+        with urllib.request.urlopen(url) as request:
+            self.data = json.loads(request.read().decode())
+        return self.data[self.VERSION_FIELD]
+
+    def get_current_version(self):
+        """Returns the latest version name recorded in METADATA."""
+        return self.metadata.third_party.version
+
+    def _write_metadata(self, url, path):
+        updated_metadata = metadata_pb2.MetaData()
+        updated_metadata.CopyFrom(self.metadata)
+        updated_metadata.third_party.version = self.data[self.VERSION_FIELD]
+        for metadata_url in updated_metadata.third_party.url:
+            if metadata_url == self.old_url:
+                metadata_url.value = url
+        fileutils.write_metadata(path, updated_metadata)
+
+    def update(self):
+        """Updates the package.
+
+        Has to call get_latest_version() before this function.
+        """
+
+        supported_assets = [
+            a for a in self.data['assets']
+            if archive_utils.is_supported_archive(a['browser_download_url'])]
+
+        # Finds the minimum sized archive to download.
+        minimum_asset = min(
+            supported_assets, key=lambda asset: asset['size'], default=None)
+        if minimum_asset is not None:
+            latest_url = minimum_asset.get('browser_download_url')
+        else:
+            # Guess the tarball url for source code.
+            latest_url = 'https://github.com/{}/{}/archive/{}.tar.gz'.format(
+                self.owner, self.repo, self.data.get('tag_name'))
+
+        temporary_dir = None
+        try:
+            temporary_dir = archive_utils.download_and_extract(latest_url)
+            package_dir = archive_utils.find_archive_root(temporary_dir)
+            self._write_metadata(latest_url, package_dir)
+            updater_utils.replace_package(package_dir, self.proj_path)
+        finally:
+            shutil.rmtree(temporary_dir, ignore_errors=True)
+            urllib.request.urlcleanup()