1*3c875a21SAndroid Build Coastguard Worker# Copyright (C) 2018 The Android Open Source Project 2*3c875a21SAndroid Build Coastguard Worker# 3*3c875a21SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 4*3c875a21SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 5*3c875a21SAndroid Build Coastguard Worker# You may obtain a copy of the License at 6*3c875a21SAndroid Build Coastguard Worker# 7*3c875a21SAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 8*3c875a21SAndroid Build Coastguard Worker# 9*3c875a21SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 10*3c875a21SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 11*3c875a21SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*3c875a21SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 13*3c875a21SAndroid Build Coastguard Worker# limitations under the License. 14*3c875a21SAndroid Build Coastguard Worker"""Module to update packages from GitHub archive.""" 15*3c875a21SAndroid Build Coastguard Worker 16*3c875a21SAndroid Build Coastguard Workerimport json 17*3c875a21SAndroid Build Coastguard Workerimport re 18*3c875a21SAndroid Build Coastguard Workerimport urllib.request 19*3c875a21SAndroid Build Coastguard Workerimport urllib.error 20*3c875a21SAndroid Build Coastguard Workerfrom typing import List, Optional, Tuple 21*3c875a21SAndroid Build Coastguard Worker 22*3c875a21SAndroid Build Coastguard Workerimport archive_utils 23*3c875a21SAndroid Build Coastguard Workerfrom base_updater import Updater 24*3c875a21SAndroid Build Coastguard Workerimport git_utils 25*3c875a21SAndroid Build Coastguard Worker# pylint: disable=import-error 26*3c875a21SAndroid Build Coastguard Workerimport updater_utils 27*3c875a21SAndroid Build Coastguard WorkerGITHUB_URL_PATTERN: str = (r'^https:\/\/github.com\/([-\w]+)\/([-\w]+)\/' + 28*3c875a21SAndroid Build Coastguard Worker r'(releases\/download\/|archive\/)') 29*3c875a21SAndroid Build Coastguard WorkerGITHUB_URL_RE: re.Pattern = re.compile(GITHUB_URL_PATTERN) 30*3c875a21SAndroid Build Coastguard Worker 31*3c875a21SAndroid Build Coastguard Worker 32*3c875a21SAndroid Build Coastguard Workerdef _edit_distance(str1: str, str2: str) -> int: 33*3c875a21SAndroid Build Coastguard Worker prev = list(range(0, len(str2) + 1)) 34*3c875a21SAndroid Build Coastguard Worker for i, chr1 in enumerate(str1): 35*3c875a21SAndroid Build Coastguard Worker cur = [i + 1] 36*3c875a21SAndroid Build Coastguard Worker for j, chr2 in enumerate(str2): 37*3c875a21SAndroid Build Coastguard Worker if chr1 == chr2: 38*3c875a21SAndroid Build Coastguard Worker cur.append(prev[j]) 39*3c875a21SAndroid Build Coastguard Worker else: 40*3c875a21SAndroid Build Coastguard Worker cur.append(min(prev[j + 1], prev[j], cur[j]) + 1) 41*3c875a21SAndroid Build Coastguard Worker prev = cur 42*3c875a21SAndroid Build Coastguard Worker return prev[len(str2)] 43*3c875a21SAndroid Build Coastguard Worker 44*3c875a21SAndroid Build Coastguard Worker 45*3c875a21SAndroid Build Coastguard Workerdef choose_best_url(urls: List[str], previous_url: str) -> str: 46*3c875a21SAndroid Build Coastguard Worker """Returns the best url to download from a list of candidate urls. 47*3c875a21SAndroid Build Coastguard Worker 48*3c875a21SAndroid Build Coastguard Worker This function calculates similarity between previous url and each of new 49*3c875a21SAndroid Build Coastguard Worker urls. And returns the one best matches previous url. 50*3c875a21SAndroid Build Coastguard Worker 51*3c875a21SAndroid Build Coastguard Worker Similarity is measured by editing distance. 52*3c875a21SAndroid Build Coastguard Worker 53*3c875a21SAndroid Build Coastguard Worker Args: 54*3c875a21SAndroid Build Coastguard Worker urls: Array of candidate urls. 55*3c875a21SAndroid Build Coastguard Worker previous_url: String of the url used previously. 56*3c875a21SAndroid Build Coastguard Worker 57*3c875a21SAndroid Build Coastguard Worker Returns: 58*3c875a21SAndroid Build Coastguard Worker One url from `urls`. 59*3c875a21SAndroid Build Coastguard Worker """ 60*3c875a21SAndroid Build Coastguard Worker return min(urls, 61*3c875a21SAndroid Build Coastguard Worker default="", 62*3c875a21SAndroid Build Coastguard Worker key=lambda url: _edit_distance(url, previous_url)) 63*3c875a21SAndroid Build Coastguard Worker 64*3c875a21SAndroid Build Coastguard Worker 65*3c875a21SAndroid Build Coastguard Workerclass GithubArchiveUpdater(Updater): 66*3c875a21SAndroid Build Coastguard Worker """Updater for archives from GitHub. 67*3c875a21SAndroid Build Coastguard Worker 68*3c875a21SAndroid Build Coastguard Worker This updater supports release archives in GitHub. Version is determined by 69*3c875a21SAndroid Build Coastguard Worker release name in GitHub. 70*3c875a21SAndroid Build Coastguard Worker """ 71*3c875a21SAndroid Build Coastguard Worker 72*3c875a21SAndroid Build Coastguard Worker UPSTREAM_REMOTE_NAME: str = "update_origin" 73*3c875a21SAndroid Build Coastguard Worker VERSION_FIELD: str = 'tag_name' 74*3c875a21SAndroid Build Coastguard Worker owner: str 75*3c875a21SAndroid Build Coastguard Worker repo: str 76*3c875a21SAndroid Build Coastguard Worker 77*3c875a21SAndroid Build Coastguard Worker def is_supported_url(self) -> bool: 78*3c875a21SAndroid Build Coastguard Worker if self._old_identifier.type.lower() != 'archive': 79*3c875a21SAndroid Build Coastguard Worker return False 80*3c875a21SAndroid Build Coastguard Worker match = GITHUB_URL_RE.match(self._old_identifier.value) 81*3c875a21SAndroid Build Coastguard Worker if match is None: 82*3c875a21SAndroid Build Coastguard Worker return False 83*3c875a21SAndroid Build Coastguard Worker try: 84*3c875a21SAndroid Build Coastguard Worker self.owner, self.repo = match.group(1, 2) 85*3c875a21SAndroid Build Coastguard Worker except IndexError: 86*3c875a21SAndroid Build Coastguard Worker return False 87*3c875a21SAndroid Build Coastguard Worker return True 88*3c875a21SAndroid Build Coastguard Worker 89*3c875a21SAndroid Build Coastguard Worker def _fetch_latest_release(self) -> Optional[Tuple[str, List[str]]]: 90*3c875a21SAndroid Build Coastguard Worker # pylint: disable=line-too-long 91*3c875a21SAndroid Build Coastguard Worker url = f'https://api.github.com/repos/{self.owner}/{self.repo}/releases/latest' 92*3c875a21SAndroid Build Coastguard Worker try: 93*3c875a21SAndroid Build Coastguard Worker with urllib.request.urlopen(url) as request: 94*3c875a21SAndroid Build Coastguard Worker data = json.loads(request.read().decode()) 95*3c875a21SAndroid Build Coastguard Worker except urllib.error.HTTPError as err: 96*3c875a21SAndroid Build Coastguard Worker if err.code == 404: 97*3c875a21SAndroid Build Coastguard Worker return None 98*3c875a21SAndroid Build Coastguard Worker raise 99*3c875a21SAndroid Build Coastguard Worker supported_assets = [ 100*3c875a21SAndroid Build Coastguard Worker a['browser_download_url'] for a in data['assets'] 101*3c875a21SAndroid Build Coastguard Worker if archive_utils.is_supported_archive(a['browser_download_url']) 102*3c875a21SAndroid Build Coastguard Worker ] 103*3c875a21SAndroid Build Coastguard Worker return data[self.VERSION_FIELD], supported_assets 104*3c875a21SAndroid Build Coastguard Worker 105*3c875a21SAndroid Build Coastguard Worker def setup_remote(self) -> None: 106*3c875a21SAndroid Build Coastguard Worker homepage = f'https://github.com/{self.owner}/{self.repo}' 107*3c875a21SAndroid Build Coastguard Worker remotes = git_utils.list_remotes(self._proj_path) 108*3c875a21SAndroid Build Coastguard Worker current_remote_url = None 109*3c875a21SAndroid Build Coastguard Worker for name, url in remotes.items(): 110*3c875a21SAndroid Build Coastguard Worker if name == self.UPSTREAM_REMOTE_NAME: 111*3c875a21SAndroid Build Coastguard Worker current_remote_url = url 112*3c875a21SAndroid Build Coastguard Worker 113*3c875a21SAndroid Build Coastguard Worker if current_remote_url is not None and current_remote_url != homepage: 114*3c875a21SAndroid Build Coastguard Worker git_utils.remove_remote(self._proj_path, self.UPSTREAM_REMOTE_NAME) 115*3c875a21SAndroid Build Coastguard Worker current_remote_url = None 116*3c875a21SAndroid Build Coastguard Worker 117*3c875a21SAndroid Build Coastguard Worker if current_remote_url is None: 118*3c875a21SAndroid Build Coastguard Worker git_utils.add_remote(self._proj_path, self.UPSTREAM_REMOTE_NAME, homepage) 119*3c875a21SAndroid Build Coastguard Worker 120*3c875a21SAndroid Build Coastguard Worker git_utils.fetch(self._proj_path, self.UPSTREAM_REMOTE_NAME) 121*3c875a21SAndroid Build Coastguard Worker 122*3c875a21SAndroid Build Coastguard Worker def create_tar_gz_url(self) -> str: 123*3c875a21SAndroid Build Coastguard Worker url = f'https://github.com/{self.owner}/{self.repo}/archive/' \ 124*3c875a21SAndroid Build Coastguard Worker f'{self._new_identifier.version}.tar.gz' 125*3c875a21SAndroid Build Coastguard Worker return url 126*3c875a21SAndroid Build Coastguard Worker 127*3c875a21SAndroid Build Coastguard Worker def create_zip_url(self) -> str: 128*3c875a21SAndroid Build Coastguard Worker url = f'https://github.com/{self.owner}/{self.repo}/archive/' \ 129*3c875a21SAndroid Build Coastguard Worker f'{self._new_identifier.version}.zip' 130*3c875a21SAndroid Build Coastguard Worker return url 131*3c875a21SAndroid Build Coastguard Worker 132*3c875a21SAndroid Build Coastguard Worker def _fetch_latest_tag(self) -> Tuple[str, List[str]]: 133*3c875a21SAndroid Build Coastguard Worker """We want to avoid hitting GitHub API rate limit by using alternative solutions.""" 134*3c875a21SAndroid Build Coastguard Worker tags = git_utils.list_remote_tags(self._proj_path, self.UPSTREAM_REMOTE_NAME) 135*3c875a21SAndroid Build Coastguard Worker parsed_tags = [updater_utils.parse_remote_tag(tag) for tag in tags] 136*3c875a21SAndroid Build Coastguard Worker tag = updater_utils.get_latest_stable_release_tag(self._old_identifier.version, parsed_tags) 137*3c875a21SAndroid Build Coastguard Worker return tag, [] 138*3c875a21SAndroid Build Coastguard Worker 139*3c875a21SAndroid Build Coastguard Worker def _fetch_latest_tag_or_release(self) -> None: 140*3c875a21SAndroid Build Coastguard Worker """Checks upstream and gets the latest release tag.""" 141*3c875a21SAndroid Build Coastguard Worker self._new_identifier.version, urls = (self._fetch_latest_release() 142*3c875a21SAndroid Build Coastguard Worker or self._fetch_latest_tag()) 143*3c875a21SAndroid Build Coastguard Worker 144*3c875a21SAndroid Build Coastguard Worker # Adds source code urls. 145*3c875a21SAndroid Build Coastguard Worker urls.append(self.create_tar_gz_url()) 146*3c875a21SAndroid Build Coastguard Worker urls.append(self.create_zip_url()) 147*3c875a21SAndroid Build Coastguard Worker 148*3c875a21SAndroid Build Coastguard Worker self._new_identifier.value = choose_best_url(urls, self._old_identifier.value) 149*3c875a21SAndroid Build Coastguard Worker 150*3c875a21SAndroid Build Coastguard Worker def _fetch_latest_commit(self) -> None: 151*3c875a21SAndroid Build Coastguard Worker """Checks upstream and gets the latest commit to default branch.""" 152*3c875a21SAndroid Build Coastguard Worker 153*3c875a21SAndroid Build Coastguard Worker # pylint: disable=line-too-long 154*3c875a21SAndroid Build Coastguard Worker branch = git_utils.detect_default_branch(self._proj_path, 155*3c875a21SAndroid Build Coastguard Worker self.UPSTREAM_REMOTE_NAME) 156*3c875a21SAndroid Build Coastguard Worker self._new_identifier.version = git_utils.get_sha_for_branch( 157*3c875a21SAndroid Build Coastguard Worker self._proj_path, self.UPSTREAM_REMOTE_NAME + '/' + branch) 158*3c875a21SAndroid Build Coastguard Worker self._new_identifier.value = ( 159*3c875a21SAndroid Build Coastguard Worker # pylint: disable=line-too-long 160*3c875a21SAndroid Build Coastguard Worker f'https://github.com/{self.owner}/{self.repo}/archive/{self._new_identifier.version}.zip' 161*3c875a21SAndroid Build Coastguard Worker ) 162*3c875a21SAndroid Build Coastguard Worker 163*3c875a21SAndroid Build Coastguard Worker def set_custom_version(self, custom_version: str) -> None: 164*3c875a21SAndroid Build Coastguard Worker super().set_custom_version(custom_version) 165*3c875a21SAndroid Build Coastguard Worker tar_gz_url = self.create_tar_gz_url() 166*3c875a21SAndroid Build Coastguard Worker zip_url = self.create_zip_url() 167*3c875a21SAndroid Build Coastguard Worker self._new_identifier.value = choose_best_url([tar_gz_url, zip_url], self._old_identifier.value) 168*3c875a21SAndroid Build Coastguard Worker 169*3c875a21SAndroid Build Coastguard Worker def check(self) -> None: 170*3c875a21SAndroid Build Coastguard Worker """Checks update for package. 171*3c875a21SAndroid Build Coastguard Worker 172*3c875a21SAndroid Build Coastguard Worker Returns True if a new version is available. 173*3c875a21SAndroid Build Coastguard Worker """ 174*3c875a21SAndroid Build Coastguard Worker self.setup_remote() 175*3c875a21SAndroid Build Coastguard Worker 176*3c875a21SAndroid Build Coastguard Worker if git_utils.is_commit(self._old_identifier.version): 177*3c875a21SAndroid Build Coastguard Worker self._fetch_latest_commit() 178*3c875a21SAndroid Build Coastguard Worker else: 179*3c875a21SAndroid Build Coastguard Worker self._fetch_latest_tag_or_release() 180*3c875a21SAndroid Build Coastguard Worker 181*3c875a21SAndroid Build Coastguard Worker def update(self) -> None: 182*3c875a21SAndroid Build Coastguard Worker """Updates the package. 183*3c875a21SAndroid Build Coastguard Worker 184*3c875a21SAndroid Build Coastguard Worker Has to call check() before this function. 185*3c875a21SAndroid Build Coastguard Worker """ 186*3c875a21SAndroid Build Coastguard Worker temporary_dir = None 187*3c875a21SAndroid Build Coastguard Worker try: 188*3c875a21SAndroid Build Coastguard Worker temporary_dir = archive_utils.download_and_extract( 189*3c875a21SAndroid Build Coastguard Worker self._new_identifier.value) 190*3c875a21SAndroid Build Coastguard Worker package_dir = archive_utils.find_archive_root(temporary_dir) 191*3c875a21SAndroid Build Coastguard Worker updater_utils.replace_package(package_dir, self._proj_path) 192*3c875a21SAndroid Build Coastguard Worker finally: 193*3c875a21SAndroid Build Coastguard Worker # Don't remove the temporary directory, or it'll be impossible 194*3c875a21SAndroid Build Coastguard Worker # to debug the failure... 195*3c875a21SAndroid Build Coastguard Worker # shutil.rmtree(temporary_dir, ignore_errors=True) 196*3c875a21SAndroid Build Coastguard Worker urllib.request.urlcleanup() 197