mirror of
https://github.com/tianocore/edk2
synced 2026-08-27 00:23:19 -04:00
316 lines
10 KiB
Python
316 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())
|