mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 15:24:40 +01:00
* Fix Windows agent harness links in postinstall * Update build/npm/postinstall.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
354 lines
12 KiB
TypeScript
354 lines
12 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as fs from 'fs';
|
|
import path from 'path';
|
|
import * as os from 'os';
|
|
import * as child_process from 'child_process';
|
|
import { dirs } from './dirs.ts';
|
|
import { root, stateFile, stateContentsFile, computeState, computeContents, isUpToDate } from './installStateHash.ts';
|
|
|
|
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
const rootNpmrcConfigKeys = getNpmrcConfigKeys(path.join(root, '.npmrc'));
|
|
|
|
function log(dir: string, message: string) {
|
|
if (process.stdout.isTTY) {
|
|
console.log(`\x1b[34m[${dir}]\x1b[0m`, message);
|
|
} else {
|
|
console.log(`[${dir}]`, message);
|
|
}
|
|
}
|
|
|
|
function run(command: string, args: string[], opts: child_process.SpawnSyncOptions) {
|
|
log(opts.cwd as string || '.', '$ ' + command + ' ' + args.join(' '));
|
|
|
|
const result = child_process.spawnSync(command, args, opts);
|
|
|
|
if (result.error) {
|
|
console.error(`ERR Failed to spawn process: ${result.error}`);
|
|
process.exit(1);
|
|
} else if (result.status !== 0) {
|
|
console.error(`ERR Process exited with code: ${result.status}`);
|
|
process.exit(result.status);
|
|
}
|
|
}
|
|
|
|
function spawnAsync(command: string, args: string[], opts: child_process.SpawnOptions): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const child = child_process.spawn(command, args, { ...opts, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
let output = '';
|
|
child.stdout?.on('data', (data: Buffer) => { output += data.toString(); });
|
|
child.stderr?.on('data', (data: Buffer) => { output += data.toString(); });
|
|
child.on('error', reject);
|
|
child.on('close', (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(`Process exited with code: ${code}\n${output}`));
|
|
} else {
|
|
resolve(output);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function npmInstallAsync(dir: string, opts?: child_process.SpawnOptions): Promise<void> {
|
|
const finalOpts: child_process.SpawnOptions = {
|
|
env: { ...process.env },
|
|
...(opts ?? {}),
|
|
cwd: path.join(root, dir),
|
|
shell: true,
|
|
};
|
|
|
|
const command = process.env['npm_command'] || 'install';
|
|
|
|
if (process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'] && /^(.build\/distro\/npm\/)?remote$/.test(dir)) {
|
|
const syncOpts: child_process.SpawnSyncOptions = {
|
|
env: finalOpts.env,
|
|
cwd: root,
|
|
stdio: 'inherit',
|
|
shell: true,
|
|
};
|
|
const userinfo = os.userInfo();
|
|
log(dir, `Installing dependencies inside container ${process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME']}...`);
|
|
|
|
if (process.env['npm_config_arch'] === 'arm64') {
|
|
run('sudo', ['docker', 'run', '--rm', '--privileged', 'multiarch/qemu-user-static', '--reset', '-p', 'yes'], syncOpts);
|
|
}
|
|
run('sudo', [
|
|
'docker', 'run',
|
|
'-e', 'GITHUB_TOKEN',
|
|
'-v', `${process.env['VSCODE_HOST_MOUNT']}:/root/vscode`,
|
|
'-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.netrc:/root/.netrc`,
|
|
'-v', `${process.env['VSCODE_NPMRC_PATH']}:/root/.npmrc`,
|
|
'-w', path.resolve('/root/vscode', dir),
|
|
process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'],
|
|
'sh', '-c', `\"chown -R root:root ${path.resolve('/root/vscode', dir)} && export PATH="/root/vscode/.build/nodejs-musl/usr/local/bin:$PATH" && npm i -g node-gyp-build && npm ci\"`
|
|
], syncOpts);
|
|
run('sudo', ['chown', '-R', `${userinfo.uid}:${userinfo.gid}`, `${path.resolve(root, dir)}`], syncOpts);
|
|
} else {
|
|
log(dir, 'Installing dependencies...');
|
|
const output = await spawnAsync(npm, command.split(' '), finalOpts);
|
|
if (output.trim()) {
|
|
for (const line of output.trim().split('\n')) {
|
|
log(dir, line);
|
|
}
|
|
}
|
|
}
|
|
removeParcelWatcherPrebuild(dir);
|
|
}
|
|
|
|
function setNpmrcConfig(dir: string, env: NodeJS.ProcessEnv) {
|
|
const npmrcPath = path.join(root, dir, '.npmrc');
|
|
const lines = fs.readFileSync(npmrcPath, 'utf8').split('\n');
|
|
|
|
for (const line of lines) {
|
|
const trimmedLine = line.trim();
|
|
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
|
const [key, value] = trimmedLine.split('=');
|
|
env[`npm_config_${key}`] = value.replace(/^"(.*)"$/, '$1');
|
|
}
|
|
}
|
|
|
|
// Use our bundled node-gyp version
|
|
env['npm_config_node_gyp'] =
|
|
process.platform === 'win32'
|
|
? path.join(import.meta.dirname, 'gyp', 'node_modules', '.bin', 'node-gyp.cmd')
|
|
: path.join(import.meta.dirname, 'gyp', 'node_modules', '.bin', 'node-gyp');
|
|
|
|
// Force node-gyp to use process.config on macOS
|
|
// which defines clang variable as expected. Otherwise we
|
|
// run into compilation errors due to incorrect compiler
|
|
// configuration.
|
|
// NOTE: This means the process.config should contain
|
|
// the correct clang variable. So keep the version check
|
|
// in preinstall sync with this logic.
|
|
// Change was first introduced in https://github.com/nodejs/node/commit/6e0a2bb54c5bbeff0e9e33e1a0c683ed980a8a0f
|
|
if ((dir === 'remote' || dir === 'build') && process.platform === 'darwin') {
|
|
env['npm_config_force_process_config'] = 'true';
|
|
} else {
|
|
delete env['npm_config_force_process_config'];
|
|
}
|
|
|
|
if (dir === 'build') {
|
|
env['npm_config_target'] = process.versions.node;
|
|
env['npm_config_arch'] = process.arch;
|
|
}
|
|
}
|
|
|
|
function removeParcelWatcherPrebuild(dir: string) {
|
|
const parcelModuleFolder = path.join(root, dir, 'node_modules', '@parcel');
|
|
if (!fs.existsSync(parcelModuleFolder)) {
|
|
return;
|
|
}
|
|
|
|
const parcelModules = fs.readdirSync(parcelModuleFolder);
|
|
for (const moduleName of parcelModules) {
|
|
if (moduleName.startsWith('watcher-')) {
|
|
const modulePath = path.join(parcelModuleFolder, moduleName);
|
|
fs.rmSync(modulePath, { recursive: true, force: true });
|
|
log(dir, `Removed @parcel/watcher prebuilt module ${modulePath}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getNpmrcConfigKeys(npmrcPath: string): string[] {
|
|
if (!fs.existsSync(npmrcPath)) {
|
|
return [];
|
|
}
|
|
const lines = fs.readFileSync(npmrcPath, 'utf8').split('\n');
|
|
const keys: string[] = [];
|
|
for (const line of lines) {
|
|
const trimmedLine = line.trim();
|
|
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
|
const eqIndex = trimmedLine.indexOf('=');
|
|
if (eqIndex > 0) {
|
|
keys.push(trimmedLine.substring(0, eqIndex).trim());
|
|
}
|
|
}
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
function clearInheritedNpmrcConfig(dir: string, env: NodeJS.ProcessEnv): void {
|
|
const dirNpmrcPath = path.join(root, dir, '.npmrc');
|
|
if (fs.existsSync(dirNpmrcPath)) {
|
|
return;
|
|
}
|
|
|
|
for (const key of rootNpmrcConfigKeys) {
|
|
const envKey = `npm_config_${key.replace(/-/g, '_')}`;
|
|
delete env[envKey];
|
|
}
|
|
}
|
|
|
|
function ensureAgentHarnessLink(sourceRelativePath: string, linkPath: string): 'existing' | 'junction' | 'symlink' | 'hard link' {
|
|
if (fs.existsSync(linkPath)) {
|
|
return 'existing';
|
|
}
|
|
|
|
const sourcePath = path.resolve(path.dirname(linkPath), sourceRelativePath);
|
|
const isDirectory = fs.statSync(sourcePath).isDirectory();
|
|
|
|
try {
|
|
if (process.platform === 'win32' && isDirectory) {
|
|
fs.symlinkSync(sourcePath, linkPath, 'junction');
|
|
return 'junction';
|
|
}
|
|
|
|
fs.symlinkSync(sourceRelativePath, linkPath, isDirectory ? 'dir' : 'file');
|
|
return 'symlink';
|
|
} catch (error) {
|
|
if (process.platform === 'win32' && !isDirectory && (error as NodeJS.ErrnoException).code === 'EPERM') {
|
|
fs.linkSync(sourcePath, linkPath);
|
|
return 'hard link';
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function runWithConcurrency(tasks: (() => Promise<void>)[], concurrency: number): Promise<void> {
|
|
const errors: Error[] = [];
|
|
let index = 0;
|
|
|
|
async function worker() {
|
|
while (index < tasks.length) {
|
|
const i = index++;
|
|
try {
|
|
await tasks[i]();
|
|
} catch (err) {
|
|
errors.push(err as Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
await Promise.all(Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker()));
|
|
|
|
if (errors.length > 0) {
|
|
for (const err of errors) {
|
|
console.error(err.message);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
if (!process.env['VSCODE_FORCE_INSTALL'] && isUpToDate()) {
|
|
log('.', 'All dependencies up to date, skipping postinstall.');
|
|
child_process.execSync('git config pull.rebase merges');
|
|
child_process.execSync('git config blame.ignoreRevsFile .git-blame-ignore-revs');
|
|
return;
|
|
}
|
|
|
|
const _state = computeState();
|
|
|
|
const nativeTasks: (() => Promise<void>)[] = [];
|
|
const parallelTasks: (() => Promise<void>)[] = [];
|
|
|
|
for (const dir of dirs) {
|
|
if (dir === '') {
|
|
removeParcelWatcherPrebuild(dir);
|
|
continue; // already executed in root
|
|
}
|
|
|
|
if (dir === 'build') {
|
|
nativeTasks.push(() => {
|
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
if (process.env['CC']) { env['CC'] = 'gcc'; }
|
|
if (process.env['CXX']) { env['CXX'] = 'g++'; }
|
|
if (process.env['CXXFLAGS']) { env['CXXFLAGS'] = ''; }
|
|
if (process.env['LDFLAGS']) { env['LDFLAGS'] = ''; }
|
|
setNpmrcConfig('build', env);
|
|
return npmInstallAsync('build', { env });
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (/^(.build\/distro\/npm\/)?remote$/.test(dir)) {
|
|
const remoteDir = dir;
|
|
nativeTasks.push(() => {
|
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
if (process.env['VSCODE_REMOTE_CC']) {
|
|
env['CC'] = process.env['VSCODE_REMOTE_CC'];
|
|
} else {
|
|
delete env['CC'];
|
|
}
|
|
if (process.env['VSCODE_REMOTE_CXX']) {
|
|
env['CXX'] = process.env['VSCODE_REMOTE_CXX'];
|
|
} else {
|
|
delete env['CXX'];
|
|
}
|
|
if (process.env['CXXFLAGS']) { delete env['CXXFLAGS']; }
|
|
if (process.env['CFLAGS']) { delete env['CFLAGS']; }
|
|
if (process.env['LDFLAGS']) { delete env['LDFLAGS']; }
|
|
if (process.env['VSCODE_REMOTE_CXXFLAGS']) { env['CXXFLAGS'] = process.env['VSCODE_REMOTE_CXXFLAGS']; }
|
|
if (process.env['VSCODE_REMOTE_LDFLAGS']) { env['LDFLAGS'] = process.env['VSCODE_REMOTE_LDFLAGS']; }
|
|
if (process.env['VSCODE_REMOTE_NODE_GYP']) { env['npm_config_node_gyp'] = process.env['VSCODE_REMOTE_NODE_GYP']; }
|
|
setNpmrcConfig('remote', env);
|
|
return npmInstallAsync(remoteDir, { env });
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const taskDir = dir;
|
|
parallelTasks.push(() => {
|
|
const env = { ...process.env };
|
|
clearInheritedNpmrcConfig(taskDir, env);
|
|
return npmInstallAsync(taskDir, { env });
|
|
});
|
|
}
|
|
|
|
// Native dirs (build, remote) run sequentially to avoid node-gyp conflicts
|
|
for (const task of nativeTasks) {
|
|
await task();
|
|
}
|
|
|
|
// JS-only dirs run in parallel
|
|
const concurrency = Math.min(os.cpus().length, 8);
|
|
log('.', `Running ${parallelTasks.length} npm installs with concurrency ${concurrency}...`);
|
|
await runWithConcurrency(parallelTasks, concurrency);
|
|
|
|
child_process.execSync('git config pull.rebase merges');
|
|
child_process.execSync('git config blame.ignoreRevsFile .git-blame-ignore-revs');
|
|
|
|
fs.writeFileSync(stateFile, JSON.stringify(_state));
|
|
fs.writeFileSync(stateContentsFile, JSON.stringify(computeContents()));
|
|
|
|
// Symlink .claude/ files to their canonical locations to test Claude agent harness
|
|
const claudeDir = path.join(root, '.claude');
|
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
|
|
const claudeMdLink = path.join(claudeDir, 'CLAUDE.md');
|
|
const claudeMdLinkType = ensureAgentHarnessLink(path.join('..', '.github', 'copilot-instructions.md'), claudeMdLink);
|
|
if (claudeMdLinkType !== 'existing') {
|
|
log('.', `Created ${claudeMdLinkType} .claude/CLAUDE.md -> .github/copilot-instructions.md`);
|
|
}
|
|
|
|
const claudeSkillsLink = path.join(claudeDir, 'skills');
|
|
const claudeSkillsLinkType = ensureAgentHarnessLink(path.join('..', '.agents', 'skills'), claudeSkillsLink);
|
|
if (claudeSkillsLinkType !== 'existing') {
|
|
log('.', `Created ${claudeSkillsLinkType} .claude/skills -> .agents/skills`);
|
|
}
|
|
|
|
// Temporary: patch @github/copilot-sdk session.js to fix ESM import
|
|
// (missing .js extension on vscode-jsonrpc/node). Fixed upstream in v0.1.32.
|
|
// TODO: Remove once @github/copilot-sdk is updated to >=0.1.32
|
|
for (const dir of ['', 'remote']) {
|
|
const sessionFile = path.join(root, dir, 'node_modules', '@github', 'copilot-sdk', 'dist', 'session.js');
|
|
if (fs.existsSync(sessionFile)) {
|
|
const content = fs.readFileSync(sessionFile, 'utf8');
|
|
const patched = content.replace(/from "vscode-jsonrpc\/node"/g, 'from "vscode-jsonrpc/node.js"');
|
|
if (content !== patched) {
|
|
fs.writeFileSync(sessionFile, patched);
|
|
log(dir || '.', 'Patched @github/copilot-sdk session.js (vscode-jsonrpc ESM import fix)');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|