xref: /aosp_15_r20/tools/external_updater/github_archive_updater.py (revision 3c875a214f382db1236d28570d1304ce57138f32)
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