it seems sometimes we update things at github, but it takes some time until our server sees the new version, so wait a bit.
400 lines
14 KiB
Python
400 lines
14 KiB
Python
# Copyright 2016-2020 Christoph Reiter
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
import os
|
|
import sys
|
|
import io
|
|
import json
|
|
import asyncio
|
|
import traceback
|
|
import hashlib
|
|
import functools
|
|
from asyncio import Event
|
|
from urllib.parse import urlparse, quote_plus
|
|
from itertools import zip_longest
|
|
from typing import Any, Dict, Tuple, List, Set, Iterable
|
|
|
|
import httpx
|
|
from aiolimiter import AsyncLimiter
|
|
|
|
from .appstate import state, Source, CygwinVersions, ArchMapping, get_repositories, get_arch_names, SrcInfoPackage, Package, DepType
|
|
from .appconfig import CYGWIN_VERSION_CONFIG, REQUEST_TIMEOUT, VERSION_CONFIG, ARCH_MAPPING_CONFIG, \
|
|
SRCINFO_CONFIG, UPDATE_INTERVAL_MAX, BUILD_STATUS_CONFIG, UPDATE_INTERVAL_MIN
|
|
from .utils import version_is_newer_than, arch_version_to_msys
|
|
from . import appconfig
|
|
from .exttarfile import ExtTarFile
|
|
|
|
|
|
def get_update_urls() -> List[str]:
|
|
urls = []
|
|
for config in VERSION_CONFIG + SRCINFO_CONFIG + ARCH_MAPPING_CONFIG + CYGWIN_VERSION_CONFIG + BUILD_STATUS_CONFIG:
|
|
urls.append(config[0])
|
|
for repo in get_repositories():
|
|
urls.append(repo.files_url)
|
|
return sorted(urls)
|
|
|
|
|
|
async def get_content_cached(url: str, *args: Any, **kwargs: Any) -> bytes:
|
|
cache_dir = appconfig.CACHE_DIR
|
|
if cache_dir is None:
|
|
async with httpx.AsyncClient(follow_redirects=True) as client:
|
|
r = await client.get(url, *args, **kwargs)
|
|
return r.content
|
|
|
|
os.makedirs(cache_dir, exist_ok=True)
|
|
|
|
cache_fn = quote_plus(
|
|
(urlparse(url).hostname or "") +
|
|
"." + hashlib.sha256(url.encode()).hexdigest()[:16] +
|
|
".cache")
|
|
|
|
fn = os.path.join(cache_dir, cache_fn)
|
|
if not os.path.exists(fn):
|
|
async with httpx.AsyncClient(follow_redirects=True) as client:
|
|
r = await client.get(url, *args, **kwargs)
|
|
with open(fn, "wb") as h:
|
|
h.write(r.content)
|
|
with open(fn, "rb") as h:
|
|
data = h.read()
|
|
return data
|
|
|
|
|
|
def parse_cygwin_versions(base_url: str, data: bytes) -> CygwinVersions:
|
|
# This is kinda hacky: extract the source name from the src tarball and take
|
|
# last version line before it
|
|
version = None
|
|
source_package = None
|
|
versions: CygwinVersions = {}
|
|
base_url = base_url.rsplit("/", 2)[0]
|
|
for line in data.decode("utf-8").splitlines():
|
|
if line.startswith("version:"):
|
|
version = line.split(":", 1)[-1].strip().split("-", 1)[0].split("+", 1)[0]
|
|
elif line.startswith("source:"):
|
|
source = line.split(":", 1)[-1].strip()
|
|
fn = source.rsplit(None, 2)[0]
|
|
source_package = fn.rsplit("/")[-1].rsplit("-", 3)[0]
|
|
src_url = base_url + "/" + fn
|
|
assert version is not None
|
|
if source_package not in versions:
|
|
versions[source_package] = (version, "https://cygwin.com/packages/summary/%s-src.html" % source_package, src_url)
|
|
return versions
|
|
|
|
|
|
async def update_cygwin_versions() -> None:
|
|
print("update cygwin info")
|
|
url = CYGWIN_VERSION_CONFIG[0][0]
|
|
print("Loading %r" % url)
|
|
data = await get_content_cached(url, timeout=REQUEST_TIMEOUT)
|
|
cygwin_versions = parse_cygwin_versions(url, data)
|
|
state.cygwin_versions = cygwin_versions
|
|
|
|
|
|
async def update_build_status() -> None:
|
|
print("update build status")
|
|
url = BUILD_STATUS_CONFIG[0][0]
|
|
print("Loading %r" % url)
|
|
data = await get_content_cached(url, timeout=REQUEST_TIMEOUT)
|
|
state.build_status = json.loads(data)
|
|
|
|
|
|
def parse_desc(t: str) -> Dict[str, List[str]]:
|
|
d: Dict[str, List[str]] = {}
|
|
cat = None
|
|
values: List[str] = []
|
|
for l in t.splitlines():
|
|
l = l.strip()
|
|
if cat is None:
|
|
cat = l
|
|
elif not l:
|
|
d[cat] = values
|
|
cat = None
|
|
values = []
|
|
else:
|
|
values.append(l)
|
|
if cat is not None:
|
|
d[cat] = values
|
|
return d
|
|
|
|
|
|
async def parse_repo(repo: str, repo_variant: str, files_url: str, download_url: str) -> Dict[str, Source]:
|
|
sources: Dict[str, Source] = {}
|
|
print("Loading %r" % files_url)
|
|
|
|
def add_desc(d: Any, download_url: str) -> None:
|
|
source = Source.from_desc(d, repo)
|
|
if source.name not in sources:
|
|
sources[source.name] = source
|
|
else:
|
|
source = sources[source.name]
|
|
|
|
source.add_desc(d, download_url, repo, repo_variant)
|
|
|
|
data = await get_content_cached(files_url, timeout=REQUEST_TIMEOUT)
|
|
|
|
with io.BytesIO(data) as f:
|
|
with ExtTarFile.open(fileobj=f, mode="r") as tar:
|
|
packages: Dict[str, list] = {}
|
|
for info in tar:
|
|
package_name = info.name.split("/", 1)[0]
|
|
infofile = tar.extractfile(info)
|
|
if infofile is None:
|
|
continue
|
|
with infofile:
|
|
packages.setdefault(package_name, []).append(
|
|
(info.name, infofile.read()))
|
|
|
|
for package_name, infos in sorted(packages.items()):
|
|
t = ""
|
|
for name, data in sorted(infos):
|
|
if name.endswith("/desc"):
|
|
t += data.decode("utf-8")
|
|
elif name.endswith("/depends"):
|
|
t += data.decode("utf-8")
|
|
elif name.endswith("/files"):
|
|
t += data.decode("utf-8")
|
|
desc = parse_desc(t)
|
|
add_desc(desc, download_url)
|
|
|
|
return sources
|
|
|
|
|
|
async def update_arch_versions() -> None:
|
|
print("update versions")
|
|
arch_versions: Dict[str, Tuple[str, str, int]] = {}
|
|
awaitables = []
|
|
for (url, repo, variant) in VERSION_CONFIG:
|
|
download_url = url.rsplit("/", 1)[0]
|
|
awaitables.append(parse_repo(repo, variant, url, download_url))
|
|
|
|
for sources in (await asyncio.gather(*awaitables)):
|
|
for source in sources.values():
|
|
msys_ver = arch_version_to_msys(source.version)
|
|
for p in source.packages.values():
|
|
url = "https://www.archlinux.org/packages/%s/%s/%s/" % (
|
|
p.repo, p.arch, p.name)
|
|
|
|
if p.name in arch_versions:
|
|
old_ver = arch_versions[p.name][0]
|
|
if version_is_newer_than(msys_ver, old_ver):
|
|
arch_versions[p.name] = (msys_ver, url, p.builddate)
|
|
else:
|
|
arch_versions[p.name] = (msys_ver, url, p.builddate)
|
|
|
|
url = "https://www.archlinux.org/packages/%s/%s/%s/" % (
|
|
source.repos[0], source.arches[0], source.name)
|
|
if source.name in arch_versions:
|
|
old_ver = arch_versions[source.name][0]
|
|
if version_is_newer_than(msys_ver, old_ver):
|
|
arch_versions[source.name] = (msys_ver, url, source.date)
|
|
else:
|
|
arch_versions[source.name] = (msys_ver, url, source.date)
|
|
|
|
print("done")
|
|
|
|
print("update versions from AUR")
|
|
# a bit hacky, try to get the remaining versions from AUR
|
|
possible_names = set()
|
|
for s in state.sources.values():
|
|
for p in s.packages.values():
|
|
possible_names.update(get_arch_names(p.realname))
|
|
possible_names.update(get_arch_names(s.realname))
|
|
|
|
r = await get_content_cached("https://aur.archlinux.org/packages.gz",
|
|
timeout=REQUEST_TIMEOUT)
|
|
aur_packages = set()
|
|
for name in r.decode().splitlines():
|
|
if name.startswith("#"):
|
|
continue
|
|
if name in arch_versions:
|
|
continue
|
|
if name not in possible_names:
|
|
continue
|
|
aur_packages.add(name)
|
|
|
|
def chunks(iterable: Iterable[str], n: int) -> List[List[str]]:
|
|
return [list(filter(None, x)) for x in zip_longest(*([iter(iterable)] * n))]
|
|
|
|
# too long URLs lead to errors, so split things up
|
|
for chunk in chunks(sorted(aur_packages), 250):
|
|
aur_url = (
|
|
"https://aur.archlinux.org/rpc/?v=5&type=info&" +
|
|
"&".join(["arg[]=%s" % n for n in chunk]))
|
|
data = await get_content_cached(aur_url, timeout=REQUEST_TIMEOUT)
|
|
json_obj = json.loads(data.decode("utf-8"))
|
|
for result in json_obj["results"]:
|
|
name = result["Name"]
|
|
if name not in chunk or name in arch_versions:
|
|
continue
|
|
last_modified = result["LastModified"]
|
|
url = "https://aur.archlinux.org/packages/%s" % name
|
|
arch_versions[name] = (result["Version"], url, last_modified)
|
|
|
|
print("done")
|
|
state.arch_versions = arch_versions
|
|
|
|
|
|
async def check_needs_update(_cache_key: List[str] = [""]) -> bool:
|
|
"""Raises RequestException"""
|
|
|
|
if appconfig.CACHE_DIR:
|
|
return True
|
|
|
|
async def get_headers(client: httpx.AsyncClient, *args: Any, **kwargs: Any) -> httpx.Headers:
|
|
r = await client.head(*args, **kwargs)
|
|
r.raise_for_status()
|
|
return r.headers
|
|
|
|
combined = ""
|
|
async with httpx.AsyncClient(follow_redirects=True) as client:
|
|
awaitables = []
|
|
for url in get_update_urls():
|
|
awaitables.append(get_headers(client, url, timeout=REQUEST_TIMEOUT))
|
|
|
|
for headers in (await asyncio.gather(*awaitables)):
|
|
key = headers.get("last-modified", "")
|
|
key += headers.get("etag", "")
|
|
combined += key
|
|
|
|
if combined != _cache_key[0]:
|
|
_cache_key[0] = combined
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
async def update_source() -> None:
|
|
"""Raises RequestException"""
|
|
|
|
print("update source")
|
|
|
|
final: Dict[str, Source] = {}
|
|
awaitables = []
|
|
for repo in get_repositories():
|
|
awaitables.append(parse_repo(repo.name, repo.variant, repo.files_url, repo.download_url))
|
|
for sources in await asyncio.gather(*awaitables):
|
|
for name, source in sources.items():
|
|
if name in final:
|
|
final[name].packages.update(source.packages)
|
|
else:
|
|
final[name] = source
|
|
|
|
fill_rdepends(final)
|
|
state.sources = final
|
|
|
|
|
|
async def update_sourceinfos() -> None:
|
|
print("update sourceinfos")
|
|
|
|
result: Dict[str, SrcInfoPackage] = {}
|
|
|
|
for cfg in SRCINFO_CONFIG:
|
|
url = cfg[0]
|
|
print("Loading %r" % url)
|
|
data = await get_content_cached(url, timeout=REQUEST_TIMEOUT)
|
|
json_obj = json.loads(data.decode("utf-8"))
|
|
for hash_, m in json_obj.items():
|
|
for repo, srcinfo in m["srcinfo"].items():
|
|
for pkg in SrcInfoPackage.for_srcinfo(srcinfo, repo, m["repo"], m["path"], m["date"]):
|
|
if pkg.pkgname in result:
|
|
print(f"WARN: duplicate: {pkg.pkgname} provided by "
|
|
f"{pkg.pkgbase} and {result[pkg.pkgname].pkgbase}")
|
|
result[pkg.pkgname] = pkg
|
|
|
|
state.sourceinfos = result
|
|
|
|
|
|
def fill_rdepends(sources: Dict[str, Source]) -> None:
|
|
deps: Dict[str, Dict[Package, Set[DepType]]] = {}
|
|
for s in sources.values():
|
|
for p in s.packages.values():
|
|
for n, r in p.depends.items():
|
|
deps.setdefault(n, dict()).setdefault(p, set()).add(DepType.NORMAL)
|
|
for n, r in p.makedepends.items():
|
|
deps.setdefault(n, dict()).setdefault(p, set()).add(DepType.MAKE)
|
|
for n, r in p.optdepends.items():
|
|
deps.setdefault(n, dict()).setdefault(p, set()).add(DepType.OPTIONAL)
|
|
for n, r in p.checkdepends.items():
|
|
deps.setdefault(n, dict()).setdefault(p, set()).add(DepType.CHECK)
|
|
|
|
for s in sources.values():
|
|
for p in s.packages.values():
|
|
rdeps = [deps.get(p.name, dict())]
|
|
for prov in p.provides:
|
|
rdeps.append(deps.get(prov, dict()))
|
|
|
|
merged: Dict[Package, Set[DepType]] = {}
|
|
for rd in rdeps:
|
|
for rp, rs in rd.items():
|
|
merged.setdefault(rp, set()).update(rs)
|
|
|
|
p.rdepends = merged
|
|
|
|
|
|
async def update_arch_mapping() -> None:
|
|
print("update arch mapping")
|
|
|
|
url = ARCH_MAPPING_CONFIG[0][0]
|
|
print("Loading %r" % url)
|
|
|
|
data = await get_content_cached(url, timeout=REQUEST_TIMEOUT)
|
|
state.arch_mapping = ArchMapping(json.loads(data))
|
|
|
|
|
|
_rate_limit = AsyncLimiter(1, UPDATE_INTERVAL_MIN)
|
|
|
|
|
|
@functools.lru_cache(maxsize=None)
|
|
def _get_update_event() -> Event:
|
|
return Event()
|
|
|
|
|
|
async def wait_for_update() -> None:
|
|
update_event = _get_update_event()
|
|
await update_event.wait()
|
|
update_event.clear()
|
|
|
|
|
|
def queue_update() -> None:
|
|
update_event = _get_update_event()
|
|
update_event.set()
|
|
|
|
|
|
async def trigger_loop() -> None:
|
|
while True:
|
|
print("Sleeping for %d" % UPDATE_INTERVAL_MAX)
|
|
await asyncio.sleep(UPDATE_INTERVAL_MAX)
|
|
queue_update()
|
|
|
|
|
|
async def update_loop() -> None:
|
|
asyncio.create_task(trigger_loop())
|
|
while True:
|
|
async with _rate_limit:
|
|
try:
|
|
print("check for update")
|
|
if await check_needs_update():
|
|
print("update needed")
|
|
rounds = []
|
|
rounds.append([
|
|
update_arch_mapping(),
|
|
update_cygwin_versions(),
|
|
update_source(),
|
|
update_sourceinfos(),
|
|
update_build_status(),
|
|
])
|
|
# update_arch_versions() depends on update_source()
|
|
rounds.append([
|
|
update_arch_versions()
|
|
])
|
|
for r in rounds:
|
|
await asyncio.gather(*r)
|
|
state.ready = True
|
|
else:
|
|
print("no update needed")
|
|
except Exception:
|
|
traceback.print_exc(file=sys.stdout)
|
|
print("Waiting for next update")
|
|
await wait_for_update()
|
|
# XXX: it seems some updates don't propagate right away, so wait a bit
|
|
await asyncio.sleep(5)
|