Merge pull request #3233 from github/dbartol/mrva-multi-bundle

Use CLI to bundle packs for MRVA
This commit is contained in:
Dave Bartolomeo
2024-01-18 09:10:21 -05:00
committed by GitHub
12 changed files with 451 additions and 127 deletions

View File

@@ -3,7 +3,10 @@ import { promisify } from "util";
import type { BaseLogger } from "../common/logging"; import type { BaseLogger } from "../common/logging";
import type { ProgressReporter } from "../common/logging/vscode"; 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. * 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"]; 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 codeQlPath The path to the CLI.
* @param command The `codeql` command to be run, provided as an array of command/subcommand names. * @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 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 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 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. * @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<OutputType>(
codeQlPath: string, codeQlPath: string,
command: string[], command: string[],
commandArgs: string[], commandArgs: string[],
description: string, description: string,
logger: BaseLogger, logger: BaseLogger,
progressReporter?: ProgressReporter, progressReporter?: ProgressReporter,
): Promise<string> { ): Promise<OutputType> {
// Add logging arguments first, in case commandArgs contains positional parameters. // Add logging arguments first, in case commandArgs contains positional parameters.
const args = command.concat(LOGGING_FLAGS).concat(commandArgs); const args = command.concat(LOGGING_FLAGS).concat(commandArgs);
const argsString = args.join(" "); const argsString = args.join(" ");
let stdout: string;
try { try {
if (progressReporter !== undefined) { if (progressReporter !== undefined) {
progressReporter.report({ message: description }); progressReporter.report({ message: description });
@@ -41,10 +45,18 @@ export async function runCodeQlCliCommand(
const result = await promisify(execFile)(codeQlPath, args); const result = await promisify(execFile)(codeQlPath, args);
void logger.log(result.stderr); void logger.log(result.stderr);
void logger.log("CLI command succeeded."); void logger.log("CLI command succeeded.");
return result.stdout; stdout = result.stdout;
} catch (err) { } catch (err) {
throw new Error( throw new Error(
`${description} failed: ${getChildProcessErrorMessage(err)}`, `${description} failed: ${getChildProcessErrorMessage(err)}`,
); );
} }
try {
return JSON.parse(stdout) as OutputType;
} catch (err) {
throw new Error(
`Parsing output of ${description} failed: ${getErrorMessage(err)}`,
);
}
} }

View File

@@ -1,25 +1,49 @@
import type { SemVer } from "semver"; import type { SemVer } from "semver";
import { parse } from "semver"; import { parse } from "semver";
import { runCodeQlCliCommand } from "./cli-command"; import { runJsonCodeQlCliCommand } from "./cli-command";
import type { Logger } from "../common/logging"; import type { Logger } from "../common/logging";
import { getErrorMessage } from "../common/helpers-pure"; 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. * Get the version of a CodeQL CLI.
*/ */
export async function getCodeQlCliVersion( export async function getCodeQlCliVersion(
codeQlPath: string, codeQlPath: string,
logger: Logger, logger: Logger,
): Promise<SemVer | undefined> { ): Promise<VersionAndFeatures | undefined> {
try { try {
const output: string = await runCodeQlCliCommand( const output: VersionResult = await runJsonCodeQlCliCommand<VersionResult>(
codeQlPath, codeQlPath,
["version"], ["version"],
["--format=terse"], ["--format=json"],
"Checking CodeQL version", "Checking CodeQL version",
logger, 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) { } catch (e) {
// Failed to run the version command. This might happen if the cli version is _really_ old, or it is corrupted. // 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. // Either way, we can't determine compatibility.

View File

@@ -34,6 +34,7 @@ import { QueryLanguage } from "../common/query-language";
import { LINE_ENDINGS, splitStreamAtSeparators } from "../common/split-stream"; import { LINE_ENDINGS, splitStreamAtSeparators } from "../common/split-stream";
import type { Position } from "../query-server/messages"; import type { Position } from "../query-server/messages";
import { LOGGING_FLAGS } from "./cli-command"; import { LOGGING_FLAGS } from "./cli-command";
import type { CliFeatures, VersionAndFeatures } from "./cli-version";
/** /**
* The version of the SARIF format that we are using. * The version of the SARIF format that we are using.
@@ -203,7 +204,9 @@ type OnLineCallback = (
line: string, line: string,
) => Promise<string | undefined> | string | undefined; ) => Promise<string | undefined> | 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 * This class manages a cli server started by `codeql execute cli-server` to
@@ -221,8 +224,8 @@ export class CodeQLCliServer implements Disposable {
/** A buffer with a single null byte. */ /** A buffer with a single null byte. */
nullBuffer: Buffer; nullBuffer: Buffer;
/** Version of current cli, lazily computed by the `getVersion()` method */ /** Version of current cli and its supported features, lazily computed by the `getVersion()` method */
private _version: SemVer | undefined; private _versionAndFeatures: VersionAndFeatures | undefined;
private _versionChangedListeners: VersionChangedListener[] = []; private _versionChangedListeners: VersionChangedListener[] = [];
@@ -298,7 +301,7 @@ export class CodeQLCliServer implements Disposable {
const callback = (): void => { const callback = (): void => {
try { try {
this.killProcessIfRunning(); this.killProcessIfRunning();
this._version = undefined; this._versionAndFeatures = undefined;
this._supportedLanguages = undefined; this._supportedLanguages = undefined;
} finally { } finally {
this.runNext(); this.runNext();
@@ -1427,16 +1430,28 @@ export class CodeQLCliServer implements Disposable {
); );
} }
/**
* Compile a CodeQL pack and bundle it into a single file.
*
* @param sourcePackDir The directory of the input CodeQL pack.
* @param workspaceFolders The workspace folders to search for additional packs.
* @param outputBundleFile The path to the output bundle file.
* @param outputPackDir The directory to contain the unbundled output pack.
* @param moreOptions Additional options to be passed to `codeql pack bundle`.
*/
async packBundle( async packBundle(
dir: string, sourcePackDir: string,
workspaceFolders: string[], workspaceFolders: string[],
outputPath: string, outputBundleFile: string,
outputPackDir: string,
moreOptions: string[], moreOptions: string[],
): Promise<void> { ): Promise<void> {
const args = [ const args = [
"-o", "-o",
outputPath, outputBundleFile,
dir, sourcePackDir,
"--pack-path",
outputPackDir,
...moreOptions, ...moreOptions,
...this.getAdditionalPacksArg(workspaceFolders), ...this.getAdditionalPacksArg(workspaceFolders),
]; ];
@@ -1491,27 +1506,35 @@ export class CodeQLCliServer implements Disposable {
); );
} }
public async getVersion() { public async getVersion(): Promise<SemVer> {
if (!this._version) { return (await this.getVersionAndFeatures()).version;
}
public async getFeatures(): Promise<CliFeatures> {
return (await this.getVersionAndFeatures()).features;
}
private async getVersionAndFeatures(): Promise<VersionAndFeatures> {
if (!this._versionAndFeatures) {
try { try {
const newVersion = await this.refreshVersion(); const newVersionAndFeatures = await this.refreshVersion();
this._version = newVersion; this._versionAndFeatures = newVersionAndFeatures;
this._versionChangedListeners.forEach((listener) => 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. // this._version is only undefined upon config change, so we reset CLI-based context key only when necessary.
await this.app.commands.execute( await this.app.commands.execute(
"setContext", "setContext",
"codeql.supportsQuickEvalCount", "codeql.supportsQuickEvalCount",
newVersion.compare( newVersionAndFeatures.version.compare(
CliVersionConstraint.CLI_VERSION_WITH_QUICK_EVAL_COUNT, CliVersionConstraint.CLI_VERSION_WITH_QUICK_EVAL_COUNT,
) >= 0, ) >= 0,
); );
await this.app.commands.execute( await this.app.commands.execute(
"setContext", "setContext",
"codeql.supportsTrimCache", "codeql.supportsTrimCache",
newVersion.compare( newVersionAndFeatures.version.compare(
CliVersionConstraint.CLI_VERSION_WITH_TRIM_CACHE, CliVersionConstraint.CLI_VERSION_WITH_TRIM_CACHE,
) >= 0, ) >= 0,
); );
@@ -1522,23 +1545,23 @@ export class CodeQLCliServer implements Disposable {
throw e; throw e;
} }
} }
return this._version; return this._versionAndFeatures;
} }
public addVersionChangedListener(listener: VersionChangedListener) { public addVersionChangedListener(listener: VersionChangedListener) {
if (this._version) { if (this._versionAndFeatures) {
listener(this._version); listener(this._versionAndFeatures);
} }
this._versionChangedListeners.push(listener); this._versionChangedListeners.push(listener);
} }
private async refreshVersion() { private async refreshVersion(): Promise<VersionAndFeatures> {
const distribution = await this.distributionProvider.getDistribution(); const distribution = await this.distributionProvider.getDistribution();
switch (distribution.kind) { switch (distribution.kind) {
case FindDistributionResultKind.CompatibleDistribution: case FindDistributionResultKind.CompatibleDistribution:
// eslint-disable-next-line no-fallthrough -- Intentional fallthrough // eslint-disable-next-line no-fallthrough -- Intentional fallthrough
case FindDistributionResultKind.IncompatibleDistribution: case FindDistributionResultKind.IncompatibleDistribution:
return distribution.version; return distribution.versionAndFeatures;
default: default:
// We should not get here because if no distributions are available, then // We should not get here because if no distributions are available, then
@@ -1755,4 +1778,8 @@ export class CliVersionConstraint {
CliVersionConstraint.CLI_VERSION_WITH_EXTENSIBLE_PREDICATE_METADATA, CliVersionConstraint.CLI_VERSION_WITH_EXTENSIBLE_PREDICATE_METADATA,
); );
} }
async supportsMrvaPackCreate(): Promise<boolean> {
return (await this.cli.getFeatures()).mrvaPackCreate === true;
}
} }

View File

@@ -1,11 +1,11 @@
import { createWriteStream, mkdtemp, pathExists, remove } from "fs-extra"; import { createWriteStream, mkdtemp, pathExists, remove } from "fs-extra";
import { tmpdir } from "os"; import { tmpdir } from "os";
import { delimiter, dirname, join } from "path"; import { delimiter, dirname, join } from "path";
import type { SemVer } from "semver";
import { Range, satisfies } from "semver"; import { Range, satisfies } from "semver";
import type { Event, ExtensionContext } from "vscode"; import type { Event, ExtensionContext } from "vscode";
import type { DistributionConfig } from "../config"; import type { DistributionConfig } from "../config";
import { extLogger } from "../common/logging/vscode"; import { extLogger } from "../common/logging/vscode";
import type { VersionAndFeatures } from "./cli-version";
import { getCodeQlCliVersion } from "./cli-version"; import { getCodeQlCliVersion } from "./cli-version";
import type { ProgressCallback } from "../common/vscode/progress"; import type { ProgressCallback } from "../common/vscode/progress";
import { reportStreamProgress } from "../common/vscode/progress"; import { reportStreamProgress } from "../common/vscode/progress";
@@ -88,11 +88,11 @@ export class DistributionManager implements DistributionProvider {
kind: FindDistributionResultKind.NoDistribution, kind: FindDistributionResultKind.NoDistribution,
}; };
} }
const version = await getCodeQlCliVersion( const versionAndFeatures = await getCodeQlCliVersion(
distribution.codeQlPath, distribution.codeQlPath,
extLogger, extLogger,
); );
if (version === undefined) { if (versionAndFeatures === undefined) {
return { return {
distribution, distribution,
kind: FindDistributionResultKind.UnknownCompatibilityDistribution, kind: FindDistributionResultKind.UnknownCompatibilityDistribution,
@@ -119,17 +119,21 @@ export class DistributionManager implements DistributionProvider {
distribution.kind !== DistributionKind.ExtensionManaged || distribution.kind !== DistributionKind.ExtensionManaged ||
this.config.includePrerelease; this.config.includePrerelease;
if (!satisfies(version, this.versionRange, { includePrerelease })) { if (
!satisfies(versionAndFeatures.version, this.versionRange, {
includePrerelease,
})
) {
return { return {
distribution, distribution,
kind: FindDistributionResultKind.IncompatibleDistribution, kind: FindDistributionResultKind.IncompatibleDistribution,
version, versionAndFeatures,
}; };
} }
return { return {
distribution, distribution,
kind: FindDistributionResultKind.CompatibleDistribution, kind: FindDistributionResultKind.CompatibleDistribution,
version, versionAndFeatures,
}; };
} }
@@ -599,7 +603,7 @@ interface DistributionResult {
interface CompatibleDistributionResult extends DistributionResult { interface CompatibleDistributionResult extends DistributionResult {
kind: FindDistributionResultKind.CompatibleDistribution; kind: FindDistributionResultKind.CompatibleDistribution;
version: SemVer; versionAndFeatures: VersionAndFeatures;
} }
interface UnknownCompatibilityDistributionResult extends DistributionResult { interface UnknownCompatibilityDistributionResult extends DistributionResult {
@@ -608,7 +612,7 @@ interface UnknownCompatibilityDistributionResult extends DistributionResult {
interface IncompatibleDistributionResult extends DistributionResult { interface IncompatibleDistributionResult extends DistributionResult {
kind: FindDistributionResultKind.IncompatibleDistribution; kind: FindDistributionResultKind.IncompatibleDistribution;
version: SemVer; versionAndFeatures: VersionAndFeatures;
} }
interface NoDistributionResult { interface NoDistributionResult {

View File

@@ -0,0 +1,117 @@
import { platform } from "os";
import { basename, dirname, join, normalize, resolve } from "path";
import { lstat, readdir } from "fs/promises";
import type { BaseLogger } from "./logging";
/**
* Expands a path that potentially contains 8.3 short names (e.g. "C:\PROGRA~1" instead of "C:\Program Files").
*
* See https://en.wikipedia.org/wiki/8.3_filename if you're not familiar with Windows 8.3 short names.
*
* @param shortPath The path to expand.
* @returns A normalized, absolute path, with any short components expanded.
*/
export async function expandShortPaths(
shortPath: string,
logger: BaseLogger,
): Promise<string> {
const absoluteShortPath = normalize(resolve(shortPath));
if (platform() !== "win32") {
// POSIX doesn't have short paths.
return absoluteShortPath;
}
void logger.log(`Expanding short paths in: ${absoluteShortPath}`);
// A quick check to see if there might be any short components.
// There might be a case where a short component doesn't contain a `~`, but if there is, I haven't
// found it.
// This may find long components that happen to have a '~', but that's OK.
if (absoluteShortPath.indexOf("~") < 0) {
// No short components to expand.
void logger.log(`Skipping due to no short components`);
return absoluteShortPath;
}
return await expandShortPathRecursive(absoluteShortPath, logger);
}
/**
* Expand a single short path component
* @param dir The absolute path of the directory containing the short path component.
* @param shortBase The shot path component to expand.
* @returns The expanded path component.
*/
async function expandShortPathComponent(
dir: string,
shortBase: string,
logger: BaseLogger,
): Promise<string> {
void logger.log(`Expanding short path component: ${shortBase}`);
const fullPath = join(dir, shortBase);
// Use `lstat` instead of `stat` to avoid following symlinks.
const stats = await lstat(fullPath, { bigint: true });
if (stats.dev === BigInt(0) || stats.ino === BigInt(0)) {
// No inode info, so we won't be able to find this in the directory listing.
void logger.log(`No inode info available. Skipping.`);
return shortBase;
}
void logger.log(`dev/inode: ${stats.dev}/${stats.ino}`);
try {
// Enumerate the children of the parent directory, and try to find one with the same dev/inode.
const children = await readdir(dir);
for (const child of children) {
void logger.log(`considering child: ${child}`);
try {
const childStats = await lstat(join(dir, child), { bigint: true });
void logger.log(`child dev/inode: ${childStats.dev}/${childStats.ino}`);
if (childStats.dev === stats.dev && childStats.ino === stats.ino) {
// Found a match.
void logger.log(`Found a match: ${child}`);
return child;
}
} catch (e) {
// Can't read stats for the child, so skip it.
void logger.log(`Error reading stats for child: ${e}`);
}
}
} catch (e) {
// Can't read the directory, so we won't be able to find this in the directory listing.
void logger.log(`Error reading directory: ${e}`);
return shortBase;
}
void logger.log(`No match found. Returning original.`);
return shortBase;
}
/**
* Expand the short path components in a path, including those in ancestor directories.
* @param shortPath The path to expand.
* @returns The expanded path.
*/
async function expandShortPathRecursive(
shortPath: string,
logger: BaseLogger,
): Promise<string> {
const shortBase = basename(shortPath);
if (shortBase.length === 0) {
// We've reached the root.
return shortPath;
}
const dir = await expandShortPathRecursive(dirname(shortPath), logger);
void logger.log(`dir: ${dir}`);
void logger.log(`base: ${shortBase}`);
if (shortBase.indexOf("~") < 0) {
// This component doesn't have a short name, so just append it to the (long) parent.
void logger.log(`Component is not a short name`);
return join(dir, shortBase);
}
// This component looks like it has a short name, so try to expand it.
const longBase = await expandShortPathComponent(dir, shortBase, logger);
return join(dir, longBase);
}

View File

@@ -430,7 +430,7 @@ export async function activate(
codeQlExtension.variantAnalysisManager, codeQlExtension.variantAnalysisManager,
); );
codeQlExtension.cliServer.addVersionChangedListener((ver) => { codeQlExtension.cliServer.addVersionChangedListener((ver) => {
telemetryListener.cliVersion = ver; telemetryListener.cliVersion = ver?.version;
}); });
let unsupportedWarningShown = false; let unsupportedWarningShown = false;
@@ -443,13 +443,16 @@ export async function activate(
return; return;
} }
if (CliVersionConstraint.OLDEST_SUPPORTED_CLI_VERSION.compare(ver) < 0) { if (
CliVersionConstraint.OLDEST_SUPPORTED_CLI_VERSION.compare(ver.version) <
0
) {
return; return;
} }
void showAndLogWarningMessage( void showAndLogWarningMessage(
extLogger, 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}. ` + `The minimum supported version is ${CliVersionConstraint.OLDEST_SUPPORTED_CLI_VERSION}. ` +
`Please upgrade to a newer version of the CodeQL CLI.`, `Please upgrade to a newer version of the CodeQL CLI.`,
); );
@@ -604,7 +607,7 @@ async function getDistributionDisplayingDistributionWarnings(
switch (result.kind) { switch (result.kind) {
case FindDistributionResultKind.CompatibleDistribution: case FindDistributionResultKind.CompatibleDistribution:
void extLogger.log( 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; break;
case FindDistributionResultKind.IncompatibleDistribution: { case FindDistributionResultKind.IncompatibleDistribution: {
@@ -624,7 +627,7 @@ async function getDistributionDisplayingDistributionWarnings(
void showAndLogWarningMessage( void showAndLogWarningMessage(
extLogger, 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}`, `is incompatible with this extension. ${fixGuidanceMessage}`,
); );
break; break;

View File

@@ -3,6 +3,7 @@ import { Uri, window } from "vscode";
import { relative, join, sep, dirname, parse, basename } from "path"; import { relative, join, sep, dirname, parse, basename } from "path";
import { dump, load } from "js-yaml"; import { dump, load } from "js-yaml";
import { copy, writeFile, readFile, mkdirp } from "fs-extra"; import { copy, writeFile, readFile, mkdirp } from "fs-extra";
import type { DirectoryResult } from "tmp-promise";
import { dir, tmpName } from "tmp-promise"; import { dir, tmpName } from "tmp-promise";
import { tmpDir } from "../tmp-dir"; import { tmpDir } from "../tmp-dir";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders"; import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
@@ -37,6 +38,7 @@ import type { QueryLanguage } from "../common/query-language";
import { tryGetQueryMetadata } from "../codeql-cli/query-metadata"; import { tryGetQueryMetadata } from "../codeql-cli/query-metadata";
import { askForLanguage, findLanguage } from "../codeql-cli/query-language"; import { askForLanguage, findLanguage } from "../codeql-cli/query-language";
import type { QlPackFile } from "../packaging/qlpack-file"; import type { QlPackFile } from "../packaging/qlpack-file";
import { expandShortPaths } from "../common/short-paths";
/** /**
* Well-known names for the query pack used by the server. * Well-known names for the query pack used by the server.
@@ -58,21 +60,52 @@ interface GeneratedQueryPack {
async function generateQueryPack( async function generateQueryPack(
cliServer: CodeQLCliServer, cliServer: CodeQLCliServer,
queryFile: string, queryFile: string,
queryPackDir: string, tmpDir: RemoteQueryTempDir,
): Promise<GeneratedQueryPack> { ): Promise<GeneratedQueryPack> {
const originalPackRoot = await findPackRoot(queryFile); const originalPackRoot = await findPackRoot(queryFile);
const packRelativePath = relative(originalPackRoot, queryFile); const packRelativePath = relative(originalPackRoot, queryFile);
const targetQueryFileName = join(queryPackDir, packRelativePath);
const workspaceFolders = getOnDiskWorkspaceFolders(); const workspaceFolders = getOnDiskWorkspaceFolders();
const extensionPacks = await getExtensionPacksToInject(
cliServer,
workspaceFolders,
);
let language: QueryLanguage | undefined; const mustSynthesizePack =
(await getQlPackPath(originalPackRoot)) === undefined;
const cliSupportsMrvaPackCreate =
await cliServer.cliConstraints.supportsMrvaPackCreate();
// Check if the query is already in a query pack. const language: QueryLanguage | undefined = mustSynthesizePack
// If so, copy the entire query pack to the temporary directory. ? await askForLanguage(cliServer) // open popup to ask for language if not already hardcoded
// Otherwise, copy only the query file to the temporary directory : await findLanguage(cliServer, Uri.file(queryFile));
// and generate a synthetic query pack. if (!language) {
if (await getQlPackPath(originalPackRoot)) { throw new UserCancellationException("Could not determine language");
// don't include ql files. We only want the queryFile to be copied. }
let queryPackDir: string;
let needsInstall: boolean;
if (mustSynthesizePack) {
// This section applies whether or not the CLI supports MRVA pack creation directly.
queryPackDir = tmpDir.queryPackDir;
// Synthesize a query pack for the query.
// copy only the query file to the query pack directory
// and generate a synthetic query pack
await createNewQueryPack(
queryFile,
queryPackDir,
language,
packRelativePath,
);
// Clear the cliServer cache so that the previous qlpack text is purged from the CLI.
await cliServer.clearCache();
// Install packs, since we just synthesized a dependency on the language's standard library.
needsInstall = true;
} else if (!cliSupportsMrvaPackCreate) {
// We need to copy the query pack to a temporary directory and then fix it up to work with MRVA.
queryPackDir = tmpDir.queryPackDir;
await copyExistingQueryPack( await copyExistingQueryPack(
cliServer, cliServer,
originalPackRoot, originalPackRoot,
@@ -81,52 +114,55 @@ async function generateQueryPack(
packRelativePath, packRelativePath,
); );
language = await findLanguage(cliServer, Uri.file(targetQueryFileName)); // We should already have all the dependencies available, but these older versions of the CLI
// have a bug where they will not search `--additional-packs` during validation in `codeql pack bundle`.
// Installing the packs will ensure that any extension packs get put in the right place.
needsInstall = true;
} else { } else {
// open popup to ask for language if not already hardcoded // The CLI supports creating a MRVA query pack directly from the source pack.
language = await askForLanguage(cliServer); queryPackDir = originalPackRoot;
// We expect any dependencies to be available already.
// copy only the query file to the query pack directory needsInstall = false;
// and generate a synthetic query pack
await createNewQueryPack(
queryFile,
queryPackDir,
targetQueryFileName,
language,
packRelativePath,
);
}
if (!language) {
throw new UserCancellationException("Could not determine language.");
} }
// Clear the cliServer cache so that the previous qlpack text is purged from the CLI. if (needsInstall) {
await cliServer.clearCache(); // Install the dependencies of the synthesized query pack.
await cliServer.packInstall(queryPackDir, {
workspaceFolders,
});
let precompilationOpts: string[] = []; // Clear the CLI cache so that the most recent qlpack lock file is used.
if (await cliServer.cliConstraints.usesGlobalCompilationCache()) { await cliServer.clearCache();
precompilationOpts = ["--qlx"]; }
} else {
const ccache = join(originalPackRoot, ".cache"); let precompilationOpts: string[];
if (cliSupportsMrvaPackCreate) {
precompilationOpts = [ precompilationOpts = [
"--qlx", "--mrva",
"--no-default-compilation-cache", "--query",
`--compilation-cache=${ccache}`, join(queryPackDir, packRelativePath),
// We need to specify the extension packs as dependencies so that they are included in the MRVA pack.
// The version range doesn't matter, since they'll always be found by source lookup.
...extensionPacks.map((p) => `--extension-pack=${p}@*`),
]; ];
} else {
if (await cliServer.cliConstraints.usesGlobalCompilationCache()) {
precompilationOpts = ["--qlx"];
} else {
const cache = join(originalPackRoot, ".cache");
precompilationOpts = [
"--qlx",
"--no-default-compilation-cache",
`--compilation-cache=${cache}`,
];
}
if (extensionPacks.length > 0) {
await addExtensionPacksAsDependencies(queryPackDir, extensionPacks);
}
} }
if (await cliServer.useExtensionPacks()) { const bundlePath = tmpDir.bundleFile;
await injectExtensionPacks(cliServer, queryPackDir, workspaceFolders);
}
await cliServer.packInstall(queryPackDir, {
workspaceFolders,
});
// Clear the CLI cache so that the most recent qlpack lock file is used.
await cliServer.clearCache();
const bundlePath = await getPackedBundlePath(queryPackDir);
void extLogger.log( void extLogger.log(
`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`, `Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`,
); );
@@ -134,6 +170,7 @@ async function generateQueryPack(
queryPackDir, queryPackDir,
workspaceFolders, workspaceFolders,
bundlePath, bundlePath,
tmpDir.compiledPackDir,
precompilationOpts, precompilationOpts,
); );
const base64Pack = (await readFile(bundlePath)).toString("base64"); const base64Pack = (await readFile(bundlePath)).toString("base64");
@@ -146,11 +183,11 @@ async function generateQueryPack(
async function createNewQueryPack( async function createNewQueryPack(
queryFile: string, queryFile: string,
queryPackDir: string, queryPackDir: string,
targetQueryFileName: string,
language: string | undefined, language: string | undefined,
packRelativePath: string, packRelativePath: string,
) { ) {
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`); void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
const targetQueryFileName = join(queryPackDir, packRelativePath);
await copy(queryFile, targetQueryFileName); await copy(queryFile, targetQueryFileName);
void extLogger.log("Generating synthetic query pack"); void extLogger.log("Generating synthetic query pack");
const syntheticQueryPack = { const syntheticQueryPack = {
@@ -242,19 +279,37 @@ function isFileSystemRoot(dir: string): boolean {
return pathObj.root === dir && pathObj.base === ""; return pathObj.root === dir && pathObj.base === "";
} }
async function createRemoteQueriesTempDirectory() { interface RemoteQueryTempDir {
const remoteQueryDir = await dir({ remoteQueryDir: DirectoryResult;
queryPackDir: string;
compiledPackDir: string;
bundleFile: string;
}
async function createRemoteQueriesTempDirectory(): Promise<RemoteQueryTempDir> {
const shortRemoteQueryDir = await dir({
dir: tmpDir.name, dir: tmpDir.name,
unsafeCleanup: true, unsafeCleanup: true,
}); });
// Expand 8.3 filenames here to work around a CLI bug where `codeql pack bundle` produces an empty
// archive if the pack path contains any 8.3 components.
const remoteQueryDir = {
...shortRemoteQueryDir,
path: await expandShortPaths(shortRemoteQueryDir.path, extLogger),
};
const queryPackDir = join(remoteQueryDir.path, "query-pack"); const queryPackDir = join(remoteQueryDir.path, "query-pack");
await mkdirp(queryPackDir); await mkdirp(queryPackDir);
return { remoteQueryDir, queryPackDir }; const compiledPackDir = join(remoteQueryDir.path, "compiled-pack");
const bundleFile = await expandShortPaths(
await getPackedBundlePath(tmpDir.name),
extLogger,
);
return { remoteQueryDir, queryPackDir, compiledPackDir, bundleFile };
} }
async function getPackedBundlePath(queryPackDir: string) { async function getPackedBundlePath(remoteQueryDir: string): Promise<string> {
return tmpName({ return tmpName({
dir: dirname(queryPackDir), dir: remoteQueryDir,
postfix: "generated.tgz", postfix: "generated.tgz",
prefix: "qlpack", prefix: "qlpack",
}); });
@@ -322,15 +377,14 @@ export async function prepareRemoteQueryRun(
throw new UserCancellationException("Cancelled"); throw new UserCancellationException("Cancelled");
} }
const { remoteQueryDir, queryPackDir } = const tempDir = await createRemoteQueriesTempDirectory();
await createRemoteQueriesTempDirectory();
let pack: GeneratedQueryPack; let pack: GeneratedQueryPack;
try { try {
pack = await generateQueryPack(cliServer, queryFile, queryPackDir); pack = await generateQueryPack(cliServer, queryFile, tempDir);
} finally { } finally {
await remoteQueryDir.cleanup(); await tempDir.remoteQueryDir.cleanup();
} }
const { base64Pack, language } = pack; const { base64Pack, language } = pack;
@@ -397,11 +451,38 @@ async function fixPackFile(
await writeFile(packPath, dump(qlpack)); await writeFile(packPath, dump(qlpack));
} }
async function injectExtensionPacks( async function getExtensionPacksToInject(
cliServer: CodeQLCliServer, cliServer: CodeQLCliServer,
queryPackDir: string,
workspaceFolders: string[], workspaceFolders: string[],
) { ): Promise<string[]> {
const result: string[] = [];
if (await cliServer.useExtensionPacks()) {
const extensionPacks = await cliServer.resolveQlpacks(
workspaceFolders,
true,
);
Object.entries(extensionPacks).forEach(([name, paths]) => {
// We are guaranteed that there is at least one path found for each extension pack.
// If there are multiple paths, then we have a problem. This means that there is
// ambiguity in which path to use. This is an error.
if (paths.length > 1) {
throw new Error(
`Multiple versions of extension pack '${name}' found: ${paths.join(
", ",
)}`,
);
}
result.push(name);
});
}
return result;
}
async function addExtensionPacksAsDependencies(
queryPackDir: string,
extensionPacks: string[],
): Promise<void> {
const qlpackFile = await getQlPackPath(queryPackDir); const qlpackFile = await getQlPackPath(queryPackDir);
if (!qlpackFile) { if (!qlpackFile) {
throw new Error( throw new Error(
@@ -410,24 +491,13 @@ async function injectExtensionPacks(
)} file in '${queryPackDir}'`, )} file in '${queryPackDir}'`,
); );
} }
const syntheticQueryPack = load( const syntheticQueryPack = load(
await readFile(qlpackFile, "utf8"), await readFile(qlpackFile, "utf8"),
) as QlPackFile; ) as QlPackFile;
const dependencies = syntheticQueryPack.dependencies ?? {}; const dependencies = syntheticQueryPack.dependencies ?? {};
extensionPacks.forEach((name) => {
const extensionPacks = await cliServer.resolveQlpacks(workspaceFolders, true);
Object.entries(extensionPacks).forEach(([name, paths]) => {
// We are guaranteed that there is at least one path found for each extension pack.
// If there are multiple paths, then we have a problem. This means that there is
// ambiguity in which path to use. This is an error.
if (paths.length > 1) {
throw new Error(
`Multiple versions of extension pack '${name}' found: ${paths.join(
", ",
)}`,
);
}
// Add this extension pack as a dependency. It doesn't matter which // Add this extension pack as a dependency. It doesn't matter which
// version we specify, since we are guaranteed that the extension pack // version we specify, since we are guaranteed that the extension pack
// is resolved from source at the given path. // is resolved from source at the given path.
@@ -437,7 +507,6 @@ async function injectExtensionPacks(
syntheticQueryPack.dependencies = dependencies; syntheticQueryPack.dependencies = dependencies;
await writeFile(qlpackFile, dump(syntheticQueryPack)); await writeFile(qlpackFile, dump(syntheticQueryPack));
await cliServer.clearCache();
} }
function updateDefaultSuite(qlpack: QlPackFile, packRelativePath: string) { function updateDefaultSuite(qlpack: QlPackFile, packRelativePath: string) {

View File

@@ -0,0 +1,50 @@
import { expect } from "@jest/globals";
import type { MatcherFunction } from "expect";
import type { QueryPackFS } from "../vscode-tests/utils/bundled-pack-helpers";
import { EOL } from "os";
/**
* Custom Jest matcher to check if a file exists in a query pack.
*/
// eslint-disable-next-line func-style -- We need to set the type of this function
const toExistInCodeQLPack: MatcherFunction<[packFS: QueryPackFS]> = function (
actual,
packFS,
) {
if (typeof actual !== "string") {
throw new TypeError(
`Expected actual value to be a string. Found ${typeof actual}`,
);
}
const pass = packFS.fileExists(actual);
if (pass) {
return {
pass: true,
message: () => `expected ${actual} not to exist in pack`,
};
} else {
const files = packFS.allFiles();
const filesString = files.length > 0 ? files.join(EOL) : "<none>";
return {
pass: false,
message: () =>
`expected ${actual} to exist in pack.\nThe following files were found in the pack:\n${filesString}`,
};
}
};
expect.extend({ toExistInCodeQLPack });
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace -- We need to extend this global declaration
namespace jest {
interface AsymmetricMatchers {
toExistInCodeQLPack(packFS: QueryPackFS): void;
}
interface Matchers<R> {
toExistInCodeQLPack(packFS: QueryPackFS): R;
}
}
}

View File

@@ -1,4 +1,4 @@
name: github/remote-query-pack name: github/remote-query-pack
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
codeql/javascript-all: ${workspace} codeql/javascript-all: "*"

View File

@@ -24,6 +24,8 @@ import { readBundledPack } from "../../utils/bundled-pack-helpers";
import { load } from "js-yaml"; import { load } from "js-yaml";
import type { ExtensionPackMetadata } from "../../../../src/model-editor/extension-pack-metadata"; import type { ExtensionPackMetadata } from "../../../../src/model-editor/extension-pack-metadata";
import type { QlPackLockFile } from "../../../../src/packaging/qlpack-lock-file"; import type { QlPackLockFile } from "../../../../src/packaging/qlpack-lock-file";
//import { expect } from "@jest/globals";
import "../../../matchers/toExistInCodeQLPack";
describe("Variant Analysis Manager", () => { describe("Variant Analysis Manager", () => {
let cli: CodeQLCliServer; let cli: CodeQLCliServer;
@@ -331,14 +333,14 @@ describe("Variant Analysis Manager", () => {
const packFS = await readBundledPack(request.query.pack); const packFS = await readBundledPack(request.query.pack);
filesThatExist.forEach((file) => { filesThatExist.forEach((file) => {
expect(packFS.fileExists(file)).toBe(true); expect(file).toExistInCodeQLPack(packFS);
}); });
qlxFilesThatExist.forEach((file) => { qlxFilesThatExist.forEach((file) => {
expect(packFS.fileExists(file)).toBe(true); expect(file).toExistInCodeQLPack(packFS);
}); });
filesThatDoNotExist.forEach((file) => { filesThatDoNotExist.forEach((file) => {
expect(packFS.fileExists(file)).toBe(false); expect(file).not.toExistInCodeQLPack(packFS);
}); });
expect( expect(
@@ -364,9 +366,17 @@ describe("Variant Analysis Manager", () => {
// Assume the first dependency to check is the core library. // Assume the first dependency to check is the core library.
if (dependenciesToCheck.length > 0) { if (dependenciesToCheck.length > 0) {
expect(qlpackContents.dependencies?.[dependenciesToCheck[0]]).toEqual( const dependencyVersion =
"*", qlpackContents.dependencies?.[dependenciesToCheck[0]];
);
// There should be a version specified.
expect(dependencyVersion).toBeDefined();
// Any `${workspace}` placeholder should have been replaced.
// The actual version might be `*` (for the legacy code path where we replace workspace
// references with `*`) or a specific version (for the new code path where the CLI does all
// the work).
expect(dependencyVersion).not.toEqual("${workspace}");
} }
const qlpackLockContents = load( const qlpackLockContents = load(
packFS.fileContents("codeql-pack.lock.yml").toString("utf-8"), packFS.fileContents("codeql-pack.lock.yml").toString("utf-8"),

View File

@@ -1,3 +1,4 @@
import { basename } from "path";
import { workspace } from "vscode"; import { workspace } from "vscode";
/** /**
@@ -6,7 +7,10 @@ import { workspace } from "vscode";
*/ */
function hasCodeQL() { function hasCodeQL() {
const folders = workspace.workspaceFolders; const folders = workspace.workspaceFolders;
return !!folders?.some((folder) => folder.uri.path.endsWith("/codeql")); return !!folders?.some((folder) => {
const name = basename(folder.uri.fsPath);
return name === "codeql" || name === "ql";
});
} }
// describeWithCodeQL will be equal to describe if the CodeQL libraries are // describeWithCodeQL will be equal to describe if the CodeQL libraries are

View File

@@ -4,10 +4,11 @@ import { extract as tar_extract } from "tar-stream";
import { pipeline } from "stream/promises"; import { pipeline } from "stream/promises";
import { createGunzip } from "zlib"; import { createGunzip } from "zlib";
interface QueryPackFS { export interface QueryPackFS {
fileExists: (name: string) => boolean; fileExists: (name: string) => boolean;
fileContents: (name: string) => Buffer; fileContents: (name: string) => Buffer;
directoryContents: (name: string) => string[]; directoryContents: (name: string) => string[];
allFiles: () => string[];
} }
export async function readBundledPack( export async function readBundledPack(
@@ -82,5 +83,8 @@ export async function readBundledPack(
) )
.map((dir) => dir.substring(name.length + 1)); .map((dir) => dir.substring(name.length + 1));
}, },
allFiles: (): string[] => {
return Object.keys(files);
},
}; };
} }