Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,15 @@ Full build pipeline (PowerShell):
.\build.ps1
```

For packaged OpenClaw resource acquisition, `build.ps1` defaults to the Huawei npm
registry (`https://repo.huaweicloud.com/repository/npm/`), independently of the
machine's ordinary npm registry. Explicit `MICROCLAW_NPM_REGISTRY` overrides take
precedence, followed by `MSIX_NPM_REGISTRY`. This setting is scoped to resource
preparation and restored on success or failure; ordinary dependency installs and
user npm configuration are unchanged. The build logs only the registry origin,
without credentials, paths, or query parameters. Pinned version, engine, and resource
validation remain required.

This script sequentially:

1. Builds the desktop app (`desktop/` → Electron Builder)
Expand Down
7 changes: 7 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,13 @@ Gateway 原生工具、插件、频道、Cron、Hooks 和 Canvas 在两种模式
.\build.ps1
```

`build.ps1` 准备内置 OpenClaw 资源时默认使用华为 npm 源
(`https://repo.huaweicloud.com/repository/npm/`),不受机器普通 npm 源配置影响。
如需覆盖,优先使用显式设置的 `MICROCLAW_NPM_REGISTRY`,其次为 `MSIX_NPM_REGISTRY`。
该设置仅作用于资源准备,成功或失败后均恢复;普通依赖安装与用户 npm 配置不变。
构建日志只显示源的协议、主机和端口,不输出凭据、路径或查询参数。
固定版本、Node 引擎和资源完整性校验仍会执行。

该脚本依次执行:

1. 构建桌面应用(`desktop/` → Electron Builder)
Expand Down
16 changes: 16 additions & 0 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,30 @@ $nativeCache = Get-BuildCache -Stage native -Context $nativeContext `
Start-BuildStage 'Step 1/7: prepare packaged resources'
if (-not $nativeCache.hit) {
Push-Location "$root\desktop"
$previousResourceRegistry = $env:MICROCLAW_NPM_REGISTRY
try {
if (-not $env:MICROCLAW_NPM_REGISTRY) {
$env:MICROCLAW_NPM_REGISTRY = if ($env:MSIX_NPM_REGISTRY) {
$env:MSIX_NPM_REGISTRY
} else {
'https://repo.huaweicloud.com/repository/npm/'
}
}
$resourceRegistryUri = $null
if (-not [Uri]::TryCreate($env:MICROCLAW_NPM_REGISTRY, [UriKind]::Absolute, [ref]$resourceRegistryUri) -or
$resourceRegistryUri.Scheme -notin @('http', 'https') -or -not $resourceRegistryUri.Host) {
throw 'OpenClaw resource npm registry must be an absolute HTTP(S) URL'
}
Write-Host (" OpenClaw resource npm registry: {0}://{1}" -f `
$resourceRegistryUri.Scheme, $resourceRegistryUri.Authority)
npm run prepare-packaged-resources
if ($LASTEXITCODE -ne 0) {
Write-Host " ERROR: packaged runtime resource preparation failed" -ForegroundColor Red
Write-BuildTimingSummary -Status failed
exit 1
}
} finally {
$env:MICROCLAW_NPM_REGISTRY = $previousResourceRegistry
Pop-Location
}
Start-BuildStage 'Step 1/7: save native cache'
Expand Down
179 changes: 179 additions & 0 deletions tests/test_build_resource_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import json
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
POWERSHELL = shutil.which("pwsh") or shutil.which("powershell")
DEFAULT_REGISTRY = "https://repo.huaweicloud.com/repository/npm/"
GENERAL_REGISTRY = "https://packagefeedproxy.microsoft.io/npm/"
REGISTRY_KEYS = ("MICROCLAW_NPM_REGISTRY", "MSIX_NPM_REGISTRY", "NPM_CONFIG_REGISTRY")


