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