blob: e492435b53f51b012003fc16f3aeb86323ba6884 [file]
#!/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())