Add Debian, Ubuntu and Mint candidate profiles with scoped systemd 255 credentials
This commit is contained in:
1 parent
b23819fff1
commit
a48a20c936
28 files changed
+2527
-274
No files matched your search
@@ -0,0 +1,193 @@
|
||||
"""Explicit Debian-family candidate targets and offline vendor validation.
|
||||
|
||||
This module never installs dependencies or imports the target's native modules.
|
||||
Profiles describe eligible build inputs, not completed OS/desktop validation.
|
||||
"""
|
||||
import base64
|
||||
import csv
|
||||
from email.parser import Parser
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
CANDIDATE_VERSION = "0.3.2~compat.20261027.1"
|
||||
VENDOR_RELATIVE = Path("original/geranas/FrameBridge-Fleet-Hub/vendor")
|
||||
SOURCE_VENDOR = Path("sources/original-geranas/FrameBridge-Fleet-Hub/vendor")
|
||||
GLIBC_BASELINE = (2, 34)
|
||||
PROFILES = {
|
||||
"debian13-amd64-py313": {"os_id": "debian", "os_version": "13", "python_minor": "3.13"},
|
||||
"ubuntu24.04-amd64-py312": {"os_id": "ubuntu", "os_version": "24.04", "python_minor": "3.12"},
|
||||
"mint22-amd64-py312": {"os_id": "linuxmint", "os_version": "22", "python_minor": "3.12"},
|
||||
"ubuntu26.04-amd64-py314": {"os_id": "ubuntu", "os_version": "26.04", "python_minor": "3.14"},
|
||||
"lmde7-amd64-py313": {"os_id": "linuxmint", "os_version": "7", "python_minor": "3.13"},
|
||||
}
|
||||
|
||||
|
||||
def target_profile(profile_id):
|
||||
profile = dict(PROFILES[profile_id])
|
||||
profile.update(schema=1, profile_id=profile_id, architecture="amd64",
|
||||
python_abi="cp" + profile["python_minor"].replace(".", ""),
|
||||
validation_status="candidate", glibc_minimum="2.34")
|
||||
return profile
|
||||
|
||||
|
||||
def _normalized(name):
|
||||
return re.sub(r"[-_.]+", "-", name).lower()
|
||||
|
||||
|
||||
def distribution_metadata(directory):
|
||||
directory = Path(directory)
|
||||
if not directory.is_dir() or directory.is_symlink():
|
||||
raise RuntimeError("Vendor input must be a real directory: " + str(directory))
|
||||
for path in directory.rglob("*"):
|
||||
if path.is_symlink():
|
||||
raise RuntimeError("Vendor symlinks are not supported: " + str(path))
|
||||
result = {}
|
||||
for info in sorted(directory.glob("*.dist-info")):
|
||||
metadata = info / "METADATA"
|
||||
if not metadata.is_file():
|
||||
raise RuntimeError("Missing vendor METADATA: " + str(info))
|
||||
msg = Parser().parsestr(metadata.read_text(encoding="utf-8"))
|
||||
name, version = msg.get("Name"), msg.get("Version")
|
||||
if not name or not version or _normalized(name) in result:
|
||||
raise RuntimeError("Missing or duplicate vendor distribution identity: " + str(info))
|
||||
result[_normalized(name)] = (version, info, msg)
|
||||
if not result:
|
||||
raise RuntimeError("Vendor input contains no distribution metadata")
|
||||
return result
|
||||
|
||||
|
||||
def vendor_versions(directory):
|
||||
return {name: value[0] for name, value in distribution_metadata(directory).items()}
|
||||
|
||||
|
||||
def _python_allowed(requirement, minor):
|
||||
# Fail closed on unsupported specifier syntax rather than silently omitting it.
|
||||
version = tuple(map(int, minor.split("."))) + (0,)
|
||||
for part in requirement.split(","):
|
||||
match = re.fullmatch(r"\s*(>=|<=|!=|==|>|<)\s*(\d+(?:\.\d+){0,2})(\.\*)?\s*", part)
|
||||
if not match:
|
||||
raise RuntimeError("Unsupported Requires-Python specifier: " + requirement)
|
||||
op, number, wildcard = match.groups()
|
||||
components = tuple(map(int, number.split(".")))
|
||||
wanted = components + (0,) * (3 - len(components))
|
||||
if wildcard:
|
||||
if op not in ("==", "!="):
|
||||
raise RuntimeError("Invalid Requires-Python wildcard: " + requirement)
|
||||
equal = version[:len(components)] == components
|
||||
allowed = equal if op == "==" else not equal
|
||||
else:
|
||||
allowed = {">=": version >= wanted, "<=": version <= wanted,
|
||||
">": version > wanted, "<": version < wanted,
|
||||
"==": version == wanted, "!=": version != wanted}[op]
|
||||
if not allowed:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _wheel_tag_allowed(tag, profile):
|
||||
parts = tag.split("-")
|
||||
if len(parts) != 3:
|
||||
return False
|
||||
interpreters, abis, platforms = [part.split(".") for part in parts]
|
||||
target = profile["python_abi"]
|
||||
target_number = int(target[2:])
|
||||
for interpreter in interpreters:
|
||||
for abi in abis:
|
||||
if abi == "none" and interpreter in ("py3", target):
|
||||
python_ok = True
|
||||
elif abi == target and interpreter == target:
|
||||
python_ok = True
|
||||
elif abi == "abi3" and re.fullmatch(r"cp3\d+", interpreter):
|
||||
python_ok = int(interpreter[2:]) <= target_number
|
||||
else:
|
||||
python_ok = False
|
||||
if not python_ok:
|
||||
continue
|
||||
for platform in platforms:
|
||||
if platform == "any" and abi == "none":
|
||||
return True
|
||||
match = re.fullmatch(r"manylinux_(\d+)_(\d+)_x86_64", platform)
|
||||
legacy = {"manylinux1_x86_64": (2, 5), "manylinux2010_x86_64": (2, 12),
|
||||
"manylinux2014_x86_64": (2, 17)}
|
||||
baseline = tuple(map(int, match.groups())) if match else legacy.get(platform)
|
||||
if baseline and baseline <= GLIBC_BASELINE and abi != "none":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def validate_vendor(directory, profile, expected_versions=None):
|
||||
directory = Path(directory)
|
||||
distributions = distribution_metadata(directory)
|
||||
versions = {name: value[0] for name, value in distributions.items()}
|
||||
if expected_versions is not None and versions != expected_versions:
|
||||
raise RuntimeError("Vendor distribution names/versions differ from reviewed source inputs")
|
||||
owners = {}
|
||||
native_owners = set()
|
||||
packages = []
|
||||
for name, (version, info, metadata) in distributions.items():
|
||||
requires_python = metadata.get("Requires-Python")
|
||||
if requires_python and not _python_allowed(requires_python, profile["python_minor"]):
|
||||
raise RuntimeError("Vendor Requires-Python rejects target: " + name)
|
||||
wheel, record = info / "WHEEL", info / "RECORD"
|
||||
if not wheel.is_file() or not record.is_file():
|
||||
raise RuntimeError("Missing vendor WHEEL/RECORD: " + name)
|
||||
wheel_metadata = Parser().parsestr(wheel.read_text(encoding="utf-8"))
|
||||
tags = wheel_metadata.get_all("Tag", [])
|
||||
if not tags or not any(_wheel_tag_allowed(tag, profile) for tag in tags):
|
||||
raise RuntimeError("Vendor wheel ABI/platform rejects target: " + name + " " + repr(tags))
|
||||
if (wheel_metadata.get("Root-Is-Purelib", "").lower() == "false"
|
||||
and any(_wheel_tag_allowed(tag, profile) and not tag.endswith("-any") for tag in tags)):
|
||||
native_owners.add(name)
|
||||
for row in csv.reader(record.read_text(encoding="utf-8").splitlines()):
|
||||
if row:
|
||||
owners.setdefault(row[0], []).append(name)
|
||||
relative = Path(row[0])
|
||||
if relative.is_absolute():
|
||||
raise RuntimeError("Absolute vendor RECORD path: " + row[0])
|
||||
# Wheel entry-point scripts are not shipped in this module tree.
|
||||
if ".." in relative.parts or "bin" in relative.parts or "__pycache__" in relative.parts:
|
||||
continue
|
||||
file = directory / relative
|
||||
if len(row) < 3 or not file.is_file():
|
||||
raise RuntimeError("Missing or malformed vendor RECORD file: " + row[0])
|
||||
if not row[1] and file != record:
|
||||
raise RuntimeError("Vendor RECORD digest missing: " + row[0])
|
||||
if row[1]:
|
||||
algorithm, separator, encoded = row[1].partition("=")
|
||||
wanted = base64.urlsafe_b64encode(hashlib.sha256(file.read_bytes()).digest()).decode().rstrip("=")
|
||||
if algorithm != "sha256" or not separator or wanted != encoded:
|
||||
raise RuntimeError("Vendor RECORD digest mismatch: " + row[0])
|
||||
if row[2] and int(row[2]) != file.stat().st_size:
|
||||
raise RuntimeError("Vendor RECORD size mismatch: " + row[0])
|
||||
packages.append({"name": name, "version": version, "wheel_tags": tags})
|
||||
native = []
|
||||
for path in sorted(directory.rglob("*.so")):
|
||||
relative = path.relative_to(directory).as_posix()
|
||||
if len(owners.get(relative, [])) != 1:
|
||||
raise RuntimeError("Native vendor file is not owned by one RECORD: " + relative)
|
||||
if owners[relative][0] not in native_owners:
|
||||
raise RuntimeError("Native vendor file lacks matching native WHEEL metadata: " + relative)
|
||||
match = re.search(r"\.cpython-(\d+)-x86_64-linux-gnu\.so$", path.name)
|
||||
if match:
|
||||
if "cp" + match.group(1) != profile["python_abi"]:
|
||||
raise RuntimeError("Native vendor extension ABI rejects target: " + relative)
|
||||
elif not path.name.endswith(".abi3.so"):
|
||||
raise RuntimeError("Native vendor extension has unrecognized ABI suffix: " + relative)
|
||||
data = path.read_bytes()
|
||||
if len(data) < 20 or data[:6] != b"\x7fELF\x02\x01" or int.from_bytes(data[18:20], "little") != 62:
|
||||
raise RuntimeError("Native vendor extension is not ELF amd64: " + relative)
|
||||
needed = [tuple(map(int, values)) for values in re.findall(rb"GLIBC_(\d+)\.(\d+)", data)]
|
||||
if needed and max(needed) > GLIBC_BASELINE:
|
||||
raise RuntimeError("Native vendor extension exceeds glibc 2.34 baseline: " + relative)
|
||||
native.append(relative)
|
||||
required = ("_cffi_backend.cpython-", "pydantic_core/_pydantic_core.cpython-", "rpds/rpds.cpython-")
|
||||
if any(not any(path.startswith(prefix) for path in native) for prefix in required):
|
||||
raise RuntimeError("Required cffi/pydantic_core/rpds native vendor extensions missing")
|
||||
digest = hashlib.sha256()
|
||||
for path in sorted(directory.rglob("*")):
|
||||
if path.is_file() and "__pycache__" not in path.parts and path.suffix != ".pyc" and "bin" not in path.relative_to(directory).parts and path.name != "FB-ABI-PREPARED.json":
|
||||
digest.update(path.relative_to(directory).as_posix().encode() + b"\0")
|
||||
digest.update(hashlib.sha256(path.read_bytes()).digest())
|
||||
return {"vendor_sha256": digest.hexdigest(), "packages": packages, "native_extensions": native}
|
||||
Reference in new issue
Block a user