diff mbox series

[v2,2/5] libgit-sys: add symlink to git repo root and build out of tree

Message ID 6befc95a2d0893aa269142a18d60ad07e79c6e88.1742594960.git.steadmon@google.com (mailing list archive)
State New
Headers show
Series Fix `cargo package` for libgit-sys | expand

Commit Message

Josh Steadmon March 21, 2025, 10:14 p.m. UTC
Unlike `cargo build`, `cargo package` does not get access to the entire Git repo
containing a Rust crate. Instead, it prepares a directory starting from the
crate root (potentially excluding files, such as those not under version
control, or explicity excluded in the Cargo.toml file).

This means that the current method of building the libgit-sys crate does not
work with `cargo package`, as it tries to execute the Makefile from "../.."
relative to the crate root.

Fix this by adding a `git-src` symlink in the crate that points to the Git
repository root. `cargo package` will flatten this to a copy of the Git repo,
excluding non-version-controlled files, any explicitly-excluded files, and trees
that contain a Cargo.toml file (this prevents infinite recursion on the
symlink).

We can then execute the Makefile under the flattened git-src directory from our
build.rs script. However, this exposes a second problem; Cargo will check that
the build script does not add, delete, or modify any files in the source tree.
Without further changes, Cargo complains about the object files and other
generated files created during the build.

To avoid this problem, add a CARGO_OUT_DIR variable to the Makefile. When this
is set, object files and other generated files will be created there, rather
than in the main source tree. This change has only been applied to files created
as part of the libgitpub.a build, to avoid unnecessary churn in the Makefile.

Signed-off-by: Josh Steadmon <steadmon@google.com>
---
 Makefile                    | 135 +++++++++++++++++++++---------------
 contrib/libgit-sys/build.rs |   7 +-
 contrib/libgit-sys/git-src  |   1 +
 shared.mak                  |   5 ++
 4 files changed, 91 insertions(+), 57 deletions(-)
 create mode 120000 contrib/libgit-sys/git-src

Comments

Eric Sunshine March 23, 2025, 1:46 a.m. UTC | #1
On Fri, Mar 21, 2025 at 6:14 PM Josh Steadmon <steadmon@google.com> wrote:
> Unlike `cargo build`, `cargo package` does not get access to the entire Git repo
> containing a Rust crate. Instead, it prepares a directory starting from the
> crate root (potentially excluding files, such as those not under version
> control, or explicity excluded in the Cargo.toml file).

s/explicity/explicitly/

> diff --git a/contrib/libgit-sys/git-src b/contrib/libgit-sys/git-src
> @@ -0,0 +1 @@
> +../..
> \ No newline at end of file

Meh.
Junio C Hamano March 24, 2025, 3:42 p.m. UTC | #2
Eric Sunshine <sunshine@sunshineco.com> writes:

> On Fri, Mar 21, 2025 at 6:14 PM Josh Steadmon <steadmon@google.com> wrote:
>> Unlike `cargo build`, `cargo package` does not get access to the entire Git repo
>> containing a Rust crate. Instead, it prepares a directory starting from the
>> crate root (potentially excluding files, such as those not under version
>> control, or explicity excluded in the Cargo.toml file).
>
> s/explicity/explicitly/
>
>> diff --git a/contrib/libgit-sys/git-src b/contrib/libgit-sys/git-src
>> @@ -0,0 +1 @@
>> +../..
>> \ No newline at end of file
>
> Meh.

https://github.com/git/git/actions/runs/14030831429/job/39278185588#step:3:1

All of the Windows test jobs (not build ones) are broken due to the
presence of ../.. symbolic link.

Is that ugly hack the only way we can make this work?
Josh Steadmon March 25, 2025, 5:57 p.m. UTC | #3
On 2025.03.24 08:42, Junio C Hamano wrote:
> Eric Sunshine <sunshine@sunshineco.com> writes:
> 
> > On Fri, Mar 21, 2025 at 6:14 PM Josh Steadmon <steadmon@google.com> wrote:
> >> Unlike `cargo build`, `cargo package` does not get access to the entire Git repo
> >> containing a Rust crate. Instead, it prepares a directory starting from the
> >> crate root (potentially excluding files, such as those not under version
> >> control, or explicity excluded in the Cargo.toml file).
> >
> > s/explicity/explicitly/
> >
> >> diff --git a/contrib/libgit-sys/git-src b/contrib/libgit-sys/git-src
> >> @@ -0,0 +1 @@
> >> +../..
> >> \ No newline at end of file
> >
> > Meh.
> 
> https://github.com/git/git/actions/runs/14030831429/job/39278185588#step:3:1
> 
> All of the Windows test jobs (not build ones) are broken due to the
> presence of ../.. symbolic link.
> 
> Is that ugly hack the only way we can make this work?

