diff --git a/docker/Dockerfile b/docker/Dockerfile index f91936fc..3647baea 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -23,6 +23,7 @@ RUN apt-get update -qq \ mold \ numactl \ openjdk-17-jre-headless \ + perl \ python3 \ python3-pip \ python3-venv \ diff --git a/docs/development-environment.md b/docs/development-environment.md index e89c53ab..72130c8c 100644 --- a/docs/development-environment.md +++ b/docs/development-environment.md @@ -11,7 +11,8 @@ remote-bitbang debug acceptance flow. Docker, Nix, and manual Ubuntu installs provide Java 17 for SBT. Docker and Nix use Python 3.10 to match the hash-pinned Ubuntu 22.04 requirement sets. The environment also installs the locked Python build and quality dependencies, clang-format-14, GNU Make, -C/C++ build tools, and runtime libraries required by those binaries. +C/C++ build tools, Perl for Verilator, and runtime libraries required by those +binaries. The environment intentionally does not include PDK repositories, managed RTL, application archives, build output, or compiler caches. These inputs are @@ -33,6 +34,9 @@ Python requirements, and writes: Bootstrap is idempotent. It reinstalls tools or Python packages when the dependency lock, selected tool set, or Python requirement hashes change. +Installed public tool archives are made readable and their directories +traversable by all users. This is required when Docker builds the image as root +and runs regressions with the caller's user ID. ~~~sh python3 scripts/development_environment.py bootstrap diff --git a/flake.nix b/flake.nix index c122e22a..3585a23d 100644 --- a/flake.nix +++ b/flake.nix @@ -65,6 +65,7 @@ ncurses5 numactl jdk17_headless + perl python310Full unzip which diff --git a/scripts/development_environment.py b/scripts/development_environment.py index e9d0e525..dfdca436 100644 --- a/scripts/development_environment.py +++ b/scripts/development_environment.py @@ -60,6 +60,7 @@ SYSTEM_EXECUTABLES = ( "make", "mold", "numactl", + "perl", ) STAMP_NAME = "development-environment.json" ACTIVATE_NAME = "activate.sh" diff --git a/scripts/install_toolchain.py b/scripts/install_toolchain.py index a9090469..d3a3bf53 100644 --- a/scripts/install_toolchain.py +++ b/scripts/install_toolchain.py @@ -6,9 +6,11 @@ import argparse import os import posixpath import shutil +import stat import sys import tarfile import tempfile +from itertools import chain from pathlib import Path, PurePosixPath from typing import Any @@ -38,6 +40,22 @@ def safe_extract(archive: Path, destination: Path) -> None: bundle.extractall(destination) +def make_tree_world_readable(root: Path) -> None: + """Make a public tool archive usable by accounts other than its installer.""" + + for path in chain((root,), root.rglob("*")): + if path.is_symlink(): + continue + mode = stat.S_IMODE(path.stat().st_mode) + if path.is_dir(): + path.chmod(mode | 0o555) + elif path.is_file(): + shared_mode = mode | 0o444 + if mode & 0o111: + shared_mode |= 0o111 + path.chmod(shared_mode) + + def install(name: str, spec: dict[str, Any], cache: Path, update: bool) -> Path: downloads = cache / "downloads" archive = downloads / spec["archive"] @@ -53,6 +71,7 @@ def install(name: str, spec: dict[str, Any], cache: Path, update: bool) -> Path: destination = cache / "toolchains" / f"{name}-{spec['version']}" marker = destination / ".complete" if marker.is_file() and marker.read_text(encoding="utf-8").strip() == spec["sha256"]: + make_tree_world_readable(destination) return destination / spec["path"] destination.parent.mkdir(parents=True, exist_ok=True) @@ -61,6 +80,7 @@ def install(name: str, spec: dict[str, Any], cache: Path, update: bool) -> Path: extracted.mkdir() safe_extract(archive, extracted) atomic_write(extracted / ".complete", spec["sha256"] + "\n") + make_tree_world_readable(extracted) if destination.exists(): shutil.rmtree(destination) os.replace(extracted, destination) diff --git a/tests/test_script_tools.py b/tests/test_script_tools.py index 42e96fcc..18795d93 100644 --- a/tests/test_script_tools.py +++ b/tests/test_script_tools.py @@ -40,7 +40,7 @@ from scripts.development_environment import ( # noqa: E402 write_activation, ) from scripts.generate_mpw import render_active_manifest, validate_extension_bindings # noqa: E402 -from scripts.install_toolchain import safe_extract # noqa: E402 +from scripts.install_toolchain import make_tree_world_readable, safe_extract # noqa: E402 from scripts.package import make_sbom # noqa: E402 from scripts.prepare_mpw import patch_serv # noqa: E402 from scripts import regress # noqa: E402 @@ -1193,10 +1193,12 @@ def test_container_and_nix_environment_files_use_locked_inputs() -> None: assert f"ubuntu@{lock['container_images']['ubuntu_22_04']['digest']}" in dockerfile assert "openjdk-17-jre-headless" in dockerfile + assert " perl \\\n" in dockerfile assert "scripts/development_environment.py" in dockerfile assert "scripts/development_environment.py" in flake assert "buildFHSEnv" in flake assert "jdk17_headless" in flake + assert " perl\n" in flake assert "python310Full" in flake assert "python3Full" not in flake assert "retrosoc-development retrosoc-dev" in flake @@ -2317,6 +2319,34 @@ def test_safe_extract_rejects_parent_traversal(tmp_path: Path) -> None: assert not (tmp_path / "outside").exists() +def test_toolchain_tree_is_accessible_to_runtime_users(tmp_path: Path) -> None: + toolchain = tmp_path / "toolchain" + binary_directory = toolchain / "bundle/bin" + binary_directory.mkdir(parents=True) + executable = binary_directory / "tool" + executable.write_text("#!/bin/sh\n", encoding="utf-8") + data = toolchain / "bundle/data" + data.write_text("public tool data\n", encoding="utf-8") + marker = toolchain / ".complete" + marker.write_text("digest\n", encoding="utf-8") + + toolchain.chmod(0o700) + (toolchain / "bundle").chmod(0o700) + binary_directory.chmod(0o700) + executable.chmod(0o700) + data.chmod(0o600) + marker.chmod(0o600) + + make_tree_world_readable(toolchain) + + assert toolchain.stat().st_mode & 0o777 == 0o755 + assert (toolchain / "bundle").stat().st_mode & 0o777 == 0o755 + assert binary_directory.stat().st_mode & 0o777 == 0o755 + assert executable.stat().st_mode & 0o777 == 0o755 + assert data.stat().st_mode & 0o777 == 0o644 + assert marker.stat().st_mode & 0o777 == 0o644 + + def test_fatfs_update_reextracts_downloaded_archive(tmp_path: Path) -> None: module_path = ROOT / "app/setup.py" spec = importlib.util.spec_from_file_location("retrosoc_app_setup", module_path)