[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:
ciregenz
2026-04-25 03:05:08 -07:00
parent 801268d6b7
commit 94ea07dd0c
15 changed files with 142480 additions and 92 deletions
+110 -27
View File
@@ -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
View File
@@ -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"
+34 -2
View File
@@ -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 ""
+40 -2
View File
@@ -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 ""