It's the only way I know of to accomplish both:
1) creating a packaged crate with `cargo package` and
2) keeping the top-level clean of any Rust code or configuration.

If we're willing to have a Cargo.toml file in the repo root, we could
create a "Cargo workspace", but I'm not sure yet if that avoids the same
problem with accessing sources outside of the crates themselves. I'll be
able to test it out later this week.

If the workspace approach doesn't work, the alternatives are:

1) avoid the issue for now; anyone who wants to experiment with
libgit-rs can do so by building from source (but it will prevent them
from creating their own packaged crates IIUC).

2) move libgit-sys and libgit-rs to separate repos and depend on the Git
source via submodules. This is what I've seen done in other -sys crates
such as zlib-sys (https://github.com/rust-lang/libz-sys).

Of those alternatives, I prefer #1 for now. If we build enough momentum
on libification and expanding the coverage of these crates, then we
could think about switching to #2.
Junio C Hamano March 25, 2025, 11:08 p.m. UTC | #4
Josh Steadmon <steadmon@google.com> writes:

> 2) keeping the top-level clean of any Rust code or configuration.
>
> If we're willing to have a Cargo.toml file in the repo root, ...

If it is more like adding a new build configuration file whereever
we have Makefile (or meson.build), and is not like we are adding one
new file per one existing source file, then I see no reason why we
want to avoid adding a few files to the root-level.

> ... we could
> create a "Cargo workspace", but I'm not sure yet if that avoids the same
> problem with accessing sources outside of the crates themselves. I'll be
> able to test it out later this week.

Yeah, that would probably be a reasonable thing to try.  Thanks.


