fix(desktop): render icons with core runtime dependencies
This commit is contained in:
6
.github/workflows/deploy-site.yml
vendored
6
.github/workflows/deploy-site.yml
vendored
@@ -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: |
|
||||
|
||||
9
.github/workflows/e2e-desktop-core.yml
vendored
9
.github/workflows/e2e-desktop-core.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/icons-freshness-check.yml
vendored
2
.github/workflows/icons-freshness-check.yml
vendored
@@ -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
|
||||
|
||||
|
||||
4
.github/workflows/rust-tests.yml
vendored
4
.github/workflows/rust-tests.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
96
Dockerfile
96
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
''
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'"]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=<hermes venv 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$"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"}
|
||||
@@ -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()
|
||||
|
||||
10
uv.lock
generated
10
uv.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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 文件。
|
||||
|
||||
### 运行测试
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user