Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pre package extensions and install them on startup for users #6403

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
13 changes: 13 additions & 0 deletions build/gulpfile.extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,12 +300,19 @@ exports.cleanExtensionsBuildTask = cleanExtensionsBuildTask;
*/
const bundleMarketplaceExtensionsBuildTask = task.define('bundle-marketplace-extensions-build', () => ext.packageMarketplaceExtensionsStream(false).pipe(gulp.dest('.build')));

// --- Start Positron ---
const bundleBootstrapExtensionsBuildTask = task.define('bundle-bootstrap-extensions-build', () => ext.packageBootstrapExtensionsStream().pipe(gulp.dest('.build')));
// --- End Positron ---

/**
* Compiles the non-native extensions for the build
* @note this does not clean the directory ahead of it. See {@link cleanExtensionsBuildTask} for that.
*/
const compileNonNativeExtensionsBuildTask = task.define('compile-non-native-extensions-build', task.series(
bundleMarketplaceExtensionsBuildTask,
// --- Start Positron ---
bundleBootstrapExtensionsBuildTask,
// --- End Positron ---
task.define('bundle-non-native-extensions-build', () => ext.packageNonNativeLocalExtensionsStream().pipe(gulp.dest('.build')))
));
gulp.task(compileNonNativeExtensionsBuildTask);
Expand All @@ -326,6 +333,9 @@ exports.compileNativeExtensionsBuildTask = compileNativeExtensionsBuildTask;
const compileAllExtensionsBuildTask = task.define('compile-extensions-build', task.series(
cleanExtensionsBuildTask,
bundleMarketplaceExtensionsBuildTask,
// --- Start Positron ---
bundleBootstrapExtensionsBuildTask,
// --- End Positron ---
task.define('bundle-extensions-build', () => ext.packageAllLocalExtensionsStream(false, false).pipe(gulp.dest('.build'))),
// --- Start Positron ---
copyExtensionBinariesTask
Expand All @@ -341,6 +351,9 @@ gulp.task(task.define('extensions-ci', task.series(compileNonNativeExtensionsBui
const compileExtensionsBuildPullRequestTask = task.define('compile-extensions-build-pr', task.series(
cleanExtensionsBuildTask,
bundleMarketplaceExtensionsBuildTask,
// --- Start Positron ---
bundleBootstrapExtensionsBuildTask,
// --- End Positron ---
task.define('bundle-extensions-build-pr', () => ext.packageAllLocalExtensionsStream(false, true).pipe(gulp.dest('.build'))),
));
gulp.task(compileExtensionsBuildPullRequestTask);
Expand Down
15 changes: 14 additions & 1 deletion build/gulpfile.reh.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,20 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa
const extensionPaths = [...localWorkspaceExtensions, ...marketplaceExtensions]
.map(name => `.build/extensions/${name}/**`);

const extensions = gulp.src(extensionPaths, { base: '.build', dot: true });
//const extensions = gulp.src(extensionPaths, { base: '.build', dot: true });
// --- Start Positron ---

const bootstrapExtensions = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'product.json'), 'utf8')).bootstrapExtensions
.filter(entry => !entry.platforms || new Set(entry.platforms).has(platform))
.filter(entry => !entry.type || entry.type === type)
.map(entry => entry.name);
const bootstrapExtensionPaths = [...bootstrapExtensions]
.map(name => `.build/extensions/bootstrap/${name}*.vsix`);

const extensions = gulp.src([...extensionPaths, ...bootstrapExtensionPaths], { base: '.build', dot: true });

// --- End Positron ---

const extensionsCommonDependencies = gulp.src('.build/extensions/node_modules/**', { base: '.build', dot: true });
const sources = es.merge(src, extensions, extensionsCommonDependencies)
.pipe(filter(['**', '!**/*.js.map'], { dot: true }));
Expand Down
139 changes: 139 additions & 0 deletions build/lib/bootstrapExtensions.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

169 changes: 169 additions & 0 deletions build/lib/bootstrapExtensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*---------------------------------------------------------------------------------------------
* 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';
import * as os from 'os';
import * as rimraf from 'rimraf';
import * as es from 'event-stream';
import * as rename from 'gulp-rename';
import * as vfs from 'vinyl-fs';
import * as ext from './extensions';
import * as fancyLog from 'fancy-log';
import * as ansiColors from 'ansi-colors';
import { Stream } from 'stream';
import { IExtensionDefinition } from './builtInExtensions';

const root = path.dirname(path.dirname(__dirname));
const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8'));
const ENABLE_LOGGING = !process.env['VSCODE_BUILD_BOOTSTRAP_EXTENSIONS_SILENCE_PLEASE'];

const bootstrapExtensions = <IExtensionDefinition[]>productjson.bootstrapExtensions || [];
const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'bootstrap-control.json');

function log(...messages: string[]): void {
if (ENABLE_LOGGING) {
fancyLog(...messages);
}
}

function getExtensionPath(extension: IExtensionDefinition): string {
return path.join(root, '.build', 'bootstrapExtensions', 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 getExtensionDownloadStream(extension: IExtensionDefinition) {
const url = extension.metadata.multiPlatformServiceUrl || productjson.extensionsGallery?.serviceUrl;
return (url ? ext.fromMarketplace(url, extension, true) : ext.fromGithub(extension))
.pipe(rename(p => { p.basename = `${extension.name}-${extension.version}.vsix`; }));
}

export function getBootstrapExtensionStream(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/bootstrapExtensions'))
.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: Bootstrap 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: Bootstrap 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, filePath: string): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(control, null, 2));
}

export function getBootstrapExtensions(): Promise<void> {

const control = readControlFile();
const streams: Stream[] = [];

for (const extension of [...bootstrapExtensions]) {
const controlState = control[extension.name] || 'marketplace';
control[extension.name] = controlState;

// Discard extensions intended for the web. The 'type' field isn't a
// formal part of the extension definition but a custom field we use to
// filter out web-only extensions (i.e. Posit Workbench)
// @ts-ignore
if (extension.type === 'reh-web') {
continue;
}

streams.push(syncExtension(extension, controlState));
}

writeControlFile(control, controlFilePath);

return new Promise((resolve, reject) => {
es.merge(streams)
.on('error', reject)
.on('end', resolve);
});
}

if (require.main === module) {
getBootstrapExtensions().then(() => process.exit(0)).catch(err => {
console.error(err);
process.exit(1);
});
}
Loading
Loading