Skip to content
Draft
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
1 change: 1 addition & 0 deletions build/darwin/create-universal-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ async function main(buildDir?: string) {
const asarRelativePath = path.join('Contents', 'Resources', 'app', 'node_modules.asar');
const outAppPath = path.join(buildDir, `VSCode-darwin-${arch}`, appName);
const productJsonPath = path.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json');
crossCopyPlatformDir(x64AppPath, arm64AppPath, path.join('Contents', 'Resources', 'app', 'node-compile-cache'));
Comment thread
deepak1556 marked this conversation as resolved.

// Copilot SDK ships platform-specific native binaries that npm only installs
// for the host architecture. The universal app merger requires both builds to
Expand Down
21 changes: 20 additions & 1 deletion build/gulpfile.vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import globCallback from 'glob';
import rceditCallback from 'rcedit';
import { spawnTsgo } from './lib/tsgo.ts';
import { runEsbuildTranspile, runEsbuildBundle } from './lib/esbuild.ts';
import { generateNodeCompileCache, shouldGenerateNodeCompileCache } from './lib/nodeCompileCache.ts';


const glob = promisify(globCallback);
Expand Down Expand Up @@ -158,6 +159,10 @@ const bootstrapEntryPoints = [
'out-build/cli.js',
'out-build/bootstrap-fork.js'
];
const bootstrapBundleEntryPoints = [
...bootstrapEntryPoints,
'out-build/mainImpl.js'
];

const bundleVSCodeTask = task.define('bundle-vscode', task.series(
util.rimraf('out-vscode'),
Expand All @@ -172,7 +177,7 @@ const bundleVSCodeTask = task.define('bundle-vscode', task.series(
src: 'out-build',
entryPoints: [
...vscodeEntryPoints,
...bootstrapEntryPoints
...bootstrapBundleEntryPoints
],
resources: vscodeResources,
skipTSBoilerplateRemoval: entryPoint => entryPoint === 'vs/code/electron-browser/workbench/workbench' || entryPoint === 'vs/sessions/electron-browser/sessions'
Expand Down Expand Up @@ -700,6 +705,17 @@ function prepareCopilotRipgrepShimTask(platform: string, arch: string, destinati
};
}

function generateNodeCompileCacheTask(platform: string, destinationFolderName: string) {
const outputDirectory = path.join(path.dirname(root), destinationFolderName);

return () => generateNodeCompileCache(
platform,
outputDirectory,
util.getVersionedResourcesFolder(platform, commit!),
product
);
}

const buildRoot = path.dirname(root);

const BUILD_TARGETS = [
Expand Down Expand Up @@ -731,6 +747,9 @@ BUILD_TARGETS.forEach(buildTarget => {
if (platform === 'win32') {
packageTasks.push(patchWin32DependenciesTask(destinationFolderName));
}
if (shouldGenerateNodeCompileCache(platform, arch, product)) {
packageTasks.push(generateNodeCompileCacheTask(platform, destinationFolderName));
}

const vscodeTaskCI = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series(...packageTasks));
task.task(vscodeTaskCI);
Expand Down
155 changes: 155 additions & 0 deletions build/lib/nodeCompileCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as cp from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

const nodeCompileCacheKinds = ['main', 'extension-host', 'shared-process', 'pty-host', 'agent-host'] as const;
const nodeCompileCacheTagPattern = /^v\d+\.\d+\.\d+-(arm64|x64)-[0-9a-f]{8}$/;
const nodeCompileCacheEntryPattern = /^[0-9a-f]{8}$/;

export interface INodeCompileCacheProduct {
readonly applicationName: string;
readonly nameLong: string;
readonly nameShort: string;
readonly quality?: string;
}

export interface INodeCompileCachePaths {
readonly application: string;
readonly cacheDirectory: string;
}

export function shouldGenerateNodeCompileCache(platform: string, arch: string, product: INodeCompileCacheProduct): boolean {
if (!product.quality) {
return false;
}

return platform === 'darwin' ? arch === 'arm64' : (platform === 'linux' || platform === 'win32') && arch === 'x64';
}

export function getNodeCompileCachePaths(platform: string, outputDirectory: string, versionedResourcesFolder: string, product: INodeCompileCacheProduct): INodeCompileCachePaths {
if (platform === 'darwin') {
const applicationRoot = path.join(outputDirectory, `${product.nameLong}.app`);
return {
application: path.join(applicationRoot, 'Contents', 'MacOS', product.nameShort),
cacheDirectory: path.join(applicationRoot, 'Contents', 'Resources', 'app', 'node-compile-cache')
};
}

const application = platform === 'win32'
? path.join(outputDirectory, `${product.nameShort}.exe`)
: path.join(outputDirectory, product.applicationName);

return {
application,
cacheDirectory: path.join(outputDirectory, versionedResourcesFolder, 'resources', 'app', 'node-compile-cache')
};
}

export function createNodeCompileCacheGenerationEnvironment(portableDirectory: string, parentEnvironment: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
const env = { ...parentEnvironment };
delete env.ELECTRON_RUN_AS_NODE;
delete env.NODE_COMPILE_CACHE;
delete env.NODE_COMPILE_CACHE_PORTABLE;
delete env.NODE_COMPILE_CACHE_READONLY;
delete env.NODE_DISABLE_COMPILE_CACHE;
delete env.VSCODE_DEV;
delete env.VSCODE_MEASURE_NODE_COMPILE_CACHE;
delete env.VSCODE_NODE_COMPILE_CACHE_KIND;
delete env.VSCODE_NODE_COMPILE_CACHE_MEASUREMENTS;
delete env.VSCODE_NODE_COMPILE_CACHE_ROOT;
env.VSCODE_GENERATE_NODE_COMPILE_CACHE = '1';
env.VSCODE_PORTABLE = portableDirectory;
return env;
}

export async function generateNodeCompileCache(platform: string, outputDirectory: string, versionedResourcesFolder: string, product: INodeCompileCacheProduct): Promise<void> {
const paths = getNodeCompileCachePaths(platform, outputDirectory, versionedResourcesFolder, product);
const temporaryDirectory = platform === 'win32' ? os.tmpdir() : '/tmp';
const portableDirectory = await fs.promises.mkdtemp(path.join(temporaryDirectory, 'vscode-cache-'));

await fs.promises.rm(paths.cacheDirectory, { recursive: true, force: true });

try {
const env = createNodeCompileCacheGenerationEnvironment(portableDirectory);
await runCacheGeneration(paths.application, paths.cacheDirectory, env);
const totalCacheEntryCount = await validateNodeCompileCache(paths.cacheDirectory, process.arch);
console.log(`Generated ${totalCacheEntryCount} total Node.js compile cache entries in ${paths.cacheDirectory}.`);
} finally {
await fs.promises.rm(portableDirectory, { recursive: true, force: true });
}
}

export async function validateNodeCompileCache(rootDirectory: string, architecture: string): Promise<number> {
let totalCacheEntryCount = 0;
for (const kind of nodeCompileCacheKinds) {
const kindDirectory = path.join(rootDirectory, kind);
const entries = await fs.promises.readdir(kindDirectory, { withFileTypes: true });
const tagDirectories = entries.filter(entry => entry.isDirectory() && nodeCompileCacheTagPattern.exec(entry.name)?.[1] === architecture);
if (tagDirectories.length !== 1) {
throw new Error(`Node.js compile cache generation produced ${tagDirectories.length} ${architecture} version-tag directories for ${kind} in ${kindDirectory}; expected exactly one.`);
}
if (!entries.some(entry => entry.isFile() && entry.name === '.ready')) {
throw new Error(`Node.js compile cache generation did not produce a readiness marker for ${kind} in ${kindDirectory}.`);
}
const unexpectedEntries = entries.filter(entry => entry.name !== '.ready' && entry.name !== tagDirectories[0].name);
if (unexpectedEntries.length > 0) {
throw new Error(`Node.js compile cache generation produced unexpected ${kind} entries in ${kindDirectory}: ${unexpectedEntries.map(entry => entry.name).join(', ')}.`);
}

const cacheDirectory = path.join(kindDirectory, tagDirectories[0].name);
const cacheEntries = await fs.promises.readdir(cacheDirectory, { withFileTypes: true });
const validCacheEntries = cacheEntries.filter(entry => entry.isFile() && nodeCompileCacheEntryPattern.test(entry.name));
const cacheEntryCount = validCacheEntries.length;
if (cacheEntryCount === 0) {
throw new Error(`Node.js compile cache generation produced no ${kind} cache entries in ${cacheDirectory}.`);
}
const unexpectedCacheEntries = cacheEntries.filter(entry => !entry.isFile() || !nodeCompileCacheEntryPattern.test(entry.name));
if (unexpectedCacheEntries.length > 0) {
throw new Error(`Node.js compile cache generation produced unexpected ${kind} cache entries in ${cacheDirectory}: ${unexpectedCacheEntries.map(entry => entry.name).join(', ')}.`);
}
totalCacheEntryCount += cacheEntryCount;
console.log(`Generated ${cacheEntryCount} ${kind} Node.js compile cache entries in ${cacheDirectory}.`);
await fs.promises.rm(path.join(kindDirectory, '.ready'));
}
return totalCacheEntryCount;
}

function runCacheGeneration(application: string, cacheDirectory: string, env: NodeJS.ProcessEnv): Promise<void> {
return new Promise((resolve, reject) => {
const child = cp.spawn(application, [], {
env,
stdio: ['ignore', 'pipe', 'pipe']
});
let output = '';
let didTimeOut = false;
const timeout = setTimeout(() => {
didTimeOut = true;
child.kill();
}, 120_000);

child.stdout.on('data', chunk => output += chunk.toString());
child.stderr.on('data', chunk => output += chunk.toString());
child.on('error', error => {
clearTimeout(timeout);
reject(error);
});
child.on('close', (code, signal) => {
clearTimeout(timeout);
if (didTimeOut) {
const missingKinds = nodeCompileCacheKinds.filter(kind => !fs.existsSync(path.join(cacheDirectory, kind, '.ready')));
const missingKindsMessage = missingKinds.length > 0 ? ` Missing readiness markers: ${missingKinds.join(', ')}.` : '';
reject(new Error(`Node.js compile cache generation timed out.${missingKindsMessage}\n${output}`));
} else if (code !== 0) {
reject(new Error(`Node.js compile cache generation exited with code ${code} and signal ${signal ?? 'none'}.\n${output}`));
} else {
resolve();
}
});
});
}
143 changes: 143 additions & 0 deletions build/lib/test/nodeCompileCache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import assert from 'assert';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { suite, test } from 'node:test';
import { createNodeCompileCacheGenerationEnvironment, getNodeCompileCachePaths, shouldGenerateNodeCompileCache, validateNodeCompileCache, type INodeCompileCacheProduct } from '../nodeCompileCache.ts';

const product: INodeCompileCacheProduct = {
applicationName: 'code-oss',
nameLong: 'Code - OSS',
nameShort: 'Code - OSS'
};
const cacheKinds = ['main', 'extension-host', 'shared-process', 'pty-host', 'agent-host'];

async function createCacheOutput(cacheDirectory: string, tag: string, unexpectedEntry?: string): Promise<void> {
for (const kind of cacheKinds) {
const kindDirectory = path.join(cacheDirectory, kind);
const tagDirectory = path.join(kindDirectory, tag);
await fs.promises.mkdir(tagDirectory, { recursive: true });
await fs.promises.writeFile(path.join(kindDirectory, '.ready'), '');
await fs.promises.writeFile(path.join(tagDirectory, '12345678'), '');
if (unexpectedEntry) {
await fs.promises.writeFile(path.join(kindDirectory, unexpectedEntry), '');
}
}
}

suite('Node compile cache', () => {
test('resolves packaged application and cache paths', () => {
assert.deepStrictEqual({
darwin: getNodeCompileCachePaths('darwin', '/build/VSCode-darwin-x64', '', product),
linux: getNodeCompileCachePaths('linux', '/build/VSCode-linux-x64', '', product),
win32: getNodeCompileCachePaths('win32', 'C:\\build\\VSCode-win32-x64', '1234567890', product)
}, {
darwin: {
application: path.join('/build/VSCode-darwin-x64', 'Code - OSS.app', 'Contents', 'MacOS', 'Code - OSS'),
cacheDirectory: path.join('/build/VSCode-darwin-x64', 'Code - OSS.app', 'Contents', 'Resources', 'app', 'node-compile-cache')
},
linux: {
application: path.join('/build/VSCode-linux-x64', 'code-oss'),
cacheDirectory: path.join('/build/VSCode-linux-x64', 'resources', 'app', 'node-compile-cache')
},
win32: {
application: path.join('C:\\build\\VSCode-win32-x64', 'Code - OSS.exe'),
cacheDirectory: path.join('C:\\build\\VSCode-win32-x64', '1234567890', 'resources', 'app', 'node-compile-cache')
}
});
});

test('generates caches only for native product build targets', () => {
assert.deepStrictEqual({
oss: shouldGenerateNodeCompileCache('darwin', 'arm64', product),
darwinX64: shouldGenerateNodeCompileCache('darwin', 'x64', { ...product, quality: 'insider' }),
darwinArm64: shouldGenerateNodeCompileCache('darwin', 'arm64', { ...product, quality: 'insider' }),
linuxX64: shouldGenerateNodeCompileCache('linux', 'x64', { ...product, quality: 'insider' }),
linuxArm64: shouldGenerateNodeCompileCache('linux', 'arm64', { ...product, quality: 'insider' }),
win32X64: shouldGenerateNodeCompileCache('win32', 'x64', { ...product, quality: 'insider' }),
win32Arm64: shouldGenerateNodeCompileCache('win32', 'arm64', { ...product, quality: 'insider' })
}, {
oss: false,
darwinX64: false,
darwinArm64: true,
linuxX64: true,
linuxArm64: false,
win32X64: true,
win32Arm64: false
});
});

test('creates an isolated cache generation environment', () => {
const inheritedEnvironment = {
PATH: '/bin',
ELECTRON_RUN_AS_NODE: '1',
NODE_COMPILE_CACHE: '/node-cache',
NODE_COMPILE_CACHE_PORTABLE: '1',
NODE_COMPILE_CACHE_READONLY: '1',
NODE_DISABLE_COMPILE_CACHE: '1',
VSCODE_DEV: '1',
VSCODE_GENERATE_NODE_COMPILE_CACHE: '0',
VSCODE_MEASURE_NODE_COMPILE_CACHE: '1',
VSCODE_NODE_COMPILE_CACHE_KIND: 'main',
VSCODE_NODE_COMPILE_CACHE_MEASUREMENTS: '/measurements',
VSCODE_NODE_COMPILE_CACHE_ROOT: '/cache',
VSCODE_PORTABLE: '/inherited-portable'
};

assert.deepStrictEqual(createNodeCompileCacheGenerationEnvironment('/portable', inheritedEnvironment), {
PATH: '/bin',
VSCODE_GENERATE_NODE_COMPILE_CACHE: '1',
VSCODE_PORTABLE: '/portable'
});
});

test('rejects cache output with unexpected role entries', async () => {
const cacheDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'vscode-node-compile-cache-test-'));

try {
await createCacheOutput(cacheDirectory, 'v24.20.0-x64-12345678', 'manifest.jsonl');

await assert.rejects(
validateNodeCompileCache(cacheDirectory, 'x64'),
/unexpected main entries.*manifest\.jsonl/
);
} finally {
await fs.promises.rm(cacheDirectory, { recursive: true, force: true });
}
});

test('validates cache output and removes readiness markers', async () => {
const cacheDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'vscode-node-compile-cache-test-'));

try {
await createCacheOutput(cacheDirectory, 'v24.20.0-x64-12345678');

assert.strictEqual(await validateNodeCompileCache(cacheDirectory, 'x64'), cacheKinds.length);
for (const kind of cacheKinds) {
assert.strictEqual(fs.existsSync(path.join(cacheDirectory, kind, '.ready')), false);
}
} finally {
await fs.promises.rm(cacheDirectory, { recursive: true, force: true });
}
});

test('rejects cache output with a UID-specific tag', async () => {
const cacheDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'vscode-node-compile-cache-test-'));

try {
await createCacheOutput(cacheDirectory, 'v24.20.0-x64-12345678-1000');

await assert.rejects(
validateNodeCompileCache(cacheDirectory, 'x64'),
/produced 0 x64 version-tag directories for main/
);
} finally {
await fs.promises.rm(cacheDirectory, { recursive: true, force: true });
}
});
});
Loading
Loading