From 31831ff0757a38ce03a7b715b70db87c132e6b63 Mon Sep 17 00:00:00 2001 From: Eddy Moulton Date: Thu, 4 Jun 2026 16:44:00 +1000 Subject: [PATCH 01/11] Add smoke test for windows to replicate the issue --- .github/workflows/ci.yml | 33 +++++++++++++++++++++++++++++++-- smoke-test/windows/Dockerfile | 6 ++++++ smoke-test/windows/loader.c | 31 +++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 smoke-test/windows/Dockerfile create mode 100644 smoke-test/windows/loader.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbf470cc..8610359e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,6 +99,35 @@ jobs: with: name: ${{ matrix.name }} path: nuget.package/runtimes/${{ matrix.rid }} + smoke-windows: + name: smoke-win-x64 + needs: build + runs-on: windows-2022 + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + - name: Download win-x64 artifact + uses: actions/download-artifact@v8.0.1 + with: + name: win-x64 + path: smoke-test/windows + - name: Compute libgit2 DLL name + id: dll + shell: bash + run: | + SHA=$(cat nuget.package/libgit2/libgit2_hash.txt) + echo "name=git2-${SHA:0:7}.dll" >> "$GITHUB_OUTPUT" + - name: Set up MSVC + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + - name: Build static-CRT loader + shell: cmd + run: cl /MT /O2 smoke-test\windows\loader.c /Fe:smoke-test\windows\loader.exe + - name: Build bare-Windows smoke image + run: docker build -t libgit2-smoke -f smoke-test/windows/Dockerfile smoke-test/windows + - name: Load git2 on bare Windows (no VC++ redist) + run: docker run --rm libgit2-smoke C:\native\${{ steps.dll.outputs.name }} package: name: Create package needs: build @@ -165,12 +194,12 @@ jobs: run: dotnet nuget push ./nuget.package/*.nupkg --api-key "$FEED_API_KEY" --source "$FEED_SOURCE" ci: name: ci - needs: [build, package] + needs: [build, package, smoke-windows] runs-on: ubuntu-24.04 steps: - name: Check results run: | - if [ "${{ needs.build.result }}" != "success" ] || [ "${{ needs.package.result }}" != "success" ]; then + if [ "${{ needs.build.result }}" != "success" ] || [ "${{ needs.package.result }}" != "success" ] || [ "${{ needs.smoke-windows.result }}" != "success" ]; then echo "One or more jobs failed" exit 1 fi diff --git a/smoke-test/windows/Dockerfile b/smoke-test/windows/Dockerfile new file mode 100644 index 00000000..b2c28a81 --- /dev/null +++ b/smoke-test/windows/Dockerfile @@ -0,0 +1,6 @@ +# Bare Windows: nanoserver ships the UCRT but NOT the VC++ Redistributable, +# so it is the clean-VM analogue for MD-2027. +FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 +COPY native/ C:/native/ +COPY loader.exe C:/loader.exe +ENTRYPOINT ["C:\\loader.exe"] diff --git a/smoke-test/windows/loader.c b/smoke-test/windows/loader.c new file mode 100644 index 00000000..850f1ead --- /dev/null +++ b/smoke-test/windows/loader.c @@ -0,0 +1,31 @@ +/* LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR makes the loader resolve git2's sibling DLLs + * from git2's own directory (where libssh2.dll/z.dll live in the broken build), + * so a missing transitive dep (VCRUNTIME140.dll) surfaces as ERROR_MOD_NOT_FOUND. + */ +#include +#include + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "usage: loader \n"); + return 2; + } + + HMODULE h = LoadLibraryExA(argv[1], NULL, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR); + if (!h) { + DWORD e = GetLastError(); + fprintf(stderr, "LoadLibraryEx('%s') failed: error %lu (0x%08lX)\n", argv[1], e, e); + return 1; + } + + FARPROC p = GetProcAddress(h, "git_libgit2_init"); + if (!p) { + fprintf(stderr, "GetProcAddress(git_libgit2_init) failed: %lu\n", GetLastError()); + return 1; + } + + int rc = ((int (*)(void))p)(); + printf("git_libgit2_init() returned %d\n", rc); + /* git_libgit2_init returns the initialization count (>=1) on success. */ + return rc > 0 ? 0 : 1; +} From d7f9593b859701b0e14e373483f65d3e7964a168 Mon Sep 17 00:00:00 2001 From: Eddy Moulton Date: Thu, 4 Jun 2026 16:59:51 +1000 Subject: [PATCH 02/11] Split build --- .github/workflows/ci.yml | 87 +++++++++++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8610359e..4651c674 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: pull_request: workflow_dispatch: jobs: - build: + build-windows: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} env: @@ -29,6 +29,64 @@ jobs: name: win-arm64 rid: win-arm64 param: -arm64 + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + with: + submodules: true + - name: Build Windows + run: ./build.libgit2.ps1 ${{ matrix.param }} + - name: Verify build output + shell: bash + run: ./verify-build.sh "${{ matrix.rid }}" "${{ matrix.variant }}" + - name: Upload artifacts + uses: actions/upload-artifact@v7.0.0 + with: + name: ${{ matrix.name }} + path: nuget.package/runtimes/${{ matrix.rid }} + + build-mac: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + env: + RID: ${{ matrix.rid }} + VARIANT: ${{ matrix.variant }} + strategy: + matrix: + include: + - os: macos-26-intel + name: osx-x64 + rid: osx-x64 + - os: macos-26 + name: osx-arm64 + rid: osx-arm64 + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + with: + submodules: true + - name: Build macOS + run: ./build.libgit2.sh + - name: Verify build output + shell: bash + run: ./verify-build.sh "${{ matrix.rid }}" "${{ matrix.variant }}" + - name: Upload artifacts + uses: actions/upload-artifact@v7.0.0 + with: + name: ${{ matrix.name }} + path: nuget.package/runtimes/${{ matrix.rid }} + + build-linux: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + env: + RID: ${{ matrix.rid }} + VARIANT: ${{ matrix.variant }} + strategy: + matrix: + include: - os: ubuntu-24.04 name: linux-x64 rid: linux-x64 @@ -64,32 +122,18 @@ jobs: - os: ubuntu-24.04 name: linux-musl-arm64 rid: linux-musl-arm64 - - os: macos-26-intel - name: osx-x64 - rid: osx-x64 - - os: macos-26 - name: osx-arm64 - rid: osx-arm64 fail-fast: false steps: - name: Checkout uses: actions/checkout@v6.0.2 with: submodules: true - - name: Build Windows - if: runner.os == 'Windows' - run: ./build.libgit2.ps1 ${{ matrix.param }} - - name: Build macOS - if: runner.os == 'macOS' - run: ./build.libgit2.sh - name: Setup QEMU - if: runner.os == 'Linux' && (matrix.rid == 'linux-arm' || matrix.rid == 'linux-arm64' || matrix.rid == 'linux-ppc64le' || matrix.rid == 'linux-musl-arm' || matrix.rid == 'linux-musl-arm64') + if: matrix.rid == 'linux-arm' || matrix.rid == 'linux-arm64' || matrix.rid == 'linux-ppc64le' || matrix.rid == 'linux-musl-arm' || matrix.rid == 'linux-musl-arm64' uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - if: runner.os == 'Linux' uses: docker/setup-buildx-action@v4 - name: Build Linux - if: runner.os == 'Linux' run: ./dockerbuild.sh - name: Verify build output shell: bash @@ -101,7 +145,7 @@ jobs: path: nuget.package/runtimes/${{ matrix.rid }} smoke-windows: name: smoke-win-x64 - needs: build + needs: build-windows runs-on: windows-2022 steps: - name: Checkout @@ -130,7 +174,10 @@ jobs: run: docker run --rm libgit2-smoke C:\native\${{ steps.dll.outputs.name }} package: name: Create package - needs: build + needs: + - build-windows + - build-mac + - build-linux runs-on: ubuntu-24.04 env: DOTNET_NOLOGO: true @@ -194,12 +241,12 @@ jobs: run: dotnet nuget push ./nuget.package/*.nupkg --api-key "$FEED_API_KEY" --source "$FEED_SOURCE" ci: name: ci - needs: [build, package, smoke-windows] + needs: [package, smoke-windows] runs-on: ubuntu-24.04 steps: - name: Check results run: | - if [ "${{ needs.build.result }}" != "success" ] || [ "${{ needs.package.result }}" != "success" ] || [ "${{ needs.smoke-windows.result }}" != "success" ]; then + if [ "${{ needs.package.result }}" != "success" ] || [ "${{ needs.smoke-windows.result }}" != "success" ]; then echo "One or more jobs failed" exit 1 fi From 6a3127cc91fba7d8fac194b518219434691fbfd1 Mon Sep 17 00:00:00 2001 From: Eddy Moulton Date: Thu, 4 Jun 2026 17:20:07 +1000 Subject: [PATCH 03/11] Build statically --- build.libgit2.ps1 | 34 ++++++++++++++------ libssh2-wincng-triplets/wincng-options.cmake | 4 +-- verify-build.sh | 8 ++++- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/build.libgit2.ps1 b/build.libgit2.ps1 index 3ff79d85..85e9adeb 100644 --- a/build.libgit2.ps1 +++ b/build.libgit2.ps1 @@ -101,6 +101,25 @@ function Assert-Consistent-Naming($expected, $path) { Ensure-Property $expected $dll.VersionInfo.OriginalFilename "VersionInfo.OriginalFilename" $dll.Fullname } +function Assert-MemoryCredentials { + # After cmake configure, libgit2 generates git2_features.h from + # src/util/git2_features.h.in. The #cmakedefine becomes either + # #define GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS 1 (probe passed) + # or + # /* #undef GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS */ (probe failed -> feature compiled out) + # A failed probe means the check_library_exists() test executable couldn't + # link the static libssh2 + transitive deps, silently disabling in-memory + # SSH key auth. Fail loudly instead. + $featuresFile = Get-ChildItem -Path . -Recurse -Filter git2_features.h -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $featuresFile) { + throw "Assert-MemoryCredentials: git2_features.h not found after configure" + } + if (-not (Select-String -Path $featuresFile.FullName -Pattern 'define GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS' -Quiet)) { + throw "GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS not defined in $($featuresFile.FullName) - in-memory SSH credentials were silently disabled (static transitive deps not visible to the check_library_exists probe)" + } + Write-Host "Verified GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS is defined ($($featuresFile.FullName))" +} + function Install-Libssh2($arch) { $triplet = "$arch-windows" @@ -169,7 +188,8 @@ try { if ($x86.IsPresent) { Write-Output "Building x86..." $ssh2 = Install-Libssh2 "x86" - Run-Command -Fatal { & $cmake -A Win32 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" .. } + Run-Command -Fatal { & $cmake -A Win32 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "CMAKE_REQUIRED_LIBRARIES=bcrypt;crypt32" .. } + Assert-MemoryCredentials Run-Command -Fatal { & $cmake --build . --config $configuration } if ($test.IsPresent) { Run-Command -Quiet -Fatal { & $ctest -V . } } cd $configuration @@ -178,8 +198,6 @@ try { Run-Command -Quiet { & rm $x86Directory\* -ErrorAction Ignore } Run-Command -Quiet { & mkdir -fo $x86Directory } Run-Command -Quiet -Fatal { & copy -fo * $x86Directory -Exclude *.lib } - Run-Command -Quiet -Fatal { & copy -fo (Join-Path $ssh2.BinDir "*.dll") $x86Directory } - if (-not (Test-Path (Join-Path $x86Directory "libssh2.dll"))) { throw "Error: libssh2.dll was not copied to $x86Directory" } cd .. } @@ -188,7 +206,8 @@ try { $ssh2 = Install-Libssh2 "x64" Run-Command -Quiet { & mkdir build64 } cd build64 - Run-Command -Fatal { & $cmake -A x64 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" ../.. } + Run-Command -Fatal { & $cmake -A x64 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "CMAKE_REQUIRED_LIBRARIES=bcrypt;crypt32" ../.. } + Assert-MemoryCredentials Run-Command -Fatal { & $cmake --build . --config $configuration } if ($test.IsPresent) { Run-Command -Quiet -Fatal { & $ctest -V . } } cd $configuration @@ -197,8 +216,6 @@ try { Run-Command -Quiet { & rm $x64Directory\* -ErrorAction Ignore } Run-Command -Quiet { & mkdir -fo $x64Directory } Run-Command -Quiet -Fatal { & copy -fo * $x64Directory -Exclude *.lib } - Run-Command -Quiet -Fatal { & copy -fo (Join-Path $ssh2.BinDir "*.dll") $x64Directory } - if (-not (Test-Path (Join-Path $x64Directory "libssh2.dll"))) { throw "Error: libssh2.dll was not copied to $x64Directory" } } if ($arm64.IsPresent) { @@ -206,7 +223,8 @@ try { $ssh2 = Install-Libssh2 "arm64" Run-Command -Quiet { & mkdir buildarm64 } cd buildarm64 - Run-Command -Fatal { & $cmake -A ARM64 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" ../.. } + Run-Command -Fatal { & $cmake -A ARM64 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "CMAKE_REQUIRED_LIBRARIES=bcrypt;crypt32" ../.. } + Assert-MemoryCredentials Run-Command -Fatal { & $cmake --build . --config $configuration } if ($test.IsPresent) { Run-Command -Quiet -Fatal { & $ctest -V . } } cd $configuration @@ -215,8 +233,6 @@ try { Run-Command -Quiet { & rm $arm64Directory\* -ErrorAction Ignore } Run-Command -Quiet { & mkdir -fo $arm64Directory } Run-Command -Quiet -Fatal { & copy -fo * $arm64Directory -Exclude *.lib } - Run-Command -Quiet -Fatal { & copy -fo (Join-Path $ssh2.BinDir "*.dll") $arm64Directory } - if (-not (Test-Path (Join-Path $arm64Directory "libssh2.dll"))) { throw "Error: libssh2.dll was not copied to $arm64Directory" } } Write-Output "Done!" diff --git a/libssh2-wincng-triplets/wincng-options.cmake b/libssh2-wincng-triplets/wincng-options.cmake index fdd66cbd..6c16a343 100644 --- a/libssh2-wincng-triplets/wincng-options.cmake +++ b/libssh2-wincng-triplets/wincng-options.cmake @@ -1,3 +1,3 @@ -set(VCPKG_CRT_LINKAGE dynamic) -set(VCPKG_LIBRARY_LINKAGE dynamic) +set(VCPKG_CRT_LINKAGE static) +set(VCPKG_LIBRARY_LINKAGE static) set(VCPKG_CMAKE_CONFIGURE_OPTIONS "-DENABLE_ECDSA_WINCNG=ON") diff --git a/verify-build.sh b/verify-build.sh index c4ec319e..7eced8f1 100755 --- a/verify-build.sh +++ b/verify-build.sh @@ -110,7 +110,13 @@ case "$RID" in win-*) require_file "$NATIVE_DIR/git2-$SHORTSHA.dll" - require_file "$NATIVE_DIR/libssh2.dll" + # libssh2 is statically linked into git2-*.dll, so no separate libssh2.dll + # should ship alongside it. + if [[ -f "$NATIVE_DIR/libssh2.dll" ]]; then + fail "libssh2.dll present in $NATIVE_DIR — expected static linkage into git2-$SHORTSHA.dll" + else + pass "no separate libssh2.dll (statically linked)" + fi ;; *) From f057b95a91caff1a1045d13d3a05b60b105909ed Mon Sep 17 00:00:00 2001 From: Eddy Moulton Date: Thu, 4 Jun 2026 17:55:26 +1000 Subject: [PATCH 04/11] Fix build --- build.libgit2.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.libgit2.ps1 b/build.libgit2.ps1 index 85e9adeb..e54053d6 100644 --- a/build.libgit2.ps1 +++ b/build.libgit2.ps1 @@ -188,7 +188,7 @@ try { if ($x86.IsPresent) { Write-Output "Building x86..." $ssh2 = Install-Libssh2 "x86" - Run-Command -Fatal { & $cmake -A Win32 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "CMAKE_REQUIRED_LIBRARIES=bcrypt;crypt32" .. } + Run-Command -Fatal { & $cmake -A Win32 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "CMAKE_REQUIRED_LIBRARIES=bcrypt;crypt32;ws2_32;$((Join-Path $ssh2.LibDir 'zlib.lib').Replace('\','/'))" .. } Assert-MemoryCredentials Run-Command -Fatal { & $cmake --build . --config $configuration } if ($test.IsPresent) { Run-Command -Quiet -Fatal { & $ctest -V . } } @@ -206,7 +206,7 @@ try { $ssh2 = Install-Libssh2 "x64" Run-Command -Quiet { & mkdir build64 } cd build64 - Run-Command -Fatal { & $cmake -A x64 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "CMAKE_REQUIRED_LIBRARIES=bcrypt;crypt32" ../.. } + Run-Command -Fatal { & $cmake -A x64 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "CMAKE_REQUIRED_LIBRARIES=bcrypt;crypt32;ws2_32;$((Join-Path $ssh2.LibDir 'zlib.lib').Replace('\','/'))" ../.. } Assert-MemoryCredentials Run-Command -Fatal { & $cmake --build . --config $configuration } if ($test.IsPresent) { Run-Command -Quiet -Fatal { & $ctest -V . } } @@ -223,7 +223,7 @@ try { $ssh2 = Install-Libssh2 "arm64" Run-Command -Quiet { & mkdir buildarm64 } cd buildarm64 - Run-Command -Fatal { & $cmake -A ARM64 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "CMAKE_REQUIRED_LIBRARIES=bcrypt;crypt32" ../.. } + Run-Command -Fatal { & $cmake -A ARM64 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "CMAKE_REQUIRED_LIBRARIES=bcrypt;crypt32;ws2_32;$((Join-Path $ssh2.LibDir 'zlib.lib').Replace('\','/'))" ../.. } Assert-MemoryCredentials Run-Command -Fatal { & $cmake --build . --config $configuration } if ($test.IsPresent) { Run-Command -Quiet -Fatal { & $ctest -V . } } From 3c88df7623c594b42c1aa1ee13517dc90164bc36 Mon Sep 17 00:00:00 2001 From: Eddy Moulton Date: Thu, 4 Jun 2026 18:23:53 +1000 Subject: [PATCH 05/11] Set HAVE_LIBSSH2_MEMORY_CREDENTIALS --- build.libgit2.ps1 | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/build.libgit2.ps1 b/build.libgit2.ps1 index e54053d6..f1674372 100644 --- a/build.libgit2.ps1 +++ b/build.libgit2.ps1 @@ -102,14 +102,15 @@ function Assert-Consistent-Naming($expected, $path) { } function Assert-MemoryCredentials { - # After cmake configure, libgit2 generates git2_features.h from - # src/util/git2_features.h.in. The #cmakedefine becomes either - # #define GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS 1 (probe passed) - # or - # /* #undef GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS */ (probe failed -> feature compiled out) - # A failed probe means the check_library_exists() test executable couldn't - # link the static libssh2 + transitive deps, silently disabling in-memory - # SSH key auth. Fail loudly instead. + # libgit2's SelectSSH uses a check_library_exists() probe to set + # GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS. That probe is unreliable against our + # static-CRT vcpkg libssh2 (the VS-generator try_compile defaults to a Debug + # /MTd link against the Release /MT libs and can't resolve it), so we pre-set + # HAVE_LIBSSH2_MEMORY_CREDENTIALS=1 on the cmake command line to skip it. + # libssh2_userauth_publickey_frommemory is part of libssh2's public API, and + # its real presence is enforced by the final git2-*.dll link (a missing symbol + # fails loudly there with LNK2019). This assertion guards against the force + # flag silently no-op'ing (e.g. a renamed cache var in a future libgit2). $featuresFile = Get-ChildItem -Path . -Recurse -Filter git2_features.h -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $featuresFile) { throw "Assert-MemoryCredentials: git2_features.h not found after configure" @@ -188,7 +189,7 @@ try { if ($x86.IsPresent) { Write-Output "Building x86..." $ssh2 = Install-Libssh2 "x86" - Run-Command -Fatal { & $cmake -A Win32 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "CMAKE_REQUIRED_LIBRARIES=bcrypt;crypt32;ws2_32;$((Join-Path $ssh2.LibDir 'zlib.lib').Replace('\','/'))" .. } + Run-Command -Fatal { & $cmake -A Win32 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "HAVE_LIBSSH2_MEMORY_CREDENTIALS=1" .. } Assert-MemoryCredentials Run-Command -Fatal { & $cmake --build . --config $configuration } if ($test.IsPresent) { Run-Command -Quiet -Fatal { & $ctest -V . } } @@ -206,7 +207,7 @@ try { $ssh2 = Install-Libssh2 "x64" Run-Command -Quiet { & mkdir build64 } cd build64 - Run-Command -Fatal { & $cmake -A x64 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "CMAKE_REQUIRED_LIBRARIES=bcrypt;crypt32;ws2_32;$((Join-Path $ssh2.LibDir 'zlib.lib').Replace('\','/'))" ../.. } + Run-Command -Fatal { & $cmake -A x64 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "HAVE_LIBSSH2_MEMORY_CREDENTIALS=1" ../.. } Assert-MemoryCredentials Run-Command -Fatal { & $cmake --build . --config $configuration } if ($test.IsPresent) { Run-Command -Quiet -Fatal { & $ctest -V . } } @@ -223,7 +224,7 @@ try { $ssh2 = Install-Libssh2 "arm64" Run-Command -Quiet { & mkdir buildarm64 } cd buildarm64 - Run-Command -Fatal { & $cmake -A ARM64 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "CMAKE_REQUIRED_LIBRARIES=bcrypt;crypt32;ws2_32;$((Join-Path $ssh2.LibDir 'zlib.lib').Replace('\','/'))" ../.. } + Run-Command -Fatal { & $cmake -A ARM64 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "HAVE_LIBSSH2_MEMORY_CREDENTIALS=1" ../.. } Assert-MemoryCredentials Run-Command -Fatal { & $cmake --build . --config $configuration } if ($test.IsPresent) { Run-Command -Quiet -Fatal { & $ctest -V . } } From 7b4199626c78d6e9ea271ca72e6936fc498d9a08 Mon Sep 17 00:00:00 2001 From: Eddy Moulton Date: Thu, 4 Jun 2026 18:39:26 +1000 Subject: [PATCH 06/11] More smoke tests --- .github/workflows/ci.yml | 54 ++++++++++++++++++++++++++++++++----- smoke-test/linux/Dockerfile | 15 +++++++++++ smoke-test/linux/loader.c | 23 ++++++++++++++++ 3 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 smoke-test/linux/Dockerfile create mode 100644 smoke-test/linux/loader.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4651c674..598eb50f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,17 +161,57 @@ jobs: run: | SHA=$(cat nuget.package/libgit2/libgit2_hash.txt) echo "name=git2-${SHA:0:7}.dll" >> "$GITHUB_OUTPUT" - - name: Set up MSVC - uses: ilammy/msvc-dev-cmd@v1 - with: - arch: x64 - name: Build static-CRT loader shell: cmd - run: cl /MT /O2 smoke-test\windows\loader.c /Fe:smoke-test\windows\loader.exe + run: | + for /f "usebackq tokens=*" %%i in (`"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do call "%%i\VC\Auxiliary\Build\vcvars64.bat" + cl /MT /O2 smoke-test\windows\loader.c /Fe:smoke-test\windows\loader.exe - name: Build bare-Windows smoke image run: docker build -t libgit2-smoke -f smoke-test/windows/Dockerfile smoke-test/windows - name: Load git2 on bare Windows (no VC++ redist) run: docker run --rm libgit2-smoke C:\native\${{ steps.dll.outputs.name }} + smoke-linux: + name: smoke-${{ matrix.name }} + needs: build-linux + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - name: linux-x64-openssl3 + artifact: linux-x64 + build_base: debian:bookworm + runtime_base: debian:bookworm-slim + runtime_pkgs: libssl3 + suffix: "" + - name: linux-x64-openssl1.1 + artifact: linux-x64-openssl1.1 + build_base: debian:bullseye + runtime_base: debian:bullseye-slim + runtime_pkgs: libssl1.1 + suffix: "-openssl1.1" + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + - name: Download artifact + uses: actions/download-artifact@v8.0.1 + with: + name: ${{ matrix.artifact }} + path: smoke-test/linux + - name: Compute libgit2 .so name + id: so + run: | + SHA=$(cat nuget.package/libgit2/libgit2_hash.txt) + echo "name=libgit2-${SHA:0:7}${{ matrix.suffix }}.so" >> "$GITHUB_OUTPUT" + - name: Build smoke image + run: | + docker build -t libgit2-smoke \ + --build-arg BUILD_BASE=${{ matrix.build_base }} \ + --build-arg RUNTIME_BASE=${{ matrix.runtime_base }} \ + --build-arg RUNTIME_PKGS=${{ matrix.runtime_pkgs }} \ + -f smoke-test/linux/Dockerfile smoke-test/linux + - name: Load libgit2 in slim runtime + run: docker run --rm -e LD_LIBRARY_PATH=/native libgit2-smoke /native/${{ steps.so.outputs.name }} package: name: Create package needs: @@ -241,12 +281,12 @@ jobs: run: dotnet nuget push ./nuget.package/*.nupkg --api-key "$FEED_API_KEY" --source "$FEED_SOURCE" ci: name: ci - needs: [package, smoke-windows] + needs: [package, smoke-windows, smoke-linux] runs-on: ubuntu-24.04 steps: - name: Check results run: | - if [ "${{ needs.package.result }}" != "success" ] || [ "${{ needs.smoke-windows.result }}" != "success" ]; then + if [ "${{ needs.package.result }}" != "success" ] || [ "${{ needs.smoke-windows.result }}" != "success" ] || [ "${{ needs.smoke-linux.result }}" != "success" ]; then echo "One or more jobs failed" exit 1 fi diff --git a/smoke-test/linux/Dockerfile b/smoke-test/linux/Dockerfile new file mode 100644 index 00000000..2bd123a8 --- /dev/null +++ b/smoke-test/linux/Dockerfile @@ -0,0 +1,15 @@ +# Build the loader on a full base, then run it in a slim runtime base that +# represents an intended target (matching OpenSSL present, no dev tooling). +ARG BUILD_BASE=debian:bookworm +ARG RUNTIME_BASE=debian:bookworm-slim +FROM ${BUILD_BASE} AS build +RUN apt-get update && apt-get install -y --no-install-recommends build-essential && rm -rf /var/lib/apt/lists/* +COPY loader.c /loader.c +RUN gcc -O2 -o /loader /loader.c -ldl + +FROM ${RUNTIME_BASE} +ARG RUNTIME_PKGS="" +RUN if [ -n "$RUNTIME_PKGS" ]; then apt-get update && apt-get install -y --no-install-recommends $RUNTIME_PKGS && rm -rf /var/lib/apt/lists/*; fi +COPY --from=build /loader /usr/local/bin/loader +COPY native/ /native/ +ENTRYPOINT ["/usr/local/bin/loader"] diff --git a/smoke-test/linux/loader.c b/smoke-test/linux/loader.c new file mode 100644 index 00000000..08350235 --- /dev/null +++ b/smoke-test/linux/loader.c @@ -0,0 +1,23 @@ +/* Minimal dlopen loader for the Linux libgit2 native binary smoke test. */ +#include +#include + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "usage: loader \n"); + return 2; + } + void *h = dlopen(argv[1], RTLD_NOW | RTLD_GLOBAL); + if (!h) { + fprintf(stderr, "dlopen('%s') failed: %s\n", argv[1], dlerror()); + return 1; + } + int (*init)(void) = (int (*)(void))dlsym(h, "git_libgit2_init"); + if (!init) { + fprintf(stderr, "dlsym(git_libgit2_init) failed: %s\n", dlerror()); + return 1; + } + int rc = init(); + printf("git_libgit2_init() returned %d\n", rc); + return rc > 0 ? 0 : 1; +} From ff78d88bdd6503b5ae93454ca44cc9b118cbbc6f Mon Sep 17 00:00:00 2001 From: Eddy Moulton Date: Fri, 5 Jun 2026 09:52:53 +1000 Subject: [PATCH 07/11] Common cmake args var --- build.libgit2.ps1 | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/build.libgit2.ps1 b/build.libgit2.ps1 index f1674372..ede6d455 100644 --- a/build.libgit2.ps1 +++ b/build.libgit2.ps1 @@ -186,10 +186,21 @@ try { Run-Command -Quiet { & mkdir build } cd build + $commonCmakeArgs = @( + '-D', 'USE_SSH=ON' + '-D', 'USE_HTTPS=Schannel' + '-D', "BUILD_TESTS=$build_tests" + '-D', 'BUILD_CLI=OFF' + '-D', "LIBGIT2_FILENAME=$binaryFilename" + '-D', 'CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib' + '-D', 'HAVE_LIBSSH2_MEMORY_CREDENTIALS=1' + ) + if ($x86.IsPresent) { Write-Output "Building x86..." $ssh2 = Install-Libssh2 "x86" - Run-Command -Fatal { & $cmake -A Win32 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "HAVE_LIBSSH2_MEMORY_CREDENTIALS=1" .. } + $cmakeArgs = @('-A', 'Win32') + $commonCmakeArgs + @('-D', "CMAKE_PREFIX_PATH=$($ssh2.Prefix)", '..') + Run-Command -Fatal { & $cmake @cmakeArgs } Assert-MemoryCredentials Run-Command -Fatal { & $cmake --build . --config $configuration } if ($test.IsPresent) { Run-Command -Quiet -Fatal { & $ctest -V . } } @@ -207,7 +218,8 @@ try { $ssh2 = Install-Libssh2 "x64" Run-Command -Quiet { & mkdir build64 } cd build64 - Run-Command -Fatal { & $cmake -A x64 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "HAVE_LIBSSH2_MEMORY_CREDENTIALS=1" ../.. } + $cmakeArgs = @('-A', 'x64') + $commonCmakeArgs + @('-D', "CMAKE_PREFIX_PATH=$($ssh2.Prefix)", '../..') + Run-Command -Fatal { & $cmake @cmakeArgs } Assert-MemoryCredentials Run-Command -Fatal { & $cmake --build . --config $configuration } if ($test.IsPresent) { Run-Command -Quiet -Fatal { & $ctest -V . } } @@ -224,7 +236,8 @@ try { $ssh2 = Install-Libssh2 "arm64" Run-Command -Quiet { & mkdir buildarm64 } cd buildarm64 - Run-Command -Fatal { & $cmake -A ARM64 -D USE_SSH=ON -D USE_HTTPS=Schannel -D "BUILD_TESTS=$build_tests" -D "BUILD_CLI=OFF" -D "LIBGIT2_FILENAME=$binaryFilename" -D "CMAKE_PREFIX_PATH=$($ssh2.Prefix)" -D "CMAKE_SHARED_LINKER_FLAGS=bcrypt.lib crypt32.lib" -D "HAVE_LIBSSH2_MEMORY_CREDENTIALS=1" ../.. } + $cmakeArgs = @('-A', 'ARM64') + $commonCmakeArgs + @('-D', "CMAKE_PREFIX_PATH=$($ssh2.Prefix)", '../..') + Run-Command -Fatal { & $cmake @cmakeArgs } Assert-MemoryCredentials Run-Command -Fatal { & $cmake --build . --config $configuration } if ($test.IsPresent) { Run-Command -Quiet -Fatal { & $ctest -V . } } From e149b9cd10071b1e9c77df2ee102e255255ace22 Mon Sep 17 00:00:00 2001 From: Eddy Moulton Date: Fri, 5 Jun 2026 09:56:50 +1000 Subject: [PATCH 08/11] Remove check for libssh2 non-existance --- verify-build.sh | 7 ------- 1 file changed, 7 deletions(-) diff --git a/verify-build.sh b/verify-build.sh index 7eced8f1..3e1882f0 100755 --- a/verify-build.sh +++ b/verify-build.sh @@ -110,13 +110,6 @@ case "$RID" in win-*) require_file "$NATIVE_DIR/git2-$SHORTSHA.dll" - # libssh2 is statically linked into git2-*.dll, so no separate libssh2.dll - # should ship alongside it. - if [[ -f "$NATIVE_DIR/libssh2.dll" ]]; then - fail "libssh2.dll present in $NATIVE_DIR — expected static linkage into git2-$SHORTSHA.dll" - else - pass "no separate libssh2.dll (statically linked)" - fi ;; *) From 1d10d420ab0a499591061c3290992bf2e280f8b1 Mon Sep 17 00:00:00 2001 From: Eddy Moulton Date: Fri, 5 Jun 2026 09:57:03 +1000 Subject: [PATCH 09/11] Remove comment --- smoke-test/windows/Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/smoke-test/windows/Dockerfile b/smoke-test/windows/Dockerfile index b2c28a81..9f929a72 100644 --- a/smoke-test/windows/Dockerfile +++ b/smoke-test/windows/Dockerfile @@ -1,5 +1,3 @@ -# Bare Windows: nanoserver ships the UCRT but NOT the VC++ Redistributable, -# so it is the clean-VM analogue for MD-2027. FROM mcr.microsoft.com/windows/nanoserver:ltsc2022 COPY native/ C:/native/ COPY loader.exe C:/loader.exe From fc35e77c0d266820e0d961dd39e030cfede85e66 Mon Sep 17 00:00:00 2001 From: Eddy Moulton Date: Fri, 5 Jun 2026 09:57:50 +1000 Subject: [PATCH 10/11] More readable loaders --- smoke-test/linux/loader.c | 24 ++++++++++++++++-------- smoke-test/windows/loader.c | 23 ++++++++++++++--------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/smoke-test/linux/loader.c b/smoke-test/linux/loader.c index 08350235..4be3ce68 100644 --- a/smoke-test/linux/loader.c +++ b/smoke-test/linux/loader.c @@ -2,22 +2,30 @@ #include #include +typedef int (*git_libgit2_init_fn)(void); + int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "usage: loader \n"); return 2; } - void *h = dlopen(argv[1], RTLD_NOW | RTLD_GLOBAL); - if (!h) { - fprintf(stderr, "dlopen('%s') failed: %s\n", argv[1], dlerror()); + + const char *libraryPath = argv[1]; + + void *library = dlopen(libraryPath, RTLD_NOW | RTLD_GLOBAL); + if (!library) { + fprintf(stderr, "dlopen('%s') failed: %s\n", libraryPath, dlerror()); return 1; } - int (*init)(void) = (int (*)(void))dlsym(h, "git_libgit2_init"); - if (!init) { + + git_libgit2_init_fn git_libgit2_init = (git_libgit2_init_fn)dlsym(library, "git_libgit2_init"); + if (!git_libgit2_init) { fprintf(stderr, "dlsym(git_libgit2_init) failed: %s\n", dlerror()); return 1; } - int rc = init(); - printf("git_libgit2_init() returned %d\n", rc); - return rc > 0 ? 0 : 1; + + int initCount = git_libgit2_init(); + printf("git_libgit2_init() returned %d\n", initCount); + + return initCount > 0 ? 0 : 1; } diff --git a/smoke-test/windows/loader.c b/smoke-test/windows/loader.c index 850f1ead..a8288d3e 100644 --- a/smoke-test/windows/loader.c +++ b/smoke-test/windows/loader.c @@ -5,27 +5,32 @@ #include #include +typedef int (*git_libgit2_init_fn)(void); + int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "usage: loader \n"); return 2; } - HMODULE h = LoadLibraryExA(argv[1], NULL, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR); - if (!h) { - DWORD e = GetLastError(); - fprintf(stderr, "LoadLibraryEx('%s') failed: error %lu (0x%08lX)\n", argv[1], e, e); + const char *dllPath = argv[1]; + + HMODULE library = LoadLibraryExA(dllPath, NULL, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR); + if (!library) { + DWORD error = GetLastError(); + fprintf(stderr, "LoadLibraryEx('%s') failed: error %lu (0x%08lX)\n", dllPath, error, error); return 1; } - FARPROC p = GetProcAddress(h, "git_libgit2_init"); - if (!p) { + git_libgit2_init_fn git_libgit2_init = (git_libgit2_init_fn)GetProcAddress(library, "git_libgit2_init"); + if (!git_libgit2_init) { fprintf(stderr, "GetProcAddress(git_libgit2_init) failed: %lu\n", GetLastError()); return 1; } - int rc = ((int (*)(void))p)(); - printf("git_libgit2_init() returned %d\n", rc); + int initCount = git_libgit2_init(); + printf("git_libgit2_init() returned %d\n", initCount); + /* git_libgit2_init returns the initialization count (>=1) on success. */ - return rc > 0 ? 0 : 1; + return initCount > 0 ? 0 : 1; } From 2cf877e9a49a44e12445ddbb66f71169056cf7c4 Mon Sep 17 00:00:00 2001 From: Eddy Moulton Date: Fri, 5 Jun 2026 10:01:38 +1000 Subject: [PATCH 11/11] Tidy comment --- build.libgit2.ps1 | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/build.libgit2.ps1 b/build.libgit2.ps1 index ede6d455..9a9eb671 100644 --- a/build.libgit2.ps1 +++ b/build.libgit2.ps1 @@ -101,16 +101,10 @@ function Assert-Consistent-Naming($expected, $path) { Ensure-Property $expected $dll.VersionInfo.OriginalFilename "VersionInfo.OriginalFilename" $dll.Fullname } +# libssh2_userauth_publickey_frommemory is quite fragile and can easily be left out by a misconfigured build or +# a changed dependency. This assertion tries to verify that we have the GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS +# feature enabled and blows up the build if not. function Assert-MemoryCredentials { - # libgit2's SelectSSH uses a check_library_exists() probe to set - # GIT_SSH_LIBSSH2_MEMORY_CREDENTIALS. That probe is unreliable against our - # static-CRT vcpkg libssh2 (the VS-generator try_compile defaults to a Debug - # /MTd link against the Release /MT libs and can't resolve it), so we pre-set - # HAVE_LIBSSH2_MEMORY_CREDENTIALS=1 on the cmake command line to skip it. - # libssh2_userauth_publickey_frommemory is part of libssh2's public API, and - # its real presence is enforced by the final git2-*.dll link (a missing symbol - # fails loudly there with LNK2019). This assertion guards against the force - # flag silently no-op'ing (e.g. a renamed cache var in a future libgit2). $featuresFile = Get-ChildItem -Path . -Recurse -Filter git2_features.h -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $featuresFile) { throw "Assert-MemoryCredentials: git2_features.h not found after configure"