edk2/BaseTools/Plugin/CodeQL/CodeQlVersionUpdate.py
Mingjie Shen 927c7b1d81 BaseTools/Plugin/CodeQL: Add CodeQL version update script
Add a maintenance script that updates the CodeQL CLI dependency
YAML files and CodeQlQueries.qls together.

The script refreshes the CLI version, release digests, and cpp query
pack pin from the GitHub release metadata and the corresponding
qlpack.yml in the CodeQL CLI release branch.

Add comments to the CodeQL CLI dependency YAML files that direct
maintainers to use the script for future version updates.

Signed-off-by: Mingjie Shen <shen497@purdue.edu>
2026-07-11 01:26:20 +00:00

315 lines
10 KiB
Python

#!/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.<BR>
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 "<sha256>" or "<sha256> <filename>".
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: <pack 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())