From fabcd3dda62da3ed648f134674b5aa0bab04d878 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 28 May 2026 21:19:38 +0000 Subject: [PATCH 1/9] [ci] E: Update zutils to v0.10.3 --- .github/workflows/nanvix-ci.yml | 2 +- z.ps1 | 35 +++++++++++++++++++++++---------- z.sh | 34 +++++++++++++++++++++++++++----- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/.github/workflows/nanvix-ci.yml b/.github/workflows/nanvix-ci.yml index 40a1740..22e8ad5 100644 --- a/.github/workflows/nanvix-ci.yml +++ b/.github/workflows/nanvix-ci.yml @@ -34,7 +34,7 @@ jobs: if: github.event_name != 'schedule' uses: nanvix/workflows/.github/workflows/nanvix-ci.yml@v2.0.2 with: - zutil-version: "v0.10.2" + zutil-version: "v0.10.3" docker-image: "ghcr.io/nanvix/toolchain-gcc@sha256:ea33e4cae4041dcf55f0ec49f58de90ae1b5e9fb7c3c507971b40975b14aecd3" # yamllint disable-line rule:line-length platforms: '["microvm"]' memory-sizes: '["128mb"]' diff --git a/z.ps1 b/z.ps1 index d119024..ff776ab 100644 --- a/z.ps1 +++ b/z.ps1 @@ -11,17 +11,33 @@ param( $ErrorActionPreference = 'Stop' -$zutilVersion = if ($env:NANVIX_ZUTIL_VERSION) { - $env:NANVIX_ZUTIL_VERSION -} -else { - "0.10.2" -} -$zutilVersion = $zutilVersion -replace "^v", "" - # z.ps1 lives at the repository root, so use its directory directly # instead of relying on git to discover the top-level checkout directory. $repoRoot = $PSScriptRoot +$versionFile = Join-Path $repoRoot ".zutils-version" + +# Resolve the pinned nanvix-zutil version. `.zutils-version` at the repo +# root is the sole source of truth: no env-var override, no GitHub fetch. +# Downstream consumers commit this file; CI and developers see the same +# pin without ambient configuration. +function Resolve-ZutilVersion { + if (-not (Test-Path -LiteralPath $versionFile)) { + throw "Error: $versionFile not found.`n Create it with a pinned nanvix-zutil version, e.g. 'v0.10.2'." + } + $content = Get-Content -LiteralPath $versionFile -Raw + $raw = if ($null -eq $content) { "" } else { $content.Trim() } + if ([string]::IsNullOrWhiteSpace($raw)) { + throw "Error: $versionFile is empty." + } + if ($raw -notmatch '^v?\d+\.\d+\.\d+([.-][A-Za-z0-9.-]+)?$') { + throw "Error: invalid nanvix-zutil version '$raw' in $versionFile (expected vX.Y.Z)." + } + return $raw +} + +$rawZutilVersion = Resolve-ZutilVersion +$zutilVersion = $rawZutilVersion -replace "^v", "" + $venvDir = Join-Path $repoRoot ".nanvix\venv" $venvPython = Join-Path $venvDir "Scripts\python.exe" $venvZutil = Join-Path $venvDir "Scripts\nanvix-zutil.exe" @@ -135,8 +151,7 @@ function NewZutilVenv { function Bootstrap { param([string]$Reason = "not found") - # Pin nanvix-zutil version for reproducible bootstrapping. - # Override with NANVIX_ZUTIL_VERSION env var if needed. + # nanvix-zutil version comes from .zutils-version (resolved above). Write-Information "nanvix-zutil ${Reason} -- bootstrapping nanvix-zutil==${zutilVersion}..." -InformationAction Continue $wheelUrl = "https://github.com/nanvix/zutils/releases/download/v${zutilVersion}/nanvix_zutil-${zutilVersion}-py3-none-any.whl" diff --git a/z.sh b/z.sh index 42c2f20..87cb138 100755 --- a/z.sh +++ b/z.sh @@ -7,11 +7,36 @@ set -euo pipefail -PINNED_VERSION="0.10.2" -RAW_ZUTIL_VERSION="${NANVIX_ZUTIL_VERSION:-$PINNED_VERSION}" -ZUTIL_VERSION="${RAW_ZUTIL_VERSION#v}" REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P)" VENV="$REPO_ROOT/.nanvix/venv" +VERSION_FILE="$REPO_ROOT/.zutils-version" + +# Resolve the pinned nanvix-zutil version. `.zutils-version` at the repo +# root is the sole source of truth: no env-var override, no GitHub fetch. +# Downstream consumers commit this file; CI and developers see the same +# pin without ambient configuration. +function _resolve_zutil_version() { + # RAW_ZUTIL_VERSION and ZUTIL_VERSION are intentionally global; + # they are consumed by bootstrap() below. + if [ ! -f "$VERSION_FILE" ]; then + echo "Error: $VERSION_FILE not found." >&2 + echo " Create it with a pinned nanvix-zutil version, e.g. 'v0.10.2'." >&2 + exit 1 + fi + local raw + raw="$(tr -d '[:space:]' <"$VERSION_FILE")" + if [ -z "$raw" ]; then + echo "Error: $VERSION_FILE is empty." >&2 + exit 1 + fi + if [[ ! "$raw" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+([.-][[:alnum:].-]+)?$ ]]; then + echo "Error: invalid nanvix-zutil version '$raw' in $VERSION_FILE (expected vX.Y.Z)." >&2 + exit 1 + fi + RAW_ZUTIL_VERSION="$raw" + ZUTIL_VERSION="${RAW_ZUTIL_VERSION#v}" +} +_resolve_zutil_version # Resolve venv layout (bin/ vs Scripts/) based on what exists on disk. # Can be called before venv creation to initialize default paths; call it @@ -126,8 +151,7 @@ function bootstrap_local() { } function bootstrap() { - # Pin nanvix-zutil version for reproducible bootstrapping. - # Override with NANVIX_ZUTIL_VERSION env var if needed. + # nanvix-zutil version comes from .zutils-version (resolved above). local reason="${1:-not found}" echo "nanvix-zutil ${reason} -- bootstrapping nanvix-zutil==${ZUTIL_VERSION}..." >&2 From f52e82c313c8f39846f774f532db51f84768ff05 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 May 2026 12:15:20 +0000 Subject: [PATCH 2/9] [ci] E: Update zutils to v0.10.3 --- .nanvix/nanvix.toml | 2 +- .zutils-version | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .zutils-version diff --git a/.nanvix/nanvix.toml b/.nanvix/nanvix.toml index 9a9624a..c76b014 100644 --- a/.nanvix/nanvix.toml +++ b/.nanvix/nanvix.toml @@ -1,7 +1,7 @@ [package] name = "posix-tests" version = "0.1.0" -nanvix-version = "0.15.26" +nanvix-version = "0.15.43" [builds] [builds.matrix] diff --git a/.zutils-version b/.zutils-version new file mode 100644 index 0000000..b4e3d2a --- /dev/null +++ b/.zutils-version @@ -0,0 +1 @@ +v0.10.3 From 57c6ba8b989b4912352c2034f6966db5abb616d2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 May 2026 14:59:10 +0000 Subject: [PATCH 3/9] [ci] E: Update nanvix workflow refs to v2.1.0 --- .github/workflows/nanvix-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nanvix-ci.yml b/.github/workflows/nanvix-ci.yml index 22e8ad5..7cb4f9d 100644 --- a/.github/workflows/nanvix-ci.yml +++ b/.github/workflows/nanvix-ci.yml @@ -32,7 +32,7 @@ concurrency: jobs: ci: if: github.event_name != 'schedule' - uses: nanvix/workflows/.github/workflows/nanvix-ci.yml@v2.0.2 + uses: nanvix/workflows/.github/workflows/nanvix-ci.yml@v2.1.0 with: zutil-version: "v0.10.3" docker-image: "ghcr.io/nanvix/toolchain-gcc@sha256:ea33e4cae4041dcf55f0ec49f58de90ae1b5e9fb7c3c507971b40975b14aecd3" # yamllint disable-line rule:line-length From 6ba42f99f6dda0c6480d9cabba625f9e1c702ed0 Mon Sep 17 00:00:00 2001 From: ada Date: Fri, 29 May 2026 11:42:42 -0500 Subject: [PATCH 4/9] ci: drop zutil-version input (removed in workflows v2.1.0) The reusable workflow at nanvix/workflows v2.1.0 no longer accepts the zutil-version input; the zutil version is now resolved from the .zutils-version file at the repo root. --- .github/workflows/nanvix-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/nanvix-ci.yml b/.github/workflows/nanvix-ci.yml index 7cb4f9d..93386ba 100644 --- a/.github/workflows/nanvix-ci.yml +++ b/.github/workflows/nanvix-ci.yml @@ -34,7 +34,6 @@ jobs: if: github.event_name != 'schedule' uses: nanvix/workflows/.github/workflows/nanvix-ci.yml@v2.1.0 with: - zutil-version: "v0.10.3" docker-image: "ghcr.io/nanvix/toolchain-gcc@sha256:ea33e4cae4041dcf55f0ec49f58de90ae1b5e9fb7c3c507971b40975b14aecd3" # yamllint disable-line rule:line-length platforms: '["microvm"]' memory-sizes: '["128mb"]' From a3122bb0504fbd192de9e1eff473c87236537df0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 May 2026 17:53:37 +0000 Subject: [PATCH 5/9] [ci] E: Update zutils to v0.10.4 --- .nanvix/nanvix.toml | 2 +- .zutils-version | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.nanvix/nanvix.toml b/.nanvix/nanvix.toml index c76b014..61a78c7 100644 --- a/.nanvix/nanvix.toml +++ b/.nanvix/nanvix.toml @@ -1,7 +1,7 @@ [package] name = "posix-tests" version = "0.1.0" -nanvix-version = "0.15.43" +nanvix-version = "0.15.44" [builds] [builds.matrix] diff --git a/.zutils-version b/.zutils-version index b4e3d2a..bc3d337 100644 --- a/.zutils-version +++ b/.zutils-version @@ -1 +1 @@ -v0.10.3 +v0.10.4 From f31d2a5ad78ff736cff3abae4fb5ce19a97652f6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Jun 2026 12:08:33 +0000 Subject: [PATCH 6/9] [ci] E: Update zutils to v0.10.4 --- .nanvix/nanvix.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nanvix/nanvix.toml b/.nanvix/nanvix.toml index 61a78c7..0530d21 100644 --- a/.nanvix/nanvix.toml +++ b/.nanvix/nanvix.toml @@ -1,7 +1,7 @@ [package] name = "posix-tests" version = "0.1.0" -nanvix-version = "0.15.44" +nanvix-version = "0.16.12" [builds] [builds.matrix] From 7c703e4a9014dd78fee3b030f1cd9df7e846f24e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 8 Jun 2026 13:06:41 +0000 Subject: [PATCH 7/9] [ci] E: Update zutils to v0.11.0 --- .nanvix/.gitignore | 10 ++-- .nanvix/nanvix.toml | 2 +- .nanvix/z.py | 111 ++++++++++++++++++++++---------------------- .zutils-version | 2 +- z.ps1 | 22 +-------- z.sh | 29 ++---------- 6 files changed, 67 insertions(+), 109 deletions(-) diff --git a/.nanvix/.gitignore b/.nanvix/.gitignore index acf7f49..5e18c38 100644 --- a/.nanvix/.gitignore +++ b/.nanvix/.gitignore @@ -1,8 +1,8 @@ -__pycache__/ -venv/ -cache/ -sysroot/ -buildroot/ +__pycache__ +venv +cache +sysroot +buildroot .yamllint.yml black.toml env.json diff --git a/.nanvix/nanvix.toml b/.nanvix/nanvix.toml index 0530d21..cb02411 100644 --- a/.nanvix/nanvix.toml +++ b/.nanvix/nanvix.toml @@ -1,7 +1,7 @@ [package] name = "posix-tests" version = "0.1.0" -nanvix-version = "0.16.12" +nanvix-version = "0.16.17" [builds] [builds.matrix] diff --git a/.nanvix/z.py b/.nanvix/z.py index 5a2a4bb..5c47e76 100644 --- a/.nanvix/z.py +++ b/.nanvix/z.py @@ -24,7 +24,6 @@ import shutil import subprocess import sys -import tarfile import tempfile import urllib.request import zipfile @@ -40,6 +39,11 @@ run, ) from nanvix_zutil.helpers import InitRdArgs +from nanvix_zutil.paths import ( + bin_out, + nanvix_root, + repo_root, +) # --------------------------------------------------------------------------- # Platform detection @@ -165,11 +169,11 @@ class PosixTestsBuild(ZScript): # ---- CLI entry point ------------------------------------------------- @classmethod - def main(cls, *, repo_root: Path | None = None) -> None: + def main(cls) -> None: """Pre-parse ``--with-nanvix`` and delegate to ZScript.main().""" if _EARLY_LOCAL_NANVIX is not None: cls._local_nanvix_path = _EARLY_LOCAL_NANVIX - super().main(repo_root=repo_root) + super().main() # ---- Local Nanvix overlay -------------------------------------------- @@ -319,7 +323,7 @@ def setup(self) -> bool: from nanvix_zutil import Sysroot - sysroot_dir = self.nanvix_dir / "sysroot" + sysroot_dir = nanvix_root() / "sysroot" if sysroot_dir.exists(): shutil.rmtree(sysroot_dir) sysroot_dir.mkdir(parents=True) @@ -405,7 +409,31 @@ def build(self) -> None: if IS_WINDOWS or not self._has_native_toolchain(): self._docker_build() else: - run(*self._make_args("all"), cwd=self.repo_root, docker=self.docker) + run(*self._make_args("all"), cwd=repo_root(), docker=self.docker) + self._stage_release_outputs() + + def _stage_release_outputs(self) -> None: + """Mirror build/.elf into bin_out() for the inherited release(). + + The inherited ``ZScript.release()`` packages ``release_dir()`` + into ``dist_dir()``; copying the per-suite ELFs into + ``bin_out()`` is what makes them appear in the tarball. Missing + suites are tolerated here (the docker build may produce a subset + on some hosts); the tarball will simply omit them and any + downstream consumer can fail loudly. + """ + build_dir = repo_root() / "build" + if not build_dir.is_dir(): + return + dest = bin_out() + dest.mkdir(parents=True, exist_ok=True) + copied = 0 + for suite in ALL_SUITES: + src = build_dir / f"{suite}.elf" + if src.is_file(): + shutil.copy2(src, dest / src.name) + copied += 1 + log.info(f"Staged {copied} test ELFs under {dest}") def test(self) -> None: """Run the POSIX test suites. @@ -417,58 +445,28 @@ def test(self) -> None: self._run_tests_native() def release(self) -> None: - """Package the posix-tests release tarball and verify it.""" + """Package the posix-tests release tarball. + + Staging happens in ``build()``; this override only short-circuits + on Windows (no release packaging supported) and delegates to the + inherited ``ZScript.release()`` everywhere else. + """ self._overlay_local_nanvix() if IS_WINDOWS: log.warning("Release packaging is not supported on Windows.") log.warning("Use a Linux host or CI to build release tarballs.") return - - build_dir = self.repo_root / "build" - dist_dir = self.repo_root / "dist" - dist_dir.mkdir(parents=True, exist_ok=True) - - artifact = ( - f"posix-tests-{self.config.machine}-{self.config.deployment_mode}" - f"-{self.config.memory_size}.tar.gz" - ) - tarball = dist_dir / artifact - - log.info("Packaging release...") - missing = [s for s in ALL_SUITES if not (build_dir / f"{s}.elf").is_file()] - if missing: - log.fatal( - f"Missing test binaries: {', '.join(missing)}", - code=EXIT_MISSING_DEP, - hint="Run `./z build` first.", - ) - - with tarfile.open(tarball, "w:gz") as tf: - for suite in ALL_SUITES: - src = build_dir / f"{suite}.elf" - tf.add(src, arcname=f"{suite}.elf") - log.info(f"Package: {tarball}") - - log.info("Verifying package...") - with tarfile.open(tarball, "r:gz") as tf: - names = set(tf.getnames()) - for suite in ALL_SUITES: - if f"{suite}.elf" not in names: - log.fatal( - f"Missing {suite}.elf in package", - code=EXIT_MISSING_DEP, - ) - log.success(f"Package verified: {tarball}") + super().release() def clean(self) -> None: """Remove build artifacts.""" if IS_WINDOWS: - build_dir = self.repo_root / "build" + build_dir = repo_root() / "build" if build_dir.is_dir(): shutil.rmtree(build_dir) log.info("Removed build/") else: - run("make", "-C", "src", "clean", cwd=self.repo_root) + run("make", "-C", "src", "clean", cwd=repo_root()) # ---- Docker build ---------------------------------------------------- @@ -481,7 +479,7 @@ def _docker_build(self) -> None: as Docker build args. """ docker_image = self._resolve_docker_image() - build_dir = self.repo_root / "build" + build_dir = repo_root() / "build" build_dir.mkdir(exist_ok=True) # Determine the sysroot path relative to the repo root. @@ -489,7 +487,7 @@ def _docker_build(self) -> None: # places them directly in .nanvix/. Pass the correct path so the # Dockerfile can forward it to Make. sysroot_rel = ".nanvix/sysroot" - if not (self.repo_root / sysroot_rel / "lib" / "libposix.a").is_file(): + if not (repo_root() / sysroot_rel / "lib" / "libposix.a").is_file(): sysroot_rel = ".nanvix" log.info(f"Building via Docker ({docker_image})...") @@ -510,7 +508,7 @@ def _docker_build(self) -> None: "--output", "type=local,dest=build", ".", - cwd=self.repo_root, + cwd=repo_root(), ) # Count produced binaries. @@ -524,7 +522,7 @@ def _resolve_docker_image(self) -> str: Falls back to the cached ``.docker-image`` file if present. """ # Check for cached .docker-image (from 'make init'). - cached = self.repo_root / ".nanvix" / ".docker-image" + cached = repo_root() / ".nanvix" / ".docker-image" if cached.is_file(): tag = cached.read_text().strip() if tag: @@ -560,7 +558,7 @@ def _run_tests_native(self) -> None: code=EXIT_MISSING_DEP, hint="Run `./z setup` first.", ) - build_dir = self.repo_root / "build" + build_dir = repo_root() / "build" # --- Smoke tests --- print("Running smoke tests...") @@ -599,7 +597,7 @@ def _run_tests_standalone( print(f"SKIP {suite}") continue print(f"RUN {suite}...") - repo_elf = self.repo_root / binary.name + repo_elf = repo_root() / binary.name copied_elf = False initrd: Path | None = None try: @@ -614,10 +612,13 @@ def _run_tests_standalone( # misc-c.elf is a special case that needs the test # environment variable set to pass its internal checks. initrd = make_initrd( - self, binary.name, InitRdArgs(app_env=["NANVIX_TEST=1"]) + self, + binary.name, + test=True, + args=InitRdArgs(app_env=["NANVIX_TEST=1"]), ) else: - initrd = make_initrd(self, binary.name) + initrd = make_initrd(self, binary.name, test=True) with tempfile.TemporaryDirectory(prefix=f"posix_test_{suite}_") as tmp: tmp_path = Path(tmp) ramfs_dir = tmp_path / "ramfs" @@ -662,7 +663,7 @@ def _run_tests_standalone( log.info(f"$ {' '.join(cmd)}") subprocess.run( cmd, - cwd=self.repo_root, + cwd=repo_root(), stdin=subprocess.DEVNULL, text=True, check=True, @@ -747,7 +748,7 @@ def _run_tests_non_standalone( log.info(f"$ {' '.join(cmd)}") subprocess.run( cmd, - cwd=self.repo_root, + cwd=repo_root(), stdin=subprocess.DEVNULL, text=True, check=True, @@ -831,7 +832,7 @@ def _download_windows_binaries(self) -> None: return # Download to cache. - cache_dir = self.nanvix_dir / "cache" + cache_dir = nanvix_root() / "cache" cache_dir.mkdir(parents=True, exist_ok=True) zip_path = cache_dir / asset_name diff --git a/.zutils-version b/.zutils-version index bc3d337..fd2726c 100644 --- a/.zutils-version +++ b/.zutils-version @@ -1 +1 @@ -v0.10.4 +v0.11.0 diff --git a/z.ps1 b/z.ps1 index ff776ab..3e20038 100644 --- a/z.ps1 +++ b/z.ps1 @@ -57,8 +57,7 @@ catch { $null } -# Extract --with-zutils PATH and --with-nanvix PATH before forwarding to -# nanvix-zutil. +# Extract --with-zutils PATH before forwarding to nanvix-zutil. # # --with-zutils PATH (optional): install nanvix-zutil from a local source # tree (editable) instead of fetching the pinned wheel from GitHub Releases. @@ -85,25 +84,6 @@ while ($i -lt $ZArgs.Count) { $withZutils = $Matches[1] $i++ } - elseif ($ZArgs[$i] -eq '--with-nanvix') { - if ($i + 1 -ge $ZArgs.Count) { - throw "ERROR: --with-nanvix requires a path argument" - } - $item = Get-Item -LiteralPath $ZArgs[$i + 1] -ErrorAction Stop - if (-not $item.PSIsContainer) { - throw "ERROR: --with-nanvix path is not a directory: $($ZArgs[$i + 1])" - } - $env:WITH_NANVIX = $item.FullName - $i += 2 - } - elseif ($ZArgs[$i] -match '^--with-nanvix=(.+)$') { - $item = Get-Item -LiteralPath $Matches[1] -ErrorAction Stop - if (-not $item.PSIsContainer) { - throw "ERROR: --with-nanvix path is not a directory: $($Matches[1])" - } - $env:WITH_NANVIX = $item.FullName - $i++ - } else { $filteredArgs.Add($ZArgs[$i]) $i++ diff --git a/z.sh b/z.sh index 87cb138..56c73be 100755 --- a/z.sh +++ b/z.sh @@ -53,9 +53,9 @@ function _resolve_venv_paths() { _resolve_venv_paths ZUTIL_GLOBAL_VERSION="$(nanvix-zutil --version 2>/dev/null || true)" -# Extract --with-zutils PATH and --with-nanvix PATH before forwarding to -# nanvix-zutil. The nanvix-zutil CLI inspects positional args to find the -# subcommand; either flag's PATH argument would be mistaken for a subcommand. +# Extract --with-zutils PATH before forwarding to nanvix-zutil. The +# nanvix-zutil CLI inspects positional args to find the subcommand; the +# flag's PATH argument would otherwise be mistaken for a subcommand. # # --with-zutils PATH (optional): install nanvix-zutil from a local source # tree (editable) instead of fetching the pinned wheel from GitHub Releases. @@ -67,8 +67,6 @@ ZUTIL_GLOBAL_VERSION="$(nanvix-zutil --version 2>/dev/null || true)" # The path must point at a nanvix-zutil source checkout (a directory # containing pyproject.toml). The venv version-match check is bypassed; the # editable install is rebuilt only when the recorded source path changes. -# -# --with-nanvix PATH is forwarded to z.py via the WITH_NANVIX env var. WITH_ZUTILS="" _resolve_zutils_path() { local raw="$1" @@ -90,15 +88,6 @@ _resolve_zutils_path() { fi } -_resolve_nanvix_path() { - local raw="$1" - if ! WITH_NANVIX="$(cd -- "$raw" 2>/dev/null && pwd -P)"; then - echo "ERROR: --with-nanvix path does not exist or is not a directory: $raw" >&2 - exit 1 - fi - export WITH_NANVIX -} - ARGS=() while [[ $# -gt 0 ]]; do case "$1" in @@ -114,18 +103,6 @@ while [[ $# -gt 0 ]]; do _resolve_zutils_path "$2" shift 2 ;; - --with-nanvix=*) - _resolve_nanvix_path "${1#--with-nanvix=}" - shift - ;; - --with-nanvix) - if [[ $# -lt 2 ]]; then - echo "ERROR: --with-nanvix requires a path argument" >&2 - exit 1 - fi - _resolve_nanvix_path "$2" - shift 2 - ;; *) ARGS+=("$1") shift From 4313124dd3862d6639e303c0b99784741bca521f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 8 Jun 2026 19:37:10 +0000 Subject: [PATCH 8/9] [ci] E: Update nanvix workflow refs to v2.2.0 --- .github/workflows/nanvix-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nanvix-ci.yml b/.github/workflows/nanvix-ci.yml index 93386ba..c75d89f 100644 --- a/.github/workflows/nanvix-ci.yml +++ b/.github/workflows/nanvix-ci.yml @@ -32,7 +32,7 @@ concurrency: jobs: ci: if: github.event_name != 'schedule' - uses: nanvix/workflows/.github/workflows/nanvix-ci.yml@v2.1.0 + uses: nanvix/workflows/.github/workflows/nanvix-ci.yml@v2.2.0 with: docker-image: "ghcr.io/nanvix/toolchain-gcc@sha256:ea33e4cae4041dcf55f0ec49f58de90ae1b5e9fb7c3c507971b40975b14aecd3" # yamllint disable-line rule:line-length platforms: '["microvm"]' From ee7a87ae95d369506e0ba967d8806caeaf101fd4 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Wed, 3 Jun 2026 18:53:48 -0700 Subject: [PATCH 9/9] [dlfcn] E: Add tests for ctors/dtors and DT_RUNPATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new test suite (`dlfcn-init-runpath-c`) that exercises the three System V ABI capabilities the Nanvix dynamic loader now implements (esaurez/nanvix PR `feat/dlfcn-init-array-and-runpath`): 1. `libctor.so` defines `.init_array` and `.fini_array` entries via `__attribute__((constructor))` / `((destructor))`. The constructor writes a sentinel into a library-local global; the destructor writes a different sentinel into the test program's exported `g_dtor_ran` global so the witness survives the library being unloaded. The test asserts both sentinels appear in the expected order. 2. `libparent.so` is linked against `libchild.so` (creating a `DT_NEEDED` edge) and built with `-Wl,--enable-new-dtags, -rpath,lib/subdir`, which the linker emits as `DT_RUNPATH lib/subdir`. At runtime `libchild.so` is staged into `lib/subdir/` only — never `lib/` — so the only way `dlopen` can succeed is by honouring `libparent.so`'s `DT_RUNPATH`. Both libraries are built flat in `build/` and re-staged into the correct ramfs paths via the existing `SUITE_RAMFS_LIBS` mechanism in `.nanvix/z.py`, matching the pattern already used by `dlfcn-c`. The suite is registered in `STANDALONE_ONLY_SUITES` because it requires ramfs-bundled `.so` files, and in `ALL_SUITES`, the host `Makefile`, and the container `src/Makefile`. Test output on a standalone Nanvix VM running the updated loader: === dlfcn init_array + DT_RUNPATH tests === PASS: init_array fires on dlopen PASS: fini_array fires on dlclose PASS: DT_RUNPATH dependency search 3 passed, 0 failed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .nanvix/z.py | 7 + Makefile | 2 +- src/Makefile | 2 +- src/dlfcn-init-runpath-c/Makefile | 44 ++++++ src/dlfcn-init-runpath-c/libs/ctor.c | 38 +++++ src/dlfcn-init-runpath-c/libs/parent.c | 19 +++ src/dlfcn-init-runpath-c/libs/subdir/child.c | 16 ++ src/dlfcn-init-runpath-c/main.c | 146 +++++++++++++++++++ 8 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 src/dlfcn-init-runpath-c/Makefile create mode 100644 src/dlfcn-init-runpath-c/libs/ctor.c create mode 100644 src/dlfcn-init-runpath-c/libs/parent.c create mode 100644 src/dlfcn-init-runpath-c/libs/subdir/child.c create mode 100644 src/dlfcn-init-runpath-c/main.c diff --git a/.nanvix/z.py b/.nanvix/z.py index 5c47e76..f83a3ca 100644 --- a/.nanvix/z.py +++ b/.nanvix/z.py @@ -67,6 +67,7 @@ "c-bindings", "dlfcn-c", "dlfcn-global-c", + "dlfcn-init-runpath-c", "dlfcn-needed-c", "dlfcn-pie-c", "echo-c", @@ -101,6 +102,7 @@ # Suites that require ramfs-bundled shared libraries and only run in standalone mode. STANDALONE_ONLY_SUITES = [ "dlfcn-c", + "dlfcn-init-runpath-c", "dlfcn-pie-c", ] @@ -113,6 +115,11 @@ # Maps suite name to a list of (source_filename_in_build_dir, ramfs_target_path). SUITE_RAMFS_LIBS: dict[str, list[tuple[str, str]]] = { "dlfcn-c": [("libmul.so", "lib/libmul.so")], + "dlfcn-init-runpath-c": [ + ("libctor.so", "lib/libctor.so"), + ("libparent.so", "lib/libparent.so"), + ("libchild.so", "lib/subdir/libchild.so"), + ], "dlfcn-pie-c": [("libmul-pie.so", "lib/libmul-pie.so")], } diff --git a/Makefile b/Makefile index bfcb117..44d796d 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ PROCESS_MODE ?= multi-process MEMORY_SIZE ?= 128mb # Test suites to build. -SUITES := c-bindings dlfcn-c dlfcn-pie-c echo-c echo-cpp file-c hello-c hello-cpp memory-c misc-c network-c noop-c noop-cpp thread-c +SUITES := c-bindings dlfcn-c dlfcn-init-runpath-c dlfcn-pie-c echo-c echo-cpp file-c hello-c hello-cpp memory-c misc-c network-c noop-c noop-cpp thread-c # ELF binaries produced by each suite. BINARIES := $(addsuffix .elf,$(SUITES)) diff --git a/src/Makefile b/src/Makefile index 45cb2f9..5eee457 100644 --- a/src/Makefile +++ b/src/Makefile @@ -65,7 +65,7 @@ export LIBRARIES_DIR ?= $(BINARIES_DIR) # Test Suites #=============================================================================== -SUITES := c-bindings dlfcn-c dlfcn-global-c dlfcn-needed-c dlfcn-pie-c echo-c echo-cpp file-c hello-c hello-cpp memory-c misc-c network-c noop-c noop-cpp thread-c +SUITES := c-bindings dlfcn-c dlfcn-global-c dlfcn-init-runpath-c dlfcn-needed-c dlfcn-pie-c echo-c echo-cpp file-c hello-c hello-cpp memory-c misc-c network-c noop-c noop-cpp thread-c #=============================================================================== # Build Rules diff --git a/src/dlfcn-init-runpath-c/Makefile b/src/dlfcn-init-runpath-c/Makefile new file mode 100644 index 0000000..70002f3 --- /dev/null +++ b/src/dlfcn-init-runpath-c/Makefile @@ -0,0 +1,44 @@ +# Copyright(c) The Maintainers of Nanvix. +# Licensed under the MIT License. +# +# dlfcn-init-runpath-c: Tests for `.init_array` / `.fini_array` +# constructor and destructor invocation, and for DT_RUNPATH-driven +# DT_NEEDED dependency search. + +PROGRAM_NAME := dlfcn-init-runpath-c + +SOURCES := $(wildcard *.c) +OBJECTS := $(SOURCES:.c=.o) +BINARY := $(PROGRAM_NAME).elf + +all: $(OBJECTS) libs-all + $(CC) $(LDFLAGS) -pie -rdynamic -Wl,--no-dynamic-linker $(OBJECTS) $(LIBRARIES) -o $(BINARIES_DIR)/$(BINARY) + +clean: libs-clean + rm -f $(OBJECTS) + rm -f $(BINARIES_DIR)/$(BINARY) + +# libctor.so - constructor/destructor witness, used by the .init_array tests. +# libchild.so - dependency of libparent.so. Will be staged into +# lib/subdir/ at ramfs time so it is only reachable via +# libparent.so's DT_RUNPATH (z.py SUITE_RAMFS_LIBS). +# libparent.so - DT_NEEDED=libchild.so, DT_RUNPATH=lib/subdir/. +# +# `--enable-new-dtags` ensures the linker emits DT_RUNPATH (not the +# deprecated DT_RPATH) for libparent.so, matching what modern +# toolchains produce by default. +libs-all: + $(CC) libs/ctor.c -shared -fPIC -o $(LIBRARIES_DIR)/libctor.so + $(CC) libs/subdir/child.c -shared -fPIC -o $(LIBRARIES_DIR)/libchild.so + $(CC) libs/parent.c -shared -fPIC \ + -L$(LIBRARIES_DIR) -lchild \ + -Wl,--enable-new-dtags,-rpath,lib/subdir \ + -o $(LIBRARIES_DIR)/libparent.so + +libs-clean: + rm -f $(LIBRARIES_DIR)/libctor.so + rm -f $(LIBRARIES_DIR)/libchild.so + rm -f $(LIBRARIES_DIR)/libparent.so + +%.o: %.c + $(CC) $(CFLAGS) -fPIE $< -c -o $@ diff --git a/src/dlfcn-init-runpath-c/libs/ctor.c b/src/dlfcn-init-runpath-c/libs/ctor.c new file mode 100644 index 0000000..5033ae2 --- /dev/null +++ b/src/dlfcn-init-runpath-c/libs/ctor.c @@ -0,0 +1,38 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + */ + +/* + * libctor.so - shared library that exercises `.init_array` and + * `.fini_array` semantics required by the System V gABI. + * + * The constructor sets `ctor_ran` to a known sentinel value, and the + * destructor writes a different sentinel into the test program's + * `g_dtor_ran` global so that observation outlives the unloaded + * library. `g_dtor_ran` is declared `extern` here and resolved by the + * Nanvix loader's global symbol table (the main executable was linked + * with `-rdynamic`). + */ + +/* Public state exposed via dlsym so the test can read it after dlopen. */ +volatile int ctor_ran = 0; + +/* Main executable owns the destructor witness. */ +extern volatile int g_dtor_ran; + +static void __attribute__((constructor)) my_ctor(void) +{ + ctor_ran = 0xC70A; +} + +static void __attribute__((destructor)) my_dtor(void) +{ + g_dtor_ran = 0xD70A; +} + +/* Sanity entry point so the test can confirm dlsym still works. */ +int ctor_value(void) +{ + return 42; +} diff --git a/src/dlfcn-init-runpath-c/libs/parent.c b/src/dlfcn-init-runpath-c/libs/parent.c new file mode 100644 index 0000000..bab54cb --- /dev/null +++ b/src/dlfcn-init-runpath-c/libs/parent.c @@ -0,0 +1,19 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + */ + +/* + * libparent.so - shared library whose DT_NEEDED entry points at + * libchild.so, but whose runtime search path (DT_RUNPATH) is + * `lib/subdir/`. The Nanvix loader must consult DT_RUNPATH before + * falling back to the default `lib/` directory, otherwise libchild.so + * will not be located and dlopen will fail. + */ + +extern int child_value(int x); + +int parent_value(int x) +{ + return child_value(x) * 2; +} diff --git a/src/dlfcn-init-runpath-c/libs/subdir/child.c b/src/dlfcn-init-runpath-c/libs/subdir/child.c new file mode 100644 index 0000000..2cdb6fc --- /dev/null +++ b/src/dlfcn-init-runpath-c/libs/subdir/child.c @@ -0,0 +1,16 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + */ + +/* + * libchild.so - dependency of libparent.so. Installed under a + * non-default directory (`lib/subdir/`) so the only way the loader + * can find it via the DT_NEEDED edge in libparent.so is by honouring + * libparent's DT_RUNPATH. + */ + +int child_value(int x) +{ + return x + 7; +} diff --git a/src/dlfcn-init-runpath-c/main.c b/src/dlfcn-init-runpath-c/main.c new file mode 100644 index 0000000..64d2a7d --- /dev/null +++ b/src/dlfcn-init-runpath-c/main.c @@ -0,0 +1,146 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + */ + +/* + * dlfcn-init-runpath-c: Tests for `.init_array` / `.fini_array` + * constructor and destructor invocation, and for DT_RUNPATH-driven + * dependency search. + * + * Test 1 (constructor): dlopen libctor.so, dlsym `ctor_ran`, and + * confirm the constructor sentinel was written. + * + * Test 2 (destructor): dlclose libctor.so and confirm the destructor + * wrote its sentinel into the main executable's `g_dtor_ran` global. + * `g_dtor_ran` must be exported via -rdynamic so the loader's global + * symbol table can satisfy the `extern volatile int g_dtor_ran;` + * reference in the library. + * + * Test 3 (DT_RUNPATH): dlopen lib/libparent.so, which has + * DT_NEEDED=libchild.so and DT_RUNPATH=lib/subdir/. The loader must + * probe DT_RUNPATH before the default `lib/` directory; otherwise + * libchild.so is not located. + */ + +#include +#include +#include +#include + +/* Witness for libctor.so's destructor — defined here so it survives the + * library being unloaded. Marked volatile to keep the optimiser away. + */ +volatile int g_dtor_ran = 0; + +static int tests_passed = 0; +static int tests_failed = 0; + +static void pass(const char *name) +{ + printf(" PASS: %s\n", name); + fflush(stdout); + tests_passed++; +} + +static void fail(const char *name, const char *reason) +{ + printf(" FAIL: %s (%s)\n", name, reason); + fflush(stdout); + tests_failed++; +} + +/* + * Test 1: Constructor in `.init_array` must run before dlopen returns. + */ +static void test_init_array(void) +{ + void *h = dlopen("lib/libctor.so", RTLD_NOW); + if (h == NULL) { + fail("init_array fires on dlopen", dlerror()); + return; + } + + volatile int *ctor_ran = (volatile int *)dlsym(h, "ctor_ran"); + if (ctor_ran == NULL) { + fail("init_array fires on dlopen", "ctor_ran symbol missing"); + dlclose(h); + return; + } + + if (*ctor_ran != 0xC70A) { + fail("init_array fires on dlopen", "constructor sentinel not set"); + dlclose(h); + return; + } + + /* Sanity check: ordinary symbol resolution still works after init. */ + int (*fn)(void) = NULL; + *(void **)(&fn) = dlsym(h, "ctor_value"); + if (fn == NULL || fn() != 42) { + fail("init_array fires on dlopen", "ctor_value() wrong"); + dlclose(h); + return; + } + + /* Reset the dtor witness so test_fini_array measures a fresh signal. */ + g_dtor_ran = 0; + dlclose(h); + + pass("init_array fires on dlopen"); + + /* Bridge directly into the destructor test while the witness is fresh. */ + if (g_dtor_ran != 0xD70A) { + fail("fini_array fires on dlclose", "destructor sentinel not set"); + return; + } + pass("fini_array fires on dlclose"); +} + +/* + * Test 3: DT_RUNPATH must be consulted when resolving DT_NEEDED bare + * names. libparent.so depends on libchild.so but libchild.so only + * exists under lib/subdir/, which is libparent's DT_RUNPATH. + */ +static void test_dt_runpath(void) +{ + void *h = dlopen("lib/libparent.so", RTLD_NOW); + if (h == NULL) { + fail("DT_RUNPATH dependency search", dlerror()); + return; + } + + int (*fn)(int) = NULL; + *(void **)(&fn) = dlsym(h, "parent_value"); + if (fn == NULL || fn(5) != 24) { + /* parent_value(5) = child_value(5) * 2 = (5 + 7) * 2 = 24 */ + fail("DT_RUNPATH dependency search", "parent_value() wrong"); + dlclose(h); + return; + } + + dlclose(h); + pass("DT_RUNPATH dependency search"); +} + +int main(int argc, const char *argv[]) +{ + (void)argc; + (void)argv; + + printf("=== dlfcn init_array + DT_RUNPATH tests ===\n"); + fflush(stdout); + + test_init_array(); + test_dt_runpath(); + + printf("\n%d passed, %d failed\n", tests_passed, tests_failed); + fflush(stdout); + + if (tests_failed == 0) { + const char *magic = "ok"; + write(STDOUT_FILENO, magic, 3); + } + + return tests_failed > 0 ? 1 : 0; +}