Merge pull request #2072 from github/aeisenberg/mrva-extension-packs

Inject extension pack dependencies into MRVA packs
This commit is contained in:
Andrew Eisenberg
2023-03-14 10:41:35 -07:00
committed by GitHub
7 changed files with 220 additions and 59 deletions

View File

@@ -1284,11 +1284,25 @@ export class CodeQLCliServer implements Disposable {
);
}
async packInstall(dir: string, forceUpdate = false) {
async packInstall(
dir: string,
{ forceUpdate = false, workspaceFolders = [] as string[] } = {},
) {
const args = [dir];
if (forceUpdate) {
args.push("--mode", "update");
}
if (workspaceFolders?.length > 0) {
if (await this.cliConstraints.supportsAdditionalPacksInstall()) {
args.push(
// Allow prerelease packs from the ql submodule.
"--allow-prerelease",
// Allow the use of --additional-packs argument without issueing a warning
"--no-strict-mode",
...this.getAdditionalPacksArg(workspaceFolders),
);
}
}
return this.runJsonCodeQlCliCommandWithAuthentication(
["pack", "install"],
args,
@@ -1692,6 +1706,13 @@ export class CliVersionConstraint {
*/
public static CLI_VERSION_WITH_QLPACKS_KIND = new SemVer("2.12.3");
/**
* CLI version that supports the `--additional-packs` option for the `pack install` command.
*/
public static CLI_VERSION_WITH_ADDITIONAL_PACKS_INSTALL = new SemVer(
"2.12.4",
);
constructor(private readonly cli: CodeQLCliServer) {
/**/
}
@@ -1755,4 +1776,10 @@ export class CliVersionConstraint {
CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND,
);
}
async supportsAdditionalPacksInstall() {
return this.isVersionAtLeast(
CliVersionConstraint.CLI_VERSION_WITH_ADDITIONAL_PACKS_INSTALL,
);
}
}

View File

@@ -2,6 +2,10 @@ import { join } from "path";
import { pathExists } from "fs-extra";
export const QLPACK_FILENAMES = ["qlpack.yml", "codeql-pack.yml"];
export const QLPACK_LOCK_FILENAMES = [
"qlpack.lock.yml",
"codeql-pack.lock.yml",
];
export const FALLBACK_QLPACK_FILENAME = QLPACK_FILENAMES[0];
export async function getQlPackPath(

View File

@@ -143,7 +143,7 @@ export async function displayQuickQuery(
if (shouldRewrite) {
await cliServer.clearCache();
await cliServer.packInstall(queriesDir, true);
await cliServer.packInstall(queriesDir, { forceUpdate: true });
}
await Window.showTextDocument(await workspace.openTextDocument(qlFile));

View File

@@ -34,6 +34,7 @@ import {
getQlPackPath,
FALLBACK_QLPACK_FILENAME,
QLPACK_FILENAMES,
QLPACK_LOCK_FILENAMES,
} from "../pure/ql";
export interface QlPack {
@@ -70,42 +71,23 @@ async function generateQueryPack(
const originalPackRoot = await findPackRoot(queryFile);
const packRelativePath = relative(originalPackRoot, queryFile);
const targetQueryFileName = join(queryPackDir, packRelativePath);
const workspaceFolders = getOnDiskWorkspaceFolders();
let language: string | undefined;
// Check if the query is already in a query pack.
// If so, copy the entire query pack to the temporary directory.
// Otherwise, copy only the query file to the temporary directory
// and generate a synthetic query pack.
if (await getQlPackPath(originalPackRoot)) {
// don't include ql files. We only want the queryFile to be copied.
const toCopy = await cliServer.packPacklist(originalPackRoot, false);
// also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist.
[
join(originalPackRoot, "qlpack.lock.yml"),
join(originalPackRoot, "codeql-pack.lock.yml"),
await copyExistingQueryPack(
cliServer,
originalPackRoot,
queryFile,
].forEach((absolutePath) => {
if (absolutePath) {
toCopy.push(absolutePath);
}
});
let copiedCount = 0;
await copy(originalPackRoot, queryPackDir, {
filter: (file: string) =>
// copy file if it is in the packlist, or it is a parent directory of a file in the packlist
!!toCopy.find((f) => {
// Normalized paths ensure that Windows drive letters are capitalized consistently.
const normalizedPath = Uri.file(f).fsPath;
const matches =
normalizedPath === file || normalizedPath.startsWith(file + sep);
if (matches) {
copiedCount++;
}
return matches;
}),
});
void extLogger.log(`Copied ${copiedCount} files to ${queryPackDir}`);
await fixPackFile(queryPackDir, packRelativePath);
queryPackDir,
packRelativePath,
);
language = await findLanguage(cliServer, Uri.file(targetQueryFileName));
} else {
@@ -114,20 +96,12 @@ async function generateQueryPack(
// copy only the query file to the query pack directory
// and generate a synthetic query pack
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
await copy(queryFile, targetQueryFileName);
void extLogger.log("Generating synthetic query pack");
const syntheticQueryPack = {
name: QUERY_PACK_NAME,
version: "0.0.0",
dependencies: {
[`codeql/${language}-all`]: "*",
},
defaultSuite: generateDefaultSuite(packRelativePath),
};
await writeFile(
join(queryPackDir, FALLBACK_QLPACK_FILENAME),
dump(syntheticQueryPack),
await createNewQueryPack(
queryFile,
queryPackDir,
targetQueryFileName,
language,
packRelativePath,
);
}
if (!language) {
@@ -149,12 +123,21 @@ async function generateQueryPack(
precompilationOpts = ["--no-precompile"];
}
if (await cliServer.useExtensionPacks()) {
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(
`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`,
);
await cliServer.packInstall(queryPackDir);
const workspaceFolders = getOnDiskWorkspaceFolders();
await cliServer.packBundle(
queryPackDir,
workspaceFolders,
@@ -168,6 +151,70 @@ async function generateQueryPack(
};
}
async function createNewQueryPack(
queryFile: string,
queryPackDir: string,
targetQueryFileName: string,
language: string | undefined,
packRelativePath: string,
) {
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
await copy(queryFile, targetQueryFileName);
void extLogger.log("Generating synthetic query pack");
const syntheticQueryPack = {
name: QUERY_PACK_NAME,
version: "0.0.0",
dependencies: {
[`codeql/${language}-all`]: "*",
},
defaultSuite: generateDefaultSuite(packRelativePath),
};
await writeFile(
join(queryPackDir, FALLBACK_QLPACK_FILENAME),
dump(syntheticQueryPack),
);
}
async function copyExistingQueryPack(
cliServer: cli.CodeQLCliServer,
originalPackRoot: string,
queryFile: string,
queryPackDir: string,
packRelativePath: string,
) {
const toCopy = await cliServer.packPacklist(originalPackRoot, false);
[
// also copy the lock file (either new name or old name) and the query file itself. These are not included in the packlist.
...QLPACK_LOCK_FILENAMES.map((f) => join(originalPackRoot, f)),
queryFile,
].forEach((absolutePath) => {
if (absolutePath) {
toCopy.push(absolutePath);
}
});
let copiedCount = 0;
await copy(originalPackRoot, queryPackDir, {
filter: (file: string) =>
// copy file if it is in the packlist, or it is a parent directory of a file in the packlist
!!toCopy.find((f) => {
// Normalized paths ensure that Windows drive letters are capitalized consistently.
const normalizedPath = Uri.file(f).fsPath;
const matches =
normalizedPath === file || normalizedPath.startsWith(file + sep);
if (matches) {
copiedCount++;
}
return matches;
}),
});
void extLogger.log(`Copied ${copiedCount} files to ${queryPackDir}`);
await fixPackFile(queryPackDir, packRelativePath);
}
async function findPackRoot(queryFile: string): Promise<string> {
// recursively find the directory containing qlpack.yml or codeql-pack.yml
let dir = dirname(queryFile);
@@ -329,19 +376,54 @@ async function fixPackFile(
}
const qlpack = load(await readFile(packPath, "utf8")) as QlPack;
// update pack name
qlpack.name = QUERY_PACK_NAME;
// update default suite
delete qlpack.defaultSuiteFile;
qlpack.defaultSuite = generateDefaultSuite(packRelativePath);
// remove any ${workspace} version references
updateDefaultSuite(qlpack, packRelativePath);
removeWorkspaceRefs(qlpack);
await writeFile(packPath, dump(qlpack));
}
async function injectExtensionPacks(
cliServer: cli.CodeQLCliServer,
queryPackDir: string,
workspaceFolders: string[],
) {
const qlpackFile = await getQlPackPath(queryPackDir);
if (!qlpackFile) {
throw new Error(
`Could not find ${QLPACK_FILENAMES.join(
" or ",
)} file in '${queryPackDir}'`,
);
}
const syntheticQueryPack = load(await readFile(qlpackFile, "utf8")) as QlPack;
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
// version we specify, since we are guaranteed that the extension pack
// is resolved from source at the given path.
syntheticQueryPack.dependencies[name] = "*";
});
await writeFile(qlpackFile, dump(syntheticQueryPack));
await cliServer.clearCache();
}
function updateDefaultSuite(qlpack: QlPack, packRelativePath: string) {
delete qlpack.defaultSuiteFile;
qlpack.defaultSuite = generateDefaultSuite(packRelativePath);
}
function generateDefaultSuite(packRelativePath: string) {
return [
{

View File

@@ -0,0 +1,12 @@
extensions:
- addsTo:
pack: codeql/javascript-all
extensible: sourceModel
data:
- [ "@example/read-write-user-data", "Member[readUserData].ReturnValue.Awaited", "remote" ]
- addsTo:
pack: codeql/javascript-all
extensible: sinkModel
data:
- [ "@example/read-write-user-data", "Member[writeUserData].Argument[0]", "command-line-injection" ]

View File

@@ -0,0 +1,8 @@
name: github/extension-pack-for-testing
version: 0.0.0
library: true
extensionTargets:
github/remote-query-pack: "*"
dataExtensions:
- extension-file.yml

View File

@@ -12,7 +12,7 @@ import * as ghApiClient from "../../../../src/variant-analysis/gh-api/gh-api-cli
import { join } from "path";
import { VariantAnalysisManager } from "../../../../src/variant-analysis/variant-analysis-manager";
import { CodeQLCliServer } from "../../../../src/cli";
import { CliVersionConstraint, CodeQLCliServer } from "../../../../src/cli";
import {
fixWorkspaceReferences,
restoreWorkspaceReferences,
@@ -255,6 +255,30 @@ describe("Variant Analysis Manager", () => {
qlxFilesThatExist: ["subfolder/in-pack.qlx"],
});
});
it("should run a remote query with extension packs inside a qlpack", async () => {
if (!(await cli.cliConstraints.supportsQlpacksKind())) {
console.log(
`Skipping test because qlpacks kind is only suppported in CLI version ${CliVersionConstraint.CLI_VERSION_WITH_QLPACKS_KIND} or later.`,
);
return;
}
await cli.setUseExtensionPacks(true);
await doVariantAnalysisTest({
queryPath: "data-remote-qlpack-nested/subfolder/in-pack.ql",
filesThatExist: [
"subfolder/in-pack.ql",
"otherfolder/lib.qll",
".codeql/libraries/semmle/targets-extension/0.0.0/ext/extension.yml",
],
filesThatDoNotExist: ["subfolder/not-in-pack.ql"],
qlxFilesThatExist: ["subfolder/in-pack.qlx"],
dependenciesToCheck: [
"codeql/javascript-all",
"semmle/targets-extension",
],
});
});
});
async function doVariantAnalysisTest({
@@ -262,11 +286,13 @@ describe("Variant Analysis Manager", () => {
filesThatExist,
qlxFilesThatExist,
filesThatDoNotExist,
dependenciesToCheck = ["codeql/javascript-all"],
}: {
queryPath: string;
filesThatExist: string[];
qlxFilesThatExist: string[];
filesThatDoNotExist: string[];
dependenciesToCheck?: string[];
}) {
const fileUri = getFile(queryPath);
await variantAnalysisManager.runVariantAnalysis(
@@ -328,8 +354,10 @@ describe("Variant Analysis Manager", () => {
const actualLockKeys = Object.keys(qlpackLockContents.dependencies);
// The lock file should contain at least codeql/javascript-all.
expect(actualLockKeys).toContain("codeql/javascript-all");
// The lock file should contain at least the specified dependencies.
dependenciesToCheck.forEach((dep) =>
expect(actualLockKeys).toContain(dep),
);
}
function getFile(file: string): Uri {