1*3c875a21SAndroid Build Coastguard Worker# Copyright (C) 2020 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 check updates from crates.io.""" 15*3c875a21SAndroid Build Coastguard Worker 16*3c875a21SAndroid Build Coastguard Workerimport json 17*3c875a21SAndroid Build Coastguard Workerimport os 18*3c875a21SAndroid Build Coastguard Workerfrom pathlib import Path 19*3c875a21SAndroid Build Coastguard Workerimport re 20*3c875a21SAndroid Build Coastguard Workerimport shutil 21*3c875a21SAndroid Build Coastguard Workerimport tempfile 22*3c875a21SAndroid Build Coastguard Workerimport urllib.request 23*3c875a21SAndroid Build Coastguard Workerfrom typing import IO 24*3c875a21SAndroid Build Coastguard Worker 25*3c875a21SAndroid Build Coastguard Workerimport archive_utils 26*3c875a21SAndroid Build Coastguard Workerfrom base_updater import Updater 27*3c875a21SAndroid Build Coastguard Workerimport git_utils 28*3c875a21SAndroid Build Coastguard Worker# pylint: disable=import-error 29*3c875a21SAndroid Build Coastguard Workerimport metadata_pb2 # type: ignore 30*3c875a21SAndroid Build Coastguard Workerimport updater_utils 31*3c875a21SAndroid Build Coastguard Worker 32*3c875a21SAndroid Build Coastguard WorkerLIBRARY_NAME_PATTERN: str = r"([-\w]+)" 33*3c875a21SAndroid Build Coastguard Worker 34*3c875a21SAndroid Build Coastguard WorkerALPHA_BETA_PATTERN: str = r"^.*[0-9]+\.[0-9]+\.[0-9]+-(alpha|beta).*" 35*3c875a21SAndroid Build Coastguard Worker 36*3c875a21SAndroid Build Coastguard WorkerALPHA_BETA_RE: re.Pattern = re.compile(ALPHA_BETA_PATTERN) 37*3c875a21SAndroid Build Coastguard Worker 38*3c875a21SAndroid Build Coastguard Worker"""Match both x.y.z and x.y.z+a.b.c which is used by some Vulkan binding libraries""" 39*3c875a21SAndroid Build Coastguard WorkerVERSION_PATTERN: str = r"([0-9]+)\.([0-9]+)\.([0-9]+)(\+([0-9]+)\.([0-9]+)\.([0-9]+))?" 40*3c875a21SAndroid Build Coastguard Worker 41*3c875a21SAndroid Build Coastguard WorkerVERSION_RE: re.Pattern = re.compile(VERSION_PATTERN) 42*3c875a21SAndroid Build Coastguard Worker 43*3c875a21SAndroid Build Coastguard WorkerCRATES_IO_ARCHIVE_URL_PATTERN: str = (r"^https:\/\/static.crates.io\/crates\/" + 44*3c875a21SAndroid Build Coastguard Worker LIBRARY_NAME_PATTERN + "/" + 45*3c875a21SAndroid Build Coastguard Worker LIBRARY_NAME_PATTERN + "-" + 46*3c875a21SAndroid Build Coastguard Worker "(.*?)" + ".crate") 47*3c875a21SAndroid Build Coastguard Worker 48*3c875a21SAndroid Build Coastguard WorkerCRATES_IO_ARCHIVE_URL_RE: re.Pattern = re.compile(CRATES_IO_ARCHIVE_URL_PATTERN) 49*3c875a21SAndroid Build Coastguard Worker 50*3c875a21SAndroid Build Coastguard WorkerDESCRIPTION_PATTERN: str = r"^description *= *(\".+\")" 51*3c875a21SAndroid Build Coastguard Worker 52*3c875a21SAndroid Build Coastguard WorkerDESCRIPTION_RE: re.Pattern = re.compile(DESCRIPTION_PATTERN) 53*3c875a21SAndroid Build Coastguard Worker 54*3c875a21SAndroid Build Coastguard Worker 55*3c875a21SAndroid Build Coastguard Workerclass CratesUpdater(Updater): 56*3c875a21SAndroid Build Coastguard Worker """Updater for crates.io packages.""" 57*3c875a21SAndroid Build Coastguard Worker 58*3c875a21SAndroid Build Coastguard Worker UPSTREAM_REMOTE_NAME: str = "update_origin" 59*3c875a21SAndroid Build Coastguard Worker download_url: str 60*3c875a21SAndroid Build Coastguard Worker package: str 61*3c875a21SAndroid Build Coastguard Worker package_dir: str 62*3c875a21SAndroid Build Coastguard Worker temp_file: IO 63*3c875a21SAndroid Build Coastguard Worker 64*3c875a21SAndroid Build Coastguard Worker def is_supported_url(self) -> bool: 65*3c875a21SAndroid Build Coastguard Worker match = CRATES_IO_ARCHIVE_URL_RE.match(self._old_identifier.value) 66*3c875a21SAndroid Build Coastguard Worker if match is None: 67*3c875a21SAndroid Build Coastguard Worker return False 68*3c875a21SAndroid Build Coastguard Worker self.package = match.group(1) 69*3c875a21SAndroid Build Coastguard Worker return True 70*3c875a21SAndroid Build Coastguard Worker 71*3c875a21SAndroid Build Coastguard Worker def setup_remote(self) -> None: 72*3c875a21SAndroid Build Coastguard Worker url = "https://crates.io/api/v1/crates/" + self.package 73*3c875a21SAndroid Build Coastguard Worker with urllib.request.urlopen(url) as request: 74*3c875a21SAndroid Build Coastguard Worker data = json.loads(request.read().decode()) 75*3c875a21SAndroid Build Coastguard Worker homepage = data["crate"]["repository"] 76*3c875a21SAndroid Build Coastguard Worker remotes = git_utils.list_remotes(self._proj_path) 77*3c875a21SAndroid Build Coastguard Worker current_remote_url = None 78*3c875a21SAndroid Build Coastguard Worker for name, url in remotes.items(): 79*3c875a21SAndroid Build Coastguard Worker if name == self.UPSTREAM_REMOTE_NAME: 80*3c875a21SAndroid Build Coastguard Worker current_remote_url = url 81*3c875a21SAndroid Build Coastguard Worker 82*3c875a21SAndroid Build Coastguard Worker if current_remote_url is not None and current_remote_url != homepage: 83*3c875a21SAndroid Build Coastguard Worker git_utils.remove_remote(self._proj_path, self.UPSTREAM_REMOTE_NAME) 84*3c875a21SAndroid Build Coastguard Worker current_remote_url = None 85*3c875a21SAndroid Build Coastguard Worker 86*3c875a21SAndroid Build Coastguard Worker if current_remote_url is None: 87*3c875a21SAndroid Build Coastguard Worker git_utils.add_remote(self._proj_path, self.UPSTREAM_REMOTE_NAME, homepage) 88*3c875a21SAndroid Build Coastguard Worker 89*3c875a21SAndroid Build Coastguard Worker branch = git_utils.detect_default_branch(self._proj_path, 90*3c875a21SAndroid Build Coastguard Worker self.UPSTREAM_REMOTE_NAME) 91*3c875a21SAndroid Build Coastguard Worker git_utils.fetch(self._proj_path, self.UPSTREAM_REMOTE_NAME, branch) 92*3c875a21SAndroid Build Coastguard Worker 93*3c875a21SAndroid Build Coastguard Worker def _get_version_numbers(self, version: str) -> tuple[int, int, int]: 94*3c875a21SAndroid Build Coastguard Worker match = VERSION_RE.match(version) 95*3c875a21SAndroid Build Coastguard Worker if match is not None: 96*3c875a21SAndroid Build Coastguard Worker return ( 97*3c875a21SAndroid Build Coastguard Worker int(match.group(1)), 98*3c875a21SAndroid Build Coastguard Worker int(match.group(2)), 99*3c875a21SAndroid Build Coastguard Worker int(match.group(3)), 100*3c875a21SAndroid Build Coastguard Worker ) 101*3c875a21SAndroid Build Coastguard Worker return (0, 0, 0) 102*3c875a21SAndroid Build Coastguard Worker 103*3c875a21SAndroid Build Coastguard Worker def _is_newer_version(self, prev_version: str, prev_id: int, 104*3c875a21SAndroid Build Coastguard Worker check_version: str, check_id: int): 105*3c875a21SAndroid Build Coastguard Worker """Return true if check_version+id is newer than prev_version+id.""" 106*3c875a21SAndroid Build Coastguard Worker return ((self._get_version_numbers(check_version), check_id) > 107*3c875a21SAndroid Build Coastguard Worker (self._get_version_numbers(prev_version), prev_id)) 108*3c875a21SAndroid Build Coastguard Worker 109*3c875a21SAndroid Build Coastguard Worker def _find_latest_non_test_version(self) -> None: 110*3c875a21SAndroid Build Coastguard Worker url = f"https://crates.io/api/v1/crates/{self.package}/versions" 111*3c875a21SAndroid Build Coastguard Worker with urllib.request.urlopen(url) as request: 112*3c875a21SAndroid Build Coastguard Worker data = json.loads(request.read().decode()) 113*3c875a21SAndroid Build Coastguard Worker last_id = 0 114*3c875a21SAndroid Build Coastguard Worker self._new_identifier.version = "" 115*3c875a21SAndroid Build Coastguard Worker for v in data["versions"]: 116*3c875a21SAndroid Build Coastguard Worker version = v["num"] 117*3c875a21SAndroid Build Coastguard Worker if (not v["yanked"] and not ALPHA_BETA_RE.match(version) and 118*3c875a21SAndroid Build Coastguard Worker self._is_newer_version( 119*3c875a21SAndroid Build Coastguard Worker self._new_identifier.version, last_id, version, int(v["id"]))): 120*3c875a21SAndroid Build Coastguard Worker last_id = int(v["id"]) 121*3c875a21SAndroid Build Coastguard Worker self._new_identifier.version = version 122*3c875a21SAndroid Build Coastguard Worker self.download_url = "https://crates.io" + v["dl_path"] 123*3c875a21SAndroid Build Coastguard Worker 124*3c875a21SAndroid Build Coastguard Worker def check(self) -> None: 125*3c875a21SAndroid Build Coastguard Worker """Checks crates.io and returns whether a new version is available.""" 126*3c875a21SAndroid Build Coastguard Worker url = "https://crates.io/api/v1/crates/" + self.package 127*3c875a21SAndroid Build Coastguard Worker with urllib.request.urlopen(url) as request: 128*3c875a21SAndroid Build Coastguard Worker data = json.loads(request.read().decode()) 129*3c875a21SAndroid Build Coastguard Worker self._new_identifier.version = data["crate"]["max_version"] 130*3c875a21SAndroid Build Coastguard Worker # Skip d.d.d-{alpha,beta}* versions 131*3c875a21SAndroid Build Coastguard Worker if ALPHA_BETA_RE.match(self._new_identifier.version): 132*3c875a21SAndroid Build Coastguard Worker print(f"Ignore alpha or beta release:{self.package}-{self._new_identifier.version}.") 133*3c875a21SAndroid Build Coastguard Worker self._find_latest_non_test_version() 134*3c875a21SAndroid Build Coastguard Worker else: 135*3c875a21SAndroid Build Coastguard Worker url = url + "/" + self._new_identifier.version 136*3c875a21SAndroid Build Coastguard Worker with urllib.request.urlopen(url) as request: 137*3c875a21SAndroid Build Coastguard Worker data = json.loads(request.read().decode()) 138*3c875a21SAndroid Build Coastguard Worker self.download_url = "https://crates.io" + data["version"]["dl_path"] 139*3c875a21SAndroid Build Coastguard Worker 140*3c875a21SAndroid Build Coastguard Worker def set_new_version_to_old(self): 141*3c875a21SAndroid Build Coastguard Worker super().refresh_without_upgrading() 142*3c875a21SAndroid Build Coastguard Worker # A shortcut to use the static download path. 143*3c875a21SAndroid Build Coastguard Worker self.download_url = f"https://static.crates.io/crates/{self.package}/" \ 144*3c875a21SAndroid Build Coastguard Worker f"{self.package}-{self._new_identifier.version}.crate" 145*3c875a21SAndroid Build Coastguard Worker 146*3c875a21SAndroid Build Coastguard Worker def update(self) -> None: 147*3c875a21SAndroid Build Coastguard Worker """Updates the package. 148*3c875a21SAndroid Build Coastguard Worker 149*3c875a21SAndroid Build Coastguard Worker Has to call check() before this function. 150*3c875a21SAndroid Build Coastguard Worker """ 151*3c875a21SAndroid Build Coastguard Worker try: 152*3c875a21SAndroid Build Coastguard Worker temporary_dir = archive_utils.download_and_extract(self.download_url) 153*3c875a21SAndroid Build Coastguard Worker self.package_dir = archive_utils.find_archive_root(temporary_dir) 154*3c875a21SAndroid Build Coastguard Worker self.temp_file = tempfile.NamedTemporaryFile() 155*3c875a21SAndroid Build Coastguard Worker updater_utils.replace_package(self.package_dir, self._proj_path, 156*3c875a21SAndroid Build Coastguard Worker self.temp_file.name) 157*3c875a21SAndroid Build Coastguard Worker self.check_for_errors() 158*3c875a21SAndroid Build Coastguard Worker finally: 159*3c875a21SAndroid Build Coastguard Worker urllib.request.urlcleanup() 160*3c875a21SAndroid Build Coastguard Worker 161*3c875a21SAndroid Build Coastguard Worker def rollback(self) -> bool: 162*3c875a21SAndroid Build Coastguard Worker # Only rollback if we have already swapped, 163*3c875a21SAndroid Build Coastguard Worker # which we denote by writing to this file. 164*3c875a21SAndroid Build Coastguard Worker if os.fstat(self.temp_file.fileno()).st_size > 0: 165*3c875a21SAndroid Build Coastguard Worker tmp_dir = tempfile.TemporaryDirectory() 166*3c875a21SAndroid Build Coastguard Worker shutil.move(self._proj_path, tmp_dir.name) 167*3c875a21SAndroid Build Coastguard Worker shutil.move(self.package_dir, self._proj_path) 168*3c875a21SAndroid Build Coastguard Worker shutil.move(Path(tmp_dir.name) / self.package, self.package_dir) 169*3c875a21SAndroid Build Coastguard Worker return True 170*3c875a21SAndroid Build Coastguard Worker return False 171*3c875a21SAndroid Build Coastguard Worker 172*3c875a21SAndroid Build Coastguard Worker def update_metadata(self, metadata: metadata_pb2.MetaData) -> metadata_pb2: 173*3c875a21SAndroid Build Coastguard Worker """Updates METADATA content.""" 174*3c875a21SAndroid Build Coastguard Worker # copy only HOMEPAGE url, and then add new ARCHIVE url. 175*3c875a21SAndroid Build Coastguard Worker updated_metadata = super().update_metadata(metadata) 176*3c875a21SAndroid Build Coastguard Worker for identifier in updated_metadata.third_party.identifier: 177*3c875a21SAndroid Build Coastguard Worker if identifier.version: 178*3c875a21SAndroid Build Coastguard Worker identifier.value = f"https://static.crates.io/crates/" \ 179*3c875a21SAndroid Build Coastguard Worker f"{updated_metadata.name}/"\ 180*3c875a21SAndroid Build Coastguard Worker f"{updated_metadata.name}" \ 181*3c875a21SAndroid Build Coastguard Worker f"-{self.latest_identifier.version}.crate" 182*3c875a21SAndroid Build Coastguard Worker break 183*3c875a21SAndroid Build Coastguard Worker # copy description from Cargo.toml to METADATA 184*3c875a21SAndroid Build Coastguard Worker cargo_toml = os.path.join(self.project_path, "Cargo.toml") 185*3c875a21SAndroid Build Coastguard Worker description = self._get_cargo_description(cargo_toml) 186*3c875a21SAndroid Build Coastguard Worker if description and description != updated_metadata.description: 187*3c875a21SAndroid Build Coastguard Worker print("New METADATA description:", description) 188*3c875a21SAndroid Build Coastguard Worker updated_metadata.description = description 189*3c875a21SAndroid Build Coastguard Worker return updated_metadata 190*3c875a21SAndroid Build Coastguard Worker 191*3c875a21SAndroid Build Coastguard Worker def check_for_errors(self) -> None: 192*3c875a21SAndroid Build Coastguard Worker # Check for .rej patches from failing to apply patches. 193*3c875a21SAndroid Build Coastguard Worker # If this has too many false positives, we could either 194*3c875a21SAndroid Build Coastguard Worker # check if the files are modified by patches or somehow 195*3c875a21SAndroid Build Coastguard Worker # track which files existed before the patching. 196*3c875a21SAndroid Build Coastguard Worker rejects = list(self._proj_path.glob('**/*.rej')) 197*3c875a21SAndroid Build Coastguard Worker if len(rejects) > 0: 198*3c875a21SAndroid Build Coastguard Worker print(f"Error: Found patch reject files: {str(rejects)}") 199*3c875a21SAndroid Build Coastguard Worker self._has_errors = True 200*3c875a21SAndroid Build Coastguard Worker 201*3c875a21SAndroid Build Coastguard Worker def _toml2str(self, line: str) -> str: 202*3c875a21SAndroid Build Coastguard Worker """Convert a quoted toml string to a Python str without quotes.""" 203*3c875a21SAndroid Build Coastguard Worker if line.startswith("\"\"\""): 204*3c875a21SAndroid Build Coastguard Worker return "" # cannot handle broken multi-line description 205*3c875a21SAndroid Build Coastguard Worker # TOML string escapes: \b \t \n \f \r \" \\ (no unicode escape) 206*3c875a21SAndroid Build Coastguard Worker line = line[1:-1].replace("\\\\", "\n").replace("\\b", "") 207*3c875a21SAndroid Build Coastguard Worker line = line.replace("\\t", " ").replace("\\n", " ").replace("\\f", " ") 208*3c875a21SAndroid Build Coastguard Worker line = line.replace("\\r", "").replace("\\\"", "\"").replace("\n", "\\") 209*3c875a21SAndroid Build Coastguard Worker # replace a unicode quotation mark, used in the libloading crate 210*3c875a21SAndroid Build Coastguard Worker return line.replace("’", "'").strip() 211*3c875a21SAndroid Build Coastguard Worker 212*3c875a21SAndroid Build Coastguard Worker def _get_cargo_description(self, cargo_toml: str) -> str: 213*3c875a21SAndroid Build Coastguard Worker """Return the description in Cargo.toml or empty string.""" 214*3c875a21SAndroid Build Coastguard Worker if os.path.isfile(cargo_toml) and os.access(cargo_toml, os.R_OK): 215*3c875a21SAndroid Build Coastguard Worker with open(cargo_toml, "r", encoding="utf-8") as toml_file: 216*3c875a21SAndroid Build Coastguard Worker for line in toml_file: 217*3c875a21SAndroid Build Coastguard Worker match = DESCRIPTION_RE.match(line) 218*3c875a21SAndroid Build Coastguard Worker if match: 219*3c875a21SAndroid Build Coastguard Worker return self._toml2str(match.group(1)) 220*3c875a21SAndroid Build Coastguard Worker return "" 221