Skip to content

Commit 4c25d41

Browse files
committed
feat(build): add dependency-aware caching and binary build scripts
Solve cache invalidation and slow test issues by: 1. Tracking critical dependencies in cache keys 2. Separating binary builds from test suite 3. Adding intelligent cache management Cache Improvements: - New cache key utility tracks dependency versions - Auto-invalidates when dependencies update - Tracks @socketsecurity/{lib,packageurl-js,sdk,registry} - Prevents stale dependency versions in builds Build Scripts: - build:binaries - Build WASM/SEA/smol without tests - build:sea/smol/wasm - Build specific binaries - build:binaries:dev - Fast dev mode builds - clean:cache - Remove stale caches intelligently Test Performance: - Changed pretest from "build" to "build:cli" - Tests now skip slow binary builds (especially WASM) - Reduces test startup time significantly Files: - packages/build-infra/lib/cache-key.mjs - Dependency tracking - scripts/build-binaries.mjs - Binary orchestration - scripts/clean-cache.mjs - Cache management - package.json - New scripts and fixed pretest Usage: pnpm run build:smol --dev # Fast smol build pnpm run build:wasm # Just WASM (slow) pnpm test # Now fast - no binary rebuild pnpm run clean:cache # Remove old caches
1 parent 212606b commit 4c25d41

File tree

5 files changed

+438
-1
lines changed

5 files changed

