Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_multicore / _build / prepare_dependencies.py: 0%

130 statements  

« 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"""Explicitly acquire or verify pinned native build dependencies.""" 

16 

17from __future__ import annotations 

18 

19import argparse 

20import hashlib 

21import json 

22from pathlib import Path 

23import subprocess 

24import sys 

25from typing import Any 

26 

27 

28_REPO_ROOT = Path(__file__).resolve().parents[2] 

29_LOCK_PATH = Path(__file__).with_name("dependencies.lock.json") 

30_DEFAULT_CACHE = _REPO_ROOT / "build" / "native" / "deps" 

31 

32 

33def _parse_args(dependencies: dict[str, Any]) -> argparse.Namespace: 

34 """Parse the explicit dependency acquisition contract.""" 

35 parser = argparse.ArgumentParser(description=__doc__) 

36 parser.add_argument( 

37 "--dependency", 

38 action="append", 

39 required=True, 

40 choices=tuple(dependencies), 

41 help="Dependency to prepare. Repeat the option to prepare several dependencies.", 

42 ) 

43 parser.add_argument("--cache-dir", default=str(_DEFAULT_CACHE)) 

44 parser.add_argument("--source-dir") 

45 parser.add_argument("--verify-only", action="store_true") 

46 return parser.parse_args() 

47 

48 

49def main() -> int: 

50 """Acquire requested dependencies, then verify their locked identities.""" 

51 dependencies = load_dependency_locks() 

52 args = _parse_args(dependencies) 

53 cache_dir = Path(args.cache_dir) 

54 if not cache_dir.is_absolute(): 

55 cache_dir = _REPO_ROOT / cache_dir 

56 if args.source_dir and len(args.dependency) != 1: 

57 raise ValueError("--source-dir can only be used with one --dependency value.") 

58 prepared: dict[str, Any] = {} 

59 for dependency in dict.fromkeys(args.dependency): 

60 source_dir = Path(args.source_dir) if args.source_dir else cache_dir / dependency / "src" 

61 if not source_dir.is_absolute(): 

62 source_dir = _REPO_ROOT / source_dir 

63 dependency_lock = dependencies[dependency] 

64 revisions = dependency_revisions(dependency_lock) 

65 external_source = bool(args.source_dir and source_dir.exists()) 

66 if not source_dir.exists(): 

67 if args.verify_only: 

68 raise ValueError(f"Dependency source directory does not exist: {source_dir}") 

69 _clone_dependency(next(iter(revisions.values())), source_dir) 

70 verified = {} 

71 for revision_name, revision in revisions.items(): 

72 _ensure_commit(revision, source_dir, allow_fetch=not (args.verify_only or external_source)) 

73 verified[revision_name] = verify_git_dependency( 

74 revision, source_dir, dependency_name=f"{dependency}.{revision_name}" 

75 if "revisions" in dependency_lock else dependency, 

76 ) 

77 prepared[dependency] = ( 

78 {"repository": dependency_lock["repository"], "source_dir": str(source_dir.resolve()), 

79 "revisions": verified} if "revisions" in dependency_lock else verified["default"] 

80 ) 

81 print(json.dumps(prepared, sort_keys=True)) 

82 return 0 

83 

84 

85def verify_git_dependency( 

86 dependency_lock: dict[str, Any], 

87 source_dir: str | Path, 

88 dependency_name: str | None = None, 

89) -> dict[str, Any]: 

90 """Verify a locked commit independently of the shared worktree's current HEAD. 

91 

92 Args: 

93 dependency_lock: Dependency entry from ``dependencies.lock.json``. 

94 source_dir: Existing Git worktree to verify. 

95 dependency_name: Stable lock name; inferred from the repository when omitted. 

96 

97 Returns: 

98 Verified source identity suitable for a run record. 

99 

100 Raises: 

101 ValueError: If the source is missing, modified, or does not match the lock. 

102 """ 

103 source_dir = Path(source_dir).resolve() 

104 if not (source_dir / ".git").exists(): 

105 raise ValueError(f"Dependency source is not a Git worktree: {source_dir}") 

106 commit = dependency_lock["commit"] 

107 actual_commit = _git_output(source_dir, "rev-parse", f"{commit}^{{commit}}") 

108 actual_tree = _git_output(source_dir, "rev-parse", f"{commit}^{{tree}}") 

109 tracked_changes = _git_output(source_dir, "status", "--porcelain", "--untracked-files=no") 

110 if actual_commit != dependency_lock["commit"]: 

111 raise ValueError(f"Dependency commit mismatch: expected={dependency_lock['commit']}, actual={actual_commit}") 

112 if actual_tree != dependency_lock["tree"]: 

113 raise ValueError(f"Dependency tree mismatch: expected={dependency_lock['tree']}, actual={actual_tree}") 

114 if tracked_changes: 

115 raise ValueError("Dependency source contains tracked modifications; upstream patching is not allowed.") 

116 repository_name = Path(dependency_lock["repository"]).stem 

117 archive_hash = _git_archive_sha256( 

118 source_dir, 

119 dependency_lock["commit"], 

120 f"{repository_name}-{dependency_lock['version']}/", 

121 ) 

122 expected_archive_hashes = { 

123 dependency_lock["git_archive_tar_sha256"], 

124 *dependency_lock.get("git_archive_tar_sha256_compat", []), 

125 } 

126 if archive_hash not in expected_archive_hashes: 

127 raise ValueError( 

128 "Dependency archive hash mismatch: " 

129 f"expected one of {sorted(expected_archive_hashes)}, actual={archive_hash}" 

130 ) 

