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>
This commit is contained in:
Josh Steadmon
2025-01-28 14:01:38 -08:00
committed by Junio C Hamano
parent 3f8f2abe05
commit e7f8bf125c
10 changed files with 263 additions and 0 deletions

1
.gitignore vendored
View File

@ -250,3 +250,4 @@ Release/
/git.VC.db /git.VC.db
*.dSYM *.dSYM
/contrib/buildsystems/out /contrib/buildsystems/out
/contrib/libgit-sys/target

View File

@ -416,6 +416,9 @@ include shared.mak
# Define LINK_FUZZ_PROGRAMS if you want `make all` to also build the fuzz test # Define LINK_FUZZ_PROGRAMS if you want `make all` to also build the fuzz test
# programs in oss-fuzz/. # programs in oss-fuzz/.
# #
# Define INCLUDE_LIBGIT_RS if you want `make all` and `make test` to build and
# test the Rust crate in contrib/libgit-sys.
#
# === Optional library: libintl === # === Optional library: libintl ===
# #
# Define NO_GETTEXT if you don't want Git output to be translated. # Define NO_GETTEXT if you don't want Git output to be translated.
@ -657,6 +660,8 @@ CURL_CONFIG = curl-config
GCOV = gcov GCOV = gcov
STRIP = strip STRIP = strip
SPATCH = spatch SPATCH = spatch
LD = ld
OBJCOPY = objcopy
export TCL_PATH TCLTK_PATH export TCL_PATH TCLTK_PATH
@ -675,6 +680,7 @@ FUZZ_OBJS =
FUZZ_PROGRAMS = FUZZ_PROGRAMS =
GIT_OBJS = GIT_OBJS =
LIB_OBJS = LIB_OBJS =
LIBGIT_PUB_OBJS =
SCALAR_OBJS = SCALAR_OBJS =
OBJECTS = OBJECTS =
OTHER_PROGRAMS = OTHER_PROGRAMS =
@ -2236,6 +2242,12 @@ ifdef FSMONITOR_OS_SETTINGS
COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o
endif endif
ifdef INCLUDE_LIBGIT_RS
# Enable symbol hiding in contrib/libgit-sys/libgitpub.a without making
# us rebuild the whole tree every time we run a Rust build.
BASIC_CFLAGS += -fvisibility=hidden
endif
ifeq ($(TCLTK_PATH),) ifeq ($(TCLTK_PATH),)
NO_TCLTK = NoThanks NO_TCLTK = NoThanks
endif endif
@ -2732,6 +2744,10 @@ OBJECTS += $(UNIT_TEST_OBJS)
OBJECTS += $(CLAR_TEST_OBJS) OBJECTS += $(CLAR_TEST_OBJS)
OBJECTS += $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS)) OBJECTS += $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS))
ifdef INCLUDE_LIBGIT_RS
OBJECTS += contrib/libgit-sys/public_symbol_export.o
endif
ifndef NO_CURL ifndef NO_CURL
OBJECTS += http.o http-walker.o remote-curl.o OBJECTS += http.o http-walker.o remote-curl.o
endif endif
@ -3726,6 +3742,10 @@ clean: profile-clean coverage-clean cocciclean
$(RM) $(htmldocs).tar.gz $(manpages).tar.gz $(RM) $(htmldocs).tar.gz $(manpages).tar.gz
$(MAKE) -C Documentation/ clean $(MAKE) -C Documentation/ clean
$(RM) Documentation/GIT-EXCLUDED-PROGRAMS $(RM) Documentation/GIT-EXCLUDED-PROGRAMS
$(RM) -r contrib/libgit-sys/target
$(RM) contrib/libgit-sys/partial_symbol_export.o
$(RM) contrib/libgit-sys/hidden_symbol_export.o
$(RM) contrib/libgit-sys/libgitpub.a
ifndef NO_PERL ifndef NO_PERL
$(RM) -r perl/build/ $(RM) -r perl/build/
endif endif
@ -3887,3 +3907,31 @@ $(CLAR_TEST_PROG): $(UNIT_TEST_DIR)/clar.suite $(CLAR_TEST_OBJS) $(GITLIBS) GIT-
build-unit-tests: $(UNIT_TEST_PROGS) $(CLAR_TEST_PROG) build-unit-tests: $(UNIT_TEST_PROGS) $(CLAR_TEST_PROG)
unit-tests: $(UNIT_TEST_PROGS) $(CLAR_TEST_PROG) t/helper/test-tool$X unit-tests: $(UNIT_TEST_PROGS) $(CLAR_TEST_PROG) t/helper/test-tool$X
$(MAKE) -C t/ unit-tests $(MAKE) -C t/ unit-tests
.PHONY: libgit-sys
libgit-sys:
$(QUIET)(\
cd contrib/libgit-sys && \
cargo build \
)
ifdef INCLUDE_LIBGIT_RS
all:: libgit-sys
endif
LIBGIT_PUB_OBJS += contrib/libgit-sys/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/libgit-sys/partial_symbol_export.o
LIBGIT_HIDDEN_EXPORT = contrib/libgit-sys/hidden_symbol_export.o
$(LIBGIT_PARTIAL_EXPORT): $(LIBGIT_PUB_OBJS)
$(LD) -r $^ -o $@
$(LIBGIT_HIDDEN_EXPORT): $(LIBGIT_PARTIAL_EXPORT)
$(OBJCOPY) --localize-hidden $^ $@
contrib/libgit-sys/libgitpub.a: $(LIBGIT_HIDDEN_EXPORT)
$(AR) $(ARFLAGS) $@ $^

