Skip to content

Commit 29d8c65

Browse files
author
Dave Bartolomeo
committed
Use codeql pack create --mrva if available
1 parent fb63ec7 commit 29d8c65

1 file changed

Lines changed: 136 additions & 77 deletions

File tree

extensions/ql-vscode/src/variant-analysis/run-remote-query.ts

Lines changed: 136 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Uri, window } from "vscode";
33
import { relative, join, sep, dirname, parse, basename } from "path";
44
import { dump, load } from "js-yaml";
55
import { copy, writeFile, readFile, mkdirp } from "fs-extra";
6+
import type { DirectoryResult } from "tmp-promise";
67
import { dir, tmpName } from "tmp-promise";
78
import { tmpDir } from "../tmp-dir";
89
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
@@ -58,84 +59,119 @@ interface GeneratedQueryPack {
5859
async function generateQueryPack(
5960
cliServer: CodeQLCliServer,
6061
queryFile: string,
61-
queryPackDir: string,
62+
tmpDir: RemoteQueryTempDir,
6263
): Promise<GeneratedQueryPack> {
6364
const originalPackRoot = await findPackRoot(queryFile);
6465
const packRelativePath = relative(originalPackRoot, queryFile);
65-
const targetQueryFileName = join(queryPackDir, packRelativePath);
6666
const workspaceFolders = getOnDiskWorkspaceFolders();
67+
const extensionPacks = await getExtensionPacksToInject(
68+
cliServer,
69+
workspaceFolders,
70+
);
6771

68-
let language: QueryLanguage | undefined;
72+
const mustSynthesizePack =
73+
(await getQlPackPath(originalPackRoot)) === undefined;
74+
const cliSupportsMrvaPackCreate =
75+
await cliServer.cliConstraints.supportsMrvaPackCreate();
6976

70-
// Check if the query is already in a query pack.
71-
// If so, copy the entire query pack to the temporary directory.
72-
// Otherwise, copy only the query file to the temporary directory
73-
// and generate a synthetic query pack.
74-
if (await getQlPackPath(originalPackRoot)) {
75-
// don't include ql files. We only want the queryFile to be copied.
76-
await copyExistingQueryPack(
77-
cliServer,
78-
originalPackRoot,
79-
queryFile,
80-
queryPackDir,
81-
packRelativePath,
82-
);
77+
const language: QueryLanguage | undefined = mustSynthesizePack
78+
? await askForLanguage(cliServer) // open popup to ask for language if not already hardcoded
79+
: await findLanguage(cliServer, Uri.file(queryFile));
80+
if (!language) {
81+
throw new UserCancellationException("Could not determine language.");
82+
}
8383

84-
language = await findLanguage(cliServer, Uri.file(targetQueryFileName));
85-
} else {
86-
// open popup to ask for language if not already hardcoded
87-
language = await askForLanguage(cliServer);
84+
let queryPackDir: string;
85+
let precompilationOpts: string[];
86+
let needsInstall: boolean;
87+
if (mustSynthesizePack) {
88+
// This section applies whether or not the CLI supports MRVA pack creation directly.
89+
90+
queryPackDir = tmpDir.queryPackDir;
8891

92+
// Synthesize a query pack for the query.
8993
// copy only the query file to the query pack directory
9094
// and generate a synthetic query pack
9195
await createNewQueryPack(
9296
queryFile,
9397
queryPackDir,
94-
targetQueryFileName,
9598
language,
9699
packRelativePath,
97100
);
98-
}
99-
if (!language) {
100-
throw new UserCancellationException("Could not determine language.");
101-
}
102-
103-
// Clear the cliServer cache so that the previous qlpack text is purged from the CLI.
104-
await cliServer.clearCache();
101+
// Clear the cliServer cache so that the previous qlpack text is purged from the CLI.
102+
await cliServer.clearCache();
103+
104+
// Install packs, since we just synthesized a dependency on the language's standard library.
105+
needsInstall = true;
106+
} else if (!cliSupportsMrvaPackCreate) {
107+
// We need to copy the query pack to a temporary directory and then fix it up to work with MRVA.
108+
queryPackDir = tmpDir.queryPackDir;
109+
await copyExistingQueryPack(
110+
cliServer,
111+
originalPackRoot,
112+
queryFile,
113+
queryPackDir,
114+
packRelativePath,
115+
);
105116

106-
let precompilationOpts: string[] = [];
107-
if (await cliServer.cliConstraints.usesGlobalCompilationCache()) {
108-
precompilationOpts = ["--qlx"];
117+
// We should already have all the dependencies available, but these older versions of the CLI
118+
// have a bug where they will not search `--additional-packs` during validation in `codeql pack bundle`.
119+
// Installing the packs will ensure that any extension packs get put in the right place.
120+
needsInstall = true;
109121
} else {
110-
const ccache = join(originalPackRoot, ".cache");
111-
precompilationOpts = [
112-
"--qlx",
113-
"--no-default-compilation-cache",
114-
`--compilation-cache=${ccache}`,
115-
];
122+
// The CLI supports creating a MRVA query pack directly from the source pack.
123+
queryPackDir = originalPackRoot;
124+
// We expect any dependencies to be available already.
125+
needsInstall = false;
116126
}
117127

118-
if (await cliServer.useExtensionPacks()) {
119-
await injectExtensionPacks(cliServer, queryPackDir, workspaceFolders);
120-
}
128+
if (needsInstall) {
129+
// Install the dependencies of the synthesized query pack.
130+
await cliServer.packInstall(queryPackDir, {
131+
workspaceFolders,
132+
});
121133

122-
await cliServer.packInstall(queryPackDir, {
123-
workspaceFolders,
124-
});
134+
// Clear the CLI cache so that the most recent qlpack lock file is used.
135+
await cliServer.clearCache();
136+
}
125137

126138
// Clear the CLI cache so that the most recent qlpack lock file is used.
127139
await cliServer.clearCache();
140+
if (cliSupportsMrvaPackCreate) {
141+
precompilationOpts = [
142+
"--mrva",
143+
"--query",
144+
join(queryPackDir, packRelativePath),
145+
// We need to specify the extension packs as dependencies so that they are included in the MRVA pack.
146+
// The version range doesn't matter, since they'll always be found by source lookup.
147+
...extensionPacks.map((p) => `--extension-pack=${p}@*`),
148+
];
149+
} else {
150+
if (await cliServer.cliConstraints.usesGlobalCompilationCache()) {
151+
precompilationOpts = ["--qlx"];
152+
} else {
153+
const ccache = join(originalPackRoot, ".cache");
154+
precompilationOpts = [
155+
"--qlx",
156+
"--no-default-compilation-cache",
157+
`--compilation-cache=${ccache}`,
158+
];
159+
}
128160

129-
const bundlePath = await getPackedBundlePath(queryPackDir);
161+
if (extensionPacks.length > 0) {
162+
await addExtensionPacksAsDependencies(queryPackDir, extensionPacks);
163+
}
164+
}
165+
166+
const bundlePath = tmpDir.bundleFile;
130167
void extLogger.log(
131168
`Compiling and bundling query pack from ${queryPackDir} to ${bundlePath}. (This may take a while.)`,
132169
);
133-
await cliServer.packBundle(
134-
queryPackDir,
135-
workspaceFolders,
136-
bundlePath,
137-
precompilationOpts,
138-
);
170+
await cliServer.packBundle(queryPackDir, workspaceFolders, bundlePath, [
171+
"--pack-path",
172+
tmpDir.compiledPackDir,
173+
...precompilationOpts,
174+
]);
139175
const base64Pack = (await readFile(bundlePath)).toString("base64");
140176
return {
141177
base64Pack,
@@ -146,11 +182,11 @@ async function generateQueryPack(
146182
async function createNewQueryPack(
147183
queryFile: string,
148184
queryPackDir: string,
149-
targetQueryFileName: string,
150185
language: string | undefined,
151186
packRelativePath: string,
152187
) {
153188
void extLogger.log(`Copying ${queryFile} to ${queryPackDir}`);
189+
const targetQueryFileName = join(queryPackDir, packRelativePath);
154190
await copy(queryFile, targetQueryFileName);
155191
void extLogger.log("Generating synthetic query pack");
156192
const syntheticQueryPack = {
@@ -242,19 +278,28 @@ function isFileSystemRoot(dir: string): boolean {
242278
return pathObj.root === dir && pathObj.base === "";
243279
}
244280

245-
async function createRemoteQueriesTempDirectory() {
281+
interface RemoteQueryTempDir {
282+
remoteQueryDir: DirectoryResult;
283+
queryPackDir: string;
284+
compiledPackDir: string;
285+
bundleFile: string;
286+
}
287+
288+
async function createRemoteQueriesTempDirectory(): Promise<RemoteQueryTempDir> {
246289
const remoteQueryDir = await dir({
247290
dir: tmpDir.name,
248291
unsafeCleanup: true,
249292
});
250293
const queryPackDir = join(remoteQueryDir.path, "query-pack");
251294
await mkdirp(queryPackDir);
252-
return { remoteQueryDir, queryPackDir };
295+
const compiledPackDir = join(remoteQueryDir.path, "compiled-pack");
296+
const bundleFile = await getPackedBundlePath(tmpDir.name);
297+
return { remoteQueryDir, queryPackDir, compiledPackDir, bundleFile };
253298
}
254299

255-
async function getPackedBundlePath(queryPackDir: string) {
300+
async function getPackedBundlePath(remoteQueryDir: string): Promise<string> {
256301
return tmpName({
257-
dir: dirname(queryPackDir),
302+
dir: remoteQueryDir,
258303
postfix: "generated.tgz",
259304
prefix: "qlpack",
260305
});
@@ -314,15 +359,14 @@ export async function prepareRemoteQueryRun(
314359
throw new UserCancellationException("Cancelled");
315360
}
316361

317-
const { remoteQueryDir, queryPackDir } =
318-
await createRemoteQueriesTempDirectory();
362+
const tempDir = await createRemoteQueriesTempDirectory();
319363

320364
let pack: GeneratedQueryPack;
321365

322366
try {
323-
pack = await generateQueryPack(cliServer, queryFile, queryPackDir);
367+
pack = await generateQueryPack(cliServer, queryFile, tempDir);
324368
} finally {
325-
await remoteQueryDir.cleanup();
369+
await tempDir.remoteQueryDir.cleanup();
326370
}
327371

328372
const { base64Pack, language } = pack;
@@ -389,11 +433,38 @@ async function fixPackFile(
389433
await writeFile(packPath, dump(qlpack));
390434
}
391435

392-
async function injectExtensionPacks(
436+
async function getExtensionPacksToInject(
393437
cliServer: CodeQLCliServer,
394-
queryPackDir: string,
395438
workspaceFolders: string[],
396-
) {
439+
): Promise<string[]> {
440+
const result: string[] = [];
441+
if (await cliServer.useExtensionPacks()) {
442+
const extensionPacks = await cliServer.resolveQlpacks(
443+
workspaceFolders,
444+
true,
445+
);
446+
Object.entries(extensionPacks).forEach(([name, paths]) => {
447+
// We are guaranteed that there is at least one path found for each extension pack.
448+
// If there are multiple paths, then we have a problem. This means that there is
449+
// ambiguity in which path to use. This is an error.
450+
if (paths.length > 1) {
451+
throw new Error(
452+
`Multiple versions of extension pack '${name}' found: ${paths.join(
453+
", ",
454+
)}`,
455+
);
456+
}
457+
result.push(name);
458+
});
459+
}
460+
461+
return result;
462+
}
463+
464+
async function addExtensionPacksAsDependencies(
465+
queryPackDir: string,
466+
extensionPacks: string[],
467+
): Promise<void> {
397468
const qlpackFile = await getQlPackPath(queryPackDir);
398469
if (!qlpackFile) {
399470
throw new Error(
@@ -402,24 +473,13 @@ async function injectExtensionPacks(
402473
)} file in '${queryPackDir}'`,
403474
);
404475
}
476+
405477
const syntheticQueryPack = load(
406478
await readFile(qlpackFile, "utf8"),
407479
) as QlPackFile;
408480

409481
const dependencies = syntheticQueryPack.dependencies ?? {};
410-
411-
const extensionPacks = await cliServer.resolveQlpacks(workspaceFolders, true);
412-
Object.entries(extensionPacks).forEach(([name, paths]) => {
413-
// We are guaranteed that there is at least one path found for each extension pack.
414-
// If there are multiple paths, then we have a problem. This means that there is
415-
// ambiguity in which path to use. This is an error.
416-
if (paths.length > 1) {
417-
throw new Error(
418-
`Multiple versions of extension pack '${name}' found: ${paths.join(
419-
", ",
420-
)}`,
421-
);
422-
}
482+
extensionPacks.forEach((name) => {
423483
// Add this extension pack as a dependency. It doesn't matter which
424484
// version we specify, since we are guaranteed that the extension pack
425485
// is resolved from source at the given path.
@@ -429,7 +489,6 @@ async function injectExtensionPacks(
429489
syntheticQueryPack.dependencies = dependencies;
430490

431491
await writeFile(qlpackFile, dump(syntheticQueryPack));
432-
await cliServer.clearCache();
433492
}
434493

435494
function updateDefaultSuite(qlpack: QlPackFile, packRelativePath: string) {
@@ -534,7 +593,7 @@ async function getControllerRepoFromApi(
534593
}
535594
}
536595

537-
function removeWorkspaceRefs(qlpack: QlPackFile) {
596+
export function removeWorkspaceRefs(qlpack: QlPackFile) {
538597
if (!qlpack.dependencies) {
539598
return;
540599
}

0 commit comments

Comments
 (0)