131 return { 

132 "dependency": dependency_name or repository_name.replace("-", "_"), 

133 "version": dependency_lock["version"], 

134 "repository": dependency_lock["repository"], 

135 "source_dir": str(source_dir), 

136 "commit": actual_commit, 

137 "tree": actual_tree, 

138 "git_archive_tar_sha256": archive_hash, 

139 "patched": False, 

140 } 

141 

142 

143def load_dependency_locks(lock_path: Path | None = None) -> dict[str, Any]: 

144 """Read schema 2 and reject repeated repository declarations before acquisition.""" 

145 lock = json.loads((lock_path or _LOCK_PATH).read_text(encoding="utf-8")) 

146 if lock.get("schema_version") != 2: 

147 raise ValueError("Native dependency lock must use schema_version 2") 

148 dependencies = lock["components"]["multicore"] 

149 repositories = {} 

150 for name, dependency in dependencies.items(): 

151 repository = dependency["repository"] 

152 if repository in repositories: 

153 raise ValueError( 

154 f"Repository {repository} is declared by both {repositories[repository]} and {name}; " 

155 "declare additional versions under revisions instead." 

156 ) 

157 repositories[repository] = name 

158 dependency_revisions(dependency) 

159 return dependencies 

160 

161 

162def dependency_revisions(dependency_lock: dict[str, Any]) -> dict[str, dict[str, Any]]: 

163 """Resolve single-version or named revisions into complete verification entries.""" 

164 if "revisions" not in dependency_lock: 

165 return {"default": dependency_lock} 

166 revisions = dependency_lock["revisions"] 

167 if not isinstance(revisions, dict) or not revisions: 

168 raise ValueError("Dependency revisions must be a nonempty mapping") 

169 if any(not isinstance(revision, dict) or "repository" in revision for revision in revisions.values()): 

170 raise ValueError("Each revision must be an object; repository belongs to the parent entry") 

171 return {name: {**revision, "repository": dependency_lock["repository"]} 

172 for name, revision in revisions.items()} 

173 

174 

175def _clone_dependency(dependency_lock: dict[str, Any], source_dir: Path) -> None: 

176 """Initialize one ordinary repository, then check out its first locked revision.""" 

177 source_dir.mkdir(parents=True) 

178 _run_git(source_dir, "init") 

179 _run_git(source_dir, "remote", "add", "origin", dependency_lock["repository"]) 

180 _ensure_commit(dependency_lock, source_dir, allow_fetch=True) 

181 _run_git(source_dir, "checkout", "--detach", dependency_lock["commit"]) 

182 

183 

184def _ensure_commit(dependency_lock: dict[str, Any], source_dir: Path, allow_fetch: bool) -> None: 

185 """Fetch only missing commits and retain them without switching the shared HEAD.""" 

186 if not (source_dir / ".git").exists(): 

187 raise ValueError(f"Dependency source is not a Git worktree: {source_dir}") 

188 commit = dependency_lock["commit"] 

189 result = subprocess.run(["git", "cat-file", "-e", f"{commit}^{{commit}}"], cwd=source_dir, 

190 check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 

191 if result.returncode != 0: 

192 if not allow_fetch: 

193 raise ValueError(f"Locked commit {commit} is missing from {source_dir}") 

194 _run_git(source_dir, "fetch", "--depth", "1", "origin", 

195 dependency_lock.get("fetch_ref") or dependency_lock["version"]) 

196 if allow_fetch: 

197 _run_git(source_dir, "update-ref", f"refs/hyper_multicore/{commit}", commit) 

198 

199 

200def _run_git(source_dir: Path, *args: str) -> None: 

201 """Execute acquisition commands with diagnostics on stderr, leaving stdout as JSON.""" 

202 result = subprocess.run(["git", *args], cwd=source_dir, check=False, stdout=sys.stderr) 

203 if result.returncode != 0: 

204 raise RuntimeError(f"Git dependency command failed ({' '.join(args)}) in {source_dir}") 

205 

206 

207def _git_output(source_dir: Path, *args: str) -> str: 

208 """Run one read-only Git inspection and return stripped stdout.""" 

209 result = subprocess.run( 

210 ["git", *args], 

211 cwd=source_dir, 

212 check=False, 

213 stdout=subprocess.PIPE, 

214 stderr=subprocess.PIPE, 

215 text=True, 

216 ) 

217 if result.returncode != 0: 

218 raise ValueError(f"Git inspection failed ({' '.join(args)}): {result.stderr.strip()}") 

219 return result.stdout.strip() 

220 

221 

222def _git_archive_sha256(source_dir: Path, commit: str, archive_prefix: str) -> str: 

223 """Hash a deterministic uncompressed Git archive without creating a tarball.""" 

224 with subprocess.Popen( 

225 ["git", "archive", "--format=tar", f"--prefix={archive_prefix}", commit], 

226 cwd=source_dir, 

227 stdout=subprocess.PIPE, 

228 stderr=subprocess.PIPE, 

229 ) as process: 

230 if process.stdout is None: 

231 raise RuntimeError("Cannot read git archive output.") 

232 digest = hashlib.sha256() 

233 for chunk in iter(lambda: process.stdout.read(1024 * 1024), b""): 

234 digest.update(chunk) 

235 _, stderr = process.communicate() 

236 if process.returncode != 0: 

237 raise ValueError(f"Git archive failed: {stderr.decode(errors='replace').strip()}") 

238 return digest.hexdigest() 

239 

240 

241if __name__ == "__main__": 

242 try: 

243 raise SystemExit(main()) 

244 except (RuntimeError, ValueError) as error: 

245 sys.stderr.write(f"[HP-NATIVE-DEPENDENCY-ERROR] {error}\n") 

246 raise SystemExit(1) from error