blob: 6e00cb9a5518b0e6637f3e9f01f3ee90cdd35900 [file] [log] [blame]
(raulenrique)dfdda472018-06-04 12:02:29 -07001# Copyright (C) 2018 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Module to update packages from GitHub archive."""
15
(raulenrique)dfdda472018-06-04 12:02:29 -070016import json
17import re
(raulenrique)dfdda472018-06-04 12:02:29 -070018import urllib.request
Haibo Huang329e6812020-05-29 14:12:20 -070019import urllib.error
20from typing import List, Optional, Tuple
(raulenrique)dfdda472018-06-04 12:02:29 -070021
22import archive_utils
Haibo Huang329e6812020-05-29 14:12:20 -070023from base_updater import Updater
Haibo Huang8845e1e2018-09-06 17:02:45 -070024import git_utils
Thiébaud Weksteen4ac289b2020-09-28 15:23:29 +020025# pylint: disable=import-error
(raulenrique)dfdda472018-06-04 12:02:29 -070026import updater_utils
Haibo Huang329e6812020-05-29 14:12:20 -070027GITHUB_URL_PATTERN: str = (r'^https:\/\/github.com\/([-\w]+)\/([-\w]+)\/' +
28 r'(releases\/download\/|archive\/)')
29GITHUB_URL_RE: re.Pattern = re.compile(GITHUB_URL_PATTERN)
(raulenrique)dfdda472018-06-04 12:02:29 -070030
31
Haibo Huang329e6812020-05-29 14:12:20 -070032def _edit_distance(str1: str, str2: str) -> int:
Haibo Huang9dcade42018-08-03 11:52:25 -070033 prev = list(range(0, len(str2) + 1))
34 for i, chr1 in enumerate(str1):
35 cur = [i + 1]
36 for j, chr2 in enumerate(str2):
37 if chr1 == chr2:
38 cur.append(prev[j])
39 else:
40 cur.append(min(prev[j + 1], prev[j], cur[j]) + 1)
41 prev = cur
42 return prev[len(str2)]
43
44
Haibo Huang329e6812020-05-29 14:12:20 -070045def choose_best_url(urls: List[str], previous_url: str) -> str:
Haibo Huang9dcade42018-08-03 11:52:25 -070046 """Returns the best url to download from a list of candidate urls.
47
48 This function calculates similarity between previous url and each of new
49 urls. And returns the one best matches previous url.
50
51 Similarity is measured by editing distance.
52
53 Args:
54 urls: Array of candidate urls.
55 previous_url: String of the url used previously.
56
57 Returns:
58 One url from `urls`.
59 """
Haibo Huang329e6812020-05-29 14:12:20 -070060 return min(urls,
61 default="",
62 key=lambda url: _edit_distance(url, previous_url))
Haibo Huang9dcade42018-08-03 11:52:25 -070063
64
Haibo Huang329e6812020-05-29 14:12:20 -070065class GithubArchiveUpdater(Updater):
(raulenrique)dfdda472018-06-04 12:02:29 -070066 """Updater for archives from GitHub.
67
68 This updater supports release archives in GitHub. Version is determined by
69 release name in GitHub.
70 """
71
Sadaf Ebrahimi9afd3102023-04-28 17:33:44 +000072 UPSTREAM_REMOTE_NAME: str = "update_origin"
Haibo Huang329e6812020-05-29 14:12:20 -070073 VERSION_FIELD: str = 'tag_name'
74 owner: str
75 repo: str
(raulenrique)dfdda472018-06-04 12:02:29 -070076
Haibo Huang329e6812020-05-29 14:12:20 -070077 def is_supported_url(self) -> bool:
Stephen Hines5dd2a492023-11-22 16:40:34 -080078 if self._old_identifier.type.lower() != 'archive':
Haibo Huang329e6812020-05-29 14:12:20 -070079 return False
Stephen Hines5dd2a492023-11-22 16:40:34 -080080 match = GITHUB_URL_RE.match(self._old_identifier.value)
(raulenrique)dfdda472018-06-04 12:02:29 -070081 if match is None:
Haibo Huang329e6812020-05-29 14:12:20 -070082 return False
(raulenrique)dfdda472018-06-04 12:02:29 -070083 try:
84 self.owner, self.repo = match.group(1, 2)
85 except IndexError:
Haibo Huang329e6812020-05-29 14:12:20 -070086 return False
87 return True
(raulenrique)dfdda472018-06-04 12:02:29 -070088
Haibo Huang329e6812020-05-29 14:12:20 -070089 def _fetch_latest_release(self) -> Optional[Tuple[str, List[str]]]:
Thiébaud Weksteen4ac289b2020-09-28 15:23:29 +020090 # pylint: disable=line-too-long
Haibo Huang329e6812020-05-29 14:12:20 -070091 url = f'https://api.github.com/repos/{self.owner}/{self.repo}/releases/latest'
Haibo Huang065d1ba2020-04-23 18:22:58 -070092 try:
93 with urllib.request.urlopen(url) as request:
94 data = json.loads(request.read().decode())
Haibo Huang329e6812020-05-29 14:12:20 -070095 except urllib.error.HTTPError as err:
96 if err.code == 404:
Haibo Huang065d1ba2020-04-23 18:22:58 -070097 return None
98 raise
Haibo Huang8845e1e2018-09-06 17:02:45 -070099 supported_assets = [
100 a['browser_download_url'] for a in data['assets']
Haibo Huang329e6812020-05-29 14:12:20 -0700101 if archive_utils.is_supported_archive(a['browser_download_url'])
102 ]
Sadaf Ebrahimi28f76a12024-02-05 20:39:32 +0000103 return data[self.VERSION_FIELD], supported_assets
Haibo Huang065d1ba2020-04-23 18:22:58 -0700104
Sadaf Ebrahimi602dd322023-06-23 19:30:07 +0000105 def setup_remote(self) -> None:
Sadaf Ebrahimi9afd3102023-04-28 17:33:44 +0000106 homepage = f'https://github.com/{self.owner}/{self.repo}'
107 remotes = git_utils.list_remotes(self._proj_path)
108 current_remote_url = None
109 for name, url in remotes.items():
110 if name == self.UPSTREAM_REMOTE_NAME:
111 current_remote_url = url
112
113 if current_remote_url is not None and current_remote_url != homepage:
114 git_utils.remove_remote(self._proj_path, self.UPSTREAM_REMOTE_NAME)
115 current_remote_url = None
116
117 if current_remote_url is None:
118 git_utils.add_remote(self._proj_path, self.UPSTREAM_REMOTE_NAME, homepage)
119
Sadaf Ebrahimi2b3546c2024-02-28 21:37:12 +0000120 git_utils.fetch(self._proj_path, self.UPSTREAM_REMOTE_NAME)
Sadaf Ebrahimi9afd3102023-04-28 17:33:44 +0000121
Haibo Huanga08fb602020-05-29 16:24:13 -0700122 def _fetch_latest_tag(self) -> Tuple[str, List[str]]:
Sadaf Ebrahimi9afd3102023-04-28 17:33:44 +0000123 """We want to avoid hitting GitHub API rate limit by using alternative solutions."""
Sadaf Ebrahimi2b3546c2024-02-28 21:37:12 +0000124 tags = git_utils.list_remote_tags(self._proj_path, self.UPSTREAM_REMOTE_NAME)
125 parsed_tags = [updater_utils.parse_remote_tag(tag) for tag in tags]
Sadaf Ebrahimid0ae4052024-03-06 20:41:37 +0000126 tag = updater_utils.get_latest_stable_release_tag(self._old_identifier.version, parsed_tags)
Sadaf Ebrahimi9afd3102023-04-28 17:33:44 +0000127 return tag, []
Haibo Huang065d1ba2020-04-23 18:22:58 -0700128
Haibo Huang329e6812020-05-29 14:12:20 -0700129 def _fetch_latest_version(self) -> None:
Haibo Huang065d1ba2020-04-23 18:22:58 -0700130 """Checks upstream and gets the latest release tag."""
Stephen Hines5dd2a492023-11-22 16:40:34 -0800131 self._new_identifier.version, urls = (self._fetch_latest_release()
Haibo Huanga08fb602020-05-29 16:24:13 -0700132 or self._fetch_latest_tag())
Haibo Huang8845e1e2018-09-06 17:02:45 -0700133
134 # Adds source code urls.
Sadaf Ebrahimi28f76a12024-02-05 20:39:32 +0000135 urls.append(f'https://github.com/{self.owner}/{self.repo}/archive/'
136 f'{self._new_identifier.version}.tar.gz')
137 urls.append(f'https://github.com/{self.owner}/{self.repo}/archive/'
138 f'{self._new_identifier.version}.zip')
Haibo Huang8845e1e2018-09-06 17:02:45 -0700139
Stephen Hines5dd2a492023-11-22 16:40:34 -0800140 self._new_identifier.value = choose_best_url(urls, self._old_identifier.value)
Haibo Huang8845e1e2018-09-06 17:02:45 -0700141
Haibo Huang329e6812020-05-29 14:12:20 -0700142 def _fetch_latest_commit(self) -> None:
Sadaf Ebrahimi8fe4c122023-07-11 20:55:42 +0000143 """Checks upstream and gets the latest commit to default branch."""
Haibo Huang8845e1e2018-09-06 17:02:45 -0700144
Thiébaud Weksteen4ac289b2020-09-28 15:23:29 +0200145 # pylint: disable=line-too-long
Sadaf Ebrahimi9afd3102023-04-28 17:33:44 +0000146 branch = git_utils.detect_default_branch(self._proj_path,
147 self.UPSTREAM_REMOTE_NAME)
Stephen Hines5dd2a492023-11-22 16:40:34 -0800148 self._new_identifier.version = git_utils.get_sha_for_branch(
Sadaf Ebrahimi9afd3102023-04-28 17:33:44 +0000149 self._proj_path, self.UPSTREAM_REMOTE_NAME + '/' + branch)
Stephen Hines5dd2a492023-11-22 16:40:34 -0800150 self._new_identifier.value = (
Thiébaud Weksteen4ac289b2020-09-28 15:23:29 +0200151 # pylint: disable=line-too-long
Stephen Hines5dd2a492023-11-22 16:40:34 -0800152 f'https://github.com/{self.owner}/{self.repo}/archive/{self._new_identifier.version}.zip'
Haibo Huanga08fb602020-05-29 16:24:13 -0700153 )
(raulenrique)dfdda472018-06-04 12:02:29 -0700154
Haibo Huanga08fb602020-05-29 16:24:13 -0700155 def check(self) -> None:
Haibo Huang24950e72018-06-29 14:53:39 -0700156 """Checks update for package.
157
158 Returns True if a new version is available.
159 """
Sadaf Ebrahimi602dd322023-06-23 19:30:07 +0000160 self.setup_remote()
Stephen Hines5dd2a492023-11-22 16:40:34 -0800161 if git_utils.is_commit(self._old_identifier.version):
Haibo Huang39aaab62019-01-25 12:23:03 -0800162 self._fetch_latest_commit()
Haibo Huang8845e1e2018-09-06 17:02:45 -0700163 else:
Haibo Huang39aaab62019-01-25 12:23:03 -0800164 self._fetch_latest_version()
Haibo Huang24950e72018-06-29 14:53:39 -0700165
Sadaf Ebrahimi7d0bab72023-10-23 19:45:03 +0000166 def update(self) -> None:
(raulenrique)dfdda472018-06-04 12:02:29 -0700167 """Updates the package.
168
Haibo Huang24950e72018-06-29 14:53:39 -0700169 Has to call check() before this function.
(raulenrique)dfdda472018-06-04 12:02:29 -0700170 """
(raulenrique)dfdda472018-06-04 12:02:29 -0700171 temporary_dir = None
172 try:
Haibo Huanga08fb602020-05-29 16:24:13 -0700173 temporary_dir = archive_utils.download_and_extract(
Stephen Hines5dd2a492023-11-22 16:40:34 -0800174 self._new_identifier.value)
(raulenrique)dfdda472018-06-04 12:02:29 -0700175 package_dir = archive_utils.find_archive_root(temporary_dir)
Haibo Huang329e6812020-05-29 14:12:20 -0700176 updater_utils.replace_package(package_dir, self._proj_path)
(raulenrique)dfdda472018-06-04 12:02:29 -0700177 finally:
Elliott Hughes1c3a4f42018-08-03 15:35:48 -0700178 # Don't remove the temporary directory, or it'll be impossible
179 # to debug the failure...
180 # shutil.rmtree(temporary_dir, ignore_errors=True)
(raulenrique)dfdda472018-06-04 12:02:29 -0700181 urllib.request.urlcleanup()