[+] remove old pre-refactor archlinux modules
1. delete archive.py, cache_db.py, cli.py, compile.py, db.py, pacman.py, resolver.py, solv_backend.py; 2. all functionality moved to apps/, cli/, resolver/ subpackages;
This commit is contained in:
parent
7a03db3e97
commit
1e1cd6c1c0
@ -1,294 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import datetime
|
|
||||||
import enum
|
|
||||||
import logging
|
|
||||||
import pathlib
|
|
||||||
import re
|
|
||||||
|
|
||||||
from typing import (
|
|
||||||
ClassVar,
|
|
||||||
Optional,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .cache_db import cache_db_t
|
|
||||||
from .db import db_parser_t
|
|
||||||
from .models import mirror_config_t
|
|
||||||
from .pacman import pacman_t
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ArchiveAction(enum.Enum):
|
|
||||||
list_dates = 'list-dates'
|
|
||||||
list_packages = 'list-packages'
|
|
||||||
show_versions = 'show-versions'
|
|
||||||
sync = 'sync'
|
|
||||||
|
|
||||||
|
|
||||||
class archive_t:
|
|
||||||
class constants_t:
|
|
||||||
base_url: ClassVar[str] = 'https://archive.archlinux.org/repos/'
|
|
||||||
href_re: ClassVar[re.Pattern[str]] = re.compile(r'href="(\d{4}/\d{2}/\d{2})/"')
|
|
||||||
default_repos: ClassVar[list[str]] = ['core', 'extra', 'multilib']
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def list_remote_dates(
|
|
||||||
base_url: Optional[str] = None,
|
|
||||||
) -> list[str]:
|
|
||||||
"""Scrape available date directories from the archive index page."""
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
if base_url is None:
|
|
||||||
base_url = archive_t.constants_t.base_url
|
|
||||||
|
|
||||||
logger.info(dict(msg='fetching archive index', url=base_url))
|
|
||||||
|
|
||||||
with urllib.request.urlopen(base_url) as resp:
|
|
||||||
html = resp.read().decode('utf-8')
|
|
||||||
|
|
||||||
dates: list[str] = []
|
|
||||||
for m in archive_t.constants_t.href_re.finditer(html):
|
|
||||||
dates.append(m.group(1))
|
|
||||||
|
|
||||||
dates.sort(reverse=True)
|
|
||||||
return dates
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def sync_date(
|
|
||||||
date: str,
|
|
||||||
cache_dir: pathlib.Path,
|
|
||||||
cache_db: cache_db_t,
|
|
||||||
repos: Optional[list[str]] = None,
|
|
||||||
arch: str = 'x86_64',
|
|
||||||
) -> None:
|
|
||||||
if repos is None:
|
|
||||||
repos = list(archive_t.constants_t.default_repos)
|
|
||||||
|
|
||||||
mirror = mirror_config_t.from_archive_date(
|
|
||||||
date=date,
|
|
||||||
repos=repos,
|
|
||||||
arch=arch,
|
|
||||||
)
|
|
||||||
|
|
||||||
db_dir = cache_dir / date
|
|
||||||
db_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
for repo_cfg in mirror.repos:
|
|
||||||
db_url = '%s/%s.db' % (repo_cfg.url, repo_cfg.name)
|
|
||||||
db_path = db_dir / ('%s.db' % repo_cfg.name)
|
|
||||||
db_rel_path = '%s/%s.db' % (date, repo_cfg.name)
|
|
||||||
|
|
||||||
if not db_path.exists():
|
|
||||||
logger.info(
|
|
||||||
dict(
|
|
||||||
msg='downloading db',
|
|
||||||
url=db_url,
|
|
||||||
dest=str(db_path),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
pacman_t.download_db(db_url, db_path)
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
dict(
|
|
||||||
msg='db already cached on disk',
|
|
||||||
path=str(db_path),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
db_sha256 = cache_db_t.file_sha256(db_path)
|
|
||||||
|
|
||||||
snapshot_id = cache_db.upsert_snapshot(
|
|
||||||
date=date,
|
|
||||||
repo=repo_cfg.name,
|
|
||||||
arch=arch,
|
|
||||||
db_sha256=db_sha256,
|
|
||||||
db_rel_path=db_rel_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
if cache_db.snapshot_package_count(snapshot_id) > 0:
|
|
||||||
snap = cache_db.get_snapshot_by_id(snapshot_id)
|
|
||||||
if snap is not None and snap.db_sha256 == db_sha256:
|
|
||||||
logger.info(
|
|
||||||
dict(
|
|
||||||
msg='snapshot already in sqlite',
|
|
||||||
date=date,
|
|
||||||
repo=repo_cfg.name,
|
|
||||||
snapshot_id=snapshot_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
index = db_parser_t.parse_db_path(db_path, repo_name=repo_cfg.name)
|
|
||||||
|
|
||||||
cache_db.store_index(
|
|
||||||
snapshot_id=snapshot_id,
|
|
||||||
index=index,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
dict(
|
|
||||||
msg='synced',
|
|
||||||
date=date,
|
|
||||||
repo=repo_cfg.name,
|
|
||||||
packages=len(index.packages),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_date(s: str) -> datetime.date:
|
|
||||||
parts = s.split('/')
|
|
||||||
if len(parts) == 3:
|
|
||||||
return datetime.date(int(parts[0]), int(parts[1]), int(parts[2]))
|
|
||||||
return datetime.date.fromisoformat(s)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _format_date(d: datetime.date) -> str:
|
|
||||||
return '%04d/%02d/%02d' % (d.year, d.month, d.day)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def sync_date_range(
|
|
||||||
start_date: str,
|
|
||||||
end_date: str,
|
|
||||||
cache_dir: pathlib.Path,
|
|
||||||
cache_db: cache_db_t,
|
|
||||||
repos: Optional[list[str]] = None,
|
|
||||||
arch: str = 'x86_64',
|
|
||||||
step_days: int = 1,
|
|
||||||
) -> None:
|
|
||||||
start = archive_t._parse_date(start_date)
|
|
||||||
end = archive_t._parse_date(end_date)
|
|
||||||
step = datetime.timedelta(days=step_days)
|
|
||||||
|
|
||||||
current = end
|
|
||||||
while current >= start:
|
|
||||||
date_str = archive_t._format_date(current)
|
|
||||||
|
|
||||||
try:
|
|
||||||
archive_t.sync_date(
|
|
||||||
date=date_str,
|
|
||||||
cache_dir=cache_dir,
|
|
||||||
cache_db=cache_db,
|
|
||||||
repos=repos,
|
|
||||||
arch=arch,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.warning(
|
|
||||||
dict(
|
|
||||||
msg='failed to sync date, skipping',
|
|
||||||
date=date_str,
|
|
||||||
),
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
current -= step
|
|
||||||
|
|
||||||
|
|
||||||
def main(args: list[str]) -> int:
|
|
||||||
archive_parser = argparse.ArgumentParser(
|
|
||||||
prog='online-fxreader-pr34-archlinux archive',
|
|
||||||
)
|
|
||||||
archive_parser.add_argument(
|
|
||||||
'action',
|
|
||||||
choices=[o.value for o in ArchiveAction],
|
|
||||||
)
|
|
||||||
archive_parser.add_argument(
|
|
||||||
'--cache-dir',
|
|
||||||
dest='cache_dir',
|
|
||||||
required=True,
|
|
||||||
help='directory for cached .db files and sqlite database',
|
|
||||||
)
|
|
||||||
archive_parser.add_argument(
|
|
||||||
'--repos',
|
|
||||||
nargs='*',
|
|
||||||
default=['core', 'extra', 'multilib'],
|
|
||||||
)
|
|
||||||
archive_parser.add_argument(
|
|
||||||
'--arch',
|
|
||||||
default='x86_64',
|
|
||||||
)
|
|
||||||
archive_parser.add_argument(
|
|
||||||
'--date',
|
|
||||||
default=None,
|
|
||||||
help='single date (e.g. 2024/01/15) for sync',
|
|
||||||
)
|
|
||||||
archive_parser.add_argument(
|
|
||||||
'--date-range',
|
|
||||||
dest='date_range',
|
|
||||||
nargs=2,
|
|
||||||
metavar=('START', 'END'),
|
|
||||||
default=None,
|
|
||||||
help='date range for sync (e.g. 2024/01/01 2024/06/30)',
|
|
||||||
)
|
|
||||||
archive_parser.add_argument(
|
|
||||||
'--date-step',
|
|
||||||
dest='date_step',
|
|
||||||
type=int,
|
|
||||||
default=1,
|
|
||||||
help='step in days when iterating date range, default 1',
|
|
||||||
)
|
|
||||||
archive_parser.add_argument(
|
|
||||||
'--packages',
|
|
||||||
nargs='*',
|
|
||||||
default=None,
|
|
||||||
help='package names for show-versions',
|
|
||||||
)
|
|
||||||
|
|
||||||
archive_options = archive_parser.parse_args(args)
|
|
||||||
archive_options.action = ArchiveAction(archive_options.action)
|
|
||||||
|
|
||||||
cache_dir = pathlib.Path(archive_options.cache_dir)
|
|
||||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
db = cache_db_t(cache_dir / 'archlinux_cache.db')
|
|
||||||
|
|
||||||
try:
|
|
||||||
if archive_options.action is ArchiveAction.list_dates:
|
|
||||||
if db.has_data():
|
|
||||||
print('=== cached dates ===')
|
|
||||||
for date_str in db.list_dates():
|
|
||||||
print(date_str)
|
|
||||||
|
|
||||||
print('=== remote dates ===')
|
|
||||||
for date_str in archive_t.list_remote_dates():
|
|
||||||
print(date_str)
|
|
||||||
|
|
||||||
elif archive_options.action is ArchiveAction.list_packages:
|
|
||||||
for row in db.package_count_per_date():
|
|
||||||
print('%s %d' % (row.date, row.count))
|
|
||||||
|
|
||||||
elif archive_options.action is ArchiveAction.show_versions:
|
|
||||||
if archive_options.packages is None or len(archive_options.packages) == 0:
|
|
||||||
logger.error('--packages required for show-versions')
|
|
||||||
return 1
|
|
||||||
|
|
||||||
for row in db.get_package_versions(archive_options.packages):
|
|
||||||
print('%s %s %s %s' % (row.date, row.repo, row.name, row.version))
|
|
||||||
|
|
||||||
elif archive_options.action is ArchiveAction.sync:
|
|
||||||
if archive_options.date is not None:
|
|
||||||
archive_t.sync_date(
|
|
||||||
date=archive_options.date,
|
|
||||||
cache_dir=cache_dir,
|
|
||||||
cache_db=db,
|
|
||||||
repos=archive_options.repos,
|
|
||||||
arch=archive_options.arch,
|
|
||||||
)
|
|
||||||
elif archive_options.date_range is not None:
|
|
||||||
archive_t.sync_date_range(
|
|
||||||
start_date=archive_options.date_range[0],
|
|
||||||
end_date=archive_options.date_range[1],
|
|
||||||
cache_dir=cache_dir,
|
|
||||||
cache_db=db,
|
|
||||||
repos=archive_options.repos,
|
|
||||||
arch=archive_options.arch,
|
|
||||||
step_days=archive_options.date_step,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.error('sync requires --date or --date-range')
|
|
||||||
return 1
|
|
||||||
else:
|
|
||||||
raise NotImplementedError
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
return 0
|
|
||||||
@ -1,689 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import hashlib
|
|
||||||
import io
|
|
||||||
import logging
|
|
||||||
import pathlib
|
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
from typing import (
|
|
||||||
ClassVar,
|
|
||||||
Generator,
|
|
||||||
Optional,
|
|
||||||
TypeVar,
|
|
||||||
)
|
|
||||||
|
|
||||||
import pydantic
|
|
||||||
|
|
||||||
from .models import (
|
|
||||||
package_desc_t,
|
|
||||||
repo_index_t,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_T = TypeVar('_T', bound=pydantic.BaseModel)
|
|
||||||
|
|
||||||
|
|
||||||
class snapshot_row_t(pydantic.BaseModel):
|
|
||||||
id: int
|
|
||||||
date: str
|
|
||||||
repo: str
|
|
||||||
arch: str
|
|
||||||
db_sha256: str
|
|
||||||
db_rel_path: str
|
|
||||||
synced_at: str
|
|
||||||
|
|
||||||
|
|
||||||
class package_row_t(pydantic.BaseModel):
|
|
||||||
id: int
|
|
||||||
snapshot_id: int
|
|
||||||
name: str
|
|
||||||
version: str
|
|
||||||
base: str = ''
|
|
||||||
desc: str = ''
|
|
||||||
filename: str = ''
|
|
||||||
csize: int = 0
|
|
||||||
isize: int = 0
|
|
||||||
md5sum: str = ''
|
|
||||||
sha256sum: str = ''
|
|
||||||
url: str = ''
|
|
||||||
arch: str = ''
|
|
||||||
builddate: int = 0
|
|
||||||
packager: str = ''
|
|
||||||
|
|
||||||
|
|
||||||
class package_version_row_t(pydantic.BaseModel):
|
|
||||||
date: str
|
|
||||||
repo: str
|
|
||||||
name: str
|
|
||||||
version: str
|
|
||||||
|
|
||||||
|
|
||||||
class date_count_row_t(pydantic.BaseModel):
|
|
||||||
date: str
|
|
||||||
count: int
|
|
||||||
|
|
||||||
|
|
||||||
class package_hash_row_t(pydantic.BaseModel):
|
|
||||||
sha256sum: str
|
|
||||||
|
|
||||||
|
|
||||||
class local_package_row_t(pydantic.BaseModel):
|
|
||||||
id: int
|
|
||||||
name: str
|
|
||||||
version: str
|
|
||||||
filename: str
|
|
||||||
sha256sum: str
|
|
||||||
local_path: str
|
|
||||||
downloaded_at: str
|
|
||||||
|
|
||||||
|
|
||||||
class signature_row_t(pydantic.BaseModel):
|
|
||||||
id: int
|
|
||||||
local_package_id: int
|
|
||||||
sig_path: str
|
|
||||||
keyring_package_version: Optional[str] = None
|
|
||||||
gpg_key_id: Optional[str] = None
|
|
||||||
verified_at: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class trusted_entry_t(pydantic.BaseModel, frozen=True):
|
|
||||||
name: str
|
|
||||||
version: str
|
|
||||||
|
|
||||||
|
|
||||||
def _stream_rows(
|
|
||||||
cur: sqlite3.Cursor,
|
|
||||||
model: type[_T],
|
|
||||||
) -> Generator[_T, None, None]:
|
|
||||||
columns = [desc[0] for desc in cur.description]
|
|
||||||
for raw in cur:
|
|
||||||
yield model.model_validate(dict(zip(columns, raw)))
|
|
||||||
|
|
||||||
|
|
||||||
def _fetch_one(
|
|
||||||
cur: sqlite3.Cursor,
|
|
||||||
model: type[_T],
|
|
||||||
) -> Optional[_T]:
|
|
||||||
columns = [desc[0] for desc in cur.description]
|
|
||||||
raw = cur.fetchone()
|
|
||||||
if raw is None:
|
|
||||||
return None
|
|
||||||
return model.model_validate(dict(zip(columns, raw)))
|
|
||||||
|
|
||||||
|
|
||||||
class cache_db_t:
|
|
||||||
class constants_t:
|
|
||||||
schema_version: ClassVar[int] = 1
|
|
||||||
|
|
||||||
list_relation_types: ClassVar[dict[str, str]] = {
|
|
||||||
'license': 'license',
|
|
||||||
'depends': 'depends',
|
|
||||||
'optdepends': 'optdepends',
|
|
||||||
'makedepends': 'makedepends',
|
|
||||||
'checkdepends': 'checkdepends',
|
|
||||||
'provides': 'provides',
|
|
||||||
'conflicts': 'conflicts',
|
|
||||||
'replaces': 'replaces',
|
|
||||||
'groups': 'groups',
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, db_path: pathlib.Path) -> None:
|
|
||||||
self._db_path = db_path
|
|
||||||
self._conn = sqlite3.connect(str(db_path))
|
|
||||||
self._conn.execute('PRAGMA journal_mode=WAL')
|
|
||||||
self._conn.execute('PRAGMA foreign_keys=ON')
|
|
||||||
self._ensure_schema()
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
self._conn.close()
|
|
||||||
|
|
||||||
def _ensure_schema(self) -> None:
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
|
|
||||||
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_meta'")
|
|
||||||
if cur.fetchone() is None:
|
|
||||||
self._create_schema(cur)
|
|
||||||
self._conn.commit()
|
|
||||||
return
|
|
||||||
|
|
||||||
cur.execute('SELECT version FROM schema_meta LIMIT 1')
|
|
||||||
row = cur.fetchone()
|
|
||||||
if row is None or row[0] < cache_db_t.constants_t.schema_version:
|
|
||||||
self._create_schema(cur)
|
|
||||||
self._conn.commit()
|
|
||||||
|
|
||||||
def _create_schema(self, cur: sqlite3.Cursor) -> None:
|
|
||||||
cur.executescript("""
|
|
||||||
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
||||||
version INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS snapshots (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
date TEXT NOT NULL,
|
|
||||||
repo TEXT NOT NULL,
|
|
||||||
arch TEXT NOT NULL DEFAULT 'x86_64',
|
|
||||||
db_sha256 TEXT NOT NULL,
|
|
||||||
db_rel_path TEXT NOT NULL DEFAULT '',
|
|
||||||
synced_at TEXT NOT NULL,
|
|
||||||
UNIQUE(date, repo, arch)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS packages (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
snapshot_id INTEGER NOT NULL REFERENCES snapshots(id) ON DELETE CASCADE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
version TEXT NOT NULL,
|
|
||||||
base TEXT NOT NULL DEFAULT '',
|
|
||||||
desc TEXT NOT NULL DEFAULT '',
|
|
||||||
filename TEXT NOT NULL DEFAULT '',
|
|
||||||
csize INTEGER NOT NULL DEFAULT 0,
|
|
||||||
isize INTEGER NOT NULL DEFAULT 0,
|
|
||||||
md5sum TEXT NOT NULL DEFAULT '',
|
|
||||||
sha256sum TEXT NOT NULL DEFAULT '',
|
|
||||||
url TEXT NOT NULL DEFAULT '',
|
|
||||||
arch TEXT NOT NULL DEFAULT '',
|
|
||||||
builddate INTEGER NOT NULL DEFAULT 0,
|
|
||||||
packager TEXT NOT NULL DEFAULT '',
|
|
||||||
UNIQUE(snapshot_id, name)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS package_relations (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
package_id INTEGER NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
|
|
||||||
relation_type TEXT NOT NULL,
|
|
||||||
value TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS local_packages (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
version TEXT NOT NULL,
|
|
||||||
filename TEXT NOT NULL,
|
|
||||||
sha256sum TEXT NOT NULL DEFAULT '',
|
|
||||||
local_path TEXT NOT NULL,
|
|
||||||
downloaded_at TEXT NOT NULL,
|
|
||||||
UNIQUE(name, version, filename)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS local_signatures (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
local_package_id INTEGER NOT NULL REFERENCES local_packages(id) ON DELETE CASCADE,
|
|
||||||
sig_path TEXT NOT NULL,
|
|
||||||
keyring_package_version TEXT DEFAULT NULL,
|
|
||||||
gpg_key_id TEXT DEFAULT NULL,
|
|
||||||
verified_at TEXT DEFAULT NULL,
|
|
||||||
UNIQUE(local_package_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_packages_snapshot ON packages(snapshot_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_packages_name ON packages(name);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_packages_name_version ON packages(name, version);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_snapshots_date ON snapshots(date);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_package_relations_pkg
|
|
||||||
ON package_relations(package_id, relation_type);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_local_packages_name_version
|
|
||||||
ON local_packages(name, version);
|
|
||||||
""")
|
|
||||||
|
|
||||||
cur.execute('DELETE FROM schema_meta')
|
|
||||||
cur.execute(
|
|
||||||
'INSERT INTO schema_meta (version) VALUES (?)',
|
|
||||||
(cache_db_t.constants_t.schema_version,),
|
|
||||||
)
|
|
||||||
|
|
||||||
# ── helpers ──
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def file_sha256(path: pathlib.Path) -> str:
|
|
||||||
h = hashlib.sha256()
|
|
||||||
with io.open(path, 'rb') as f:
|
|
||||||
while True:
|
|
||||||
chunk = f.read(65536)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
h.update(chunk)
|
|
||||||
return h.hexdigest()
|
|
||||||
|
|
||||||
# ── snapshot CRUD ──
|
|
||||||
|
|
||||||
def upsert_snapshot(
|
|
||||||
self,
|
|
||||||
date: str,
|
|
||||||
repo: str,
|
|
||||||
arch: str,
|
|
||||||
db_sha256: str,
|
|
||||||
db_rel_path: str = '',
|
|
||||||
) -> int:
|
|
||||||
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
|
|
||||||
cur.execute(
|
|
||||||
'SELECT id, db_sha256 FROM snapshots WHERE date=? AND repo=? AND arch=?',
|
|
||||||
(date, repo, arch),
|
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
|
|
||||||
if row is not None:
|
|
||||||
snapshot_id: int = row[0]
|
|
||||||
if row[1] == db_sha256:
|
|
||||||
return snapshot_id
|
|
||||||
|
|
||||||
cur.execute(
|
|
||||||
'DELETE FROM packages WHERE snapshot_id=?',
|
|
||||||
(snapshot_id,),
|
|
||||||
)
|
|
||||||
cur.execute(
|
|
||||||
'UPDATE snapshots SET db_sha256=?, db_rel_path=?, synced_at=? WHERE id=?',
|
|
||||||
(db_sha256, db_rel_path, now, snapshot_id),
|
|
||||||
)
|
|
||||||
self._conn.commit()
|
|
||||||
return snapshot_id
|
|
||||||
|
|
||||||
cur.execute(
|
|
||||||
'INSERT INTO snapshots (date, repo, arch, db_sha256, db_rel_path, synced_at) VALUES (?, ?, ?, ?, ?, ?)',
|
|
||||||
(date, repo, arch, db_sha256, db_rel_path, now),
|
|
||||||
)
|
|
||||||
self._conn.commit()
|
|
||||||
assert cur.lastrowid is not None
|
|
||||||
return cur.lastrowid
|
|
||||||
|
|
||||||
def get_snapshot(
|
|
||||||
self,
|
|
||||||
date: str,
|
|
||||||
repo: str,
|
|
||||||
arch: str,
|
|
||||||
) -> Optional[snapshot_row_t]:
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
cur.execute(
|
|
||||||
'SELECT * FROM snapshots WHERE date=? AND repo=? AND arch=?',
|
|
||||||
(date, repo, arch),
|
|
||||||
)
|
|
||||||
return _fetch_one(cur, snapshot_row_t)
|
|
||||||
|
|
||||||
def get_snapshot_by_id(
|
|
||||||
self,
|
|
||||||
snapshot_id: int,
|
|
||||||
) -> Optional[snapshot_row_t]:
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
cur.execute(
|
|
||||||
'SELECT * FROM snapshots WHERE id=?',
|
|
||||||
(snapshot_id,),
|
|
||||||
)
|
|
||||||
return _fetch_one(cur, snapshot_row_t)
|
|
||||||
|
|
||||||
def list_snapshots(self) -> Generator[snapshot_row_t, None, None]:
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
cur.execute('SELECT * FROM snapshots ORDER BY date DESC, repo')
|
|
||||||
yield from _stream_rows(cur, snapshot_row_t)
|
|
||||||
|
|
||||||
def list_dates(self) -> list[str]:
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
cur.execute('SELECT DISTINCT date FROM snapshots ORDER BY date DESC')
|
|
||||||
return [row[0] for row in cur.fetchall()]
|
|
||||||
|
|
||||||
def snapshot_package_count(self, snapshot_id: int) -> int:
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
cur.execute(
|
|
||||||
'SELECT COUNT(*) FROM packages WHERE snapshot_id=?',
|
|
||||||
(snapshot_id,),
|
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
return row[0] if row is not None else 0
|
|
||||||
|
|
||||||
# ── package CRUD ──
|
|
||||||
|
|
||||||
def store_index(
|
|
||||||
self,
|
|
||||||
snapshot_id: int,
|
|
||||||
index: repo_index_t,
|
|
||||||
) -> None:
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
|
|
||||||
pkg_rows: list[tuple[int, str, str, str, str, str, int, int, str, str, str, str, int, str]] = []
|
|
||||||
for pkg in index.packages.values():
|
|
||||||
pkg_rows.append(
|
|
||||||
(
|
|
||||||
snapshot_id,
|
|
||||||
pkg.name,
|
|
||||||
pkg.version,
|
|
||||||
pkg.base,
|
|
||||||
pkg.desc,
|
|
||||||
pkg.filename,
|
|
||||||
pkg.csize,
|
|
||||||
pkg.isize,
|
|
||||||
pkg.md5sum,
|
|
||||||
pkg.sha256sum,
|
|
||||||
pkg.url,
|
|
||||||
pkg.arch,
|
|
||||||
pkg.builddate,
|
|
||||||
pkg.packager,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
cur.executemany(
|
|
||||||
'INSERT OR REPLACE INTO packages '
|
|
||||||
'(snapshot_id, name, version, base, desc, filename, csize, isize, '
|
|
||||||
'md5sum, sha256sum, url, arch, builddate, packager) '
|
|
||||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
||||||
pkg_rows,
|
|
||||||
)
|
|
||||||
|
|
||||||
cur.execute(
|
|
||||||
'SELECT id, name FROM packages WHERE snapshot_id=?',
|
|
||||||
(snapshot_id,),
|
|
||||||
)
|
|
||||||
pkg_id_map: dict[str, int] = {}
|
|
||||||
for row_raw in cur.fetchall():
|
|
||||||
pkg_id_map[row_raw[1]] = row_raw[0]
|
|
||||||
|
|
||||||
rel_rows: list[tuple[int, str, str]] = []
|
|
||||||
for pkg in index.packages.values():
|
|
||||||
pkg_id = pkg_id_map.get(pkg.name)
|
|
||||||
if pkg_id is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for rel_type, attr_name in cache_db_t.constants_t.list_relation_types.items():
|
|
||||||
values: list[str] = getattr(pkg, attr_name)
|
|
||||||
for v in values:
|
|
||||||
rel_rows.append((pkg_id, rel_type, v))
|
|
||||||
|
|
||||||
if len(rel_rows) > 0:
|
|
||||||
cur.executemany(
|
|
||||||
'INSERT INTO package_relations (package_id, relation_type, value) VALUES (?, ?, ?)',
|
|
||||||
rel_rows,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._conn.commit()
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
dict(
|
|
||||||
msg='stored index',
|
|
||||||
snapshot_id=snapshot_id,
|
|
||||||
packages=len(pkg_rows),
|
|
||||||
relations=len(rel_rows),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def package_count_per_date(self) -> Generator[date_count_row_t, None, None]:
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
cur.execute('SELECT s.date AS date, COUNT(p.id) AS count FROM snapshots s JOIN packages p ON p.snapshot_id = s.id GROUP BY s.date ORDER BY s.date DESC')
|
|
||||||
yield from _stream_rows(cur, date_count_row_t)
|
|
||||||
|
|
||||||
def get_package_versions(
|
|
||||||
self,
|
|
||||||
names: list[str],
|
|
||||||
) -> Generator[package_version_row_t, None, None]:
|
|
||||||
if len(names) == 0:
|
|
||||||
yield from ()
|
|
||||||
return
|
|
||||||
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
placeholders = ','.join('?' for _ in names)
|
|
||||||
cur.execute(
|
|
||||||
'SELECT s.date AS date, s.repo AS repo, p.name AS name, p.version AS version '
|
|
||||||
'FROM packages p '
|
|
||||||
'JOIN snapshots s ON s.id = p.snapshot_id '
|
|
||||||
'WHERE p.name IN (%s) '
|
|
||||||
'ORDER BY p.name, s.date DESC' % placeholders,
|
|
||||||
names,
|
|
||||||
)
|
|
||||||
yield from _stream_rows(cur, package_version_row_t)
|
|
||||||
|
|
||||||
def find_package_hash(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
version: str,
|
|
||||||
) -> Optional[package_hash_row_t]:
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
cur.execute(
|
|
||||||
"SELECT sha256sum FROM packages WHERE name=? AND version=? AND sha256sum != '' ORDER BY snapshot_id DESC LIMIT 1",
|
|
||||||
(name, version),
|
|
||||||
)
|
|
||||||
return _fetch_one(cur, package_hash_row_t)
|
|
||||||
|
|
||||||
# ── repo_index_t loading ──
|
|
||||||
|
|
||||||
def load_repo_index(
|
|
||||||
self,
|
|
||||||
snapshot_id: int,
|
|
||||||
repo_name: str,
|
|
||||||
) -> repo_index_t:
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
|
|
||||||
cur.execute(
|
|
||||||
'SELECT * FROM packages WHERE snapshot_id=?',
|
|
||||||
(snapshot_id,),
|
|
||||||
)
|
|
||||||
|
|
||||||
index = repo_index_t(name=repo_name)
|
|
||||||
|
|
||||||
pkg_ids: list[int] = []
|
|
||||||
pkg_by_id: dict[int, package_desc_t] = {}
|
|
||||||
|
|
||||||
columns = [desc[0] for desc in cur.description]
|
|
||||||
for raw in cur.fetchall():
|
|
||||||
row_dict = dict(zip(columns, raw))
|
|
||||||
pkg = package_desc_t(
|
|
||||||
name=row_dict['name'],
|
|
||||||
version=row_dict['version'],
|
|
||||||
base=row_dict['base'],
|
|
||||||
desc=row_dict['desc'],
|
|
||||||
filename=row_dict['filename'],
|
|
||||||
csize=row_dict['csize'],
|
|
||||||
isize=row_dict['isize'],
|
|
||||||
md5sum=row_dict['md5sum'],
|
|
||||||
sha256sum=row_dict['sha256sum'],
|
|
||||||
url=row_dict['url'],
|
|
||||||
arch=row_dict['arch'],
|
|
||||||
builddate=row_dict['builddate'],
|
|
||||||
packager=row_dict['packager'],
|
|
||||||
)
|
|
||||||
index.packages[pkg.name] = pkg
|
|
||||||
pkg_ids.append(row_dict['id'])
|
|
||||||
pkg_by_id[row_dict['id']] = pkg
|
|
||||||
|
|
||||||
if len(pkg_ids) > 0:
|
|
||||||
self._load_relations(cur, pkg_ids, pkg_by_id)
|
|
||||||
|
|
||||||
index.build_provides_index()
|
|
||||||
return index
|
|
||||||
|
|
||||||
def load_all_indices(self) -> list[repo_index_t]:
|
|
||||||
"""Load all snapshots as repo_index_t objects via bulk queries.
|
|
||||||
|
|
||||||
Returns one index per (snapshot_id, repo) so the solver sees all
|
|
||||||
package versions across all synced dates. Uses two bulk queries
|
|
||||||
instead of per-snapshot loading for performance.
|
|
||||||
"""
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
|
|
||||||
cur.execute('SELECT * FROM snapshots ORDER BY date ASC')
|
|
||||||
snap_columns = [desc[0] for desc in cur.description]
|
|
||||||
snapshots = [dict(zip(snap_columns, raw)) for raw in cur.fetchall()]
|
|
||||||
|
|
||||||
cur.execute(
|
|
||||||
'SELECT id, snapshot_id, name, version, base, desc, filename, csize, isize, md5sum, sha256sum, url, arch, builddate, packager FROM packages'
|
|
||||||
)
|
|
||||||
pkg_columns = [desc[0] for desc in cur.description]
|
|
||||||
|
|
||||||
pkgs_by_snapshot: dict[int, dict[str, package_desc_t]] = {}
|
|
||||||
all_pkg_ids: list[int] = []
|
|
||||||
pkg_by_id: dict[int, package_desc_t] = {}
|
|
||||||
|
|
||||||
for raw in cur.fetchall():
|
|
||||||
rd = dict(zip(pkg_columns, raw))
|
|
||||||
pkg = package_desc_t(
|
|
||||||
name=rd['name'],
|
|
||||||
version=rd['version'],
|
|
||||||
base=rd['base'],
|
|
||||||
desc=rd['desc'],
|
|
||||||
filename=rd['filename'],
|
|
||||||
csize=rd['csize'],
|
|
||||||
isize=rd['isize'],
|
|
||||||
md5sum=rd['md5sum'],
|
|
||||||
sha256sum=rd['sha256sum'],
|
|
||||||
url=rd['url'],
|
|
||||||
arch=rd['arch'],
|
|
||||||
builddate=rd['builddate'],
|
|
||||||
packager=rd['packager'],
|
|
||||||
)
|
|
||||||
snap_id: int = rd['snapshot_id']
|
|
||||||
if snap_id not in pkgs_by_snapshot:
|
|
||||||
pkgs_by_snapshot[snap_id] = {}
|
|
||||||
pkgs_by_snapshot[snap_id][pkg.name] = pkg
|
|
||||||
all_pkg_ids.append(rd['id'])
|
|
||||||
pkg_by_id[rd['id']] = pkg
|
|
||||||
|
|
||||||
if len(all_pkg_ids) > 0:
|
|
||||||
self._load_relations(cur, all_pkg_ids, pkg_by_id)
|
|
||||||
|
|
||||||
indices: list[repo_index_t] = []
|
|
||||||
for snap in snapshots:
|
|
||||||
pkgs = pkgs_by_snapshot.get(snap['id'])
|
|
||||||
if pkgs is None or len(pkgs) == 0:
|
|
||||||
continue
|
|
||||||
idx = repo_index_t(name=snap['repo'], packages=pkgs)
|
|
||||||
idx.build_provides_index()
|
|
||||||
indices.append(idx)
|
|
||||||
|
|
||||||
return indices
|
|
||||||
|
|
||||||
def _load_relations(
|
|
||||||
self,
|
|
||||||
cur: sqlite3.Cursor,
|
|
||||||
pkg_ids: list[int],
|
|
||||||
pkg_by_id: dict[int, package_desc_t],
|
|
||||||
) -> None:
|
|
||||||
batch_size = 500
|
|
||||||
for i in range(0, len(pkg_ids), batch_size):
|
|
||||||
batch = pkg_ids[i : i + batch_size]
|
|
||||||
placeholders = ','.join('?' for _ in batch)
|
|
||||||
cur.execute(
|
|
||||||
'SELECT package_id, relation_type, value FROM package_relations WHERE package_id IN (%s)' % placeholders,
|
|
||||||
batch,
|
|
||||||
)
|
|
||||||
for row_raw in cur.fetchall():
|
|
||||||
pkg = pkg_by_id.get(row_raw[0])
|
|
||||||
if pkg is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
attr_name = cache_db_t.constants_t.list_relation_types.get(row_raw[1])
|
|
||||||
if attr_name is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
target_list: list[str] = getattr(pkg, attr_name)
|
|
||||||
target_list.append(row_raw[2])
|
|
||||||
|
|
||||||
# ── local packages & signatures ──
|
|
||||||
|
|
||||||
def record_local_package(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
version: str,
|
|
||||||
filename: str,
|
|
||||||
sha256sum: str,
|
|
||||||
local_path: str,
|
|
||||||
) -> int:
|
|
||||||
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
cur.execute(
|
|
||||||
'INSERT OR REPLACE INTO local_packages (name, version, filename, sha256sum, local_path, downloaded_at) VALUES (?, ?, ?, ?, ?, ?)',
|
|
||||||
(name, version, filename, sha256sum, local_path, now),
|
|
||||||
)
|
|
||||||
self._conn.commit()
|
|
||||||
assert cur.lastrowid is not None
|
|
||||||
return cur.lastrowid
|
|
||||||
|
|
||||||
def record_signature(
|
|
||||||
self,
|
|
||||||
local_package_id: int,
|
|
||||||
sig_path: str,
|
|
||||||
keyring_package_version: Optional[str] = None,
|
|
||||||
gpg_key_id: Optional[str] = None,
|
|
||||||
) -> None:
|
|
||||||
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
cur.execute(
|
|
||||||
'INSERT OR REPLACE INTO local_signatures (local_package_id, sig_path, keyring_package_version, gpg_key_id, verified_at) VALUES (?, ?, ?, ?, ?)',
|
|
||||||
(local_package_id, sig_path, keyring_package_version, gpg_key_id, now),
|
|
||||||
)
|
|
||||||
self._conn.commit()
|
|
||||||
|
|
||||||
def get_signature_info(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
version: str,
|
|
||||||
) -> Optional[signature_row_t]:
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
cur.execute(
|
|
||||||
'SELECT ls.id, ls.local_package_id, ls.sig_path, '
|
|
||||||
'ls.keyring_package_version, ls.gpg_key_id, ls.verified_at '
|
|
||||||
'FROM local_signatures ls '
|
|
||||||
'JOIN local_packages lp ON lp.id = ls.local_package_id '
|
|
||||||
'WHERE lp.name=? AND lp.version=?',
|
|
||||||
(name, version),
|
|
||||||
)
|
|
||||||
return _fetch_one(cur, signature_row_t)
|
|
||||||
|
|
||||||
def get_trusted_package_set(
|
|
||||||
self,
|
|
||||||
trust_keyring_versions: Optional[list[str]] = None,
|
|
||||||
trust_gpg_keys: Optional[list[str]] = None,
|
|
||||||
exclude_keyring_versions: Optional[list[str]] = None,
|
|
||||||
exclude_gpg_keys: Optional[list[str]] = None,
|
|
||||||
) -> Optional[set[trusted_entry_t]]:
|
|
||||||
"""Return set of trusted (name, version) entries that pass trust filters.
|
|
||||||
|
|
||||||
Returns None if no trust filters are set (meaning all packages pass).
|
|
||||||
"""
|
|
||||||
has_filters = (
|
|
||||||
(trust_keyring_versions is not None and len(trust_keyring_versions) > 0)
|
|
||||||
or (trust_gpg_keys is not None and len(trust_gpg_keys) > 0)
|
|
||||||
or (exclude_keyring_versions is not None and len(exclude_keyring_versions) > 0)
|
|
||||||
or (exclude_gpg_keys is not None and len(exclude_gpg_keys) > 0)
|
|
||||||
)
|
|
||||||
if not has_filters:
|
|
||||||
return None
|
|
||||||
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
cur.execute(
|
|
||||||
'SELECT lp.name, lp.version, ls.keyring_package_version, ls.gpg_key_id '
|
|
||||||
'FROM local_packages lp '
|
|
||||||
'JOIN local_signatures ls ON ls.local_package_id = lp.id'
|
|
||||||
)
|
|
||||||
|
|
||||||
trusted: set[trusted_entry_t] = set()
|
|
||||||
|
|
||||||
for row_raw in cur.fetchall():
|
|
||||||
keyring_ver = row_raw[2]
|
|
||||||
gpg_key = row_raw[3]
|
|
||||||
|
|
||||||
if exclude_keyring_versions and keyring_ver in exclude_keyring_versions:
|
|
||||||
continue
|
|
||||||
if exclude_gpg_keys and gpg_key in exclude_gpg_keys:
|
|
||||||
continue
|
|
||||||
|
|
||||||
is_trusted = False
|
|
||||||
|
|
||||||
if trust_keyring_versions and keyring_ver in trust_keyring_versions:
|
|
||||||
is_trusted = True
|
|
||||||
if trust_gpg_keys and gpg_key in trust_gpg_keys:
|
|
||||||
is_trusted = True
|
|
||||||
|
|
||||||
if not trust_keyring_versions and not trust_gpg_keys:
|
|
||||||
is_trusted = True
|
|
||||||
|
|
||||||
if is_trusted:
|
|
||||||
trusted.add(trusted_entry_t(name=row_raw[0], version=row_raw[1]))
|
|
||||||
|
|
||||||
return trusted
|
|
||||||
|
|
||||||
# ── status ──
|
|
||||||
|
|
||||||
def has_data(self) -> bool:
|
|
||||||
cur = self._conn.cursor()
|
|
||||||
cur.execute('SELECT COUNT(*) FROM snapshots')
|
|
||||||
row = cur.fetchone()
|
|
||||||
return row is not None and row[0] > 0
|
|
||||||
@ -1,442 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import enum
|
|
||||||
import logging
|
|
||||||
import math
|
|
||||||
import pathlib
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
from typing import (
|
|
||||||
ClassVar,
|
|
||||||
Optional,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Command(enum.Enum):
|
|
||||||
list_installed = 'list-installed'
|
|
||||||
compile = 'compile'
|
|
||||||
download = 'download'
|
|
||||||
archive = 'archive'
|
|
||||||
|
|
||||||
|
|
||||||
class parse_rate_t:
|
|
||||||
class constants_t:
|
|
||||||
rate_re: ClassVar[re.Pattern[str]] = re.compile(r'^(\d+(?:\.\d+)?)\s*([bBkKmMgGpPtT]?)(?:[iI]?[bB])?(?:/s)?$')
|
|
||||||
|
|
||||||
units: ClassVar[dict[str, int]] = {
|
|
||||||
'': 0,
|
|
||||||
'b': 0,
|
|
||||||
'B': 0,
|
|
||||||
'k': 1,
|
|
||||||
'K': 1,
|
|
||||||
'm': 2,
|
|
||||||
'M': 2,
|
|
||||||
'g': 3,
|
|
||||||
'G': 3,
|
|
||||||
't': 4,
|
|
||||||
'T': 4,
|
|
||||||
'p': 5,
|
|
||||||
'P': 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse(s: str) -> int:
|
|
||||||
m = parse_rate_t.constants_t.rate_re.match(s.strip())
|
|
||||||
if not m:
|
|
||||||
raise ValueError('invalid rate: %s' % s)
|
|
||||||
|
|
||||||
value = float(m.group(1))
|
|
||||||
unit = m.group(2)
|
|
||||||
|
|
||||||
power = parse_rate_t.constants_t.units.get(unit, 0)
|
|
||||||
|
|
||||||
return int(value * (1024**power))
|
|
||||||
|
|
||||||
|
|
||||||
class downloader_t:
|
|
||||||
class constants_t:
|
|
||||||
class backend_t(enum.Enum):
|
|
||||||
urllib = 'urllib'
|
|
||||||
curl = 'curl'
|
|
||||||
aria2c = 'aria2c'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def download(
|
|
||||||
url: str,
|
|
||||||
dest: pathlib.Path,
|
|
||||||
backend: 'downloader_t.constants_t.backend_t',
|
|
||||||
limit_rate: int,
|
|
||||||
) -> None:
|
|
||||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
if backend is downloader_t.constants_t.backend_t.urllib:
|
|
||||||
urllib.request.urlretrieve(url, str(dest))
|
|
||||||
elif backend is downloader_t.constants_t.backend_t.curl:
|
|
||||||
cmd = [
|
|
||||||
'curl',
|
|
||||||
'-fSL',
|
|
||||||
'--limit-rate',
|
|
||||||
'%d' % limit_rate,
|
|
||||||
'-o',
|
|
||||||
str(dest),
|
|
||||||
url,
|
|
||||||
]
|
|
||||||
subprocess.check_call(cmd)
|
|
||||||
elif backend is downloader_t.constants_t.backend_t.aria2c:
|
|
||||||
cmd = [
|
|
||||||
'aria2c',
|
|
||||||
'--max-download-limit=%d' % limit_rate,
|
|
||||||
'-d',
|
|
||||||
str(dest.parent),
|
|
||||||
'-o',
|
|
||||||
dest.name,
|
|
||||||
url,
|
|
||||||
]
|
|
||||||
subprocess.check_call(cmd)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class download_requirements_t:
|
|
||||||
@staticmethod
|
|
||||||
def parse_requirements(txt: str) -> list[tuple[str, str]]:
|
|
||||||
entries: list[tuple[str, str]] = []
|
|
||||||
url: Optional[str] = None
|
|
||||||
|
|
||||||
for line in txt.splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if line == '':
|
|
||||||
continue
|
|
||||||
if line.startswith('#'):
|
|
||||||
candidate = line[1:].strip()
|
|
||||||
if '/' in candidate and '://' in candidate:
|
|
||||||
url = candidate
|
|
||||||
continue
|
|
||||||
|
|
||||||
parts = line.split()
|
|
||||||
if len(parts) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
pkg_spec = parts[0]
|
|
||||||
|
|
||||||
if url is not None:
|
|
||||||
filename = url.rsplit('/', 1)[-1] if '/' in url else pkg_spec
|
|
||||||
entries.append((url, filename))
|
|
||||||
url = None
|
|
||||||
|
|
||||||
return entries
|
|
||||||
|
|
||||||
|
|
||||||
def _find_cached_pkg(
|
|
||||||
cache_dir: pathlib.Path,
|
|
||||||
name: str,
|
|
||||||
version: str,
|
|
||||||
) -> Optional[pathlib.Path]:
|
|
||||||
"""Find a cached .pkg.tar.* file for a given package name and version."""
|
|
||||||
for suffix in ['.pkg.tar.zst', '.pkg.tar.xz', '.pkg.tar.gz', '.pkg.tar.bz2', '.pkg.tar']:
|
|
||||||
for arch in ['x86_64', 'any']:
|
|
||||||
candidate = cache_dir / ('%s-%s-%s%s' % (name, version, arch, suffix))
|
|
||||||
if candidate.exists():
|
|
||||||
return candidate
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv: Optional[list[str]] = None) -> int:
|
|
||||||
if argv is None:
|
|
||||||
argv = sys.argv[1:]
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
prog='online-fxreader-pr34-archlinux',
|
|
||||||
description='Arch Linux package management tools',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'command',
|
|
||||||
choices=[o.value for o in Command],
|
|
||||||
)
|
|
||||||
|
|
||||||
options, args = parser.parse_known_args(argv)
|
|
||||||
options.command = Command(options.command)
|
|
||||||
|
|
||||||
if options.command is Command.list_installed:
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from .pacman import pacman_t
|
|
||||||
|
|
||||||
list_parser = argparse.ArgumentParser()
|
|
||||||
list_parser.add_argument(
|
|
||||||
'--format',
|
|
||||||
choices=['plain', 'constraints', 'compiled'],
|
|
||||||
default='plain',
|
|
||||||
help='plain: name version; constraints: name>=version; compiled: name==version with optional hashes',
|
|
||||||
)
|
|
||||||
list_parser.add_argument(
|
|
||||||
'--generate-hashes',
|
|
||||||
action='store_true',
|
|
||||||
default=False,
|
|
||||||
help='include sha256 from local /var/cache/pacman/pkg/ files; fails if file not found for any package',
|
|
||||||
)
|
|
||||||
list_parser.add_argument(
|
|
||||||
'--db-path',
|
|
||||||
dest='db_path',
|
|
||||||
default='/var/lib/pacman',
|
|
||||||
help='pacman db path, default /var/lib/pacman',
|
|
||||||
)
|
|
||||||
list_parser.add_argument(
|
|
||||||
'--pkg-cache-dir',
|
|
||||||
dest='pkg_cache_dir',
|
|
||||||
default='/var/cache/pacman/pkg',
|
|
||||||
help='local pacman package cache directory, default /var/cache/pacman/pkg',
|
|
||||||
)
|
|
||||||
|
|
||||||
list_options = list_parser.parse_args(args)
|
|
||||||
|
|
||||||
installed = pacman_t.list_installed_simple(
|
|
||||||
db_path=pathlib.Path(list_options.db_path),
|
|
||||||
)
|
|
||||||
|
|
||||||
pkg_cache_dir = pathlib.Path(list_options.pkg_cache_dir)
|
|
||||||
|
|
||||||
if list_options.format == 'plain':
|
|
||||||
for name, version in installed:
|
|
||||||
print('%s %s' % (name, version))
|
|
||||||
elif list_options.format == 'constraints':
|
|
||||||
for name, version in installed:
|
|
||||||
print('%s>=%s' % (name, version))
|
|
||||||
elif list_options.format == 'compiled':
|
|
||||||
missing_hashes: list[str] = []
|
|
||||||
|
|
||||||
for name, version in installed:
|
|
||||||
line = '%s==%s' % (name, version)
|
|
||||||
|
|
||||||
if list_options.generate_hashes:
|
|
||||||
pkg_file = _find_cached_pkg(
|
|
||||||
pkg_cache_dir,
|
|
||||||
name,
|
|
||||||
version,
|
|
||||||
)
|
|
||||||
|
|
||||||
if pkg_file is not None:
|
|
||||||
h = hashlib.sha256()
|
|
||||||
with open(pkg_file, 'rb') as fh:
|
|
||||||
while True:
|
|
||||||
chunk = fh.read(65536)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
h.update(chunk)
|
|
||||||
line += ' --hash=sha256:%s' % h.hexdigest()
|
|
||||||
else:
|
|
||||||
missing_hashes.append(name)
|
|
||||||
|
|
||||||
print(line)
|
|
||||||
|
|
||||||
if len(missing_hashes) > 0:
|
|
||||||
logger.error(
|
|
||||||
"can't determine checksum of installed package(s) - no cached file found for %d package(s): %s" % (len(missing_hashes), missing_hashes)
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
return 0
|
|
||||||
elif options.command is Command.compile:
|
|
||||||
compile_parser = argparse.ArgumentParser()
|
|
||||||
compile_parser.add_argument(
|
|
||||||
'packages',
|
|
||||||
nargs='*',
|
|
||||||
)
|
|
||||||
compile_parser.add_argument(
|
|
||||||
'-r',
|
|
||||||
dest='requirements_file',
|
|
||||||
default=None,
|
|
||||||
help='path to file with package constraints (one per line)',
|
|
||||||
)
|
|
||||||
compile_parser.add_argument(
|
|
||||||
'--index',
|
|
||||||
dest='index_url',
|
|
||||||
default=None,
|
|
||||||
help='mirror URL',
|
|
||||||
)
|
|
||||||
compile_parser.add_argument(
|
|
||||||
'--archive-date',
|
|
||||||
dest='archive_date',
|
|
||||||
default=None,
|
|
||||||
help='Arch Linux Archive date (e.g. 2024/01/15)',
|
|
||||||
)
|
|
||||||
compile_parser.add_argument(
|
|
||||||
'--offline',
|
|
||||||
action='store_true',
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
compile_parser.add_argument(
|
|
||||||
'--no-cache',
|
|
||||||
action='store_true',
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
compile_parser.add_argument(
|
|
||||||
'--generate-hashes',
|
|
||||||
action='store_true',
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
compile_parser.add_argument(
|
|
||||||
'--cache-dir',
|
|
||||||
dest='cache_dir',
|
|
||||||
default=None,
|
|
||||||
)
|
|
||||||
compile_parser.add_argument(
|
|
||||||
'--repos',
|
|
||||||
nargs='*',
|
|
||||||
default=['core', 'extra', 'multilib'],
|
|
||||||
)
|
|
||||||
compile_parser.add_argument(
|
|
||||||
'--arch',
|
|
||||||
default='x86_64',
|
|
||||||
)
|
|
||||||
compile_parser.add_argument(
|
|
||||||
'--backend',
|
|
||||||
choices=['python', 'solv'],
|
|
||||||
default='solv',
|
|
||||||
)
|
|
||||||
compile_parser.add_argument(
|
|
||||||
'--archive-cache',
|
|
||||||
dest='archive_cache',
|
|
||||||
default=None,
|
|
||||||
help='path to archive cache dir (with archlinux_cache.db from archive sync); loads all synced dates into the solver pool',
|
|
||||||
)
|
|
||||||
compile_parser.add_argument(
|
|
||||||
'--reference',
|
|
||||||
default=None,
|
|
||||||
help='path to previously compiled requirements file to use as version pins',
|
|
||||||
)
|
|
||||||
compile_parser.add_argument(
|
|
||||||
'--resolution-strategy',
|
|
||||||
dest='resolution_strategy',
|
|
||||||
choices=['upgrade-all', 'pin-referenced'],
|
|
||||||
default='upgrade-all',
|
|
||||||
help='upgrade-all: resolve fresh; pin-referenced: keep referenced versions, only upgrade explicitly requested packages',
|
|
||||||
)
|
|
||||||
|
|
||||||
compile_options = compile_parser.parse_args(args)
|
|
||||||
|
|
||||||
from .models import compile_options_t, resolution_strategy_t
|
|
||||||
|
|
||||||
packages: list[str] = list(compile_options.packages)
|
|
||||||
|
|
||||||
if compile_options.requirements_file is not None:
|
|
||||||
for line in pathlib.Path(compile_options.requirements_file).read_text().splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if line != '' and not line.startswith('#'):
|
|
||||||
packages.append(line)
|
|
||||||
|
|
||||||
opts = compile_options_t(
|
|
||||||
packages=packages,
|
|
||||||
index_url=compile_options.index_url,
|
|
||||||
archive_date=compile_options.archive_date,
|
|
||||||
offline=compile_options.offline,
|
|
||||||
no_cache=compile_options.no_cache,
|
|
||||||
generate_hashes=compile_options.generate_hashes,
|
|
||||||
repos=compile_options.repos,
|
|
||||||
arch=compile_options.arch,
|
|
||||||
cache_dir=compile_options.cache_dir,
|
|
||||||
reference=compile_options.reference,
|
|
||||||
resolution_strategy=resolution_strategy_t(compile_options.resolution_strategy),
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if compile_options.backend == 'solv':
|
|
||||||
from .solv_backend import compile_solv_t, repo_store_t
|
|
||||||
|
|
||||||
stores = None
|
|
||||||
if compile_options.archive_cache is not None:
|
|
||||||
from .cache_db import cache_db_t
|
|
||||||
|
|
||||||
archive_cache_dir = pathlib.Path(compile_options.archive_cache)
|
|
||||||
db_path = archive_cache_dir / 'archlinux_cache.db'
|
|
||||||
if db_path.exists():
|
|
||||||
cache_db = cache_db_t(db_path)
|
|
||||||
indices = cache_db.load_all_indices()
|
|
||||||
cache_db.close()
|
|
||||||
stores = [repo_store_t(index=idx) for idx in indices]
|
|
||||||
|
|
||||||
result = compile_solv_t.compile(opts, stores=stores)
|
|
||||||
else:
|
|
||||||
from .compile import compile_t
|
|
||||||
|
|
||||||
result = compile_t.compile(opts)
|
|
||||||
except RuntimeError as e:
|
|
||||||
logger.error(str(e))
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print(result.txt)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
elif options.command is Command.download:
|
|
||||||
download_parser = argparse.ArgumentParser()
|
|
||||||
download_parser.add_argument(
|
|
||||||
'-r',
|
|
||||||
dest='requirements',
|
|
||||||
required=True,
|
|
||||||
help='path to compiled requirements file',
|
|
||||||
)
|
|
||||||
download_parser.add_argument(
|
|
||||||
'-d',
|
|
||||||
dest='dest_dir',
|
|
||||||
required=True,
|
|
||||||
help='destination directory for downloaded packages',
|
|
||||||
)
|
|
||||||
download_parser.add_argument(
|
|
||||||
'--downloader',
|
|
||||||
choices=[o.value for o in downloader_t.constants_t.backend_t],
|
|
||||||
default='urllib',
|
|
||||||
)
|
|
||||||
download_parser.add_argument(
|
|
||||||
'--limit-rate',
|
|
||||||
dest='limit_rate',
|
|
||||||
default='128KiB/s',
|
|
||||||
help='download speed limit (e.g. 128KiB/s, 1MiB/s, 512K), default 128KiB/s',
|
|
||||||
)
|
|
||||||
|
|
||||||
download_options = download_parser.parse_args(args)
|
|
||||||
|
|
||||||
dest_dir = pathlib.Path(download_options.dest_dir)
|
|
||||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
backend = downloader_t.constants_t.backend_t(download_options.downloader)
|
|
||||||
limit_rate = parse_rate_t.parse(download_options.limit_rate)
|
|
||||||
|
|
||||||
requirements_txt = pathlib.Path(download_options.requirements).read_text()
|
|
||||||
entries = download_requirements_t.parse_requirements(requirements_txt)
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
for url, filename in entries:
|
|
||||||
dest_path = dest_dir / filename
|
|
||||||
|
|
||||||
if dest_path.exists():
|
|
||||||
logger.info(dict(msg='already downloaded', path=str(dest_path)))
|
|
||||||
else:
|
|
||||||
logger.info(dict(msg='downloading', url=url, dest=str(dest_path), backend=backend.value, limit_rate=limit_rate))
|
|
||||||
downloader_t.download(
|
|
||||||
url=url,
|
|
||||||
dest=dest_path,
|
|
||||||
backend=backend,
|
|
||||||
limit_rate=limit_rate,
|
|
||||||
)
|
|
||||||
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
logger.info(dict(msg='download complete', count=count))
|
|
||||||
|
|
||||||
return 0
|
|
||||||
elif options.command is Command.archive:
|
|
||||||
from . import archive as _archive
|
|
||||||
|
|
||||||
return _archive.main(args)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.exit(main())
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
import io
|
|
||||||
import hashlib
|
|
||||||
import pathlib
|
|
||||||
import tempfile
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from typing import (
|
|
||||||
Optional,
|
|
||||||
Any,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .models import (
|
|
||||||
compile_options_t,
|
|
||||||
compile_entry_t,
|
|
||||||
compile_result_t,
|
|
||||||
mirror_config_t,
|
|
||||||
repo_index_t,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .db import db_parser_t
|
|
||||||
from .pacman import pacman_t
|
|
||||||
from .resolver import resolver_t
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class compile_t:
|
|
||||||
@staticmethod
|
|
||||||
def build_mirror_config(options: compile_options_t) -> mirror_config_t:
|
|
||||||
if options.archive_date is not None:
|
|
||||||
return mirror_config_t.from_archive_date(
|
|
||||||
date=options.archive_date,
|
|
||||||
repos=options.repos,
|
|
||||||
arch=options.arch,
|
|
||||||
)
|
|
||||||
elif options.index_url is not None:
|
|
||||||
return mirror_config_t.from_mirror_url(
|
|
||||||
mirror_url=options.index_url,
|
|
||||||
repos=options.repos,
|
|
||||||
arch=options.arch,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return mirror_config_t.from_mirror_url(
|
|
||||||
mirror_url='https://archive.archlinux.org/repos/last',
|
|
||||||
repos=options.repos,
|
|
||||||
arch=options.arch,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def fetch_indices(
|
|
||||||
mirror: mirror_config_t,
|
|
||||||
cache_dir: Optional[pathlib.Path] = None,
|
|
||||||
no_cache: bool = False,
|
|
||||||
offline: bool = False,
|
|
||||||
) -> list[repo_index_t]:
|
|
||||||
indices: list[repo_index_t] = []
|
|
||||||
|
|
||||||
for repo in mirror.repos:
|
|
||||||
db_url = '%s/%s.db' % (repo.url, repo.name)
|
|
||||||
|
|
||||||
if cache_dir is not None and not no_cache:
|
|
||||||
cached_path = cache_dir / ('%s.db' % repo.name)
|
|
||||||
|
|
||||||
if cached_path.exists():
|
|
||||||
logger.info(
|
|
||||||
dict(
|
|
||||||
repo=repo.name,
|
|
||||||
msg='using cached db',
|
|
||||||
path=str(cached_path),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
index = db_parser_t.parse_db_path(cached_path, repo_name=repo.name)
|
|
||||||
indices.append(index)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if offline:
|
|
||||||
raise FileNotFoundError('offline mode: cached db not found for %s at %s' % (repo.name, str(cached_path)))
|
|
||||||
|
|
||||||
pacman_t.download_db(db_url, cached_path)
|
|
||||||
index = db_parser_t.parse_db_path(cached_path, repo_name=repo.name)
|
|
||||||
indices.append(index)
|
|
||||||
else:
|
|
||||||
if offline:
|
|
||||||
raise FileNotFoundError('offline mode requires --cache-dir with pre-fetched db files')
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
|
|
||||||
pacman_t.download_db(db_url, pathlib.Path(tmp.name))
|
|
||||||
index = db_parser_t.parse_db_path(pathlib.Path(tmp.name), repo_name=repo.name)
|
|
||||||
indices.append(index)
|
|
||||||
|
|
||||||
return indices
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def compile(
|
|
||||||
options: compile_options_t,
|
|
||||||
) -> compile_result_t.res_t:
|
|
||||||
mirror = compile_t.build_mirror_config(options)
|
|
||||||
|
|
||||||
cache_dir: Optional[pathlib.Path] = None
|
|
||||||
if options.cache_dir is not None:
|
|
||||||
cache_dir = pathlib.Path(options.cache_dir)
|
|
||||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
indices = compile_t.fetch_indices(
|
|
||||||
mirror=mirror,
|
|
||||||
cache_dir=cache_dir,
|
|
||||||
no_cache=options.no_cache,
|
|
||||||
offline=options.offline,
|
|
||||||
)
|
|
||||||
|
|
||||||
resolved = resolver_t.resolve(
|
|
||||||
packages=options.packages,
|
|
||||||
indices=indices,
|
|
||||||
)
|
|
||||||
|
|
||||||
result = compile_result_t.res_t()
|
|
||||||
|
|
||||||
for pkg_name in resolved.resolution_order:
|
|
||||||
pkg = resolved.resolved[pkg_name]
|
|
||||||
|
|
||||||
repo_name = ''
|
|
||||||
for idx in indices:
|
|
||||||
if pkg_name in idx.packages:
|
|
||||||
repo_name = idx.name
|
|
||||||
break
|
|
||||||
|
|
||||||
repo_url = ''
|
|
||||||
for repo_cfg in mirror.repos:
|
|
||||||
if repo_cfg.name == repo_name:
|
|
||||||
repo_url = repo_cfg.url
|
|
||||||
break
|
|
||||||
|
|
||||||
entry = compile_entry_t(
|
|
||||||
name=pkg.name,
|
|
||||||
version=pkg.version,
|
|
||||||
filename=pkg.filename,
|
|
||||||
repo=repo_name,
|
|
||||||
url='%s/%s' % (repo_url, pkg.filename) if repo_url and pkg.filename else '',
|
|
||||||
sha256=pkg.sha256sum if options.generate_hashes else '',
|
|
||||||
depends=pkg.depends,
|
|
||||||
)
|
|
||||||
|
|
||||||
result.entries.append(entry)
|
|
||||||
|
|
||||||
result.txt = result.to_txt()
|
|
||||||
|
|
||||||
return result
|
|
||||||
@ -1,157 +0,0 @@
|
|||||||
import io
|
|
||||||
import re
|
|
||||||
import tarfile
|
|
||||||
import logging
|
|
||||||
import pathlib
|
|
||||||
|
|
||||||
from typing import (
|
|
||||||
ClassVar,
|
|
||||||
Optional,
|
|
||||||
Any,
|
|
||||||
BinaryIO,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .models import (
|
|
||||||
package_desc_t,
|
|
||||||
repo_index_t,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class db_parser_t:
|
|
||||||
class constants_t:
|
|
||||||
field_re: ClassVar[re.Pattern[str]] = re.compile(r'^%([A-Z0-9]+)%$')
|
|
||||||
|
|
||||||
list_fields: ClassVar[set[str]] = {
|
|
||||||
'LICENSE',
|
|
||||||
'DEPENDS',
|
|
||||||
'OPTDEPENDS',
|
|
||||||
'MAKEDEPENDS',
|
|
||||||
'CHECKDEPENDS',
|
|
||||||
'PROVIDES',
|
|
||||||
'CONFLICTS',
|
|
||||||
'REPLACES',
|
|
||||||
'GROUPS',
|
|
||||||
}
|
|
||||||
|
|
||||||
field_map: ClassVar[dict[str, str]] = {
|
|
||||||
'FILENAME': 'filename',
|
|
||||||
'NAME': 'name',
|
|
||||||
'VERSION': 'version',
|
|
||||||
'DESC': 'desc',
|
|
||||||
'CSIZE': 'csize',
|
|
||||||
'ISIZE': 'isize',
|
|
||||||
'MD5SUM': 'md5sum',
|
|
||||||
'SHA256SUM': 'sha256sum',
|
|
||||||
'URL': 'url',
|
|
||||||
'ARCH': 'arch',
|
|
||||||
'BUILDDATE': 'builddate',
|
|
||||||
'PACKAGER': 'packager',
|
|
||||||
'LICENSE': 'license',
|
|
||||||
'DEPENDS': 'depends',
|
|
||||||
'OPTDEPENDS': 'optdepends',
|
|
||||||
'MAKEDEPENDS': 'makedepends',
|
|
||||||
'CHECKDEPENDS': 'checkdepends',
|
|
||||||
'PROVIDES': 'provides',
|
|
||||||
'CONFLICTS': 'conflicts',
|
|
||||||
'REPLACES': 'replaces',
|
|
||||||
'GROUPS': 'groups',
|
|
||||||
'BASE': 'base',
|
|
||||||
}
|
|
||||||
|
|
||||||
int_fields: ClassVar[set[str]] = {
|
|
||||||
'CSIZE',
|
|
||||||
'ISIZE',
|
|
||||||
'BUILDDATE',
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_desc(content: str) -> package_desc_t:
|
|
||||||
fields: dict[str, Any] = {}
|
|
||||||
lines = content.split('\n')
|
|
||||||
i = 0
|
|
||||||
|
|
||||||
while i < len(lines):
|
|
||||||
line = lines[i].strip()
|
|
||||||
|
|
||||||
if line == '':
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
m = db_parser_t.constants_t.field_re.match(line)
|
|
||||||
if not m:
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
field_name = m.group(1)
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
values: list[str] = []
|
|
||||||
while i < len(lines) and lines[i].strip() != '':
|
|
||||||
values.append(lines[i].strip())
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
attr_name = db_parser_t.constants_t.field_map.get(field_name)
|
|
||||||
if attr_name is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if field_name in db_parser_t.constants_t.list_fields:
|
|
||||||
fields[attr_name] = values
|
|
||||||
elif field_name in db_parser_t.constants_t.int_fields:
|
|
||||||
fields[attr_name] = int(values[0]) if len(values) > 0 else 0
|
|
||||||
else:
|
|
||||||
fields[attr_name] = values[0] if len(values) > 0 else ''
|
|
||||||
|
|
||||||
if 'name' not in fields or 'version' not in fields:
|
|
||||||
raise ValueError('desc missing NAME or VERSION')
|
|
||||||
|
|
||||||
return package_desc_t(**fields)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_db(
|
|
||||||
f: BinaryIO,
|
|
||||||
repo_name: str = '',
|
|
||||||
) -> repo_index_t:
|
|
||||||
index = repo_index_t(name=repo_name)
|
|
||||||
|
|
||||||
with tarfile.open(fileobj=f, mode='r:*') as tar:
|
|
||||||
desc_members: list[tarfile.TarInfo] = []
|
|
||||||
|
|
||||||
for member in tar.getmembers():
|
|
||||||
if member.name.endswith('/desc') and member.isfile():
|
|
||||||
desc_members.append(member)
|
|
||||||
|
|
||||||
for member in desc_members:
|
|
||||||
extracted = tar.extractfile(member)
|
|
||||||
if extracted is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
content = extracted.read().decode('utf-8')
|
|
||||||
extracted.close()
|
|
||||||
|
|
||||||
try:
|
|
||||||
pkg = db_parser_t.parse_desc(content)
|
|
||||||
index.packages[pkg.name] = pkg
|
|
||||||
except ValueError:
|
|
||||||
logger.warning(
|
|
||||||
dict(
|
|
||||||
member=member.name,
|
|
||||||
msg='failed to parse desc',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
index.build_provides_index()
|
|
||||||
|
|
||||||
return index
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_db_path(
|
|
||||||
path: pathlib.Path,
|
|
||||||
repo_name: Optional[str] = None,
|
|
||||||
) -> repo_index_t:
|
|
||||||
if repo_name is None:
|
|
||||||
repo_name = path.stem.split('.')[0]
|
|
||||||
|
|
||||||
with io.open(path, 'rb') as f:
|
|
||||||
return db_parser_t.parse_db(f, repo_name=repo_name)
|
|
||||||
@ -1,182 +0,0 @@
|
|||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import pathlib
|
|
||||||
import dataclasses
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from typing import (
|
|
||||||
ClassVar,
|
|
||||||
Optional,
|
|
||||||
Any,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .models import (
|
|
||||||
package_desc_t,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class pacman_t:
|
|
||||||
class constants_t:
|
|
||||||
default_db_path: ClassVar[pathlib.Path] = pathlib.Path('/var/lib/pacman')
|
|
||||||
default_cache_dir: ClassVar[pathlib.Path] = pathlib.Path('/var/cache/pacman/pkg')
|
|
||||||
field_re: ClassVar[re.Pattern[str]] = re.compile(r'^([A-Za-z ]+?)\s*:\s*(.*)$')
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class query_entry_t:
|
|
||||||
name: str
|
|
||||||
version: str
|
|
||||||
description: str = ''
|
|
||||||
architecture: str = ''
|
|
||||||
url: str = ''
|
|
||||||
depends_on: list[str] = dataclasses.field(default_factory=lambda: list[str]())
|
|
||||||
provides: list[str] = dataclasses.field(default_factory=lambda: list[str]())
|
|
||||||
conflicts_with: list[str] = dataclasses.field(default_factory=lambda: list[str]())
|
|
||||||
replaces: list[str] = dataclasses.field(default_factory=lambda: list[str]())
|
|
||||||
install_size: str = ''
|
|
||||||
packager: str = ''
|
|
||||||
groups: list[str] = dataclasses.field(default_factory=lambda: list[str]())
|
|
||||||
|
|
||||||
class list_installed_t:
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class res_t:
|
|
||||||
packages: list['pacman_t.query_entry_t'] = dataclasses.field(default_factory=lambda: list[pacman_t.query_entry_t]())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_info_block(block: str) -> 'pacman_t.query_entry_t':
|
|
||||||
fields: dict[str, list[str]] = {}
|
|
||||||
current_key: Optional[str] = None
|
|
||||||
|
|
||||||
for line in block.split('\n'):
|
|
||||||
m = pacman_t.constants_t.field_re.match(line)
|
|
||||||
if m:
|
|
||||||
current_key = m.group(1).strip()
|
|
||||||
value = m.group(2).strip()
|
|
||||||
assert isinstance(current_key, str)
|
|
||||||
if current_key not in fields:
|
|
||||||
fields[current_key] = []
|
|
||||||
if value and value != 'None':
|
|
||||||
fields[current_key].append(value)
|
|
||||||
elif current_key and line.startswith(' '):
|
|
||||||
value = line.strip()
|
|
||||||
if value and value != 'None':
|
|
||||||
fields[current_key].append(value)
|
|
||||||
|
|
||||||
name = fields.get('Name', [''])[0]
|
|
||||||
version = fields.get('Version', [''])[0]
|
|
||||||
|
|
||||||
if not name or not version:
|
|
||||||
raise ValueError('missing Name or Version in block')
|
|
||||||
|
|
||||||
return pacman_t.query_entry_t(
|
|
||||||
name=name,
|
|
||||||
version=version,
|
|
||||||
description=fields.get('Description', [''])[0] if fields.get('Description') else '',
|
|
||||||
architecture=fields.get('Architecture', [''])[0] if fields.get('Architecture') else '',
|
|
||||||
url=fields.get('URL', [''])[0] if fields.get('URL') else '',
|
|
||||||
depends_on=fields.get('Depends On', []),
|
|
||||||
provides=fields.get('Provides', []),
|
|
||||||
conflicts_with=fields.get('Conflicts With', []),
|
|
||||||
replaces=fields.get('Replaces', []),
|
|
||||||
install_size=fields.get('Installed Size', [''])[0] if fields.get('Installed Size') else '',
|
|
||||||
packager=fields.get('Packager', [''])[0] if fields.get('Packager') else '',
|
|
||||||
groups=fields.get('Groups', []),
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def list_installed(
|
|
||||||
db_path: Optional[pathlib.Path] = None,
|
|
||||||
) -> 'pacman_t.list_installed_t.res_t':
|
|
||||||
cmd: list[str] = ['pacman', '-Qi']
|
|
||||||
|
|
||||||
if db_path is not None:
|
|
||||||
cmd.extend(['--dbpath', str(db_path)])
|
|
||||||
|
|
||||||
output = subprocess.check_output(
|
|
||||||
cmd,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
).decode('utf-8')
|
|
||||||
|
|
||||||
blocks = output.split('\n\n')
|
|
||||||
result = pacman_t.list_installed_t.res_t()
|
|
||||||
|
|
||||||
for block in blocks:
|
|
||||||
block = block.strip()
|
|
||||||
if not block:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
entry = pacman_t.parse_info_block(block)
|
|
||||||
result.packages.append(entry)
|
|
||||||
except ValueError:
|
|
||||||
logger.warning(
|
|
||||||
dict(
|
|
||||||
msg='failed to parse pacman info block',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def list_installed_simple(
|
|
||||||
db_path: Optional[pathlib.Path] = None,
|
|
||||||
) -> list[tuple[str, str]]:
|
|
||||||
cmd: list[str] = ['pacman', '-Q']
|
|
||||||
|
|
||||||
if db_path is not None:
|
|
||||||
cmd.extend(['--dbpath', str(db_path)])
|
|
||||||
|
|
||||||
output = subprocess.check_output(
|
|
||||||
cmd,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
).decode('utf-8')
|
|
||||||
|
|
||||||
result: list[tuple[str, str]] = []
|
|
||||||
|
|
||||||
for line in output.strip().split('\n'):
|
|
||||||
parts = line.strip().split(None, 1)
|
|
||||||
if len(parts) == 2:
|
|
||||||
result.append((parts[0], parts[1]))
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def sync_db(
|
|
||||||
mirror_url: str,
|
|
||||||
db_path: pathlib.Path,
|
|
||||||
repos: Optional[list[str]] = None,
|
|
||||||
) -> None:
|
|
||||||
if repos is None:
|
|
||||||
repos = ['core', 'extra', 'multilib']
|
|
||||||
|
|
||||||
cmd: list[str] = [
|
|
||||||
'pacman',
|
|
||||||
'-Sy',
|
|
||||||
'--dbpath',
|
|
||||||
str(db_path),
|
|
||||||
]
|
|
||||||
|
|
||||||
subprocess.check_call(cmd)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def download_db(
|
|
||||||
url: str,
|
|
||||||
output_path: pathlib.Path,
|
|
||||||
) -> None:
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
dict(
|
|
||||||
url=url,
|
|
||||||
output_path=str(output_path),
|
|
||||||
msg='downloading db',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
urllib.request.urlretrieve(
|
|
||||||
url,
|
|
||||||
str(output_path),
|
|
||||||
)
|
|
||||||
@ -1,161 +0,0 @@
|
|||||||
import dataclasses
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from typing import (
|
|
||||||
Optional,
|
|
||||||
Any,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .models import (
|
|
||||||
package_desc_t,
|
|
||||||
package_constraint_t,
|
|
||||||
repo_index_t,
|
|
||||||
vercmp_t,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class resolver_t:
|
|
||||||
class error_t:
|
|
||||||
class not_found_t(Exception):
|
|
||||||
def __init__(self, name: str) -> None:
|
|
||||||
self.name = name
|
|
||||||
super().__init__('package not found: %s' % name)
|
|
||||||
|
|
||||||
class conflict_t(Exception):
|
|
||||||
def __init__(self, pkg_a: str, pkg_b: str, constraint: str) -> None:
|
|
||||||
self.pkg_a = pkg_a
|
|
||||||
self.pkg_b = pkg_b
|
|
||||||
self.constraint = constraint
|
|
||||||
super().__init__('conflict: %s conflicts with %s (%s)' % (pkg_a, pkg_b, constraint))
|
|
||||||
|
|
||||||
class unsatisfied_t(Exception):
|
|
||||||
def __init__(self, parent: str, dep: str) -> None:
|
|
||||||
self.parent = parent
|
|
||||||
self.dep = dep
|
|
||||||
super().__init__('unsatisfied dependency: %s requires %s' % (parent, dep))
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class res_t:
|
|
||||||
resolved: dict[str, package_desc_t] = dataclasses.field(default_factory=lambda: dict[str, package_desc_t]())
|
|
||||||
resolution_order: list[str] = dataclasses.field(default_factory=lambda: list[str]())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _find_provider(
|
|
||||||
constraint: package_constraint_t,
|
|
||||||
indices: list[repo_index_t],
|
|
||||||
) -> Optional[tuple[package_desc_t, str]]:
|
|
||||||
for index in indices:
|
|
||||||
if constraint.name in index.packages:
|
|
||||||
pkg = index.packages[constraint.name]
|
|
||||||
if constraint.satisfied_by(pkg.version):
|
|
||||||
return (pkg, index.name)
|
|
||||||
|
|
||||||
for index in indices:
|
|
||||||
if constraint.name in index.provides_index:
|
|
||||||
for provider_name in index.provides_index[constraint.name]:
|
|
||||||
pkg = index.packages[provider_name]
|
|
||||||
for prov in pkg.parsed_provides():
|
|
||||||
if prov.name == constraint.name:
|
|
||||||
if constraint.version is None or prov.version is None:
|
|
||||||
return (pkg, index.name)
|
|
||||||
if constraint.satisfied_by(prov.version):
|
|
||||||
return (pkg, index.name)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve(
|
|
||||||
packages: list[str],
|
|
||||||
indices: list[repo_index_t],
|
|
||||||
skip_installed: Optional[set[str]] = None,
|
|
||||||
) -> 'resolver_t.res_t':
|
|
||||||
if skip_installed is None:
|
|
||||||
skip_installed = set()
|
|
||||||
|
|
||||||
result = resolver_t.res_t()
|
|
||||||
visited: set[str] = set()
|
|
||||||
stack: list[tuple[package_constraint_t, Optional[str]]] = []
|
|
||||||
|
|
||||||
for pkg_str in packages:
|
|
||||||
constraint = package_constraint_t.parse(pkg_str)
|
|
||||||
stack.append((constraint, None))
|
|
||||||
|
|
||||||
while len(stack) > 0:
|
|
||||||
constraint, parent = stack.pop()
|
|
||||||
|
|
||||||
if constraint.name in visited:
|
|
||||||
if constraint.name in result.resolved:
|
|
||||||
pkg = result.resolved[constraint.name]
|
|
||||||
if not constraint.satisfied_by(pkg.version):
|
|
||||||
raise resolver_t.error_t.unsatisfied_t(
|
|
||||||
parent=parent or '<root>',
|
|
||||||
dep=constraint.to_str(),
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if constraint.name in skip_installed:
|
|
||||||
visited.add(constraint.name)
|
|
||||||
continue
|
|
||||||
|
|
||||||
found = resolver_t._find_provider(constraint, indices)
|
|
||||||
|
|
||||||
if found is None:
|
|
||||||
exists = any(constraint.name in idx.packages or constraint.name in idx.provides_index for idx in indices)
|
|
||||||
if exists:
|
|
||||||
raise resolver_t.error_t.unsatisfied_t(
|
|
||||||
parent=parent or '<root>',
|
|
||||||
dep=constraint.to_str(),
|
|
||||||
)
|
|
||||||
raise resolver_t.error_t.not_found_t(constraint.name)
|
|
||||||
|
|
||||||
pkg, repo_name = found
|
|
||||||
|
|
||||||
if pkg.name in visited:
|
|
||||||
if pkg.name in result.resolved and constraint.op is not None:
|
|
||||||
resolved_pkg = result.resolved[pkg.name]
|
|
||||||
if constraint.name == resolved_pkg.name:
|
|
||||||
if not constraint.satisfied_by(resolved_pkg.version):
|
|
||||||
raise resolver_t.error_t.unsatisfied_t(
|
|
||||||
parent=parent or '<root>',
|
|
||||||
dep=constraint.to_str(),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
matched = False
|
|
||||||
for prov in resolved_pkg.parsed_provides():
|
|
||||||
if prov.name == constraint.name:
|
|
||||||
if prov.version is not None and constraint.satisfied_by(prov.version):
|
|
||||||
matched = True
|
|
||||||
break
|
|
||||||
elif prov.version is None:
|
|
||||||
matched = True
|
|
||||||
break
|
|
||||||
if not matched:
|
|
||||||
raise resolver_t.error_t.unsatisfied_t(
|
|
||||||
parent=parent or '<root>',
|
|
||||||
dep=constraint.to_str(),
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
visited.add(pkg.name)
|
|
||||||
visited.add(constraint.name)
|
|
||||||
|
|
||||||
result.resolved[pkg.name] = pkg
|
|
||||||
result.resolution_order.append(pkg.name)
|
|
||||||
|
|
||||||
for conflict in pkg.parsed_conflicts():
|
|
||||||
if conflict.name in result.resolved:
|
|
||||||
resolved_version = result.resolved[conflict.name].version
|
|
||||||
if conflict.satisfied_by(resolved_version):
|
|
||||||
raise resolver_t.error_t.conflict_t(
|
|
||||||
pkg_a=pkg.name,
|
|
||||||
pkg_b=conflict.name,
|
|
||||||
constraint=conflict.to_str(),
|
|
||||||
)
|
|
||||||
|
|
||||||
for dep in pkg.parsed_depends():
|
|
||||||
if dep.name not in visited and dep.name not in skip_installed:
|
|
||||||
stack.append((dep, pkg.name))
|
|
||||||
|
|
||||||
return result
|
|
||||||
@ -1,416 +0,0 @@
|
|||||||
import hashlib
|
|
||||||
import io
|
|
||||||
import logging
|
|
||||||
import pathlib
|
|
||||||
import re
|
|
||||||
|
|
||||||
from typing import (
|
|
||||||
ClassVar,
|
|
||||||
Optional,
|
|
||||||
Any,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .models import (
|
|
||||||
package_desc_t,
|
|
||||||
repo_index_t,
|
|
||||||
compile_options_t,
|
|
||||||
compile_entry_t,
|
|
||||||
compile_result_t,
|
|
||||||
mirror_config_t,
|
|
||||||
resolution_strategy_t,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .db import db_parser_t
|
|
||||||
from .compile import compile_t as compile_base_t
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class repo_store_t:
|
|
||||||
class constants_t:
|
|
||||||
checksum_filename: ClassVar[str] = 'checksum.sha256'
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
index: repo_index_t,
|
|
||||||
db_checksum: str = '',
|
|
||||||
) -> None:
|
|
||||||
self.index = index
|
|
||||||
self.db_checksum = db_checksum
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _file_checksum(path: pathlib.Path) -> str:
|
|
||||||
h = hashlib.sha256()
|
|
||||||
with io.open(path, 'rb') as f:
|
|
||||||
while True:
|
|
||||||
chunk = f.read(65536)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
h.update(chunk)
|
|
||||||
return h.hexdigest()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_db(
|
|
||||||
db_path: pathlib.Path,
|
|
||||||
repo_name: Optional[str] = None,
|
|
||||||
cache_dir: Optional[pathlib.Path] = None,
|
|
||||||
) -> 'repo_store_t':
|
|
||||||
if repo_name is None:
|
|
||||||
repo_name = db_path.stem.split('.')[0]
|
|
||||||
|
|
||||||
db_checksum = repo_store_t._file_checksum(db_path)
|
|
||||||
|
|
||||||
if cache_dir is not None:
|
|
||||||
solv_cache_path = cache_dir / ('%s.solv' % repo_name)
|
|
||||||
checksum_path = cache_dir / ('%s.solv.sha256' % repo_name)
|
|
||||||
index_cache_path = cache_dir / ('%s.index.solv' % repo_name)
|
|
||||||
|
|
||||||
if solv_cache_path.exists() and checksum_path.exists():
|
|
||||||
stored_checksum = checksum_path.read_text().strip()
|
|
||||||
if stored_checksum == db_checksum:
|
|
||||||
logger.info(
|
|
||||||
dict(
|
|
||||||
repo=repo_name,
|
|
||||||
msg='using cached solv',
|
|
||||||
path=str(solv_cache_path),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
index = db_parser_t.parse_db_path(db_path, repo_name=repo_name)
|
|
||||||
|
|
||||||
return repo_store_t(
|
|
||||||
index=index,
|
|
||||||
db_checksum=db_checksum,
|
|
||||||
)
|
|
||||||
|
|
||||||
index = db_parser_t.parse_db_path(db_path, repo_name=repo_name)
|
|
||||||
|
|
||||||
return repo_store_t(
|
|
||||||
index=index,
|
|
||||||
db_checksum=db_checksum,
|
|
||||||
)
|
|
||||||
|
|
||||||
def write_solv_cache(
|
|
||||||
self,
|
|
||||||
cache_dir: pathlib.Path,
|
|
||||||
solv_repo: Any,
|
|
||||||
) -> None:
|
|
||||||
import solv
|
|
||||||
|
|
||||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
solv_cache_path = cache_dir / ('%s.solv' % self.index.name)
|
|
||||||
checksum_path = cache_dir / ('%s.solv.sha256' % self.index.name)
|
|
||||||
|
|
||||||
f = solv.xfopen(str(solv_cache_path), 'w')
|
|
||||||
solv_repo.write(f)
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
checksum_path.write_text(self.db_checksum)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
dict(
|
|
||||||
repo=self.index.name,
|
|
||||||
msg='wrote solv cache',
|
|
||||||
path=str(solv_cache_path),
|
|
||||||
size=solv_cache_path.stat().st_size,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class solv_pool_t:
|
|
||||||
class constants_t:
|
|
||||||
dep_re: ClassVar[re.Pattern[str]] = re.compile(r'^([a-zA-Z0-9@._+\-]+?)(?:(>=|<=|>|<|=)(.+))?$')
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
stores: Optional[list[repo_store_t]] = None,
|
|
||||||
cache_dir: Optional[pathlib.Path] = None,
|
|
||||||
) -> None:
|
|
||||||
import solv
|
|
||||||
|
|
||||||
self._solv = solv
|
|
||||||
self._pool = solv.Pool()
|
|
||||||
self._pool.setdisttype(solv.Pool.DISTTYPE_ARCH)
|
|
||||||
self._pool.setarch('x86_64')
|
|
||||||
self._rel_map = {
|
|
||||||
'>=': solv.REL_GT | solv.REL_EQ,
|
|
||||||
'<=': solv.REL_LT | solv.REL_EQ,
|
|
||||||
'>': solv.REL_GT,
|
|
||||||
'<': solv.REL_LT,
|
|
||||||
'=': solv.REL_EQ,
|
|
||||||
}
|
|
||||||
self._stores: list[repo_store_t] = []
|
|
||||||
|
|
||||||
if stores is not None:
|
|
||||||
for store in stores:
|
|
||||||
self.add_store(store, cache_dir=cache_dir)
|
|
||||||
self.finalize()
|
|
||||||
|
|
||||||
def _parse_dep(self, dep_str: str) -> Any:
|
|
||||||
m = solv_pool_t.constants_t.dep_re.match(dep_str.strip())
|
|
||||||
if not m:
|
|
||||||
return self._pool.str2id(dep_str)
|
|
||||||
|
|
||||||
name = m.group(1)
|
|
||||||
op = m.group(2)
|
|
||||||
ver = m.group(3)
|
|
||||||
|
|
||||||
name_id = self._pool.str2id(name)
|
|
||||||
|
|
||||||
if op and ver:
|
|
||||||
ver_id = self._pool.str2id(ver)
|
|
||||||
return self._pool.rel2id(name_id, ver_id, self._rel_map[op])
|
|
||||||
|
|
||||||
return name_id
|
|
||||||
|
|
||||||
def add_store(
|
|
||||||
self,
|
|
||||||
store: repo_store_t,
|
|
||||||
cache_dir: Optional[pathlib.Path] = None,
|
|
||||||
) -> None:
|
|
||||||
solv = self._solv
|
|
||||||
|
|
||||||
self._stores.append(store)
|
|
||||||
|
|
||||||
loaded_from_cache = False
|
|
||||||
|
|
||||||
if cache_dir is not None:
|
|
||||||
solv_cache_path = cache_dir / ('%s.solv' % store.index.name)
|
|
||||||
checksum_path = cache_dir / ('%s.solv.sha256' % store.index.name)
|
|
||||||
|
|
||||||
if solv_cache_path.exists() and checksum_path.exists():
|
|
||||||
stored_checksum = checksum_path.read_text().strip()
|
|
||||||
if stored_checksum == store.db_checksum:
|
|
||||||
repo = self._pool.add_repo(store.index.name)
|
|
||||||
f = solv.xfopen(str(solv_cache_path))
|
|
||||||
repo.add_solv(f)
|
|
||||||
f.close()
|
|
||||||
loaded_from_cache = True
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
dict(
|
|
||||||
repo=store.index.name,
|
|
||||||
msg='loaded solv from cache',
|
|
||||||
solvables=repo.nsolvables,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not loaded_from_cache:
|
|
||||||
repo = self._pool.add_repo(store.index.name)
|
|
||||||
for pkg in store.index.packages.values():
|
|
||||||
s = repo.add_solvable()
|
|
||||||
s.name = pkg.name
|
|
||||||
s.evr = pkg.version
|
|
||||||
s.arch = 'noarch' if pkg.arch == 'any' else (pkg.arch or 'x86_64')
|
|
||||||
|
|
||||||
for dep_str in pkg.depends:
|
|
||||||
s.add_requires(self._parse_dep(dep_str))
|
|
||||||
|
|
||||||
for prov_str in pkg.provides:
|
|
||||||
s.add_provides(self._parse_dep(prov_str))
|
|
||||||
|
|
||||||
s.add_provides(self._pool.rel2id(s.nameid, s.evrid, solv.REL_EQ))
|
|
||||||
|
|
||||||
for conf_str in pkg.conflicts:
|
|
||||||
s.add_conflicts(self._parse_dep(conf_str))
|
|
||||||
|
|
||||||
repo.internalize()
|
|
||||||
|
|
||||||
if cache_dir is not None:
|
|
||||||
store.write_solv_cache(cache_dir, repo)
|
|
||||||
|
|
||||||
def finalize(self) -> None:
|
|
||||||
self._pool.createwhatprovides()
|
|
||||||
|
|
||||||
class resolve_t:
|
|
||||||
class res_t:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.resolved: dict[str, Any] = {}
|
|
||||||
self.problems: list[str] = []
|
|
||||||
|
|
||||||
def expand_groups(
|
|
||||||
self,
|
|
||||||
packages: list[str],
|
|
||||||
) -> list[str]:
|
|
||||||
expanded: list[str] = []
|
|
||||||
for pkg_name in packages:
|
|
||||||
found_group = False
|
|
||||||
for store in self._stores:
|
|
||||||
if pkg_name in store.index.groups_index:
|
|
||||||
expanded.extend(store.index.groups_index[pkg_name])
|
|
||||||
found_group = True
|
|
||||||
break
|
|
||||||
if not found_group:
|
|
||||||
expanded.append(pkg_name)
|
|
||||||
return expanded
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_reference(txt: str) -> dict[str, str]:
|
|
||||||
pinned: dict[str, str] = {}
|
|
||||||
for line in txt.splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if line == '' or line.startswith('#'):
|
|
||||||
continue
|
|
||||||
parts = line.split()
|
|
||||||
pkg_spec = parts[0]
|
|
||||||
if '==' in pkg_spec:
|
|
||||||
name, version = pkg_spec.split('==', 1)
|
|
||||||
pinned[name] = version
|
|
||||||
return pinned
|
|
||||||
|
|
||||||
def resolve(
|
|
||||||
self,
|
|
||||||
packages: list[str],
|
|
||||||
expand_groups: bool = True,
|
|
||||||
pinned: Optional[dict[str, str]] = None,
|
|
||||||
upgrade_packages: Optional[list[str]] = None,
|
|
||||||
) -> 'solv_pool_t.resolve_t.res_t':
|
|
||||||
solv = self._solv
|
|
||||||
|
|
||||||
if expand_groups:
|
|
||||||
packages = self.expand_groups(packages)
|
|
||||||
|
|
||||||
result = solv_pool_t.resolve_t.res_t()
|
|
||||||
|
|
||||||
solver = self._pool.Solver()
|
|
||||||
jobs: list[Any] = []
|
|
||||||
|
|
||||||
upgrade_set: set[str] = set()
|
|
||||||
if upgrade_packages is not None:
|
|
||||||
if expand_groups:
|
|
||||||
upgrade_packages = self.expand_groups(upgrade_packages)
|
|
||||||
upgrade_set = set(upgrade_packages)
|
|
||||||
|
|
||||||
for pkg_spec in packages:
|
|
||||||
pkg_name = pkg_spec.split('>=')[0].split('<=')[0].split('>')[0].split('<')[0].split('=')[0]
|
|
||||||
|
|
||||||
if pinned is not None and pkg_name in pinned and pkg_name not in upgrade_set:
|
|
||||||
pinned_spec = '%s=%s' % (pkg_name, pinned[pkg_name])
|
|
||||||
dep = self._parse_dep(pinned_spec)
|
|
||||||
jobs.append(self._pool.Job(solv.Job.SOLVER_INSTALL | solv.Job.SOLVER_SOLVABLE_PROVIDES, dep))
|
|
||||||
else:
|
|
||||||
dep = self._parse_dep(pkg_spec)
|
|
||||||
|
|
||||||
sel = self._pool.select(pkg_name, solv.Selection.SELECTION_NAME | solv.Selection.SELECTION_PROVIDES)
|
|
||||||
if sel.isempty():
|
|
||||||
result.problems.append('package not found: %s' % pkg_spec)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if pkg_name != pkg_spec:
|
|
||||||
jobs.append(self._pool.Job(solv.Job.SOLVER_INSTALL | solv.Job.SOLVER_SOLVABLE_PROVIDES, dep))
|
|
||||||
else:
|
|
||||||
jobs += sel.jobs(solv.Job.SOLVER_INSTALL)
|
|
||||||
|
|
||||||
if len(result.problems) > 0:
|
|
||||||
return result
|
|
||||||
|
|
||||||
problems = solver.solve(jobs)
|
|
||||||
|
|
||||||
if problems:
|
|
||||||
for p in problems:
|
|
||||||
result.problems.append(str(p))
|
|
||||||
return result
|
|
||||||
|
|
||||||
trans = solver.transaction()
|
|
||||||
for s in trans.newsolvables():
|
|
||||||
result.resolved[s.name] = s
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class compile_solv_t:
|
|
||||||
@staticmethod
|
|
||||||
def compile(
|
|
||||||
options: compile_options_t,
|
|
||||||
stores: Optional[list[repo_store_t]] = None,
|
|
||||||
) -> compile_result_t.res_t:
|
|
||||||
mirror = compile_base_t.build_mirror_config(options)
|
|
||||||
|
|
||||||
cache_dir: Optional[pathlib.Path] = None
|
|
||||||
if options.cache_dir is not None:
|
|
||||||
cache_dir = pathlib.Path(options.cache_dir)
|
|
||||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
if stores is None:
|
|
||||||
indices = compile_base_t.fetch_indices(
|
|
||||||
mirror=mirror,
|
|
||||||
cache_dir=cache_dir,
|
|
||||||
no_cache=options.no_cache,
|
|
||||||
offline=options.offline,
|
|
||||||
)
|
|
||||||
stores = [repo_store_t(index=idx) for idx in indices]
|
|
||||||
|
|
||||||
pool = solv_pool_t(stores=stores, cache_dir=cache_dir)
|
|
||||||
|
|
||||||
pinned: Optional[dict[str, str]] = None
|
|
||||||
upgrade_packages: Optional[list[str]] = None
|
|
||||||
|
|
||||||
if options.reference is not None:
|
|
||||||
ref_txt = pathlib.Path(options.reference).read_text()
|
|
||||||
pinned = solv_pool_t.parse_reference(ref_txt)
|
|
||||||
|
|
||||||
if options.resolution_strategy is resolution_strategy_t.pin_referenced:
|
|
||||||
upgrade_packages = options.packages
|
|
||||||
packages = list(pinned.keys()) + [p for p in options.packages if p not in pinned]
|
|
||||||
else:
|
|
||||||
packages = options.packages
|
|
||||||
else:
|
|
||||||
packages = options.packages
|
|
||||||
|
|
||||||
resolved = pool.resolve(
|
|
||||||
packages,
|
|
||||||
pinned=pinned if options.resolution_strategy is resolution_strategy_t.pin_referenced else None,
|
|
||||||
upgrade_packages=upgrade_packages,
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(resolved.problems) > 0:
|
|
||||||
raise RuntimeError('resolution failed with %d problem(s):\n%s' % (len(resolved.problems), '\n'.join(resolved.problems)))
|
|
||||||
|
|
||||||
result = compile_result_t.res_t()
|
|
||||||
|
|
||||||
for pkg_name, solvable in resolved.resolved.items():
|
|
||||||
repo_name = solvable.repo.name if solvable.repo else ''
|
|
||||||
|
|
||||||
pkg_desc: Optional[package_desc_t] = None
|
|
||||||
for store in stores:
|
|
||||||
candidate = store.index.packages.get(pkg_name)
|
|
||||||
if candidate is not None and candidate.version == solvable.evr:
|
|
||||||
pkg_desc = candidate
|
|
||||||
if store.index.name == repo_name:
|
|
||||||
break
|
|
||||||
|
|
||||||
filename = pkg_desc.filename if pkg_desc else ''
|
|
||||||
sha256 = (pkg_desc.sha256sum if pkg_desc else '') if options.generate_hashes else ''
|
|
||||||
|
|
||||||
url = ''
|
|
||||||
if filename:
|
|
||||||
repo_url = ''
|
|
||||||
for repo_cfg in mirror.repos:
|
|
||||||
if repo_cfg.name == repo_name:
|
|
||||||
repo_url = repo_cfg.url
|
|
||||||
break
|
|
||||||
|
|
||||||
if repo_url:
|
|
||||||
url = '%s/%s' % (repo_url, filename)
|
|
||||||
else:
|
|
||||||
url = 'https://archive.archlinux.org/packages/%s/%s/%s' % (
|
|
||||||
pkg_name[0],
|
|
||||||
pkg_name,
|
|
||||||
filename,
|
|
||||||
)
|
|
||||||
|
|
||||||
entry = compile_entry_t(
|
|
||||||
name=pkg_name,
|
|
||||||
version=solvable.evr,
|
|
||||||
filename=filename,
|
|
||||||
repo=repo_name,
|
|
||||||
url=url,
|
|
||||||
sha256=sha256,
|
|
||||||
depends=pkg_desc.depends if pkg_desc else [],
|
|
||||||
)
|
|
||||||
|
|
||||||
result.entries.append(entry)
|
|
||||||
|
|
||||||
result.txt = result.to_txt()
|
|
||||||
|
|
||||||
return result
|
|
||||||
Loading…
Reference in New Issue
Block a user