69
contrib/libgit-sys/Cargo.lock generated Normal file
View File

@ -0,0 +1,69 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "cc"
version = "1.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
dependencies = [
"shlex",
]
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "libgit-sys"
version = "0.1.0"
dependencies = [
"autocfg",
"libz-sys",
"make-cmd",
]
[[package]]
name = "libz-sys"
version = "1.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "make-cmd"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3"
[[package]]
name = "pkg-config"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"

View File

@ -0,0 +1,19 @@
[package]
name = "libgit-sys"
version = "0.1.0"
edition = "2021"
build = "build.rs"
links = "gitpub"
rust-version = "1.63" # TODO: Once we hit 1.84 or newer, we may want to remove Cargo.lock from
# version control. See https://lore.kernel.org/git/Z47jgK-oMjFRSslr@tapette.crustytoothpaste.net/
description = "Native bindings to a portion of libgit"
[lib]
path = "src/lib.rs"
[dependencies]
libz-sys = "1.1.19"
[build-dependencies]
autocfg = "1.4.0"
make-cmd = "0.1.0"

View File

@ -0,0 +1,4 @@
# libgit-sys
A small proof-of-concept crate showing how to provide a Rust FFI to Git
internals.

View File

@ -0,0 +1,35 @@
use std::env;
use std::path::PathBuf;
pub fn main() -> std::io::Result<()> {
let ac = autocfg::new();
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 dst = PathBuf::from(env::var_os("OUT_DIR").unwrap());
let make_output = make_cmd::gnu_make()
.env("DEVELOPER", "1")
.env_remove("PROFILE")
.current_dir(git_root.clone())
.args([
"INCLUDE_LIBGIT_RS=YesPlease",
"contrib/libgit-sys/libgitpub.a",
])
.output()
.expect("Make failed to run");
if !make_output.status.success() {
panic!(
"Make failed:\n stdout = {}\n stderr = {}\n",
String::from_utf8(make_output.stdout).unwrap(),
String::from_utf8(make_output.stderr).unwrap()
);
}
std::fs::copy(crate_root.join("libgitpub.a"), dst.join("libgitpub.a"))?;
println!("cargo:rustc-link-search=native={}", dst.display());
println!("cargo:rustc-link-lib=gitpub");
println!("cargo:rerun-if-changed={}", git_root.display());
Ok(())
}

View File

@ -0,0 +1,23 @@
/*
* Shim to publicly export Git symbols. These must be renamed so that the
* original symbols can be hidden. Renaming these with a "libgit_" prefix also
* avoids conflicts with other libraries such as libgit2.
*/
#include "git-compat-util.h"
#include "contrib/libgit-sys/public_symbol_export.h"
#include "version.h"
#pragma GCC visibility push(default)
const char *libgit_user_agent(void)
{
return git_user_agent();
}
const char *libgit_user_agent_sanitized(void)
{
return git_user_agent_sanitized();
}
#pragma GCC visibility pop

View File

@ -0,0 +1,8 @@
#ifndef PUBLIC_SYMBOL_EXPORT_H
#define PUBLIC_SYMBOL_EXPORT_H
const char *libgit_user_agent(void);
const char *libgit_user_agent_sanitized(void);
#endif /* PUBLIC_SYMBOL_EXPORT_H */

View File

@ -0,0 +1,46 @@
#[cfg(has_std__ffi__c_char)]
use std::ffi::c_char;
#[cfg(not(has_std__ffi__c_char))]
#[allow(non_camel_case_types)]
pub type c_char = i8;
extern crate libz_sys;
extern "C" {
pub fn libgit_user_agent() -> *const c_char;
pub fn libgit_user_agent_sanitized() -> *const c_char;
}
#[cfg(test)]
mod tests {
use std::ffi::CStr;
use super::*;
#[test]
fn user_agent_starts_with_git() {
let c_str = unsafe { CStr::from_ptr(libgit_user_agent()) };
let agent = c_str
.to_str()
.expect("User agent contains invalid UTF-8 data");
assert!(
agent.starts_with("git/"),
r#"Expected user agent to start with "git/", got: {}"#,
agent
);
}
#[test]
fn sanitized_user_agent_starts_with_git() {
let c_str = unsafe { CStr::from_ptr(libgit_user_agent_sanitized()) };
let agent = c_str
.to_str()
.expect("Sanitized user agent contains invalid UTF-8 data");
assert!(
agent.starts_with("git/"),
r#"Expected user agent to start with "git/", got: {}"#,
agent
);
}
}

View File

@ -177,3 +177,13 @@ perf:
.PHONY: pre-clean $(T) aggregate-results clean valgrind perf \ .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \
check-chainlint clean-chainlint test-chainlint $(UNIT_TESTS) check-chainlint clean-chainlint test-chainlint $(UNIT_TESTS)
.PHONY: libgit-sys-test
libgit-sys-test:
$(QUIET)(\
cd ../contrib/libgit-sys && \
cargo test \
)
ifdef INCLUDE_LIBGIT_RS
all:: libgit-sys-test
endif