libgit-sys: introduce Rust wrapper for libgit.a

Introduce libgit-sys, a Rust wrapper crate that allows Rust code to call functions in libgit.a. This initial patch defines build rules and an interface that exposes user agent string getter functions as a proof of concept. This library can be tested with `cargo test`. In later commits, a higher-level library containing a more Rust-friendly interface will be added at `contrib/libgit-rs`. Symbols in libgit can collide with symbols from other libraries such as libgit2. We avoid this by first exposing library symbols in public_symbol_export.[ch]. These symbols are prepended with "libgit_" to avoid collisions and set to visible using a visibility pragma. In build.rs, Rust builds contrib/libgit-rs/libgit-sys/libgitpub.a, which also contains libgit.a and other dependent libraries, with -fvisibility=hidden to hide all symbols within those libraries that haven't been exposed with a visibility pragma. Co-authored-by: Kyle Lippincott <spectral@google.com> Co-authored-by: Calvin Wan <calvinwan@google.com> Signed-off-by: Calvin Wan <calvinwan@google.com> Signed-off-by: Kyle Lippincott <spectral@google.com> Signed-off-by: Josh Steadmon <steadmon@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Josh Steadmon committed Jan 28, 2025 at 14:01 UTC e7f8bf125c078004232e254e71b97226c4fed81b
10 files changed +263
.gitignore
+1
@@ -250,3 +250,4 @@ Release/
250 /git.VC.db
251 *.dSYM
252 /contrib/buildsystems/out
253 +/contrib/libgit-sys/target
Makefile
+48
@@ -416,6 +416,9 @@ include shared.mak
416 # Define LINK_FUZZ_PROGRAMS if you want `make all` to also build the fuzz test
417 # programs in oss-fuzz/.
418 #
419 +# Define INCLUDE_LIBGIT_RS if you want `make all` and `make test` to build and
420 +# test the Rust crate in contrib/libgit-sys.
421 +#
422 # === Optional library: libintl ===
423 #
424 # Define NO_GETTEXT if you don't want Git output to be translated.
@@ -657,6 +660,8 @@ CURL_CONFIG = curl-config
660 GCOV = gcov
661 STRIP = strip
662 SPATCH = spatch
663 +LD = ld
664 +OBJCOPY = objcopy
665
666 export TCL_PATH TCLTK_PATH
667
@@ -675,6 +680,7 @@ FUZZ_OBJS =
680 FUZZ_PROGRAMS =
681 GIT_OBJS =
682 LIB_OBJS =
683 +LIBGIT_PUB_OBJS =
684 SCALAR_OBJS =
685 OBJECTS =
686 OTHER_PROGRAMS =
@@ -2236,6 +2242,12 @@ ifdef FSMONITOR_OS_SETTINGS
2242 COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o
2243 endif
2244
2245 +ifdef INCLUDE_LIBGIT_RS
2246 + # Enable symbol hiding in contrib/libgit-sys/libgitpub.a without making
2247 + # us rebuild the whole tree every time we run a Rust build.
2248 + BASIC_CFLAGS += -fvisibility=hidden
2249 +endif
2250 +
2251 ifeq ($(TCLTK_PATH),)
2252 NO_TCLTK = NoThanks
2253 endif
@@ -2732,6 +2744,10 @@ OBJECTS += $(UNIT_TEST_OBJS)
2744 OBJECTS += $(CLAR_TEST_OBJS)
2745 OBJECTS += $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS))
2746
2747 +ifdef INCLUDE_LIBGIT_RS
2748 + OBJECTS += contrib/libgit-sys/public_symbol_export.o
2749 +endif
2750 +
2751 ifndef NO_CURL
2752 OBJECTS += http.o http-walker.o remote-curl.o
2753 endif
@@ -3726,6 +3742,10 @@ clean: profile-clean coverage-clean cocciclean
3742 $(RM) $(htmldocs).tar.gz $(manpages).tar.gz
3743 $(MAKE) -C Documentation/ clean
3744 $(RM) Documentation/GIT-EXCLUDED-PROGRAMS
3745 + $(RM) -r contrib/libgit-sys/target
3746 + $(RM) contrib/libgit-sys/partial_symbol_export.o
3747 + $(RM) contrib/libgit-sys/hidden_symbol_export.o
3748 + $(RM) contrib/libgit-sys/libgitpub.a
3749 ifndef NO_PERL
3750 $(RM) -r perl/build/
3751 endif
@@ -3887,3 +3907,31 @@ $(CLAR_TEST_PROG): $(UNIT_TEST_DIR)/clar.suite $(CLAR_TEST_OBJS) $(GITLIBS) GIT-
3907 build-unit-tests: $(UNIT_TEST_PROGS) $(CLAR_TEST_PROG)
3908 unit-tests: $(UNIT_TEST_PROGS) $(CLAR_TEST_PROG) t/helper/test-tool$X
3909 $(MAKE) -C t/ unit-tests
3910 +
3911 +.PHONY: libgit-sys
3912 +libgit-sys:
3913 + $(QUIET)(\
3914 + cd contrib/libgit-sys && \
3915 + cargo build \
3916 + )
3917 +ifdef INCLUDE_LIBGIT_RS
3918 +all:: libgit-sys
3919 +endif
3920 +
3921 +LIBGIT_PUB_OBJS += contrib/libgit-sys/public_symbol_export.o
3922 +LIBGIT_PUB_OBJS += libgit.a
3923 +LIBGIT_PUB_OBJS += reftable/libreftable.a
3924 +LIBGIT_PUB_OBJS += xdiff/lib.a
3925 +
3926 +LIBGIT_PARTIAL_EXPORT = contrib/libgit-sys/partial_symbol_export.o
3927 +
3928 +LIBGIT_HIDDEN_EXPORT = contrib/libgit-sys/hidden_symbol_export.o
3929 +
3930 +$(LIBGIT_PARTIAL_EXPORT): $(LIBGIT_PUB_OBJS)
3931 + $(LD) -r $^ -o $@
3932 +
3933 +$(LIBGIT_HIDDEN_EXPORT): $(LIBGIT_PARTIAL_EXPORT)
3934 + $(OBJCOPY) --localize-hidden $^ $@
3935 +
3936 +contrib/libgit-sys/libgitpub.a: $(LIBGIT_HIDDEN_EXPORT)
3937 + $(AR) $(ARFLAGS) $@ $^
contrib/libgit-sys/Cargo.lock new
+69
@@ -0,0 +1,69 @@
1 +# This file is automatically @generated by Cargo.
2 +# It is not intended for manual editing.
3 +version = 3
4 +
5 +[[package]]
6 +name = "autocfg"
7 +version = "1.4.0"
8 +source = "registry+https://github.com/rust-lang/crates.io-index"
9 +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
10 +
11 +[[package]]
12 +name = "cc"
13 +version = "1.1.15"
14 +source = "registry+https://github.com/rust-lang/crates.io-index"
15 +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
16 +dependencies = [
17 + "shlex",
18 +]
19 +
20 +[[package]]
21 +name = "libc"
22 +version = "0.2.158"
23 +source = "registry+https://github.com/rust-lang/crates.io-index"
24 +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
25 +
26 +[[package]]
27 +name = "libgit-sys"
28 +version = "0.1.0"
29 +dependencies = [
30 + "autocfg",
31 + "libz-sys",
32 + "make-cmd",
33 +]
34 +
35 +[[package]]
36 +name = "libz-sys"
37 +version = "1.1.20"
38 +source = "registry+https://github.com/rust-lang/crates.io-index"
39 +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
40 +dependencies = [
41 + "cc",
42 + "libc",
43 + "pkg-config",
44 + "vcpkg",
45 +]
46 +
47 +[[package]]
48 +name = "make-cmd"
49 +version = "0.1.0"
50 +source = "registry+https://github.com/rust-lang/crates.io-index"
51 +checksum = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3"
52 +
53 +[[package]]
54 +name = "pkg-config"
55 +version = "0.3.30"
56 +source = "registry+https://github.com/rust-lang/crates.io-index"
57 +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
58 +
59 +[[package]]
60 +name = "shlex"
61 +version = "1.3.0"
62 +source = "registry+https://github.com/rust-lang/crates.io-index"
63 +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
64 +
65 +[[package]]
66 +name = "vcpkg"
67 +version = "0.2.15"
68 +source = "registry+https://github.com/rust-lang/crates.io-index"
69 +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
contrib/libgit-sys/Cargo.toml new
+19
@@ -0,0 +1,19 @@
1 +[package]
2 +name = "libgit-sys"
3 +version = "0.1.0"
4 +edition = "2021"
5 +build = "build.rs"
6 +links = "gitpub"
7 +rust-version = "1.63" # TODO: Once we hit 1.84 or newer, we may want to remove Cargo.lock from
8 + # version control. See https://lore.kernel.org/git/Z47jgK-oMjFRSslr@tapette.crustytoothpaste.net/
9 +description = "Native bindings to a portion of libgit"
10 +
11 +[lib]
12 +path = "src/lib.rs"
13 +
14 +[dependencies]
15 +libz-sys = "1.1.19"
16 +
17 +[build-dependencies]
18 +autocfg = "1.4.0"
19 +make-cmd = "0.1.0"
contrib/libgit-sys/README.md new
+4
@@ -0,0 +1,4 @@
1 +# libgit-sys
2 +
3 +A small proof-of-concept crate showing how to provide a Rust FFI to Git
4 +internals.
contrib/libgit-sys/build.rs new
+35
@@ -0,0 +1,35 @@
1 +use std::env;
2 +use std::path::PathBuf;
3 +
4 +pub fn main() -> std::io::Result<()> {
5 + let ac = autocfg::new();
6 + ac.emit_has_path("std::ffi::c_char");
7 +
8 + let crate_root = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
9 + let git_root = crate_root.join("../..");
10 + let dst = PathBuf::from(env::var_os("OUT_DIR").unwrap());
11 +
12 + let make_output = make_cmd::gnu_make()
13 + .env("DEVELOPER", "1")
14 + .env_remove("PROFILE")
15 + .current_dir(git_root.clone())
16 + .args([
17 + "INCLUDE_LIBGIT_RS=YesPlease",
18 + "contrib/libgit-sys/libgitpub.a",
19 + ])
20 + .output()
21 + .expect("Make failed to run");
22 + if !make_output.status.success() {
23 + panic!(
24 + "Make failed:\n stdout = {}\n stderr = {}\n",
25 + String::from_utf8(make_output.stdout).unwrap(),
26 + String::from_utf8(make_output.stderr).unwrap()
27 + );
28 + }
29 + std::fs::copy(crate_root.join("libgitpub.a"), dst.join("libgitpub.a"))?;
30 + println!("cargo:rustc-link-search=native={}", dst.display());
31 + println!("cargo:rustc-link-lib=gitpub");
32 + println!("cargo:rerun-if-changed={}", git_root.display());
33 +
34 + Ok(())
35 +}
contrib/libgit-sys/public_symbol_export.c new
+23
@@ -0,0 +1,23 @@
1 +/*
2 + * Shim to publicly export Git symbols. These must be renamed so that the
3 + * original symbols can be hidden. Renaming these with a "libgit_" prefix also
4 + * avoids conflicts with other libraries such as libgit2.
5 + */
6 +
7 +#include "git-compat-util.h"
8 +#include "contrib/libgit-sys/public_symbol_export.h"
9 +#include "version.h"
10 +
11 +#pragma GCC visibility push(default)
12 +
13 +const char *libgit_user_agent(void)
14 +{
15 + return git_user_agent();
16 +}
17 +
18 +const char *libgit_user_agent_sanitized(void)
19 +{
20 + return git_user_agent_sanitized();
21 +}
22 +
23 +#pragma GCC visibility pop
contrib/libgit-sys/public_symbol_export.h new
+8
@@ -0,0 +1,8 @@
1 +#ifndef PUBLIC_SYMBOL_EXPORT_H
2 +#define PUBLIC_SYMBOL_EXPORT_H
3 +
4 +const char *libgit_user_agent(void);
5 +
6 +const char *libgit_user_agent_sanitized(void);
7 +
8 +#endif /* PUBLIC_SYMBOL_EXPORT_H */
contrib/libgit-sys/src/lib.rs new
+46
@@ -0,0 +1,46 @@
1 +#[cfg(has_std__ffi__c_char)]
2 +use std::ffi::c_char;
3 +
4 +#[cfg(not(has_std__ffi__c_char))]
5 +#[allow(non_camel_case_types)]
6 +pub type c_char = i8;
7 +
8 +extern crate libz_sys;
9 +
10 +extern "C" {
11 + pub fn libgit_user_agent() -> *const c_char;
12 + pub fn libgit_user_agent_sanitized() -> *const c_char;
13 +}
14 +
15 +#[cfg(test)]
16 +mod tests {
17 + use std::ffi::CStr;
18 +
19 + use super::*;
20 +
21 + #[test]
22 + fn user_agent_starts_with_git() {
23 + let c_str = unsafe { CStr::from_ptr(libgit_user_agent()) };
24 + let agent = c_str
25 + .to_str()
26 + .expect("User agent contains invalid UTF-8 data");
27 + assert!(
28 + agent.starts_with("git/"),
29 + r#"Expected user agent to start with "git/", got: {}"#,
30 + agent
31 + );
32 + }
33 +
34 + #[test]
35 + fn sanitized_user_agent_starts_with_git() {
36 + let c_str = unsafe { CStr::from_ptr(libgit_user_agent_sanitized()) };
37 + let agent = c_str
38 + .to_str()
39 + .expect("Sanitized user agent contains invalid UTF-8 data");
40 + assert!(
41 + agent.starts_with("git/"),
42 + r#"Expected user agent to start with "git/", got: {}"#,
43 + agent
44 + );
45 + }
46 +}
t/Makefile
+10
@@ -177,3 +177,13 @@ perf:
177
178 .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \
179 check-chainlint clean-chainlint test-chainlint $(UNIT_TESTS)
180 +
181 +.PHONY: libgit-sys-test
182 +libgit-sys-test:
183 + $(QUIET)(\
184 + cd ../contrib/libgit-sys && \
185 + cargo test \
186 + )
187 +ifdef INCLUDE_LIBGIT_RS
188 +all:: libgit-sys-test
189 +endif