blob: a350384f9d745936d7b89eef85de59fa467e9729 [file] [log] [blame]
Chih-Hung Hsieh11cf9962020-03-19 02:02:25 -07001# 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 Hsieh11cf9962020-03-19 02:02:25 -070016import json
Chih-Hung Hsieh634abe82020-10-02 16:51:01 -070017import os
Chih-Hung Hsieh634abe82020-10-02 16:51:01 -070018from pathlib import Path
Chih-Hung Hsieh11cf9962020-03-19 02:02:25 -070019import re
Joel Galenson40a5a4a2021-05-20 09:50:44 -070020import shutil
21import tempfile
Chih-Hung Hsieh11cf9962020-03-19 02:02:25 -070022import urllib.request
Dan Albert828d82d2023-01-27 22:39:22 +000023from typing import IO
Chih-Hung Hsieh11cf9962020-03-19 02:02:25 -070024
25import archive_utils
Haibo Huang329e6812020-05-29 14:12:20 -070026from base_updater import Updater
Sadaf Ebrahimi602dd322023-06-23 19:30:07 +000027import git_utils
Thiébaud Weksteen4ac289b2020-09-28 15:23:29 +020028# pylint: disable=import-error
Haibo Huang329e6812020-05-29 14:12:20 -070029import metadata_pb2 # type: ignore
Chih-Hung Hsieh11cf9962020-03-19 02:02:25 -070030import updater_utils
31
Sadaf Ebrahimi602dd322023-06-23 19:30:07 +000032LIBRARY_NAME_PATTERN: str = r"([-\w]+)"
Chih-Hung Hsieh11cf9962020-03-19 02:02:25 -070033
Sadaf Ebrahimi602dd322023-06-23 19:30:07 +000034ALPHA_BETA_PATTERN: str = r"^.*[0-9]+\.[0-9]+\.[0-9]+-(alpha|beta).*"
Chih-Hung Hsieh701abda2020-09-28 00:05:06 -070035
36ALPHA_BETA_RE: re.Pattern = re.compile(ALPHA_BETA_PATTERN)
37
Kaiyi Li2a040f82023-06-02 14:23:44 -070038"""Match both x.y.z and x.y.z+a.b.c which is used by some Vulkan binding libraries"""
Sadaf Ebrahimi602dd322023-06-23 19:30:07 +000039VERSION_PATTERN: str = r"([0-9]+)\.([0-9]+)\.([0-9]+)(\+([0-9]+)\.([0-9]+)\.([0-9]+))?"
Chih-Hung Hsieh701abda2020-09-28 00:05:06 -070040
Sadaf Ebrahimi2f5c8902023-11-30 20:20:07 +000041VERSION_RE: re.Pattern = re.compile(VERSION_PATTERN)
Chih-Hung Hsieh701abda2020-09-28 00:05:06 -070042
Sadaf Ebrahimi8c22ee32023-02-24 17:42:26 +000043CRATES_IO_ARCHIVE_URL_PATTERN: str = (r"^https:\/\/static.crates.io\/crates\/" +
44 LIBRARY_NAME_PATTERN + "/" +
45 LIBRARY_NAME_PATTERN + "-" +
Sadaf Ebrahimi2f5c8902023-11-30 20:20:07 +000046 "(.*?)" + ".crate")
Sadaf Ebrahimi8c22ee32023-02-24 17:42:26 +000047
48CRATES_IO_ARCHIVE_URL_RE: re.Pattern = re.compile(CRATES_IO_ARCHIVE_URL_PATTERN)
49
Sadaf Ebrahimi602dd322023-06-23 19:30:07 +000050DESCRIPTION_PATTERN: str = r"^description *= *(\".+\")"
Chih-Hung Hsieh634abe82020-10-02 16:51:01 -070051
Sadaf Ebrahimi2f5c8902023-11-30 20:20:07 +000052DESCRIPTION_RE: re.Pattern = re.compile(DESCRIPTION_PATTERN)
Chih-Hung Hsieh634abe82020-10-02 16:51:01 -070053
Chih-Hung Hsieh11cf9962020-03-19 02:02:25 -070054
Haibo Huang329e6812020-05-29 14:12:20 -070055class CratesUpdater(Updater):
Chih-Hung Hsieh11cf9962020-03-19 02:02:25 -070056 """Updater for crates.io packages."""
57
Sadaf Ebrahimi602dd322023-06-23 19:30:07 +000058 UPSTREAM_REMOTE_NAME: str = "update_origin"
Chih-Hung Hsiehdd1915d2020-09-29 14:22:12 -070059 download_url: str
Haibo Huang329e6812020-05-29 14:12:20 -070060 package: str
Joel Galenson40a5a4a2021-05-20 09:50:44 -070061 package_dir: str
Dan Albert828d82d2023-01-27 22:39:22 +000062 temp_file: IO
Chih-Hung Hsieh11cf9962020-03-19 02:02:25 -070063
Haibo Huang329e6812020-05-29 14:12:20 -070064 def is_supported_url(self) -> bool:
Stephen Hines5dd2a492023-11-22 16:40:34 -080065 match = CRATES_IO_ARCHIVE_URL_RE.match(self._old_identifier.value)
Haibo Huang329e6812020-05-29 14:12:20 -070066 if match is None:
67 return False
68 self.package = match.group(1)
69 return True
70
Sadaf Ebrahimi602dd322023-06-23 19:30:07 +000071 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 Albert828d82d2023-01-27 22:39:22 +000093 def _get_version_numbers(self, version: str) -> tuple[int, int, int]:
Sadaf Ebrahimi2f5c8902023-11-30 20:20:07 +000094 match = VERSION_RE.match(version)
Chih-Hung Hsieh701abda2020-09-28 00:05:06 -070095 if match is not None:
Dan Albert828d82d2023-01-27 22:39:22 +000096 return (
97 int(match.group(1)),
98 int(match.group(2)),
99 int(match.group(3)),
100 )
Chih-Hung Hsieh701abda2020-09-28 00:05:06 -0700101 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 Ebrahimic252d882023-03-09 21:27:29 +0000110 url = f"https://crates.io/api/v1/crates/{self.package}/versions"
Chih-Hung Hsieh701abda2020-09-28 00:05:06 -0700111 with urllib.request.urlopen(url) as request:
112 data = json.loads(request.read().decode())
113 last_id = 0
Stephen Hines5dd2a492023-11-22 16:40:34 -0800114 self._new_identifier.version = ""
Chih-Hung Hsieh701abda2020-09-28 00:05:06 -0700115 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 Hines5dd2a492023-11-22 16:40:34 -0800119 self._new_identifier.version, last_id, version, int(v["id"]))):
Chih-Hung Hsieh701abda2020-09-28 00:05:06 -0700120 last_id = int(v["id"])
Stephen Hines5dd2a492023-11-22 16:40:34 -0800121 self._new_identifier.version = version
Chih-Hung Hsiehdd1915d2020-09-29 14:22:12 -0700122 self.download_url = "https://crates.io" + v["dl_path"]
Chih-Hung Hsieh701abda2020-09-28 00:05:06 -0700123
Haibo Huang329e6812020-05-29 14:12:20 -0700124 def check(self) -> None:
Chih-Hung Hsieh11cf9962020-03-19 02:02:25 -0700125 """Checks crates.io and returns whether a new version is available."""
Chih-Hung Hsieh5015d5b2020-05-04 16:53:45 -0700126 url = "https://crates.io/api/v1/crates/" + self.package
Chih-Hung Hsieh11cf9962020-03-19 02:02:25 -0700127 with urllib.request.urlopen(url) as request:
128 data = json.loads(request.read().decode())
Stephen Hines5dd2a492023-11-22 16:40:34 -0800129 self._new_identifier.version = data["crate"]["max_version"]
Chih-Hung Hsieh701abda2020-09-28 00:05:06 -0700130 # Skip d.d.d-{alpha,beta}* versions
Stephen Hines5dd2a492023-11-22 16:40:34 -0800131 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 Hsieh701abda2020-09-28 00:05:06 -0700133 self._find_latest_non_test_version()
134 else:
Stephen Hines5dd2a492023-11-22 16:40:34 -0800135 url = url + "/" + self._new_identifier.version
Chih-Hung Hsieh701abda2020-09-28 00:05:06 -0700136 with urllib.request.urlopen(url) as request:
137 data = json.loads(request.read().decode())
Chih-Hung Hsiehdd1915d2020-09-29 14:22:12 -0700138 self.download_url = "https://crates.io" + data["version"]["dl_path"]
139
Sadaf Ebrahimid0d87aa2024-01-17 21:39:03 +0000140 def set_new_version_to_old(self):
Sadaf Ebrahimi28f76a12024-02-05 20:39:32 +0000141 super().refresh_without_upgrading()
Chih-Hung Hsiehdd1915d2020-09-29 14:22:12 -0700142 # A shortcut to use the static download path.
Sadaf Ebrahimic252d882023-03-09 21:27:29 +0000143 self.download_url = f"https://static.crates.io/crates/{self.package}/" \
Stephen Hines5dd2a492023-11-22 16:40:34 -0800144 f"{self.package}-{self._new_identifier.version}.crate"
Chih-Hung Hsieh11cf9962020-03-19 02:02:25 -0700145
Sadaf Ebrahimi7d0bab72023-10-23 19:45:03 +0000146 def update(self) -> None:
Chih-Hung Hsieh11cf9962020-03-19 02:02:25 -0700147 """Updates the package.
148
149 Has to call check() before this function.
150 """
151 try:
Chih-Hung Hsiehdd1915d2020-09-29 14:22:12 -0700152 temporary_dir = archive_utils.download_and_extract(self.download_url)
Joel Galenson40a5a4a2021-05-20 09:50:44 -0700153 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 Galenson58eeae72021-04-07 12:44:31 -0700157 self.check_for_errors()
Chih-Hung Hsieh11cf9962020-03-19 02:02:25 -0700158 finally:
159 urllib.request.urlcleanup()
Chih-Hung Hsieh63a9c392020-08-25 15:11:11 -0700160
Joel Galenson40a5a4a2021-05-20 09:50:44 -0700161 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 Ebrahimi93fca7e2023-04-20 19:41:47 +0000172 def update_metadata(self, metadata: metadata_pb2.MetaData) -> metadata_pb2:
Chih-Hung Hsieh63a9c392020-08-25 15:11:11 -0700173 """Updates METADATA content."""
174 # copy only HOMEPAGE url, and then add new ARCHIVE url.
Sadaf Ebrahimi93fca7e2023-04-20 19:41:47 +0000175 updated_metadata = super().update_metadata(metadata)
Stephen Hines5dd2a492023-11-22 16:40:34 -0800176 for identifier in updated_metadata.third_party.identifier:
177 if identifier.version:
Sadaf Ebrahimi28f76a12024-02-05 20:39:32 +0000178 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 Ebrahimi93fca7e2023-04-20 19:41:47 +0000182 break
Chih-Hung Hsieh634abe82020-10-02 16:51:01 -0700183 # copy description from Cargo.toml to METADATA
Sadaf Ebrahimi93fca7e2023-04-20 19:41:47 +0000184 cargo_toml = os.path.join(self.project_path, "Cargo.toml")
Chih-Hung Hsieh634abe82020-10-02 16:51:01 -0700185 description = self._get_cargo_description(cargo_toml)
Sadaf Ebrahimi93fca7e2023-04-20 19:41:47 +0000186 if description and description != updated_metadata.description:
Chih-Hung Hsieh634abe82020-10-02 16:51:01 -0700187 print("New METADATA description:", description)
Sadaf Ebrahimi93fca7e2023-04-20 19:41:47 +0000188 updated_metadata.description = description
189 return updated_metadata
Chih-Hung Hsieh634abe82020-10-02 16:51:01 -0700190
Joel Galenson58eeae72021-04-07 12:44:31 -0700191 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 Ebrahimic252d882023-03-09 21:27:29 +0000198 print(f"Error: Found patch reject files: {str(rejects)}")
Joel Galenson58eeae72021-04-07 12:44:31 -0700199 self._has_errors = True
Joel Galenson58eeae72021-04-07 12:44:31 -0700200
Chih-Hung Hsieh634abe82020-10-02 16:51:01 -0700201 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 Albert2801c852024-02-08 00:39:31 +0000215 with open(cargo_toml, "r", encoding="utf-8") as toml_file:
Chih-Hung Hsieh634abe82020-10-02 16:51:01 -0700216 for line in toml_file:
Sadaf Ebrahimi2f5c8902023-11-30 20:20:07 +0000217 match = DESCRIPTION_RE.match(line)
Chih-Hung Hsieh634abe82020-10-02 16:51:01 -0700218 if match:
219 return self._toml2str(match.group(1))
220 return ""