mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 04:37:44 +02:00
[eric] make Windows install + boot way faster: splash window so it's not frozen on startup, swap MCP node_modules for tiny esbuild bundles
(138MB → 7MB), pre-compile python bytecode, parallelize Widevine, never silently quit
This commit is contained in:
+110
-27
@@ -83,53 +83,136 @@ if (-not (Test-Path (Join-Path $UvBinDir 'uvx.exe'))) {
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# --- Step 0b: Bundle reddit-mcp-buddy via esbuild ---
|
||||
# --- Step 0b: Bundle npm MCP servers via esbuild ---
|
||||
# Each bundle compiles down to a single ~5-15 MB CommonJS file under
|
||||
# backend\mcp-bundles\, runs on Electron's bundled Node at runtime
|
||||
# (ELECTRON_RUN_AS_NODE=1), and is preferred by tools_lib.py:521 over
|
||||
# any pre-installed node_modules tree. Bundling instead of shipping
|
||||
# node_modules cuts the installer file count from ~28k -> ~9k, which
|
||||
# is the dominant lever on NSIS install time + Defender scan cost.
|
||||
$McpBundleDir = Join-Path $ProjectRoot 'backend\mcp-bundles'
|
||||
New-Item -ItemType Directory -Force -Path $McpBundleDir | Out-Null
|
||||
$RedditBundle = Join-Path $McpBundleDir 'reddit-mcp-buddy.js'
|
||||
if (-not (Test-Path $RedditBundle)) {
|
||||
Write-Host "[0b] Bundling reddit-mcp-buddy..."
|
||||
|
||||
# Single-file CJS bundle. Output path: mcp-bundles\<output>.js. Use for
|
||||
# packages that don't read sibling files at runtime. The import.meta.url
|
||||
# polyfill is applied uniformly because nearly every modern ESM package
|
||||
# uses createRequire(import.meta.url) somewhere — without the polyfill,
|
||||
# esbuild's ESM->CJS transform leaves import.meta.url as undefined and
|
||||
# the bundle crashes at module load.
|
||||
function Build-McpBundleSingle($PackageName, $EntrySubpath, $OutputName) {
|
||||
$OutFile = Join-Path $McpBundleDir $OutputName
|
||||
if ((Test-Path $OutFile) -and -not $env:OPENSWARM_REBUILD_BUNDLES) {
|
||||
Write-Host "[0b] $PackageName bundle already present (set `$env:OPENSWARM_REBUILD_BUNDLES='1' to force rebuild)."
|
||||
return
|
||||
}
|
||||
Write-Host "[0b] Bundling $PackageName -> $OutputName ..."
|
||||
$TmpDir = Join-Path $env:TEMP "openswarm-mcp-$([guid]::NewGuid())"
|
||||
New-Item -ItemType Directory -Force -Path $TmpDir | Out-Null
|
||||
Push-Location $TmpDir
|
||||
try {
|
||||
& npm install reddit-mcp-buddy --silent 2>$null
|
||||
& npx esbuild "node_modules/reddit-mcp-buddy/dist/index.js" --bundle --platform=node --format=cjs "--outfile=$RedditBundle"
|
||||
if ($LASTEXITCODE -ne 0) { throw "esbuild failed" }
|
||||
Write-Host "reddit-mcp-buddy bundled."
|
||||
& npm install $PackageName --silent 2>$null
|
||||
if ($LASTEXITCODE -ne 0) { throw "$PackageName install failed" }
|
||||
$EntryPath = Join-Path (Join-Path $TmpDir 'node_modules') $EntrySubpath
|
||||
if (-not (Test-Path $EntryPath)) { throw "$PackageName entry not found at $EntryPath" }
|
||||
$banner = 'const __OPENSWARM_IMPORT_META_URL__ = require("url").pathToFileURL(__filename).href;'
|
||||
& npx esbuild $EntryPath --bundle --platform=node --format=cjs --target=node22 --legal-comments=none `
|
||||
--define:import.meta.url=__OPENSWARM_IMPORT_META_URL__ `
|
||||
"--banner:js=$banner" `
|
||||
"--outfile=$OutFile"
|
||||
if ($LASTEXITCODE -ne 0) { throw "esbuild failed for $PackageName" }
|
||||
Write-Host "$PackageName bundled."
|
||||
} finally {
|
||||
Pop-Location
|
||||
Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue
|
||||
}
|
||||
} else {
|
||||
Write-Host "[0b] reddit-mcp-buddy bundle already present."
|
||||
}
|
||||
|
||||
# --- Step 0c: npm MCP servers that can't be bundled ---
|
||||
$NpmServersDir = Join-Path $ProjectRoot 'backend\npm-servers'
|
||||
New-Item -ItemType Directory -Force -Path $NpmServersDir | Out-Null
|
||||
|
||||
function Install-NpmServer($SubDir, $PackageName) {
|
||||
$TargetDir = Join-Path $NpmServersDir $SubDir
|
||||
$NodeModules = Join-Path $TargetDir 'node_modules'
|
||||
if (Test-Path $NodeModules) {
|
||||
Write-Host "[0c] $PackageName already present."
|
||||
# Multi-file bundle. Output is a directory mcp-bundles\<dir>\ that mirrors the
|
||||
# upstream SDK's "package_root\dist\index.js + ..\package.json" layout. Use this
|
||||
# for packages whose source reads __dirname\..\package.json (for --version) or
|
||||
# other sibling data files (e.g. @softeria\ms-365-mcp-server reads endpoints.json).
|
||||
function Build-McpBundleDir($PackageName, $EntrySubpath, $OutDirName, $Extras, $External) {
|
||||
$OutDir = Join-Path $McpBundleDir $OutDirName
|
||||
$OutBundle = Join-Path (Join-Path $OutDir 'dist') 'index.js'
|
||||
if ((Test-Path $OutBundle) -and -not $env:OPENSWARM_REBUILD_BUNDLES) {
|
||||
Write-Host "[0b] $PackageName bundle dir already present."
|
||||
return
|
||||
}
|
||||
Write-Host "[0c] Installing $PackageName..."
|
||||
New-Item -ItemType Directory -Force -Path $TargetDir | Out-Null
|
||||
Push-Location $TargetDir
|
||||
Write-Host "[0b] Bundling $PackageName -> $OutDirName\ ..."
|
||||
$TmpDir = Join-Path $env:TEMP "openswarm-mcp-$([guid]::NewGuid())"
|
||||
New-Item -ItemType Directory -Force -Path $TmpDir | Out-Null
|
||||
if (Test-Path $OutDir) { Remove-Item -Recurse -Force $OutDir }
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $OutDir 'dist') | Out-Null
|
||||
Push-Location $TmpDir
|
||||
try {
|
||||
& npm init -y *>$null
|
||||
& npm install $PackageName --silent 2>$null
|
||||
if ($LASTEXITCODE -ne 0) { throw "$PackageName install failed" }
|
||||
Write-Host "$PackageName installed."
|
||||
$EntryPath = Join-Path (Join-Path $TmpDir 'node_modules') $EntrySubpath
|
||||
if (-not (Test-Path $EntryPath)) { throw "$PackageName entry not found at $EntryPath" }
|
||||
|
||||
# Stripped sibling package.json (omits "type":"module" so Node treats the CJS bundle correctly)
|
||||
$SdkPkgPath = Join-Path (Join-Path $TmpDir 'node_modules') (Join-Path $PackageName 'package.json')
|
||||
$SdkPkgJson = Get-Content -Raw $SdkPkgPath | ConvertFrom-Json
|
||||
$SdkVersion = $SdkPkgJson.version
|
||||
$StrippedPkg = "{`"name`":`"$PackageName`",`"version`":`"$SdkVersion`"}"
|
||||
Set-Content -Path (Join-Path $OutDir 'package.json') -Value $StrippedPkg -NoNewline
|
||||
|
||||
# Copy sibling data files
|
||||
if ($Extras) {
|
||||
foreach ($pair in $Extras) {
|
||||
$src, $dst = $pair -split '='
|
||||
$srcAbs = Join-Path (Join-Path $TmpDir 'node_modules') $src
|
||||
$dstAbs = Join-Path $OutDir $dst
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $dstAbs -Parent) | Out-Null
|
||||
Copy-Item -Force $srcAbs $dstAbs
|
||||
}
|
||||
}
|
||||
|
||||
$banner = 'const __OPENSWARM_IMPORT_META_URL__ = require("url").pathToFileURL(__filename).href;'
|
||||
$esbuildArgs = @(
|
||||
$EntryPath, '--bundle', '--platform=node', '--format=cjs',
|
||||
'--target=node22', '--legal-comments=none',
|
||||
'--define:import.meta.url=__OPENSWARM_IMPORT_META_URL__',
|
||||
"--banner:js=$banner",
|
||||
"--outfile=$OutBundle"
|
||||
)
|
||||
if ($External) {
|
||||
foreach ($ext in $External) { $esbuildArgs += "--external:$ext" }
|
||||
}
|
||||
& npx esbuild @esbuildArgs
|
||||
if ($LASTEXITCODE -ne 0) { throw "esbuild failed for $PackageName" }
|
||||
Write-Host "$PackageName bundled."
|
||||
} finally {
|
||||
Pop-Location
|
||||
Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
Install-NpmServer 'softeria-ms-365-mcp-server' '@softeria/ms-365-mcp-server'
|
||||
Install-NpmServer 'notionhq-notion-mcp-server' '@notionhq/notion-mcp-server'
|
||||
|
||||
Build-McpBundleSingle 'reddit-mcp-buddy' 'reddit-mcp-buddy/dist/index.js' 'reddit-mcp-buddy.js'
|
||||
Build-McpBundleDir '@notionhq/notion-mcp-server' '@notionhq/notion-mcp-server/bin/cli.mjs' `
|
||||
'notionhq-notion-mcp-server' `
|
||||
@('@notionhq/notion-mcp-server/scripts/notion-openapi.json=scripts/notion-openapi.json') `
|
||||
@()
|
||||
Build-McpBundleDir '@softeria/ms-365-mcp-server' '@softeria/ms-365-mcp-server/dist/index.js' `
|
||||
'softeria-ms-365-mcp-server' `
|
||||
@('@softeria/ms-365-mcp-server/dist/endpoints.json=dist/endpoints.json') `
|
||||
@('keytar')
|
||||
|
||||
# Wipe legacy single-file Notion bundle if the dir-style bundle now supersedes it.
|
||||
$LegacyNotionFile = Join-Path $McpBundleDir 'notionhq-notion-mcp-server.js'
|
||||
$NotionDir = Join-Path $McpBundleDir 'notionhq-notion-mcp-server'
|
||||
if ((Test-Path $LegacyNotionFile) -and (Test-Path $NotionDir)) {
|
||||
Remove-Item -Force $LegacyNotionFile
|
||||
}
|
||||
|
||||
# Defensively wipe any legacy npm-servers/ tree from prior builds so it
|
||||
# doesn't ride along into the installer (would re-introduce the ~19k
|
||||
# files we just removed by switching to bundling).
|
||||
$LegacyNpmServers = Join-Path $ProjectRoot 'backend\npm-servers'
|
||||
if (Test-Path $LegacyNpmServers) {
|
||||
Write-Host "[0b] Removing legacy backend\npm-servers\ (now superseded by mcp-bundles)..."
|
||||
Remove-Item -Recurse -Force $LegacyNpmServers
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# --- Step 1: Frontend build ---
|
||||
@@ -197,7 +280,7 @@ function Copy-Excluded($Source, $Dest, $Exclude) {
|
||||
|
||||
Copy-Excluded `
|
||||
(Join-Path $ProjectRoot 'backend') (Join-Path $Staging 'backend') `
|
||||
@{ Dirs = @('__pycache__','.venv','tools','tests'); Files = @('*.pyc') }
|
||||
@{ Dirs = @('__pycache__','.venv','tools','tests'); Files = @('*.pyc','.env','.env.*') }
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $Staging 'backend\data\tools') | Out-Null
|
||||
|
||||
Copy-Excluded `
|
||||
|
||||
+128
-35
@@ -87,45 +87,137 @@ else
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 0b: Bundle npm MCP servers (uses Electron's Node at runtime)
|
||||
# Step 0b: Bundle npm MCP servers via esbuild
|
||||
# Each bundle compiles down to a single ~5-15 MB CommonJS file under
|
||||
# backend/mcp-bundles/, runs on Electron's bundled Node at runtime
|
||||
# (ELECTRON_RUN_AS_NODE=1), and is preferred by tools_lib.py:521 over
|
||||
# any pre-installed node_modules tree. Bundling instead of shipping
|
||||
# node_modules cuts the installer file count from ~28k -> ~9k, the
|
||||
# dominant lever on NSIS install time + Defender scan cost.
|
||||
MCP_BUNDLE_DIR="$PROJECT_ROOT/backend/mcp-bundles"
|
||||
mkdir -p "$MCP_BUNDLE_DIR"
|
||||
if [[ ! -f "$MCP_BUNDLE_DIR/reddit-mcp-buddy.js" ]]; then
|
||||
echo "[0b] Bundling reddit-mcp-buddy..."
|
||||
TMPDIR_MCP=$(mktemp -d)
|
||||
cd "$TMPDIR_MCP"
|
||||
npm install reddit-mcp-buddy --silent 2>/dev/null
|
||||
npx esbuild node_modules/reddit-mcp-buddy/dist/index.js --bundle --platform=node --format=cjs --outfile="$MCP_BUNDLE_DIR/reddit-mcp-buddy.js" 2>/dev/null
|
||||
rm -rf "$TMPDIR_MCP"
|
||||
echo "reddit-mcp-buddy bundled."
|
||||
else
|
||||
echo "[0b] reddit-mcp-buddy bundle already present."
|
||||
|
||||
# Single-file CJS bundles. Output path is mcp-bundles/<output>.js. Use for
|
||||
# packages that don't read sibling files at runtime. The import.meta.url
|
||||
# polyfill is applied uniformly because nearly every modern ESM package
|
||||
# uses createRequire(import.meta.url) somewhere in its dependency tree —
|
||||
# without the polyfill, esbuild's ESM->CJS transform leaves import.meta.url
|
||||
# as undefined and the bundle crashes at module load.
|
||||
build_mcp_bundle_single() {
|
||||
local pkg_name="$1"
|
||||
local entry_subpath="$2"
|
||||
local output_name="$3"
|
||||
local out_file="$MCP_BUNDLE_DIR/$output_name"
|
||||
if [[ -f "$out_file" && -z "${OPENSWARM_REBUILD_BUNDLES:-}" ]]; then
|
||||
echo "[0b] $pkg_name bundle already present (set OPENSWARM_REBUILD_BUNDLES=1 to force rebuild)."
|
||||
return
|
||||
fi
|
||||
echo "[0b] Bundling $pkg_name -> $output_name ..."
|
||||
local tmp_dir; tmp_dir=$(mktemp -d)
|
||||
(
|
||||
cd "$tmp_dir"
|
||||
npm install "$pkg_name" --silent 2>/dev/null
|
||||
local entry="node_modules/$entry_subpath"
|
||||
if [[ ! -f "$entry" ]]; then echo "ERROR: $pkg_name entry not found at $entry" >&2; exit 1; fi
|
||||
local banner='const __OPENSWARM_IMPORT_META_URL__ = require("url").pathToFileURL(__filename).href;'
|
||||
npx esbuild "$entry" --bundle --platform=node --format=cjs --target=node22 --legal-comments=none \
|
||||
--define:import.meta.url=__OPENSWARM_IMPORT_META_URL__ \
|
||||
"--banner:js=$banner" \
|
||||
--outfile="$out_file"
|
||||
)
|
||||
rm -rf "$tmp_dir"
|
||||
echo "$pkg_name bundled ($(du -h "$out_file" | cut -f1))."
|
||||
}
|
||||
|
||||
# Multi-file bundle. Output is a directory mcp-bundles/<dir>/ that mirrors the
|
||||
# upstream SDK's "package_root/dist/index.js + ../package.json" layout. Use this
|
||||
# for packages whose source reads __dirname/../package.json (for --version) or
|
||||
# other sibling data files (e.g. @softeria/ms-365-mcp-server reads endpoints.json).
|
||||
# `extras` is a space-separated list of "src=dst" pairs relative to node_modules
|
||||
# and the bundle dir respectively (e.g. "@softeria/ms-365-mcp-server/dist/endpoints.json=dist/endpoints.json").
|
||||
# `external` is a comma-separated list of npm package names to leave unbundled
|
||||
# (e.g. "keytar" — the SDK gracefully degrades when keytar can't be imported).
|
||||
build_mcp_bundle_dir() {
|
||||
local pkg_name="$1"
|
||||
local entry_subpath="$2"
|
||||
local out_dir_name="$3"
|
||||
local extras="$4" # e.g. "@softeria/ms-365-mcp-server/dist/endpoints.json=dist/endpoints.json"
|
||||
local external="$5" # comma-separated package names
|
||||
local out_dir="$MCP_BUNDLE_DIR/$out_dir_name"
|
||||
if [[ -f "$out_dir/dist/index.js" && -z "${OPENSWARM_REBUILD_BUNDLES:-}" ]]; then
|
||||
echo "[0b] $pkg_name bundle dir already present."
|
||||
return
|
||||
fi
|
||||
echo "[0b] Bundling $pkg_name -> $out_dir_name/ ..."
|
||||
local tmp_dir; tmp_dir=$(mktemp -d)
|
||||
rm -rf "$out_dir"
|
||||
mkdir -p "$out_dir/dist"
|
||||
(
|
||||
cd "$tmp_dir"
|
||||
npm install "$pkg_name" --silent 2>/dev/null
|
||||
local entry="node_modules/$entry_subpath"
|
||||
if [[ ! -f "$entry" ]]; then echo "ERROR: $pkg_name entry not found at $entry" >&2; exit 1; fi
|
||||
|
||||
# Stripped sibling package.json — the SDK reads packageJson.version.
|
||||
# Critically OMIT "type":"module" so Node treats the CJS bundle correctly.
|
||||
local sdk_version
|
||||
sdk_version=$(node -e "console.log(require('./node_modules/$pkg_name/package.json').version)")
|
||||
printf '{"name":"%s","version":"%s"}' "$pkg_name" "$sdk_version" > "$out_dir/package.json"
|
||||
|
||||
# Copy any sibling data files the SDK reads at runtime
|
||||
if [[ -n "$extras" ]]; then
|
||||
for pair in $extras; do
|
||||
local src="${pair%%=*}"
|
||||
local dst="${pair##*=}"
|
||||
mkdir -p "$(dirname "$out_dir/$dst")"
|
||||
cp "node_modules/$src" "$out_dir/$dst"
|
||||
done
|
||||
fi
|
||||
|
||||
# Banner polyfills `require` for the import.meta.url polyfill.
|
||||
local banner='const __OPENSWARM_IMPORT_META_URL__ = require("url").pathToFileURL(__filename).href;'
|
||||
|
||||
local external_args=""
|
||||
if [[ -n "$external" ]]; then
|
||||
# Portable comma-split (works in bash and zsh) — `read -ra` is bash-only.
|
||||
local _old_ifs="$IFS"
|
||||
IFS=','
|
||||
local ext
|
||||
for ext in $external; do external_args="$external_args --external:$ext"; done
|
||||
IFS="$_old_ifs"
|
||||
fi
|
||||
|
||||
npx esbuild "$entry" --bundle --platform=node --format=cjs --target=node22 --legal-comments=none \
|
||||
--define:import.meta.url=__OPENSWARM_IMPORT_META_URL__ \
|
||||
"--banner:js=$banner" \
|
||||
$external_args \
|
||||
--outfile="$out_dir/dist/index.js"
|
||||
)
|
||||
rm -rf "$tmp_dir"
|
||||
echo "$pkg_name bundled ($(du -sh "$out_dir" | cut -f1))."
|
||||
}
|
||||
|
||||
build_mcp_bundle_single 'reddit-mcp-buddy' 'reddit-mcp-buddy/dist/index.js' 'reddit-mcp-buddy.js'
|
||||
build_mcp_bundle_dir '@notionhq/notion-mcp-server' '@notionhq/notion-mcp-server/bin/cli.mjs' \
|
||||
'notionhq-notion-mcp-server' \
|
||||
'@notionhq/notion-mcp-server/scripts/notion-openapi.json=scripts/notion-openapi.json' \
|
||||
''
|
||||
build_mcp_bundle_dir '@softeria/ms-365-mcp-server' '@softeria/ms-365-mcp-server/dist/index.js' \
|
||||
'softeria-ms-365-mcp-server' \
|
||||
'@softeria/ms-365-mcp-server/dist/endpoints.json=dist/endpoints.json' \
|
||||
'keytar'
|
||||
|
||||
# Wipe the legacy single-file Notion bundle if the dir bundle now supersedes it.
|
||||
if [[ -f "$MCP_BUNDLE_DIR/notionhq-notion-mcp-server.js" && -d "$MCP_BUNDLE_DIR/notionhq-notion-mcp-server" ]]; then
|
||||
rm -f "$MCP_BUNDLE_DIR/notionhq-notion-mcp-server.js"
|
||||
fi
|
||||
|
||||
# Step 0c: Pre-install npm MCP servers that can't be esbuild'd
|
||||
NPM_SERVERS_DIR="$PROJECT_ROOT/backend/npm-servers"
|
||||
mkdir -p "$NPM_SERVERS_DIR"
|
||||
|
||||
if [[ ! -d "$NPM_SERVERS_DIR/softeria-ms-365-mcp-server/node_modules" ]]; then
|
||||
echo "[0c] Installing @softeria/ms-365-mcp-server..."
|
||||
mkdir -p "$NPM_SERVERS_DIR/softeria-ms-365-mcp-server"
|
||||
cd "$NPM_SERVERS_DIR/softeria-ms-365-mcp-server"
|
||||
npm init -y > /dev/null 2>&1
|
||||
npm install @softeria/ms-365-mcp-server --silent 2>/dev/null
|
||||
echo "@softeria/ms-365-mcp-server installed."
|
||||
else
|
||||
echo "[0c] @softeria/ms-365-mcp-server already present."
|
||||
fi
|
||||
|
||||
if [[ ! -d "$NPM_SERVERS_DIR/notionhq-notion-mcp-server/node_modules" ]]; then
|
||||
echo "[0c] Installing @notionhq/notion-mcp-server..."
|
||||
mkdir -p "$NPM_SERVERS_DIR/notionhq-notion-mcp-server"
|
||||
cd "$NPM_SERVERS_DIR/notionhq-notion-mcp-server"
|
||||
npm init -y > /dev/null 2>&1
|
||||
npm install @notionhq/notion-mcp-server --silent 2>/dev/null
|
||||
echo "@notionhq/notion-mcp-server installed."
|
||||
else
|
||||
echo "[0c] @notionhq/notion-mcp-server already present."
|
||||
# Defensively wipe any legacy npm-servers/ tree from prior builds so it
|
||||
# doesn't ride along into the installer (would re-introduce ~19k files).
|
||||
LEGACY_NPM_SERVERS="$PROJECT_ROOT/backend/npm-servers"
|
||||
if [[ -d "$LEGACY_NPM_SERVERS" ]]; then
|
||||
echo "[0b] Removing legacy backend/npm-servers/ (superseded by mcp-bundles)..."
|
||||
rm -rf "$LEGACY_NPM_SERVERS"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
@@ -179,6 +271,7 @@ rsync -a \
|
||||
--exclude='*.pyc' --exclude='.venv' \
|
||||
--exclude='data/tools' \
|
||||
--exclude='tests' --exclude='**/tests' \
|
||||
--exclude='.env' --exclude='.env.*' --exclude='**/.env' --exclude='**/.env.*' \
|
||||
"$PROJECT_ROOT/backend/" "$STAGING_DIR/backend/"
|
||||
# Create empty tools directory so the app has a place to write
|
||||
mkdir -p "$STAGING_DIR/backend/data/tools"
|
||||
|
||||
@@ -81,7 +81,9 @@ Write-Host "Verifying claude-agent-sdk..."
|
||||
& $PythonBin -c "import claude_agent_sdk; print('claude-agent-sdk installed')"
|
||||
if ($LASTEXITCODE -ne 0) { throw "claude-agent-sdk verification failed" }
|
||||
|
||||
# Cleanup
|
||||
# Cleanup. Drop test packages and any stale __pycache__/.pyc from the
|
||||
# upstream tarball — we want our own freshly-compiled bytecode (next
|
||||
# step), not whatever the upstream build happened to ship.
|
||||
Write-Host "Cleaning up..."
|
||||
Get-ChildItem -Path $PythonEnvDir -Recurse -Force -Directory `
|
||||
| Where-Object { $_.Name -in @('__pycache__','tests','test') } `
|
||||
@@ -89,12 +91,42 @@ Get-ChildItem -Path $PythonEnvDir -Recurse -Force -Directory `
|
||||
Get-ChildItem -Path $PythonEnvDir -Recurse -Force -Filter '*.pyc' `
|
||||
| Remove-Item -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Strip parts of the Python distribution we provably don't use at runtime.
|
||||
# Each removal here has been individually verified — conservative on purpose.
|
||||
# NOT removing pip/, babel/locale-data/, pygments lexers, or PIL — each had
|
||||
# at least one weak import-evidence trail.
|
||||
Write-Host "Stripping unused Python distribution files..."
|
||||
$ToStrip = @(
|
||||
(Join-Path $PythonEnvDir 'include'), # C headers — never used at runtime
|
||||
(Join-Path $PythonEnvDir 'lib\python3.13\idlelib'), # IDLE editor — headless backend has no GUI
|
||||
(Join-Path $PythonEnvDir 'lib\python3.13\tkinter'), # Tk GUI toolkit — same
|
||||
(Join-Path $PythonEnvDir 'lib\python3.13\ensurepip'), # Pip bootstrap — backend never installs at runtime
|
||||
(Join-Path $PythonEnvDir 'lib\python3.13\turtledemo'), # Educational drawing examples
|
||||
(Join-Path $PythonEnvDir 'share') # Man pages / desktop integration
|
||||
)
|
||||
foreach ($p in $ToStrip) {
|
||||
if (Test-Path $p) { Remove-Item -Recurse -Force $p -ErrorAction SilentlyContinue }
|
||||
}
|
||||
|
||||
# Pre-compile bytecode so cold backend startup skips parse+compile on
|
||||
# every imported .py. Worth ~5-10s on Windows under Defender (parsing
|
||||
# Python source is parser-bound; loading .pyc is just bytes). We cap
|
||||
# concurrency at 4 — `-j 0` (all cores) is fine on dev boxes but
|
||||
# unstable on small CI runners. Missing .pyc is non-fatal at runtime
|
||||
# (Python falls back to in-memory compile), so we warn rather than fail.
|
||||
Write-Host "Pre-compiling bytecode..."
|
||||
& $PythonBin -m compileall -q -j 4 (Join-Path $PythonEnvDir 'lib')
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "WARNING: some files failed to compile; runtime will fall back to in-memory compile." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$Size = (Get-ChildItem -Path $PythonEnvDir -Recurse -File `
|
||||
| Measure-Object -Property Length -Sum).Sum
|
||||
$SizeMB = [math]::Round($Size / 1MB, 1)
|
||||
$PycCount = (Get-ChildItem -Path $PythonEnvDir -Recurse -File -Filter '*.pyc' | Measure-Object).Count
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Python Environment Ready ==="
|
||||
Write-Host "Location: $PythonEnvDir"
|
||||
Write-Host ("Size: {0} MB" -f $SizeMB)
|
||||
Write-Host ("Size: {0} MB ({1} .pyc files)" -f $SizeMB, $PycCount)
|
||||
Write-Host ""
|
||||
|
||||
@@ -106,16 +106,54 @@ else
|
||||
echo "WARNING: Claude binary not found at $CLAUDE_BIN"
|
||||
fi
|
||||
|
||||
# Clean up build artifacts to reduce size
|
||||
# Clean up build artifacts to reduce size. Drop test packages and any
|
||||
# stale __pycache__/.pyc from the upstream Python tarball — we want our
|
||||
# own freshly-compiled bytecode (next step), not whatever the upstream
|
||||
# build happened to ship.
|
||||
echo "Cleaning up..."
|
||||
find "$PYTHON_ENV_DIR" -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||
find "$PYTHON_ENV_DIR" -name "*.pyc" -delete 2>/dev/null || true
|
||||
find "$PYTHON_ENV_DIR" -type d -name "tests" -exec rm -rf {} + 2>/dev/null || true
|
||||
find "$PYTHON_ENV_DIR" -type d -name "test" -exec rm -rf {} + 2>/dev/null || true
|
||||
|
||||
# Strip parts of the Python distribution we provably don't use at runtime.
|
||||
# Each removal here has been individually verified — see the audit notes in
|
||||
# the project plan. Doing this BEFORE compileall would also work, but doing
|
||||
# it after means the dirs are already definitely-not-imported (compileall
|
||||
# would have surfaced any backend code that touches them).
|
||||
#
|
||||
# Conservative on purpose. NOT removing pip/, babel/locale-data/, pygments
|
||||
# lexers, or PIL — each had at least one weak import-evidence trail.
|
||||
echo "Stripping unused Python distribution files..."
|
||||
# C headers — only needed when building C extensions, never at runtime.
|
||||
rm -rf "$PYTHON_ENV_DIR/include"
|
||||
# IDLE editor + Tk GUI toolkit — embedded headless backend has no UI.
|
||||
rm -rf "$PYTHON_ENV_DIR/lib/python3.13/idlelib"
|
||||
rm -rf "$PYTHON_ENV_DIR/lib/python3.13/tkinter"
|
||||
# Pip bootstrap module — backend never installs packages at runtime.
|
||||
rm -rf "$PYTHON_ENV_DIR/lib/python3.13/ensurepip"
|
||||
# Educational drawing examples that ship with stdlib — never imported.
|
||||
rm -rf "$PYTHON_ENV_DIR/lib/python3.13/turtledemo"
|
||||
# Man pages / desktop-integration files — embedded Python doesn't read these.
|
||||
rm -rf "$PYTHON_ENV_DIR/share"
|
||||
|
||||
# Pre-compile bytecode so cold backend startup skips the parse+compile
|
||||
# step on every imported .py. Worth ~5-10s on Windows under Defender
|
||||
# (parsing Python source is parser-bound; loading .pyc is just bytes).
|
||||
# Concurrency capped at 4 — `-j 0` (all cores) is fine on dev boxes
|
||||
# but unstable on small CI runners. Failures on individual files are
|
||||
# survivable (compileall continues on SyntaxError-tagged files used by
|
||||
# version-shim packages); a non-zero exit here would rather be visible
|
||||
# than silent so we don't `|| true` the whole thing — but missing .pyc
|
||||
# is non-fatal at runtime, so a hard fail isn't warranted either.
|
||||
echo "Pre-compiling bytecode..."
|
||||
"$PYTHON_BIN" -m compileall -q -j 4 "$PYTHON_ENV_DIR/lib" || \
|
||||
echo "WARNING: some files failed to compile; runtime will fall back to in-memory compile."
|
||||
|
||||
TOTAL_SIZE=$(du -sh "$PYTHON_ENV_DIR" | cut -f1)
|
||||
PYC_COUNT=$(find "$PYTHON_ENV_DIR" -name '*.pyc' -type f | wc -l | tr -d ' ')
|
||||
echo ""
|
||||
echo "=== Python Environment Ready ==="
|
||||
echo "Location: $PYTHON_ENV_DIR"
|
||||
echo "Size: $TOTAL_SIZE"
|
||||
echo "Size: $TOTAL_SIZE ($PYC_COUNT .pyc files)"
|
||||
echo ""
|
||||
|
||||
Reference in New Issue
Block a user