@@ -3,6 +3,7 @@ import { Uri, window } from "vscode";
33import { relative , join , sep , dirname , parse , basename } from "path" ;
44import { dump , load } from "js-yaml" ;
55import { copy , writeFile , readFile , mkdirp } from "fs-extra" ;
6+ import type { DirectoryResult } from "tmp-promise" ;
67import { dir , tmpName } from "tmp-promise" ;
78import { tmpDir } from "../tmp-dir" ;
89import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders" ;
@@ -58,84 +59,119 @@ interface GeneratedQueryPack {
5859async 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(
146182async 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
435494function 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