diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 922c76a74f..68f3edabb2 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -69,9 +69,11 @@ jobs: private-key: ${{ secrets.APP_PRIVATE_KEY }} - name: Set up the locked site toolchain + id: pm uses: ./.github/actions/setup-pm with: toolchain: all + extras: '[]' node-cache-dependency-path: website/package-lock.json - name: Install ruamel.yaml for skill extraction @@ -182,6 +184,10 @@ jobs: - name: Build Docusaurus run: npm run build working-directory: website + env: + # prebuild renders icons on a Hermes runtime interpreter; the ci-tools + # step above re-exports HERMES_PYTHON to a tools-only environment. + HERMES_PYTHON: ${{ steps.pm.outputs.python-path }} - name: Stage deployment run: | diff --git a/.github/workflows/e2e-desktop-core.yml b/.github/workflows/e2e-desktop-core.yml index 3eb37accae..b77cbfc1a6 100644 --- a/.github/workflows/e2e-desktop-core.yml +++ b/.github/workflows/e2e-desktop-core.yml @@ -60,19 +60,20 @@ jobs: pyproject.toml uv.lock - - name: Set up Python 3.11 + - name: Set up Python 3.14 uses: ./.github/actions/retry with: - command: uv python install 3.11 + command: uv python install 3.14 - name: Install Python dependencies uses: ./.github/actions/retry with: - command: uv sync --locked --python 3.11 --extra all --extra dev + command: uv sync --locked --python 3.14 --extra all --extra dev - name: Build the desktop app working-directory: apps/desktop - run: npm run build + # The generator uses the runtime's Pillow + resvg-py, not a build-only venv. + run: HERMES_PYTHON="$GITHUB_WORKSPACE/.venv/bin/python" npm run build - name: Run the core suite under xvfb working-directory: apps/desktop diff --git a/.github/workflows/icons-freshness-check.yml b/.github/workflows/icons-freshness-check.yml index ffdf634c71..b00c0e3e2a 100644 --- a/.github/workflows/icons-freshness-check.yml +++ b/.github/workflows/icons-freshness-check.yml @@ -28,9 +28,11 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + # The generator runs on a Hermes runtime interpreter; [] exports one as HERMES_PYTHON. - uses: ./.github/actions/setup-pm with: toolchain: all + extras: '[]' cache-python: true prune-python-cache: true diff --git a/.github/workflows/rust-tests.yml b/.github/workflows/rust-tests.yml index 3cdf50f77a..490fadf16d 100644 --- a/.github/workflows/rust-tests.yml +++ b/.github/workflows/rust-tests.yml @@ -39,10 +39,12 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # tauri::generate_context! embeds the app icons, which are generated, - # not committed. + # not committed. The generator runs on a Hermes runtime interpreter; [] + # exports one as HERMES_PYTHON. - uses: ./.github/actions/setup-pm with: toolchain: all + extras: '[]' cache-python: true prune-python-cache: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc30e0daa1..04136e1404 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -195,9 +195,9 @@ Use a Node/npm version accepted by the corresponding `package.json` engines. Native desktop dependencies can also require the platform build toolchain. Logos and icons are generated from `assets/nous-girl-*.svg` and -`assets/backgrounds/`. `node scripts/generate-icons.mjs` uses the locked, -isolated `icon-build` dependency group. Do not commit generated PNG/ICO/ICNS -outputs or add icon renderers to production dependencies. +`assets/backgrounds/`. `node scripts/generate-icons.mjs` renders them with the +Hermes runtime Python (`HERMES_PYTHON`, else `python` on PATH): Pillow and +resvg-py are core dependencies. Do not commit generated PNG/ICO/ICNS outputs. ### Run tests diff --git a/Dockerfile b/Dockerfile index e6c5aa5619..973bd1ce3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -222,50 +222,7 @@ ENV HERMES_PYTHON=/usr/local/bin/python3 # native extensions must use the compiler installed in this image. ENV CC=gcc CXX=g++ -# Frontend dependencies never enter the runtime layers. -FROM runtime_base AS frontend_build -COPY package.json package-lock.json ./ -COPY web/package.json web/ -COPY ui-tui/package.json ui-tui/ -COPY ui-tui/packages/hermes-ink/ ui-tui/packages/hermes-ink/ -COPY apps/shared/ apps/shared/ -COPY scripts/build/node-deps.mjs scripts/build/node-deps.mjs -ENV npm_config_install_links=false -RUN node scripts/build/node-deps.mjs --source /opt/hermes --workspace ui-tui --workspace web - -COPY pyproject.toml uv.lock ./ -COPY web/ web/ -COPY ui-tui/ ui-tui/ -COPY scripts/build/*.mjs scripts/build/ -COPY scripts/build/icon_environment.py scripts/build/icon_environment.py -COPY scripts/generate-icons.mjs scripts/generate_icons.py scripts/ -COPY assets/ assets/ -RUN node scripts/generate-icons.mjs --source /opt/hermes --out /tmp/hermes-icons && \ - node scripts/build/tui.mjs --source /opt/hermes --out /opt/products/tui && \ - node scripts/build/web.mjs --source /opt/hermes --icons /tmp/hermes-icons --out /opt/products/web - -FROM runtime_base AS runtime -# Standalone TypeScript linting is a runtime feature; Vite/esbuild are not. -COPY --from=frontend_build /opt/hermes/node_modules/typescript /opt/hermes/node_modules/typescript -RUN mkdir -p /opt/hermes/node_modules/.bin && \ - ln -s ../typescript/bin/tsc /opt/hermes/node_modules/.bin/tsc - -# ---------- Photon iMessage sidecar deps (baked, NS-606) ---------- -# The photon plugin's Node sidecar needs its own node_modules -# (spectrum-ts). The install tree is immutable at runtime, so a lazy -# `npm ci` on first connect would hit EROFS — bake the deps here instead -# (deterministic installs, NS-559). The patch script is copied alongside -# the manifests because package.json's postinstall runs it, which also -# means the spectrum-ts patch is applied at build time. Layer-cached: -# only re-runs when the sidecar manifests/patch change. -COPY plugins/platforms/photon/sidecar/package.json \ - plugins/platforms/photon/sidecar/package-lock.json \ - plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs \ - plugins/platforms/photon/sidecar/ -RUN cd plugins/platforms/photon/sidecar && \ - npm ci --no-audit --fetch-retries=5 && \ - npm cache clean --force - +FROM runtime_base AS python_deps # ---------- Layer-cached Python dependency install ---------- # Copy only pyproject.toml + uv.lock so the Python dep resolve + wheel # download + native-extension compile layer is cached unless those inputs @@ -320,6 +277,57 @@ RUN python3 -m pm.build_env --source /opt/hermes --python /usr/local/bin/python3 --extra all --extra messaging --extra otlp --extra anthropic --extra bedrock \ --extra azure-identity --extra hindsight --extra matrix --extra google-chat +# Icons render on the runtime environment: Pillow and resvg-py are core +# dependencies. A stage of its own so the frontend stage keeps building its +# Node dependencies in parallel with the Python ones. +FROM python_deps AS icons +COPY scripts/generate_icons.py scripts/ +COPY assets/ assets/ +RUN /opt/hermes/.venv/bin/python -I scripts/generate_icons.py --source /opt/hermes --out /tmp/hermes-icons + +# Frontend dependencies never enter the runtime layers. +FROM runtime_base AS frontend_build +COPY package.json package-lock.json ./ +COPY web/package.json web/ +COPY ui-tui/package.json ui-tui/ +COPY ui-tui/packages/hermes-ink/ ui-tui/packages/hermes-ink/ +COPY apps/shared/ apps/shared/ +COPY scripts/build/node-deps.mjs scripts/build/node-deps.mjs +ENV npm_config_install_links=false +RUN node scripts/build/node-deps.mjs --source /opt/hermes --workspace ui-tui --workspace web + +COPY pyproject.toml uv.lock ./ +COPY web/ web/ +COPY ui-tui/ ui-tui/ +COPY scripts/build/*.mjs scripts/build/ +COPY scripts/generate-icons.mjs scripts/generate_icons.py scripts/ +COPY assets/ assets/ +COPY --from=icons /tmp/hermes-icons /tmp/hermes-icons +RUN node scripts/build/tui.mjs --source /opt/hermes --out /opt/products/tui && \ + node scripts/build/web.mjs --source /opt/hermes --icons /tmp/hermes-icons --out /opt/products/web + +FROM python_deps AS runtime +# Standalone TypeScript linting is a runtime feature; Vite/esbuild are not. +COPY --from=frontend_build /opt/hermes/node_modules/typescript /opt/hermes/node_modules/typescript +RUN mkdir -p /opt/hermes/node_modules/.bin && \ + ln -s ../typescript/bin/tsc /opt/hermes/node_modules/.bin/tsc + +# ---------- Photon iMessage sidecar deps (baked, NS-606) ---------- +# The photon plugin's Node sidecar needs its own node_modules +# (spectrum-ts). The install tree is immutable at runtime, so a lazy +# `npm ci` on first connect would hit EROFS — bake the deps here instead +# (deterministic installs, NS-559). The patch script is copied alongside +# the manifests because package.json's postinstall runs it, which also +# means the spectrum-ts patch is applied at build time. Layer-cached: +# only re-runs when the sidecar manifests/patch change. +COPY plugins/platforms/photon/sidecar/package.json \ + plugins/platforms/photon/sidecar/package-lock.json \ + plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs \ + plugins/platforms/photon/sidecar/ +RUN cd plugins/platforms/photon/sidecar && \ + npm ci --no-audit --fetch-retries=5 && \ + npm cache clean --force + # Shared product outputs are independent of application dependency assembly. COPY --from=frontend_build /opt/products/tui /opt/hermes/ui-tui COPY --from=frontend_build /opt/products/web /opt/hermes/hermes_cli/web_dist diff --git a/apps/desktop/BUILDING.md b/apps/desktop/BUILDING.md index 0acccb4ea3..85aa2c8777 100644 --- a/apps/desktop/BUILDING.md +++ b/apps/desktop/BUILDING.md @@ -339,9 +339,9 @@ For an ordinary package of that desktop build, use the workspace's replace the complete tagged build described above. Icons are generated from `assets/nous-girl-*.svg` and `assets/backgrounds/`. -`node scripts/generate-icons.mjs` uses the locked, isolated `icon-build` group. -Generated PNG/ICO/ICNS files are not source assets. Build-only renderers do not -belong in production payload dependencies. +`node scripts/generate-icons.mjs` renders them with the Hermes runtime Python +(`HERMES_PYTHON`, else `python` on PATH); Pillow and resvg-py are core +dependencies. Generated PNG/ICO/ICNS files are not source assets. [Stable release admission](../../docs/stable-releases.md) requires the full pipeline, not just successful packaging. Native signed-package update tests diff --git a/apps/desktop/scripts/build.mjs b/apps/desktop/scripts/build.mjs index e3276d9b5b..ad6317b083 100644 --- a/apps/desktop/scripts/build.mjs +++ b/apps/desktop/scripts/build.mjs @@ -6,13 +6,13 @@ import { parseArgs } from 'node:util' import { generateIcons } from '../../../scripts/generate-icons.mjs' import { isMain, repoRoot } from '../../../scripts/build/frontend-common.mjs' -export function buildSourceDesktop({ source = repoRoot, icons, onDemand = false, run = execFileSync, generate = generateIcons } = {}) { +export function buildSourceDesktop({ source = repoRoot, icons, run = execFileSync, generate = generateIcons } = {}) { source = resolve(source) const app = join(source, 'apps/desktop') const step = (script, args = []) => run(process.execPath, [join(source, script), ...args], { cwd: app, stdio: 'inherit' }) step('apps/desktop/scripts/assert-root-install.mjs') if (!icons) { - if (generate(['--source', source, '--out', source, ...(onDemand ? ['--on-demand'] : [])]) !== 0) throw new Error('Icon preparation failed') + if (generate(['--source', source, '--out', source]) !== 0) throw new Error('Icon preparation failed') icons = source } icons = resolve(icons) @@ -28,6 +28,6 @@ export function buildSourceDesktop({ source = repoRoot, icons, onDemand = false, } if (isMain(import.meta.url)) { - const { values } = parseArgs({ options: { icons: { type: 'string' }, 'on-demand': { type: 'boolean' } } }) - buildSourceDesktop({ ...values, onDemand: values['on-demand'] }) + const { values } = parseArgs({ options: { icons: { type: 'string' } } }) + buildSourceDesktop(values) } diff --git a/hermes_cli/main_desktop.py b/hermes_cli/main_desktop.py index 46d6e2daa2..ba716d3e07 100644 --- a/hermes_cli/main_desktop.py +++ b/hermes_cli/main_desktop.py @@ -1177,7 +1177,7 @@ def _promote_staged_desktop_app( def build_prepared_desktop(desktop_dir: Path, *, source_mode: bool, npm: str, env: dict, - icons: Path | None = None, explicit: bool = False) -> Optional[Path]: + icons: Path | None = None) -> Optional[Path]: """Build prepared desktop sources, then publish the verified staged app.""" build_label = "source build" if source_mode else "packaged app" print(f"→ Building desktop {build_label}...") @@ -1185,7 +1185,7 @@ def build_prepared_desktop(desktop_dir: Path, *, source_mode: bool, npm: str, en if _force_adhoc_macos_signing(build_env, source_mode=source_mode): print(" → No Developer ID configured; ad-hoc signing this local rebuild " "(CSC_IDENTITY_AUTO_DISCOVERY=false)") - build_args = (["--icons", str(icons)] if icons else []) + ([] if explicit else ["--on-demand"]) + build_args = ["--icons", str(icons)] if icons else [] build_cmd = [npm, "run", "build", "--", *build_args] staging_dir = None if source_mode else _desktop_staging_dir(desktop_dir) if staging_dir is not None: @@ -1357,8 +1357,7 @@ def cmd_gui(args: argparse.Namespace): elif needs_build: prepare_source_dependencies(PROJECT_ROOT, ("ui-tui", "web", "apps/desktop"), env=build_env, explicit=force_build or getattr(args, "build_only", False)) - built = build_prepared_desktop(desktop_dir, source_mode=source_mode, npm=npm, env=build_env, - explicit=force_build or getattr(args, "build_only", False)) + built = build_prepared_desktop(desktop_dir, source_mode=source_mode, npm=npm, env=build_env) if not source_mode: packaged_executable = built else: diff --git a/hermes_cli/source_build.py b/hermes_cli/source_build.py index 08deb040a3..56585b8b3a 100644 --- a/hermes_cli/source_build.py +++ b/hermes_cli/source_build.py @@ -71,11 +71,10 @@ def build_source_tui(project_root: Path, *, env: dict) -> None: run_source_script(project_root, "scripts/build/tui.mjs", env=env) -def build_source_web(project_root: Path, *, env: dict, icons: Path | None = None, - explicit: bool = False) -> None: +def build_source_web(project_root: Path, *, env: dict, icons: Path | None = None) -> None: if icons is None: icons = project_root - run_source_script(project_root, "scripts/generate-icons.mjs", *(() if explicit else ("--on-demand",)), env=env) + run_source_script(project_root, "scripts/generate-icons.mjs", env=env) run_source_script(project_root, "scripts/build/web.mjs", "--source", str(project_root), "--icons", str(icons), "--out", str(project_root / "hermes_cli/web_dist"), env=env) @@ -107,14 +106,14 @@ def build_update_products(project_root: Path, *, desktop: bool) -> None: build_source_tui(project_root, env=env) if "web" in frontends: publish_stage("Building the web UI") - build_source_web(project_root, env=env, explicit=True) + build_source_web(project_root, env=env) if desktop: from hermes_cli.main_desktop import _install_rebuilt_desktop_app, build_prepared_desktop publish_stage("Building the desktop app") build_prepared_desktop( project_root / "apps/desktop", source_mode=False, - npm=shutil.which("npm", path=env["PATH"]), env=env, icons=project_root, explicit=True, + npm=shutil.which("npm", path=env["PATH"]), env=env, icons=project_root, ) # A current release/ can still sit beside a stale installed copy (an earlier # update rebuilt but never installed); healing must not wait for the next build. diff --git a/nix/hermes-agent.nix b/nix/hermes-agent.nix index 00b2dab093..1c15746544 100644 --- a/nix/hermes-agent.nix +++ b/nix/hermes-agent.nix @@ -92,8 +92,9 @@ let inherit uv2nix pyproject-nix pyproject-build-systems; }; + # Icons render on the runtime venv: Pillow and resvg-py are core dependencies. generatedIcons = callPackage ./icons.nix { - inherit (mkHermesVenv [ ]) iconBuildVenv; + inherit (mkHermesVenv [ ]) venv; }; hermesNpmLib = callPackage ./lib.nix { diff --git a/nix/icons.nix b/nix/icons.nix index 90f9e5cfe2..2016f33ef5 100644 --- a/nix/icons.nix +++ b/nix/icons.nix @@ -1,5 +1,5 @@ # Run the shared generator offline; generated assets are not tracked in git. -{ lib, runCommand, iconBuildVenv }: +{ lib, runCommand, venv }: let src = lib.fileset.toSource { root = ./..; @@ -9,7 +9,7 @@ let ]; }; in -runCommand "hermes-icons" { nativeBuildInputs = [ iconBuildVenv ]; } '' +runCommand "hermes-icons" { nativeBuildInputs = [ venv ]; } '' python ${src}/scripts/generate_icons.py --source ${src} --out $out python ${src}/scripts/generate_icons.py --source ${src} --out $out --check '' diff --git a/nix/python.nix b/nix/python.nix index 48ed4de579..47bd301ae1 100644 --- a/nix/python.nix +++ b/nix/python.nix @@ -165,11 +165,6 @@ in { inherit python; - # Equivalent to uv's --only-group: use the lock-derived dependency spec, - # without installing Hermes or its runtime dependencies in the build env. - iconBuildVenv = pythonSet.mkVirtualEnv "hermes-icon-build-env" - pythonSet.hermes-agent.dependency-groups.icon-build; - venv = pythonSet.mkVirtualEnv "hermes-agent-env" { hermes-agent = dependency-groups; }; diff --git a/pm/build_env.py b/pm/build_env.py index 65c41da0e4..4c5a071c3f 100644 --- a/pm/build_env.py +++ b/pm/build_env.py @@ -17,7 +17,6 @@ def main(argv: Sequence[str] | None = None) -> int: parser.add_argument("--cache", type=Path) parser.add_argument("--extra", dest="extras", action="append", default=[]) parser.add_argument("--group", dest="groups", action="append", default=[]) - parser.add_argument("--only-groups", action="store_true", help="install selected groups without the application") parser.add_argument("--all-extras", action="store_true") parser.add_argument("--no-install-project", action="store_true") parser.add_argument("--resolve", action="store_true", help="resolve the source lock before building") @@ -96,7 +95,7 @@ def main(argv: Sequence[str] | None = None) -> int: else: executable = pm.build_environment( source=args.source, out=args.out, python=args.python, cache=args.cache, - extras=args.extras, groups=args.groups, only_groups=args.only_groups, all_extras=args.all_extras, + extras=args.extras, groups=args.groups, all_extras=args.all_extras, no_install_project=args.no_install_project, frozen=not args.resolve, sealed=args.sealed, offline=args.offline, explicit=True, ) diff --git a/pm/client.py b/pm/client.py index 8f8ef1f307..c1c95c4d6d 100644 --- a/pm/client.py +++ b/pm/client.py @@ -238,7 +238,7 @@ def _python_operation(operation: str, arguments: dict): def build_environment( *, source: Path, out: Path, python: Path | None = None, cache: Path | None = None, env: Mapping[str, str] | None = None, - extras: Sequence[str] = (), groups: Sequence[str] = (), only_groups: bool = False, + extras: Sequence[str] = (), groups: Sequence[str] = (), all_extras: bool = False, no_install_project: bool = False, frozen: bool = True, sealed: bool = False, offline: bool = False, explicit: bool = False, timeout: int = 1800, @@ -246,7 +246,7 @@ def build_environment( """Build a validated Python environment without exposing install machinery.""" return Path(_python_operation("build_environment", { "source": Path(source), "out": Path(out), "python": python, "cache": cache, - "env": dict(env) if env is not None else None, "extras": list(extras), "groups": list(groups), "only_groups": only_groups, + "env": dict(env) if env is not None else None, "extras": list(extras), "groups": list(groups), "all_extras": all_extras, "no_install_project": no_install_project, "frozen": frozen, "sealed": sealed, "offline": offline, "explicit": explicit, "timeout": timeout, diff --git a/pm/environment.py b/pm/environment.py index 735b3e73cc..7627760295 100644 --- a/pm/environment.py +++ b/pm/environment.py @@ -306,14 +306,12 @@ class PythonEnvironment: def sync(self, source: Path, *, extras: Sequence[str] = (), groups: Sequence[str] = (), timeout: int = 1800, frozen: bool = True, all_extras: bool = False, no_install_project: bool = False, locked: bool = False, - no_default_groups: bool = False, only_groups: bool = False) -> None: + no_default_groups: bool = False) -> None: """Install the root and every member; resolve only in a writable workspace. ``frozen=False`` is reserved for the caller-owned generated workspace, never the original project's lock. Seed/replay policy belongs to PM. """ - if only_groups and (not groups or extras or all_extras): - raise ValueError("group-only builds require groups and cannot select extras") if not frozen: self.lock(source, timeout=timeout) # Locking members alone is insufficient: plain sync only installs root deps. @@ -333,7 +331,7 @@ class PythonEnvironment: for extra in sorted(set(extras)): command += ["--extra", extra] for group in sorted(set(groups)): - command += ["--only-group" if only_groups else "--group", group] + command += ["--group", group] result = self._run(command, cwd=source, timeout=timeout) if result.returncode: raise classify_uv_failure("sync", result.returncode, result.stderr or result.stdout) diff --git a/pm/operations.py b/pm/operations.py index 14c4a5fdd0..e26cba4742 100644 --- a/pm/operations.py +++ b/pm/operations.py @@ -28,7 +28,7 @@ def _require_install_allowed(explicit: bool) -> None: def build_environment( *, source: Path, out: Path, python: Path | None = None, cache: Path | None = None, env: Mapping[str, str] | None = None, - extras: Sequence[str] = (), groups: Sequence[str] = (), only_groups: bool = False, + extras: Sequence[str] = (), groups: Sequence[str] = (), all_extras: bool = False, no_install_project: bool = False, frozen: bool = True, sealed: bool = False, offline: bool = False, explicit: bool = False, timeout: int = 1800, @@ -60,7 +60,7 @@ def build_environment( offline=offline, explicit=explicit, output=sys.stderr, ) with _fresh_build(environment, sealed=sealed): - environment.sync(source, extras=extras, groups=groups, only_groups=only_groups, all_extras=all_extras, + environment.sync(source, extras=extras, groups=groups, all_extras=all_extras, no_install_project=no_install_project, frozen=frozen, timeout=timeout) return environment.executable diff --git a/pyproject.toml b/pyproject.toml index 13ec01144e..e3cca6374f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -172,6 +172,12 @@ dependencies = [ # Ships prebuilt wheels bundling libheif for the common platforms (no system # libs needed), so it's safe in the base install alongside Pillow. "pillow-heif>=1.4.0,<2; python_version >= '3.14'", + # SVG rendering for the icon generator (scripts/generate_icons.py). Source + # builds render icons with the runtime interpreter (`hermes desktop`, + # `hermes update`), so a source install carries its renderer instead of + # preparing a separate environment at launch. abi3 wheels for every + # shipped platform, including Windows ARM64 and Android. + "resvg-py==0.4.0; python_version >= '3.14'", # Windows log rotation. Stdlib ``RotatingFileHandler.doRollover()`` uses # ``os.rename()`` which fails with ``PermissionError [WinError 32]`` on # Windows whenever any other process holds an append-mode handle on @@ -533,8 +539,6 @@ dev = [ "ruff==0.15.10", "setuptools==83.0.0", ] # starlette: CVE-2026-48710; setuptools: 83 (torch >=2.13 requires setuptools 83) -# --only-group supplies Pillow without installing the Hermes application. -icon-build = ["Pillow==12.3.0", "resvg-py==0.4.0"] # Native launcher acceptance is required in test environments, not payloads. test = ["distlib==0.4.3; sys_platform == 'win32'"] diff --git a/scripts/build/README.md b/scripts/build/README.md index 189274ce92..eebdb223c2 100644 --- a/scripts/build/README.md +++ b/scripts/build/README.md @@ -169,7 +169,8 @@ The web builder reads `web/public/` beneath it and requires `favicon.ico`. The desktop builder reads `apps/desktop/public/` beneath it and requires `apple-touch-icon.png`. -Run the generator with a prepared `icon-build` Python environment: +Run the generator with a Hermes runtime Python (Pillow and resvg-py are core +dependencies): ```sh python scripts/generate_icons.py --source /work/source --out /work/products/icons @@ -183,18 +184,16 @@ output image properties. It does not compare output bytes with regenerated bytes The generator writes targets directly, not through the frontend publication helper. -The convenience wrapper prepares the locked environment and can access the -network: +The convenience wrapper runs the same generator on `HERMES_PYTHON` (else +`python` on PATH) and never installs anything: ```sh node scripts/generate-icons.mjs --source /work/source --out /work/products/icons ``` -It uses the isolated `icon-build` dependency group and -`SOURCE/.cache/icon-build`. Both icon commands accept `--check`. The convenience -wrapper also accepts `--on-demand`, which preserves PM's lazy-install admission -policy rather than treating an automatic stale build as explicit installation. Without -explicit paths, they use the source checkout as the output root. +Both icon commands accept `--check`. Without explicit paths, they use the +source checkout as the output root. Builders without a runtime environment +prepare one with `scripts/build/icon_environment.py`. The desktop native tree contains prepared packages, including `node-pty` with its compiled binding. macOS also requires `get-windows/main`. The provider owns diff --git a/scripts/build/icon_environment.py b/scripts/build/icon_environment.py index 8e0af84c38..44fa806869 100644 --- a/scripts/build/icon_environment.py +++ b/scripts/build/icon_environment.py @@ -1,46 +1,24 @@ -"""Prepare the locked icon-only environment through PM, then run the generator.""" +"""Prepare an interpreter that can run the icon generator without a Hermes install. + +Pillow and resvg-py are core runtime dependencies, so builders that have no +Hermes runtime environment (desktop bundles, product staging) render icons on +the locked runtime dependencies alone, without installing the application. +""" from __future__ import annotations -import argparse from pathlib import Path -import subprocess import sys -from tempfile import TemporaryDirectory -# The Node wrapper can run this file from a separately prepared source tree. +# Builders import this file from a separately prepared source tree. ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) import pm -def prepare_icon_environment(source: Path, out: Path, cache: Path, *, explicit: bool = True) -> Path: - """Prepare a fresh locked build-only interpreter without generating assets.""" +def prepare_icon_environment(source: Path, out: Path, cache: Path | None) -> Path: + """Prepare a fresh interpreter with the locked runtime dependencies only.""" return pm.build_environment( - source=source.resolve(), out=out.resolve(), cache=cache.resolve(), - groups=["icon-build"], only_groups=True, explicit=explicit, + source=source.resolve(), out=out.resolve(), cache=cache.resolve() if cache else None, + no_install_project=True, explicit=True, ) - - -def main(argv: list[str] | None = None) -> int: - argv = list(sys.argv[1:] if argv is None else argv) - parser = argparse.ArgumentParser(add_help=False, allow_abbrev=False) - parser.add_argument("--source", type=Path, default=ROOT) - parser.add_argument("--on-demand", action="store_true") - args, _ = parser.parse_known_args(argv) - if args.on_demand: - argv.remove("--on-demand") - source = args.source.resolve() - with TemporaryDirectory(prefix="hermes-icon-build-") as temporary: - python = prepare_icon_environment( - source, Path(temporary) / "venv", source / ".cache/icon-build", - explicit=not args.on_demand, - ) - return subprocess.run( - [str(python), "-I", str(ROOT / "scripts/generate_icons.py"), *argv], - cwd=source, - ).returncode - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/bundles/stage.py b/scripts/bundles/stage.py index a552abaa85..229b68095c 100644 --- a/scripts/bundles/stage.py +++ b/scripts/bundles/stage.py @@ -2,6 +2,7 @@ from __future__ import annotations import argparse +import os import shutil import subprocess import sys @@ -37,7 +38,11 @@ def main(argv=None) -> int: products = Path(temp) source = products / "source" from scripts.bundles.payload import snapshot + from scripts.build.icon_environment import prepare_icon_environment snapshot(ROOT, args.ref, source) + # The staging interpreter need not be a Hermes runtime; render icons on one. + icon_python = prepare_icon_environment(source, products / "icon-environment", args.cache) + env = {**os.environ, "HERMES_PYTHON": str(icon_python)} commands = [ ["scripts/build/node-deps.mjs", "--source", str(source), "--workspace", "ui-tui", "--workspace", "web"], ["scripts/generate-icons.mjs", "--source", str(source), "--out", str(products / "icons")], @@ -45,7 +50,7 @@ def main(argv=None) -> int: ["scripts/build/web.mjs", "--source", str(source), "--icons", str(products / "icons"), "--out", str(products / "web")], ] for command in commands: - subprocess.run([node, *command], cwd=ROOT, check=True) + subprocess.run([node, *command], cwd=ROOT, env=env, check=True) args.frontends = {"tui": products / "tui", "web": products / "web"} return stage_native(args) diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs index e578a11673..ab99222080 100644 --- a/scripts/generate-icons.mjs +++ b/scripts/generate-icons.mjs @@ -9,8 +9,9 @@ * - installer: apps/bootstrap-installer/package.json prebuild * - web: web/package.json prebuild * - * The locked icon-build group runs outside the application environment. - * It is not a runtime extra, so --all-extras payloads do not include resvg. + * The renderer runs on the Hermes runtime interpreter (HERMES_PYTHON, else + * `python` on PATH): Pillow and resvg-py are core dependencies, so every + * runtime environment can draw its own icons. */ import { spawnSync } from 'node:child_process' import path from 'node:path' @@ -22,21 +23,20 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..' export function generateIcons(args = [], { root = repoRoot, run = spawnSync, env = process.env } = {}) { const { values } = parseArgs({ args, options: { source: { type: 'string' }, out: { type: 'string' }, check: { type: 'boolean' }, - 'on-demand': { type: 'boolean' }, } }) const source = path.resolve(values.source ?? root) const out = path.resolve(values.out ?? source) const childEnv = { ...env } - // Parent payload paths must not shadow the isolated build dependencies. + // Parent payload paths must not shadow the runtime interpreter's own packages. delete childEnv.PYTHONPATH delete childEnv.PYTHONHOME const result = run(env.HERMES_PYTHON || 'python', [ - path.join(root, 'scripts', 'build', 'icon_environment.py'), '--source', source, '--out', out, - ...(values.check ? ['--check'] : []), ...(values['on-demand'] ? ['--on-demand'] : []) + '-I', path.join(root, 'scripts', 'generate_icons.py'), '--source', source, '--out', out, + ...(values.check ? ['--check'] : []) ], { cwd: source, stdio: 'inherit', windowsHide: true, env: childEnv }) if (result.error) { console.error('[generate-icons] failed to launch icon generator:', result.error.message) - console.error('[generate-icons] a prepared Python (HERMES_PYTHON or PATH) is required to run the PM build driver') + console.error('[generate-icons] a Hermes runtime Python (HERMES_PYTHON or PATH) is required') return 1 } return result.status ?? 1 diff --git a/scripts/generate_icons.py b/scripts/generate_icons.py index c79ed06f74..ec3e8e8ebb 100644 --- a/scripts/generate_icons.py +++ b/scripts/generate_icons.py @@ -47,9 +47,9 @@ byte-compare against. Rendering: resvg (resvg-py) for SVG -> PNG fidelity at every size. Containers: Pillow for multi-size .ico and .icns. -Build dependencies: - The runner uses the locked icon-build group in an isolated environment. - resvg-py is not a runtime extra and does not enter production payloads. +Dependencies: + Pillow and resvg-py are core runtime dependencies; run this file with a + Hermes runtime interpreter (scripts/generate-icons.mjs uses HERMES_PYTHON). Outputs (30 files): assets/icon-master.svg generated light master @@ -102,13 +102,13 @@ try: import resvg_py except ImportError: sys.exit( - "resvg-py is build-only. Run the isolated generator with:\n" - " node scripts/generate-icons.mjs" + "resvg-py is missing: run the generator with a Hermes runtime interpreter\n" + " (HERMES_PYTHON= node scripts/generate-icons.mjs)" ) -# Copy of hermes_cli.update_channel._CANARY_TAG_RE: this renderer runs in an -# isolated icon-build venv (Nix, Docker, PM) where the application package is -# absent. tests/scripts/test_icon_flavors.py pins it to the canonical one. +# Copy of hermes_cli.update_channel._CANARY_TAG_RE: builders run this renderer +# on the runtime dependencies without the application package installed +# (Docker, bundles). tests/scripts/test_icon_flavors.py pins it to the canonical one. _CANARY_TAG_RE = re.compile( r"^v(?:0|[1-9]\d{0,2})\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)" r"\+canary\.20\d{6}T\d{6}Z$" diff --git a/tests/docker/test_image_payload.py b/tests/docker/test_image_payload.py index 24cf846f59..bcaafdd08d 100644 --- a/tests/docker/test_image_payload.py +++ b/tests/docker/test_image_payload.py @@ -31,14 +31,11 @@ print('PM interpreter and application dependencies load as hermes') def test_dashboard_ships_generated_icon_without_build_environment(built_image: str) -> None: probe = """ from pathlib import Path -import importlib.util from PIL import Image with Image.open('/opt/hermes/hermes_cli/web_dist/favicon.ico') as image: image.load() assert image.width > 0 and image.height > 0 -assert importlib.util.find_spec('resvg_py') is None -assert not Path('/opt/hermes/.cache/icon-build').exists() assert not Path('/opt/hermes/node_modules/vite').exists() assert not Path('/opt/hermes/node_modules/esbuild').exists() assert Path('/opt/hermes/node_modules/typescript/bin/tsc').is_file() diff --git a/tests/pm/test_environment_build.py b/tests/pm/test_environment_build.py index 6b9ab6c999..9f8e489460 100644 --- a/tests/pm/test_environment_build.py +++ b/tests/pm/test_environment_build.py @@ -188,21 +188,6 @@ def test_public_dependency_only_build_needs_no_application_source(installable_pr assert not Path(env["HERMES_HOME"]).exists() -def test_group_only_build_excludes_application_dependencies(locked_project, tmp_path, build_worker): - import pm - - source, _, env = locked_project - metadata = source / "pyproject.toml" - metadata.write_text(metadata.read_text() + '\n[dependency-groups]\nicons=["chosen-dep==1.0"]\n') - pm.lock_project(source, python=Path(sys.executable), cache=tmp_path / "cache", env=env, - offline=True, explicit=True) - python = pm.build_environment(source=source, out=tmp_path / "icons", groups=["icons"], - only_groups=True, python=Path(sys.executable), cache=tmp_path / "cache", - env=env, offline=True, explicit=True) - assert _run([str(python), "-I", "-c", "import chosen_dep, importlib.util; " - "assert importlib.util.find_spec('base_dep') is None; print(chosen_dep.__version__)"], - cwd=tmp_path, env=env) == "1.0" - @pytest.mark.parametrize("lazy", [False, True]) def test_first_bundle_extension_preserves_shipped_extras(locked_project, build_worker, tmp_path, monkeypatch, lazy): diff --git a/tests/scripts/test_desktop_toolchain.py b/tests/scripts/test_desktop_toolchain.py index 3fb12ece19..0132d7d0a0 100644 --- a/tests/scripts/test_desktop_toolchain.py +++ b/tests/scripts/test_desktop_toolchain.py @@ -295,45 +295,3 @@ def test_native_cache_leaves_room_for_sdist_compiler_outputs(tmp_path, monkeypat / f"build/temp.{architecture}-cpython-314/Release/src/SKP_SILK_SRC" / "SKP_Silk_NLSF_VQ_rate_distortion_FIX.obj") assert len(str(output)) < 260, str(output) - - -@pytest.mark.parametrize("on_demand", [False, True]) -def test_icon_environment_prepares_locked_group_before_generation(tmp_path, monkeypatch, on_demand): - import pm - from scripts.build import icon_environment - - source, out, cache = (tmp_path / name for name in ("source", "icons-venv", "build-cache")) - python = out / "bin/python" - acquired = [] - - def build(**kwargs): - acquired.append(kwargs) - return python - - monkeypatch.setattr(pm, "build_environment", build) - assert icon_environment.prepare_icon_environment(source, out, cache) == python - assert acquired == [{"source": source, "out": out, "cache": cache, - "groups": ["icon-build"], "only_groups": True, "explicit": True}] - calls = [] - - def prepare(source, out, cache, *, explicit=True): - calls.append((source, out, cache, explicit)) - return python - - launched = [] - - def run(command, *, cwd): - launched.append(command) - assert calls and cwd == source - return SimpleNamespace(returncode=9) - - monkeypatch.setattr(icon_environment, "prepare_icon_environment", prepare) - monkeypatch.setattr(icon_environment.subprocess, "run", run) - args = ["--source", str(source), "--out", str(tmp_path / "icons")] - if on_demand: - args.append("--on-demand") - assert icon_environment.main(args) == 9 - assert calls[0][0] == source and calls[0][2] == source / ".cache/icon-build" - assert calls[0][3] is not on_demand - assert launched[0][:2] == [str(python), "-I"] - assert "--on-demand" not in launched[0] diff --git a/tests/scripts/test_icon_build_dependencies.py b/tests/scripts/test_icon_build_dependencies.py deleted file mode 100644 index de8de28a01..0000000000 --- a/tests/scripts/test_icon_build_dependencies.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Icon tooling must not join the runtime's all-extras dependency set.""" -import re -import shutil -import subprocess -from pathlib import Path - - -def test_icon_group_is_separate_from_runtime_extras(): - root = Path(__file__).resolve().parents[2] - uv = shutil.which("uv") - assert uv, "uv is required for the dependency-selection contract" - - def packages(*selection): - result = subprocess.run( - [uv, "export", "--frozen", "--offline", "--no-hashes", "--no-annotate", - "--no-header", "--no-emit-project", *selection], - cwd=root, capture_output=True, text=True, encoding="utf-8", check=True, - ) - return set(re.findall(r"^([a-z0-9-]+)==", result.stdout, re.MULTILINE)) - - assert "resvg-py" not in packages("--all-extras") - assert packages("--only-group", "icon-build") == {"pillow", "resvg-py"} diff --git a/tests/scripts/test_icon_flavors.py b/tests/scripts/test_icon_flavors.py index e3ce61449d..92de6e61ee 100644 --- a/tests/scripts/test_icon_flavors.py +++ b/tests/scripts/test_icon_flavors.py @@ -1,12 +1,9 @@ -"""Build native icons with PM's locked renderer. Measure pixels, not SVG text.""" +"""Build native icons on the runtime interpreter. Measure pixels, not SVG text.""" import colorsys import io import itertools import os -import json import shutil -import tomllib -import zipfile from pathlib import Path import struct import subprocess @@ -28,40 +25,30 @@ def generate(tmp_path_factory): foreign.mkdir() (foreign / "sitecustomize.py").write_text("raise SystemExit('foreign interpreter path leaked')\n", encoding="utf-8") shutil.copytree(ROOT / "assets", source / "assets") - # A real app-only wheel makes --only-group load-bearing: startup dies if - # the driver accidentally includes application dependencies in the renderer. - from tests.pm._fixtures import _wheel - wheel = _wheel(source, "application_only") - with zipfile.ZipFile(wheel, "a") as archive: - archive.writestr("application_only.pth", "import sys; sys.exit('application dependency leaked into icon renderer')\n") - group = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8-sig"))["dependency-groups"]["icon-build"] - (source / "pyproject.toml").write_text( - '[project]\nname="icon-fixture"\nversion="1"\nrequires-python=">=3.11"\n' - 'dependencies=["application-only==1.0"]\n[dependency-groups]\nicon-build=' + json.dumps(group) + '\n' - '[tool.uv]\npackage=false\n[tool.uv.sources]\napplication-only={path=' + json.dumps(wheel.as_posix()) + '}\n', encoding="utf-8") - uv, node = shutil.which("uv"), shutil.which("node") - assert uv and node, "icon acceptance requires prepared uv and Node" - subprocess.run([uv, "lock", "--python", sys.executable], cwd=source, check=True, capture_output=True, timeout=60) + from scripts.build.icon_environment import prepare_icon_environment + python = prepare_icon_environment(ROOT, root / "runtime", root / "cache") + node = shutil.which("node") + assert node, "icon acceptance requires prepared Node" outputs = {} sequence = itertools.count() - def build(tag="", commit="", *, rejected=False, on_demand=False): - key = (tag, commit, on_demand) + def build(tag="", commit="", *, rejected=False): + key = (tag, commit) if key not in outputs: out = root / str(next(sequence)) + # The runtime interpreter renders with its own packages: foreign + # interpreter paths must not leak in, and nothing may be installed. env = {**os.environ, "HERMES_HOME": str(root / "home"), "HERMES_RUNTIME_DIR": str(root / "tools"), "HERMES_PAYLOAD_TAG": tag, "HERMES_BUILD_COMMIT": commit, - "HERMES_PYTHON": sys.executable, "PYTHONPATH": str(root / "foreign-site"), + "HERMES_PYTHON": str(python), "PYTHONPATH": str(root / "foreign-site"), "PYTHONHOME": str(root / "foreign-python"), "HERMES_DISABLE_LAZY_INSTALLS": "1"} command = [node, str(ROOT / "scripts/generate-icons.mjs"), - "--source", str(source), "--out", str(out), *(["--on-demand"] if on_demand else [])] + "--source", str(source), "--out", str(out)] result = subprocess.run(command, env=env, capture_output=True, text=True, timeout=180) if rejected: assert result.returncode != 0, "invalid build identity generated icons" assert not out.exists() - if on_demand: - assert "disabled" in (result.stdout + result.stderr).lower() return assert result.returncode == 0, result.stdout + result.stderr if not outputs: @@ -73,10 +60,6 @@ def generate(tmp_path_factory): return build -def test_on_demand_build_obeys_disabled_lazy_install_admission(generate): - generate(on_demand=True, rejected=True) - - def frames(path): """Read every native frame, including ICO entries Pillow's n_frames misses.""" data = path.read_bytes() diff --git a/uv.lock b/uv.lock index 9796699410..cce08fea93 100644 --- a/uv.lock +++ b/uv.lock @@ -2040,6 +2040,7 @@ dependencies = [ { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "pywinpty", marker = "sys_platform == 'win32'" }, { name = "requests" }, + { name = "resvg-py" }, { name = "rich" }, { name = "ruamel-yaml" }, { name = "snowballstemmer" }, @@ -2314,10 +2315,6 @@ dev = [ { name = "starlette" }, { name = "ty" }, ] -icon-build = [ - { name = "pillow" }, - { name = "resvg-py" }, -] test = [ { name = "distlib", marker = "sys_platform == 'win32'" }, ] @@ -2448,6 +2445,7 @@ requires-dist = [ { name = "qrcode", marker = "extra == 'feishu'", specifier = "==7.4.2" }, { name = "qrcode", marker = "extra == 'messaging'", specifier = "==7.4.2" }, { name = "requests", marker = "python_full_version >= '3.14'", specifier = "==2.33.0" }, + { name = "resvg-py", marker = "python_full_version >= '3.14'", specifier = "==0.4.0" }, { name = "rich", marker = "python_full_version >= '3.14'", specifier = "==14.3.3" }, { name = "ruamel-yaml", marker = "python_full_version >= '3.14'", specifier = "==0.18.16" }, { name = "sentencepiece", marker = "extra == 'wake'", specifier = "==0.2.2" }, @@ -2501,10 +2499,6 @@ dev = [ { name = "starlette", specifier = "==1.3.1" }, { name = "ty", specifier = "==0.0.82" }, ] -icon-build = [ - { name = "pillow", specifier = "==12.3.0" }, - { name = "resvg-py", specifier = "==0.4.0" }, -] test = [{ name = "distlib", marker = "sys_platform == 'win32'", specifier = "==0.4.3" }] [[package]] diff --git a/website/docs/developer-guide/contributing.md b/website/docs/developer-guide/contributing.md index 6a556e4d41..ce3bff3cdf 100644 --- a/website/docs/developer-guide/contributing.md +++ b/website/docs/developer-guide/contributing.md @@ -118,9 +118,9 @@ Use a Node/npm version accepted by the corresponding `package.json` engines. Native desktop dependencies can also require the platform build toolchain. Logos and icons are generated from `assets/nous-girl-*.svg` and -`assets/backgrounds/`. `node scripts/generate-icons.mjs` uses the locked, -isolated `icon-build` dependency group. Do not commit generated PNG/ICO/ICNS -outputs or add icon renderers to production dependencies. +`assets/backgrounds/`. `node scripts/generate-icons.mjs` renders them with the +Hermes runtime Python (`HERMES_PYTHON`, else `python` on PATH): Pillow and +resvg-py are core dependencies. Do not commit generated PNG/ICO/ICNS outputs. ### Run tests diff --git a/website/docs/developer-guide/shared-bundle-builds.md b/website/docs/developer-guide/shared-bundle-builds.md index 6253ebae64..64033875c2 100644 --- a/website/docs/developer-guide/shared-bundle-builds.md +++ b/website/docs/developer-guide/shared-bundle-builds.md @@ -21,7 +21,7 @@ bytes, dependency selections, or Python environments across targets. | Product | Implementation | Inputs | |---|---|---| -| Icons | `scripts/generate_icons.py` | Artwork and prepared `icon-build` environment | +| Icons | `scripts/generate_icons.py` | Artwork and a Python with the runtime dependencies | | TUI | `scripts/build/tui.mjs` | Prepared TUI workspace | | Dashboard | `scripts/build/web.mjs` | Prepared web workspace and generated icons | | Desktop UI | `scripts/build/desktop.mjs` | Prepared desktop workspace, icons, stamp, and native bindings | @@ -326,9 +326,10 @@ consumers. They are not interchangeable cleanup targets. desktop preparation uses its own input snapshot namespace. Plain `uv cache prune` removes dangling entries without discarding offline wheel inputs. It does not remove all historical versions or enforce a size cap. -- Standalone icon preparation uses `SOURCE/.cache/icon-build`. Desktop - preparation places its icon environment under the job workdir and its wheel - cache under `CACHE/python/build`. Neither enters the shipped runtime. +- Source builds render icons on their runtime interpreter. Desktop preparation + and product staging, which have none, prepare the locked runtime dependencies + (without the application) under the job workdir; desktop's wheel cache is + `CACHE/python/build`. That environment does not enter the shipped runtime. - Frontend `node_modules` is a provider input, not a frontend product. Docker's runtime TypeScript and Photon selections are separate exceptions. diff --git a/website/docs/reference/package-management.md b/website/docs/reference/package-management.md index f471ce7825..926628e5ef 100644 --- a/website/docs/reference/package-management.md +++ b/website/docs/reference/package-management.md @@ -463,7 +463,6 @@ Use the public `pm` module for Python dependency work: | `pm.sync_venv(extras, explicit=True)` | Prepare and select the complete application dependency union, including enabled plugins. | | `pm.sync_venv(repair=True, explicit=True)` | Replay the recorded dependency set in a new application generation. | | `pm.build_environment(source=..., out=..., explicit=True)` | Build and validate a fresh caller-owned output. No plugin discovery or application selection. | -| `pm.build_environment(source=..., out=..., groups=[...], only_groups=True, explicit=True)` | Build only the selected locked dependency groups, without application dependencies. Used by icon builds. | | `pm.lock_project(source, explicit=True)` | Refresh an explicit project's lock without selecting an environment. | | `pm.ensure_environment(name, requirements, explicit=True)` | Prepare and select an isolated dependency generation. Return its Python path. | | `pm.ensure_python_tool(name, requirements, executable, explicit=True)` | Prepare an isolated tool and return its executable path. | diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/contributing.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/contributing.md index c3a78cdb81..6e43a7c3a5 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/contributing.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/contributing.md @@ -91,7 +91,7 @@ npm run build:fast --prefix website ``` 图标从 `assets/nous-girl-*.svg` 和 `assets/backgrounds/` 生成。 -`node scripts/generate-icons.mjs` 使用隔离的 `icon-build` 依赖组,不应将这些构建依赖加入生产包。 +`node scripts/generate-icons.mjs` 使用 Hermes 运行时 Python(`HERMES_PYTHON`,否则为 PATH 上的 `python`)渲染图标:Pillow 和 resvg-py 是核心依赖。不要提交生成的 PNG/ICO/ICNS 文件。 ### 运行测试 diff --git a/website/scripts/prebuild.mjs b/website/scripts/prebuild.mjs index baa0538a35..0c3c2ca683 100644 --- a/website/scripts/prebuild.mjs +++ b/website/scripts/prebuild.mjs @@ -132,7 +132,7 @@ console.log("[prebuild] generating icon assets…"); { const r = spawnSync("node", [iconGenScript, "--source", repoRoot, "--out", repoRoot], { stdio: "inherit", cwd: repoRoot }); if (r.status !== 0) { - console.error("[prebuild] icon generation failed — see the isolated icon-build runner output above"); + console.error("[prebuild] icon generation failed — it needs a Hermes runtime Python (HERMES_PYTHON); see the output above"); process.exit(1); } }