personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / integration / test_root_launcher_install.py
17 kB 537 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4from __future__ import annotations 5 6import base64 7import email 8import hashlib 9import json 10import os 11import shutil 12import subprocess 13import tomllib 14import venv 15import zipfile 16from dataclasses import dataclass 17from pathlib import Path 18from typing import Iterator 19 20import pytest 21from packaging.requirements import Requirement 22from packaging.utils import canonicalize_name 23 24import scripts.check_wheel_contents as wheel_checker 25 26ROOT = Path(__file__).resolve().parents[2] 27pytestmark = [pytest.mark.integration, pytest.mark.timeout(900)] 28 29 30@dataclass(frozen=True) 31class BuiltWheels: 32 version: str 33 dist: Path 34 root: Path 35 core: Path 36 37 38def _run( 39 argv: list[str | Path], 40 *, 41 cwd: Path, 42 env: dict[str, str] | None = None, 43 timeout: int = 300, 44) -> subprocess.CompletedProcess[str]: 45 return subprocess.run( 46 [str(token) for token in argv], 47 cwd=cwd, 48 env=env, 49 capture_output=True, 50 text=True, 51 check=False, 52 timeout=timeout, 53 ) 54 55 56def _root_version() -> str: 57 data = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) 58 return data["project"]["version"] 59 60 61def _lock_versions() -> dict[str, str]: 62 data = tomllib.loads((ROOT / "uv.lock").read_text(encoding="utf-8")) 63 return { 64 canonicalize_name(package["name"]): package["version"] 65 for package in data["package"] 66 } 67 68 69def _record_hash(content: bytes) -> str: 70 digest = hashlib.sha256(content).digest() 71 encoded = base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=") 72 return f"sha256={encoded}" 73 74 75def _write_wheel( 76 wheel_dir: Path, 77 distribution: str, 78 version: str, 79 *, 80 scripts: dict[str, bytes] | None = None, 81 entry_points: str | None = None, 82) -> Path: 83 wheel_dir.mkdir(parents=True, exist_ok=True) 84 normalized = distribution.replace("-", "_") 85 dist_info = f"{normalized}-{version}.dist-info" 86 wheel_path = wheel_dir / f"{normalized}-{version}-py3-none-any.whl" 87 members = { 88 f"{dist_info}/METADATA": ( 89 f"Metadata-Version: 2.1\nName: {distribution}\nVersion: {version}\n" 90 ).encode(), 91 f"{dist_info}/WHEEL": ( 92 "Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n" 93 ).encode(), 94 } 95 if entry_points is not None: 96 members[f"{dist_info}/entry_points.txt"] = entry_points.encode("utf-8") 97 for name, content in (scripts or {}).items(): 98 members[f"{normalized}-{version}.data/scripts/{name}"] = content 99 rows = [ 100 f"{name},{_record_hash(content)},{len(content)}" 101 for name, content in members.items() 102 ] 103 rows.append(f"{dist_info}/RECORD,,") 104 with zipfile.ZipFile(wheel_path, "w") as wheel: 105 for name, content in members.items(): 106 info = zipfile.ZipInfo(name) 107 info.create_system = 3 108 info.external_attr = ( 109 0o755 << 16 if ".data/scripts/" in name else 0o644 << 16 110 ) 111 wheel.writestr(info, content) 112 wheel.writestr(f"{dist_info}/RECORD", "\n".join(rows).encode("utf-8")) 113 return wheel_path 114 115 116def _runtime_requirements(root_wheel: Path) -> tuple[Requirement, ...]: 117 with zipfile.ZipFile(root_wheel) as wheel: 118 metadata_name = next( 119 name for name in wheel.namelist() if name.endswith("METADATA") 120 ) 121 message = email.message_from_bytes(wheel.read(metadata_name)) 122 requirements = [] 123 for line in message.get_all("Requires-Dist") or (): 124 requirement = Requirement(line) 125 name = canonicalize_name(requirement.name) 126 if name == "solstone-core": 127 continue 128 if requirement.marker is not None and not requirement.marker.evaluate( 129 {"extra": ""} 130 ): 131 continue 132 requirements.append(requirement) 133 return tuple(requirements) 134 135 136def _write_dependency_stub_wheels(root_wheel: Path, wheel_dir: Path) -> None: 137 versions = _lock_versions() 138 for requirement in _runtime_requirements(root_wheel): 139 name = canonicalize_name(requirement.name) 140 version = versions[name] 141 assert requirement.specifier.contains(version, prereleases=True) 142 _write_wheel(wheel_dir, name, version) 143 144 145@pytest.fixture(scope="module") 146def built_wheels(tmp_path_factory: pytest.TempPathFactory) -> Iterator[BuiltWheels]: 147 if shutil.which("uv") is None: 148 pytest.skip("uv is not installed") 149 if shutil.which("cargo") is None: 150 pytest.skip("cargo is not installed") 151 152 dist = tmp_path_factory.mktemp("root-launcher-wheels") 153 env = os.environ.copy() 154 env.pop("MATURIN_PEP517_ARGS", None) 155 for argv in ( 156 [ 157 "uv", 158 "build", 159 "--offline", 160 "--package", 161 "solstone-core", 162 "--wheel", 163 "--out-dir", 164 dist, 165 ], 166 ["uv", "build", "--offline", "--wheel", "--out-dir", dist], 167 ): 168 result = _run(argv, cwd=ROOT, env=env, timeout=900) 169 assert result.returncode == 0, result.stderr or result.stdout 170 171 version = _root_version() 172 root_wheels = sorted(dist.glob(f"solstone-{version}-py3-none-any.whl")) 173 core_wheels = sorted(dist.glob(f"solstone_core-{version}-*.whl")) 174 assert len(root_wheels) == 1 175 assert len(core_wheels) == 1 176 _write_dependency_stub_wheels(root_wheels[0], dist) 177 yield BuiltWheels( 178 version=version, dist=dist, root=root_wheels[0], core=core_wheels[0] 179 ) 180 181 182def _isolated_uv_env(tmp_path: Path) -> tuple[dict[str, str], Path, Path, Path]: 183 home = tmp_path / "home" 184 tool_dir = tmp_path / "uv-tools" 185 tool_bin = tmp_path / "uv-bin" 186 cache = tmp_path / "uv-cache" 187 unrelated = tmp_path / "unrelated" 188 for path in (home, tool_dir, tool_bin, cache, unrelated): 189 path.mkdir(parents=True) 190 env = os.environ.copy() 191 env.update( 192 { 193 "HOME": str(home), 194 "XDG_DATA_HOME": str(tmp_path / "xdg-data"), 195 "UV_TOOL_DIR": str(tool_dir), 196 "UV_TOOL_BIN_DIR": str(tool_bin), 197 "UV_CACHE_DIR": str(cache), 198 "UV_PYTHON_DOWNLOADS": "never", 199 } 200 ) 201 env.pop("PYTHONPATH", None) 202 return env, tool_bin, tool_dir, unrelated 203 204 205def _uv_tool_install( 206 *, 207 root_wheel: Path, 208 core_wheel: Path, 209 env: dict[str, str], 210 cwd: Path, 211 force: bool = False, 212) -> subprocess.CompletedProcess[str]: 213 argv: list[str | Path] = [ 214 "uv", 215 "tool", 216 "install", 217 "--offline", 218 "--no-index", 219 "--find-links", 220 root_wheel.parent, 221 "--with", 222 core_wheel, 223 ] 224 if force: 225 argv.append("--force") 226 argv.append(root_wheel) 227 return _run(argv, cwd=cwd, env=env, timeout=900) 228 229 230def _tool_bin_names(tool_bin: Path) -> set[str]: 231 return {path.name for path in tool_bin.iterdir() if not path.name.startswith(".")} 232 233 234def _tool_env_bin(tool_bin: Path, name: str = "sol") -> Path: 235 return (tool_bin / name).resolve().parent 236 237 238def _install_python_spy(env_bin: Path, log: Path) -> None: 239 for name in ("python3", "python"): 240 interpreter = env_bin / name 241 real = env_bin / f"{name}.real" 242 interpreter.rename(real) 243 wrapper = ( 244 "#!/bin/sh\n" 245 f"printf '%s %s\\n' {name!r} \"$*\" >> {str(log)!r}\n" 246 f'exec {str(real)!r} "$@"\n' 247 ) 248 interpreter.write_text(wrapper, encoding="utf-8") 249 interpreter.chmod(0o755) 250 251 252def _script_owners(python: Path, names: tuple[str, ...]) -> dict[str, list[str]]: 253 script = """ 254import csv 255import importlib.metadata as metadata 256import json 257import os 258from pathlib import Path, PurePosixPath 259 260bin_dir = Path(os.environ["BIN_DIR"]) 261targets = set(json.loads(os.environ["SCRIPT_NAMES"])) 262scripts = {str((bin_dir / name).resolve()): name for name in targets} 263owners = {name: [] for name in targets} 264for dist in metadata.distributions(): 265 dist_name = dist.metadata.get("Name", "") 266 for file in dist.files or []: 267 basename = PurePosixPath(str(file).replace("\\\\", "/")).name 268 if basename.endswith(".exe"): 269 basename = basename[:-4] 270 if basename not in targets: 271 continue 272 try: 273 located = str(dist.locate_file(file).resolve()) 274 except OSError: 275 continue 276 if located in scripts: 277 owners[scripts[located]].append(dist_name) 278print(json.dumps({key: sorted(value) for key, value in owners.items()}, sort_keys=True)) 279""" 280 result = _run( 281 [python, "-c", script], 282 cwd=python.parent, 283 env={ 284 **os.environ, 285 "BIN_DIR": str(python.parent), 286 "SCRIPT_NAMES": json.dumps(list(names)), 287 }, 288 ) 289 assert result.returncode == 0, result.stderr or result.stdout 290 return json.loads(result.stdout) 291 292 293def _assert_new_owners(python: Path) -> None: 294 expected = { 295 **{name: ["solstone"] for name in wheel_checker.ROOT_LAUNCHER_NAMES}, 296 **{name: ["solstone-core"] for name in wheel_checker.CORE_SCRIPT_NAMES}, 297 } 298 assert ( 299 _script_owners( 300 python, wheel_checker.ROOT_LAUNCHER_NAMES + wheel_checker.CORE_SCRIPT_NAMES 301 ) 302 == expected 303 ) 304 305 306def _create_venv(path: Path) -> Path: 307 venv.EnvBuilder(with_pip=True, symlinks=False).create(path) 308 return path / "bin" / "python" 309 310 311def _pip_install( 312 python: Path, 313 wheels: tuple[Path, ...], 314 *, 315 force: bool = False, 316) -> subprocess.CompletedProcess[str]: 317 argv: list[str | Path] = [ 318 python, 319 "-m", 320 "pip", 321 "install", 322 "--no-index", 323 "--no-deps", 324 ] 325 if force: 326 argv.append("--force-reinstall") 327 argv.extend(wheels) 328 return _run(argv, cwd=python.parent) 329 330 331def _old_layout_wheels(tmp_path: Path) -> tuple[Path, Path]: 332 old_version = "1.0.14" 333 wheel_dir = tmp_path / "old-wheels" 334 core_scripts = { 335 "sol": b"#!/bin/sh\necho old sol\n", 336 "solstone": b"#!/bin/sh\necho old solstone\n", 337 "solstone-core": b"#!/bin/sh\necho old solstone-core\n", 338 } 339 old_core = _write_wheel( 340 wheel_dir, 341 "solstone-core", 342 old_version, 343 scripts=core_scripts, 344 ) 345 old_root = _write_wheel( 346 wheel_dir, 347 "solstone", 348 old_version, 349 entry_points=( 350 "[console_scripts]\n" 351 "solstone-python-compat = solstone.think.sol_compat_cli:main\n" 352 ), 353 ) 354 return old_root, old_core 355 356 357def test_cold_uv_tool_install_exports_root_launchers_only( 358 tmp_path: Path, 359 built_wheels: BuiltWheels, 360) -> None: 361 env, tool_bin, _tool_dir, unrelated = _isolated_uv_env(tmp_path) 362 assert _tool_bin_names(tool_bin) == set() 363 364 install = _uv_tool_install( 365 root_wheel=built_wheels.root, 366 core_wheel=built_wheels.core, 367 env=env, 368 cwd=unrelated, 369 ) 370 371 assert install.returncode == 0, install.stderr or install.stdout 372 assert _tool_bin_names(tool_bin) == set(wheel_checker.ROOT_LAUNCHER_NAMES) 373 assert not (tool_bin / "solstone-core").exists() 374 assert not (tool_bin / "solstone-python-compat").exists() 375 376 env_bin = _tool_env_bin(tool_bin) 377 assert (env_bin / "solstone-core").exists() 378 assert (tool_bin / "sol").resolve().parent == env_bin 379 assert (tool_bin / "solstone").resolve().parent == env_bin 380 381 log = tmp_path / "python-spy.log" 382 _install_python_spy(env_bin, log) 383 native = _run([tool_bin / "sol", "--version"], cwd=unrelated, env=env) 384 assert native.returncode == 0, native.stderr 385 assert native.stdout == f"sol (solstone) {built_wheels.version}\n" 386 native_solstone = _run( 387 [tool_bin / "solstone", "--version"], 388 cwd=unrelated, 389 env=env, 390 ) 391 assert native_solstone.returncode == 0, native_solstone.stderr 392 assert native_solstone.stdout == f"sol (solstone) {built_wheels.version}\n" 393 assert not log.exists() 394 395 journal = tmp_path / "journal" 396 journal.mkdir() 397 compat_env = {**env, "SOLSTONE_JOURNAL": str(journal)} 398 compat = _run( 399 [tool_bin / "sol", "call", "journal", "--help"], 400 cwd=unrelated, 401 env=compat_env, 402 ) 403 assert compat.returncode != 78, compat.stderr 404 assert log.exists(), compat.stderr 405 spy = log.read_text(encoding="utf-8") 406 assert "-P -m solstone.think.sol_compat_cli" in spy 407 assert "__solstone_native_argv0=sol call journal --help" in spy 408 409 410def test_isolated_pip_venv_install_exposes_new_layout( 411 tmp_path: Path, 412 built_wheels: BuiltWheels, 413) -> None: 414 python = _create_venv(tmp_path / "venv") 415 install = _pip_install(python, (built_wheels.core, built_wheels.root)) 416 assert install.returncode == 0, install.stderr or install.stdout 417 418 bin_dir = python.parent 419 for name in wheel_checker.ROOT_LAUNCHER_NAMES + wheel_checker.CORE_SCRIPT_NAMES: 420 result = _run([bin_dir / name, "--version"], cwd=tmp_path) 421 assert result.returncode == 0, result.stderr 422 _assert_new_owners(python) 423 assert not (bin_dir / "solstone-python-compat").exists() 424 425 426@pytest.mark.parametrize("installer", ("pip", "uv")) 427def test_upgrade_from_old_script_ownership_converges( 428 tmp_path: Path, 429 built_wheels: BuiltWheels, 430 installer: str, 431) -> None: 432 old_root, old_core = _old_layout_wheels(tmp_path) 433 if installer == "pip": 434 python = _create_venv(tmp_path / "upgrade-venv") 435 old_install = _pip_install(python, (old_core, old_root)) 436 assert old_install.returncode == 0, old_install.stderr or old_install.stdout 437 bin_dir = python.parent 438 assert (bin_dir / "solstone-python-compat").exists() 439 upgrade = _pip_install( 440 python, (built_wheels.core, built_wheels.root), force=True 441 ) 442 assert upgrade.returncode == 0, upgrade.stderr or upgrade.stdout 443 reinstall = _pip_install( 444 python, (built_wheels.core, built_wheels.root), force=True 445 ) 446 assert reinstall.returncode == 0, reinstall.stderr or reinstall.stdout 447 assert not (bin_dir / "solstone-python-compat").exists() 448 _assert_new_owners(python) 449 return 450 451 env, tool_bin, _tool_dir, unrelated = _isolated_uv_env(tmp_path) 452 old_install = _uv_tool_install( 453 root_wheel=old_root, 454 core_wheel=old_core, 455 env=env, 456 cwd=unrelated, 457 ) 458 assert old_install.returncode == 0, old_install.stderr or old_install.stdout 459 assert _tool_bin_names(tool_bin) == {"solstone-python-compat"} 460 upgrade = _uv_tool_install( 461 root_wheel=built_wheels.root, 462 core_wheel=built_wheels.core, 463 env=env, 464 cwd=unrelated, 465 force=True, 466 ) 467 assert upgrade.returncode == 0, upgrade.stderr or upgrade.stdout 468 reinstall = _uv_tool_install( 469 root_wheel=built_wheels.root, 470 core_wheel=built_wheels.core, 471 env=env, 472 cwd=unrelated, 473 force=True, 474 ) 475 assert reinstall.returncode == 0, reinstall.stderr or reinstall.stdout 476 assert _tool_bin_names(tool_bin) == set(wheel_checker.ROOT_LAUNCHER_NAMES) 477 env_bin = _tool_env_bin(tool_bin) 478 assert not (env_bin / "solstone-python-compat").exists() 479 _assert_new_owners(env_bin / "python") 480 481 482def test_partial_uninstall_ownership_boundaries( 483 tmp_path: Path, 484 built_wheels: BuiltWheels, 485) -> None: 486 root_only_python = _create_venv(tmp_path / "root-only") 487 assert ( 488 _pip_install( 489 root_only_python, (built_wheels.core, built_wheels.root) 490 ).returncode 491 == 0 492 ) 493 root_only_bin = root_only_python.parent 494 result = _run( 495 [root_only_python, "-m", "pip", "uninstall", "-y", "solstone"], cwd=tmp_path 496 ) 497 assert result.returncode == 0, result.stderr or result.stdout 498 assert not any( 499 (root_only_bin / name).exists() for name in wheel_checker.ROOT_LAUNCHER_NAMES 500 ) 501 assert (root_only_bin / "solstone-core").exists() 502 503 core_only_python = _create_venv(tmp_path / "core-only") 504 assert ( 505 _pip_install( 506 core_only_python, (built_wheels.core, built_wheels.root) 507 ).returncode 508 == 0 509 ) 510 core_only_bin = core_only_python.parent 511 result = _run( 512 [core_only_python, "-m", "pip", "uninstall", "-y", "solstone-core"], 513 cwd=tmp_path, 514 ) 515 assert result.returncode == 0, result.stderr or result.stdout 516 assert not (core_only_bin / "solstone-core").exists() 517 for name in wheel_checker.ROOT_LAUNCHER_NAMES: 518 launcher = _run([core_only_bin / name, "--version"], cwd=tmp_path) 519 assert launcher.returncode == 78 520 assert launcher.stdout == "" 521 assert "native solstone-core sibling is missing" in launcher.stderr 522 523 full_python = _create_venv(tmp_path / "full") 524 assert ( 525 _pip_install(full_python, (built_wheels.core, built_wheels.root)).returncode 526 == 0 527 ) 528 full_bin = full_python.parent 529 result = _run( 530 [full_python, "-m", "pip", "uninstall", "-y", "solstone", "solstone-core"], 531 cwd=tmp_path, 532 ) 533 assert result.returncode == 0, result.stderr or result.stdout 534 assert not any( 535 (full_bin / name).exists() 536 for name in wheel_checker.ROOT_LAUNCHER_NAMES + wheel_checker.CORE_SCRIPT_NAMES 537 )