+438
-1
lines changed

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@
171171
"build:force": "node scripts/build.mjs --force",
172172
"build:cli": "pnpm --filter @socketsecurity/cli run build",
173173
"build:watch": "pnpm --filter @socketsecurity/cli run build:watch",
174+
"build:binaries": "node scripts/build-binaries.mjs",
175+
"build:binaries:dev": "node scripts/build-binaries.mjs --dev",
176+
"build:sea": "node scripts/build-binaries.mjs --sea",
177+
"build:smol": "node scripts/build-binaries.mjs --smol",
178+
"build:wasm": "node scripts/build-binaries.mjs --wasm",
174179
"dev": "pnpm run build:watch",
175180
"// Quality Checks": "",
176181
"check": "node scripts/check.mjs",
@@ -198,12 +203,14 @@
198203
"wasm:setup": "node scripts/wasm/setup-build-env.mjs",
199204
"// Maintenance": "",
200205
"clean": "pnpm --filter \"./packages/**\" run clean",
206+
"clean:cache": "node scripts/clean-cache.mjs",
207+
"clean:cache:all": "node scripts/clean-cache.mjs --all",
201208
"update": "node scripts/update.mjs",
202209
"// Publishing": "",
203210
"publish": "node scripts/publish.mjs",
204211
"// Setup": "",
205212
"prepare": "husky",
206-
"pretest": "pnpm run build"
213+
"pretest": "pnpm run build:cli"
207214
},
208215
"lint-staged": {
209216
"*.{cjs,cts,js,json,md,mjs,mts,ts}": [
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* Generate cache keys with version and content hash for proper invalidation.
3+
*
4+
* Cache directory format: v{nodeVersion}-{arch}-{contentHash}-{pkgVersion}
5+
* Example: v24.10.0-arm64-b71671ba-2.1.5
6+
*
7+
* Cache-busting dependencies:
8+
* - Bootstrap: @socketsecurity/lib, @socketsecurity/packageurl-js
9+
* - CLI: @socketsecurity/lib, @socketsecurity/packageurl-js, @socketsecurity/sdk, @socketsecurity/registry
10+
* - CLI-with-sentry: @socketsecurity/lib, @socketsecurity/packageurl-js
11+
*/
12+
13+
import { createHash } from 'node:crypto'
14+
import { readFileSync } from 'node:fs'
15+
import { join, dirname } from 'node:path'
16+
import { platform, arch } from 'node:process'
17+
18+
/**
19+
* Critical dependencies that trigger cache invalidation.
20+
* When these packages are updated, caches must be rebuilt.
21+
*/
22+
const CACHE_BUSTING_DEPS = {
23+
bootstrap: [
24+
'@socketsecurity/lib',
25+
'@socketsecurity/packageurl-js',
26+
],
27+
cli: [
28+
'@socketsecurity/lib',
29+
'@socketsecurity/packageurl-js',
30+
'@socketsecurity/sdk',
31+
'@socketsecurity/registry',
32+
],
33+
'cli-with-sentry': [
34+
'@socketsecurity/lib',
35+
'@socketsecurity/packageurl-js',
36+
],
37+
}
38+
39+
/**
40+
* Get dependency versions from package.json.
41+
*
42+
* @param {string} packageJsonPath - Path to package.json
43+
* @param {string[]} depNames - Dependency names to extract
44+
* @returns {Record<string, string>} Dependency versions
45+
*/
46+
function getDependencyVersions(packageJsonPath, depNames) {
47+
try {
48+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
49+
const versions = {}
50+
51+
for (const depName of depNames) {
52+
const version = packageJson.dependencies?.[depName] || packageJson.devDependencies?.[depName]
53+
if (version) {
54+
versions[depName] = version
55+
}
56+
}
57+
58+
return versions
59+
} catch (error) {
60+
return {}
61+
}
62+
}
63+
64+
/**
65+
* Generate a cache key for a package build.
66+
*
67+
* @param {object} options
68+
* @param {string} options.nodeVersion - Node.js version (e.g., '24.10.0')
69+
* @param {string} [options.platform] - Platform (defaults to current)
70+
* @param {string} [options.arch] - Architecture (defaults to current)
71+
* @param {string} options.packageVersion - Package version from package.json
72+
* @param {string} [options.packageName] - Package name for dependency tracking (e.g., 'bootstrap', 'cli')
73+
* @param {string} [options.packageJsonPath] - Path to package.json for dependency resolution
74+
* @param {string[]} [options.contentFiles] - Files to hash for content-based invalidation
75+
* @param {string[]} [options.cacheBustingDeps] - Override cache-busting dependencies
76+
* @returns {string} Cache key
77+
*/
78+
export function generateCacheKey({
79+
nodeVersion,
80+
platform: targetPlatform = platform,
81+
arch: targetArch = arch,
82+
packageVersion,
83+
packageName,
84+
packageJsonPath,
85+
contentFiles = [],
86+
cacheBustingDeps,
87+
}) {
88+
// Hash content files.
89+
const hash = createHash('sha256')
90+
91+
for (const file of contentFiles) {
92+
try {
93+
const content = readFileSync(file, 'utf8')
94+
hash.update(content)
95+
} catch (error) {
96+
// File doesn't exist - use filename in hash.
97+
hash.update(file)
98+
}
99+
}
100+
101+
// Include cache-busting dependency versions.
102+
const depsToCheck = cacheBustingDeps || (packageName ? CACHE_BUSTING_DEPS[packageName] : null)
103+
if (depsToCheck && packageJsonPath) {
104+
const depVersions = getDependencyVersions(packageJsonPath, depsToCheck)
105+
// Sort for consistent hashing.
106+
const sortedDeps = Object.keys(depVersions).sort()
107+
for (const dep of sortedDeps) {
108+
hash.update(`${dep}@${depVersions[dep]}`)
109+
}
110+
}
111+
112+
const contentHash = hash.digest('hex').slice(0, 8)
113+
114+
// Format: v{nodeVersion}-{arch}-{contentHash}-{pkgVersion}
115+
return `v${nodeVersion}-${targetArch}-${contentHash}-${packageVersion.replace(/\./g, '')}`
116+
}
117+
118+
/**
119+
* Parse a cache key to extract components.
120+
*
121+
* @param {string} cacheKey
122+
* @returns {object|null}
123+
*/
124+
export function parseCacheKey(cacheKey) {
125+
const match = cacheKey.match(/^v([\d.]+)-(\w+)-([a-f0-9]+)-(\d+)$/)
126+
if (!match) return null
127+
128+
return {
129+
nodeVersion: match[1],
130+
arch: match[2],
131+
contentHash: match[3],
132+
packageVersion: match[4].replace(/(\d)(\d)(\d)/, '$1.$2.$3'), // Restore dots.
133+
}
134+
}
135+
136+
/**
137+
* Check if a cache key is still valid.
138+
*
139+
* @param {string} cacheKey
140+
* @param {object} currentOptions - Current build options (same as generateCacheKey)
141+
* @returns {boolean}
142+
*/
143+
export function isCacheValid(cacheKey, currentOptions) {
144+
const parsed = parseCacheKey(cacheKey)
145+
if (!parsed) return false
146+
147+
const currentKey = generateCacheKey(currentOptions)
148+
return cacheKey === currentKey
149+
}

packages/build-infra/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"./lib/cmake-builder": "./lib/cmake-builder.mjs",
1313
"./lib/emscripten-builder": "./lib/emscripten-builder.mjs",
1414
"./lib/esbuild-plugin-unicode-transform": "./lib/esbuild-plugin-unicode-transform.mjs",
15+
"./lib/cache-key": "./lib/cache-key.mjs",
1516
"./lib/extraction-cache": "./lib/extraction-cache.mjs",
1617
"./lib/fetch-with-retry": "./lib/fetch-with-retry.mjs",
1718
"./lib/patch-validator": "./lib/patch-validator.mjs",

scripts/build-binaries.mjs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Build Socket binaries (WASM, SEA, smol) without running tests.
4+
*
5+
* Usage:
6+
* pnpm run build:binaries # All binaries
7+
* pnpm run build:binaries --wasm # Just WASM
8+
* pnpm run build:binaries --sea # Just SEA
9+
* pnpm run build:binaries --smol # Just smol
10+
* pnpm run build:binaries --smol --sea # Multiple
11+
*/
12+
13+
import { spawn } from 'node:child_process'
14+
import { parseArgs } from 'node:util'
15+
16+
const { values } = parseArgs({
17+
options: {
18+
wasm: { type: 'boolean' },
19+
sea: { type: 'boolean' },
20+
smol: { type: 'boolean' },
21+
dev: { type: 'boolean' },
22+
prod: { type: 'boolean' },
23+
clean: { type: 'boolean' },
24+
},
25+
strict: false,
26+
})
27+
28+
// If no specific binary selected, build all.
29+
const buildAll = !values.wasm && !values.sea && !values.smol
30+
const buildWasm = buildAll || values.wasm
31+
const buildSea = buildAll || values.sea
32+
const buildSmol = buildAll || values.smol
33+
34+
const tasks = []
35+
36+
// Build bootstrap first (required for all binaries).
37+
console.log('\n🔨 Building bootstrap...\n')
38+
tasks.push({ name: 'bootstrap', cmd: 'pnpm', args: ['--filter', '@socketsecurity/bootstrap', 'run', 'build'] })
39+
40+
// Build CLI (required for binaries).
41+
console.log('🔨 Building CLI...\n')
42+
tasks.push({ name: 'cli', cmd: 'pnpm', args: ['--filter', '@socketsecurity/cli', 'run', 'build'] })
43+
44+
// Build WASM (slowest, optional).
45+
if (buildWasm) {
46+
const wasmArgs = ['run', values.dev ? 'wasm:build:dev' : 'wasm:build']
47+
console.log(`🔨 Building WASM ${values.dev ? '(dev mode)' : ''}...\n`)
48+
tasks.push({ name: 'wasm', cmd: 'pnpm', args: wasmArgs })
49+
}
50+
51+
// Build SEA binary.
52+
if (buildSea) {
53+
const seaArgs = ['--filter', '@socketsecurity/node-sea-builder', 'run', 'build']
54+
if (values.dev) seaArgs.push('--dev')
55+
if (values.prod) seaArgs.push('--prod')
56+
if (values.clean) seaArgs.push('--clean')
57+
console.log('🔨 Building SEA binary...\n')
58+
tasks.push({ name: 'sea', cmd: 'pnpm', args: seaArgs })
59+
}
60+
61+
// Build smol binary.
62+
if (buildSmol) {
63+
const smolArgs = ['--filter', '@socketsecurity/node-smol-builder', 'run', 'build']
64+
if (values.dev) smolArgs.push('--dev')
65+
if (values.prod) smolArgs.push('--prod')
66+
if (values.clean) smolArgs.push('--clean')
67+
console.log('🔨 Building smol binary...\n')
68+
tasks.push({ name: 'smol', cmd: 'pnpm', args: smolArgs })
69+
}
70+
71+
// Run tasks sequentially.
72+
async function runTask(task) {
73+
return new Promise((resolve, reject) => {
74+
const proc = spawn(task.cmd, task.args, {
75+
stdio: 'inherit',
76+
shell: true,
77+
})
78+
79+
proc.on('close', (code) => {
80+
if (code === 0) {
81+
resolve()
82+
} else {
83+
reject(new Error(`${task.name} build failed with code ${code}`))
84+
}
85+
})
86+
})
87+
}
88+
89+
(async () => {
90+
const startTime = Date.now()
91+
92+
for (const task of tasks) {
93+
try {
94+
await runTask(task)
95+
} catch (error) {
96+
console.error(`\n❌ Build failed: ${error.message}`)
97+
process.exit(1)
98+
}
99+
}
100+
101+
const duration = ((Date.now() - startTime) / 1000 / 60).toFixed(1)
102+
console.log(`\n✅ All binaries built successfully in ${duration}m`)
103+
})()

0 commit comments

Comments
 (0)