#!/usr/bin/env python3 """ @file CodeQlVersionUpdate.py Update CodeQL CLI dependencies and the CodeQL query pack version. This maintainer-only helper updates the pinned CodeQL CLI release and the corresponding CodeQL C/C++ query pack version used by the EDK II BaseTools CodeQL plugin. The CodeQL plugin does not invoke this script automatically. Audience -------- EDK II BaseTools CodeQL plugin maintainers. Developers who build with the CodeQL plugin enabled, or who review CodeQL analysis results, do not need to run this script. Usage ----- Run this script manually from a local checkout: # Update to the latest published CodeQL CLI release. python3 BaseTools/Plugin/CodeQL/CodeQlVersionUpdate.py # Update to a specific CodeQL CLI version. python3 BaseTools/Plugin/CodeQL/CodeQlVersionUpdate.py --codeql-version 2.25.3 Then review and commit the resulting file changes. Updated files ------------- - BaseTools/Plugin/CodeQL/codeqlcli_ext_dep.yaml - BaseTools/Plugin/CodeQL/codeqlcli_linux_ext_dep.yaml - BaseTools/Plugin/CodeQL/codeqlcli_windows_ext_dep.yaml - BaseTools/Plugin/CodeQL/CodeQlQueries.qls Data sources ------------ - SHA256 digests are read from GitHub release metadata. - The codeql/cpp-queries version is read from qlpack.yml in the corresponding CodeQL CLI branch. Release metadata note --------------------- This script depends on GitHub release-asset digests being present in the release metadata. GitHub started exposing those digests on 2025-06-03: https://github.blog/changelog/2025-06-03-releases-now-expose-digests-for-release-assets/ Releases published before that change may not have `digest` values in the API. Copyright (c) 2026, Purdue University. All rights reserved.
SPDX-License-Identifier: BSD-2-Clause-Patent """ from __future__ import annotations import argparse import json import re import sys import urllib.error import urllib.request from pathlib import Path from typing import Dict SCRIPT_DIR = Path(__file__).resolve().parent EXT_DEP_FILES = { "codeql.zip": SCRIPT_DIR / "codeqlcli_ext_dep.yaml", "codeql-linux64.zip": SCRIPT_DIR / "codeqlcli_linux_ext_dep.yaml", "codeql-win64.zip": SCRIPT_DIR / "codeqlcli_windows_ext_dep.yaml", } QUERY_FILE = SCRIPT_DIR / "CodeQlQueries.qls" HTTP_TIMEOUT_SECONDS = 30 def _http_get(url: str) -> bytes: req = urllib.request.Request(url, headers={"User-Agent": "edk2-codeql-updater"}) with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_SECONDS) as resp: return resp.read() def _http_get_json(url: str) -> dict: return json.loads(_http_get(url).decode("utf-8")) def _http_get_text(url: str) -> str: return _http_get(url).decode("utf-8") def _extract_sha256_from_text(text: str) -> str: # Expected content is either "" or " ". token = text.strip().split()[0] if not re.fullmatch(r"[0-9a-fA-F]{64}", token): raise ValueError(f"Invalid sha256 text content: {text.strip()!r}") return token.lower() def fetch_latest_codeql_version() -> str: latest_release_url = ( "https://api.github.com/repos/github/codeql-cli-binaries/releases/latest" ) latest_release = _http_get_json(latest_release_url) tag_name = latest_release.get("tag_name") or "" if not isinstance(tag_name, str) or not tag_name: raise ValueError("Unable to determine latest CodeQL version from GitHub") return tag_name.lstrip("v") def fetch_release_sha256_map(codeql_version: str) -> Dict[str, str]: release_url = ( "https://api.github.com/repos/github/codeql-cli-binaries/releases/tags/" f"v{codeql_version}" ) release = _http_get_json(release_url) assets = {asset["name"]: asset for asset in release.get("assets", [])} sha_map: Dict[str, str] = {} for asset_name in EXT_DEP_FILES: asset = assets.get(asset_name) if asset is None: raise KeyError( f"Release v{codeql_version} does not include required asset: {asset_name}" ) digest = asset.get("digest") or "" if digest.startswith("sha256:"): sha_map[asset_name] = digest.split(":", 1)[1].lower() continue sha_asset = assets.get(f"{asset_name}.checksum.txt") if sha_asset: sha_text = _http_get_text(sha_asset["browser_download_url"]) sha_map[asset_name] = _extract_sha256_from_text(sha_text) continue raise KeyError( f"Unable to find SHA256 for {asset_name} in release v{codeql_version}" ) return sha_map def fetch_cpp_queries_version(codeql_version: str) -> str: qlpack_url = ( "https://raw.githubusercontent.com/github/codeql/" f"codeql-cli/v{codeql_version}/cpp/ql/src/qlpack.yml" ) qlpack_text = _http_get_text(qlpack_url) # qlpack.yml for cpp queries uses: # name: codeql/cpp-queries # version: if "name: codeql/cpp-queries" not in qlpack_text: raise ValueError( f"Unable to validate cpp queries pack in qlpack.yml for v{codeql_version}" ) match = re.search(r"(?m)^\s*version:\s*([0-9A-Za-z.\-_]+)\s*$", qlpack_text) if not match: raise ValueError( f"Unable to parse codeql/cpp-queries version for v{codeql_version}" ) return match.group(1) def read_text(path: Path) -> str: with path.open("r", encoding="utf-8", newline="") as f: return f.read() def detect_newline_style(text: str) -> str: if "\r\n" in text: return "\r\n" return "\n" def normalize_newlines(text: str, newline: str) -> str: normalized = text.replace("\r\n", "\n") if newline == "\r\n": return normalized.replace("\n", "\r\n") return normalized def write_text(path: Path, text: str) -> None: path.write_text(text, encoding="utf-8", newline="") def replace_or_fail(pattern: str, replacement: str, text: str, path: Path) -> str: new_text, count = re.subn(pattern, replacement, text, count=1, flags=re.MULTILINE) if count != 1: raise ValueError(f"Expected one match for pattern {pattern!r} in {path}") return new_text def update_ext_dep_file(path: Path, asset_name: str, codeql_version: str, sha256: str) -> bool: original = read_text(path) newline_style = detect_newline_style(original) text = original source_url = ( "https://github.com/github/codeql-cli-binaries/releases/download/" f"v{codeql_version}/{asset_name}" ) text = replace_or_fail( r'("source"\s*:\s*")[^"]+(")', rf'\g<1>{source_url}\g<2>', text, path, ) text = replace_or_fail( r'("version"\s*:\s*")[^"]+(")', rf'\g<1>{codeql_version}\g<2>', text, path, ) text = replace_or_fail( r'("sha256"\s*:\s*")[0-9a-fA-F]{64}(")', rf'\g<1>{sha256}\g<2>', text, path, ) if text != original: write_text(path, normalize_newlines(text, newline_style)) return True return False def update_queries_file(path: Path, cpp_queries_version: str) -> bool: original = read_text(path) newline_style = detect_newline_style(original) text = replace_or_fail( r"(from:\s*codeql/cpp-queries@)[0-9A-Za-z.\-_]+", rf"\g<1>{cpp_queries_version}", original, path, ) if text != original: write_text(path, normalize_newlines(text, newline_style)) return True return False def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Update CodeQL CLI versions and cpp query pack version." ) parser.add_argument( "--codeql-version", help=( "CodeQL CLI version (for example: 2.24.1). If omitted, the latest " "published release is used." ), ) parser.add_argument( "--dry-run", action="store_true", help="Calculate and print updates without writing files.", ) return parser.parse_args() def main() -> int: args = parse_args() try: codeql_version = ( args.codeql_version.lstrip("v") if args.codeql_version else fetch_latest_codeql_version() ) sha_map: Dict[str, str] = fetch_release_sha256_map(codeql_version) for asset_name, sha in sha_map.items(): if not re.fullmatch(r"[0-9a-f]{64}", sha): raise ValueError(f"Invalid SHA256 value for {asset_name}: {sha}") cpp_queries_version = fetch_cpp_queries_version(codeql_version) print(f"CodeQL version: v{codeql_version}") print(f"codeql/cpp-queries version: {cpp_queries_version}") for asset_name in sorted(sha_map): print(f"{asset_name} sha256: {sha_map[asset_name]}") if args.dry_run: print("Dry run: no files were modified.") return 0 changed_files = [] for asset_name, path in EXT_DEP_FILES.items(): if update_ext_dep_file(path, asset_name, codeql_version, sha_map[asset_name]): changed_files.append(path) if update_queries_file(QUERY_FILE, cpp_queries_version): changed_files.append(QUERY_FILE) if changed_files: print("Updated files:") for path in changed_files: print(f" - {path}") else: print("No file changes were necessary.") return 0 except urllib.error.HTTPError as err: print(f"HTTP error while fetching release data: {err}", file=sys.stderr) except (urllib.error.URLError, TimeoutError) as err: print(f"Network error while fetching release data: {err}", file=sys.stderr) except (KeyError, ValueError) as err: print(f"Error: {err}", file=sys.stderr) return 1 if __name__ == "__main__": raise SystemExit(main())