Files
vscode/build/lib/builtInExtensions.ts
T
4e6997ae07 build: support platform-specific built-in extensions from GitHub releases (#324206)
* Support platform-specific built-in extensions from GitHub releases

Adds support for downloading platform-specific built-in extension VSIXs
from GitHub releases, keyed by marketplace target platform. Also downloads
the latest pre-release assets for insiders builds, ignoring the pinned
version and checksum.

- extensionTarget.ts: resolve build target + release asset name
- builtInExtensions.ts: platformSpecific checksum map + insiders detection
- extensions.ts/fetch.ts: fromGithub asset selection + prerelease support
- CI: platform-aware cache key and target env plumbing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Insiders downloads the latest release instead of latest pre-release

Insiders builds should always be on the newest published release, so the
GitHub download now resolves the most recently published release (including
pre-releases) rather than filtering to pre-releases only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address code review feedback for platform-specific extensions

- Sort latest releases by published_at instead of created_at
- Log a warning when skipping checksum validation in latest mode
- Document that platform-specific extensions always download from GitHub
- Skip gracefully on unsupported platforms; keep a clear error for a
  known target missing from the platformSpecific map

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address CCR feedback: restrict targets and harden helpers

- Restrict getExtensionTarget to the supported marketplace targets, returning
  undefined for unsupported OS/arch (e.g. win32-ia32, linux-riscv64) so callers
  skip gracefully instead of failing with a missing-asset error
- Remove the bogus ia32 -> x86 mapping (no win32-x86 target exists)
- Validate the target format in getPlatformSpecificAssetName and throw a clear
  error for malformed targets
- Guard the latest-release sort against NaN timestamps (missing published_at
  sorts to the end deterministically)
- Update tests accordingly

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Align asset naming with VS Code convention and support product.overrides.json

The platform-specific extension this feature targets (typescript-go's
TypeScriptTeam.native-preview) publishes VSIX assets named
<name>-<target>.vsix using the raw marketplace target platform, not the
node-vsce-sign osx/win/arm aliasing. Update getPlatformSpecificAssetName to
the standard <name>-<target>.vsix convention and validate against the
supported target set.

- fetch.ts: in latest mode, select the newest published release that actually
  contains the requested asset, so releases shipping only other artifacts
  (e.g. tarballs) without a matching VSIX are skipped
- builtInExtensions.ts: merge product.overrides.json (gitignored, local) so
  overridden built-in extensions are downloaded, mirroring bootstrap-meta
- Update tests for the new naming convention

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Detect extensions project by path segment, not substring

ESBuildTranspiler decided CJS vs ESM output via configFilePath.includes('extensions').
When the repo is checked out (e.g. as a git worktree) into a folder whose name
merely contains the substring 'extensions', the 'src' project was wrongly treated
as an extension and transpiled to CJS, breaking top-level await in src/cli.ts,
src/server-cli.ts and src/server-main.ts. Match an 'extensions' path segment instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove product.overrides.json support from built-in extensions download

Revert the download script to read product.json directly, per review feedback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-14 10:26:58 +00:00

229 lines
8.2 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 fs from 'fs';
import path from 'path';
import os from 'os';
import rimraf from 'rimraf';
import es from 'event-stream';
import { rename } from './gulp/facade.ts';
import vfs from 'vinyl-fs';
import * as ext from './extensions.ts';
import { getCurrentExtensionTarget, getPlatformSpecificAssetName } from './extensionTarget.ts';
import fancyLog from 'fancy-log';
import ansiColors from 'ansi-colors';
import { Stream } from 'stream';
export interface IExtensionDefinition {
name: string;
version: string;
sha256: string;
repo: string;
platforms?: string[];
vsix?: string;
/**
* Per-target checksums for a platform-specific extension published to a GitHub release.
* Keyed by marketplace target platform (e.g. `win32-x64`, `linux-armhf`, `alpine-x64`).
* The matching release asset is resolved by convention as `<name>-<osAlias>-<arch>.vsix`.
*/
platformSpecific?: { [target: string]: string };
metadata: {
id: string;
publisherId: {
publisherId: string;
publisherName: string;
displayName: string;
flags: string;
};
publisherDisplayName: string;
};
}
const root = path.dirname(path.dirname(import.meta.dirname));
const productjson = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '../../product.json'), 'utf8'));
const builtInExtensions = productjson.builtInExtensions as IExtensionDefinition[] || [];
const webBuiltInExtensions = productjson.webBuiltInExtensions as IExtensionDefinition[] || [];
const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json');
const ENABLE_LOGGING = !process.env['VSCODE_BUILD_BUILTIN_EXTENSIONS_SILENCE_PLEASE'];
function log(...messages: string[]): void {
if (ENABLE_LOGGING) {
fancyLog(...messages);
}
}
function getExtensionPath(extension: IExtensionDefinition): string {
return path.join(root, '.build', 'builtInExtensions', extension.name);
}
function isUpToDate(extension: IExtensionDefinition): boolean {
const packagePath = path.join(getExtensionPath(extension), 'package.json');
if (!fs.existsSync(packagePath)) {
return false;
}
const packageContents = fs.readFileSync(packagePath, { encoding: 'utf8' });
try {
const diskVersion = JSON.parse(packageContents).version;
return (diskVersion === extension.version);
} catch (err) {
return false;
}
}
function isInsiders(): boolean {
return process.env['VSCODE_QUALITY'] === 'insider';
}
function getExtensionDownloadStream(extension: IExtensionDefinition) {
let input: Stream;
if (extension.vsix) {
input = ext.fromVsix(path.join(root, extension.vsix), extension);
} else if (extension.platformSpecific) {
// A platform-specific extension publishes its VSIX assets on a GitHub release using a
// specific asset naming convention, so it is always downloaded from GitHub and never falls
// back to the Marketplace (which does not serve those assets) even when a gallery is configured.
const asset = resolvePlatformSpecificAsset(extension);
if (!asset) {
return es.readArray([]);
}
input = ext.fromGithub(extension, { asset, latest: isInsiders() });
} else if (productjson.extensionsGallery?.serviceUrl) {
input = ext.fromMarketplace(productjson.extensionsGallery.serviceUrl, extension);
} else {
input = ext.fromGithub(extension, { latest: isInsiders() });
}
return input.pipe(rename(p => p.dirname = `${extension.name}/${p.dirname}`));
}
function resolvePlatformSpecificAsset(extension: IExtensionDefinition): { assetName: string; sha256: string } | undefined {
const target = getCurrentExtensionTarget();
if (!target) {
// Unsupported build platform (e.g. FreeBSD): no platform-specific asset can apply, so skip
// this extension gracefully instead of failing the whole build.
log(ansiColors.yellow('[skip]'), `${extension.name}: no platform-specific asset for unsupported platform '${process.platform}-${process.env['VSCODE_ARCH'] ?? process.arch}'`);
return undefined;
}
const sha256 = extension.platformSpecific![target];
if (!sha256) {
// The extension declares platform-specific assets but is missing one for this known target.
// This is a configuration error, so fail loudly.
throw new Error(`Built-in extension '${extension.name}' is platform-specific but has no asset for target '${target}'. Available targets: [${Object.keys(extension.platformSpecific!)}]`);
}
return { assetName: getPlatformSpecificAssetName(extension.name, target), sha256 };
}
export function getExtensionStream(extension: IExtensionDefinition) {
// if the extension exists on disk, use those files instead of downloading anew
if (isUpToDate(extension)) {
log('[extensions]', `${extension.name}@${extension.version} up to date`, ansiColors.green('✔︎'));
return vfs.src(['**'], { cwd: getExtensionPath(extension), dot: true })
.pipe(rename(p => p.dirname = `${extension.name}/${p.dirname}`));
}
return getExtensionDownloadStream(extension);
}
function syncMarketplaceExtension(extension: IExtensionDefinition): Stream {
const galleryServiceUrl = productjson.extensionsGallery?.serviceUrl;
const source = ansiColors.blue(galleryServiceUrl ? '[marketplace]' : '[github]');
if (isUpToDate(extension)) {
log(source, `${extension.name}@${extension.version}`, ansiColors.green('✔︎'));
return es.readArray([]);
}
rimraf.sync(getExtensionPath(extension));
return getExtensionDownloadStream(extension)
.pipe(vfs.dest('.build/builtInExtensions'))
.on('end', () => log(source, extension.name, ansiColors.green('✔︎')));
}
function syncExtension(extension: IExtensionDefinition, controlState: 'disabled' | 'marketplace'): Stream {
if (extension.platforms) {
const platforms = new Set(extension.platforms);
if (!platforms.has(process.platform)) {
log(ansiColors.gray('[skip]'), `${extension.name}@${extension.version}: Platform '${process.platform}' not supported: [${extension.platforms}]`, ansiColors.green('✔︎'));
return es.readArray([]);
}
}
switch (controlState) {
case 'disabled':
log(ansiColors.blue('[disabled]'), ansiColors.gray(extension.name));
return es.readArray([]);
case 'marketplace':
return syncMarketplaceExtension(extension);
default:
if (!fs.existsSync(controlState)) {
log(ansiColors.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but that path does not exist.`));
return es.readArray([]);
} else if (!fs.existsSync(path.join(controlState, 'package.json'))) {
log(ansiColors.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but there is no 'package.json' file in that directory.`));
return es.readArray([]);
}
log(ansiColors.blue('[local]'), `${extension.name}: ${ansiColors.cyan(controlState)}`, ansiColors.green('✔︎'));
return es.readArray([]);
}
}
interface IControlFile {
[name: string]: 'disabled' | 'marketplace';
}
function readControlFile(): IControlFile {
try {
return JSON.parse(fs.readFileSync(controlFilePath, 'utf8'));
} catch (err) {
return {};
}
}
function writeControlFile(control: IControlFile): void {
fs.mkdirSync(path.dirname(controlFilePath), { recursive: true });
fs.writeFileSync(controlFilePath, JSON.stringify(control, null, 2));
}
export function getBuiltInExtensions(): Promise<void> {
log('Synchronizing built-in extensions...');
log(`You can manage built-in extensions with the ${ansiColors.cyan('--builtin')} flag`);
const control = readControlFile();
const streams: Stream[] = [];
for (const extension of [...builtInExtensions, ...webBuiltInExtensions]) {
const controlState = control[extension.name] || 'marketplace';
control[extension.name] = controlState;
streams.push(syncExtension(extension, controlState));
}
writeControlFile(control);
return new Promise((resolve, reject) => {
es.merge(streams)
.on('error', reject)
.on('end', resolve);
});
}
if (import.meta.main) {
getBuiltInExtensions().then(() => process.exit(0)).catch(err => {
console.error(err);
process.exit(1);
});
}