diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index cbb928c5e88..0417a44d57a 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -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); @@ -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 @@ -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); diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index 31530372fe2..b203049c2be 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -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 })); diff --git a/build/lib/bootstrapExtensions.js b/build/lib/bootstrapExtensions.js new file mode 100644 index 00000000000..6ba10faeac6 --- /dev/null +++ b/build/lib/bootstrapExtensions.js @@ -0,0 +1,139 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getBootstrapExtensionStream = getBootstrapExtensionStream; +exports.getBootstrapExtensions = getBootstrapExtensions; +const fs = require("fs"); +const path = require("path"); +const os = require("os"); +const rimraf = require("rimraf"); +const es = require("event-stream"); +const rename = require("gulp-rename"); +const vfs = require("vinyl-fs"); +const ext = require("./extensions"); +const fancyLog = require("fancy-log"); +const ansiColors = require("ansi-colors"); +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 = productjson.bootstrapExtensions || []; +const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'bootstrap-control.json'); +function log(...messages) { + if (ENABLE_LOGGING) { + fancyLog(...messages); + } +} +function getExtensionPath(extension) { + return path.join(root, '.build', 'bootstrapExtensions', extension.name); +} +function isUpToDate(extension) { + 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) { + 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`; })); +} +function getBootstrapExtensionStream(extension) { + // 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) { + 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, controlState) { + 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([]); + } +} +function readControlFile() { + try { + return JSON.parse(fs.readFileSync(controlFilePath, 'utf8')); + } + catch (err) { + return {}; + } +} +function writeControlFile(control, filePath) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(control, null, 2)); +} +function getBootstrapExtensions() { + const control = readControlFile(); + const streams = []; + 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); + }); +} +//# sourceMappingURL=bootstrapExtensions.js.map \ No newline at end of file diff --git a/build/lib/bootstrapExtensions.ts b/build/lib/bootstrapExtensions.ts new file mode 100644 index 00000000000..6410f807d3f --- /dev/null +++ b/build/lib/bootstrapExtensions.ts @@ -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 = 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 { + + 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); + }); +} diff --git a/build/lib/extensions.js b/build/lib/extensions.js index c72bbbbe75a..c725de92b43 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -11,6 +11,7 @@ exports.packageNonNativeLocalExtensionsStream = packageNonNativeLocalExtensionsS exports.packageNativeLocalExtensionsStream = packageNativeLocalExtensionsStream; exports.packageAllLocalExtensionsStream = packageAllLocalExtensionsStream; exports.packageMarketplaceExtensionsStream = packageMarketplaceExtensionsStream; +exports.packageBootstrapExtensionsStream = packageBootstrapExtensionsStream; exports.scanBuiltinExtensions = scanBuiltinExtensions; exports.translatePackageJSON = translatePackageJSON; exports.webpackExtensions = webpackExtensions; @@ -34,6 +35,7 @@ const buffer = require('gulp-buffer'); const jsoncParser = require("jsonc-parser"); const dependencies_1 = require("./dependencies"); const builtInExtensions_1 = require("./builtInExtensions"); +const bootstrapExtensions_1 = require("./bootstrapExtensions"); const getVersion_1 = require("./getVersion"); const fetch_1 = require("./fetch"); // --- Start Positron --- @@ -234,7 +236,9 @@ const baseHeaders = { 'User-Agent': userAgent, 'X-Market-User-Id': '291C1CD0-051A-4123-9B4B-30D60EF52EE2', }; -function fromMarketplace(serviceUrl, { name: extensionName, version, sha256, metadata }) { +// --- Start Positron --- +function fromMarketplace(serviceUrl, { name: extensionName, version, sha256, metadata }, bootstrap = false) { + // --- End Positron --- const json = require('gulp-json-editor'); const [publisher, name] = extensionName.split('.'); // --- Start Positron --- @@ -263,20 +267,34 @@ function fromMarketplace(serviceUrl, { name: extensionName, version, sha256, met // --- End Positron --- fancyLog('Downloading extension:', ansiColors.yellow(`${extensionName}@${version}`), '...'); const packageJsonFilter = filter('package.json', { restore: true }); - return (0, fetch_1.fetchUrls)('', { - base: url, - nodeFetchOptions: { - headers: baseHeaders - }, - checksumSha256: sha256 - }) - .pipe(vzip.src()) - .pipe(filter('extension/**')) - .pipe(rename(p => p.dirname = p.dirname.replace(/^extension\/?/, ''))) - .pipe(packageJsonFilter) - .pipe(buffer()) - .pipe(json({ __metadata: metadata })) - .pipe(packageJsonFilter.restore); + // --- Start Positron --- + if (bootstrap) { + return (0, fetch_1.fetchUrls)('', { + base: url, + nodeFetchOptions: { + headers: baseHeaders + }, + checksumSha256: sha256 + }) + .pipe(buffer()); + } + else { + return (0, fetch_1.fetchUrls)('', { + base: url, + nodeFetchOptions: { + headers: baseHeaders + }, + checksumSha256: sha256 + }) + .pipe(vzip.src()) + .pipe(filter('extension/**')) + .pipe(rename(p => p.dirname = p.dirname.replace(/^extension\/?/, ''))) + .pipe(packageJsonFilter) + .pipe(buffer()) + .pipe(json({ __metadata: metadata })) + .pipe(packageJsonFilter.restore); + } + // --- End Positron --- } // --- Start PWB: Bundle PWB extension --- function fromPositUrl({ name: extensionName, version, sha256, positUrl, metadata }) { @@ -348,6 +366,9 @@ const marketplaceWebExtensionsExclude = new Set([ ]); const productJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); const builtInExtensions = productJson.builtInExtensions || []; +// --- Start Positron --- +const bootstrapExtensions = productJson.bootstrapExtensions || []; +// --- End Positron --- const webBuiltInExtensions = productJson.webBuiltInExtensions || []; /** * Loosely based on `getExtensionKind` from `src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts` @@ -480,6 +501,16 @@ function packageMarketplaceExtensionsStream(forWeb) { return (marketplaceExtensionsStream .pipe(util2.setExecutableBit(['**/*.sh']))); } +// --- Start Positron --- +function packageBootstrapExtensionsStream() { + return es.merge(...bootstrapExtensions + .map(extension => { + const src = (0, bootstrapExtensions_1.getBootstrapExtensionStream)(extension).pipe(rename(p => { + p.dirname = `extensions/bootstrap`; + })); + return src; + })); +} function scanBuiltinExtensions(extensionsRoot, exclude = []) { const scannedExtensions = []; try { diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 74e28e3360a..e3ca11c64ef 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -23,6 +23,7 @@ import * as jsoncParser from 'jsonc-parser'; import webpack = require('webpack'); import { getProductionDependencies } from './dependencies'; import { IExtensionDefinition, getExtensionStream } from './builtInExtensions'; +import { getBootstrapExtensionStream } from './bootstrapExtensions'; import { getVersion } from './getVersion'; import { fetchUrls, fetchGithub } from './fetch'; @@ -257,7 +258,9 @@ const baseHeaders = { 'X-Market-User-Id': '291C1CD0-051A-4123-9B4B-30D60EF52EE2', }; -export function fromMarketplace(serviceUrl: string, { name: extensionName, version, sha256, metadata }: IExtensionDefinition): Stream { +// --- Start Positron --- +export function fromMarketplace(serviceUrl: string, { name: extensionName, version, sha256, metadata }: IExtensionDefinition, bootstrap: boolean = false): Stream { + // --- End Positron --- const json = require('gulp-json-editor') as typeof import('gulp-json-editor'); const [publisher, name] = extensionName.split('.'); @@ -290,20 +293,33 @@ export function fromMarketplace(serviceUrl: string, { name: extensionName, versi const packageJsonFilter = filter('package.json', { restore: true }); - return fetchUrls('', { - base: url, - nodeFetchOptions: { - headers: baseHeaders - }, - checksumSha256: sha256 - }) - .pipe(vzip.src()) - .pipe(filter('extension/**')) - .pipe(rename(p => p.dirname = p.dirname!.replace(/^extension\/?/, ''))) - .pipe(packageJsonFilter) - .pipe(buffer()) - .pipe(json({ __metadata: metadata })) - .pipe(packageJsonFilter.restore); + // --- Start Positron --- + if (bootstrap) { + return fetchUrls('', { + base: url, + nodeFetchOptions: { + headers: baseHeaders + }, + checksumSha256: sha256 + }) + .pipe(buffer()); + } else { + return fetchUrls('', { + base: url, + nodeFetchOptions: { + headers: baseHeaders + }, + checksumSha256: sha256 + }) + .pipe(vzip.src()) + .pipe(filter('extension/**')) + .pipe(rename(p => p.dirname = p.dirname!.replace(/^extension\/?/, ''))) + .pipe(packageJsonFilter) + .pipe(buffer()) + .pipe(json({ __metadata: metadata })) + .pipe(packageJsonFilter.restore); + } + // --- End Positron --- } // --- Start PWB: Bundle PWB extension --- @@ -388,6 +404,9 @@ const marketplaceWebExtensionsExclude = new Set([ const productJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); const builtInExtensions: IExtensionDefinition[] = productJson.builtInExtensions || []; +// --- Start Positron --- +const bootstrapExtensions: IExtensionDefinition[] = productJson.bootstrapExtensions || []; +// --- End Positron --- const webBuiltInExtensions: IExtensionDefinition[] = productJson.webBuiltInExtensions || []; type ExtensionKind = 'ui' | 'workspace' | 'web'; @@ -558,6 +577,20 @@ export function packageMarketplaceExtensionsStream(forWeb: boolean): Stream { ); } +// --- Start Positron --- +export function packageBootstrapExtensionsStream(): Stream { + return es.merge( + ...bootstrapExtensions + .map(extension => { + const src = getBootstrapExtensionStream(extension).pipe(rename(p => { + p.dirname = `extensions/bootstrap`; + })); + return src; + }) + ); +} +// --- End Positron --- + export interface IScannedBuiltinExtension { extensionPath: string; packageJSON: any; diff --git a/build/lib/preLaunch.js b/build/lib/preLaunch.js index 4791514fdfe..7b1f4c8c422 100644 --- a/build/lib/preLaunch.js +++ b/build/lib/preLaunch.js @@ -46,6 +46,8 @@ async function main() { // Can't require this until after dependencies are installed const { getBuiltInExtensions } = require('./builtInExtensions'); await getBuiltInExtensions(); + const { getBootstrapExtensions } = require('./bootstrapExtensions'); + await getBootstrapExtensions(); } if (require.main === module) { main().catch(err => { diff --git a/build/lib/preLaunch.ts b/build/lib/preLaunch.ts index e0ea274458a..b8e1ee0d7f7 100644 --- a/build/lib/preLaunch.ts +++ b/build/lib/preLaunch.ts @@ -53,6 +53,8 @@ async function main() { // Can't require this until after dependencies are installed const { getBuiltInExtensions } = require('./builtInExtensions'); await getBuiltInExtensions(); + const { getBootstrapExtensions } = require('./bootstrapExtensions'); + await getBootstrapExtensions(); } if (require.main === module) { diff --git a/build/secrets/.secrets.baseline b/build/secrets/.secrets.baseline index 80f1ea6baee..b0fd54a6f27 100644 --- a/build/secrets/.secrets.baseline +++ b/build/secrets/.secrets.baseline @@ -608,7 +608,8 @@ "filename": "product.json", "hashed_secret": "4762b62a1d96fce4a3071114c8e0223be11d1fda", "is_verified": false, - "line_number": 231 + "line_number": 95, + "is_secret": false } ], "scripts\\playground-server.ts": [ @@ -1901,5 +1902,5 @@ } ] }, - "generated_at": "2025-02-24T22:34:34Z" + "generated_at": "2025-02-25T15:07:15Z" } diff --git a/product.json b/product.json index a2eed0403ba..f9124d4db59 100644 --- a/product.json +++ b/product.json @@ -88,18 +88,30 @@ } }, { - "name": "ms-toolsai.jupyter-keymap", - "version": "1.1.2", - "repo": "https://github.com/Microsoft/vscode-jupyter-keymap", + "name": "rstudio.rstudio-workbench", + "version": "1.5.28", + "positUrl": "https://cdn.posit.co/pwb-components/extension", + "type": "reh-web", + "sha256": "a1052e6cf884977b68883950360d29b394acdbdd2a84f0b17a7cc9887436918f", "metadata": { - "id": "9f6dc8db-620c-4844-b8c5-e74914f1be27", + "publisherDisplayName": "Posit Software, PBC" + } + } + ], + "bootstrapExtensions": [ + { + "name": "ms-python.black-formatter", + "version": "2024.2.0", + "repo": "https://github.com/microsoft/vscode-black-formatter", + "metadata": { + "id": "859e640c-c157-47da-8699-9080b81c8371", "publisherId": { - "publisherId": "ac8eb7c9-3e59-4b39-8040-f0484d8170ce", - "publisherName": "ms-toolsai", - "displayName": "Jupyter Keymap", + "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8", + "publisherName": "ms-python", + "displayName": "Black Formatter", "flags": "verified" }, - "publisherDisplayName": "ms-toolsai" + "publisherDisplayName": "ms-python" } }, { @@ -117,6 +129,21 @@ "publisherDisplayName": "ms-toolsai" } }, + { + "name": "ms-toolsai.jupyter-keymap", + "version": "1.1.2", + "repo": "https://github.com/Microsoft/vscode-jupyter-keymap", + "metadata": { + "id": "9f6dc8db-620c-4844-b8c5-e74914f1be27", + "publisherId": { + "publisherId": "ac8eb7c9-3e59-4b39-8040-f0484d8170ce", + "publisherName": "ms-toolsai", + "displayName": "Jupyter Keymap", + "flags": "verified" + }, + "publisherDisplayName": "ms-toolsai" + } + }, { "name": "ms-toolsai.vscode-jupyter-slideshow", "version": "0.1.6", @@ -177,34 +204,43 @@ "publisherDisplayName": "ms-pyright" } }, - { - "name": "ms-python.black-formatter", - "version": "2024.2.0", - "repo": "https://github.com/microsoft/vscode-black-formatter", - "metadata": { - "id": "859e640c-c157-47da-8699-9080b81c8371", - "publisherId": { - "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8", - "publisherName": "ms-python", - "displayName": "Black Formatter", - "flags": "verified" - }, - "publisherDisplayName": "ms-python" - } - }, { "name": "ms-python.debugpy", "version": "2024.8.0", "repo": "https://github.com/microsoft/vscode-python-debugger", "metadata": { "id": "4bd5d2c9-9d65-401a-b0b2-7498d9f17615", - "publisherId": { - "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8", - "publisherName": "ms-python", - "displayName": "Python Debugger", + "publisherid": { + "publisherid": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8", + "publishername": "ms-python", + "displayname": "python debugger", "flags": "verified" }, - "publisherDisplayName": "ms-python", + "publisherdisplayname": "ms-python", + "multiPlatformServiceUrl": "https://open-vsx.org/api" + } + }, + { + "name": "posit.publisher", + "version": "1.10.0", + "repo": "https://github.com/posit-dev/publisher", + "metadata": { + "id": "ccc4be7e-a835-4fcf-b439-79c25a0ae11e", + "publisherId": "090804ff-7eb2-4fbd-bb61-583e34f2b070", + "displayName": "Posit Publisher", + "multiPlatformServiceUrl": "https://open-vsx.org/api" + }, + "publisherDisplayName": "Posit Software, PBC" + }, + { + "name": "posit.shiny", + "version": "1.1.2", + "repo": "http://github.com/posit-dev/shiny-vscode", + "type": "reh-web", + "metadata": { + "id": "f1b3b3b4-3b3b-4b3b-8b3b-3b3b3b3b3b3b", + "publisherId": "090804ff-7eb2-4fbd-bb61-583e34f2b070", + "displayName": "Shiny", "multiPlatformServiceUrl": "https://open-vsx.org/api" } }, @@ -222,16 +258,6 @@ }, "publisherDisplayName": "Quarto" } - }, - { - "name": "rstudio.rstudio-workbench", - "version": "1.5.28", - "positUrl": "https://cdn.posit.co/pwb-components/extension", - "type": "reh-web", - "sha256": "a1052e6cf884977b68883950360d29b394acdbdd2a84f0b17a7cc9887436918f", - "metadata": { - "publisherDisplayName": "Posit PBC" - } } ], "extensionsGallery": { diff --git a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index c1999e774a6..516ef787b0f 100644 --- a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -122,6 +122,9 @@ import { IEphemeralStateService } from '../../../platform/ephemeralState/common/ import { EphemeralStateService } from '../../../platform/ephemeralState/common/ephemeralStateService.js'; import { DefaultExtensionsInitializer } from './contrib/defaultExtensionsInitializer.js'; import { AllowedExtensionsService } from '../../../platform/extensionManagement/common/allowedExtensionsService.js'; +// --- Start Positron --- +import { PositronBootstrapExtensionsInitializer } from '../../../platform/extensionManagement/node/positronBootstrapExtensionsInitializer.js'; +// --- End Positron --- class SharedProcessMain extends Disposable implements IClientConnectionFilter { @@ -192,7 +195,10 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { instantiationService.createInstance(LocalizationsUpdater), instantiationService.createInstance(ExtensionsContributions), instantiationService.createInstance(UserDataProfilesCleaner), - instantiationService.createInstance(DefaultExtensionsInitializer) + // --- Start Positron --- + instantiationService.createInstance(DefaultExtensionsInitializer), + instantiationService.createInstance(PositronBootstrapExtensionsInitializer) + // --- End Positron --- )); } diff --git a/src/vs/platform/extensionManagement/node/positronBootstrapExtensionsInitializer.ts b/src/vs/platform/extensionManagement/node/positronBootstrapExtensionsInitializer.ts new file mode 100644 index 00000000000..8f7e60e3e59 --- /dev/null +++ b/src/vs/platform/extensionManagement/node/positronBootstrapExtensionsInitializer.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ +import { IExtensionManagementService } from '../common/extensionManagement.js'; +import { IExtensionManifest } from '../../extensions/common/extensions.js'; +import { URI } from '../../../base/common/uri.js'; +import { join } from 'path'; +import { ILogService } from '../../log/common/log.js'; +import { IProductService } from '../../product/common/productService.js'; +import { INativeEnvironmentService } from '../../environment/common/environment.js'; +import { FileOperationResult, IFileService, IFileStat, toFileOperationResult } from '../../files/common/files.js'; +import { getErrorMessage } from '../../../base/common/errors.js'; +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { Disposable } from '../../../base/common/lifecycle.js'; + +export class PositronBootstrapExtensionsInitializer extends Disposable { + private readonly storageFilePath: string; + + constructor( + @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IFileService private readonly fileService: IFileService, + @IProductService private readonly productService: IProductService, + @ILogService private readonly logService: ILogService + ) { + super(); + + this.storageFilePath = join(this.getVSIXPath().fsPath, '.version'); + const currentVersion = this.productService.positronVersion; + + const lastKnownVersion = existsSync(this.storageFilePath) ? readFileSync(this.storageFilePath, 'utf8').trim() : ''; + + if (lastKnownVersion !== currentVersion) { + this.logService.info('First launch after first install, upgrade, or downgrade. Installing bootstrapped extensions'); + this.installVSIXOnStartup() + .then(() => { + try { + writeFileSync(this.storageFilePath, currentVersion); + } catch (error) { + this.logService.error('Error writing bootstrapped extension storage file', this.storageFilePath, getErrorMessage(error)); + } + }) + .catch(error => { + this.logService.error('Error installing bootstrapped extensions', getErrorMessage(error)); + }); + } else { + this.logService.info('Subsequent launch, skipping bootstrapped extensions'); + } + } + + async installVSIXOnStartup() { + + const extensionsLocation = this.getVSIXPath(); + let stat: IFileStat; + try { + stat = await this.fileService.resolve(extensionsLocation); + if (!stat.children) { + this.logService.debug('There are no extensions to install', extensionsLocation.toString()); + return; + } + } catch (error) { + if (toFileOperationResult(error) === FileOperationResult.FILE_NOT_FOUND) { + this.logService.debug('There are no extensions to install', extensionsLocation.toString()); + } + this.logService.error('Error initializing extensions', error); + return; + } + + const vsixFiles = stat.children.filter(child => child.name.endsWith('.vsix')); + if (vsixFiles.length === 0) { + this.logService.debug('There are no VSIX extension files to install', extensionsLocation.toString()); + return; + } + + const installedExtensions = await this.extensionManagementService.getInstalled(); + await Promise.all(vsixFiles.map(async vsix => { + this.logService.info('Installing extension:', vsix.resource.toString()); + try { + const vsixManifest: IExtensionManifest = await this.extensionManagementService.getManifest(vsix.resource); + const extensionId = vsixManifest.publisher + '.' + vsixManifest.name; + + const installedExtension = installedExtensions.find(e => e.identifier.id === extensionId); + if (installedExtension) { + const installedVersion = installedExtension.manifest.version; + if (!this.isVSIXNewer(installedVersion, vsixManifest.version)) { + this.logService.info('Extension is already installed and is up to date:', vsix.resource.toString()); + return; + } + } + await this.extensionManagementService.install(vsix.resource, { donotIncludePackAndDependencies: true, keepExisting: false }); + this.logService.info('Successfully installed extension:', vsix.resource.toString()); + } catch (error) { + this.logService.error('Error installing extension:', vsix.resource.toString(), getErrorMessage(error)); + } + })); + this.logService.info('Bootstrapped extensions initialized', extensionsLocation.toString()); + + } + + private isVSIXNewer(installedVersion: string, vsixVersion: string): boolean { + const [iMajor, iMinor, iPatch] = installedVersion.split('.').map(Number); + const [vMajor, vMinor, vPatch] = vsixVersion.split('.').map(Number); + + return vMajor > iMajor || (vMajor === iMajor && vMinor > iMinor) || (vMajor === iMajor && vMinor === iMinor && vPatch > iPatch); + } + + + private getVSIXPath(): URI { + return process.env['VSCODE_DEV'] ? URI.file(join(this.environmentService.appRoot, '.build', 'bootstrapExtensions')) : URI.file(join(this.environmentService.appRoot, 'extensions', 'bootstrap')); + } + + override dispose(): void { + this.logService.info('Disposing PositronBootstrapExtensionsInitializer'); + super.dispose(); + } +} diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index 553995c2471..14ab0f795c6 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -52,6 +52,10 @@ import { MandatoryServerConnectionToken } from './serverConnectionToken.js'; // --- Start PWB: Server proxy support --- import { kProxyRegex, kSessionUrl } from './pwbConstants.js'; import { IPwbHeartbeatService } from './pwbHeartbeat.js'; +import { PositronBootstrapExtensionsInitializer } from '../../platform/extensionManagement/node/positronBootstrapExtensionsInitializer.js'; +import { INativeEnvironmentService } from '../../platform/environment/common/environment.js'; +import { IExtensionManagementService } from '../../platform/extensionManagement/common/extensionManagement.js'; +import { IFileService } from '../../platform/files/common/files.js'; // --- End PWB --- const SHUTDOWN_TIMEOUT = 5 * 60 * 1000; @@ -768,6 +772,18 @@ export async function createServer(address: string | net.AddressInfo | null, arg const disposables = new DisposableStore(); const { socketServer, instantiationService } = await setupServerServices(connectionToken, args, REMOTE_DATA_FOLDER, disposables); + // --- Start Positron --- + instantiationService.invokeFunction((accessor) => { + const environmentService = accessor.get(INativeEnvironmentService); + const extensionManagementService = accessor.get(IExtensionManagementService); + const fileService = accessor.get(IFileService); + const productService = accessor.get(IProductService); + const logService = accessor.get(ILogService); + new PositronBootstrapExtensionsInitializer(environmentService, extensionManagementService, fileService, productService, logService); + }); + // --- End Positron --- + + // Set the unexpected error handler after the services have been initialized, to avoid having // the telemetry service overwrite our handler instantiationService.invokeFunction((accessor) => {