Merge remote-tracking branch 'origin/master' into fix/worker-timer-clamp

This commit is contained in:
Chinesezjc
2026-07-27 19:22:09 +08:00
10 changed files with 291 additions and 146 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-27-wine-windows-gates-experiment.md
2026-07-27-wine-windows-gates-experiment.md: aab8aecdfca06c1f15641044a071015f543a84b6
2026-07-27-wine-windows-gates-experiment.zh.md: 5239b185e1e0c63aa626ee3f20f3f298c0c8579d
2026-07-27-wine-windows-gates-experiment.md: 640c8e455b1a35ea4ac83454227147b9979316dc
2026-07-27-wine-windows-gates-experiment.zh.md: f30e09ca7411ef83d02faf012ce54d6c6c65dff1
@@ -18,7 +18,9 @@ Dependencies install natively on Linux with `supportedArchitectures` extended to
The lane holds the wall clock of the Linux CI jobs through four levers: the master-refreshed pnpm store cache (restore-only, same key as the Linux jobs), Wine provisioning (apt install, Windows Node download, `wineboot`) running concurrently with `pnpm install`, the two blocking surfaces running concurrently — the same shape `run-gates` gives them on native Windows — and an apt-archive cache keyed on the runner image, seeded from master by the `wine apt cache` job so every pull request restores from the default-branch scope.
Four environment constraints shape the job, each found as a red run: Ubuntu's `wine64` package alone puts nothing on PATH (install `wine`, the dispatcher); Node under Wine cannot attach stdio to the Actions runner's pipes (`Socket open EBADF` at bootstrap — every invocation routes stdio through a file); Wine does not realpath pnpm's isolated-layout Unix symlinks (the hoisted layout above); and Wine cannot create Windows symlinks (`ENOTSUP` from VitePress's `linkVue` — the `vue` link is laid down host-side before the gate).
The gate logic lives in one script, [scripts/wine-windows-gates.sh](../../../../scripts/wine-windows-gates.sh): the ci.yml job provisions runner state (caches, apt Wine) and calls it, and the optional local gate `pnpm run check:windows-wine` runs the identical script on a developer machine that has Wine installed — one implementation, so local reproduction of a red CI lane needs no translation between environments. The local gate is a diagnosis tool, not a routine check: run it only when investigating a known Windows-related failure; CI owns the everyday win32 signal, and [dsh-pre-push-checks](../../../skills/dsh-pre-push-checks/SKILL.md) never selects it. The script never mutates the working tree: it snapshots tracked plus untracked-unignored files into a scratch directory, applies the Wine-specific pnpm overrides to the snapshot only, and installs there against the shared store; the Wine prefix and the checksum-verified Windows Node zip persist under `.cache/wine-windows/` so local reruns skip provisioning, with an offline fallback to the newest cached zip when nodejs.org is unreachable.
Five environment constraints shape CI and local execution, each found as a red run: Ubuntu's `wine64` package alone puts nothing on PATH (install `wine`, the dispatcher); Node under Wine cannot attach stdio to the caller's pipes (`Socket open EBADF` at bootstrap — every invocation routes stdio through a file); Wine does not realpath pnpm's isolated-layout Unix symlinks (the hoisted layout above); macOS Wine also exposes hoisted workspace links as ordinary directories, so the client test aggregate includes every package-local CSS module declaration instead of relying on project-reference realpaths; and Wine cannot create Windows symlinks (`ENOTSUP` from VitePress's `linkVue` — the `vue` link is laid down host-side before the gate).
## Measured results
@@ -18,7 +18,9 @@ Pull request 的 Windows 通道存在的意义是证明两个阻断性 win32 表
该通道靠四个杠杆保持 Linux CI 作业的墙钟:master 刷新的 pnpm store 缓存(只恢复,与 Linux 作业同键)、Wine 供给(apt 安装、Windows Node 下载、`wineboot`)与 `pnpm install` 并发运行、两个阻断表面并发运行——与 `run-gates` 在原生 Windows 上给它们的形状相同——以及按 runner 镜像为键的 apt 归档缓存,由 master 的 `wine apt cache` 作业播种,使每个 pull request 都能从默认分支作用域恢复。
四条环境约束塑造了该作业,每条都以一次红色运行被发现:Ubuntu 的 `wine64` 包本身不往 PATH 放任何东西(要装 `wine` 调度器);Wine 下的 Node 无法把 stdio 接到 Actions runner 的管道上(引导期 `Socket open EBADF`——所有调用都经文件中转 stdio);Wine 不对 pnpm isolated 布局的 Unix 符号链接做 realpath(即上文的 hoisted 布局);Wine 无法创建 Windows 符号链接(VitePress 的 `linkVue``ENOTSUP`——`vue` 链接在门禁前由宿主侧铺好)
门禁逻辑集中在一个脚本里,[scripts/wine-windows-gates.sh](../../../../scripts/wine-windows-gates.sh)ci.yml 作业只供给 runner 状态(缓存、apt Wine)然后调用它,可选的本地门禁 `pnpm run check:windows-wine` 在装有 Wine 的开发机上运行同一个脚本——单一实现,因此本地复现红色 CI 通道不需要在环境之间做任何转译。该本地门禁是诊断工具而非例行检查:仅在排查已知的 Windows 相关失败时运行;日常 win32 信号归 CI 所有,[dsh-pre-push-checks](../../../skills/dsh-pre-push-checks/SKILL.md) 也从不选择它。脚本从不改动工作树:把被跟踪加未跟踪未忽略的文件快照进一个临时目录,只对快照施加 Wine 特有的 pnpm 覆盖,并在那里对着共享 store 安装;Wine prefix 与校验和验证过的 Windows Node zip 持久存放在 `.cache/wine-windows/` 下,本地重跑跳过供给,nodejs.org 不可达时回退到最新的已缓存 zip
五条环境约束塑造了 CI 与本地执行,每条都以一次红色运行被发现:Ubuntu 的 `wine64` 包本身不往 PATH 放任何东西(要装 `wine` 调度器);Wine 下的 Node 无法把 stdio 接到调用方的管道上(引导期 `Socket open EBADF`——所有调用都经文件中转 stdio);Wine 不对 pnpm isolated 布局的 Unix 符号链接做 realpath(即上文的 hoisted 布局);macOS Wine 也会把 hoisted workspace 链接暴露为普通目录,因此 client 测试聚合会纳入每个包自己的 CSS 模块声明,而不依赖 project-reference realpathWine 无法创建 Windows 符号链接(VitePress 的 `linkVue``ENOTSUP`——`vue` 链接在门禁前由宿主侧铺好)。
## 实测结果
+21 -141
View File
@@ -339,20 +339,16 @@ jobs:
# Windows Node under Wine on standard hosted Linux. The master
# serial-windows job below keeps the complete native-kernel inventory —
# including the observational portability gates this lane does not run —
# on real windows-2025. Direct tool entrypoints stand in for pnpm's cmd
# shims, which a Linux-side install does not create; layout, fidelity
# limits, and measured timings live in
# on real windows-2025. This job only provisions runner state (caches,
# apt); scripts/wine-windows-gates.sh owns the gate logic and is the same
# script the optional local gate `pnpm run check:windows-wine` runs.
# Layout, fidelity limits, and measured timings live in
# .agents/notes/implemented/process/2026-07-27-wine-windows-gates-experiment.md
windows:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
name: windows node 24 / wine blocking
timeout-minutes: 15
env:
WINEDEBUG: '-all'
WINEARCH: win64
# Skip Wine Mono / Gecko installers: Node needs neither.
WINEDLLOVERRIDES: 'mscoree,mshtml='
steps:
- uses: actions/checkout@v6
with:
@@ -391,142 +387,26 @@ jobs:
path: ~/wine-debs
key: ${{ steps.wine-cache-key.outputs.key }}
- name: Install dependencies and provision Wine concurrently
# Runner provisioning only — a developer machine installs Wine through
# its own package manager; the gate script assumes a wine binary and
# fails loud without one. Wine from the apt cache when present; else
# download the full dependency closure once and keep it for the next
# run. The `wine` dispatcher package (not bare `wine64`) is what puts a
# binary on PATH.
- name: Install Wine
run: |
# Windows-lane install-time overrides. supportedArchitectures
# additionally materializes the win32-x64 platform packages
# (@esbuild/win32-x64, rolldown and rollup MSVC bindings) the
# Windows toolchain resolves at runtime; nodeLinker: hoisted lays
# node_modules out flat with real files because Windows Node under
# Wine does not realpath pnpm's isolated-layout symlinks. Neither
# override is recorded in the lockfile, so --frozen-lockfile stays
# valid. --ignore-scripts skips Linux lifecycle scripts no gate in
# this lane loads; the win32 binaries ship prebuilt.
cat >> pnpm-workspace.yaml <<'EOF'
nodeLinker: hoisted
supportedArchitectures:
os: [current, win32]
cpu: [current, x64]
EOF
pnpm install --frozen-lockfile --ignore-scripts &
install_pid=$!
provision_wine() {
set -euo pipefail
# Wine from the apt cache when present; else download the full
# dependency closure once and keep it for the next run. The
# `wine` dispatcher package (not bare `wine64`) is what puts a
# binary on PATH.
if compgen -G "$HOME/wine-debs/*.deb" > /dev/null; then
sudo apt-get install -y --no-install-recommends "$HOME"/wine-debs/*.deb
else
sudo apt-get update
sudo apt-get install -y --no-install-recommends --download-only wine
mkdir -p "$HOME/wine-debs"
cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/" 2>/dev/null || true
sudo apt-get install -y --no-install-recommends wine
fi
WINE_BIN=''
for candidate in "$(command -v wine || true)" "$(command -v wine64 || true)" /usr/lib/wine/wine64; do
if [ -n "$candidate" ] && [ -x "$candidate" ]; then WINE_BIN="$candidate"; break; fi
done
[ -n "$WINE_BIN" ] || { echo '::error::no wine binary found after install'; exit 1; }
echo "WINE_BIN=$WINE_BIN" >> "$GITHUB_ENV"
# Windows Node for the repo's primary line, checksum-verified
# against the same dist directory.
version=$(curl -fsSL https://nodejs.org/dist/index.json \
| jq -r --arg p "v${PRIMARY_NODE_VERSION}." '[.[] | select(.version | startswith($p))][0].version')
echo "Windows Node: $version"
curl -fsSL -o "$RUNNER_TEMP/node-win.zip" \
"https://nodejs.org/dist/${version}/node-${version}-win-x64.zip"
curl -fsSL "https://nodejs.org/dist/${version}/SHASUMS256.txt" \
| awk -v a="node-${version}-win-x64.zip" '$2 == a { print $1 " '"$RUNNER_TEMP"'/node-win.zip" }' \
| sha256sum --check -
unzip -q "$RUNNER_TEMP/node-win.zip" -d "$RUNNER_TEMP/node-win"
echo "NODE_WIN=$RUNNER_TEMP/node-win/node-${version}-win-x64/node.exe" >> "$GITHUB_ENV"
"$WINE_BIN" wineboot --init || true
wineserver -w || true
}
provision_wine &
wine_pid=$!
install_status=0
wait "$install_pid" || install_status=$?
wine_status=0
wait "$wine_pid" || wine_status=$?
if (( install_status != 0 )); then exit "$install_status"; fi
exit "$wine_status"
- name: Resolve entrypoints, link vue, smoke Windows Node
run: |
# Node under Wine cannot attach stdio to the Actions runner's pipes
# (Socket open EBADF at bootstrap), so every invocation runs through
# this wrapper: stdio to a regular file, replayed after exit.
cat > "$RUNNER_TEMP/wine-node.sh" <<'SH'
#!/usr/bin/env bash
set -u
log="$1"; shift
"$WINE_BIN" "$NODE_WIN" "$@" < /dev/null > "$log" 2>&1
status=$?
tail -n 300 "$log"
exit "$status"
SH
chmod +x "$RUNNER_TEMP/wine-node.sh"
resolve() {
local name="$1"; shift
for p in "$@"; do
if [ -f "$p" ]; then echo "$name=$PWD/$p" >> "$GITHUB_ENV"; return 0; fi
done
echo "::error::$name not found at any of: $*"; return 1
}
resolve TSC_JS node_modules/typescript/bin/tsc
resolve TSDOWN_JS node_modules/tsdown/dist/run.mjs
resolve VITEPRESS_JS website/node_modules/vitepress/bin/vitepress.js node_modules/vitepress/bin/vitepress.js
# VitePress links vue into the site's node_modules at build time;
# Wine cannot CREATE Windows symlinks (ENOTSUP) but follows
# pre-existing Unix ones, so lay the link down host-side.
if [ -d node_modules/vue ] && [ ! -e website/node_modules/vue ]; then
mkdir -p website/node_modules
ln -s ../../node_modules/vue website/node_modules/vue
if compgen -G "$HOME/wine-debs/*.deb" > /dev/null; then
sudo apt-get install -y --no-install-recommends "$HOME"/wine-debs/*.deb
else
sudo apt-get update
sudo apt-get install -y --no-install-recommends --download-only wine
mkdir -p "$HOME/wine-debs"
cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/" 2>/dev/null || true
sudo apt-get install -y --no-install-recommends wine
fi
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/smoke.log" -p "'smoke: ' + process.platform + ' ' + process.arch + ' ' + process.version"
# The two blocking surfaces run concurrently, the same shape run-gates
# gives ci-windows-blocking on native Windows: `build` = tsc -b then
# tsdown, `production site` = the VitePress build. Both statuses are
# captured so one failure cannot hide the other's result.
- name: Run blocking Windows gates concurrently under Wine
run: |
build_gate() {
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/tsc.log" "$TSC_JS" -b --pretty false || return $?
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/tsdown.log" "$TSDOWN_JS"
}
site_gate() {
cd website
"$RUNNER_TEMP/wine-node.sh" "$RUNNER_TEMP/site.log" "$VITEPRESS_JS" build .
}
start=$SECONDS
build_gate > "$RUNNER_TEMP/build-gate.out" 2>&1 &
build_pid=$!
site_gate > "$RUNNER_TEMP/site-gate.out" 2>&1 &
site_pid=$!
build_status=0
wait "$build_pid" || build_status=$?
site_status=0
wait "$site_pid" || site_status=$?
echo "== build gate (exit $build_status, $((SECONDS - start))s elapsed) =="
tail -n 120 "$RUNNER_TEMP/build-gate.out"
echo "== production site gate (exit $site_status, $((SECONDS - start))s elapsed) =="
tail -n 120 "$RUNNER_TEMP/site-gate.out"
if (( build_status != 0 )); then exit "$build_status"; fi
exit "$site_status"
- name: Run the Wine Windows gates
run: bash scripts/wine-windows-gates.sh
- name: Shut down wineserver
if: always()
+1
View File
@@ -59,6 +59,7 @@ pnpm run typecheck
pnpm run lint
pnpm run duplication # cross-file TypeScript clone detection
pnpm run build # tsc emits lib/types, tsdown bundles runtime
pnpm run check:windows-wine # ONLY when diagnosing a known Windows failure (needs wine); CI owns this signal
pnpm run hygiene # knip + publint + workspace constraints + NodeNext consumer check
pnpm run doc-sync # all documentation gates; leaf list in scripts/run-gates.ts
pnpm run website:build # VitePress build (doubles as dead-link check)
+1
View File
@@ -40,6 +40,7 @@
"check:ci:windows-blocking": "tsx scripts/run-gates.ts ci-windows-blocking",
"check:ci:windows-complete": "tsx scripts/run-gates.ts ci-windows-complete",
"check:ci:windows-observational": "tsx scripts/run-gates.ts ci-windows-observational",
"check:windows-wine": "bash scripts/wine-windows-gates.sh",
"check:node-compat": "tsx scripts/run-gates.ts node-compat",
"knip": "knip --treat-config-hints-as-errors",
"publint": "tsx scripts/publint-all.ts",
+33
View File
@@ -0,0 +1,33 @@
/** Regression coverage for source declarations owned by the client test aggregate. */
import { existsSync, readdirSync } from 'node:fs'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import ts from 'typescript'
import { describe, expect, it } from 'vitest'
const root = fileURLToPath(new URL('..', import.meta.url))
function clientCssDeclarations(): string[] {
const clientRoot = resolve(root, 'packages/client')
return readdirSync(clientRoot, { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => resolve(clientRoot, entry.name, 'src/css-modules.d.ts'))
.filter(existsSync)
.sort()
}
describe('client TypeScript aggregate', () => {
it('loads package CSS declarations without relying on workspace-link realpaths', () => {
const configPath = resolve(root, 'tsconfig.client.json')
const read = ts.readConfigFile(configPath, file => ts.sys.readFile(file))
if (read.error !== undefined) {
throw new Error(ts.flattenDiagnosticMessageText(read.error.messageText, '\n'))
}
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, root)
const loaded = parsed.fileNames
.filter(file => file.endsWith('/src/css-modules.d.ts'))
.sort()
expect(loaded).toEqual(clientCssDeclarations())
})
})
+1 -1
View File
@@ -1,5 +1,5 @@
{
"AGENTS.md": 1680,
"AGENTS.md": 1695,
"docs/AGENTS.md": 1150,
"docs/architecture.md": 1800,
"docs/cordis-primer.md": 600,
+223
View File
@@ -0,0 +1,223 @@
#!/usr/bin/env bash
# Run the blocking Windows gates (workspace build, production site) with real
# win-x64 Node.js under Wine — the same script the pull-request `windows` job
# in ci.yml executes and the optional local gate `pnpm run check:windows-wine`
# wraps. Owning rationale, fidelity limits, and measured timings:
# .agents/notes/implemented/process/2026-07-27-wine-windows-gates-experiment.md
#
# The working tree is never mutated: tracked plus untracked-unignored files
# are snapshotted into a scratch directory, the Wine-specific pnpm overrides
# (hoisted layout, win32-x64 platform packages) are appended to the SNAPSHOT's
# pnpm-workspace.yaml, and the install and gates run there against the shared
# pnpm store. The Wine prefix and the checksum-verified Windows Node zip
# persist in .cache/wine-windows/ so reruns skip provisioning.
#
# Environment: DSH_WINE_NODE_MAJOR (default $PRIMARY_NODE_VERSION, then 24)
# picks the Windows Node line; DSH_WINE_GATE_CACHE_DIR relocates the cache;
# DSH_WINE_GATE_KEEP=1 preserves the scratch tree for inspection.
set -euo pipefail
repo_root="$(git rev-parse --show-toplevel)"
node_major="${DSH_WINE_NODE_MAJOR:-${PRIMARY_NODE_VERSION:-24}}"
cache_dir="${DSH_WINE_GATE_CACHE_DIR:-$repo_root/.cache/wine-windows}"
export WINEDEBUG='-all'
export WINEARCH=win64
# Skip Wine Mono / Gecko installers: Node needs neither.
export WINEDLLOVERRIDES='mscoree,mshtml='
export WINEPREFIX="$cache_dir/prefix"
# ---- preflight: fail loud before any expensive work --------------------
wine_bin=''
for candidate in "$(command -v wine || true)" "$(command -v wine64 || true)" /usr/lib/wine/wine64; do
if [ -n "$candidate" ] && [ -x "$candidate" ]; then wine_bin="$candidate"; break; fi
done
# GNU coreutils sha256sum on Linux; perl shasum ships with macOS. Both
# accept the same "<hash> <file>" --check input.
checksum_tool=''
if command -v sha256sum > /dev/null; then
checksum_tool='sha256sum'
elif command -v shasum > /dev/null; then
checksum_tool='shasum'
fi
missing=()
[ -n "$wine_bin" ] || missing+=('wine (apt: wine | brew: wine-stable)')
command -v curl > /dev/null || missing+=('curl')
command -v unzip > /dev/null || missing+=('unzip')
[ -n "$checksum_tool" ] || missing+=('sha256sum or shasum (apt: coreutils | macOS ships shasum)')
if ! command -v pnpm > /dev/null; then corepack enable > /dev/null 2>&1 || true; fi
command -v pnpm > /dev/null || missing+=('pnpm (corepack enable)')
if (( ${#missing[@]} > 0 )); then
printf 'wine-windows-gates: missing required tool: %s\n' "${missing[@]}" >&2
exit 1
fi
# Verify file $2 against SHA-256 hex $1 with whichever tool preflight found.
verify_sha256() {
case "$checksum_tool" in
sha256sum) printf '%s %s\n' "$1" "$2" | sha256sum --check - > /dev/null ;;
shasum) printf '%s %s\n' "$1" "$2" | shasum -a 256 --check - > /dev/null ;;
esac
}
scratch="$(mktemp -d "${TMPDIR:-/tmp}/dsh-wine-gates.XXXXXX")"
cleanup() {
wineserver -k > /dev/null 2>&1 || true
if [ "${DSH_WINE_GATE_KEEP:-0}" = '1' ]; then
echo "wine-windows-gates: scratch tree kept at $scratch"
else
rm -rf "$scratch"
fi
}
trap cleanup EXIT
mkdir -p "$cache_dir" "$scratch/logs"
# ---- provision Windows Node, boot Wine, snapshot + install concurrently ----
provision_node() {
# Latest release of the primary line, checksum-verified against the same
# dist directory. Offline runs fall back to the newest cached zip, loudly.
local version zip
version="$(curl -fsSL --max-time 30 https://nodejs.org/dist/index.json 2> /dev/null \
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{const v=JSON.parse(d).find(r=>r.version.startsWith('v$node_major.'));if(v)console.log(v.version)})" \
|| true)"
if [ -n "$version" ]; then
zip="$cache_dir/node-$version-win-x64.zip"
if [ ! -f "$zip" ]; then
curl -fsSL -o "$zip.tmp" "https://nodejs.org/dist/$version/node-$version-win-x64.zip"
local expected
expected="$(curl -fsSL "https://nodejs.org/dist/$version/SHASUMS256.txt" \
| awk -v a="node-$version-win-x64.zip" '$2 == a { print $1; exit }')"
[ -n "$expected" ] || { echo "wine-windows-gates: no SHASUMS256 entry for node-$version-win-x64.zip" >&2; exit 1; }
verify_sha256 "$expected" "$zip.tmp"
mv "$zip.tmp" "$zip"
fi
else
zip="$(ls -t "$cache_dir"/node-v"$node_major".*-win-x64.zip 2> /dev/null | head -1 || true)"
[ -n "$zip" ] || { echo "wine-windows-gates: nodejs.org unreachable and no cached Windows Node v$node_major zip in $cache_dir" >&2; exit 1; }
echo "wine-windows-gates: nodejs.org unreachable; using cached $(basename "$zip")" >&2
fi
unzip -q -o "$zip" -d "$scratch/node-win"
echo "$scratch/node-win/$(basename "$zip" .zip)/node.exe" > "$scratch/node-win-path"
}
boot_wine() {
"$wine_bin" wineboot --init > /dev/null 2>&1 || true
wineserver -w || true
}
snapshot_and_install() {
# Tracked + untracked-unignored files, minus agent-session litter; the
# existence filter drops paths staged as deleted. Then the Wine-specific
# install-time overrides go on the SNAPSHOT only: hoisted because Windows
# Node under Wine does not realpath pnpm's isolated-layout symlinks, and
# win32-x64 so the Windows esbuild/rolldown/rollup binaries materialize.
# Neither is recorded in the lockfile, so --frozen-lockfile stays valid;
# --ignore-scripts skips host lifecycle scripts no gate loads.
git -C "$repo_root" ls-files -z --cached --others --exclude-standard -- . ':!:.claude' ':!:.codex' \
| while IFS= read -r -d '' file; do [ -e "$repo_root/$file" ] && printf '%s\0' "$file"; done \
| tar -C "$repo_root" --null --files-from=- -cf - \
| tar -C "$scratch/tree" -xf -
cat >> "$scratch/tree/pnpm-workspace.yaml" << 'EOF'
nodeLinker: hoisted
supportedArchitectures:
os: [current, win32]
cpu: [current, x64]
EOF
(cd "$scratch/tree" && pnpm install --frozen-lockfile --ignore-scripts > "$scratch/logs/install.log" 2>&1) \
|| { tail -40 "$scratch/logs/install.log" >&2; return 1; }
}
mkdir "$scratch/tree"
start=$SECONDS
provision_node & node_pid=$!
boot_wine & wine_pid=$!
snapshot_and_install & install_pid=$!
# Wait for EVERY child before judging any: a bare `wait` under set -e would
# exit on the first failure and let the EXIT trap delete $scratch while the
# other children still run inside it. Named statuses also make the report
# point at the root cause instead of a downstream symptom.
node_status=0; wait "$node_pid" || node_status=$?
wine_status=0; wait "$wine_pid" || wine_status=$?
install_status=0; wait "$install_pid" || install_status=$?
provision_failed=0
report_provision() {
if (( $2 != 0 )); then
echo "wine-windows-gates: FAILED $1 (exit $2)" >&2
provision_failed=$2
fi
}
report_provision 'Windows Node provisioning' "$node_status"
report_provision 'wineboot' "$wine_status"
report_provision 'workspace snapshot + pnpm install' "$install_status"
if (( provision_failed != 0 )); then exit "$provision_failed"; fi
node_win="$(cat "$scratch/node-win-path")"
echo "wine-windows-gates: provisioned in $((SECONDS - start))s (wine $("$wine_bin" --version 2> /dev/null), node $(basename "$(dirname "$node_win")"))"
# ---- resolve entrypoints, lay the vue link, smoke ------------------------
# Node under Wine cannot attach stdio to pipes the caller owns (Socket open
# EBADF at bootstrap), so every invocation routes stdio through a file.
wine_node() {
local log="$1"
shift
local status=0
"$wine_bin" "$node_win" "$@" < /dev/null > "$log" 2>&1 || status=$?
return "$status"
}
cd "$scratch/tree"
tsc_js='node_modules/typescript/bin/tsc'
tsdown_js='node_modules/tsdown/dist/run.mjs'
vitepress_js='node_modules/vitepress/bin/vitepress.js'
[ -f "$vitepress_js" ] || vitepress_js='website/node_modules/vitepress/bin/vitepress.js'
for entry in "$tsc_js" "$tsdown_js" "$vitepress_js"; do
[ -f "$entry" ] || { echo "wine-windows-gates: expected entrypoint missing after hoisted install: $entry" >&2; exit 1; }
done
# VitePress links vue into the site's node_modules at build time; Wine cannot
# CREATE Windows symlinks (ENOTSUP) but follows pre-existing Unix ones.
if [ -d node_modules/vue ] && [ ! -e website/node_modules/vue ]; then
mkdir -p website/node_modules
ln -s ../../node_modules/vue website/node_modules/vue
fi
wine_node "$scratch/logs/smoke.log" -p "'smoke: ' + process.platform + ' ' + process.arch + ' ' + process.version"
cat "$scratch/logs/smoke.log"
grep -q '^smoke: win32 x64' "$scratch/logs/smoke.log" || { echo 'wine-windows-gates: Windows Node smoke did not report win32 x64' >&2; exit 1; }
# ---- the two blocking surfaces, concurrently ------------------------------
# The same shape run-gates gives ci-windows-blocking on native Windows:
# `build` = tsc -b then tsdown, `production site` = the VitePress build. Both
# statuses are captured so one failure cannot hide the other's result.
build_gate() {
wine_node "$scratch/logs/tsc.log" "$tsc_js" -b --pretty false || return $?
wine_node "$scratch/logs/tsdown.log" "$tsdown_js"
}
site_gate() {
cd website
wine_node "$scratch/logs/site.log" "../$vitepress_js" build .
}
start=$SECONDS
build_gate & build_pid=$!
site_gate & site_pid=$!
build_status=0
wait "$build_pid" || build_status=$?
site_status=0
wait "$site_pid" || site_status=$?
elapsed=$((SECONDS - start))
report() {
local label="$1" status="$2"
shift 2
if (( status == 0 )); then
echo "wine-windows-gates: PASS $label (${elapsed}s window)"
else
echo "== FAILED $label (exit $status) ==" >&2
for log in "$@"; do tail -n 200 "$log" >&2 || true; done
fi
}
report 'build (tsc -b, tsdown)' "$build_status" "$scratch/logs/tsc.log" "$scratch/logs/tsdown.log"
report 'production site (vitepress build)' "$site_status" "$scratch/logs/site.log"
if (( build_status != 0 )); then exit "$build_status"; fi
exit "$site_status"
+3
View File
@@ -14,6 +14,9 @@
"types": ["node"]
},
"include": [
// Source-subpath test imports can arrive through workspace links whose
// realpath semantics vary by host. Load package CSS declarations directly.
"packages/client/*/src/css-modules.d.ts",
"packages/client/*/tests/**/*.ts",
"packages/client/*/tests/**/*.tsx",
"packages/client/tsdown.client.ts",