name: Cache query compilation
description: Caches CodeQL compilation caches - should be run both on PRs and pushes to main.

inputs:
  key:
    description: 'The cache key to use - should be unique to the workflow'
    required: true

outputs:
  cache-dir:
    description: "The directory where the cache was stored"
    value: ${{ steps.output-compilation-dir.outputs.compdir }}

runs:
  using: composite
  steps:
    # calculate the merge-base with main, in a way that works both on PRs and pushes to main.
    - name: Calculate merge-base
      shell: bash
      if: ${{ github.event_name == 'pull_request' }}
      env:
        BASE_BRANCH: ${{ github.base_ref }}
      run: |
        MERGE_BASE=$(git cat-file commit $GITHUB_SHA | grep '^parent ' | head -1 | cut -f 2 -d " ")
        echo "merge_base=$MERGE_BASE" >> $GITHUB_ENV
    - name: Restore cache (PR)
      if: ${{ github.event_name == 'pull_request' }}
      uses: actions/cache/restore@v3
      with:
        path: |
          **/.cache
          ~/.codeql/compile-cache
        key: codeql-compile-${{ inputs.key }}-pr-${{ github.sha }}
        restore-keys: |
          codeql-compile-${{ inputs.key }}-${{ github.base_ref }}-${{ env.merge_base }}
          codeql-compile-${{ inputs.key }}-${{ github.base_ref }}-
          codeql-compile-${{ inputs.key }}-main-
    - name: Fill cache (only branch push)
      if: ${{ github.event_name != 'pull_request' }}
      uses: actions/cache@v3
      with:
        path: |
          **/.cache
          ~/.codeql/compile-cache
        key: codeql-compile-${{ inputs.key }}-${{ github.ref_name }}-${{ github.sha }} # just fill on main
        restore-keys: | # restore the latest cache if the exact cache is unavailable, to speed up compilation.
          codeql-compile-${{ inputs.key }}-${{ github.ref_name }}-
          codeql-compile-${{ inputs.key }}-main-
    - name: Output-compilationdir
      id: output-compilation-dir
      shell: bash
      run: |
        echo "compdir=${COMBINED_CACHE_DIR}" >> $GITHUB_OUTPUT
      env:
        COMBINED_CACHE_DIR: ${{ runner.temp }}/compilation-dir
    - name: Fill compilation cache directory
      id: fill-compilation-dir
      uses: actions/github-script@v6
      env: 
        COMBINED_CACHE_DIR: ${{ runner.temp }}/compilation-dir
      with:
        script: |
          // # Move all the existing cache into another folder, so we only preserve the cache for the current queries.
          // mkdir -p ${COMBINED_CACHE_DIR}
          // rm -f **/.cache/{lock,size} # -f to avoid errors if the cache is empty.
          // # copy the contents of the .cache folders into the combined cache folder.
          // cp -r **/.cache/* ${COMBINED_CACHE_DIR}/ || : # ignore missing files
          // # clean up the .cache folders
          // rm -rf **/.cache/*

          const fs = require("fs");
          const path = require("path");
          const os = require("os");

          // the first argv is the cache folder to create.
          const COMBINED_CACHE_DIR = process.env.COMBINED_CACHE_DIR;

          function* walkCaches(dir) {
            const files = fs.readdirSync(dir, { withFileTypes: true });
            for (const file of files) {
              if (file.isDirectory()) {
                const filePath = path.join(dir, file.name);
                yield* walkCaches(filePath);
                if (file.name === ".cache") {
                  yield filePath;
                }
              }
            }
          }

          async function copyDir(src, dest) {
            for await (const file of await fs.promises.readdir(src, { withFileTypes: true })) {
              const srcPath = path.join(src, file.name);
              const destPath = path.join(dest, file.name);
              if (file.isDirectory()) {
                if (!fs.existsSync(destPath)) {
                  fs.mkdirSync(destPath);
                }
                await copyDir(srcPath, destPath);
              } else {
                await fs.promises.copyFile(srcPath, destPath);
              }
            }
          }

          async function main() {
            const cacheDirs = [...walkCaches(".")];

            for (const dir of cacheDirs) {
              console.log(`Found .cache dir at ${dir}`);
            }

            const globalCacheDir = path.join(os.homedir(), ".codeql", "compile-cache");
            if (fs.existsSync(globalCacheDir)) {
              console.log("Found global home dir: " + globalCacheDir);
              cacheDirs.push(globalCacheDir);
            }

            if (cacheDirs.length === 0) {
              console.log("No cache dirs found");
              return;
            }

            // mkdir -p ${COMBINED_CACHE_DIR}
            fs.mkdirSync(COMBINED_CACHE_DIR, { recursive: true });

            // rm -f **/.cache/{lock,size} # -f to avoid errors if the cache is empty.
            await Promise.all(
              cacheDirs.map((cacheDir) =>
                (async function () {
                  await fs.promises.rm(path.join(cacheDir, "lock"), { force: true });
                  await fs.promises.rm(path.join(cacheDir, "size"), { force: true });
                })()
              )
            );

            // # copy the contents of the .cache folders into the combined cache folder.
            // cp -r **/.cache/* ${COMBINED_CACHE_DIR}/ || : # ignore missing files
            await Promise.all(
              cacheDirs.map((cacheDir) => copyDir(cacheDir, COMBINED_CACHE_DIR))
            );

            // # clean up the .cache folders
            // rm -rf **/.cache/*
            await Promise.all(
              cacheDirs.map((cacheDir) => fs.promises.rm(cacheDir, { recursive: true }))
            );
          }
          main();