@unittest.skipUnless(POWERSHELL, "Requires PowerShell")
class BuildResourceRegistryTests(unittest.TestCase):
def run_stage(self, *, microclaw=None, msix=None, failure=None, cache_hit=False):
source = (ROOT / "build.ps1").read_text(encoding="utf-8-sig")
dependencies = source[
source.index("Start-BuildStage 'Preflight: desktop dependencies'") : source.index(
"# -- Step 1:"
)
]
resources = source[
source.index("Start-BuildStage 'Step 1/7: prepare packaged resources'") : source.index(
"Start-BuildStage 'Preflight: verify core resource target'"
)
]
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
(root / "desktop").mkdir()
probe = root / "npm_probe.py"
probe.write_text(
"import json, os, sys\n"
"with open(os.environ['BUILD_TEST_LOG'], 'a', encoding='utf-8') as log:\n"
" log.write(json.dumps({'args': sys.argv[1:], 'env': {\n"
f" key: os.environ.get(key) for key in {REGISTRY_KEYS!r}"
"}}) + '\\n')\n"
"if sys.argv[1:] == ['run', 'prepare-packaged-resources']:\n"
" sys.exit(23 if os.environ.get('BUILD_TEST_FAILURE') == 'exit' else 0)\n",
encoding="utf-8",
)
snapshot = (
"@{ microclaw=$env:MICROCLAW_NPM_REGISTRY; msix=$env:MSIX_NPM_REGISTRY; "
"general=$env:NPM_CONFIG_REGISTRY; location=(Get-Location).Path }"
)
harness = root / "build.ps1"
harness.write_text(
"Set-StrictMode -Version Latest\n$ErrorActionPreference = 'Stop'\n"
"$root = $PSScriptRoot\n"
"function Start-BuildStage { param($Name) }\n"
"function Save-BuildCache { param($Cache) }\n"
"function Write-BuildTimingSummary { param($Status) }\n"
"function npm {\n"
" & $env:BUILD_TEST_PYTHON $env:BUILD_TEST_PROBE @args\n"
" if ($args[0] -eq 'run' -and $env:BUILD_TEST_FAILURE -eq 'throw') {\n"
" throw 'intentional resource launch failure'\n"
" }\n"
"}\n"
f"$nativeCache = [pscustomobject]@{{ hit=${str(cache_hit).lower()} }}\n"
f"$before = {snapshot}\n"
"try {\n" + dependencies + resources + "} finally {\n"
f" $after = {snapshot}\n"
" 'ENV_JSON:' + (ConvertTo-Json -Compress @{ before=$before; after=$after })\n"
"}\n",
encoding="utf-8-sig",
)
env = {
key: value for key, value in os.environ.items() if key.upper() not in REGISTRY_KEYS
}
env.update(
NPM_CONFIG_REGISTRY=GENERAL_REGISTRY,
BUILD_TEST_PYTHON=sys.executable,
BUILD_TEST_PROBE=str(probe),
BUILD_TEST_LOG=str(root / "calls.jsonl"),
BUILD_TEST_FAILURE=failure or "",
)
if microclaw is not None:
env["MICROCLAW_NPM_REGISTRY"] = microclaw
if msix is not None:
env["MSIX_NPM_REGISTRY"] = msix
result = subprocess.run(
[
POWERSHELL,
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
str(harness),
],
cwd=ROOT,
env=env,
capture_output=True,
text=True,
encoding="utf-8",
timeout=30,
)
calls = [
json.loads(line)
for line in (root / "calls.jsonl").read_text(encoding="utf-8").splitlines()
]
state = json.loads(result.stdout.split("ENV_JSON:", 1)[1].splitlines()[0])
self.assertEqual(state["before"], state["after"], result.stdout + result.stderr)
self.assertEqual(calls[0]["args"], ["install"])
self.assertEqual(
calls[0]["env"],
dict(zip(REGISTRY_KEYS, (microclaw, msix, GENERAL_REGISTRY), strict=True)),
)
return result, calls

def test_resource_registry_precedence_without_changing_dependency_registry(self):
for microclaw, msix, expected in (
(None, None, DEFAULT_REGISTRY),
("https://custom.example/npm/", None, "https://custom.example/npm/"),
(None, "https://msix.example/npm/", "https://msix.example/npm/"),
(
"https://custom.example/npm/",
"https://msix.example/npm/",
"https://custom.example/npm/",
),
):
with self.subTest(microclaw=microclaw, msix=msix):
result, calls = self.run_stage(microclaw=microclaw, msix=msix)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(calls[1]["args"], ["run", "prepare-packaged-resources"])
self.assertEqual(calls[1]["env"]["MICROCLAW_NPM_REGISTRY"], expected)
self.assertEqual(calls[1]["env"]["MSIX_NPM_REGISTRY"], msix)
self.assertEqual(calls[1]["env"]["NPM_CONFIG_REGISTRY"], GENERAL_REGISTRY)
self.assertIn(expected.split("://")[1].split("/")[0], result.stdout)

def test_preparation_failures_restore_environment_and_remain_failures(self):
for failure, microclaw in (
("exit", None),
("exit", "https://custom.example/npm/"),
("throw", None),
):
with self.subTest(failure=failure, microclaw=microclaw):
result, calls = self.run_stage(microclaw=microclaw, failure=failure)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(
calls[1]["env"]["MICROCLAW_NPM_REGISTRY"], microclaw or DEFAULT_REGISTRY
)
self.assertIn(
"resource launch failure" if failure == "throw" else "preparation failed",
result.stdout + result.stderr,
)

def test_diagnostic_logs_only_registry_origin_not_credentials_path_or_query(self):
registry = (
"https://builder:private-password@registry.example/private-path/?token=private-token"
)
result, calls = self.run_stage(microclaw=registry)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(calls[1]["env"]["MICROCLAW_NPM_REGISTRY"], registry)
diagnostics = result.stdout.split("ENV_JSON:", 1)[0] + result.stderr
self.assertIn("https://registry.example", diagnostics)
for secret in ("builder", "private-password", "private-path", "private-token"):
self.assertNotIn(secret, diagnostics)

def test_cache_hit_skips_acquisition_without_changing_environment(self):
result, calls = self.run_stage(cache_hit=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(len(calls), 1)

def test_invalid_registry_fails_without_exposing_value_or_leaking_environment(self):
registry = "invalid-registry private-secret"
result, calls = self.run_stage(microclaw=registry)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(len(calls), 1)
diagnostics = result.stdout.split("ENV_JSON:", 1)[0] + result.stderr
self.assertIn("must be an absolute HTTP(S) URL", diagnostics)
self.assertNotIn(registry, diagnostics)


if __name__ == "__main__":
unittest.main()
Loading