Use Gitea Releases for installation and ship VS Code highlighting
This commit is contained in:
1 parent
a46bd6e132
commit
201bdee372
243 files changed
+3289
-12521
No files matched your search
@@ -36,24 +36,30 @@ import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
RELEASES = "https://git.dever.cc/shemic/dever/dist"
|
||||
RELEASES = "https://git.dever.cc/api/v1/repos/shemic/dever/releases"
|
||||
MAX_METADATA = 2 * 1024 * 1024
|
||||
MAX_RELEASE = 2 * 1024 * 1024 * 1024
|
||||
ASSET_HOSTS = {"git.dever.cc"}
|
||||
DEFAULT_PUBLIC_KEY = "7889e0edaf5fbf1060bef4ad6e27106128485c8412a6f6cd4457dc67e112986e"
|
||||
|
||||
|
||||
def official_address(address):
|
||||
target = urllib.parse.urlsplit(address)
|
||||
if (target.scheme != "https" or target.hostname not in ASSET_HOSTS
|
||||
or target.username or target.password or target.port not in (None, 443)
|
||||
or target.fragment):
|
||||
raise ValueError("发行下载地址必须位于官方 HTTPS 站点")
|
||||
return address
|
||||
|
||||
|
||||
class OfficialRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, request, stream, code, message, headers, address):
|
||||
target = urllib.parse.urlsplit(address)
|
||||
if (target.scheme != "https" or target.hostname not in ASSET_HOSTS
|
||||
or target.username or target.password or target.port not in (None, 443)
|
||||
or target.fragment):
|
||||
raise ValueError("发行下载重定向离开官方 HTTPS 站点")
|
||||
official_address(address)
|
||||
return super().redirect_request(request, stream, code, message, headers, address)
|
||||
|
||||
|
||||
def download(address):
|
||||
official_address(address)
|
||||
# Installation does not inherit HTTP proxy/environment configuration.
|
||||
client = urllib.request.build_opener(
|
||||
urllib.request.ProxyHandler({}), OfficialRedirect(),
|
||||
@@ -99,15 +105,61 @@ def read_bounded(path, limit):
|
||||
return contents
|
||||
|
||||
|
||||
def metadata(release, source, selection, platform):
|
||||
def resolve_release(selection):
|
||||
endpoint = "latest" if selection == "latest" else f"tags/v{selection}"
|
||||
with download(f"{RELEASES}/{endpoint}") as stream:
|
||||
contents = stream.read(MAX_METADATA + 1)
|
||||
if len(contents) > MAX_METADATA:
|
||||
raise ValueError("发行元数据超过大小限制")
|
||||
record = json.loads(contents)
|
||||
if (not isinstance(record, dict) or record.get("draft") is not False
|
||||
or record.get("prerelease") is not False):
|
||||
raise ValueError("需要已发布的稳定发行版本")
|
||||
tag = record.get("tag_name")
|
||||
if not isinstance(tag, str) or not re.fullmatch(
|
||||
r"v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)", tag):
|
||||
raise ValueError("发行标签不是准确的 v<major.minor.patch>")
|
||||
version = tag[1:]
|
||||
if selection != "latest" and version != selection:
|
||||
raise ValueError("发行版本与请求不匹配")
|
||||
entries = record.get("assets")
|
||||
if not isinstance(entries, list) or not 1 <= len(entries) <= 512:
|
||||
raise ValueError("发行附件列表缺失或超限")
|
||||
attachments = {}
|
||||
for entry in entries:
|
||||
if not isinstance(entry, dict):
|
||||
raise ValueError("发行附件元数据错误")
|
||||
name, size, address = (entry.get(key) for key in ("name", "size", "browser_download_url"))
|
||||
if (not isinstance(name, str) or not name or name in attachments
|
||||
or type(size) is not int or size <= 0 or size > MAX_RELEASE
|
||||
or not isinstance(address, str)):
|
||||
raise ValueError("发行附件名称重复、长度或地址错误")
|
||||
attachments[name] = {"address": official_address(address), "bytes": size}
|
||||
return version, attachments
|
||||
|
||||
|
||||
def asset(attachments, platform, suffix, limit=MAX_RELEASE):
|
||||
name = f"dever-{platform}.{suffix}"
|
||||
entry = attachments.get(name)
|
||||
if entry is None:
|
||||
raise ValueError(f"该发行版本缺少附件:{name}")
|
||||
if entry["bytes"] > limit:
|
||||
raise ValueError("发行附件超过大小限制")
|
||||
return entry
|
||||
|
||||
|
||||
def metadata(release, source, attachments, platform):
|
||||
for filename in ("manifest.json", "manifest.sig"):
|
||||
if source:
|
||||
contents = read_bounded(source / filename, MAX_METADATA)
|
||||
else:
|
||||
with download(f"{RELEASES}/{selection}/dever-{platform}.{filename}") as stream:
|
||||
entry = asset(attachments, platform, filename, MAX_METADATA)
|
||||
with download(entry["address"]) as stream:
|
||||
contents = stream.read(MAX_METADATA + 1)
|
||||
if len(contents) > MAX_METADATA:
|
||||
raise ValueError("发行元数据超过大小限制")
|
||||
if len(contents) != entry["bytes"]:
|
||||
raise ValueError("发行元数据长度不匹配")
|
||||
(release / filename).write_bytes(contents)
|
||||
|
||||
|
||||
@@ -181,7 +233,7 @@ def copy_artifact(stream, destination, artifact):
|
||||
destination.chmod(0o755 if executable else 0o644)
|
||||
|
||||
|
||||
def payload(release, source, version, platform, catalog, trusted_key=None):
|
||||
def payload(release, source, platform, catalog, trusted_key=None, attachments=None):
|
||||
if source:
|
||||
for name, artifact in catalog.items():
|
||||
path = source / name
|
||||
@@ -202,15 +254,20 @@ def payload(release, source, version, platform, catalog, trusted_key=None):
|
||||
with tempfile.TemporaryDirectory(prefix=".extract-", dir=release.parent) as directory:
|
||||
scratch = Path(directory)
|
||||
helper = scratch / "helper"
|
||||
base = f"{RELEASES}/{version}/dever-{platform}"
|
||||
for name, artifact in catalog.items():
|
||||
if name == "bootstrap/dever" or name.startswith("bootstrap/lib/"):
|
||||
with download(f"{base}.blob-{artifact['sha256']}") as stream:
|
||||
entry = asset(attachments, platform, f"blob-{artifact['sha256']}")
|
||||
if entry["bytes"] != artifact["bytes"]:
|
||||
raise ValueError("bootstrap 附件长度与签名不匹配")
|
||||
with download(entry["address"]) as stream:
|
||||
copy_artifact(stream, helper / name, artifact)
|
||||
if stream.read(1):
|
||||
raise ValueError("bootstrap 下载超过签名长度")
|
||||
compressed = scratch / "base.tar.zst"
|
||||
download_archive(f"{base}.tar.zst", compressed)
|
||||
entry = asset(attachments, platform, "tar.zst")
|
||||
download_archive(entry["address"], compressed)
|
||||
if compressed.stat().st_size != entry["bytes"]:
|
||||
raise ValueError("发行附件长度不匹配")
|
||||
configuration = scratch / "extract.json"
|
||||
configuration.write_text(json.dumps({
|
||||
"release": str(release), "archive": str(compressed),
|
||||
@@ -235,7 +292,8 @@ def install(arguments):
|
||||
raise ValueError("机器安装需要管理员权限;请通过根 install 入口执行")
|
||||
platform = platform_name()
|
||||
prerequisites(platform)
|
||||
if arguments.version != "latest" and not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", arguments.version):
|
||||
if arguments.version != "latest" and not re.fullmatch(
|
||||
r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)", arguments.version):
|
||||
raise ValueError("--version 需要 latest 或准确版本")
|
||||
system_root = arguments.system_root.absolute()
|
||||
if not system_root.is_dir() or system_root.is_symlink():
|
||||
@@ -248,15 +306,17 @@ def install(arguments):
|
||||
scratch = Path(directory)
|
||||
release = scratch / "release"
|
||||
release.mkdir()
|
||||
metadata(release, source, arguments.version, platform)
|
||||
selection, attachments = ((arguments.version, None) if source
|
||||
else resolve_release(arguments.version))
|
||||
metadata(release, source, attachments, platform)
|
||||
key_path = arguments.trusted_key
|
||||
if key_path is None:
|
||||
key_path = scratch / "independent-release.pub"
|
||||
key_path.write_text(DEFAULT_PUBLIC_KEY)
|
||||
version, catalog, key = authenticate(release, key_path, scratch, platform, arguments.version)
|
||||
version, catalog, key = authenticate(release, key_path, scratch, platform, selection)
|
||||
trusted = scratch / "release.pub"
|
||||
trusted.write_text(key.hex() + "\n")
|
||||
payload(release, source, version, platform, catalog, trusted)
|
||||
payload(release, source, platform, catalog, trusted, attachments)
|
||||
configuration = scratch / "config"
|
||||
configuration.mkdir()
|
||||
(configuration / "setting.json").write_text(json.dumps({
|
||||
|
||||
Reference in new issue
Block a user