diff --git a/extensions/ql-vscode/src/codeql-cli/cli-command.ts b/extensions/ql-vscode/src/codeql-cli/cli-command.ts index 1b4ab75f9..b211793e1 100644 --- a/extensions/ql-vscode/src/codeql-cli/cli-command.ts +++ b/extensions/ql-vscode/src/codeql-cli/cli-command.ts @@ -3,7 +3,10 @@ import { promisify } from "util"; import type { BaseLogger } from "../common/logging"; import type { ProgressReporter } from "../common/logging/vscode"; -import { getChildProcessErrorMessage } from "../common/helpers-pure"; +import { + getChildProcessErrorMessage, + getErrorMessage, +} from "../common/helpers-pure"; /** * Flags to pass to all cli commands. @@ -11,26 +14,27 @@ import { getChildProcessErrorMessage } from "../common/helpers-pure"; export const LOGGING_FLAGS = ["-v", "--log-to-stderr"]; /** - * Runs a CodeQL CLI command without invoking the CLI server, returning the output as a string. + * Runs a CodeQL CLI command without invoking the CLI server, deserializing the output as JSON. * @param codeQlPath The path to the CLI. * @param command The `codeql` command to be run, provided as an array of command/subcommand names. * @param commandArgs The arguments to pass to the `codeql` command. * @param description Description of the action being run, to be shown in log and error messages. * @param logger Logger to write command log messages, e.g. to an output channel. * @param progressReporter Used to output progress messages, e.g. to the status bar. - * @returns The contents of the command's stdout, if the command succeeded. + * @returns A JSON object parsed from the contents of the command's stdout, if the command succeeded. */ -export async function runCodeQlCliCommand( +export async function runJsonCodeQlCliCommand( codeQlPath: string, command: string[], commandArgs: string[], description: string, logger: BaseLogger, progressReporter?: ProgressReporter, -): Promise { +): Promise { // Add logging arguments first, in case commandArgs contains positional parameters. const args = command.concat(LOGGING_FLAGS).concat(commandArgs); const argsString = args.join(" "); + let stdout: string; try { if (progressReporter !== undefined) { progressReporter.report({ message: description }); @@ -41,10 +45,18 @@ export async function runCodeQlCliCommand( const result = await promisify(execFile)(codeQlPath, args); void logger.log(result.stderr); void logger.log("CLI command succeeded."); - return result.stdout; + stdout = result.stdout; } catch (err) { throw new Error( `${description} failed: ${getChildProcessErrorMessage(err)}`, ); } + + try { + return JSON.parse(stdout) as OutputType; + } catch (err) { + throw new Error( + `Parsing output of ${description} failed: ${getErrorMessage(err)}`, + ); + } } diff --git a/extensions/ql-vscode/src/codeql-cli/cli-version.ts b/extensions/ql-vscode/src/codeql-cli/cli-version.ts index 17e995d02..975ed7d79 100644 --- a/extensions/ql-vscode/src/codeql-cli/cli-version.ts +++ b/extensions/ql-vscode/src/codeql-cli/cli-version.ts @@ -1,25 +1,49 @@ import type { SemVer } from "semver"; import { parse } from "semver"; -import { runCodeQlCliCommand } from "./cli-command"; +import { runJsonCodeQlCliCommand } from "./cli-command"; import type { Logger } from "../common/logging"; import { getErrorMessage } from "../common/helpers-pure"; +interface VersionResult { + version: string; + features: CliFeatures | undefined; +} + +export interface CliFeatures { + featuresInVersionResult?: boolean; + mrvaPackCreate?: boolean; +} + +export interface VersionAndFeatures { + version: SemVer; + features: CliFeatures; +} + /** * Get the version of a CodeQL CLI. */ export async function getCodeQlCliVersion( codeQlPath: string, logger: Logger, -): Promise { +): Promise { try { - const output: string = await runCodeQlCliCommand( + const output: VersionResult = await runJsonCodeQlCliCommand( codeQlPath, ["version"], - ["--format=terse"], + ["--format=json"], "Checking CodeQL version", logger, ); - return parse(output.trim()) || undefined; + + const version = parse(output.version.trim()) || undefined; + if (version === undefined) { + return undefined; + } + + return { + version, + features: output.features ?? {}, + }; } catch (e) { // Failed to run the version command. This might happen if the cli version is _really_ old, or it is corrupted. // Either way, we can't determine compatibility. diff --git a/extensions/ql-vscode/src/codeql-cli/cli.ts b/extensions/ql-vscode/src/codeql-cli/cli.ts index e04e012ea..1db2fa0ca 100644 --- a/extensions/ql-vscode/src/codeql-cli/cli.ts +++ b/extensions/ql-vscode/src/codeql-cli/cli.ts @@ -34,6 +34,7 @@ import { QueryLanguage } from "../common/query-language"; import { LINE_ENDINGS, splitStreamAtSeparators } from "../common/split-stream"; import type { Position } from "../query-server/messages"; import { LOGGING_FLAGS } from "./cli-command"; +import type { CliFeatures, VersionAndFeatures } from "./cli-version"; /** * The version of the SARIF format that we are using. @@ -193,7 +194,9 @@ type OnLineCallback = ( line: string, ) => Promise | string | undefined; -type VersionChangedListener = (newVersion: SemVer | undefined) => void; +type VersionChangedListener = ( + newVersionAndFeatures: VersionAndFeatures | undefined, +) => void; /** * This class manages a cli server started by `codeql execute cli-server` to @@ -211,8 +214,8 @@ export class CodeQLCliServer implements Disposable { /** A buffer with a single null byte. */ nullBuffer: Buffer; - /** Version of current cli, lazily computed by the `getVersion()` method */ - private _version: SemVer | undefined; + /** Version of current cli and its supported features, lazily computed by the `getVersion()` method */ + private _versionAndFeatures: VersionAndFeatures | undefined; private _versionChangedListeners: VersionChangedListener[] = []; @@ -288,7 +291,7 @@ export class CodeQLCliServer implements Disposable { const callback = (): void => { try { this.killProcessIfRunning(); - this._version = undefined; + this._versionAndFeatures = undefined; this._supportedLanguages = undefined; } finally { this.runNext(); @@ -1417,6 +1420,27 @@ export class CodeQLCliServer implements Disposable { ); } + public async packCreate( + dir: string, + workspaceFolders: string[], + outputPath: string, + moreOptions: string[], + ): Promise { + const args = [ + "--output", + outputPath, + dir, + ...moreOptions, + ...this.getAdditionalPacksArg(workspaceFolders), + ]; + + return this.runJsonCodeQlCliCommandWithAuthentication( + ["pack", "create"], + args, + "Creating pack", + ); + } + async packBundle( dir: string, workspaceFolders: string[], @@ -1481,27 +1505,35 @@ export class CodeQLCliServer implements Disposable { ); } - public async getVersion() { - if (!this._version) { + public async getVersion(): Promise { + return (await this.getVersionAndFeatures()).version; + } + + public async getFeatures(): Promise { + return (await this.getVersionAndFeatures()).features; + } + + public async getVersionAndFeatures(): Promise { + if (!this._versionAndFeatures) { try { - const newVersion = await this.refreshVersion(); - this._version = newVersion; + const newVersionAndFeatures = await this.refreshVersion(); + this._versionAndFeatures = newVersionAndFeatures; this._versionChangedListeners.forEach((listener) => - listener(newVersion), + listener(newVersionAndFeatures), ); // this._version is only undefined upon config change, so we reset CLI-based context key only when necessary. await this.app.commands.execute( "setContext", "codeql.supportsQuickEvalCount", - newVersion.compare( + newVersionAndFeatures.version.compare( CliVersionConstraint.CLI_VERSION_WITH_QUICK_EVAL_COUNT, ) >= 0, ); await this.app.commands.execute( "setContext", "codeql.supportsTrimCache", - newVersion.compare( + newVersionAndFeatures.version.compare( CliVersionConstraint.CLI_VERSION_WITH_TRIM_CACHE, ) >= 0, ); @@ -1512,23 +1544,23 @@ export class CodeQLCliServer implements Disposable { throw e; } } - return this._version; + return this._versionAndFeatures; } public addVersionChangedListener(listener: VersionChangedListener) { - if (this._version) { - listener(this._version); + if (this._versionAndFeatures) { + listener(this._versionAndFeatures); } this._versionChangedListeners.push(listener); } - private async refreshVersion() { + private async refreshVersion(): Promise { const distribution = await this.distributionProvider.getDistribution(); switch (distribution.kind) { case FindDistributionResultKind.CompatibleDistribution: // eslint-disable-next-line no-fallthrough -- Intentional fallthrough case FindDistributionResultKind.IncompatibleDistribution: - return distribution.version; + return distribution.versionAndFeatures; default: // We should not get here because if no distributions are available, then @@ -1745,4 +1777,8 @@ export class CliVersionConstraint { CliVersionConstraint.CLI_VERSION_WITH_EXTENSIBLE_PREDICATE_METADATA, ); } + + async supportsMrvaPackCreate(): Promise { + return (await this.cli.getFeatures()).mrvaPackCreate === true; + } } diff --git a/extensions/ql-vscode/src/codeql-cli/distribution.ts b/extensions/ql-vscode/src/codeql-cli/distribution.ts index 108fb4e01..e69fca721 100644 --- a/extensions/ql-vscode/src/codeql-cli/distribution.ts +++ b/extensions/ql-vscode/src/codeql-cli/distribution.ts @@ -1,11 +1,11 @@ import { createWriteStream, mkdtemp, pathExists, remove } from "fs-extra"; import { tmpdir } from "os"; import { delimiter, dirname, join } from "path"; -import type { SemVer } from "semver"; import { Range, satisfies } from "semver"; import type { Event, ExtensionContext } from "vscode"; import type { DistributionConfig } from "../config"; import { extLogger } from "../common/logging/vscode"; +import type { VersionAndFeatures } from "./cli-version"; import { getCodeQlCliVersion } from "./cli-version"; import type { ProgressCallback } from "../common/vscode/progress"; import { reportStreamProgress } from "../common/vscode/progress"; @@ -88,11 +88,11 @@ export class DistributionManager implements DistributionProvider { kind: FindDistributionResultKind.NoDistribution, }; } - const version = await getCodeQlCliVersion( + const versionAndFeatures = await getCodeQlCliVersion( distribution.codeQlPath, extLogger, ); - if (version === undefined) { + if (versionAndFeatures === undefined) { return { distribution, kind: FindDistributionResultKind.UnknownCompatibilityDistribution, @@ -119,17 +119,21 @@ export class DistributionManager implements DistributionProvider { distribution.kind !== DistributionKind.ExtensionManaged || this.config.includePrerelease; - if (!satisfies(version, this.versionRange, { includePrerelease })) { + if ( + !satisfies(versionAndFeatures.version, this.versionRange, { + includePrerelease, + }) + ) { return { distribution, kind: FindDistributionResultKind.IncompatibleDistribution, - version, + versionAndFeatures, }; } return { distribution, kind: FindDistributionResultKind.CompatibleDistribution, - version, + versionAndFeatures, }; } @@ -599,7 +603,7 @@ interface DistributionResult { interface CompatibleDistributionResult extends DistributionResult { kind: FindDistributionResultKind.CompatibleDistribution; - version: SemVer; + versionAndFeatures: VersionAndFeatures; } interface UnknownCompatibilityDistributionResult extends DistributionResult { @@ -608,7 +612,7 @@ interface UnknownCompatibilityDistributionResult extends DistributionResult { interface IncompatibleDistributionResult extends DistributionResult { kind: FindDistributionResultKind.IncompatibleDistribution; - version: SemVer; + versionAndFeatures: VersionAndFeatures; } interface NoDistributionResult { diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 58be99fad..ecc4cda54 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -418,7 +418,7 @@ export async function activate( codeQlExtension.variantAnalysisManager, ); codeQlExtension.cliServer.addVersionChangedListener((ver) => { - telemetryListener.cliVersion = ver; + telemetryListener.cliVersion = ver?.version; }); let unsupportedWarningShown = false; @@ -431,13 +431,16 @@ export async function activate( return; } - if (CliVersionConstraint.OLDEST_SUPPORTED_CLI_VERSION.compare(ver) < 0) { + if ( + CliVersionConstraint.OLDEST_SUPPORTED_CLI_VERSION.compare(ver.version) < + 0 + ) { return; } void showAndLogWarningMessage( extLogger, - `You are using an unsupported version of the CodeQL CLI (${ver}). ` + + `You are using an unsupported version of the CodeQL CLI (${ver.version}). ` + `The minimum supported version is ${CliVersionConstraint.OLDEST_SUPPORTED_CLI_VERSION}. ` + `Please upgrade to a newer version of the CodeQL CLI.`, ); @@ -592,7 +595,7 @@ async function getDistributionDisplayingDistributionWarnings( switch (result.kind) { case FindDistributionResultKind.CompatibleDistribution: void extLogger.log( - `Found compatible version of CodeQL CLI (version ${result.version.raw})`, + `Found compatible version of CodeQL CLI (version ${result.versionAndFeatures.version.raw})`, ); break; case FindDistributionResultKind.IncompatibleDistribution: { @@ -612,7 +615,7 @@ async function getDistributionDisplayingDistributionWarnings( void showAndLogWarningMessage( extLogger, - `The current version of the CodeQL CLI (${result.version.raw}) ` + + `The current version of the CodeQL CLI (${result.versionAndFeatures.version.raw}) ` + `is incompatible with this extension. ${fixGuidanceMessage}`, ); break;