Add single-command installation: install.sh, Makefile, frontend build hook

- hatch_build.py: custom hatchling build hook that compiles the React
  frontend (npm ci + build) when frontend/dist is missing or stale, so
  'uv tool install git+https://gitea.zephyre.one/Pakobbix/nvcurve.git'
  works as a single command
- install.sh: curl|bash installer (checks prerequisites, auto-installs uv,
  clones and installs)
- Makefile: dev targets (frontend, install, dev, test, clean)
- README/docs: document the one-liner, clone, and direct-git installs
This commit is contained in:
ARIA committed 2026-09-16 10:43:23 +02:00
1 parent d810c44478
commit a8462e696c
7 files changed
+287 -30

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
# NVCurve — developer convenience targets.
#
# End users don't need make: run ./install.sh (see README "Installation").
UV ?= uv
NPM ?= npm
.PHONY: help frontend frontend-dev install dev test clean
help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
frontend: ## Build the React frontend into frontend/dist
cd frontend && $(NPM) ci && $(NPM) run build
frontend-dev: ## Run the Vite dev server (hot reload)
cd frontend && $(NPM) run dev
install: ## Install nvcurve as a uv tool (builds frontend if missing/stale)
$(UV) tool install --force .
dev: ## Create/refresh the dev environment (uv sync)
$(UV) sync
test: ## Run the test suite
$(UV) run python tests/test_security.py
clean: ## Remove build artifacts
rm -rf frontend/dist frontend/node_modules
+20 -11
View File
@@ -37,20 +37,28 @@ NVCurve brings MSI Afterburner-style per-point voltage-frequency curve control t
- **[uv](https://docs.astral.sh/uv/)** — Python package manager - **[uv](https://docs.astral.sh/uv/)** — Python package manager
- **Root/sudo access** (required for GPU hardware interactions) - **Root/sudo access** (required for GPU hardware interactions)
## Installation from Source ## Installation
### One-liner
```bash ```bash
git clone <this-repo-url>.git curl -fsSL https://gitea.zephyre.one/Pakobbix/nvcurve/raw/branch/main/install.sh | bash
```
The script checks prerequisites (installs `uv` if missing), clones the repo, and installs NVCurve — the React frontend is compiled automatically during the build.
### From a clone
```bash
git clone https://gitea.zephyre.one/Pakobbix/nvcurve.git
cd nvcurve cd nvcurve
./install.sh
```
# Build the React frontend ### Direct from git (no clone, no script)
cd frontend
npm install
npm run build
cd ..
# Install the Python package (includes bundled frontend) ```bash
uv tool install . uv tool install "git+https://gitea.zephyre.one/Pakobbix/nvcurve.git"
``` ```
After installation, verify hardware compatibility: After installation, verify hardware compatibility:
@@ -200,10 +208,11 @@ The GPU key can be a UUID, `pci:XXXX`, or `idx:N` fallback. Find your GPU key wi
```bash ```bash
cd nvcurve cd nvcurve
git pull git pull
cd frontend && npm run build && cd .. uv tool install --force .
uv tool install .
``` ```
The frontend is rebuilt automatically if it is missing or older than the frontend sources. If you modified frontend code locally, run `make frontend` first (or `rm -rf frontend/dist`).
If running as a systemd service: If running as a systemd service:
```bash ```bash
+28 -19
View File
@@ -29,37 +29,45 @@ sudo pacman -S uv
pip install uv pip install uv
``` ```
## Installation from Source ## Installation
### Step 1: Clone the Repository ### Option 1: One-liner (recommended)
```bash ```bash
git clone <this-repo-url>.git curl -fsSL https://gitea.zephyre.one/Pakobbix/nvcurve/raw/branch/main/install.sh | bash
```
The script checks prerequisites (installs `uv` if missing), clones the repository, and installs NVCurve. The React frontend is compiled automatically during the build by a hatchling build hook (`hatch_build.py`).
### Option 2: From a clone
```bash
git clone https://gitea.zephyre.one/Pakobbix/nvcurve.git
cd nvcurve cd nvcurve
./install.sh
``` ```
### Step 2: Build the Frontend Equivalent manual steps (what the script does):
The frontend is a React + TypeScript + Vite application in the `frontend/` directory.
```bash ```bash
cd frontend git clone https://gitea.zephyre.one/Pakobbix/nvcurve.git
npm install cd nvcurve
npm run build uv tool install . # frontend is built automatically if missing/stale
cd ..
``` ```
This produces a `dist/` directory with the compiled static assets. The hatch build system bundles `frontend/dist` into the Python package. ### Option 3: Direct from git (no clone, no script)
### Step 3: Install the Python Package
```bash ```bash
uv tool install . uv tool install "git+https://gitea.zephyre.one/Pakobbix/nvcurve.git"
``` ```
This installs `nvcurve` as a system-wide tool with the bundled frontend. To install a specific branch:
### Step 4: Verify ```bash
uv tool install "git+https://gitea.zephyre.one/Pakobbix/nvcurve.git@<branch>"
```
### Verify
```bash ```bash
nvcurve setup nvcurve setup
@@ -94,10 +102,11 @@ nvcurve serve start
```bash ```bash
cd nvcurve cd nvcurve
git pull git pull
cd frontend && npm run build && cd .. uv tool install --force .
uv tool install .
``` ```
The frontend is rebuilt automatically if it is missing or older than the frontend sources. If you modified frontend code locally, run `make frontend` first (or `rm -rf frontend/dist`).
If running as a systemd service: If running as a systemd service:
```bash ```bash
@@ -116,7 +125,7 @@ source ~/.local/bin/env # or wherever uv installed
### Frontend not loading in the web UI ### Frontend not loading in the web UI
Verify that `frontend/dist` exists and contains built assets. If the directory is empty or missing, rebuild with `npm run build` and reinstall with `uv tool install .`. Verify that the installed package contains the frontend. If `frontend/dist` is empty or missing, rebuild with `make frontend` (or `cd frontend && npm ci && npm run build`) and reinstall with `uv tool install --force .`.
### NvAPI functions not found ### NvAPI functions not found
+68
View File
@@ -0,0 +1,68 @@
"""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
Executable
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
#
# NVCurve single-command installer.
#
# curl -fsSL https://gitea.zephyre.one/Pakobbix/nvcurve/raw/branch/main/install.sh | bash
#
# or from a local clone:
#
# git clone https://gitea.zephyre.one/Pakobbix/nvcurve.git && cd nvcurve && ./install.sh
#
# The React frontend is compiled automatically during the Python build
# (see hatch_build.py), so Node.js 18+ and npm must be available.
#
# Environment:
# NVCURVE_BRANCH branch to install (default: main)
set -euo pipefail
REPO_URL="https://gitea.zephyre.one/Pakobbix/nvcurve.git"
BRANCH="${NVCURVE_BRANCH:-main}"
fail() {
echo "error: $*" >&2
exit 1
}
# --- prerequisites -----------------------------------------------------------
command -v git >/dev/null 2>&1 \
|| fail "git is required. Install it first."
command -v node >/dev/null 2>&1 \
|| fail "Node.js 18+ is required (e.g. 'sudo pacman -S nodejs npm' or 'sudo apt install nodejs npm')."
command -v npm >/dev/null 2>&1 \
|| fail "npm is required (usually installed together with Node.js)."
if ! command -v uv >/dev/null 2>&1; then
echo "uv not found — installing it from https://astral.sh/uv ..."
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
command -v uv >/dev/null 2>&1 \
|| fail "uv installation failed. Install uv manually: https://docs.astral.sh/uv/"
fi
# --- locate the source tree ---------------------------------------------------
if [ -f pyproject.toml ] && [ -d frontend ]; then
src="$(pwd)"
echo "Installing from current directory: $src"
else
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
echo "Cloning $REPO_URL (branch: $BRANCH) ..."
git clone --quiet --depth 1 --branch "$BRANCH" "$REPO_URL" "$tmp/nvcurve"
src="$tmp/nvcurve"
fi
# --- install -------------------------------------------------------------------
# The frontend is built automatically by the build hook (hatch_build.py).
uv tool install --force "$src"
echo
echo "NVCurve installed."
echo " Verify your GPU: nvcurve setup"
echo " Start the web UI: nvcurve"
+11
View File
@@ -19,9 +19,20 @@ dependencies = [
[project.scripts] [project.scripts]
nvcurve = "nvcurve.cli:main" nvcurve = "nvcurve.cli:main"
[dependency-groups]
dev = [
"hatchling", # enables local `hatch build` and resolves hatch_build.py imports
]
[tool.hatch.build.hooks.custom]
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["nvcurve"] packages = ["nvcurve"]
# The custom build hook (hatch_build.py) compiles the React frontend when
# frontend/dist is missing or stale, so `uv tool install git+<repo-url>` works
# as a single command. It runs for both wheel and sdist builds.
[tool.hatch.build.targets.wheel.force-include] [tool.hatch.build.targets.wheel.force-include]
"frontend/dist" = "nvcurve/frontend/dist" "frontend/dist" = "nvcurve/frontend/dist"
Generated
+69
View File
@@ -154,6 +154,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
] ]
[[package]]
name = "hatchling"
version = "1.32.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "pathspec" },
{ name = "pluggy" },
{ name = "tomlkit" },
{ name = "trove-classifiers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/69/08/33331757185504aae48b8d9bd78cec03a76e3aecfb52e549d05a2347c0dd/hatchling-1.32.0.tar.gz", hash = "sha256:0bdbde4a52b06c37e3eca395f85a762bf0ef06fe374fd8ae429dc6be10230f5f", size = 57783, upload-time = "2026-08-11T05:03:44.114Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/84/1798b6d85ecde0e31546004efd25c5de1b1f49250644a60cce460e12593a/hatchling-1.32.0-py3-none-any.whl", hash = "sha256:0e17c9c3b9aa7c625acc8d0f5b622f107d5049af9ecf5ada4de1aada5be7cdbc", size = 78435, upload-time = "2026-08-11T05:03:42.644Z" },
]
[[package]] [[package]]
name = "httpcore" name = "httpcore"
version = "1.0.9" version = "1.0.9"
@@ -233,6 +249,11 @@ dependencies = [
{ name = "uvicorn", extra = ["standard"] }, { name = "uvicorn", extra = ["standard"] },
] ]
[package.dev-dependencies]
dev = [
{ name = "hatchling" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "bcrypt", specifier = ">=4.0" }, { name = "bcrypt", specifier = ">=4.0" },
@@ -243,6 +264,9 @@ requires-dist = [
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.30" },
] ]
[package.metadata.requires-dev]
dev = [{ name = "hatchling" }]
[[package]] [[package]]
name = "nvidia-ml-py" name = "nvidia-ml-py"
version = "13.590.48" version = "13.590.48"
@@ -252,6 +276,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/72/fb2af0d259a651affdce65fd6a495f0e07a685a0136baf585c5065204ee7/nvidia_ml_py-13.590.48-py3-none-any.whl", hash = "sha256:fd43d30ee9cd0b7940f5f9f9220b68d42722975e3992b6c21d14144c48760e43", size = 50680, upload-time = "2026-01-22T01:14:55.281Z" }, { url = "https://files.pythonhosted.org/packages/fd/72/fb2af0d259a651affdce65fd6a495f0e07a685a0136baf585c5065204ee7/nvidia_ml_py-13.590.48-py3-none-any.whl", hash = "sha256:fd43d30ee9cd0b7940f5f9f9220b68d42722975e3992b6c21d14144c48760e43", size = 50680, upload-time = "2026-01-22T01:14:55.281Z" },
] ]
[[package]]
name = "packaging"
version = "26.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
]
[[package]]
name = "pathspec"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.12.5" version = "2.12.5"
@@ -406,6 +457,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" },
] ]
[[package]]
name = "tomlkit"
version = "0.15.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/96/e07752635b98536177fa1f37671c8f3cdde2e724c6bcf6034b2cfb571565/tomlkit-0.15.1.tar.gz", hash = "sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97", size = 180129, upload-time = "2026-07-17T01:48:04.562Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/bc/8c13eb66537dce1d2bd3a57132902f38d0e7f5bb46fa9f4daed9fe9d76ee/tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304", size = 49449, upload-time = "2026-07-17T01:48:05.728Z" },
]
[[package]]
name = "trove-classifiers"
version = "2026.6.1.19"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/e3/7ca82ee24c82d344584abd5b8637b3bd056f2900226e8d82fc22f1184b92/trove_classifiers-2026.6.1.19.tar.gz", hash = "sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745", size = 17059, upload-time = "2026-06-01T19:41:34.649Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl", hash = "sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3", size = 14211, upload-time = "2026-06-01T19:41:33.434Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"