Files
nvcurve/hatch_build.py
ARIA e148c83622 feat: experimental NVIDIA power control via RM ioctl interface
Adds an experimental power-cap mode using the undocumented RM ioctl
interface (based on panchovix's LACT PR #1205) to set power limits
below the VBIOS minimum (down to 30 W).

- hal/rm_power.py: RM ioctl power-cap read/write/reset + runtime probe
- limits.py: power_cap_mode (nvml/ioctl) with support detection
- config.py: persist power_cap_mode per GPU
- profiles: record/apply power_cap_mode
- server.py: POST /api/limits validates ioctl support (409 on failure)
- cli.py: profile save falls back to persisted mode
- client.py: power_cap_mode in Limits
- frontend: toggle + warning with panchovix attribution (LACT #1205)
- tests: test_rm_power.py (unit) + integration coverage
- Makefile: add test_rm_power.py to make test

Also includes automated linter reformatting (prettier, ruff, shellcheck,
isort, markdownlint) that the linter would apply anyway.
2026-09-17 22:44:27 +02:00

75 lines
2.5 KiB
Python

"""Custom hatchling build hook: build the React frontend if it is missing or stale.
This makes NVCurve installable with a single command, e.g.::
uv tool install "git+https://gitea.zephyre.one/Pakobbix/nvcurve.git"
The hook runs inside the isolated build environment right before the wheel
(or sdist) is assembled. If ``frontend/dist`` does not exist yet — or is older
than the frontend sources — it compiles the frontend using the host's ``npm``
(PATH is inherited from the environment).
"""
from __future__ import annotations
import os
import shutil
import subprocess
import sys
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
# Frontend inputs that must be newer than dist/index.html to trigger a rebuild.
_FRONTEND_INPUTS = (
"src",
"index.html",
"vite.config.ts",
"package.json",
"tsconfig.json",
)
class FrontendBuildHook(BuildHookInterface):
"""Build ``frontend/dist`` with npm when it is missing or stale."""
PLUGIN_NAME = "custom"
def initialize(self, version: str, build_data: dict) -> None:
frontend = os.path.join(self.root, "frontend")
dist_index = os.path.join(frontend, "dist", "index.html")
if not self._needs_build(frontend, dist_index):
return
npm = shutil.which("npm")
if npm is None:
raise RuntimeError(
"npm not found on PATH. Node.js 18+ and npm are required to build "
"the NVCurve frontend. Install them and retry, or use install.sh "
"which checks prerequisites for you."
)
print(
"[nvcurve] frontend/dist missing or stale — building frontend with npm ...",
file=sys.stderr,
)
subprocess.run([npm, "ci", "--no-audit", "--no-fund"], cwd=frontend, check=True)
subprocess.run([npm, "run", "build"], cwd=frontend, check=True)
@staticmethod
def _needs_build(frontend: str, dist_index: str) -> bool:
if not os.path.isfile(dist_index):
return True
dist_mtime = os.path.getmtime(dist_index)
for name in _FRONTEND_INPUTS:
path = os.path.join(frontend, name)
if os.path.isfile(path):
if os.path.getmtime(path) > dist_mtime:
return True
elif os.path.isdir(path):
for root, _dirs, files in os.walk(path):
for file in files:
if os.path.getmtime(os.path.join(root, file)) > dist_mtime:
return True
return False