Source code

Revision control

Copy as Markdown

Other Tools

#!/usr/bin/env python3
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import datetime
import os
import re
import subprocess
import sys
from pathlib import Path
VENDOR_DIR = Path(__file__).parent.resolve()
# Arithmetic codec files present in upstream JPEG_SOURCES when built with
# arithmetic support, but disabled in Firefox via jconfig.h.
UPSTREAM_SOURCES_NOT_BUILT = {
"jaricom.c",
"jcarith.c",
"jdarith.c",
}
# These .c files are #included by other .c files and are not compiled directly.
# They do not appear in CMakeLists.txt source lists.
C_FILES_INCLUDED_BY_OTHERS = {
"jccolext.c", # included by jccolor.c
"jdcol565.c", # included by jdcolor.c
"jdcolext.c", # included by jdcolor.c
"jdmrg565.c", # included by jdmerge.c
"jdmrgext.c", # included by jdmerge.c
"jstdhuff.c", # included by jcparam.c
}
JCONFIG_TEMPLATES = [
"src/jconfig.h.in",
"src/jconfigint.h.in",
"src/jversion.h.in",
"simd/arm/neon-compat.h.in",
]
# Together with the header extension patterns (".h", ".h.in", ".inc", ".inc.h") checked
# in check_vcs_changes(), this set forms the complete allow list for new vendored files
# that are not listed in any moz.build SOURCES. Filenames are matched by basename so
# moves within the tree don't require updating this set. "CMakeLists.txt" matches both
# the root and simd/CMakeLists.txt via basename.
# When upstream adds a new non-compiled file that stays in the tree, add it here.
KNOWN_NON_MOZ_BUILD_FILES = (
{
# Build system files (needed by update.py to check source lists)
"CMakeLists.txt",
# Metadata and licenses
"LICENSE.md",
"README.md",
"README.ijg",
"ChangeLog.md",
# Mozilla-specific files (kept in-tree via moz.yaml keep: or patches:)
"MOZCHANGES",
"update.py",
"mozilla.diff",
}
| UPSTREAM_SOURCES_NOT_BUILT
| C_FILES_INCLUDED_BY_OTHERS
)
def parse_cmake_var(text, var_name):
"""Return the list of literal tokens from the first CMake set() call for var_name.
Tokens that are cmake variable references (${VAR}) are skipped.
"""
m = re.search(
rf"set\(\s*{re.escape(var_name)}\s+(.*?)\)",
text,
re.DOTALL,
)
if not m:
return []
return [tok for tok in m.group(1).split() if not re.fullmatch(r"\$\{(\w+)\}", tok)]
def _collect_list_vars(text):
"""Return a dict mapping variable name to the union of all its list-literal definitions.
Captures `name = ['...', ...]` patterns. Multiple definitions of the same name are
unioned so that arch-specific SIMD lists defined in separate if/elif branches are
all included when that variable is later referenced by a SOURCES += assignment.
"""
list_vars = {}
for m in re.finditer(r"(\w+)\s*=\s*\[(.*?)\]", text, re.DOTALL):
items = re.findall(r"'([^']+)'", m.group(2))
if items:
list_vars.setdefault(m.group(1), []).extend(items)
return list_vars
def _iter_sources_items(text, list_vars):
"""Yield every item from SOURCES += [...] and SOURCES += varname in moz.build text."""
for m in re.finditer(r"SOURCES\s*\+=\s*(?:\[(.*?)\]|(\w+))", text, re.DOTALL):
if m.group(1) is not None:
yield from re.findall(r"'([^']+)'", m.group(1))
else:
yield from list_vars.get(m.group(2), [])
def parse_moz_build_sources(path, exclude_prefix=None):
"""Return the set of bare filenames from SOURCES blocks in a moz.build.
Handles both literal SOURCES += [...] blocks and SOURCES += varname assignments.
exclude_prefix: if set, skip entries that start with this prefix (e.g. 'simd/' to
exclude SIMD files from the JPEG_SOURCES comparison).
"""
text = path.read_text()
list_vars = _collect_list_vars(text)
sources = set()
for item in _iter_sources_items(text, list_vars):
if exclude_prefix and item.startswith(exclude_prefix):
continue
sources.add(Path(item).name)
return sources
def collect_all_moz_build_sources():
"""Return a set of VENDOR_DIR-relative paths from all moz.build SOURCES blocks.
Uses full relative paths rather than bare filenames to avoid false matches when
the same filename appears in multiple simd subdirectories (e.g. jsimd.c exists
for each architecture). Paths from subdirectory moz.builds (which reference
parent files via '../') are normalized to be relative to VENDOR_DIR.
"""
sources = set()
for mb_path in VENDOR_DIR.rglob("moz.build"):
mb_dir = mb_path.parent
text = mb_path.read_text()
list_vars = _collect_list_vars(text)
for item in _iter_sources_items(text, list_vars):
abs_path = Path(os.path.normpath(mb_dir / item))
try:
sources.add(abs_path.relative_to(VENDOR_DIR).as_posix())
except ValueError:
pass
return sources
def check_source_lists():
cmake_path = VENDOR_DIR / "CMakeLists.txt"
if not cmake_path.exists():
print("WARNING: CMakeLists.txt not found; skipping source list check.")
return True
text = cmake_path.read_text()
# Use only the first set() call for JPEG_SOURCES; subsequent ones add arithmetic
# files conditionally. Filter those via UPSTREAM_SOURCES_NOT_BUILT as well.
base_jpeg = parse_cmake_var(text, "JPEG_SOURCES")
jpeg_sources = [
f for f in base_jpeg if Path(f).name not in UPSTREAM_SOURCES_NOT_BUILT
]
# Split into direct src/*.c files and wrapper files.
# Wrapper files live under src/wrapper/ and have -8/-12/-16 suffixes.
cmake_direct = set() # basenames of src/*.c compiled directly
cmake_wrapper = set() # basenames of src/wrapper/*.c files
for f in jpeg_sources:
name = Path(f).name
if "wrapper" in f:
cmake_wrapper.add(name)
else:
cmake_direct.add(name)
moz_main = parse_moz_build_sources(VENDOR_DIR / "moz.build", exclude_prefix="simd/")
moz_wrapper = parse_moz_build_sources(VENDOR_DIR / "src" / "wrapper" / "moz.build")
ok = True
for cmake_set, moz_set, label in [
(cmake_direct, moz_main, "JPEG direct sources (moz.build)"),
(cmake_wrapper, moz_wrapper, "JPEG wrapper sources (src/wrapper/moz.build)"),
]:
added = cmake_set - moz_set
removed = moz_set - cmake_set
if added or removed:
ok = False
print(f"\nSource list mismatch: {label}:")
for f in sorted(added):
print(f" + {f} (in CMakeLists.txt but not moz.build)")
for f in sorted(removed):
print(f" - {f} (in moz.build but not CMakeLists.txt)")
return ok
def find_vcs():
"""Walk up from VENDOR_DIR to find a .hg or .git root. Returns (vcs, root)."""
d = VENDOR_DIR
while True:
if (d / ".hg").is_dir():
return "hg", d
if (d / ".git").is_dir():
return "git", d
parent = d.parent
if parent == d:
return None, None
d = parent
def _has_vcs_changes(vcs, root, path):
"""Return True if path changed in the tip/HEAD commit or has uncommitted changes."""
rel = str(path.relative_to(root))
if vcs == "hg":
tip = subprocess.run(
["hg", "status", "--change", ".", rel],
capture_output=True,
text=True,
cwd=str(root),
)
wdir = subprocess.run(
["hg", "status", rel],
capture_output=True,
text=True,
cwd=str(root),
)
return bool(tip.stdout.strip() or wdir.stdout.strip())
else:
tip = subprocess.run(
[
"git",
"diff-tree",
"--no-commit-id",
"-r",
"--name-only",
"HEAD",
"--",
rel,
],
capture_output=True,
text=True,
cwd=str(root),
)
wdir = subprocess.run(
["git", "status", "--porcelain", rel],
capture_output=True,
text=True,
cwd=str(root),
)
return bool(tip.stdout.strip() or wdir.stdout.strip())
def update_version_strings():
"""Auto-update version fields in jconfig.h and jconfigint.h from CMakeLists.txt.
Returns True on success, False if anything fails or expected substitutions are missing.
All substitutions are validated before any file is written.
"""
cmake_path = VENDOR_DIR / "CMakeLists.txt"
if not cmake_path.exists():
print("ERROR: CMakeLists.txt not found; cannot update version strings.")
return False
cmake_text = cmake_path.read_text()
m = re.search(r"^set\(VERSION\s+(\S+)\)", cmake_text, re.MULTILINE)
if not m:
print("ERROR: Could not find VERSION in CMakeLists.txt.")
return False
version = m.group(1)
parts = version.split(".")
if len(parts) < 3 or not all(p.isdigit() for p in parts):
print(f"ERROR: Unexpected VERSION format: {version!r}")
return False
major, minor, revision = int(parts[0]), int(parts[1]), int(parts[2])
version_number = f"{major}{minor:03d}{revision:03d}"
# Use the mtime of CMakeLists.txt as the build date. mach vendor sets this to
# the time of the vendor operation when it copies the file into the tree.
cmake_mtime = datetime.datetime.fromtimestamp(cmake_path.stat().st_mtime)
build = cmake_mtime.strftime("%Y%m%d")
ok = True
# Prepare jconfig.h substitutions
jconfig_path = VENDOR_DIR / "jconfig.h"
if not jconfig_path.exists():
print("ERROR: jconfig.h not found.")
return False
jconfig = jconfig_path.read_text()
jconfig, n1 = re.subn(
r"(#define LIBJPEG_TURBO_VERSION\s+)\S+",
rf"\g<1>{version}",
jconfig,
)
jconfig, n2 = re.subn(
r"(#define LIBJPEG_TURBO_VERSION_NUMBER\s+)\S+",
rf"\g<1>{version_number}",
jconfig,
)
if n1 != 1 or n2 != 1:
print(
f"ERROR: Unexpected substitution counts in jconfig.h "
f"(LIBJPEG_TURBO_VERSION: {n1}, LIBJPEG_TURBO_VERSION_NUMBER: {n2}; expected 1 each)."
)
ok = False
# Prepare jconfigint.h substitutions
jconfigint_path = VENDOR_DIR / "jconfigint.h"
if not jconfigint_path.exists():
print("ERROR: jconfigint.h not found.")
return False
jconfigint = jconfigint_path.read_text()
jconfigint, n3 = re.subn(
r'(#define VERSION\s+)"[^"]*"',
rf'\g<1>"{version}"',
jconfigint,
)
jconfigint, n4 = re.subn(
r'(#define BUILD\s+)"[^"]*"',
rf'\g<1>"{build}"',
jconfigint,
)
if n3 != 1 or n4 != 1:
print(
f"ERROR: Unexpected substitution counts in jconfigint.h "
f"(VERSION: {n3}, BUILD: {n4}; expected 1 each)."
)
ok = False
# Sync moz.yaml revision to the current version if mach vendor left it stale.
moz_yaml_path = VENDOR_DIR / "moz.yaml"
if not moz_yaml_path.exists():
print("ERROR: moz.yaml not found.")
return False
moz_yaml = moz_yaml_path.read_text()
yaml_m = re.search(r'revision:\s*"([^"]*)"', moz_yaml)
if not yaml_m:
print("ERROR: Could not find revision field in moz.yaml.")
ok = False
elif yaml_m.group(1) != version:
moz_yaml, n5 = re.subn(
r'(revision:\s*)"[^"]*"',
rf'\g<1>"{version}"',
moz_yaml,
)
if n5 != 1:
print(
f"ERROR: Unexpected substitution count in moz.yaml "
f"(revision: {n5}; expected 1)."
)
ok = False
if ok:
jconfig_path.write_text(jconfig)
jconfigint_path.write_text(jconfigint)
if yaml_m and yaml_m.group(1) != version:
moz_yaml_path.write_text(moz_yaml)
return ok
def check_jconfig_changes():
"""Alert if jconfig template files changed, indicating jconfig.h/jconfigint.h may need review."""
vcs, root = find_vcs()
if vcs is None:
return True
changed = [
fname
for fname in JCONFIG_TEMPLATES
if _has_vcs_changes(vcs, root, VENDOR_DIR / fname)
]
if not changed:
return True
print("\nConfig template files changed:")
for f in changed:
print(f" {f}")
print()
print(" Version strings in jconfig.h and jconfigint.h have been auto-updated.")
print(" Review any remaining structural changes in the templates and update")
print(" the corresponding in-tree files (jconfig.h, jconfigint.h,")
print(" jversion.h, simd/arm/neon-compat.h) accordingly if needed.")
return False
def _get_vcs_changes(vcs, root, rel_vendor, *, committed):
"""Return (new_files, removed_files) for rel_vendor.
If committed is True, inspects the tip/HEAD commit; otherwise inspects the
working directory. Paths are relative to the repo root.
"""
if vcs == "hg":
# hg status --change . shows files changed in the working directory's
# parent revision (the most recently committed revision).
flags = (
["--change", ".", "--added", "--removed"]
if committed
else ["--added", "--removed", "--deleted", "--unknown"]
)
result = subprocess.run(
["hg", "status"] + flags + [rel_vendor],
capture_output=True,
text=True,
cwd=str(root),
)
new_files, removed_files = [], []
for line in result.stdout.splitlines():
status, path = line[0], line[2:]
if status in ("A", "?"):
new_files.append(path)
elif status in ("R", "!"):
removed_files.append(path)
else:
if committed:
result = subprocess.run(
[
"git",
"diff-tree",
"--no-commit-id",
"-r",
"--name-status",
"--diff-filter=AD",
"HEAD",
"--",
rel_vendor,
],
capture_output=True,
text=True,
cwd=str(root),
)
new_files, removed_files = [], []
for line in result.stdout.splitlines():
status, path = line[0], line.split("\t", 1)[1]
if status == "A":
new_files.append(path)
elif status == "D":
removed_files.append(path)
else:
result = subprocess.run(
["git", "status", "--porcelain", rel_vendor],
capture_output=True,
text=True,
cwd=str(root),
)
new_files, removed_files = [], []
for line in result.stdout.splitlines():
xy, path = line[:2], line[3:]
if "?" in xy or xy[0] == "A":
new_files.append(path)
elif "D" in xy:
removed_files.append(path)
return new_files, removed_files
def _get_tip_changes(vcs, root, rel_vendor):
return _get_vcs_changes(vcs, root, rel_vendor, committed=True)
def _get_uncommitted_changes(vcs, root, rel_vendor):
return _get_vcs_changes(vcs, root, rel_vendor, committed=False)
def check_vcs_changes():
"""Alert on file additions or removals in the vendored tree.
Checks the tip/HEAD commit (the committed fetch step) for new or removed files
and validates that each change is properly handled:
- New files must be in a moz.build SOURCES block, be a header/include file
(extension .h, .h.in, .inc, .inc.h), or be listed in KNOWN_NON_MOZ_BUILD_FILES.
- Removed files must no longer appear in any moz.build SOURCES block.
Also checks the working directory for any uncommitted additions or removals as a
belt-and-suspenders fallback (warns but does not fail on these).
"""
vcs, root = find_vcs()
if vcs is None:
print("WARNING: No VCS root found; skipping file change check.")
return True
rel_vendor = str(VENDOR_DIR.relative_to(root))
rel_vendor_path = Path(rel_vendor)
all_moz_build_sources = collect_all_moz_build_sources()
tip_new, tip_removed = _get_tip_changes(vcs, root, rel_vendor)
uncommitted_new, uncommitted_removed = _get_uncommitted_changes(
vcs, root, rel_vendor
)
ok = True
if tip_new or tip_removed:
unhandled_new = []
for f in tip_new:
rel_path = Path(f).relative_to(rel_vendor_path).as_posix()
name = Path(rel_path).name
if name.endswith((".h", ".h.in", ".inc", ".inc.h")):
continue
if rel_path in all_moz_build_sources:
continue
if name in KNOWN_NON_MOZ_BUILD_FILES:
continue
unhandled_new.append(f)
still_in_moz_build = [
f
for f in tip_removed
if Path(f).relative_to(rel_vendor_path).as_posix() in all_moz_build_sources
]
if unhandled_new or still_in_moz_build:
ok = False
print(
"\nUnhandled file changes in the vendored tree (from the fetch commit):"
)
for f in sorted(unhandled_new):
print(f" new (unhandled): {f}")
for f in sorted(still_in_moz_build):
print(f" removed (still in moz.build): {f}")
print()
print(" For unhandled new files:")
print(" - Add to SOURCES in moz.build if Firefox needs to build it.")
print(" - Add to 'exclude:' in moz.yaml if Firefox doesn't need it.")
print(" - Add to KNOWN_NON_MOZ_BUILD_FILES in update.py if it stays in")
print(
" the tree but isn't compiled (e.g. a new metadata or config file)."
)
print(" For removed files still in moz.build: remove them from SOURCES.")
else:
print(
"\nFile changes in the vendored tree (from the fetch commit, all handled):"
)
for f in sorted(tip_new):
print(f" new: {f}")
for f in sorted(tip_removed):
print(f" removed: {f}")
print()
print(" To find renames between upstream tags:")
print(
" git -C /path/to/libjpeg-turbo diff -M50 --diff-filter=R --name-status OLD_TAG..NEW_TAG"
)
if vcs == "hg":
print()
print(" If files were renamed upstream, record the move with:")
print(" hg mv --after OLD_NAME NEW_NAME")
print(
" (The file is already at NEW_NAME on disk; --after just records it.)"
)
else:
print()
print(" If files were renamed upstream, record the rename with:")
print(" git add NEW_NAME && git rm OLD_NAME")
if uncommitted_new or uncommitted_removed:
print("\nUncommitted file changes detected in the vendored tree:")
for f in sorted(uncommitted_new):
print(f" new: {f}")
for f in sorted(uncommitted_removed):
print(f" removed: {f}")
print()
print(" These changes are uncommitted. If they are from the fetch step,")
print(" commit them (and update moz.build/moz.yaml as needed) before")
print(" re-running --patch-mode only.")
return ok
def main():
# Auto-update version strings in jconfig.h and jconfigint.h from the vendored
# CMakeLists.txt. This handles the routine per-release version bump.
if not update_version_strings():
print(
"\nFailed to auto-update version strings. Fix the errors above and re-run:\n"
" ./mach vendor media/libjpeg/moz.yaml --patch-mode only"
)
sys.exit(1)
ok = True
# Verify that the upstream CMakeLists.txt source lists still match our moz.build files.
# If upstream added or removed .c files, moz.build must be updated manually.
if not check_source_lists():
ok = False
# Alert if the jconfig template files changed beyond the version strings, which may
# require manual updates to jconfig.h / jconfigint.h.
if not check_jconfig_changes():
ok = False
# Alert on file additions or removals from the fetch commit. Validates that
# each new file is accounted for (in moz.build, a header, or KNOWN_NON_MOZ_BUILD_FILES)
# and that removed files are no longer referenced in moz.build.
if not check_vcs_changes():
ok = False
if not ok:
print(
"\nSee guidance above. After making changes, commit them and re-run:\n"
" ./mach vendor media/libjpeg/moz.yaml --patch-mode only"
)
sys.exit(1)
if __name__ == "__main__":
main()