diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01df302..8051499 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,12 +15,14 @@ jobs: matrix: include: # ── Debian stable ────────────────────────────────────────────────── + # static: true — libgit2-dev ships libgit2.a on Debian - os: debian:stable runner: ubuntu-latest compiler: gcc cc: gcc cxx: g++ build_type: Release + static: true - os: debian:stable runner: ubuntu-latest @@ -28,6 +30,7 @@ jobs: cc: gcc cxx: g++ build_type: Debug + static: true - os: debian:stable runner: ubuntu-latest @@ -35,6 +38,7 @@ jobs: cc: clang cxx: clang++ build_type: Release + static: true - os: debian:stable runner: ubuntu-latest @@ -42,14 +46,17 @@ jobs: cc: clang cxx: clang++ build_type: Debug + static: true # ── Ubuntu LTS (24.04) ───────────────────────────────────────────── + # static: true — libgit2-dev ships libgit2.a on Ubuntu - os: ubuntu:24.04 runner: ubuntu-latest compiler: gcc cc: gcc cxx: g++ build_type: Release + static: true - os: ubuntu:24.04 runner: ubuntu-latest @@ -57,6 +64,7 @@ jobs: cc: gcc cxx: g++ build_type: Debug + static: true - os: ubuntu:24.04 runner: ubuntu-latest @@ -64,6 +72,7 @@ jobs: cc: clang cxx: clang++ build_type: Release + static: true - os: ubuntu:24.04 runner: ubuntu-latest @@ -71,14 +80,17 @@ jobs: cc: clang cxx: clang++ build_type: Debug + static: true # ── Fedora (latest) ─────────────────────────────────────────────────── + # static: false — Fedora does not ship libgit2.a / libssh2.a / libssl.a - os: fedora:latest runner: ubuntu-latest compiler: gcc cc: gcc cxx: g++ build_type: Release + static: false - os: fedora:latest runner: ubuntu-latest @@ -86,6 +98,7 @@ jobs: cc: gcc cxx: g++ build_type: Debug + static: false - os: fedora:latest runner: ubuntu-latest @@ -93,6 +106,7 @@ jobs: cc: clang cxx: clang++ build_type: Release + static: false - os: fedora:latest runner: ubuntu-latest @@ -100,14 +114,17 @@ jobs: cc: clang cxx: clang++ build_type: Debug + static: false # ── Arch Linux (stable) ─────────────────────────────────────────────── + # static: false — Arch does not ship libgit2.a - os: archlinux:latest runner: ubuntu-latest compiler: gcc cc: gcc cxx: g++ build_type: Release + static: false - os: archlinux:latest runner: ubuntu-latest @@ -115,6 +132,7 @@ jobs: cc: gcc cxx: g++ build_type: Debug + static: false - os: archlinux:latest runner: ubuntu-latest @@ -122,6 +140,7 @@ jobs: cc: clang cxx: clang++ build_type: Release + static: false - os: archlinux:latest runner: ubuntu-latest @@ -129,6 +148,7 @@ jobs: cc: clang cxx: clang++ build_type: Debug + static: false runs-on: ${{ matrix.runner }} @@ -147,7 +167,7 @@ jobs: - name: Cache CMake FetchContent (spdlog, clipp, fmt) uses: actions/cache@v5 with: - path: build/_deps + path: build-so/_deps key: fetchcontent-${{ matrix.os }}-${{ matrix.compiler }}-${{ hashFiles('CMakeLists.txt') }} restore-keys: | fetchcontent-${{ matrix.os }}-${{ matrix.compiler }}- @@ -159,23 +179,46 @@ jobs: git config --global user.name "GitHub Actions" git config --global init.defaultBranch master - - name: Build + - name: Build dynamic + env: + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} + CI: "1" + run: make BUILD=build-so TYPE=${{ matrix.build_type }} + + - name: Test dynamic + env: + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} + CI: "1" + run: make BUILD=build-so TYPE=${{ matrix.build_type }} test + + - name: Install static dependencies + if: matrix.static + env: + DEBIAN_FRONTEND: noninteractive + run: bash dependencies.sh --compiler=${{ matrix.compiler }} --static + + - name: Build static + if: matrix.static env: CC: ${{ matrix.cc }} CXX: ${{ matrix.cxx }} CI: "1" - run: make TYPE=${{ matrix.build_type }} + run: make BUILD=build-a STATIC=1 TYPE=${{ matrix.build_type }} - - name: Test + - name: Test static + if: matrix.static env: CC: ${{ matrix.cc }} CXX: ${{ matrix.cxx }} CI: "1" - run: make TYPE=${{ matrix.build_type }} test + run: make BUILD=build-a STATIC=1 TYPE=${{ matrix.build_type }} test - name: Compute artifact name if: failure() id: artifact-name + shell: bash run: | # Colons and slashes are not allowed in artifact names; replace with dashes. raw="test-artifacts-${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.build_type }}" @@ -187,8 +230,10 @@ jobs: with: name: ${{ steps.artifact-name.outputs.name }} path: | - build/test/ - build/Testing/ + build-so/test/ + build-so/Testing/ + build-a/test/ + build-a/Testing/ retention-days: 7 coverage: diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e929db..def2761 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) option(WIP_COVERAGE "Enable code coverage instrumentation" OFF) +option(WIP_STATIC "Build a fully static binary" OFF) include(FetchContent) include(CheckCXXSourceCompiles) @@ -51,7 +52,47 @@ endif() include(FindPkgConfig) -pkg_check_modules(LIBGIT2 REQUIRED libgit2) +if(WIP_STATIC) + message(STATUS "WIP_STATIC=ON — building a mostly-static binary (libgit2 and all its deps statically linked; glibc stays shared)") + pkg_check_modules(LIBGIT2 REQUIRED libgit2) + + # Collect the full transitive static link flags from pkg-config. + # We split them into two groups so CMake can handle each token cleanly: + # LIBGIT2_STATIC_LIBS — -lfoo tokens (library names, no -L prefix) + # LIBGIT2_STATIC_DIRS — -L/path tokens (search paths) + execute_process( + COMMAND pkg-config --static --libs libgit2 + OUTPUT_VARIABLE _git2_raw + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + execute_process( + COMMAND pkg-config --static --libs-only-l libgit2 + OUTPUT_VARIABLE _git2_libs_raw + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + execute_process( + COMMAND pkg-config --static --libs-only-L libgit2 + OUTPUT_VARIABLE _git2_dirs_raw + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + # Also pull in the transitive deps of libssh2 (uses OpenSSL on Debian) + execute_process( + COMMAND pkg-config --static --libs-only-l libssh2 + OUTPUT_VARIABLE _ssh2_libs_raw + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + string(REPLACE "-l" "" _git2_libs_stripped "${_git2_libs_raw}") + string(REPLACE "-L" "" _git2_dirs_stripped "${_git2_dirs_raw}") + string(REPLACE "-l" "" _ssh2_libs_stripped "${_ssh2_libs_raw}") + separate_arguments(LIBGIT2_STATIC_LIBS UNIX_COMMAND "${_git2_libs_stripped}") + separate_arguments(LIBGIT2_STATIC_DIRS UNIX_COMMAND "${_git2_dirs_stripped}") + separate_arguments(SSH2_STATIC_LIBS UNIX_COMMAND "${_ssh2_libs_stripped}") + # Deduplicate (SSH2 libs are a subset) + list(APPEND LIBGIT2_STATIC_LIBS ${SSH2_STATIC_LIBS}) + list(REMOVE_DUPLICATES LIBGIT2_STATIC_LIBS) +else() + pkg_check_modules(LIBGIT2 REQUIRED libgit2) +endif() # Optional: GTest/GMock for unit tests (only available on some distros) find_package(GTest QUIET) diff --git a/Makefile b/Makefile index 03daccc..e3196f0 100644 --- a/Makefile +++ b/Makefile @@ -32,21 +32,25 @@ NPROC ?= $(shell nproc || echo 1) CC ?= $(shell which clang gcc cc | head -n1) CXX ?= $(shell which clang g++ c++ | head -n1) COVERAGE ?= false +STATIC ?= 0 # Locate the right gcov-compatible tool to match the compiler: # - if CC is clang, use llvm-cov (prefer plain symlink, fall back to versioned) # - otherwise use plain gcov # Use = (recursive) not := (immediate) so CC override on the command line is respected. _IS_CLANG = $(shell $(CC) --version 2>/dev/null | grep -c clang) GCOV_TOOL = $(if $(filter 1,$(_IS_CLANG)),$(shell which llvm-cov 2>/dev/null || ls /usr/bin/llvm-cov-* 2>/dev/null | sort -V | tail -1) gcov,gcov) -$(info ## TYPE=${TYPE} CC=${CC} CXX=${CXX} COVERAGE=${COVERAGE}) +$(info ## TYPE=${TYPE} CC=${CC} CXX=${CXX} COVERAGE=${COVERAGE} STATIC=${STATIC}) # Coverage flag for CMake COVERAGE_FLAG = $(if $(filter 1 yes true YES TRUE,${COVERAGE}),-DWIP_COVERAGE=ON,) +# Static flag for CMake +STATIC_FLAG = $(if $(filter 1 yes true YES TRUE,${STATIC}),-DWIP_STATIC=ON,) + GIT_WIP = ${BUILD}/src/git-wip -all: ## [default] build the project (uses TYPE={Release,Debug}, COVERAGE={true,false}) - ${Q}${CMAKE} -G ${GENERATOR} -S. -B${BUILD} -DCMAKE_INSTALL_PREFIX="$(PREFIX)" -DCMAKE_BUILD_TYPE="${TYPE}" ${COVERAGE_FLAG} +all: ## [default] build the project (uses TYPE={Release,Debug}, COVERAGE={0,1}, STATIC={0,1}) + ${Q}${CMAKE} -G ${GENERATOR} -S. -B${BUILD} -DCMAKE_INSTALL_PREFIX="$(PREFIX)" -DCMAKE_BUILD_TYPE="${TYPE}" ${COVERAGE_FLAG} ${STATIC_FLAG} ${Q}${CMAKE} --build "${BUILD}" --config "${TYPE}" --parallel "${NPROC}" ${Q}ln -fs "${BUILD}"/compile_commands.json compile_commands.json ${Q}ln -fs "${GIT_WIP}" . @@ -60,14 +64,14 @@ distclean: ## remove build directory completely help: ${Q}python3 -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) -test: ## run unit tests (with ctest, uses REBUILD={true,false}, COVERAGE={true,false}) +test: ## run unit tests (with ctest, uses REBUILD={0,1}, COVERAGE={0,1}, STATIC={0,1}) ${Q}$(if $(filter 1 yes true YES TRUE,${REBUILD}),rm -rf "${BUILD}"/) - ${Q}${CMAKE} -G ${GENERATOR} -S. -B${BUILD} -DCMAKE_INSTALL_PREFIX="$(PREFIX)" -DCMAKE_BUILD_TYPE="${TYPE}" ${COVERAGE_FLAG} + ${Q}${CMAKE} -G ${GENERATOR} -S. -B${BUILD} -DCMAKE_INSTALL_PREFIX="$(PREFIX)" -DCMAKE_BUILD_TYPE="${TYPE}" ${COVERAGE_FLAG} ${STATIC_FLAG} ${Q}${CMAKE} --build "${BUILD}" --config "${TYPE}" --parallel "${NPROC}" ${Q}cd "${BUILD}"/ && ctest -C "${TYPE}" $(if ${CI},--output-on-failure -VV) ${Q}echo " ✅ Unit tests complete." -coverage: ## check code coverage (with lcov, uses REBUILD={true,false}) +coverage: ## check code coverage (with lcov, uses REBUILD={0,1}) ${Q}$(if $(filter 1 yes true YES TRUE,${REBUILD}),rm -rf "${BUILD}"/) ${Q}${CMAKE} -G ${GENERATOR} -S. -B${BUILD} -DCMAKE_INSTALL_PREFIX="$(PREFIX)" -DCMAKE_BUILD_TYPE="${TYPE}" -DWIP_COVERAGE=ON -DCMAKE_C_COMPILER="${CC}" -DCMAKE_CXX_COMPILER="${CXX}" ${Q}${CMAKE} --build "${BUILD}" --config "${TYPE}" --parallel "${NPROC}" @@ -97,15 +101,15 @@ coverage: ## check code coverage (with lcov, uses REBUILD={true,false}) --ignore-errors category ${Q}echo " ✅ Coverage report generated in coverage-report/" -install: ## install the package (to the `PREFIX`, uses REBUILD={true,false}) +install: ## install the package (to the `PREFIX`, uses REBUILD={0,1}, STATIC={0,1}) ${Q}$(if $(filter 1 yes true YES TRUE,${REBUILD}),rm -rf "${BUILD}"/) - ${Q}${CMAKE} -G ${GENERATOR} -S. -B${BUILD} -DCMAKE_INSTALL_PREFIX="$(PREFIX)" -DCMAKE_BUILD_TYPE="${TYPE}" + ${Q}${CMAKE} -G ${GENERATOR} -S. -B${BUILD} -DCMAKE_INSTALL_PREFIX="$(PREFIX)" -DCMAKE_BUILD_TYPE="${TYPE}" ${STATIC_FLAG} ${Q}${CMAKE} --build "${BUILD}" --config "${TYPE}" --parallel "${NPROC}" ${Q}${CMAKE} --build "${BUILD}" --target install --config "${TYPE}" -format: ## format the project sources (uses REBUILD={true,false}) +format: ## format the project sources (uses REBUILD={0,1}) ${Q}$(if $(filter 1 yes true YES TRUE,${REBUILD}),rm -rf "${BUILD}"/) - ${Q}${CMAKE} -G ${GENERATOR} -S. -B${BUILD} -DCMAKE_INSTALL_PREFIX="$(PREFIX)" -DCMAKE_BUILD_TYPE="${TYPE}" + ${Q}${CMAKE} -G ${GENERATOR} -S. -B${BUILD} -DCMAKE_INSTALL_PREFIX="$(PREFIX)" -DCMAKE_BUILD_TYPE="${TYPE}" ${STATIC_FLAG} ${Q}${CMAKE} --build "${BUILD}" --target clang-format full-test: ## like test, but with a full rebuild diff --git a/README.md b/README.md index a0f9078..c76153c 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,13 @@ Dependencies (`spdlog`) are fetched automatically by CMake via `FetchContent`. `libgit2` must be installed system-wide (e.g. `apt install libgit2-dev`). +If you'd rather build a static binary (tested on Debian/Ubuntu), you can use: + +```sh +$ ./dependencies.sh --static # install extra dependencies +$ make STATIC=1 # build the static binary +``` + --- ## Installation diff --git a/dependencies.sh b/dependencies.sh index 03b1c73..b1cc6a5 100755 --- a/dependencies.sh +++ b/dependencies.sh @@ -58,13 +58,15 @@ function must_have_one_of() { for n in "${pkg_names[@]}" ; do case "$pkg_mgr" in apt) - if apt policy "$n" >/dev/null 2>&1 ; then + # apt-cache show exits non-zero when the package is unknown; + # apt policy always exits 0 so it cannot be used for existence checks. + if apt-cache show "$n" >/dev/null 2>&1 ; then echo "$n" return fi ;; dnf) - if dnf list installed "$n" >/dev/null 2>&1 ; then + if dnf list available "$n" >/dev/null 2>&1 ; then echo "$n" return fi @@ -86,13 +88,15 @@ function want_one_of() { for n in "${pkg_names[@]}" ; do case "$pkg_mgr" in apt) - if apt policy "$n" >/dev/null 2>&1 ; then + # apt-cache show exits non-zero when the package is unknown; + # apt policy always exits 0 so it cannot be used for existence checks. + if apt-cache show "$n" >/dev/null 2>&1 ; then echo "$n" return fi ;; dnf) - if dnf list installed "$n" >/dev/null 2>&1 ; then + if dnf list available "$n" >/dev/null 2>&1 ; then echo "$n" return fi @@ -114,6 +118,7 @@ function want_one_of() { compiler="" # empty → auto-select via must_have_one_of coverage=0 # --coverage → install lcov, curl, gpg +static=0 # --static → install static libs needed for STATIC=1 builds for arg in "$@" ; do case "$arg" in @@ -125,9 +130,11 @@ for arg in "$@" ; do die "unknown --compiler value '${arg#--compiler=}' (expected gcc or clang)" ;; --coverage) coverage=1 ;; + --static) + static=1 ;; -h|--help) cat <<'EOF' -Usage: dependencies.sh [--compiler=] [--coverage] [-h|--help] +Usage: dependencies.sh [--compiler=] [--coverage] [--static] [-h|--help] Install build dependencies for git-wip. @@ -138,6 +145,9 @@ Options: --coverage Also install coverage tools (lcov, curl, gpg) + --static Also install static libraries required for `make STATIC=1` + (libllhttp-dev and any other missing static .a files) + -h, --help Show this help and exit EOF exit 0 @@ -250,6 +260,37 @@ if [ "$coverage" = 1 ]; then esac fi +# Static-build extra libs (only when --static is requested) +# Most static .a files come from the -dev packages already installed above. +# The extras needed on Debian/Ubuntu: +# libgpg-error-dev → libgpg-error.a (libssh2 transitive dep) +# libzstd-dev → libzstd.a (libssh2 transitive dep) +# libkrb5-dev → provides libgssapi_krb5.so stubs for the dynamic link +# libllhttp-dev → libllhttp.a (libgit2 HTTP parser) +# Present on Debian stable; Ubuntu 24.04 (noble) embeds +# llhttp statically inside libgit2.a so the package does +# not exist there — detected and skipped automatically. +# Fedora and Arch do not ship libgit2.a / libssh2.a / libssl.a, so the static +# build is not supported on those distros; nothing extra to install. +if [ "$static" = 1 ]; then + case "$pkg_mgr" in + apt) + packages+=( libgpg-error-dev libzstd-dev libkrb5-dev ) + # libllhttp-dev is optional — present on Debian stable, absent on + # Ubuntu 24.04 (noble embeds llhttp statically inside libgit2.a) + llhttp_pkg=$(want_one_of libllhttp-dev) + [ -n "$llhttp_pkg" ] && packages+=( "$llhttp_pkg" ) + ;; + dnf) + # Fedora does not ship libgit2.a / libssh2.a / libssl.a, so a + # fully static build is not supported there. Nothing to install. + ;; + pacman) + # Arch does not ship libgit2.a either; nothing to install. + ;; + esac +fi + set -e -x case "$pkg_mgr" in diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2325093..23efe7c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,14 +24,51 @@ target_include_directories(git-wip PRIVATE ${LIBGIT2_INCLUDE_DIRS} ) -target_link_libraries(git-wip PRIVATE - spdlog::spdlog - ${LIBGIT2_LIBRARIES} -) - -target_link_directories(git-wip PRIVATE - ${LIBGIT2_LIBRARY_DIRS} -) +if(WIP_STATIC) + # Link libgit2 and all its transitive deps as static archives. + # Strategy: + # 1. -Wl,-Bstatic wraps the static block. + # 2. --start-group / --end-group around all static libs resolves any + # circular or out-of-order symbol dependencies (e.g. libcrypto → libz). + # 3. -Wl,-Bdynamic switches back to dynamic for the remainder + # (glibc, gssapi_krb5 and its kerberos chain have no static .a here). + # 4. GSSAPI / krb5 are added explicitly as dynamic-only after the switch. + # + # spdlog is always a static archive (built by FetchContent) and does NOT + # need to be inside -Bstatic because CMake links it by full path anyway. + target_link_directories(git-wip PRIVATE + ${LIBGIT2_STATIC_DIRS} + ) + set(_static_lib_args + "-Wl,-Bstatic" + "-Wl,--start-group" + ) + foreach(_lib IN LISTS LIBGIT2_STATIC_LIBS) + list(APPEND _static_lib_args "-l${_lib}") + endforeach() + list(APPEND _static_lib_args + "-Wl,--end-group" + "-Wl,-Bdynamic" + # GSSAPI / Kerberos — only available as shared libs on most distros + "-lgssapi_krb5" + "-lkrb5" + "-lk5crypto" + "-lkrb5support" + "-lcom_err" + ) + target_link_libraries(git-wip PRIVATE + spdlog::spdlog + ${_static_lib_args} + ) +else() + target_link_libraries(git-wip PRIVATE + spdlog::spdlog + ${LIBGIT2_LIBRARIES} + ) + target_link_directories(git-wip PRIVATE + ${LIBGIT2_LIBRARY_DIRS} + ) +endif() if(WIP_HAVE_STD_PRINT) target_compile_definitions(git-wip PRIVATE WIP_HAVE_STD_PRINT)