From 3f8f2abe05c0aeb0ea60768d44a99261ed456d44 Mon Sep 17 00:00:00 2001 From: Josh Steadmon Date: Tue, 28 Jan 2025 14:01:37 -0800 Subject: common-main: split init and exit code into new files Currently, object files in libgit.a reference common_exit(), which is contained in common-main.o. However, common-main.o also includes main(), which references cmd_main() in git.o, which in turn depends on all the builtin/*.o objects. We would like to allow external users to link libgit.a without needing to include so many extra objects. Enable this by splitting common_exit() and check_bug_if_BUG() into a new file common-exit.c, and add common-exit.o to LIB_OBJS so that these are included in libgit.a. This split has previously been proposed ([1], [2]) to support fuzz tests and unit tests by avoiding conflicting definitions for main(). However, both of those issues were resolved by other methods of avoiding symbol conflicts. Now we are trying to make libgit.a more self-contained, so hopefully we can revisit this approach. Additionally, move the initialization code out of main() into a new init_git() function in its own file. Include this in libgit.a as well, so that external users can share our setup code without calling our main(). [1] https://lore.kernel.org/git/Yp+wjCPhqieTku3X@google.com/ [2] https://lore.kernel.org/git/20230517-unit-tests-v2-v2-1-21b5b60f4b32@google.com/ Signed-off-by: Josh Steadmon Signed-off-by: Junio C Hamano --- Makefile | 2 ++ common-exit.c | 26 +++++++++++++++++++ common-init.c | 63 +++++++++++++++++++++++++++++++++++++++++++++ common-init.h | 6 +++++ common-main.c | 83 ++--------------------------------------------------------- meson.build | 2 ++ 6 files changed, 101 insertions(+), 81 deletions(-) create mode 100644 common-exit.c create mode 100644 common-init.c create mode 100644 common-init.h diff --git a/Makefile b/Makefile index 97e8385b66..27e68ac039 100644 --- a/Makefile +++ b/Makefile @@ -981,6 +981,8 @@ LIB_OBJS += combine-diff.o LIB_OBJS += commit-graph.o LIB_OBJS += commit-reach.o LIB_OBJS += commit.o +LIB_OBJS += common-exit.o +LIB_OBJS += common-init.o LIB_OBJS += compat/nonblock.o LIB_OBJS += compat/obstack.o LIB_OBJS += compat/terminal.o diff --git a/common-exit.c b/common-exit.c new file mode 100644 index 0000000000..1aaa538be3 --- /dev/null +++ b/common-exit.c @@ -0,0 +1,26 @@ +#include "git-compat-util.h" +#include "trace2.h" + +static void check_bug_if_BUG(void) +{ + if (!bug_called_must_BUG) + return; + BUG("on exit(): had bug() call(s) in this process without explicit BUG_if_bug()"); +} + +/* We wrap exit() to call common_exit() in git-compat-util.h */ +int common_exit(const char *file, int line, int code) +{ + /* + * For non-POSIX systems: Take the lowest 8 bits of the "code" + * to e.g. turn -1 into 255. On a POSIX system this is + * redundant, see exit(3) and wait(2), but as it doesn't harm + * anything there we don't need to guard this with an "ifdef". + */ + code &= 0xff; + + check_bug_if_BUG(); + trace2_cmd_exit_fl(file, line, code); + + return code; +} diff --git a/common-init.c b/common-init.c new file mode 100644 index 0000000000..5cc73f058c --- /dev/null +++ b/common-init.c @@ -0,0 +1,63 @@ +#define USE_THE_REPOSITORY_VARIABLE + +#include "git-compat-util.h" +#include "common-init.h" +#include "exec-cmd.h" +#include "gettext.h" +#include "attr.h" +#include "repository.h" +#include "setup.h" +#include "strbuf.h" +#include "trace2.h" + +/* + * Many parts of Git have subprograms communicate via pipe, expect the + * upstream of a pipe to die with SIGPIPE when the downstream of a + * pipe does not need to read all that is written. Some third-party + * programs that ignore or block SIGPIPE for their own reason forget + * to restore SIGPIPE handling to the default before spawning Git and + * break this carefully orchestrated machinery. + * + * Restore the way SIGPIPE is handled to default, which is what we + * expect. + */ +static void restore_sigpipe_to_default(void) +{ + sigset_t unblock; + + sigemptyset(&unblock); + sigaddset(&unblock, SIGPIPE); + sigprocmask(SIG_UNBLOCK, &unblock, NULL); + signal(SIGPIPE, SIG_DFL); +} + +void init_git(const char **argv) +{ + struct strbuf tmp = STRBUF_INIT; + + trace2_initialize_clock(); + + /* + * Always open file descriptors 0/1/2 to avoid clobbering files + * in die(). It also avoids messing up when the pipes are dup'ed + * onto stdin/stdout/stderr in the child processes we spawn. + */ + sanitize_stdfds(); + restore_sigpipe_to_default(); + + git_resolve_executable_dir(argv[0]); + + setlocale(LC_CTYPE, ""); + git_setup_gettext(); + + initialize_repository(the_repository); + + attr_start(); + + trace2_initialize(); + trace2_cmd_start(argv); + trace2_collect_process_info(TRACE2_PROCESS_INFO_STARTUP); + + if (!strbuf_getcwd(&tmp)) + tmp_original_cwd = strbuf_detach(&tmp, NULL); +} diff --git a/common-init.h b/common-init.h new file mode 100644 index 0000000000..3e6db20cae --- /dev/null +++ b/common-init.h @@ -0,0 +1,6 @@ +#ifndef COMMON_INIT_H +#define COMMON_INIT_H + +void init_git(const char **argv); + +#endif /* COMMON_INIT_H */ diff --git a/common-main.c b/common-main.c index 8e68ac9e42..6b7ab077b0 100644 --- a/common-main.c +++ b/common-main.c @@ -1,92 +1,13 @@ -#define USE_THE_REPOSITORY_VARIABLE - #include "git-compat-util.h" -#include "exec-cmd.h" -#include "gettext.h" -#include "attr.h" -#include "repository.h" -#include "setup.h" -#include "strbuf.h" -#include "trace2.h" - -/* - * Many parts of Git have subprograms communicate via pipe, expect the - * upstream of a pipe to die with SIGPIPE when the downstream of a - * pipe does not need to read all that is written. Some third-party - * programs that ignore or block SIGPIPE for their own reason forget - * to restore SIGPIPE handling to the default before spawning Git and - * break this carefully orchestrated machinery. - * - * Restore the way SIGPIPE is handled to default, which is what we - * expect. - */ -static void restore_sigpipe_to_default(void) -{ - sigset_t unblock; - - sigemptyset(&unblock); - sigaddset(&unblock, SIGPIPE); - sigprocmask(SIG_UNBLOCK, &unblock, NULL); - signal(SIGPIPE, SIG_DFL); -} +#include "common-init.h" int main(int argc, const char **argv) { int result; - struct strbuf tmp = STRBUF_INIT; - - trace2_initialize_clock(); - - /* - * Always open file descriptors 0/1/2 to avoid clobbering files - * in die(). It also avoids messing up when the pipes are dup'ed - * onto stdin/stdout/stderr in the child processes we spawn. - */ - sanitize_stdfds(); - restore_sigpipe_to_default(); - - git_resolve_executable_dir(argv[0]); - - setlocale(LC_CTYPE, ""); - git_setup_gettext(); - - initialize_repository(the_repository); - - attr_start(); - - trace2_initialize(); - trace2_cmd_start(argv); - trace2_collect_process_info(TRACE2_PROCESS_INFO_STARTUP); - - if (!strbuf_getcwd(&tmp)) - tmp_original_cwd = strbuf_detach(&tmp, NULL); + init_git(argv); result = cmd_main(argc, argv); /* Not exit(3), but a wrapper calling our common_exit() */ exit(result); } - -static void check_bug_if_BUG(void) -{ - if (!bug_called_must_BUG) - return; - BUG("on exit(): had bug() call(s) in this process without explicit BUG_if_bug()"); -} - -/* We wrap exit() to call common_exit() in git-compat-util.h */ -int common_exit(const char *file, int line, int code) -{ - /* - * For non-POSIX systems: Take the lowest 8 bits of the "code" - * to e.g. turn -1 into 255. On a POSIX system this is - * redundant, see exit(3) and wait(2), but as it doesn't harm - * anything there we don't need to guard this with an "ifdef". - */ - code &= 0xff; - - check_bug_if_BUG(); - trace2_cmd_exit_fl(file, line, code); - - return code; -} diff --git a/meson.build b/meson.build index 0064eb64f5..e5ba28b47f 100644 --- a/meson.build +++ b/meson.build @@ -245,6 +245,8 @@ libgit_sources = [ 'commit-graph.c', 'commit-reach.c', 'commit.c', + 'common-exit.c', + 'common-init.c', 'compat/nonblock.c', 'compat/obstack.c', 'compat/terminal.c', -- cgit v1.2.3 From e7f8bf125c078004232e254e71b97226c4fed81b Mon Sep 17 00:00:00 2001 From: Josh Steadmon Date: Tue, 28 Jan 2025 14:01:38 -0800 Subject: 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 Co-authored-by: Calvin Wan Signed-off-by: Calvin Wan Signed-off-by: Kyle Lippincott Signed-off-by: Josh Steadmon Signed-off-by: Junio C Hamano --- .gitignore | 1 + Makefile | 48 +++++++++++++++++++++ contrib/libgit-sys/Cargo.lock | 69 +++++++++++++++++++++++++++++++ contrib/libgit-sys/Cargo.toml | 19 +++++++++ contrib/libgit-sys/README.md | 4 ++ contrib/libgit-sys/build.rs | 35 ++++++++++++++++ contrib/libgit-sys/public_symbol_export.c | 23 +++++++++++ contrib/libgit-sys/public_symbol_export.h | 8 ++++ contrib/libgit-sys/src/lib.rs | 46 +++++++++++++++++++++ t/Makefile | 10 +++++ 10 files changed, 263 insertions(+) create mode 100644 contrib/libgit-sys/Cargo.lock create mode 100644 contrib/libgit-sys/Cargo.toml create mode 100644 contrib/libgit-sys/README.md create mode 100644 contrib/libgit-sys/build.rs create mode 100644 contrib/libgit-sys/public_symbol_export.c create mode 100644 contrib/libgit-sys/public_symbol_export.h create mode 100644 contrib/libgit-sys/src/lib.rs diff --git a/.gitignore b/.gitignore index e82aa19df0..31d7e64287 100644 --- a/.gitignore +++ b/.gitignore @@ -250,3 +250,4 @@ Release/ /git.VC.db *.dSYM /contrib/buildsystems/out +/contrib/libgit-sys/target diff --git a/Makefile b/Makefile index 27e68ac039..81091b95f4 100644 --- a/Makefile +++ b/Makefile @@ -416,6 +416,9 @@ include shared.mak # Define LINK_FUZZ_PROGRAMS if you want `make all` to also build the fuzz test # 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 === # # Define NO_GETTEXT if you don't want Git output to be translated. @@ -657,6 +660,8 @@ CURL_CONFIG = curl-config GCOV = gcov STRIP = strip SPATCH = spatch +LD = ld +OBJCOPY = objcopy export TCL_PATH TCLTK_PATH @@ -675,6 +680,7 @@ FUZZ_OBJS = FUZZ_PROGRAMS = GIT_OBJS = LIB_OBJS = +LIBGIT_PUB_OBJS = SCALAR_OBJS = OBJECTS = OTHER_PROGRAMS = @@ -2236,6 +2242,12 @@ ifdef FSMONITOR_OS_SETTINGS COMPAT_OBJS += compat/fsmonitor/fsm-path-utils-$(FSMONITOR_OS_SETTINGS).o 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),) NO_TCLTK = NoThanks endif @@ -2732,6 +2744,10 @@ OBJECTS += $(UNIT_TEST_OBJS) OBJECTS += $(CLAR_TEST_OBJS) 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 OBJECTS += http.o http-walker.o remote-curl.o endif @@ -3726,6 +3742,10 @@ clean: profile-clean coverage-clean cocciclean $(RM) $(htmldocs).tar.gz $(manpages).tar.gz $(MAKE) -C Documentation/ clean $(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 $(RM) -r perl/build/ 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) unit-tests: $(UNIT_TEST_PROGS) $(CLAR_TEST_PROG) t/helper/test-tool$X $(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) $@ $^ diff --git a/contrib/libgit-sys/Cargo.lock b/contrib/libgit-sys/Cargo.lock new file mode 100644 index 0000000000..427a4c66b7 --- /dev/null +++ b/contrib/libgit-sys/Cargo.lock @@ -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" diff --git a/contrib/libgit-sys/Cargo.toml b/contrib/libgit-sys/Cargo.toml new file mode 100644 index 0000000000..e0623022c3 --- /dev/null +++ b/contrib/libgit-sys/Cargo.toml @@ -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" diff --git a/contrib/libgit-sys/README.md b/contrib/libgit-sys/README.md new file mode 100644 index 0000000000..c061cfcaf5 --- /dev/null +++ b/contrib/libgit-sys/README.md @@ -0,0 +1,4 @@ +# libgit-sys + +A small proof-of-concept crate showing how to provide a Rust FFI to Git +internals. diff --git a/contrib/libgit-sys/build.rs b/contrib/libgit-sys/build.rs new file mode 100644 index 0000000000..3ffd80ad91 --- /dev/null +++ b/contrib/libgit-sys/build.rs @@ -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(()) +} diff --git a/contrib/libgit-sys/public_symbol_export.c b/contrib/libgit-sys/public_symbol_export.c new file mode 100644 index 0000000000..4c153c6f0d --- /dev/null +++ b/contrib/libgit-sys/public_symbol_export.c @@ -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 diff --git a/contrib/libgit-sys/public_symbol_export.h b/contrib/libgit-sys/public_symbol_export.h new file mode 100644 index 0000000000..a3372f93fa --- /dev/null +++ b/contrib/libgit-sys/public_symbol_export.h @@ -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 */ diff --git a/contrib/libgit-sys/src/lib.rs b/contrib/libgit-sys/src/lib.rs new file mode 100644 index 0000000000..d4853f3074 --- /dev/null +++ b/contrib/libgit-sys/src/lib.rs @@ -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 + ); + } +} diff --git a/t/Makefile b/t/Makefile index daa5fcae86..53ba01c21b 100644 --- a/t/Makefile +++ b/t/Makefile @@ -177,3 +177,13 @@ perf: .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \ 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 -- cgit v1.2.3 From d76eb0dcccb19d2f85924a4be177ae76126bf5d3 Mon Sep 17 00:00:00 2001 From: Josh Steadmon Date: Wed, 29 Jan 2025 13:50:43 -0800 Subject: libgit-sys: also export some config_set functions In preparation for implementing a higher-level Rust API for accessing Git configs, export some of the upstream configset API via libgitpub and libgit-sys. Since this will be exercised as part of the higher-level API in the next commit, no tests have been added for libgit-sys. While we're at it, add git_configset_alloc() and git_configset_free() functions in libgitpub so that callers can manage config_set structs on the heap. This also allows non-C external consumers to treat config_sets as opaque structs. Co-authored-by: Calvin Wan Signed-off-by: Calvin Wan Signed-off-by: Josh Steadmon Signed-off-by: Junio C Hamano --- contrib/libgit-sys/public_symbol_export.c | 36 +++++++++++++++++++++++++++++++ contrib/libgit-sys/public_symbol_export.h | 10 +++++++++ contrib/libgit-sys/src/lib.rs | 31 +++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/contrib/libgit-sys/public_symbol_export.c b/contrib/libgit-sys/public_symbol_export.c index 4c153c6f0d..dfbb257115 100644 --- a/contrib/libgit-sys/public_symbol_export.c +++ b/contrib/libgit-sys/public_symbol_export.c @@ -5,11 +5,47 @@ */ #include "git-compat-util.h" +#include "config.h" #include "contrib/libgit-sys/public_symbol_export.h" #include "version.h" #pragma GCC visibility push(default) +struct libgit_config_set { + struct config_set cs; +}; + +struct libgit_config_set *libgit_configset_alloc(void) +{ + struct libgit_config_set *cs = + xmalloc(sizeof(struct libgit_config_set)); + git_configset_init(&cs->cs); + return cs; +} + +void libgit_configset_free(struct libgit_config_set *cs) +{ + git_configset_clear(&cs->cs); + free(cs); +} + +int libgit_configset_add_file(struct libgit_config_set *cs, const char *filename) +{ + return git_configset_add_file(&cs->cs, filename); +} + +int libgit_configset_get_int(struct libgit_config_set *cs, const char *key, + int *dest) +{ + return git_configset_get_int(&cs->cs, key, dest); +} + +int libgit_configset_get_string(struct libgit_config_set *cs, const char *key, + char **dest) +{ + return git_configset_get_string(&cs->cs, key, dest); +} + const char *libgit_user_agent(void) { return git_user_agent(); diff --git a/contrib/libgit-sys/public_symbol_export.h b/contrib/libgit-sys/public_symbol_export.h index a3372f93fa..701db92d53 100644 --- a/contrib/libgit-sys/public_symbol_export.h +++ b/contrib/libgit-sys/public_symbol_export.h @@ -1,6 +1,16 @@ #ifndef PUBLIC_SYMBOL_EXPORT_H #define PUBLIC_SYMBOL_EXPORT_H +struct libgit_config_set *libgit_configset_alloc(void); + +void libgit_configset_free(struct libgit_config_set *cs); + +int libgit_configset_add_file(struct libgit_config_set *cs, const char *filename); + +int libgit_configset_get_int(struct libgit_config_set *cs, const char *key, int *dest); + +int libgit_configset_get_string(struct libgit_config_set *cs, const char *key, char **dest); + const char *libgit_user_agent(void); const char *libgit_user_agent_sanitized(void); diff --git a/contrib/libgit-sys/src/lib.rs b/contrib/libgit-sys/src/lib.rs index d4853f3074..dadb4e5f40 100644 --- a/contrib/libgit-sys/src/lib.rs +++ b/contrib/libgit-sys/src/lib.rs @@ -1,15 +1,44 @@ #[cfg(has_std__ffi__c_char)] -use std::ffi::c_char; +use std::ffi::{c_char, c_int}; #[cfg(not(has_std__ffi__c_char))] #[allow(non_camel_case_types)] pub type c_char = i8; +#[cfg(not(has_std__ffi__c_char))] +#[allow(non_camel_case_types)] +pub type c_int = i32; + extern crate libz_sys; +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct libgit_config_set { + _data: [u8; 0], + _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>, +} + extern "C" { pub fn libgit_user_agent() -> *const c_char; pub fn libgit_user_agent_sanitized() -> *const c_char; + + pub fn libgit_configset_alloc() -> *mut libgit_config_set; + pub fn libgit_configset_free(cs: *mut libgit_config_set); + + pub fn libgit_configset_add_file(cs: *mut libgit_config_set, filename: *const c_char) -> c_int; + + pub fn libgit_configset_get_int( + cs: *mut libgit_config_set, + key: *const c_char, + int: *mut c_int, + ) -> c_int; + + pub fn libgit_configset_get_string( + cs: *mut libgit_config_set, + key: *const c_char, + dest: *mut *mut c_char, + ) -> c_int; + } #[cfg(test)] -- cgit v1.2.3 From 65c10aa8d5000e0ecab34a9652056f0520fe51ed Mon Sep 17 00:00:00 2001 From: Calvin Wan Date: Wed, 29 Jan 2025 13:50:44 -0800 Subject: libgit: add higher-level libgit crate The C functions exported by libgit-sys do not provide an idiomatic Rust interface. To make it easier to use these functions via Rust, add a higher-level "libgit" crate, that wraps the lower-level configset API with an interface that is more Rust-y. This combination of $X and $X-sys crates is a common pattern for FFI in Rust, as documented in "The Cargo Book" [1]. [1] https://doc.rust-lang.org/cargo/reference/build-scripts.html#-sys-packages Co-authored-by: Josh Steadmon Signed-off-by: Josh Steadmon Signed-off-by: Calvin Wan Signed-off-by: Junio C Hamano --- .gitignore | 1 + Makefile | 12 ++--- contrib/libgit-rs/Cargo.lock | 77 +++++++++++++++++++++++++++ contrib/libgit-rs/Cargo.toml | 17 ++++++ contrib/libgit-rs/README.md | 13 +++++ contrib/libgit-rs/build.rs | 4 ++ contrib/libgit-rs/src/config.rs | 106 +++++++++++++++++++++++++++++++++++++ contrib/libgit-rs/src/lib.rs | 1 + contrib/libgit-rs/testdata/config1 | 2 + contrib/libgit-rs/testdata/config2 | 2 + contrib/libgit-rs/testdata/config3 | 2 + contrib/libgit-sys/src/lib.rs | 4 ++ t/Makefile | 9 +++- 13 files changed, 242 insertions(+), 8 deletions(-) create mode 100644 contrib/libgit-rs/Cargo.lock create mode 100644 contrib/libgit-rs/Cargo.toml create mode 100644 contrib/libgit-rs/README.md create mode 100644 contrib/libgit-rs/build.rs create mode 100644 contrib/libgit-rs/src/config.rs create mode 100644 contrib/libgit-rs/src/lib.rs create mode 100644 contrib/libgit-rs/testdata/config1 create mode 100644 contrib/libgit-rs/testdata/config2 create mode 100644 contrib/libgit-rs/testdata/config3 diff --git a/.gitignore b/.gitignore index 31d7e64287..acdd8ce7c7 100644 --- a/.gitignore +++ b/.gitignore @@ -250,4 +250,5 @@ Release/ /git.VC.db *.dSYM /contrib/buildsystems/out +/contrib/libgit-rs/target /contrib/libgit-sys/target diff --git a/Makefile b/Makefile index 81091b95f4..c9c54b6ab0 100644 --- a/Makefile +++ b/Makefile @@ -417,7 +417,7 @@ include shared.mak # 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. +# test the Rust crates in contrib/libgit-sys and contrib/libgit-rs. # # === Optional library: libintl === # @@ -3742,7 +3742,7 @@ clean: profile-clean coverage-clean cocciclean $(RM) $(htmldocs).tar.gz $(manpages).tar.gz $(MAKE) -C Documentation/ clean $(RM) Documentation/GIT-EXCLUDED-PROGRAMS - $(RM) -r contrib/libgit-sys/target + $(RM) -r contrib/libgit-sys/target contrib/libgit-rs/target $(RM) contrib/libgit-sys/partial_symbol_export.o $(RM) contrib/libgit-sys/hidden_symbol_export.o $(RM) contrib/libgit-sys/libgitpub.a @@ -3908,14 +3908,14 @@ build-unit-tests: $(UNIT_TEST_PROGS) $(CLAR_TEST_PROG) unit-tests: $(UNIT_TEST_PROGS) $(CLAR_TEST_PROG) t/helper/test-tool$X $(MAKE) -C t/ unit-tests -.PHONY: libgit-sys -libgit-sys: +.PHONY: libgit-sys libgit-rs +libgit-sys libgit-rs: $(QUIET)(\ - cd contrib/libgit-sys && \ + cd contrib/$@ && \ cargo build \ ) ifdef INCLUDE_LIBGIT_RS -all:: libgit-sys +all:: libgit-sys libgit-rs endif LIBGIT_PUB_OBJS += contrib/libgit-sys/public_symbol_export.o diff --git a/contrib/libgit-rs/Cargo.lock b/contrib/libgit-rs/Cargo.lock new file mode 100644 index 0000000000..a30c7c8d33 --- /dev/null +++ b/contrib/libgit-rs/Cargo.lock @@ -0,0 +1,77 @@ +# 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" +version = "0.1.0" +dependencies = [ + "autocfg", + "libgit-sys", +] + +[[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" diff --git a/contrib/libgit-rs/Cargo.toml b/contrib/libgit-rs/Cargo.toml new file mode 100644 index 0000000000..c3289e69db --- /dev/null +++ b/contrib/libgit-rs/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "libgit" +version = "0.1.0" +edition = "2021" +build = "build.rs" +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/ + + +[lib] +path = "src/lib.rs" + +[dependencies] +libgit-sys = { version = "0.1.0", path = "../libgit-sys" } + +[build-dependencies] +autocfg = "1.4.0" diff --git a/contrib/libgit-rs/README.md b/contrib/libgit-rs/README.md new file mode 100644 index 0000000000..ff945e1ce2 --- /dev/null +++ b/contrib/libgit-rs/README.md @@ -0,0 +1,13 @@ +# libgit-rs + +Proof-of-concept Git bindings for Rust. + +```toml +[dependencies] +libgit = "0.1.0" +``` + +## Rust version requirements + +libgit-rs should support Rust versions at least as old as the version included +in Debian stable (currently 1.63). diff --git a/contrib/libgit-rs/build.rs b/contrib/libgit-rs/build.rs new file mode 100644 index 0000000000..f8bd01a690 --- /dev/null +++ b/contrib/libgit-rs/build.rs @@ -0,0 +1,4 @@ +pub fn main() { + let ac = autocfg::new(); + ac.emit_has_path("std::ffi::c_char"); +} diff --git a/contrib/libgit-rs/src/config.rs b/contrib/libgit-rs/src/config.rs new file mode 100644 index 0000000000..6bf04845c8 --- /dev/null +++ b/contrib/libgit-rs/src/config.rs @@ -0,0 +1,106 @@ +use std::ffi::{c_void, CStr, CString}; +use std::path::Path; + +#[cfg(has_std__ffi__c_char)] +use std::ffi::{c_char, c_int}; + +#[cfg(not(has_std__ffi__c_char))] +#[allow(non_camel_case_types)] +type c_char = i8; + +#[cfg(not(has_std__ffi__c_char))] +#[allow(non_camel_case_types)] +type c_int = i32; + +use libgit_sys::*; + +/// A ConfigSet is an in-memory cache for config-like files such as `.gitmodules` or `.gitconfig`. +/// It does not support all config directives; notably, it will not process `include` or +/// `includeIf` directives (but it will store them so that callers can choose whether and how to +/// handle them). +pub struct ConfigSet(*mut libgit_config_set); +impl ConfigSet { + /// Allocate a new ConfigSet + pub fn new() -> Self { + unsafe { ConfigSet(libgit_configset_alloc()) } + } + + /// Load the given files into the ConfigSet; conflicting directives in later files will + /// override those given in earlier files. + pub fn add_files(&mut self, files: &[&Path]) { + for file in files { + let pstr = file.to_str().expect("Invalid UTF-8"); + let rs = CString::new(pstr).expect("Couldn't convert to CString"); + unsafe { + libgit_configset_add_file(self.0, rs.as_ptr()); + } + } + } + + /// Load the value for the given key and attempt to parse it as an i32. Dies with a fatal error + /// if the value cannot be parsed. Returns None if the key is not present. + pub fn get_int(&mut self, key: &str) -> Option { + let key = CString::new(key).expect("Couldn't convert to CString"); + let mut val: c_int = 0; + unsafe { + if libgit_configset_get_int(self.0, key.as_ptr(), &mut val as *mut c_int) != 0 { + return None; + } + } + + Some(val.into()) + } + + /// Clones the value for the given key. Dies with a fatal error if the value cannot be + /// converted to a String. Returns None if the key is not present. + pub fn get_string(&mut self, key: &str) -> Option { + let key = CString::new(key).expect("Couldn't convert key to CString"); + let mut val: *mut c_char = std::ptr::null_mut(); + unsafe { + if libgit_configset_get_string(self.0, key.as_ptr(), &mut val as *mut *mut c_char) != 0 + { + return None; + } + let borrowed_str = CStr::from_ptr(val); + let owned_str = + String::from(borrowed_str.to_str().expect("Couldn't convert val to str")); + free(val as *mut c_void); // Free the xstrdup()ed pointer from the C side + Some(owned_str) + } + } +} + +impl Default for ConfigSet { + fn default() -> Self { + Self::new() + } +} + +impl Drop for ConfigSet { + fn drop(&mut self) { + unsafe { + libgit_configset_free(self.0); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn load_configs_via_configset() { + let mut cs = ConfigSet::new(); + cs.add_files(&[ + Path::new("testdata/config1"), + Path::new("testdata/config2"), + Path::new("testdata/config3"), + ]); + // ConfigSet retrieves correct value + assert_eq!(cs.get_int("trace2.eventTarget"), Some(1)); + // ConfigSet respects last config value set + assert_eq!(cs.get_int("trace2.eventNesting"), Some(3)); + // ConfigSet returns None for missing key + assert_eq!(cs.get_string("foo.bar"), None); + } +} diff --git a/contrib/libgit-rs/src/lib.rs b/contrib/libgit-rs/src/lib.rs new file mode 100644 index 0000000000..ef68c36943 --- /dev/null +++ b/contrib/libgit-rs/src/lib.rs @@ -0,0 +1 @@ +pub mod config; diff --git a/contrib/libgit-rs/testdata/config1 b/contrib/libgit-rs/testdata/config1 new file mode 100644 index 0000000000..4e9a9d25d1 --- /dev/null +++ b/contrib/libgit-rs/testdata/config1 @@ -0,0 +1,2 @@ +[trace2] + eventNesting = 1 diff --git a/contrib/libgit-rs/testdata/config2 b/contrib/libgit-rs/testdata/config2 new file mode 100644 index 0000000000..b8d1eca423 --- /dev/null +++ b/contrib/libgit-rs/testdata/config2 @@ -0,0 +1,2 @@ +[trace2] + eventTarget = 1 diff --git a/contrib/libgit-rs/testdata/config3 b/contrib/libgit-rs/testdata/config3 new file mode 100644 index 0000000000..ca7b9a7c38 --- /dev/null +++ b/contrib/libgit-rs/testdata/config3 @@ -0,0 +1,2 @@ +[trace2] + eventNesting = 3 diff --git a/contrib/libgit-sys/src/lib.rs b/contrib/libgit-sys/src/lib.rs index dadb4e5f40..4bfc650450 100644 --- a/contrib/libgit-sys/src/lib.rs +++ b/contrib/libgit-sys/src/lib.rs @@ -1,3 +1,5 @@ +use std::ffi::c_void; + #[cfg(has_std__ffi__c_char)] use std::ffi::{c_char, c_int}; @@ -19,6 +21,8 @@ pub struct libgit_config_set { } extern "C" { + pub fn free(ptr: *mut c_void); + pub fn libgit_user_agent() -> *const c_char; pub fn libgit_user_agent_sanitized() -> *const c_char; diff --git a/t/Makefile b/t/Makefile index 53ba01c21b..2994eb5fa9 100644 --- a/t/Makefile +++ b/t/Makefile @@ -178,12 +178,17 @@ perf: .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \ check-chainlint clean-chainlint test-chainlint $(UNIT_TESTS) -.PHONY: libgit-sys-test +.PHONY: libgit-sys-test libgit-rs-test libgit-sys-test: $(QUIET)(\ cd ../contrib/libgit-sys && \ cargo test \ ) +libgit-rs-test: + $(QUIET)(\ + cd ../contrib/libgit-rs && \ + cargo test \ + ) ifdef INCLUDE_LIBGIT_RS -all:: libgit-sys-test +all:: libgit-sys-test libgit-rs-test endif -- cgit v1.2.3