From a8462e696c0414a95ef8a5dda66c2d69741f8a7d Mon Sep 17 00:00:00 2001 From: ARIA Date: Wed, 16 Sep 2026 10:43:23 +0200 Subject: [PATCH] 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 --- Makefile | 29 +++++++++++++++++++ README.md | 31 +++++++++++++------- docs/Installation.md | 47 ++++++++++++++++++------------ hatch_build.py | 68 +++++++++++++++++++++++++++++++++++++++++++ install.sh | 62 +++++++++++++++++++++++++++++++++++++++ pyproject.toml | 11 +++++++ uv.lock | 69 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 287 insertions(+), 30 deletions(-) create mode 100644 Makefile create mode 100644 hatch_build.py create mode 100755 install.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..43b5e7e --- /dev/null +++ b/Makefile @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 3588f5f..78e31ef 100644 --- a/README.md +++ b/README.md @@ -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 - **Root/sudo access** (required for GPU hardware interactions) -## Installation from Source +## Installation + +### One-liner ```bash -git clone .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 +./install.sh +``` -# Build the React frontend -cd frontend -npm install -npm run build -cd .. +### Direct from git (no clone, no script) -# Install the Python package (includes bundled frontend) -uv tool install . +```bash +uv tool install "git+https://gitea.zephyre.one/Pakobbix/nvcurve.git" ``` 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 cd nvcurve git pull -cd frontend && npm run build && cd .. -uv tool install . +uv tool install --force . ``` +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: ```bash diff --git a/docs/Installation.md b/docs/Installation.md index ee3fcad..d765481 100644 --- a/docs/Installation.md +++ b/docs/Installation.md @@ -29,37 +29,45 @@ sudo pacman -S uv pip install uv ``` -## Installation from Source +## Installation -### Step 1: Clone the Repository +### Option 1: One-liner (recommended) ```bash -git clone .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 +./install.sh ``` -### Step 2: Build the Frontend - -The frontend is a React + TypeScript + Vite application in the `frontend/` directory. +Equivalent manual steps (what the script does): ```bash -cd frontend -npm install -npm run build -cd .. +git clone https://gitea.zephyre.one/Pakobbix/nvcurve.git +cd nvcurve +uv tool install . # frontend is built automatically if missing/stale ``` -This produces a `dist/` directory with the compiled static assets. The hatch build system bundles `frontend/dist` into the Python package. - -### Step 3: Install the Python Package +### Option 3: Direct from git (no clone, no script) ```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@" +``` + +### Verify ```bash nvcurve setup @@ -94,10 +102,11 @@ nvcurve serve start ```bash cd nvcurve git pull -cd frontend && npm run build && cd .. -uv tool install . +uv tool install --force . ``` +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: ```bash @@ -116,7 +125,7 @@ source ~/.local/bin/env # or wherever uv installed ### 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 diff --git a/hatch_build.py b/hatch_build.py new file mode 100644 index 0000000..3f3fbd7 --- /dev/null +++ b/hatch_build.py @@ -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 diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..65e40bd --- /dev/null +++ b/install.sh @@ -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" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f397960..6a0d03e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,9 +19,20 @@ dependencies = [ [project.scripts] 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] 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+` works +# as a single command. It runs for both wheel and sdist builds. + [tool.hatch.build.targets.wheel.force-include] "frontend/dist" = "nvcurve/frontend/dist" diff --git a/uv.lock b/uv.lock index 287f06c..30d4f5b 100644 --- a/uv.lock +++ b/uv.lock @@ -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" }, ] +[[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]] name = "httpcore" version = "1.0.9" @@ -233,6 +249,11 @@ dependencies = [ { name = "uvicorn", extra = ["standard"] }, ] +[package.dev-dependencies] +dev = [ + { name = "hatchling" }, +] + [package.metadata] requires-dist = [ { name = "bcrypt", specifier = ">=4.0" }, @@ -243,6 +264,9 @@ requires-dist = [ { name = "uvicorn", extras = ["standard"], specifier = ">=0.30" }, ] +[package.metadata.requires-dev] +dev = [{ name = "hatchling" }] + [[package]] name = "nvidia-ml-py" 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" }, ] +[[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]] name = "pydantic" 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" }, ] +[[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]] name = "typing-extensions" version = "4.15.0"