Treat them like all other packages. They used to cause various problems by hitting edge cases, but this should all be fixed now.
345 lines
12 KiB
Python
345 lines
12 KiB
Python
# Copyright 2016-2020 Christoph Reiter
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
import os
|
|
import io
|
|
import tarfile
|
|
import json
|
|
import asyncio
|
|
import traceback
|
|
from typing import Any, Dict, Tuple, List, Set
|
|
|
|
import httpx
|
|
|
|
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
|
|
from .utils import version_is_newer_than, arch_version_to_msys
|
|
from . import appconfig
|
|
|
|
|
|
def get_update_urls() -> List[str]:
|
|
urls = []
|
|
for config in VERSION_CONFIG + SRCINFO_CONFIG + ARCH_MAPPING_CONFIG + CYGWIN_VERSION_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:
|
|
if not appconfig.CACHE_LOCAL:
|
|
async with httpx.AsyncClient() as client:
|
|
r = await client.get(url, *args, **kwargs)
|
|
return r.content
|
|
|
|
base = os.path.dirname(os.path.realpath(__file__))
|
|
cache_dir = os.path.join(base, "_cache")
|
|
os.makedirs(cache_dir, exist_ok=True)
|
|
|
|
fn = os.path.join(cache_dir, url.replace("/", "_").replace(":", "_"))
|
|
if not os.path.exists(fn):
|
|
async with httpx.AsyncClient() 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
|
|
|
|
|
|
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, url: str) -> Dict[str, Source]:
|
|
base_url = url.rsplit("/", 1)[0]
|
|
sources: Dict[str, Source] = {}
|
|
print("Loading %r" % url)
|
|
|
|
def add_desc(d: Any, base_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, base_url, repo, repo_variant)
|
|
|
|
data = await get_content_cached(url, timeout=REQUEST_TIMEOUT)
|
|
|
|
with io.BytesIO(data) as f:
|
|
with tarfile.open(fileobj=f, mode="r:gz") as tar:
|
|
packages: Dict[str, list] = {}
|
|
for info in tar.getmembers():
|
|
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, base_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:
|
|
awaitables.append(parse_repo(repo, variant, 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))
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
r = await client.get("https://aur.archlinux.org/packages.gz",
|
|
timeout=REQUEST_TIMEOUT)
|
|
aur_packages = set()
|
|
for name in r.text.splitlines():
|
|
if name.startswith("#"):
|
|
continue
|
|
if name in arch_versions:
|
|
continue
|
|
if name not in possible_names:
|
|
continue
|
|
aur_packages.add(name)
|
|
|
|
aur_url = (
|
|
"https://aur.archlinux.org/rpc/?v=5&type=info&" +
|
|
"&".join(["arg[]=%s" % n for n in aur_packages]))
|
|
r = await client.get(aur_url, timeout=REQUEST_TIMEOUT)
|
|
for result in r.json()["results"]:
|
|
name = result["Name"]
|
|
if name not in aur_packages 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_LOCAL:
|
|
return True
|
|
|
|
# XXX: github doesn't support redirects with HEAD and returns bogus headers
|
|
async def get_headers(client: httpx.AsyncClient, *args: Any, **kwargs: Any) -> httpx.Headers:
|
|
async with client.stream('GET', *args, **kwargs) as r:
|
|
r.raise_for_status()
|
|
return r.headers
|
|
|
|
combined = ""
|
|
async with httpx.AsyncClient() 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))
|
|
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} provideded 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))
|
|
|
|
|
|
async def update_loop() -> None:
|
|
while True:
|
|
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_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()
|
|
print("Sleeping for %d" % UPDATE_INTERVAL)
|
|
await asyncio.sleep(UPDATE_INTERVAL)
|