From 5b771fc25ecd4fa8e91d01ec7e9f3066a5730f05 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 18 Mar 2026 12:10:30 -0700 Subject: [PATCH] Also handle copilot native deps in remote server build (#302603) * Remote server copilot build fixes * Don't strip copilot from stable builds * Handle all platforms for copilot --- build/gulpfile.reh.ts | 12 ++++- build/gulpfile.vscode.ts | 71 ++----------------------- build/lib/copilot.ts | 110 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 69 deletions(-) create mode 100644 build/lib/copilot.ts diff --git a/build/gulpfile.reh.ts b/build/gulpfile.reh.ts index 0ad08ab6fbe..f3ba46d9f89 100644 --- a/build/gulpfile.reh.ts +++ b/build/gulpfile.reh.ts @@ -34,6 +34,7 @@ import * as cp from 'child_process'; import log from 'fancy-log'; import buildfile from './buildfile.ts'; import { fetchUrls, fetchGithub } from './lib/fetch.ts'; +import { getCopilotExcludeFilter, copyCopilotNativeDeps } from './lib/copilot.ts'; import jsonEditor from 'gulp-json-editor'; @@ -343,6 +344,7 @@ function packageTask(type: string, platform: string, arch: string, sourceFolderN .pipe(filter(['**', '!**/package-lock.json', '!**/*.{js,css}.map'])) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, '.moduleignore'))) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, `.moduleignore.${process.platform}`))) + .pipe(filter(getCopilotExcludeFilter(platform, arch))) .pipe(jsFilter) .pipe(util.stripSourceMappingURL()) .pipe(jsFilter.restore); @@ -461,6 +463,13 @@ function patchWin32DependenciesTask(destinationFolderName: string) { }; } +function copyCopilotNativeDepsTaskREH(platform: string, arch: string, destinationFolderName: string) { + return async () => { + const nodeModulesDir = path.join(BUILD_ROOT, destinationFolderName, 'node_modules'); + copyCopilotNativeDeps(platform, arch, nodeModulesDir); + }; +} + /** * @param product The parsed product.json file contents */ @@ -509,7 +518,8 @@ function tweakProductForServerWeb(product: typeof import('../product.json')) { compileNativeExtensionsBuildTask, gulp.task(`node-${platform}-${arch}`) as task.Task, util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), - packageTask(type, platform, arch, sourceFolderName, destinationFolderName) + packageTask(type, platform, arch, sourceFolderName, destinationFolderName), + copyCopilotNativeDepsTaskREH(platform, arch, destinationFolderName) ]; if (platform === 'win32') { diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 35f0d93a6e9..336a8947fbb 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -31,6 +31,7 @@ import minimist from 'minimist'; import { compileBuildWithoutManglingTask, compileBuildWithManglingTask } from './gulpfile.compile.ts'; import { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask } from './gulpfile.extensions.ts'; import { copyCodiconsTask } from './lib/compilation.ts'; +import { getCopilotExcludeFilter, copyCopilotNativeDeps } from './lib/copilot.ts'; import type { EmbeddedProductInfo } from './lib/embeddedType.ts'; import { useEsbuildTranspile } from './buildConfig.ts'; import { promisify } from 'util'; @@ -318,38 +319,6 @@ function computeChecksum(filename: string): string { return hash; } -const copilotPlatforms = [ - 'darwin-arm64', 'darwin-x64', - 'linux-arm64', 'linux-x64', - 'win32-arm64', 'win32-x64', -]; - -/** - * Returns a glob filter that strips @github/copilot platform packages and - * prebuilt native modules for architectures other than the build target. - * On stable builds, all copilot SDK dependencies are stripped entirely. - */ -function getCopilotExcludeFilter(platform: string, arch: string, quality: string | undefined): string[] { - const targetPlatformArch = `${platform}-${arch}`; - const nonTargetPlatforms = copilotPlatforms.filter(p => p !== targetPlatformArch); - - // Strip wrong-architecture @github/copilot-{platform} packages. - // All copilot prebuilds are stripped by .moduleignore; VS Code's own - // node-pty is copied into the prebuilds location by a post-packaging task. - const excludes = nonTargetPlatforms.map(p => `!**/node_modules/@github/copilot-${p}/**`); - - // Strip agent host SDK dependencies entirely from stable builds - if (quality === 'stable') { - excludes.push( - '!**/node_modules/@github/copilot/**', - '!**/node_modules/@github/copilot-sdk/**', - '!**/node_modules/@github/copilot-*/**', - ); - } - - return ['**', ...excludes]; -} - function packageTask(platform: string, arch: string, sourceFolderName: string, destinationFolderName: string, _opts?: { stats?: boolean }) { const destination = path.join(path.dirname(root), destinationFolderName); platform = platform || process.platform; @@ -469,7 +438,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d .pipe(filter(depFilterPattern)) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, '.moduleignore'))) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, `.moduleignore.${process.platform}`))) - .pipe(filter(getCopilotExcludeFilter(platform, arch, quality))) + .pipe(filter(getCopilotExcludeFilter(platform, arch))) .pipe(jsFilter) .pipe(util.rewriteSourceMappingURL(sourceMappingURLBase)) .pipe(jsFilter.restore) @@ -713,27 +682,10 @@ function patchWin32DependenciesTask(destinationFolderName: string) { }; } -/** - * Copies VS Code's own node-pty binaries into the copilot SDK's - * expected locations so the copilot CLI subprocess can find them at runtime. - * The copilot-bundled prebuilds are stripped by .moduleignore; - * this replaces them with the same binaries VS Code already ships, avoiding - * new system dependency requirements. - * - * node-pty: `prebuilds/{platform}-{arch}/` (pty.node + spawn-helper) - */ function copyCopilotNativeDepsTask(platform: string, arch: string, destinationFolderName: string) { const outputDir = path.join(path.dirname(root), destinationFolderName); return async () => { - const quality = (product as { quality?: string }).quality; - - // On stable builds the copilot SDK is stripped entirely -- nothing to copy into. - if (quality === 'stable') { - console.log(`[copyCopilotNativeDeps] Skipping -- stable build`); - return; - } - // On Windows with win32VersionedUpdate, app resources live under a // commit-hash prefix: {output}/{commitHash}/resources/app/ const versionedResourcesFolder = util.getVersionedResourcesFolder(platform, commit!); @@ -741,24 +693,7 @@ function copyCopilotNativeDepsTask(platform: string, arch: string, destinationFo ? path.join(outputDir, `${product.nameLong}.app`, 'Contents', 'Resources', 'app') : path.join(outputDir, versionedResourcesFolder, 'resources', 'app'); - // Source and destination are both in node_modules/, which exists as a real - // directory on disk on all platforms after packaging. - const nodeModulesDir = path.join(appBase, 'node_modules'); - const copilotBase = path.join(nodeModulesDir, '@github', 'copilot'); - const platformArch = `${platform === 'win32' ? 'win32' : platform}-${arch}`; - - const nodePtySource = path.join(nodeModulesDir, 'node-pty', 'build', 'Release'); - - // Fail-fast: source binaries must exist on non-stable builds. - if (!fs.existsSync(nodePtySource)) { - throw new Error(`[copyCopilotNativeDeps] node-pty source not found at ${nodePtySource}`); - } - - // Copy node-pty (pty.node + spawn-helper) into copilot prebuilds - const copilotPrebuildsDir = path.join(copilotBase, 'prebuilds', platformArch); - fs.mkdirSync(copilotPrebuildsDir, { recursive: true }); - fs.cpSync(nodePtySource, copilotPrebuildsDir, { recursive: true }); - console.log(`[copyCopilotNativeDeps] Copied node-pty from ${nodePtySource} to ${copilotPrebuildsDir}`); + copyCopilotNativeDeps(platform, arch, path.join(appBase, 'node_modules')); }; } diff --git a/build/lib/copilot.ts b/build/lib/copilot.ts new file mode 100644 index 00000000000..f182c9829a9 --- /dev/null +++ b/build/lib/copilot.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as path from 'path'; + +/** + * The platforms that @github/copilot ships platform-specific packages for. + * These are the `@github/copilot-{platform}` optional dependency packages. + */ +export const copilotPlatforms = [ + 'darwin-arm64', 'darwin-x64', + 'linux-arm64', 'linux-x64', + 'win32-arm64', 'win32-x64', +]; + +/** + * Converts VS Code build platform/arch to the values that Node.js reports + * at runtime via `process.platform` and `process.arch`. + * + * The copilot SDK's `loadNativeModule` looks up native binaries under + * `prebuilds/${process.platform}-${process.arch}/`, so the directory names + * must match these runtime values exactly. + */ +function toNodePlatformArch(platform: string, arch: string): { nodePlatform: string; nodeArch: string } { + // alpine is musl-linux; Node still reports process.platform === 'linux' + let nodePlatform = platform === 'alpine' ? 'linux' : platform; + let nodeArch = arch; + + if (arch === 'armhf') { + // VS Code build uses 'armhf'; Node reports process.arch === 'arm' + nodeArch = 'arm'; + } else if (arch === 'alpine') { + // Legacy: { platform: 'linux', arch: 'alpine' } means alpine-x64 + nodePlatform = 'linux'; + nodeArch = 'x64'; + } + + return { nodePlatform, nodeArch }; +} + +/** + * Returns a glob filter that strips @github/copilot platform packages + * for architectures other than the build target. + * + * For platforms the copilot SDK doesn't natively support (e.g. alpine, armhf), + * ALL platform packages are stripped - that's fine because the SDK doesn't ship + * binaries for those platforms anyway, and we replace them with VS Code's own. + */ +export function getCopilotExcludeFilter(platform: string, arch: string): string[] { + const { nodePlatform, nodeArch } = toNodePlatformArch(platform, arch); + const targetPlatformArch = `${nodePlatform}-${nodeArch}`; + const nonTargetPlatforms = copilotPlatforms.filter(p => p !== targetPlatformArch); + + // Strip wrong-architecture @github/copilot-{platform} packages. + // All copilot prebuilds are stripped by .moduleignore; VS Code's own + // node-pty is copied into the prebuilds location by a post-packaging task. + const excludes = nonTargetPlatforms.map(p => `!**/node_modules/@github/copilot-${p}/**`); + + return ['**', ...excludes]; +} + +/** + * Copies VS Code's own node-pty binaries into the copilot SDK's + * expected locations so the copilot CLI subprocess can find them at runtime. + * The copilot-bundled prebuilds are stripped by .moduleignore; + * this replaces them with the same binaries VS Code already ships, avoiding + * new system dependency requirements. + * + * This works even for platforms the copilot SDK doesn't natively support + * (e.g. alpine, armhf) because the SDK's native module loader simply + * looks for `prebuilds/{process.platform}-{process.arch}/pty.node` - it + * doesn't validate the platform against a supported list. + * + * Failures are logged but do not throw, to avoid breaking the build on + * platforms where something unexpected happens. + * + * @param nodeModulesDir Absolute path to the node_modules directory that + * contains both the source binaries (node-pty) and the copilot SDK + * target directories. + */ +export function copyCopilotNativeDeps(platform: string, arch: string, nodeModulesDir: string): void { + const { nodePlatform, nodeArch } = toNodePlatformArch(platform, arch); + const platformArch = `${nodePlatform}-${nodeArch}`; + + const copilotBase = path.join(nodeModulesDir, '@github', 'copilot'); + if (!fs.existsSync(copilotBase)) { + console.warn(`[copyCopilotNativeDeps] @github/copilot not found at ${copilotBase}, skipping`); + return; + } + + const nodePtySource = path.join(nodeModulesDir, 'node-pty', 'build', 'Release'); + if (!fs.existsSync(nodePtySource)) { + console.warn(`[copyCopilotNativeDeps] node-pty source not found at ${nodePtySource}, skipping`); + return; + } + + try { + // Copy node-pty (pty.node + spawn-helper on Unix, conpty.node + conpty/ on Windows) + // into copilot prebuilds so the SDK finds them via loadNativeModule. + const copilotPrebuildsDir = path.join(copilotBase, 'prebuilds', platformArch); + fs.mkdirSync(copilotPrebuildsDir, { recursive: true }); + fs.cpSync(nodePtySource, copilotPrebuildsDir, { recursive: true }); + console.log(`[copyCopilotNativeDeps] Copied node-pty from ${nodePtySource} to ${copilotPrebuildsDir}`); + } catch (err) { + console.warn(`[copyCopilotNativeDeps] Failed to copy node-pty for ${platformArch}: ${err}`); + } +}