Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_multicore / _build / build_publication.py: 0%
126 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 05:03 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 05:03 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""Validate staged artifacts and atomically expose complete filesystem outputs."""
17from __future__ import annotations
18import hashlib
19import re
20import base64
21import csv
22import ctypes
23from email.parser import BytesParser
24import io
25import os
26from pathlib import Path
27import shutil
28import tempfile
29import zipfile
30from packaging.requirements import Requirement
31from .artifacts import _shmem_cpp_test_sources, _source_identity
32from .command_runner import Commands
33from ._bootstrap import load_module
35MANIFEST = load_module("native_manifest")
38def publish_wheel(source: Path, destination: Path) -> None:
39 """Expose a complete wheel atomically, preserving any prior successful artifact."""
40 with tempfile.NamedTemporaryFile(
41 dir=destination.parent, prefix=f".{destination.name}.", suffix=".tmp", delete=False
42 ) as file:
43 temporary = Path(file.name)
44 try:
45 shutil.copy2(source, temporary)
46 with temporary.open("rb") as file:
47 os.fsync(file.fileno())
48 os.replace(temporary, destination)
49 finally:
50 temporary.unlink(missing_ok=True)
53def exchange_directories(first: Path, second: Path) -> None:
54 """Exchange Linux directory entries without a missing-payload interval."""
55 library = ctypes.CDLL(None, use_errno=True)
56 rename = getattr(library, "renameat2", None)
57 if rename is None:
58 raise RuntimeError("Atomic native payload publication requires Linux renameat2")
59 rename.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
60 rename.restype = ctypes.c_int
61 if rename(-100, os.fsencode(first), -100, os.fsencode(second), 2) != 0: # AT_FDCWD, RENAME_EXCHANGE
62 error = ctypes.get_errno()
63 raise OSError(error, os.strerror(error), str(second))
66def publish_payload(candidate: Path, selected: Path) -> None:
67 """Keep the visible payload complete even if interrupted immediately after publication."""
68 try:
69 if selected.is_symlink():
70 raise ValueError(f"Refusing to replace an external payload symlink: {selected}")
71 if selected.exists() and not selected.is_dir():
72 raise ValueError(f"Native payload destination is not a directory: {selected}")
73 if selected.exists():
74 exchange_directories(candidate, selected)
75 else:
76 os.replace(candidate, selected)
77 finally:
78 if candidate.exists():
79 shutil.rmtree(candidate)
82def validate_elf(path: Path, arch: str, run: Commands, env: dict) -> None:
83 """Keep target architecture, RPATH, private dependencies and ELF hardening explicit."""
84 data = run(["readelf", "-hW", "-lW", "-dW", str(path)], label=f"elf-{path.name}", env=env, capture=True)
85 expected = "AArch64" if arch == "aarch64" else "Advanced Micro Devices X86-64"
86 if expected not in data or "GNU_RELRO" not in data or "BIND_NOW" not in data:
87 raise ValueError(f"Missing architecture or ELF hardening: {path}")
88 if re.search(r"GNU_STACK[^\n]*RWE", data):
89 raise ValueError(f"Executable stack is forbidden: {path}")
90 if "(RPATH)" in data or "(RUNPATH)" in data or "Shared library: [libcust_opapi.so]" in data:
91 raise ValueError(f"Forbidden adapter runtime dependency/path: {path}")
92 if "Shared library: [libshmem" in data:
93 raise ValueError(f"Generic SHMEM dependency: {path}")
94 linked = run(["ldd", str(path)], label=f"dependencies-{path.name}", env=env, capture=True)
95 missing = [line.strip().split()[0] for line in linked.splitlines() if "not found" in line]
96 if any(not name.startswith(("libtorch", "libc10")) for name in missing):
97 raise ValueError(f"Missing adapter dependencies: {missing}")
100def validate_wheel(wheel: Path, payload: Path, info: dict) -> None:
101 """Verify archive membership, RECORD, identities and native payload before publication."""
102 with zipfile.ZipFile(wheel) as archive:
103 names, record_path, metadata_path = wheel_identity_members(archive)
104 validate_wheel_record(archive, record_path, names)
105 validate_wheel_metadata(archive, metadata_path, info)
106 validate_wheel_payload(archive, names, payload, info)
109def wheel_identity_members(archive):
110 """Require unique safe entries and exactly one distribution identity."""
111 names = archive.namelist()
112 if len(names) != len(set(names)) or any(Path(n).is_absolute() or ".." in Path(n).parts for n in names):
113 raise ValueError("Wheel contains duplicate or unsafe entries")
114 records = [n for n in names if len(Path(n).parts) == 2 and n.endswith(".dist-info/RECORD")]
115 metas = [n for n in names if len(Path(n).parts) == 2 and n.endswith(".dist-info/METADATA")]
116 if len(records) != 1 or len(metas) != 1:
117 raise ValueError("Wheel requires unique RECORD and METADATA")
118 return names, records[0], metas[0]
121def validate_wheel_record(archive, record_path, names):
122 """Require RECORD to cover every archive member with its correct size and digest."""
123 entries = list(csv.reader(io.StringIO(archive.read(record_path).decode())))
124 if len(entries) != len({row[0] for row in entries}) or {row[0] for row in entries} != set(names):
125 raise ValueError("Wheel RECORD membership mismatch")
126 for name, checksum, size in entries:
127 if name == record_path:
128 continue
129 contents = archive.read(name)
130 expected = "sha256=" + base64.urlsafe_b64encode(hashlib.sha256(contents).digest()).rstrip(b"=").decode()
131 if checksum != expected or size != str(len(contents)):
132 raise ValueError(f"Wheel RECORD mismatch: {name}")
135def validate_wheel_metadata(archive, metadata_path, info):
136 """Verify the component identity and unconstrained framework dependency."""
137 metadata = BytesParser().parsebytes(archive.read(metadata_path))
138 if metadata["Name"].replace("_", "-") != "hyper-multicore" or metadata["Version"] != info["artifact_version"]:
139 raise ValueError("Wheel distribution identity mismatch")
140 torch = [Requirement(v) for v in metadata.get_all("Requires-Dist", []) if Requirement(v).name.lower() == "torch"]
141 if len(torch) != 1 or str(torch[0].specifier) or torch[0].url:
142 raise ValueError("Wheel must declare Torch without a version constraint or direct URL")
145def validate_wheel_payload(archive, names, payload, info):
146 """Check complete native membership, resource hashes and assembled identity bytes."""
147 expected = {"hyper_multicore/" + n for n in info["payload_sha256"]}
148 actual = {
149 n
150 for n in names
151 if any(n.startswith("hyper_multicore/" + prefix + "/") for prefix in ("lib", "shmem/lib", "adapters"))
152 }
153 if actual != expected:
154 raise ValueError("Wheel native payload membership mismatch")
155 for name, checksum in info["payload_sha256"].items():
156 if hashlib.sha256(archive.read("hyper_multicore/" + name)).hexdigest() != checksum:
157 raise ValueError(f"Wheel payload mismatch: {name}")
158 for name in ("build_info.json", "adapters/manifest.json"):
159 if archive.read("hyper_multicore/" + name) != (payload / name).read_bytes():
160 raise ValueError(f"Wheel identity mismatch: {name}")
163class SourceGuard:
164 """Reject mixed repository sources before and after formal publication."""
166 def __init__(self, root: Path):
167 self.root = root
168 self.frozen = self.observe()
170 def observe(self):
171 """Record content and file generations, including edit-and-restore changes."""
172 paths = []
173 for name in ("hyper_multicore", "scripts"):
174 paths.extend(p for p in (self.root / name).rglob("*")
175 if p.is_file() and not {"lib", "__pycache__"}.intersection(p.relative_to(self.root).parts))
176 paths.extend(self.root / name for name in
177 ("setup.py", "MANIFEST.in", "build.sh", "requirements.txt"))
178 paths.extend(_shmem_cpp_test_sources(self.root))
179 generations = {}
180 for path in paths:
181 if path.is_file():
182 stat = path.stat()
183 generations[str(path)] = (stat.st_ino, stat.st_size, stat.st_mtime_ns, stat.st_ctime_ns)
184 return _source_identity(self.root), generations
186 def check_source(self):
187 if self.observe() != self.frozen:
188 raise RuntimeError("Source inputs changed during build; refusing mixed-source payload")