> If the workspace approach doesn't work, the alternatives are:
>
> 1) avoid the issue for now; anyone who wants to experiment with
> libgit-rs can do so by building from source (but it will prevent them
> from creating their own packaged crates IIUC).
>
> 2) move libgit-sys and libgit-rs to separate repos and depend on the Git
> source via submodules. This is what I've seen done in other -sys crates
> such as zlib-sys (https://github.com/rust-lang/libz-sys).
>
> Of those alternatives, I prefer #1 for now. If we build enough momentum
> on libification and expanding the coverage of these crates, then we
> could think about switching to #2.

Yeah, or putting it another way, #1 would help us gather enough Rust
minded folks who are familiar enough to come up with ideas and offer
better ways to manage this part of the system.

Thanks.
Josh Steadmon March 27, 2025, 6:58 p.m. UTC | #5
On 2025.03.25 16:08, Junio C Hamano wrote:
> Josh Steadmon <steadmon@google.com> writes:
> 
> > 2) keeping the top-level clean of any Rust code or configuration.
> >
> > If we're willing to have a Cargo.toml file in the repo root, ...
> 
> If it is more like adding a new build configuration file whereever
> we have Makefile (or meson.build), and is not like we are adding one
> new file per one existing source file, then I see no reason why we
> want to avoid adding a few files to the root-level.
> 
> > ... we could
> > create a "Cargo workspace", but I'm not sure yet if that avoids the same
> > problem with accessing sources outside of the crates themselves. I'll be
> > able to test it out later this week.
> 
> Yeah, that would probably be a reasonable thing to try.  Thanks.
> 
> 
> > If the workspace approach doesn't work, the alternatives are:
> >
> > 1) avoid the issue for now; anyone who wants to experiment with
> > libgit-rs can do so by building from source (but it will prevent them
> > from creating their own packaged crates IIUC).
> >
> > 2) move libgit-sys and libgit-rs to separate repos and depend on the Git
> > source via submodules. This is what I've seen done in other -sys crates
> > such as zlib-sys (https://github.com/rust-lang/libz-sys).
> >
> > Of those alternatives, I prefer #1 for now. If we build enough momentum
> > on libification and expanding the coverage of these crates, then we
> > could think about switching to #2.
> 
> Yeah, or putting it another way, #1 would help us gather enough Rust
> minded folks who are familiar enough to come up with ideas and offer
> better ways to manage this part of the system.
> 
> Thanks.

Unfortunately creating a workspace doesn't provide access to the
top-level source. Symlinks seem to be the only recommended approach [1]
for embedded crates, but since that breaks Windows CI let's just drop
the series for now.

[1] https://users.rust-lang.org/t/including-files-from-parent-directory-in-package/88969
Junio C Hamano March 29, 2025, 10:46 a.m. UTC | #6
Josh Steadmon <steadmon@google.com> writes:

> Unfortunately creating a workspace doesn't provide access to the
> top-level source. Symlinks seem to be the only recommended approach [1]
> for embedded crates, but since that breaks Windows CI let's just drop
> the series for now.

Understood.  Thanks.
Johannes Schindelin March 31, 2025, 2:52 p.m. UTC | #7
Hi Josh,

On Thu, 27 Mar 2025, Josh Steadmon wrote:

> On 2025.03.25 16:08, Junio C Hamano wrote:
> > Josh Steadmon <steadmon@google.com> writes:
> >
> > > 2) keeping the top-level clean of any Rust code or configuration.
> > >
> > > If we're willing to have a Cargo.toml file in the repo root, ...
> >
> > If it is more like adding a new build configuration file whereever
> > we have Makefile (or meson.build), and is not like we are adding one
> > new file per one existing source file, then I see no reason why we
> > want to avoid adding a few files to the root-level.
> >
> > > ... we could
> > > create a "Cargo workspace", but I'm not sure yet if that avoids the same
> > > problem with accessing sources outside of the crates themselves. I'll be
> > > able to test it out later this week.
> >
> > Yeah, that would probably be a reasonable thing to try.  Thanks.
> >
> >
> > > If the workspace approach doesn't work, the alternatives are:
> > >
> > > 1) avoid the issue for now; anyone who wants to experiment with
> > > libgit-rs can do so by building from source (but it will prevent them
> > > from creating their own packaged crates IIUC).
> > >
> > > 2) move libgit-sys and libgit-rs to separate repos and depend on the Git
> > > source via submodules. This is what I've seen done in other -sys crates
> > > such as zlib-sys (https://github.com/rust-lang/libz-sys).
> > >
> > > Of those alternatives, I prefer #1 for now. If we build enough momentum
> > > on libification and expanding the coverage of these crates, then we
> > > could think about switching to #2.
> >
> > Yeah, or putting it another way, #1 would help us gather enough Rust
> > minded folks who are familiar enough to come up with ideas and offer
> > better ways to manage this part of the system.
> >
> > Thanks.
>
> Unfortunately creating a workspace doesn't provide access to the
> top-level source. Symlinks seem to be the only recommended approach [1]
> for embedded crates, but since that breaks Windows CI let's just drop
> the series for now.
>
> [1] https://users.rust-lang.org/t/including-files-from-parent-directory-in-package/88969

If you need symbolic linkson Windows in CI, please set
`MSYS=winsymlinks:nativestrict` like it is done here:
https://github.com/git-for-windows/git/blob/4ca71ba5311a8f1bafbf002e97e076f15dcfc15b/t/t2040-checkout-symlink-attr.sh#L8-L10.

It might fail, though, if it requires _Git_ to be able to create symbolic
links because I have not yet managed to upstream the patches to implement
that.

We could fast-track support for `readlink()` without the other parts
(https://github.com/git-for-windows/git/commit/86420a1b84d1), but last
time I checked, Git's test suite did not pass under
`MSYS=winsymlinks:nativestrict` for some reasons I was unable to analyze
for lack of time.

Ciao,
Johannes
diff mbox series

Patch

diff --git a/Makefile b/Makefile
index 52eed88dde..91677448ba 100644
--- a/Makefile
+++ b/Makefile
@@ -420,6 +420,9 @@  include shared.mak
 # Define INCLUDE_LIBGIT_RS if you want `make all` and `make test` to build and
 # test the Rust crates in contrib/libgit-sys and contrib/libgit-rs.
 #
+# Define CARGO_OUT_DIR to specify a directory where object files and other files
+# generated during the build of libgitpub.a should be created.
+#
 # === Optional library: libintl ===
 #
 # Define NO_GETTEXT if you don't want Git output to be translated.
@@ -699,6 +702,7 @@  THIRD_PARTY_SOURCES =
 UNIT_TEST_PROGRAMS =
 UNIT_TEST_DIR = t/unit-tests
 UNIT_TEST_BIN = $(UNIT_TEST_DIR)/bin
+CARGO_OUT_DIR =
 
 # Having this variable in your environment would break pipelines because
 # you cause "cd" to echo its destination to stdout.  It can also take
@@ -917,11 +921,21 @@  export PYTHON_PATH
 
 TEST_SHELL_PATH = $(SHELL_PATH)
 
-LIB_FILE = libgit.a
-XDIFF_LIB = xdiff/lib.a
-REFTABLE_LIB = reftable/libreftable.a
+### Generated files which may need to live in Cargo output directory
+
+GIT_CFLAGS = $(call maybe_join_path,$(CARGO_OUT_DIR),GIT-CFLAGS)
+GIT_PREFIX = $(call maybe_join_path,$(CARGO_OUT_DIR),GIT-PREFIX)
+GIT_USER_AGENT_FILE = $(call maybe_join_path,$(CARGO_OUT_DIR),GIT-USER-AGENT)
+GIT_VERSION_FILE = $(call maybe_join_path,$(CARGO_OUT_DIR),GIT-VERSION-FILE)
+GIT_VERSION_PATH = $(dir $(GIT_VERSION_FILE))$(notdir $(GIT_VERSION_FILE))
+COMMAND_LIST_H = $(call maybe_join_path,$(CARGO_OUT_DIR),command-list.h)
+VERSION_DEF_H = $(call maybe_join_path,$(CARGO_OUT_DIR),version-def.h)
 
-GENERATED_H += command-list.h
+LIB_FILE = $(call maybe_join_path,$(CARGO_OUT_DIR),libgit.a)
+XDIFF_LIB = $(call maybe_join_path,$(CARGO_OUT_DIR),xdiff/lib.a)
+REFTABLE_LIB = $(call maybe_join_path,$(CARGO_OUT_DIR),reftable/libreftable.a)
+
+GENERATED_H += $(COMMAND_LIST_H)
 GENERATED_H += config-list.h
 GENERATED_H += hook-list.h
 GENERATED_H += $(UNIT_TEST_DIR)/clar-decls.h
@@ -1472,7 +1486,7 @@  ifdef DEVELOPER
 include config.mak.dev
 endif
 
-GIT-VERSION-FILE: FORCE
+$(GIT_VERSION_FILE): FORCE
 	@OLD=$$(cat $@ 2>/dev/null || :) && \
 	$(call version_gen,"$(shell pwd)",GIT-VERSION-FILE.in,$@) && \
 	NEW=$$(cat $@ 2>/dev/null || :) && \
@@ -1482,7 +1496,7 @@  GIT-VERSION-FILE: FORCE
 # otherwise any user-provided value for GIT_VERSION would have been overridden
 # already.
 GIT_VERSION_OVERRIDE := $(GIT_VERSION)
--include GIT-VERSION-FILE
+-include $(GIT_VERSION_FILE)
 
 # what 'all' will build and 'install' will install in gitexecdir,
 # excluding programs for built-in commands
@@ -2403,9 +2417,9 @@  endif
 GIT_USER_AGENT_SQ = $(subst ','\'',$(GIT_USER_AGENT))
 GIT_USER_AGENT_CQ = "$(subst ",\",$(subst \,\\,$(GIT_USER_AGENT)))"
 GIT_USER_AGENT_CQ_SQ = $(subst ','\'',$(GIT_USER_AGENT_CQ))
-GIT-USER-AGENT: FORCE
-	@if test x'$(GIT_USER_AGENT_SQ)' != x"`cat GIT-USER-AGENT 2>/dev/null`"; then \
-		echo '$(GIT_USER_AGENT_SQ)' >GIT-USER-AGENT; \
+$(GIT_USER_AGENT_FILE): FORCE
+	@if test x'$(GIT_USER_AGENT_SQ)' != x"`cat $(GIT_USER_AGENT_FILE) 2>/dev/null`"; then \
+		echo '$(GIT_USER_AGENT_SQ)' >$(GIT_USER_AGENT_FILE); \
 	fi
 
 ifdef DEFAULT_HELP_FORMAT
@@ -2523,7 +2537,7 @@  strip: $(PROGRAMS) git$X
 #   dependencies here will not need to change if the force-build
 #   details change some day.
 
-git.sp git.s git.o: GIT-PREFIX
+git.sp git.s git.o: $(GIT_PREFIX)
 git.sp git.s git.o: EXTRA_CPPFLAGS = \
 	'-DGIT_HTML_PATH="$(htmldir_relative_SQ)"' \
 	'-DGIT_MAN_PATH="$(mandir_relative_SQ)"' \
@@ -2533,10 +2547,10 @@  git$X: git.o GIT-LDFLAGS $(BUILTIN_OBJS) $(GITLIBS)
 	$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \
 		$(filter %.o,$^) $(LIBS)
 
-help.sp help.s help.o: command-list.h
+help.sp help.s $(call maybe_join_path,$(CARGO_OUT_DIR),help.o): $(COMMAND_LIST_H)
 builtin/bugreport.sp builtin/bugreport.s builtin/bugreport.o: hook-list.h
 
-builtin/help.sp builtin/help.s builtin/help.o: config-list.h GIT-PREFIX
+builtin/help.sp builtin/help.s builtin/help.o: config-list.h $(GIT_PREFIX)
 builtin/help.sp builtin/help.s builtin/help.o: EXTRA_CPPFLAGS = \
 	'-DGIT_HTML_PATH="$(htmldir_relative_SQ)"' \
 	'-DGIT_MAN_PATH="$(mandir_relative_SQ)"' \
@@ -2545,13 +2559,13 @@  builtin/help.sp builtin/help.s builtin/help.o: EXTRA_CPPFLAGS = \
 PAGER_ENV_SQ = $(subst ','\'',$(PAGER_ENV))
 PAGER_ENV_CQ = "$(subst ",\",$(subst \,\\,$(PAGER_ENV)))"
 PAGER_ENV_CQ_SQ = $(subst ','\'',$(PAGER_ENV_CQ))
-pager.sp pager.s pager.o: EXTRA_CPPFLAGS = \
+pager.sp pager.s $(call maybe_join_path,$(CARGO_OUT_DIR),pager.o): EXTRA_CPPFLAGS = \
 	-DPAGER_ENV='$(PAGER_ENV_CQ_SQ)'
 
-version-def.h: version-def.h.in GIT-VERSION-GEN GIT-VERSION-FILE GIT-USER-AGENT
+$(VERSION_DEF_H): version-def.h.in GIT-VERSION-GEN $(GIT_VERSION_FILE) $(GIT_USER_AGENT_FILE)
 	$(QUIET_GEN)$(call version_gen,"$(shell pwd)",$<,$@)
 
-version.sp version.s version.o: version-def.h
+version.sp version.s $(call maybe_join_path,$(CARGO_OUT_DIR),version.o): $(VERSION_DEF_H)
 
 $(BUILT_INS): git$X
 	$(QUIET_BUILT_IN)$(RM) $@ && \
@@ -2564,9 +2578,9 @@  config-list.h: generate-configlist.sh
 config-list.h: Documentation/*config.adoc Documentation/config/*.adoc
 	$(QUIET_GEN)$(SHELL_PATH) ./generate-configlist.sh . $@
 
-command-list.h: generate-cmdlist.sh command-list.txt
+$(COMMAND_LIST_H): generate-cmdlist.sh command-list.txt
 
-command-list.h: $(wildcard Documentation/git*.adoc)
+$(COMMAND_LIST_H): $(wildcard Documentation/git*.adoc)
 	$(QUIET_GEN)$(SHELL_PATH) ./generate-cmdlist.sh \
 		$(patsubst %,--exclude-program %,$(EXCLUDED_PROGRAMS)) \
 		. $@
@@ -2589,10 +2603,10 @@  $(SCRIPT_SH_GEN) $(SCRIPT_LIB) : % : %.sh generate-script.sh GIT-BUILD-OPTIONS G
 	$(QUIET_GEN)./generate-script.sh "$<" "$@+" ./GIT-BUILD-OPTIONS && \
 	mv $@+ $@
 
-git.rc: git.rc.in GIT-VERSION-GEN GIT-VERSION-FILE
+git.rc: git.rc.in GIT-VERSION-GEN $(GIT_VERSION_FILE)
 	$(QUIET_GEN)$(call version_gen,"$(shell pwd)",$<,$@)
 
-git.res: git.rc GIT-PREFIX
+git.res: git.rc $(GIT_PREFIX)
 	$(QUIET_RC)$(RC) -i $< -o $@
 
 # This makes sure we depend on the NO_PERL setting itself.
@@ -2626,8 +2640,8 @@  endif
 
 PERL_DEFINES += $(gitexecdir) $(perllibdir) $(localedir)
 
-$(SCRIPT_PERL_GEN): % : %.perl generate-perl.sh GIT-PERL-DEFINES GIT-PERL-HEADER GIT-VERSION-FILE
-	$(QUIET_GEN)$(SHELL_PATH) generate-perl.sh ./GIT-BUILD-OPTIONS ./GIT-VERSION-FILE GIT-PERL-HEADER "$<" "$@+" && \
+$(SCRIPT_PERL_GEN): % : %.perl generate-perl.sh GIT-PERL-DEFINES GIT-PERL-HEADER $(GIT_VERSION_FILE)
+	$(QUIET_GEN)$(SHELL_PATH) generate-perl.sh ./GIT-BUILD-OPTIONS $(GIT_VERSION_PATH) GIT-PERL-HEADER "$<" "$@+" && \
 	mv $@+ $@
 
 PERL_DEFINES := $(subst $(space),:,$(PERL_DEFINES))
@@ -2691,7 +2705,7 @@  CONFIGURE_RECIPE = sed -e 's/@GIT_VERSION@/$(GIT_VERSION)/g' \
 		   autoconf -o configure configure.ac+ && \
 		   $(RM) configure.ac+
 
-configure: configure.ac GIT-VERSION-FILE
+configure: configure.ac $(GIT_VERSION_FILE)
 	$(QUIET_GEN)$(CONFIGURE_RECIPE)
 
 ifdef AUTOCONFIGURED
@@ -2774,6 +2788,14 @@  endif
 .PHONY: objects
 objects: $(OBJECTS)
 
+ifdef CARGO_OUT_DIR
+OBJECTS := $(addprefix $(CARGO_OUT_DIR)/,$(OBJECTS))
+LIB_OBJS := $(addprefix $(CARGO_OUT_DIR)/,$(LIB_OBJS))
+REFTABLE_OBJS := $(addprefix $(CARGO_OUT_DIR)/,$(REFTABLE_OBJS))
+XDIFF_OBJS := $(addprefix $(CARGO_OUT_DIR)/,$(XDIFF_OBJS))
+BASIC_CFLAGS += -I$(CARGO_OUT_DIR)
+endif
+
 dep_files := $(foreach f,$(OBJECTS),$(dir $f).depend/$(notdir $f).d)
 dep_dirs := $(addsuffix .depend,$(sort $(dir $(OBJECTS))))
 
@@ -2805,10 +2827,15 @@  missing_compdb_dir =
 compdb_args =
 endif
 
-$(OBJECTS): %.o: %.c GIT-CFLAGS $(missing_dep_dirs) $(missing_compdb_dir)
+ifdef CARGO_OUT_DIR
+$(OBJECTS): $(CARGO_OUT_DIR)/%.o: %.c $(GIT_CFLAGS) $(missing_dep_dirs) $(missing_compdb_dir)
+	$(QUIET_CC)$(CC) -o $(CARGO_OUT_DIR)/$*.o -c $(dep_args) $(compdb_args) $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $<
+else
+$(OBJECTS): %.o: %.c $(GIT_CFLAGS) $(missing_dep_dirs) $(missing_compdb_dir)
 	$(QUIET_CC)$(CC) -o $*.o -c $(dep_args) $(compdb_args) $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $<
+endif
 
-%.s: %.c GIT-CFLAGS FORCE
+%.s: %.c $(GIT_CFLAGS) FORCE
 	$(QUIET_CC)$(CC) -o $@ -S $(ALL_CFLAGS) $(EXTRA_CPPFLAGS) $<
 
 ifdef USE_COMPUTED_HEADER_DEPENDENCIES
@@ -2829,27 +2856,27 @@  compile_commands.json:
 	@if test -s $@+; then mv $@+ $@; else $(RM) $@+; fi
 endif
 
-exec-cmd.sp exec-cmd.s exec-cmd.o: GIT-PREFIX
-exec-cmd.sp exec-cmd.s exec-cmd.o: EXTRA_CPPFLAGS = \
+exec-cmd.sp exec-cmd.s $(call maybe_join_path,$(CARGO_OUT_DIR),exec-cmd.o): $(GIT_PREFIX)
+exec-cmd.sp exec-cmd.s $(call maybe_join_path,$(CARGO_OUT_DIR),exec-cmd.o): EXTRA_CPPFLAGS = \
 	'-DGIT_EXEC_PATH="$(gitexecdir_SQ)"' \
 	'-DGIT_LOCALE_PATH="$(localedir_relative_SQ)"' \
 	'-DBINDIR="$(bindir_relative_SQ)"' \
 	'-DFALLBACK_RUNTIME_PREFIX="$(prefix_SQ)"'
 
-setup.sp setup.s setup.o: GIT-PREFIX
-setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \
+setup.sp setup.s $(call maybe_join_path,$(CARGO_OUT_DIR),setup.o): $(GIT_PREFIX)
+setup.sp setup.s $(call maybe_join_path,$(CARGO_OUT_DIR),setup.o): EXTRA_CPPFLAGS = \
 	-DDEFAULT_GIT_TEMPLATE_DIR='"$(template_dir_SQ)"'
 
-config.sp config.s config.o: GIT-PREFIX
-config.sp config.s config.o: EXTRA_CPPFLAGS = \
+config.sp config.s $(call maybe_join_path,$(CARGO_OUT_DIR),config.o): $(GIT_PREFIX)
+config.sp config.s $(call maybe_join_path,$(CARGO_OUT_DIR),config.o): EXTRA_CPPFLAGS = \
 	-DETC_GITCONFIG='"$(ETC_GITCONFIG_SQ)"'
 
-attr.sp attr.s attr.o: GIT-PREFIX
-attr.sp attr.s attr.o: EXTRA_CPPFLAGS = \
+attr.sp attr.s $(call maybe_join_path,$(CARGO_OUT_DIR),attr.o): $(GIT_PREFIX)
+attr.sp attr.s $(call maybe_join_path,$(CARGO_OUT_DIR),attr.o): EXTRA_CPPFLAGS = \
 	-DETC_GITATTRIBUTES='"$(ETC_GITATTRIBUTES_SQ)"'
 
-gettext.sp gettext.s gettext.o: GIT-PREFIX
-gettext.sp gettext.s gettext.o: EXTRA_CPPFLAGS = \
+gettext.sp gettext.s $(call maybe_join_path,$(CARGO_OUT_DIR),gettext.o): $(GIT_PREFIX)
+gettext.sp gettext.s $(call maybe_join_path,$(CARGO_OUT_DIR),gettext.o): EXTRA_CPPFLAGS = \
 	-DGIT_LOCALE_PATH='"$(localedir_relative_SQ)"'
 
 http-push.sp http.sp http-walker.sp remote-curl.sp imap-send.sp: SP_EXTRA_FLAGS += \
@@ -2872,7 +2899,7 @@  compat/nedmalloc/nedmalloc.sp compat/nedmalloc/nedmalloc.o: EXTRA_CPPFLAGS = \
 compat/nedmalloc/nedmalloc.sp: SP_EXTRA_FLAGS += -Wno-non-pointer-null
 endif
 
-headless-git.o: compat/win32/headless.c GIT-CFLAGS
+headless-git.o: compat/win32/headless.c $(GIT_CFLAGS)
 	$(QUIET_CC)$(CC) $(ALL_CFLAGS) $(COMPAT_CFLAGS) \
 		-fno-stack-protector -o $@ -c -Wall -Wwrite-strings $<
 
@@ -3116,9 +3143,9 @@  endif
 NO_PERL_CPAN_FALLBACKS_SQ = $(subst ','\'',$(NO_PERL_CPAN_FALLBACKS))
 endif
 
-perl/build/lib/%.pm: perl/%.pm generate-perl.sh GIT-BUILD-OPTIONS GIT-VERSION-FILE GIT-PERL-DEFINES
+perl/build/lib/%.pm: perl/%.pm generate-perl.sh GIT-BUILD-OPTIONS $(GIT_VERSION_FILE) GIT-PERL-DEFINES
 	$(call mkdir_p_parent_template)
-	$(QUIET_GEN)$(SHELL_PATH) generate-perl.sh ./GIT-BUILD-OPTIONS ./GIT-VERSION-FILE GIT-PERL-HEADER "$<" "$@"
+	$(QUIET_GEN)$(SHELL_PATH) generate-perl.sh ./GIT-BUILD-OPTIONS $(GIT_VERSION_PATH) GIT-PERL-HEADER "$<" "$@"
 
 perl/build/man/man3/Git.3pm: perl/Git.pm
 	$(call mkdir_p_parent_template)
@@ -3146,20 +3173,20 @@  cscope: cscope.out
 TRACK_PREFIX = $(bindir_SQ):$(gitexecdir_SQ):$(template_dir_SQ):$(prefix_SQ):\
 		$(localedir_SQ)
 
-GIT-PREFIX: FORCE
+$(GIT_PREFIX): FORCE
 	@FLAGS='$(TRACK_PREFIX)'; \
-	if test x"$$FLAGS" != x"`cat GIT-PREFIX 2>/dev/null`" ; then \
+	if test x"$$FLAGS" != x"`cat $(GIT_PREFIX) 2>/dev/null`" ; then \
 		echo >&2 "    * new prefix flags"; \
-		echo "$$FLAGS" >GIT-PREFIX; \
+		echo "$$FLAGS" >$(GIT_PREFIX); \
 	fi
 
 TRACK_CFLAGS = $(CC):$(subst ','\'',$(ALL_CFLAGS)):$(USE_GETTEXT_SCHEME)
 
-GIT-CFLAGS: FORCE
+$(GIT_CFLAGS): FORCE
 	@FLAGS='$(TRACK_CFLAGS)'; \
-	    if test x"$$FLAGS" != x"`cat GIT-CFLAGS 2>/dev/null`" ; then \
+	    if test x"$$FLAGS" != x"`cat $(GIT_CFLAGS) 2>/dev/null`" ; then \
 		echo >&2 "    * new build flags"; \
-		echo "$$FLAGS" >GIT-CFLAGS; \
+		echo "$$FLAGS" >$(GIT_CFLAGS); \
             fi
 
 TRACK_LDFLAGS = $(subst ','\'',$(ALL_LDFLAGS))
@@ -3751,7 +3778,7 @@  clean: profile-clean coverage-clean cocciclean
 	$(RM) $(FUZZ_PROGRAMS)
 	$(RM) $(SP_OBJ)
 	$(RM) $(HCC)
-	$(RM) version-def.h
+	$(RM) $(VERSION_DEF_H)
 	$(RM) -r $(dep_dirs) $(compdb_dir) compile_commands.json
 	$(RM) $(test_bindir_programs)
 	$(RM) -r po/build/
@@ -3774,8 +3801,8 @@  ifndef NO_TCLTK
 	$(MAKE) -C gitk-git clean
 	$(MAKE) -C git-gui clean
 endif
-	$(RM) GIT-VERSION-FILE GIT-CFLAGS GIT-LDFLAGS GIT-BUILD-OPTIONS
-	$(RM) GIT-USER-AGENT GIT-PREFIX
+	$(RM) $(GIT_VERSION_FILE) $(GIT_CFLAGS) GIT-LDFLAGS GIT-BUILD-OPTIONS
+	$(RM) $(GIT_USER_AGENT_FILE) $(GIT_PREFIX)
 	$(RM) GIT-SCRIPT-DEFINES GIT-PERL-DEFINES GIT-PERL-HEADER GIT-PYTHON-VARS
 ifdef MSVC
 	$(RM) $(patsubst %.o,%.o.pdb,$(OBJECTS))
@@ -3937,14 +3964,14 @@  ifdef INCLUDE_LIBGIT_RS
 all:: libgit-sys libgit-rs
 endif
 
-LIBGIT_PUB_OBJS += contrib/libgitpub/public_symbol_export.o
-LIBGIT_PUB_OBJS += libgit.a
-LIBGIT_PUB_OBJS += reftable/libreftable.a
-LIBGIT_PUB_OBJS += xdiff/lib.a
-
-LIBGIT_PARTIAL_EXPORT = contrib/libgitpub/partial_symbol_export.o
+LIBGIT_PUB_OBJS += $(CARGO_OUT_DIR)/contrib/libgitpub/public_symbol_export.o
+LIBGIT_PUB_OBJS += $(LIB_FILE)
+LIBGIT_PUB_OBJS += $(REFTABLE_LIB)
+LIBGIT_PUB_OBJS += $(XDIFF_LIB)
 
-LIBGIT_HIDDEN_EXPORT = contrib/libgitpub/hidden_symbol_export.o
+LIBGIT_PARTIAL_EXPORT = $(CARGO_OUT_DIR)/contrib/libgitpub/partial_symbol_export.o
+LIBGIT_HIDDEN_EXPORT = $(CARGO_OUT_DIR)/contrib/libgitpub/hidden_symbol_export.o
+GITPUB_LIB = $(call maybe_join_path,$(CARGO_OUT_DIR),contrib/libgitpub/libgitpub.a)
 
 $(LIBGIT_PARTIAL_EXPORT): $(LIBGIT_PUB_OBJS)
 	$(LD) -r $^ -o $@
@@ -3952,5 +3979,5 @@  $(LIBGIT_PARTIAL_EXPORT): $(LIBGIT_PUB_OBJS)
 $(LIBGIT_HIDDEN_EXPORT): $(LIBGIT_PARTIAL_EXPORT)
 	$(OBJCOPY) --localize-hidden $^ $@
 
-contrib/libgitpub/libgitpub.a: $(LIBGIT_HIDDEN_EXPORT)
+$(GITPUB_LIB): $(LIBGIT_HIDDEN_EXPORT)
 	$(AR) $(ARFLAGS) $@ $^
diff --git a/contrib/libgit-sys/build.rs b/contrib/libgit-sys/build.rs
index e0d979c196..19407663f5 100644
--- a/contrib/libgit-sys/build.rs
+++ b/contrib/libgit-sys/build.rs
@@ -6,7 +6,7 @@  pub fn main() -> std::io::Result<()> {
     ac.emit_has_path("std::ffi::c_char");
 
     let crate_root = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
-    let git_root = crate_root.join("../..");
+    let git_root = crate_root.join("git-src");
     let dst = PathBuf::from(env::var_os("OUT_DIR").unwrap());
 
     let make_output = make_cmd::gnu_make()
@@ -14,8 +14,9 @@  pub fn main() -> std::io::Result<()> {
         .env_remove("PROFILE")
         .current_dir(git_root.clone())
         .args([
+            &format!("CARGO_OUT_DIR={}", dst.display()),
             "INCLUDE_LIBGIT_RS=YesPlease",
-            "contrib/libgitpub/libgitpub.a",
+            &format!("{}/contrib/libgitpub/libgitpub.a", dst.display()),
         ])
         .output()
         .expect("Make failed to run");
@@ -26,8 +27,8 @@  pub fn main() -> std::io::Result<()> {
             String::from_utf8(make_output.stderr).unwrap()
         );
     }
-    std::fs::copy(git_root.join("contrib/libgitpub/libgitpub.a"), dst.join("libgitpub.a"))?;
     println!("cargo:rustc-link-search=native={}", dst.display());
+    println!("cargo:rustc-link-search=native={}", dst.join("contrib/libgitpub").display());
     println!("cargo:rustc-link-lib=gitpub");
     println!("cargo:rerun-if-changed={}", git_root.display());
 
diff --git a/contrib/libgit-sys/git-src b/contrib/libgit-sys/git-src
new file mode 120000
index 0000000000..c25bddb6dd
--- /dev/null
+++ b/contrib/libgit-sys/git-src
@@ -0,0 +1 @@ 
+../..
\ No newline at end of file
diff --git a/shared.mak b/shared.mak
index 1a99848a95..0dc611dd90 100644
--- a/shared.mak
+++ b/shared.mak
@@ -127,3 +127,8 @@  GIT_USER_AGENT="$(GIT_USER_AGENT)" \
 GIT_VERSION="$(GIT_VERSION_OVERRIDE)" \
 $(SHELL_PATH) "$(1)/GIT-VERSION-GEN" "$(1)" "$(2)" "$(3)"
 endef
+
+# Apply a path prefix if the prefix is non-empty
+define maybe_join_path
+$(if $(1),$(1)/$(2),$(2))
+endef