Add experimental option --patches

This generates Nix expressions which include local patches (additional
commits in local git repositories).
This commit is contained in:
Michal Sojka 2024-12-22 23:00:49 +01:00
parent 9193811bfc
commit dc3de1f289
3 changed files with 92 additions and 3 deletions

View file

@ -25,7 +25,7 @@
from operator import attrgetter
from textwrap import dedent, indent
from time import gmtime, strftime
from typing import Iterable, Set, Optional
from typing import Iterable, Set, Optional, List
from superflore.utils import get_license
@ -95,11 +95,13 @@ class NixExpression:
src_param: Optional[str] = None,
source_root: Optional[str] = None,
do_check: Optional[bool] = None,
patches: Optional[List[str]] = None,
) -> None:
self.name = name
self.version = version
self.src_param = src_param
self.src_expr = src_expr
self.patches = patches
self.source_root = source_root
self.do_check = do_check
@ -168,6 +170,8 @@ class NixExpression:
version=self.version,
src=src,
build_type=self.build_type)
if self.patches:
ret += f""" patches = [\n {"\n ".join(self.patches)}\n ];\n"""
if self.source_root:
ret += f' sourceRoot = "{self.source_root}";\n'

View file

@ -4,6 +4,7 @@
# Copyright 2019-2024 Ben Wolsieffer <benwolsieffer@gmail.com>
# Copyright 2024 Michal Sojka <michal.sojka@cvut.cz>
from os.path import dirname
import argcomplete, argparse
import difflib
import io
@ -246,6 +247,13 @@ def ros2nix(args):
"The fetch function and its parameters are determined from the local git work tree. "
"sourceRoot attribute is set if needed and not overridden by --source-root.",
)
parser.add_argument(
"--patches",
action=argparse.BooleanOptionalAction,
help="""Add git commits not present in git remote named "origin" as patches in the """
"""generated Nix expression. Only allowed with --fetch. This option is experimental """
"""and may be changed in the future.""",
)
parser.add_argument(
"--distro",
default="rolling",
@ -333,8 +341,13 @@ def ros2nix(args):
if args.output_dir is None and (args.output_as_nix_pkg_name or args.output_as_ros_pkg_name):
args.output_dir = "."
if args.patches and not args.fetch:
err("--patches cannot be used without --fetch")
return 1
expressions: dict[str, str] = {}
git_cache = {}
patch_filenames = set()
for source in args.source:
try:
@ -370,6 +383,7 @@ def ros2nix(args):
buildtool_deps | buildtool_export_deps)
kwargs = {}
patches = []
if args.src_param:
kwargs["src_param"] = args.src_param
@ -388,12 +402,25 @@ def ros2nix(args):
"git rev-parse --show-toplevel".split(), cwd=srcdir
).decode().strip()
head = subprocess.check_output(
"git rev-parse HEAD".split(), cwd=srcdir
).decode().strip()
if toplevel in git_cache:
info = git_cache[toplevel]
upstream_rev = info["rev"]
else:
# Latest commit present in the upstream repo. If
# the local repository doesn't have additional
# commits, it is the same as HEAD. Should work
# even with detached HEAD.
upstream_rev = subprocess.check_output(
"git merge-base HEAD $(git for-each-ref refs/remotes/origin --format='%(objectname)')",
shell=True, cwd=srcdir
).decode().strip()
info = json.loads(
subprocess.check_output(
["nix-prefetch-git", "--quiet", toplevel],
["nix-prefetch-git", "--quiet", toplevel, upstream_rev],
).decode()
)
git_cache[toplevel] = info
@ -421,6 +448,14 @@ def ros2nix(args):
# kwargs["src_expr"] = f'''let fullSrc = {kwargs["src_expr"]}; in "${{fullSrc}}/{prefix}"'''
kwargs["source_root"] = f"${{src.name}}/{prefix}"
if args.patches:
patches = subprocess.check_output(
f"if ! git diff --quiet {upstream_rev}..HEAD -- .; then git format-patch --relative {upstream_rev}..HEAD; fi",
shell=True, cwd=srcdir,
).decode().strip().splitlines()
elif head != upstream_rev:
warn(f"{toplevel} contains commits not available upstream. Consider using --patches")
else:
if args.output_dir is None:
kwargs["src_expr"] = "./."
@ -444,6 +479,7 @@ def ros2nix(args):
propagated_build_inputs=propagated_build_inputs | set(args.extra_propagated_build_inputs),
check_inputs=check_inputs | set(args.extra_check_inputs),
native_build_inputs=native_build_inputs | set(args.extra_native_build_inputs),
patches=[f"./{p}" for p in patches],
**kwargs,
)
@ -478,6 +514,20 @@ def ros2nix(args):
output_file_name = get_output_file_name(source, pkg, args)
with file_writer(output_file_name, args.compare) as recipe_file:
recipe_file.write(derivation_text)
for patch in patches:
patch_filename = os.path.join(dirname(output_file_name), patch)
if not patch_filename in patch_filenames:
patch_filenames.add(patch_filename)
else:
# TODO Allow better handling of patch name collisions (e.g. by
# having them in per-package directories, perhaps via
# --output_subdir_as_nix_pkg_name)
msg = f"Patch {patch_filename} already exists"
err(msg)
raise Exception(msg)
with file_writer(patch_filename, args.compare) as patch_dest, \
open(os.path.join(os.path.dirname(source), patch), "r") as patch_src:
patch_dest.write(patch_src.read())
if not args.compare:
ok(f"Successfully generated derivation for package '{pkg.name}' as '{output_file_name}'.")

View file

@ -59,6 +59,41 @@ load common.bash
@test "--fetch from github over https" {
git clone https://github.com/wentasah/ros2nix
ros2nix --output-as-nix-pkg-name --fetch $(find "ros2nix/test/ws/src" -name package.xml)
cat ros-node.nix
nix-build -A rosPackages.jazzy.ros-node
run ./result/lib/ros_node/node
assert_success
assert_line --partial "hello world"
}
@test "--patches without --fetch" {
run ! ros2nix --patches $(find ws/src -name package.xml)
}
@test "--fetch --patches" {
git clone https://github.com/wentasah/ros2nix
pushd ros2nix
sed -i -e 's/hello world/hello patch/' test/ws/src/ros_node/src/node.cpp
git commit -m 'test patch' -- test/ws/src/ros_node/src/node.cpp
popd
ros2nix --output-as-nix-pkg-name --fetch --patches $(find "ros2nix/test/ws/src" -name package.xml)
# remove original (abd patched) sources
rm -rf ros2nix
nix-build -A rosPackages.jazzy.ros-node
# validate that we get patched binary
run ./result/lib/ros_node/node
assert_success
assert_line --partial "hello patch"
}
@test "--fetch --patches with colliding changes" {
git clone https://github.com/wentasah/ros2nix
pushd ros2nix
sed -i -e 's/hello world/hello patch/' test/ws/src/ros_node/src/node.cpp
git commit -m 'test patch' -- test/ws/src/ros_node/src/node.cpp
sed -i -e '1a// comment' test/ws/src/library/src/library.cpp
git commit -m 'test patch' -- test/ws/src/library/src/library.cpp
popd
run ros2nix --output-as-nix-pkg-name --fetch --patches $(find "ros2nix/test/ws/src" -name package.xml)
assert_failure
assert_line --partial "Patch ./0001-test-patch.patch already exists"
}