blob: 437cbb955f4215655001f3458c9c8319eb5e23e9 [file]
#!/usr/bin/env python3
# Copyright (C) 2026 Free Software Foundation, Inc.
#
# This file is part of GCC.
#
# GCC is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3, or (at your option) any later
# version.
#
# GCC is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License
# along with GCC; see the file COPYING3. If not see
# <http://www.gnu.org/licenses/>.
"""Apply the adjacent patch series to the vendored Rust libraries."""
import argparse
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
SCRIPT_DIR = Path(__file__).resolve().parent
RUSTC_LIB_DIR = SCRIPT_DIR.parent / "rustc-lib"
PATCHES_DIR = SCRIPT_DIR / "patches"
PATCH_FILENAME_SUFFIX = ".for-rfl-patch"
def git(directory, *arguments, data=None, check=False):
result = subprocess.run(
["git", "-C", str(directory), *arguments],
input=data,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if check and result.returncode:
raise RuntimeError(result.stderr.decode(errors="replace").strip())
return result
def apply_patch(directory, data, reverse=False, check=False):
# strip a/libgrust/rustc-lib/ from repository-relative patch paths
arguments = ["apply", "-p3"]
prefix = git(directory, "rev-parse", "--show-prefix")
if prefix.returncode == 0 and prefix.stdout.strip():
arguments.append(f"--directory={prefix.stdout.decode().strip()}")
if reverse:
arguments.append("--reverse")
if check:
arguments.append("--check")
return git(directory, *arguments, data=data)
def patch_in_place(directory=RUSTC_LIB_DIR):
for patch in sorted(PATCHES_DIR.glob(f"*{PATCH_FILENAME_SUFFIX}")):
data = patch.read_bytes()
if apply_patch(directory, data, check=True).returncode:
# skip patches that can't be applied because they might have already been applied
continue
result = apply_patch(directory, data)
if result.returncode:
raise RuntimeError(
f"failed to apply {patch.name}: "
f"{result.stderr.decode(errors='replace').strip()}"
)
def patch_library(output=None):
if output is not None:
shutil.copytree(RUSTC_LIB_DIR, output)
patch_in_place(output)
else:
patch_in_place()
def generate_patch(name):
if not name.endswith(PATCH_FILENAME_SUFFIX):
raise RuntimeError(f"patch filename should end with {PATCH_FILENAME_SUFFIX}")
output_patch = PATCHES_DIR / name
if output_patch.exists():
raise RuntimeError(f"patch filename already exists: {output_patch}")
prefix = (
git(
RUSTC_LIB_DIR,
"rev-parse",
"--show-prefix",
check=True,
)
.stdout.decode()
.strip()
)
tree = git(
RUSTC_LIB_DIR,
"rev-parse",
f"HEAD:{prefix.rstrip('/')}",
check=True,
).stdout
objects = git(
RUSTC_LIB_DIR,
"rev-parse",
"--git-path",
"objects",
check=True,
).stdout
objects = (RUSTC_LIB_DIR / objects.decode().strip()).resolve()
patches = sorted(PATCHES_DIR.glob(f"*{PATCH_FILENAME_SUFFIX}"))
# using a temporary repository so generation cannot change the user's index
with tempfile.TemporaryDirectory(prefix="rustc-lib-patch-") as temporary:
directory = Path(temporary)
git(directory, "init", "--quiet", check=True)
(directory / ".git/objects/info/alternates").write_text(str(objects) + "\n")
git(directory, "read-tree", tree.decode().strip(), check=True)
for patch in patches:
result = git(directory, "apply", "--cached", "-p3", data=patch.read_bytes())
if result.returncode:
raise RuntimeError(
f"failed to apply {patch.name} to HEAD: "
f"{result.stderr.decode(errors='replace').strip()}"
)
baseline = git(directory, "write-tree", check=True).stdout.decode().strip()
git(
directory,
f"--work-tree={RUSTC_LIB_DIR}",
"add",
"-A",
"--",
".",
check=True,
)
data = git(
directory,
"diff",
"--cached",
"--binary",
"--no-ext-diff",
"--no-textconv",
"--no-renames",
"--no-color",
"--src-prefix=a/libgrust/rustc-lib/",
"--dst-prefix=b/libgrust/rustc-lib/",
baseline,
"--",
".",
check=True,
).stdout
if not data:
print("No changes beyond the existing patch series.")
return
with output_patch.open("xb") as output:
output.write(data)
print(f"Generated {output_patch}")
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
mode = parser.add_mutually_exclusive_group()
mode.add_argument(
"-o",
"--output",
type=Path,
help="write a patched copy to a new directory instead of patching in place",
)
mode.add_argument(
"--generate",
metavar=f"NAME{PATCH_FILENAME_SUFFIX}",
help=f"write local changes beyond the existing patches to patches/NAME{PATCH_FILENAME_SUFFIX}",
)
args = parser.parse_args(argv)
try:
if args.generate is not None:
generate_patch(args.generate)
else:
patch_library(args.output)
except (RuntimeError, OSError) as error:
print(f"{parser.prog}: error: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())