Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
322 changes: 165 additions & 157 deletions .github/workflows/inspect-r-api-update.yml

Large diffs are not rendered by default.

129 changes: 72 additions & 57 deletions scripts/install_local_r_nns.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
#!/usr/bin/env python3
"""Install R NNS 13.0 from the vendored package source in this repository.
"""Install an R NNS package from a local source directory, tarball, or binary ZIP.

This installs NNS from the local source under ``tools/`` and never from CRAN.
It prefers the extracted package directory ``tools/NNS`` and falls back to the
vendored tarball ``tools/NNS_13.0.tar.gz``. After installation it verifies that
the loaded package reports version ``13.0``.
The helper never downloads NNS from CRAN. With no ``--source`` argument it
prefers ``tools/NNS`` and otherwise uses a vendored NNS archive under ``tools``.
A Windows binary can be supplied directly, for example::

Usage::
python scripts/install_local_r_nns.py --source C:/Users/me/Documents/NNS_13.1.zip

python scripts/install_local_r_nns.py

Requires ``R`` and ``Rscript`` on PATH. CI must not depend on this script; it is
a developer helper for regenerating the committed parity cache with a local,
non-CRAN R NNS install.
After installation the helper loads NNS and reports the actual installed
version. Use ``--expected-version`` only when an exact version must be enforced.
"""

from __future__ import annotations

import argparse
import json
import os
import shutil
import subprocess
import sys
Expand All @@ -26,8 +24,6 @@
_REPO_ROOT = Path(__file__).resolve().parents[1]
_TOOLS_DIR = _REPO_ROOT / "tools"
_SOURCE_DIR = _TOOLS_DIR / "NNS"
_SOURCE_TARBALL = _TOOLS_DIR / "NNS_13.0.tar.gz"
_EXPECTED_VERSION = "13.0"

_VERSION_SCRIPT = (
"suppressPackageStartupMessages(library(NNS)); "
Expand All @@ -36,80 +32,98 @@


def _resolve_source(override: Path | None = None) -> Path:
"""Return the NNS source path to install.

With no override, prefer the vendored extracted directory and fall back to
the vendored tarball. With an override (for example an upstream checkout at a
recorded R commit), install that path directly after validating it is a
package source directory or tarball.
"""
"""Return a local R package source directory or package archive."""

if override is not None:
if override.is_dir() and (override / "DESCRIPTION").is_file():
return override
if override.is_file():
return override
source = override.expanduser().resolve()
if source.is_dir() and (source / "DESCRIPTION").is_file():
return source
if source.is_file() and (
source.suffix.lower() == ".zip"
or source.name.lower().endswith((".tar.gz", ".tgz"))
):
return source
raise SystemExit(
f"ERROR: --source {override} is not an R package source. Expected a "
"directory containing DESCRIPTION or a package tarball."
f"ERROR: --source {source} is not an R package source. Expected a directory "
"containing DESCRIPTION, a source .tar.gz/.tgz, or a Windows binary .zip."
)

if (_SOURCE_DIR / "DESCRIPTION").is_file():
return _SOURCE_DIR
if _SOURCE_TARBALL.is_file():
return _SOURCE_TARBALL

archives = sorted(
[
*(_TOOLS_DIR.glob("NNS_*.tar.gz")),
*(_TOOLS_DIR.glob("NNS_*.tgz")),
*(_TOOLS_DIR.glob("NNS_*.zip")),
],
key=lambda path: path.stat().st_mtime,
reverse=True,
)
if archives:
return archives[0]

raise SystemExit(
"ERROR: no vendored NNS source found. Expected "
f"{_SOURCE_DIR}/DESCRIPTION or {_SOURCE_TARBALL}."
"ERROR: no vendored NNS source found. Expected tools/NNS/DESCRIPTION or an "
"NNS_*.tar.gz, NNS_*.tgz, or NNS_*.zip archive under tools/."
)


def _require(tool: str) -> str:
path = shutil.which(tool)
if path is None:
raise SystemExit(
f"ERROR: {tool!r} is not on PATH. Install R before running this helper; "
"this script installs NNS from local source, not from CRAN."
f"ERROR: {tool!r} is not on PATH. Install R before running this helper."
)
return path


def _install(source: Path, r_bin: str, rscript_bin: str) -> int:
if source.suffix.lower() == ".zip":
if os.name != "nt":
print(
"ERROR: an R Windows binary ZIP can only be installed on Windows.",
file=sys.stderr,
)
return 1
expression = (
f"install.packages({json.dumps(str(source))}, repos = NULL, type = 'win.binary')"
)
install = subprocess.run([rscript_bin, "-e", expression], check=False)
else:
install = subprocess.run([r_bin, "CMD", "INSTALL", str(source)], check=False)

if install.returncode != 0:
print("ERROR: local R package installation failed.", file=sys.stderr)
return install.returncode


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--source",
type=Path,
default=None,
help=(
"Install from this R package source (directory with DESCRIPTION or a "
"tarball) instead of the vendored tools/NNS. Use an upstream checkout "
"to install live R NNS at a recorded commit."
"Install from this local R package source: a directory with DESCRIPTION, "
"a source tarball, or a Windows binary ZIP."
),
)
parser.add_argument(
"--expected-version",
default=_EXPECTED_VERSION,
help=(
"Package version the install must report after loading. Defaults to "
f"{_EXPECTED_VERSION!r}. Pass the recorded upstream version when "
"installing from a non-vendored source."
),
default=None,
help="Optionally require the installed NNS package to report this exact version.",
)
args = parser.parse_args()
expected_version = args.expected_version or _EXPECTED_VERSION

r_bin = _require("R")
rscript_bin = _require("Rscript")
source = _resolve_source(args.source)

print(f"Installing R NNS from local source: {source} (not CRAN)")
install = subprocess.run(
[r_bin, "CMD", "INSTALL", str(source)],
check=False,
)
if install.returncode != 0:
print("ERROR: R CMD INSTALL failed.", file=sys.stderr)
return install.returncode
print(f"Installing R NNS from local package: {source} (not CRAN)")
install_status = _install(source, r_bin, rscript_bin)
if install_status != 0:
return install_status

probe = subprocess.run(
[rscript_bin, "-e", _VERSION_SCRIPT],
Expand All @@ -118,23 +132,24 @@ def main() -> int:
text=True,
)
if probe.returncode != 0:
print(
"ERROR: failed to load NNS after install:\n" + probe.stderr,
file=sys.stderr,
)
print("ERROR: failed to load NNS after install:\n" + probe.stderr, file=sys.stderr)
return probe.returncode

installed_version = probe.stdout.strip()
if not installed_version:
print("ERROR: installed NNS reported an empty version.", file=sys.stderr)
return 1

print(f"Installed NNS version: {installed_version}")
if installed_version != expected_version:
if args.expected_version is not None and installed_version != args.expected_version:
print(
"ERROR: installed NNS version "
f"{installed_version!r} does not match expected {expected_version!r}.",
f"{installed_version!r} does not match expected {args.expected_version!r}.",
file=sys.stderr,
)
return 1

print(f"OK: R NNS {expected_version} installed from local source.")
print(f"OK: R NNS {installed_version} installed from local package.")
return 0


Expand Down
Loading
Loading