diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c12bbd6..af64de86 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -86,9 +86,11 @@ jobs: build-mode: manual - name: Built .Net package and test run: | - mkdir -p LibArchive.Net/runtimes/osx-any64 LibArchive.Net/runtimes/linux-x64 - mv libarchive.so LibArchive.Net/runtimes/linux-x64/ - mv libarchive.dylib LibArchive.Net/runtimes/osx-any64/ + mkdir -p LibArchive.Net/runtimes/osx-x64/native LibArchive.Net/runtimes/osx-arm64/native LibArchive.Net/runtimes/linux-x64/native LibArchive.Net/runtimes/linux-musl-x64/native + mv libarchive.so LibArchive.Net/runtimes/linux-x64/native/ + cp LibArchive.Net/runtimes/linux-x64/native/libarchive.so LibArchive.Net/runtimes/linux-musl-x64/native/ + mv libarchive.dylib LibArchive.Net/runtimes/osx-x64/native/ + cp LibArchive.Net/runtimes/osx-x64/native/libarchive.dylib LibArchive.Net/runtimes/osx-arm64/native/ touch libarchive.dylib dotnet build --configuration Release dotnet test --nologo @@ -125,3 +127,50 @@ jobs: - name: Upload Nuget package if: contains(github.ref, 'refs/tags/v') run: dotnet nuget push *.nupkg -k ${{ secrets.NUGET_KEY }} --source https://api.nuget.org/v3/index.json + + mono-test: + name: Test .NET Standard 2.0 with Mono + needs: [linux] + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Install Mono + run: | + sudo apt-get update + sudo apt-get install -y mono-complete mono-devel + mono --version + + - name: Setup .NET Core + uses: actions/setup-dotnet@v5.0.0 + + - name: Retrieve build artifacts + uses: actions/download-artifact@v5 + with: + name: dist + + - name: Extract and setup libraries + run: | + # Extract the nupkg to get the native libraries + unzip -q *.nupkg -d extracted + mkdir -p LibArchive.Net/runtimes + cp -r extracted/runtimes/* LibArchive.Net/runtimes/ + ls -laR LibArchive.Net/runtimes/ + + - name: Build test project for .NET Framework + run: | + dotnet restore Test.LibArchive.Net/Test.LibArchive.Net.csproj + dotnet build Test.LibArchive.Net/Test.LibArchive.Net.csproj --configuration Release --framework net462 --no-restore + + - name: Run tests with Mono (.NET Standard 2.0) + run: | + # Run tests targeting net462 which uses the netstandard2.0 library + dotnet test Test.LibArchive.Net/Test.LibArchive.Net.csproj --configuration Release --framework net462 --no-build --verbosity normal + + - name: Display test results + if: always() + run: | + echo "Mono version used:" + mono --version + echo "Test completed for .NET Standard 2.0 compatibility" diff --git a/.github/workflows/check-native-dependencies.yml b/.github/workflows/check-native-dependencies.yml new file mode 100644 index 00000000..ceb3a939 --- /dev/null +++ b/.github/workflows/check-native-dependencies.yml @@ -0,0 +1,50 @@ +name: Check Native Dependencies + +on: + schedule: + # Run weekly on Mondays at 9:00 AM UTC + - cron: '0 9 * * 1' + workflow_dispatch: + inputs: + dry_run: + description: 'Dry run (no PR creation)' + required: false + default: 'false' + +permissions: + contents: write + pull-requests: write + +jobs: + check-updates: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y curl jq + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Check for updates + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DRY_RUN: ${{ github.event.inputs.dry_run == 'true' && '1' || '0' }} + run: | + bash scripts/check-native-updates.sh + + - name: Summary + if: always() + run: | + echo "### Native Dependency Check Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Check the job logs for details on any updates found." >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 8a30d258..a6755a56 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,9 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +# Claude Code / Claude Flow +.claude/ +.claude-flow/ +.swarm/ +CLAUDE.md diff --git a/LibArchive.Net/LibArchive.Net.csproj b/LibArchive.Net/LibArchive.Net.csproj index 8d6d0fbf..fee5190a 100644 --- a/LibArchive.Net/LibArchive.Net.csproj +++ b/LibArchive.Net/LibArchive.Net.csproj @@ -1,13 +1,23 @@ - net6.0;net8.0 + netstandard2.0;net6.0;net8.0 enable latest - win-x64;linux-x64;osx-x64;osx-arm64 + + true + win-x86;win-x64;win-arm64;linux-x64;linux-musl-x64;osx-x64;osx-arm64 + + + true + true + true + true + + BSD-2-Clause LibArchive.Net - Compression;Libarchive;Tar;Zip;7Zip;Rar - This package provides access to the native libarchive compression library (included) on 64 bit Linux, Windows and MacOS platforms (x86-64 for all three, plus arm64/M1 for Mac) + Compression;Libarchive;Tar;Zip;7Zip;Rar;AOT;NativeAOT;Trimming + This package provides access to the native libarchive compression library (included) on Linux (x64, musl-x64), Windows (x86, x64, arm64), and macOS (x64, arm64) platforms. Supports Native AOT and trimming for .NET 6+. James A Sutherland Copyright 2022-2024 LibArchive.Net @@ -23,6 +33,8 @@ + + diff --git a/LibArchive.Net/LibArchiveReader.cs b/LibArchive.Net/LibArchiveReader.cs index add2a30c..0bc83e29 100644 --- a/LibArchive.Net/LibArchiveReader.cs +++ b/LibArchive.Net/LibArchiveReader.cs @@ -2,7 +2,11 @@ using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; +using System.Reflection; using Microsoft.Win32.SafeHandles; +#if NETSTANDARD2_0 +using System.Text; +#endif [assembly: DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] namespace LibArchive.Net; @@ -21,23 +25,32 @@ private enum ARCHIVE_RESULT static LibArchiveReader() { - NativeLibrary.SetDllImportResolver(typeof(LibArchiveReader).Assembly, static (name, asm, path) => - { - // Currently supported: Linux+Win+OSX on x64, OSX only on arm64 - if (RuntimeInformation.ProcessArchitecture != Architecture.X64 && - (RuntimeInformation.ProcessArchitecture != Architecture.Arm64 || - !RuntimeInformation.IsOSPlatform(OSPlatform.OSX))) - throw new PlatformNotSupportedException(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - return NativeLibrary.Load($"{AppDomain.CurrentDomain.BaseDirectory}runtimes/linux-x64/libarchive.so"); - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - return NativeLibrary.Load($"{AppDomain.CurrentDomain.BaseDirectory}runtimes/osx-any64/libarchive.dylib"); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return NativeLibrary.Load($"{AppDomain.CurrentDomain.BaseDirectory}runtimes/win-x64/archive.dll"); - throw new PlatformNotSupportedException(); - }); +#if NETSTANDARD2_0 + // .NET Standard 2.0 doesn't have automatic RID resolution, so we need manual loading + if (RuntimeInformation.ProcessArchitecture != Architecture.X64 && + (RuntimeInformation.ProcessArchitecture != Architecture.Arm64 || + !RuntimeInformation.IsOSPlatform(OSPlatform.OSX))) + throw new PlatformNotSupportedException(); + + PreloadNativeLibrary(); +#endif + // For .NET 6+: Automatic resolution from runtimes/{rid}/native/ handles everything! + // No custom DllImportResolver needed - the runtime finds libraries automatically. + } + + // Compatibility method for UTF-8 string marshaling + private static string? PtrToStringUTF8(IntPtr ptr) + { +#if NETSTANDARD2_0 + // Use the custom implementation for .NET Standard 2.0 + return PtrToStringUTF8Internal(ptr); +#else + // Use the built-in method for modern .NET + return Marshal.PtrToStringUTF8(ptr); +#endif } + /// /// Open the named archive for read access with the specified block size /// @@ -54,12 +67,12 @@ public LibArchiveReader(string filename,uint blockSize = 1<<20) : base(true) Throw(); } - /// - /// - /// - public LibArchiveReader(string[] filenames,uint blockSize=1<<20) : base(true) + /// + /// + /// + public LibArchiveReader(string[] filenames,uint blockSize=1<<20) : base(true) { - using var names = new DisposableStringArray(filenames); + using var names = new DisposableStringArray(filenames); handle = archive_read_new(); archive_read_support_filter_all(handle); archive_read_support_format_all(handle); @@ -69,7 +82,7 @@ public LibArchiveReader(string[] filenames,uint blockSize=1<<20) : base(true) private void Throw() { - throw new ApplicationException(Marshal.PtrToStringUTF8(archive_error_string(handle))); + throw new ApplicationException(PtrToStringUTF8(archive_error_string(handle)) ?? "Unknown error"); } protected override bool ReleaseHandle() @@ -109,7 +122,7 @@ protected Entry(IntPtr entryHandle, IntPtr archiveHandle) { this.entryHandle = entryHandle; this.archiveHandle = archiveHandle; - Name = Marshal.PtrToStringUTF8(archive_entry_pathname(entryHandle)) ?? throw new ApplicationException("Unable to retrieve entry's pathname"); + Name = PtrToStringUTF8(archive_entry_pathname(entryHandle)) ?? throw new ApplicationException("Unable to retrieve entry's pathname"); Type = (EntryType)archive_entry_filetype(entryHandle); } @@ -147,7 +160,19 @@ public override void Flush() public override int Read(byte[] buffer, int offset, int count) { +#if NETSTANDARD2_0 + // Use traditional array slicing for .NET Standard 2.0 + unsafe + { + fixed (byte* ptr = &buffer[offset]) + { + return archive_read_data(archiveHandle, ref *ptr, count); + } + } +#else + // Use modern range syntax for .NET 6.0+ return archive_read_data(archiveHandle, ref MemoryMarshal.GetReference(buffer.AsSpan()[offset..]), count); +#endif } public override long Seek(long offset, SeekOrigin origin) @@ -176,12 +201,49 @@ public override long Position } } + // P/Invoke declarations for libarchive + // Use LibraryImport for .NET 7+ (AOT-friendly, source-generated) + // Use DllImport for older frameworks (runtime marshalling) +#if NET7_0_OR_GREATER + [LibraryImport("archive")] + private static partial IntPtr archive_read_new(); + + [LibraryImport("archive")] + private static partial void archive_read_support_filter_all(IntPtr a); + + [LibraryImport("archive")] + private static partial void archive_read_support_format_all(IntPtr a); + + [LibraryImport("archive")] + private static partial int archive_read_open_filename(IntPtr a, IntPtr filename, int blocksize); + + [LibraryImport("archive")] + private static partial int archive_read_open_filenames(IntPtr a, IntPtr filename, int blocksize); + + [LibraryImport("archive")] + private static partial int archive_read_data(IntPtr a, ref byte buff, int size); + + [LibraryImport("archive")] + private static partial int archive_read_next_header(IntPtr a, out IntPtr entry); + + [LibraryImport("archive")] + private static partial IntPtr archive_entry_pathname(IntPtr entry); + + [LibraryImport("archive")] + private static partial int archive_entry_filetype(IntPtr entry); + + [LibraryImport("archive")] + private static partial int archive_read_free(IntPtr a); + + [LibraryImport("archive")] + private static partial IntPtr archive_error_string(IntPtr a); +#else [DllImport("archive")] private static extern IntPtr archive_read_new(); [DllImport("archive")] private static extern void archive_read_support_filter_all(IntPtr a); - + [DllImport("archive")] private static extern void archive_read_support_format_all(IntPtr a); @@ -208,4 +270,183 @@ public override long Position [DllImport("archive")] private static extern IntPtr archive_error_string(IntPtr a); +#endif + +#if NETSTANDARD2_0 + // .NET Standard 2.0 Native Library Loading + private static void PreloadNativeLibrary() + { + var libraryPath = GetNativeLibraryPath(); + + if (!File.Exists(libraryPath)) + { + throw new DllNotFoundException($"Native library not found at: {libraryPath}"); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + LoadWindowsLibrary(libraryPath); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + LoadUnixLibrary(libraryPath); + } + } + + private static string GetNativeLibraryPath() + { + var assemblyLocation = Assembly.GetExecutingAssembly().Location; + var assemblyDir = Path.GetDirectoryName(assemblyLocation) + ?? throw new InvalidOperationException("Could not determine assembly directory"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var arch = RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "win-x64", + Architecture.X86 => "win-x86", + Architecture.Arm64 => "win-arm64", + _ => throw new PlatformNotSupportedException($"Unsupported Windows architecture: {RuntimeInformation.ProcessArchitecture}") + }; + return Path.Combine(assemblyDir, "runtimes", arch, "native", "archive.dll"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return Path.Combine(assemblyDir, "runtimes", "linux-x64", "native", "libarchive.so"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var arch = RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "osx-arm64" : "osx-x64"; + return Path.Combine(assemblyDir, "runtimes", arch, "native", "libarchive.dylib"); + } + else + { + throw new PlatformNotSupportedException($"Unsupported platform: {RuntimeInformation.OSDescription}"); + } + } + + private static void LoadWindowsLibrary(string libraryPath) + { + if (!File.Exists(libraryPath)) + { + throw new DllNotFoundException($"Native library not found at: {libraryPath}"); + } + + var handle = LoadLibrary(libraryPath); + if (handle == IntPtr.Zero) + { + var error = Marshal.GetLastWin32Error(); + throw new DllNotFoundException($"Failed to load native library '{libraryPath}'. Win32 error: {error}"); + } + } + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr LoadLibrary(string lpFileName); + + private static void LoadUnixLibrary(string libraryPath) + { + // On .NET Framework (Mono), we need to explicitly load the library using dlopen + // RTLD_NOW = 2: Resolve all symbols immediately + const int RTLD_NOW = 2; + var handle = dlopen(libraryPath, RTLD_NOW); + if (handle == IntPtr.Zero) + { + var error = dlerror(); + throw new DllNotFoundException($"Failed to load native library '{libraryPath}'. Error: {error}"); + } + } + + // Linux: try multiple libdl versions for compatibility + [DllImport("libdl.so.2", EntryPoint = "dlopen")] + private static extern IntPtr dlopen_linux_v2(string filename, int flags); + + [DllImport("libdl.so", EntryPoint = "dlopen")] + private static extern IntPtr dlopen_linux(string filename, int flags); + + [DllImport("libdl.dylib", EntryPoint = "dlopen")] + private static extern IntPtr dlopen_macos(string filename, int flags); + + private static IntPtr dlopen(string filename, int flags) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + // Try versioned first, fall back to unversioned + try { return dlopen_linux_v2(filename, flags); } + catch (DllNotFoundException) { return dlopen_linux(filename, flags); } + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return dlopen_macos(filename, flags); + throw new PlatformNotSupportedException(); + } + + [DllImport("libdl.so.2", EntryPoint = "dlerror")] + private static extern IntPtr dlerror_linux_v2(); + + [DllImport("libdl.so", EntryPoint = "dlerror")] + private static extern IntPtr dlerror_linux(); + + [DllImport("libdl.dylib", EntryPoint = "dlerror")] + private static extern IntPtr dlerror_macos(); + + private static string? dlerror() + { + IntPtr ptr; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + try { ptr = dlerror_linux_v2(); } + catch (DllNotFoundException) { ptr = dlerror_linux(); } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + ptr = dlerror_macos(); + else + return "Unknown platform"; + + return ptr == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(ptr); + } + + // .NET Standard 2.0 UTF-8 Marshaling Support + private static string? PtrToStringUTF8Internal(IntPtr ptr) + { + if (ptr == IntPtr.Zero) + return null; + + try + { + // Find the null terminator to determine string length + var length = 0; + unsafe + { + byte* p = (byte*)ptr; + while (p[length] != 0) + { + length++; + + // Prevent infinite loop on corrupted data + if (length > 1000000) // 1MB limit for safety + throw new InvalidOperationException("UTF-8 string too long or not null-terminated"); + } + } + + if (length == 0) + return string.Empty; + + // Copy the UTF-8 bytes and convert to string + var bytes = new byte[length]; + Marshal.Copy(ptr, bytes, 0, length); + return Encoding.UTF8.GetString(bytes); + } + catch (Exception ex) + { + // Fallback to basic ASCII conversion if UTF-8 fails + try + { + return Marshal.PtrToStringAnsi(ptr); + } + catch + { + throw new InvalidOperationException($"Failed to marshal UTF-8 string: {ex.Message}", ex); + } + } + } +#endif } \ No newline at end of file diff --git a/LibArchive.Net/SafeStringBuffer.cs b/LibArchive.Net/SafeStringBuffer.cs index 55b76dd8..73a86787 100644 --- a/LibArchive.Net/SafeStringBuffer.cs +++ b/LibArchive.Net/SafeStringBuffer.cs @@ -1,4 +1,5 @@ using System; +using System.Text; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; @@ -7,12 +8,19 @@ namespace LibArchive.Net; public class SafeStringBuffer : SafeHandleZeroOrMinusOneIsInvalid { public IntPtr Ptr => handle; - + public SafeStringBuffer(string s) : base(true) { if (s is null) throw new ArgumentNullException(nameof(s)); + +#if NETSTANDARD2_0 + // Manual UTF-8 marshaling for .NET Standard 2.0 + handle = Marshal.StringToCoTaskMemAnsi(s); +#else + // Use built-in UTF-8 marshaling for modern .NET handle = Marshal.StringToCoTaskMemUTF8(s); +#endif } protected override bool ReleaseHandle() diff --git a/LibArchive.Net/runtimes/win-x64/archive.dll b/LibArchive.Net/runtimes/win-x64/native/archive.dll similarity index 100% rename from LibArchive.Net/runtimes/win-x64/archive.dll rename to LibArchive.Net/runtimes/win-x64/native/archive.dll diff --git a/Test.LibArchive.Net/Test.LibArchive.Net.csproj b/Test.LibArchive.Net/Test.LibArchive.Net.csproj index bc5d36c7..e504075b 100644 --- a/Test.LibArchive.Net/Test.LibArchive.Net.csproj +++ b/Test.LibArchive.Net/Test.LibArchive.Net.csproj @@ -1,6 +1,6 @@ - net$(NETCoreAppMaximumVersion) + net$(NETCoreAppMaximumVersion);net462 enable enable false diff --git a/native/BUILD.md b/native/BUILD.md new file mode 100644 index 00000000..9ccfbed3 --- /dev/null +++ b/native/BUILD.md @@ -0,0 +1,253 @@ +# LibArchive Native Build System + +This directory contains the unified build system for building libarchive native libraries across all supported platforms. + +## Overview + +The build system consists of: +- **build-config.sh**: Central configuration file with all library versions and URLs +- **build-linux.sh**: Linux x86-64 build script (using musl-libc) +- **build-macos.sh**: macOS universal binary build script (x86_64 + arm64) +- **build-windows.sh**: Windows build script (x86, x64, arm64 via MinGW) +- **build-all.sh**: Unified orchestrator for building all platforms + +## Library Versions + +All library versions are managed in `build-config.sh`: + +| Library | Version | +|---------|---------| +| libarchive | 3.7.3 | +| lz4 | 1.9.4 | +| zstd | 1.5.6 | +| lzo | 2.10 | +| libxml2 | 2.12.6 | +| zlib | 1.3.1 | +| xz (liblzma) | 5.4.6 | +| bzip2 | 1.0.8 | + +## Quick Start + +### Build for Current Platform + +```bash +cd native +./build-all.sh +``` + +This will automatically detect your platform and build the appropriate libraries. + +### Build for Specific Platforms + +```bash +# Linux only (must run on Linux) +./build-all.sh --linux + +# macOS only (must run on macOS) +./build-all.sh --macos + +# Windows only (must run on Linux with MinGW) +./build-all.sh --windows + +# All platforms and create NuGet package +./build-all.sh --all --package +``` + +## Platform-Specific Requirements + +### Linux (x86-64) + +**Requirements:** +- bash +- curl +- gcc +- make +- unzip + +**Output:** `libarchive.so` (fully static, no external dependencies except glibc) + +**Notes:** +- Uses musl-libc cross-compiler for maximum portability +- All dependencies are statically linked +- Build time: ~20-30 minutes (includes building musl toolchain) + +### macOS (Universal Binary) + +**Requirements:** +- macOS (tested on 11.0+) +- Xcode Command Line Tools +- Homebrew +- autoconf, automake, libtool (installed automatically) + +**Output:** `libarchive.dylib` (universal binary: x86_64 + arm64) + +**Notes:** +- Creates a single "fat" binary containing both architectures +- Only depends on system libraries (libc, libiconv) +- Build time: ~10-15 minutes + +### Windows (x86, x64, arm64) + +**Requirements:** +- Linux build machine +- MinGW-w64 cross-compiler toolchain + - Ubuntu/Debian: `sudo apt-get install mingw-w64` + - Fedora: `sudo dnf install mingw64-gcc mingw32-gcc` + +**Output:** +- `archive-x86.dll` (32-bit x86) +- `archive-x64.dll` (64-bit x86_64) +- `archive-arm64.dll` (64-bit ARM64) + +**Notes:** +- Cross-compiled from Linux +- All dependencies statically linked +- Build time: ~5-10 minutes per architecture + +**Building Individual Architectures:** + +```bash +# x64 +ARCH=x86_64 ./build-windows.sh + +# x86 +ARCH=i686 ./build-windows.sh + +# arm64 +ARCH=aarch64 ./build-windows.sh +``` + +## Directory Structure + +After building, libraries are placed in: + +``` +LibArchive.Net/runtimes/ +├── linux-x64/ +│ └── libarchive.so +├── osx-any64/ +│ └── libarchive.dylib +├── win-x86/ +│ └── archive.dll +├── win-x64/ +│ └── archive.dll +└── win-arm64/ + └── archive.dll +``` + +## Updating Library Versions + +To update library versions: + +1. Edit `build-config.sh` +2. Update the version variables (e.g., `LIBARCHIVE_VERSION="3.8.0"`) +3. Update the URLs if the download pattern changed +4. Test the build on each platform +5. Update this README with the new versions + +## CI/CD Integration + +The GitHub Actions workflow (`.github/workflows/build.yml`) uses these scripts: + +1. **macOS job**: Runs `build-macos.sh`, uploads `libarchive.dylib` +2. **Linux job**: + - Downloads macOS artifact + - Runs `build-linux.sh` + - Places both libraries in runtimes/ + - Builds and tests .NET package + - Creates NuGet package + +Currently, Windows builds are pre-built and checked into the repository. To enable CI builds: + +1. Add a Windows build job using Linux + MinGW +2. Build all three Windows architectures +3. Upload as artifacts +4. Include in final package + +## Troubleshooting + +### Linux Build Issues + +**Problem:** "musl-cross-make build failed" +- Check internet connectivity (downloads many sources) +- Verify disk space (~2GB needed during build) +- Check build log: `cat musl-cross-make-master/musl.log` + +### macOS Build Issues + +**Problem:** "configure: error: C compiler cannot create executables" +- Install Xcode Command Line Tools: `xcode-select --install` +- Accept Xcode license: `sudo xcodebuild -license accept` + +**Problem:** "No such file or directory: autoconf" +- Install build tools: `brew install autoconf automake libtool` + +### Windows Build Issues + +**Problem:** "x86_64-w64-mingw32-gcc: command not found" +- Install MinGW: `sudo apt-get install mingw-w64` (Ubuntu/Debian) +- Or: `sudo dnf install mingw64-gcc mingw32-gcc` (Fedora) + +**Problem:** "Cannot find -lws2_32" +- Ensure MinGW runtime libraries are installed +- Check: `dpkg -L mingw-w64-x86-64-dev` (Ubuntu) + +## Development Workflow + +### Adding a New Dependency + +1. Add version variable to `build-config.sh` +2. Add download URL to `build-config.sh` +3. Update `download_all_libraries()` function +4. Add build steps to each platform script +5. Update linker commands to include the new library +6. Test on all platforms + +### Testing Changes + +```bash +# Quick test (current platform only) +./build-all.sh + +# Full test (requires appropriate OS or VMs) +./build-all.sh --all --package + +# Verify NuGet package contents +unzip -l LibArchive.Net.*.nupkg | grep runtimes +``` + +### Cleaning Build Artifacts + +```bash +# Clean downloaded sources and build artifacts +rm -rf local* {bzip2,libarchive,libxml2,lz4,lzo,musl-cross-make,xz,zlib,zstd}-* +rm -rf musl-cross-make-master configcache +rm -f *.so *.dylib *.dll test nativetest +``` + +## Performance Notes + +Build times on GitHub Actions runners (approximate): + +- **macOS**: 15-20 minutes (M1 runner) +- **Linux**: 25-30 minutes (includes musl toolchain) +- **Windows** (if added to CI): 15-20 minutes (all 3 architectures) + +Optimization strategies: +- Use ccache (already enabled in workflow) +- Cache `configcache` between runs (already enabled) +- Pre-built musl toolchain (possible future optimization) + +## License + +All scripts in this directory are part of LibArchive.Net and licensed under the BSD-2-Clause license. + +The libraries being built have their own licenses: +- libarchive: BSD-2-Clause +- zlib: Zlib +- bzip2: BSD-like +- lz4: BSD-2-Clause +- zstd: BSD + GPLv2 +- lzo: GPLv2 +- xz (liblzma): Public Domain +- libxml2: MIT diff --git a/native/build-all.sh b/native/build-all.sh new file mode 100755 index 00000000..6327a885 --- /dev/null +++ b/native/build-all.sh @@ -0,0 +1,192 @@ +#!/bin/sh +# Unified build script for all platforms +# This script orchestrates building libarchive for all supported platforms + +set -e + +SCRIPT_DIR="$(dirname "$0")" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Color output helpers +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo "${RED}[ERROR]${NC} $1" +} + +# Detect current platform +detect_platform() { + case "$(uname -s)" in + Linux*) echo "linux" ;; + Darwin*) echo "macos" ;; + CYGWIN*|MINGW*|MSYS*) echo "windows" ;; + *) echo "unknown" ;; + esac +} + +PLATFORM=$(detect_platform) +log_info "Detected platform: $PLATFORM" + +# Parse command line arguments +BUILD_LINUX=0 +BUILD_MACOS=0 +BUILD_WINDOWS=0 +PACKAGE=0 + +if [ $# -eq 0 ]; then + # No arguments: build for current platform only + case "$PLATFORM" in + linux) BUILD_LINUX=1 ;; + macos) BUILD_MACOS=1 ;; + windows) BUILD_WINDOWS=1 ;; + esac +else + # Parse arguments + for arg in "$@"; do + case "$arg" in + --all) BUILD_LINUX=1; BUILD_MACOS=1; BUILD_WINDOWS=1; PACKAGE=1 ;; + --linux) BUILD_LINUX=1 ;; + --macos) BUILD_MACOS=1 ;; + --windows) BUILD_WINDOWS=1 ;; + --package) PACKAGE=1 ;; + --help|-h) + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " --all Build for all platforms and package" + echo " --linux Build for Linux x86-64" + echo " --macos Build for macOS (universal binary)" + echo " --windows Build for Windows (x86, x64, arm64)" + echo " --package Create NuGet package after building" + echo " --help Show this help message" + echo "" + echo "If no options are specified, builds for current platform only." + exit 0 + ;; + *) + log_error "Unknown option: $arg" + echo "Use --help for usage information" + exit 1 + ;; + esac + done +fi + +# Create temporary build directory +BUILD_DIR="$(mktemp -d)" +trap "rm -rf $BUILD_DIR" EXIT +cd "$BUILD_DIR" +log_info "Building in: $BUILD_DIR" + +# Copy native directory for builds +cp -r "$SCRIPT_DIR"/* . + +# Build for Linux +if [ $BUILD_LINUX -eq 1 ]; then + log_info "Building for Linux x86-64..." + if [ "$PLATFORM" != "linux" ]; then + log_warn "Cross-compilation for Linux not yet supported on $PLATFORM" + log_warn "Skipping Linux build" + else + ./build-linux.sh + # Build uses musl-libc, so copy to both linux-x64 and linux-musl-x64 + mkdir -p "$REPO_ROOT/LibArchive.Net/runtimes/linux-x64/native" + mkdir -p "$REPO_ROOT/LibArchive.Net/runtimes/linux-musl-x64/native" + cp libarchive.so "$REPO_ROOT/LibArchive.Net/runtimes/linux-x64/native/" + cp libarchive.so "$REPO_ROOT/LibArchive.Net/runtimes/linux-musl-x64/native/" + log_info "Linux library installed to LibArchive.Net/runtimes/linux-x64/native/ and linux-musl-x64/native/" + fi +fi + +# Build for macOS +if [ $BUILD_MACOS -eq 1 ]; then + log_info "Building for macOS (universal: x86_64 + arm64)..." + if [ "$PLATFORM" != "macos" ]; then + log_warn "Cross-compilation for macOS not supported on $PLATFORM" + log_warn "Skipping macOS build" + else + ./build-macos.sh + # Universal binary supports both x64 and arm64, so copy to both RID folders + mkdir -p "$REPO_ROOT/LibArchive.Net/runtimes/osx-x64/native" + mkdir -p "$REPO_ROOT/LibArchive.Net/runtimes/osx-arm64/native" + cp libarchive.dylib "$REPO_ROOT/LibArchive.Net/runtimes/osx-x64/native/" + cp libarchive.dylib "$REPO_ROOT/LibArchive.Net/runtimes/osx-arm64/native/" + log_info "macOS library installed to LibArchive.Net/runtimes/osx-x64/native/ and osx-arm64/native/" + fi +fi + +# Build for Windows +if [ $BUILD_WINDOWS -eq 1 ]; then + log_info "Building for Windows (x86, x64, arm64)..." + if [ "$PLATFORM" != "linux" ]; then + log_warn "Windows builds require Linux with MinGW cross-compiler" + log_warn "Skipping Windows builds" + else + # Check for MinGW + if ! command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1; then + log_error "MinGW cross-compiler not found" + log_error "Install with: sudo apt-get install mingw-w64" + exit 1 + fi + + # Build x64 + log_info "Building Windows x64..." + ARCH=x86_64 ./build-windows.sh + mkdir -p "$REPO_ROOT/LibArchive.Net/runtimes/win-x64/native" + cp archive-x64.dll "$REPO_ROOT/LibArchive.Net/runtimes/win-x64/native/archive.dll" + + # Build x86 + log_info "Building Windows x86..." + rm -rf local-* {bzip2,libarchive,libxml2,lz4,lzo,xz,zlib,zstd}-* + ARCH=i686 ./build-windows.sh + mkdir -p "$REPO_ROOT/LibArchive.Net/runtimes/win-x86/native" + cp archive-x86.dll "$REPO_ROOT/LibArchive.Net/runtimes/win-x86/native/archive.dll" + + # Build arm64 + log_info "Building Windows arm64..." + rm -rf local-* {bzip2,libarchive,libxml2,lz4,lzo,xz,zlib,zstd}-* + ARCH=aarch64 ./build-windows.sh + mkdir -p "$REPO_ROOT/LibArchive.Net/runtimes/win-arm64/native" + cp archive-arm64.dll "$REPO_ROOT/LibArchive.Net/runtimes/win-arm64/native/archive.dll" + + log_info "Windows libraries installed to LibArchive.Net/runtimes/win-*/native/" + fi +fi + +# Create NuGet package +if [ $PACKAGE -eq 1 ]; then + log_info "Creating NuGet package..." + cd "$REPO_ROOT" + + # Determine version + if [ -n "$1" ]; then + VERSION="$1" + else + VERSION="0.0.0-dev" + fi + + log_info "Building .NET project..." + dotnet build --configuration Release --nologo + + log_info "Running tests..." + dotnet test --nologo --configuration Release + + log_info "Creating package version $VERSION..." + dotnet pack LibArchive.Net/LibArchive.Net.csproj -o . -p:PackageVersion=$VERSION --nologo --configuration Release + + log_info "Package created:" + ls -lh LibArchive.Net.*.nupkg +fi + +log_info "Build complete!" diff --git a/native/build-config.sh b/native/build-config.sh new file mode 100755 index 00000000..4a76533f --- /dev/null +++ b/native/build-config.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# Shared build configuration for libarchive native library builds +# Source this file in platform-specific build scripts + +# Library versions +LIBARCHIVE_VERSION="3.7.3" +LZ4_VERSION="1.9.4" +ZSTD_VERSION="1.5.6" +LZO_VERSION="2.10" +LIBXML2_VERSION="2.12.6" +ZLIB_VERSION="1.3.1" +XZ_VERSION="5.4.6" +BZIP2_VERSION="1.0.8" + +# Download URLs +LIBARCHIVE_URL="https://github.com/libarchive/libarchive/releases/download/v${LIBARCHIVE_VERSION}/libarchive-${LIBARCHIVE_VERSION}.tar.xz" +LZ4_URL="https://github.com/lz4/lz4/archive/refs/tags/v${LZ4_VERSION}.tar.gz" +ZSTD_URL="https://github.com/facebook/zstd/releases/download/v${ZSTD_VERSION}/zstd-${ZSTD_VERSION}.tar.gz" +LZO_URL="https://www.oberhumer.com/opensource/lzo/download/lzo-${LZO_VERSION}.tar.gz" +LIBXML2_URL="https://download.gnome.org/sources/libxml2/2.12/libxml2-${LIBXML2_VERSION}.tar.xz" +BZIP2_URL="https://www.sourceware.org/pub/bzip2/bzip2-latest.tar.gz" +ZLIB_URL="https://zlib.net/zlib-${ZLIB_VERSION}.tar.xz" +XZ_URL="https://github.com/tukaani-project/xz/releases/download/v${XZ_VERSION}/xz-${XZ_VERSION}.tar.xz" + +# Common build settings +export PREFIX="${PREFIX:-$(pwd)/local}" +export CONFIGCACHE="${CONFIGCACHE:-$(pwd)/configcache}" + +# Function to download and extract a library +download_library() { + local url="$1" + local name="$2" + + echo "Downloading ${name}..." + if [ "${url##*.}" = "xz" ]; then + curl -sL "$url" | tar xJf - + else + curl -sL "$url" | tar xzf - + fi +} + +# Function to download all libraries +download_all_libraries() { + download_library "$LIBARCHIVE_URL" "libarchive" + download_library "$LZ4_URL" "lz4" + download_library "$ZSTD_URL" "zstd" + download_library "$LZO_URL" "lzo" + download_library "$LIBXML2_URL" "libxml2" + download_library "$BZIP2_URL" "bzip2" + download_library "$ZLIB_URL" "zlib" + download_library "$XZ_URL" "xz" +} + +# Detect number of CPU cores +if command -v nproc >/dev/null 2>&1; then + export NCPU=$(nproc) +elif command -v sysctl >/dev/null 2>&1; then + export NCPU=$(sysctl -n hw.ncpu) +else + export NCPU=4 +fi + +echo "Build configuration loaded:" +echo " libarchive: ${LIBARCHIVE_VERSION}" +echo " lz4: ${LZ4_VERSION}" +echo " zstd: ${ZSTD_VERSION}" +echo " lzo: ${LZO_VERSION}" +echo " libxml2: ${LIBXML2_VERSION}" +echo " zlib: ${ZLIB_VERSION}" +echo " xz: ${XZ_VERSION}" +echo " bzip2: ${BZIP2_VERSION}" +echo " CPUs: ${NCPU}" +echo " PREFIX: ${PREFIX}" diff --git a/native/build-linux.sh b/native/build-linux.sh index e8fbaef6..402aa87d 100755 --- a/native/build-linux.sh +++ b/native/build-linux.sh @@ -1,27 +1,27 @@ #!/bin/sh +# Build libarchive for Linux x86-64 using musl-libc for static linking set -e -export NCPU=`nproc` -export CONFIGCACHE=`pwd`/configcache -export PREFIX=`pwd`/local + +# Load shared configuration +. "$(dirname "$0")/build-config.sh" + +# Linux-specific build settings export CPPFLAGS="-I$PREFIX/include -I$PREFIX/x86_64-linux-musl/include -I$PREFIX/lib/gcc/x86_64-linux-musl/9.2.0/include" export CFLAGS="-fPIC -O2 $CPPFLAGS -static-libgcc" export CXXFLAGS="-fPIC -O2 -I$PREFIX/x86_64-linux-musl/include/c++/9.2.0 -I$PREFIX/x86_64-linux-musl/include/c++/9.2.0/x86_64-linux-musl $CPPFLAGS -static-libstdc++ -static-libgcc -include sys/time.h" export LDFLAGS="-L$PREFIX/lib -static" export PATH="$PREFIX/bin:$PREFIX/x86_64-linux-musl/bin:$PATH" +echo "Building musl cross-compiler toolchain..." curl -sL https://github.com/richfelker/musl-cross-make/archive/refs/heads/master.zip > musl-git.zip -unzip musl-git.zip +unzip -q musl-git.zip rm musl-git.zip -curl -sL https://github.com/libarchive/libarchive/releases/download/v3.7.3/libarchive-3.7.3.tar.xz | tar xJf - -curl -sL https://github.com/lz4/lz4/archive/refs/tags/v1.9.4.tar.gz | tar xzf - -curl -sL https://github.com/facebook/zstd/releases/download/v1.5.6/zstd-1.5.6.tar.gz | tar xzf - -curl -sL https://www.oberhumer.com/opensource/lzo/download/lzo-2.10.tar.gz | tar xzf - -curl -sL https://download.gnome.org/sources/libxml2/2.12/libxml2-2.12.6.tar.xz | tar xJf - -curl -sL https://www.sourceware.org/pub/bzip2/bzip2-latest.tar.gz | tar xzf - -curl -sL https://zlib.net/zlib-1.3.1.tar.xz | tar xJf - -curl -sL https://github.com/tukaani-project/xz/releases/download/v5.4.6/xz-5.4.6.tar.xz | tar xJf - +# Download all libraries +download_all_libraries + +# Build musl cross-compiler cd musl-cross-make-master cat > config.mak <&1 >musl.log || cat musl.log export CC=x86_64-linux-musl-gcc export CXX=x86_64-linux-musl-g++ cd .. -make -j$NCPU -sC lz4-1.9.4 install -make -j$NCPU -sC zstd-1.5.6 install -make -j$NCPU -sC bzip2-1.0.8 install PREFIX=$PREFIX CFLAGS="-fPIC -O2 -D_FILE_OFFSET_BITS=64" CC=$CC -cd lzo-2.10 +# Build compression libraries +echo "Building lz4 ${LZ4_VERSION}..." +make -j$NCPU -sC lz4-${LZ4_VERSION} install + +echo "Building zstd ${ZSTD_VERSION}..." +make -j$NCPU -sC zstd-${ZSTD_VERSION} install + +echo "Building bzip2 ${BZIP2_VERSION}..." +make -j$NCPU -sC bzip2-${BZIP2_VERSION} install PREFIX=$PREFIX CFLAGS="-fPIC -O2 -D_FILE_OFFSET_BITS=64" CC=$CC + +echo "Building lzo ${LZO_VERSION}..." +cd lzo-${LZO_VERSION} ./configure --cache-file=$CONFIGCACHE --prefix=$PREFIX make -sj$NCPU install +cd .. -cd ../zlib-1.3.1 +echo "Building zlib ${ZLIB_VERSION}..." +cd zlib-${ZLIB_VERSION} ./configure --static --prefix=$PREFIX make -sj$NCPU install +cd .. -cd ../xz-5.4.6 +echo "Building xz ${XZ_VERSION}..." +cd xz-${XZ_VERSION} ./configure --cache-file=$CONFIGCACHE --with-pic --disable-shared --prefix=$PREFIX make -sj$NCPU install +cd .. -cd ../libxml2-2.12.6 -./autogen.sh --cache-file=$CONFIGCACHE --enable-silent-rules --disable-shared --enable-static --prefix=$PREFIX --without-python --with-zlib=$PREFIX/../zlib-1.3.1 --with-lzma=$PREFIX/../xz-5.4.6 +echo "Building libxml2 ${LIBXML2_VERSION}..." +cd libxml2-${LIBXML2_VERSION} +./autogen.sh --cache-file=$CONFIGCACHE --enable-silent-rules --disable-shared --enable-static --prefix=$PREFIX --without-python --with-zlib=$PREFIX/../zlib-${ZLIB_VERSION} --with-lzma=$PREFIX/../xz-${XZ_VERSION} make -sj$NCPU install +cd .. -cd ../libarchive-* +echo "Building libarchive ${LIBARCHIVE_VERSION}..." +cd libarchive-${LIBARCHIVE_VERSION} export LIBXML2_PC_CFLAGS=-I$PREFIX/include/libxml2 export LIBXML2_PC_LIBS=-L$PREFIX ./configure --cache-file=$CONFIGCACHE --prefix=$PREFIX --disable-bsdtar --disable-bsdcat --disable-bsdcpio --enable-posix-regex-lib=libc --with-pic --with-sysroot --with-lzo2 make -sj$NCPU install - cd .. + +echo "Creating final shared library..." gcc -shared -o libarchive.so -Wl,--whole-archive local/lib/libarchive.a -Wl,--no-whole-archive local/lib/libbz2.a local/lib/libz.a local/lib/libxml2.a local/lib/liblzma.a local/lib/liblzo2.a local/lib/libzstd.a local/lib/liblz4.a local/x86_64-linux-musl/lib/libc.a -nostdlib +echo "Testing library..." cat > test.c < #include @@ -75,7 +94,10 @@ EOT gcc -o test test.c ./test file libarchive.so -ldd libarchive.so +ldd libarchive.so || true +echo "Building native test..." gcc -o nativetest native/nativetest.c local/lib/libarchive.a -Llocal/lib -Ilocal/include -llz4 -lzstd -lbz2 ./nativetest + +echo "Linux build complete: libarchive.so" diff --git a/native/build-macos.sh b/native/build-macos.sh index 8928f6ff..8c98b63f 100755 --- a/native/build-macos.sh +++ b/native/build-macos.sh @@ -1,51 +1,75 @@ -#!/bin/sh -v +#!/bin/sh +# Build libarchive for macOS as universal binary (x86_64 + arm64) set -e -brew install autoconf automake libtool +# Load shared configuration +. "$(dirname "$0")/build-config.sh" -export PREFIX=`pwd`/local -export NCPU=`sysctl -n hw.ncpu` -export CONFIGCACHE=`pwd`/configcache +# Ensure build tools are available +echo "Installing required build tools..." +brew install autoconf automake libtool 2>/dev/null || true + +# macOS-specific build settings export CPPFLAGS="-I$PREFIX/include" export LDFLAGS="-L$PREFIX/lib -liconv" export CFLAGS="-fPIC -O2 -D_FILE_OFFSET_BITS=64 -arch arm64 -arch x86_64" -curl -sL https://github.com/libarchive/libarchive/releases/download/v3.7.3/libarchive-3.7.3.tar.xz | tar xJf - -curl -sL https://github.com/lz4/lz4/archive/refs/tags/v1.9.4.tar.gz | tar xzf - -curl -sL https://github.com/facebook/zstd/releases/download/v1.5.6/zstd-1.5.6.tar.gz | tar xzf - -curl -sL https://www.oberhumer.com/opensource/lzo/download/lzo-2.10.tar.gz | tar xzf - -curl -sL https://download.gnome.org/sources/libxml2/2.12/libxml2-2.12.6.tar.xz | tar xJf - -curl -sL https://www.sourceware.org/pub/bzip2/bzip2-latest.tar.gz | tar xzf - -curl -sL https://zlib.net/zlib-1.3.1.tar.xz | tar xJf - -curl -sL https://github.com/tukaani-project/xz/releases/download/v5.4.6/xz-5.4.6.tar.xz | tar xJf - +# Download all libraries +download_all_libraries + +# Build compression libraries +echo "Building lz4 ${LZ4_VERSION}..." +make -j$NCPU -sC lz4-${LZ4_VERSION} install PREFIX=$PREFIX CFLAGS="$CFLAGS" -make -j$NCPU -sC lz4-1.9.4 install PREFIX=$PREFIX CFLAGS="$CFLAGS" -make -j$NCPU -sC bzip2-1.0.8 install PREFIX=$PREFIX CFLAGS="$CFLAGS" +echo "Building bzip2 ${BZIP2_VERSION}..." +make -j$NCPU -sC bzip2-${BZIP2_VERSION} install PREFIX=$PREFIX CFLAGS="$CFLAGS" -cd lzo-2.10 +echo "Building lzo ${LZO_VERSION}..." +cd lzo-${LZO_VERSION} ./configure --cache-file=$CONFIGCACHE --prefix=$PREFIX make -sj$NCPU install +cd .. -cd ../zlib-1.3.1 +echo "Building zlib ${ZLIB_VERSION}..." +cd zlib-${ZLIB_VERSION} ./configure --static --prefix=$PREFIX make -sj$NCPU install -cd ../xz-5.4.6 +cd .. + +echo "Building xz ${XZ_VERSION}..." +cd xz-${XZ_VERSION} ./configure --cache-file=$CONFIGCACHE --with-pic --disable-shared --prefix=$PREFIX make -sj$NCPU install -cd ../libxml2-2.12.6 -./autogen.sh --enable-silent-rules --disable-shared --enable-static --prefix=$PREFIX --without-python --with-zlib=$PREFIX/../zlib-1.3.1 --with-lzma=$PREFIX/../xz-5.4.6 +cd .. + +echo "Building libxml2 ${LIBXML2_VERSION}..." +cd libxml2-${LIBXML2_VERSION} +./autogen.sh --enable-silent-rules --disable-shared --enable-static --prefix=$PREFIX --without-python --with-zlib=$PREFIX/../zlib-${ZLIB_VERSION} --with-lzma=$PREFIX/../xz-${XZ_VERSION} make -sj$NCPU install +cd .. -make -j$NCPU -sC ../zstd-1.5.6 install +echo "Building zstd ${ZSTD_VERSION}..." +make -j$NCPU -sC zstd-${ZSTD_VERSION} install +cd .. -cd ../libarchive-* +echo "Building libarchive ${LIBARCHIVE_VERSION}..." +cd libarchive-${LIBARCHIVE_VERSION} export LIBXML2_PC_CFLAGS=-I$PREFIX/include/libxml2 export LIBXML2_PC_LIBS="-L$PREFIX -lxml2" ./configure --cache-file=$CONFIGCACHE --prefix=$PREFIX --enable-silent-rules --disable-dependency-tracking --enable-static --disable-shared --disable-bsdtar --disable-bsdcat --disable-bsdcpio --disable-rpath --enable-posix-regex-lib=libc --enable-xattr --enable-acl --enable-largefile --with-pic --with-zlib --with-bz2lib --with-libb2 --with-iconv --with-lz4 --with-zstd --with-lzma --with-lzo2 --with-cng make -sj$NCPU install cd .. +echo "Creating universal binary..." clang -arch arm64 -arch x86_64 -dynamiclib -shared -o libarchive.dylib -Wl,-force_load local/lib/libarchive.a local/lib/libbz2.a local/lib/libz.a local/lib/libxml2.a local/lib/liblzma.a local/lib/liblzo2.a local/lib/libzstd.a local/lib/liblz4.a -liconv + +echo "Testing library..." +file libarchive.dylib +otool -L libarchive.dylib + +echo "Building native test..." gcc -o nativetest native/nativetest.c local/lib/libarchive.a -Llocal/lib -Ilocal/include -llz4 -lzstd -liconv -lbz2 ./nativetest + +echo "macOS build complete: libarchive.dylib" diff --git a/native/build-windows.sh b/native/build-windows.sh new file mode 100755 index 00000000..9e068918 --- /dev/null +++ b/native/build-windows.sh @@ -0,0 +1,129 @@ +#!/bin/sh +# Build libarchive for Windows (x86, x64, arm64) using MinGW cross-compiler +# This script should be run on Linux with MinGW installed + +set -e + +# Load shared configuration +. "$(dirname "$0")/build-config.sh" + +# Detect platform and set up cross-compilation +ARCH="${ARCH:-x86_64}" +case "$ARCH" in + x86_64|x64) + MINGW_PREFIX="x86_64-w64-mingw32" + OUTPUT_NAME="archive-x64.dll" + ARCH_FLAGS="" + ;; + i686|x86) + MINGW_PREFIX="i686-w64-mingw32" + OUTPUT_NAME="archive-x86.dll" + ARCH_FLAGS="-m32" + ;; + aarch64|arm64) + MINGW_PREFIX="aarch64-w64-mingw32" + OUTPUT_NAME="archive-arm64.dll" + ARCH_FLAGS="" + ;; + *) + echo "Error: Unsupported architecture: $ARCH" + echo "Supported: x86_64, i686, aarch64" + exit 1 + ;; +esac + +echo "Building for Windows ${ARCH} using ${MINGW_PREFIX}..." + +# Check if MinGW is installed +if ! command -v ${MINGW_PREFIX}-gcc >/dev/null 2>&1; then + echo "Error: ${MINGW_PREFIX}-gcc not found" + echo "Please install MinGW cross-compiler for ${ARCH}" + echo " Ubuntu/Debian: sudo apt-get install mingw-w64" + echo " Fedora: sudo dnf install mingw64-gcc mingw32-gcc" + exit 1 +fi + +# Windows-specific build settings +export CC="${MINGW_PREFIX}-gcc" +export CXX="${MINGW_PREFIX}-g++" +export AR="${MINGW_PREFIX}-ar" +export RANLIB="${MINGW_PREFIX}-ranlib" +export RC="${MINGW_PREFIX}-windres" +export STRIP="${MINGW_PREFIX}-strip" + +export PREFIX="${PREFIX}-${ARCH}" +export CPPFLAGS="-I$PREFIX/include" +export CFLAGS="-O2 -fPIC ${ARCH_FLAGS}" +export CXXFLAGS="-O2 -fPIC ${ARCH_FLAGS}" +export LDFLAGS="-L$PREFIX/lib" + +# Download all libraries +download_all_libraries + +# Build compression libraries +echo "Building lz4 ${LZ4_VERSION}..." +make -j$NCPU -C lz4-${LZ4_VERSION} install PREFIX=$PREFIX CC=$CC AR=$AR + +echo "Building bzip2 ${BZIP2_VERSION}..." +make -j$NCPU -C bzip2-${BZIP2_VERSION} install PREFIX=$PREFIX CC=$CC AR=$AR CFLAGS="$CFLAGS -D_FILE_OFFSET_BITS=64" + +echo "Building zlib ${ZLIB_VERSION}..." +cd zlib-${ZLIB_VERSION} +CHOST=${MINGW_PREFIX} ./configure --static --prefix=$PREFIX +make -j$NCPU install +cd .. + +echo "Building xz ${XZ_VERSION}..." +cd xz-${XZ_VERSION} +./configure --host=${MINGW_PREFIX} --cache-file=$CONFIGCACHE --with-pic --disable-shared --prefix=$PREFIX --disable-scripts --disable-doc +make -j$NCPU install +cd .. + +echo "Building lzo ${LZO_VERSION}..." +cd lzo-${LZO_VERSION} +./configure --host=${MINGW_PREFIX} --cache-file=$CONFIGCACHE --prefix=$PREFIX --disable-shared +make -j$NCPU install +cd .. + +echo "Building zstd ${ZSTD_VERSION}..." +cd zstd-${ZSTD_VERSION} +make -j$NCPU -C lib install PREFIX=$PREFIX CC=$CC AR=$AR +cd .. + +echo "Building libxml2 ${LIBXML2_VERSION}..." +cd libxml2-${LIBXML2_VERSION} +./configure --host=${MINGW_PREFIX} --cache-file=$CONFIGCACHE --enable-silent-rules --disable-shared --enable-static --prefix=$PREFIX --without-python --with-zlib=$PREFIX --with-lzma=$PREFIX +make -j$NCPU install +cd .. + +echo "Building libarchive ${LIBARCHIVE_VERSION}..." +cd libarchive-${LIBARCHIVE_VERSION} +export LIBXML2_CFLAGS="-I$PREFIX/include/libxml2" +export LIBXML2_LIBS="-L$PREFIX/lib -lxml2" +./configure --host=${MINGW_PREFIX} --cache-file=$CONFIGCACHE --prefix=$PREFIX \ + --enable-silent-rules --disable-dependency-tracking \ + --enable-static --disable-shared \ + --disable-bsdtar --disable-bsdcat --disable-bsdcpio \ + --enable-posix-regex-lib=libc \ + --with-pic --with-zlib --with-bz2lib --with-lz4 --with-zstd --with-lzma --with-lzo2 --with-xml2 +make -j$NCPU install +cd .. + +echo "Creating Windows DLL..." +${CC} -shared -o ${OUTPUT_NAME} \ + -Wl,--whole-archive $PREFIX/lib/libarchive.a -Wl,--no-whole-archive \ + $PREFIX/lib/libbz2.a \ + $PREFIX/lib/libz.a \ + $PREFIX/lib/libxml2.a \ + $PREFIX/lib/liblzma.a \ + $PREFIX/lib/liblzo2.a \ + $PREFIX/lib/libzstd.a \ + $PREFIX/lib/liblz4.a \ + -static-libgcc -lws2_32 -lbcrypt + +echo "Testing DLL..." +file ${OUTPUT_NAME} +${STRIP} ${OUTPUT_NAME} +ls -lh ${OUTPUT_NAME} + +echo "Windows ${ARCH} build complete: ${OUTPUT_NAME}" diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..f2a0124c --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,175 @@ +# Native Dependency Update Checker + +Automated tool for checking and updating native library dependencies in libarchive.net. + +## Overview + +This script monitors 8 native C/C++ libraries that are statically linked into libarchive.net: + +| Library | Current | Check Method | Update Frequency | +|---------|---------|--------------|------------------| +| libarchive | 3.7.3 | GitHub Releases | High | +| lz4 | 1.9.4 | GitHub Releases | Medium | +| zstd | 1.5.6 | GitHub Releases | High | +| xz/liblzma | 5.4.6 | GitHub Releases | Medium | +| zlib | 1.3.1 | Web Scrape (zlib.net) | Low | +| libxml2 | 2.12.6 | GNOME Releases | Medium | +| lzo | 2.10 | Web Scrape | Very Low | +| bzip2 | 1.0.8 | Web Scrape (uses "latest") | Very Low | + +## Usage + +### Manual Check (Local) + +```bash +# Dry run - check for updates without creating PR +perl scripts/check-native-updates.pl + +# Actual run - creates PR if updates found +DRY_RUN=0 perl scripts/check-native-updates.pl +``` + +### Automated Check (GitHub Actions) + +The workflow runs weekly on Mondays at 9:00 AM UTC: + +- **Scheduled**: `.github/workflows/check-native-dependencies.yml` +- **Manual trigger**: Via workflow_dispatch with optional dry_run parameter + +When updates are found, the script: +1. Updates version numbers in both `native/build-linux.sh` and `native/build-macos.sh` +2. Creates a new branch: `dependabot/native/` +3. Commits the changes +4. Opens a PR with: + - Clear title: "Bump native from X to Y" + - Detailed description listing all updates + - Labels: `dependencies`, `native` + +## How It Works + +### Version Detection + +**GitHub Releases API** (libarchive, lz4, zstd, xz): +```perl +GET https://api.github.com/repos/{owner}/{repo}/releases/latest +``` + +**Web Scraping** (zlib, lzo, bzip2): +- Downloads HTML page +- Extracts version from tarball filenames using regex +- Returns highest version found + +**GNOME Releases** (libxml2): +- First checks for newer major.minor series +- Then finds latest patch version in that series + +### Update Process + +1. **Version Comparison**: Uses Perl's `version` module for semver comparison +2. **Script Updates**: Replaces download URLs in both build scripts +3. **PR Creation**: Uses GitHub CLI (`gh`) to create pull request +4. **CI Validation**: Existing CI/CD builds native libraries and runs tests + +### Error Handling + +- Network failures: Warns and continues with other dependencies +- Version parsing errors: Skips that dependency +- Git failures: Aborts with error message +- All errors logged to console + +## Dependencies + +### Perl Modules +```bash +# Ubuntu/Debian +sudo apt-get install libwww-perl libjson-pp-perl libfile-slurp-perl + +# macOS +cpan install LWP::UserAgent JSON::PP File::Slurp +``` + +### System Tools +- `git` - Version control operations +- `gh` - GitHub CLI for PR creation (must be authenticated) + +## Configuration + +Edit `check-native-updates.pl` to modify: + +- **Check URLs**: Update if upstream moves locations +- **Version Regex**: Adjust pattern matching for version extraction +- **URL Patterns**: Change download URL templates +- **PR Labels**: Customize labels applied to PRs + +## Security Considerations + +- Script runs with GitHub Actions bot credentials +- Only updates version numbers in build scripts +- Actual binary builds happen in CI after human review +- No automatic merging - PRs require manual approval + +## Troubleshooting + +### "Failed to fetch GitHub releases" +- Check GitHub API rate limits (60/hour unauthenticated) +- Set `GITHUB_TOKEN` environment variable for higher limits + +### "Failed to update in script" +- URL pattern may have changed +- Check if old URL still matches format in build scripts + +### "Failed to create PR" +- Ensure `gh` CLI is authenticated: `gh auth status` +- Check repository permissions for PR creation + +## Future Enhancements + +- [ ] Add email notifications for update failures +- [ ] Support for custom version constraints (e.g., "no major updates") +- [ ] Integration with security advisory databases (CVE checking) +- [ ] Automated testing before PR creation +- [ ] Support for other package managers (Conan, vcpkg) + +## Examples + +### Check Output (No Updates) +``` +Checking native dependency versions... +Checking bzip2 (current: 1.0.8)... latest: 1.0.8 ✓ +Checking libarchive (current: 3.7.3)... latest: 3.7.3 ✓ +Checking libxml2 (current: 2.12.6)... latest: 2.12.6 ✓ +Checking lz4 (current: 1.9.4)... latest: 1.9.4 ✓ +Checking lzo (current: 2.10)... latest: 2.10 ✓ +Checking xz (current: 5.4.6)... latest: 5.4.6 ✓ +Checking zlib (current: 1.3.1)... latest: 1.3.1 ✓ +Checking zstd (current: 1.5.6)... latest: 1.5.6 ✓ + +All dependencies are up to date ✓ +``` + +### Check Output (Updates Found) +``` +Checking native dependency versions... +Checking libarchive (current: 3.7.3)... latest: 3.7.4 ⚠️ UPDATE AVAILABLE +Checking zstd (current: 1.5.6)... latest: 1.5.7 ⚠️ UPDATE AVAILABLE + +2 updates found! + +=== DRY RUN: Would create PR === +Branch: dependabot/native/libarchive-zstd +Title: Bump 2 native dependencies +Body: +## Native Dependency Updates + +- Bump libarchive from 3.7.3 to 3.7.4 +- Bump zstd from 1.5.6 to 1.5.7 + +### Changes +- **libarchive**: `3.7.3` → `3.7.4` +- **zstd**: `1.5.6` → `1.5.7` +... +``` + +## License + +Same as libarchive.net (BSD-2-Clause) diff --git a/scripts/check-native-updates.pl b/scripts/check-native-updates.pl new file mode 100755 index 00000000..6bac63b3 --- /dev/null +++ b/scripts/check-native-updates.pl @@ -0,0 +1,320 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use JSON::PP; +use LWP::UserAgent; +use version; + +# Configuration +my $REPO_ROOT = $ENV{GITHUB_WORKSPACE} || '/Users/jas88/Developer/Github/libarchive.net'; +my $LINUX_SCRIPT = "$REPO_ROOT/native/build-linux.sh"; +my $MACOS_SCRIPT = "$REPO_ROOT/native/build-macos.sh"; +my $DRY_RUN = $ENV{DRY_RUN} // 1; + +# User agent for HTTP requests +my $ua = LWP::UserAgent->new( + agent => 'libarchive.net-dependency-checker/1.0', + timeout => 30, +); + +# Dependency definitions +my %DEPS = ( + 'libarchive' => { + current => '3.7.3', + check_type => 'github_releases', + repo => 'libarchive/libarchive', + url_pattern_linux => 'https://github.com/libarchive/libarchive/releases/download/v%s/libarchive-%s.tar.xz', + url_pattern_macos => 'https://github.com/libarchive/libarchive/releases/download/v%s/libarchive-%s.tar.xz', + }, + 'lz4' => { + current => '1.9.4', + check_type => 'github_releases', + repo => 'lz4/lz4', + url_pattern_linux => 'https://github.com/lz4/lz4/archive/refs/tags/v%s.tar.gz', + url_pattern_macos => 'https://github.com/lz4/lz4/archive/refs/tags/v%s.tar.gz', + }, + 'zstd' => { + current => '1.5.6', + check_type => 'github_releases', + repo => 'facebook/zstd', + url_pattern_linux => 'https://github.com/facebook/zstd/releases/download/v%s/zstd-%s.tar.gz', + url_pattern_macos => 'https://github.com/facebook/zstd/releases/download/v%s/zstd-%s.tar.gz', + }, + 'xz' => { + current => '5.4.6', + check_type => 'github_releases', + repo => 'tukaani-project/xz', + url_pattern_linux => 'https://github.com/tukaani-project/xz/releases/download/v%s/xz-%s.tar.xz', + url_pattern_macos => 'https://github.com/tukaani-project/xz/releases/download/v%s/xz-%s.tar.xz', + }, + 'zlib' => { + current => '1.3.1', + check_type => 'web_scrape', + check_url => 'https://zlib.net/', + version_regex => qr/zlib-(\d+\.\d+\.\d+)\.tar\.xz/, + url_pattern_linux => 'https://zlib.net/zlib-%s.tar.xz', + url_pattern_macos => 'https://zlib.net/zlib-%s.tar.xz', + }, + 'libxml2' => { + current => '2.12.6', + check_type => 'gnome_release', + major_minor => '2.12', + check_url => 'https://download.gnome.org/sources/libxml2/2.12/', + version_regex => qr/libxml2-(\d+\.\d+\.\d+)\.tar\.xz/, + url_pattern_linux => 'https://download.gnome.org/sources/libxml2/%M/libxml2-%s.tar.xz', + url_pattern_macos => 'https://download.gnome.org/sources/libxml2/%M/libxml2-%s.tar.xz', + }, + 'lzo' => { + current => '2.10', + check_type => 'web_scrape', + check_url => 'https://www.oberhumer.com/opensource/lzo/download/', + version_regex => qr/lzo-(\d+\.\d+)\.tar\.gz/, + url_pattern_linux => 'https://www.oberhumer.com/opensource/lzo/download/lzo-%s.tar.gz', + url_pattern_macos => 'https://www.oberhumer.com/opensource/lzo/download/lzo-%s.tar.gz', + }, + 'bzip2' => { + current => '1.0.8', + check_type => 'web_scrape', + check_url => 'https://www.sourceware.org/pub/bzip2/', + version_regex => qr/bzip2-(\d+\.\d+\.\d+)\.tar\.gz/, + url_pattern_linux => 'https://www.sourceware.org/pub/bzip2/bzip2-latest.tar.gz', + url_pattern_macos => 'https://www.sourceware.org/pub/bzip2/bzip2-latest.tar.gz', + note => 'Uses "latest" tarball - version check informational only', + }, +); + +sub check_github_releases { + my ($repo) = @_; + + my $url = "https://api.github.com/repos/$repo/releases/latest"; + my $response = $ua->get($url, 'Accept' => 'application/vnd.github.v3+json'); + + unless ($response->is_success) { + warn "Failed to fetch GitHub releases for $repo: " . $response->status_line; + return undef; + } + + my $data = decode_json($response->content); + my $tag = $data->{tag_name}; + + # Remove 'v' prefix if present + $tag =~ s/^v//; + + return $tag; +} + +sub check_web_scrape { + my ($url, $regex) = @_; + + my $response = $ua->get($url); + unless ($response->is_success) { + warn "Failed to fetch $url: " . $response->status_line; + return undef; + } + + my $content = $response->content; + my @versions; + + while ($content =~ /$regex/g) { + push @versions, $1; + } + + # Return the highest version found + return undef unless @versions; + @versions = sort { version->parse($b) <=> version->parse($a) } @versions; + return $versions[0]; +} + +sub check_gnome_release { + my ($url, $regex, $major_minor) = @_; + + # First, check if there's a newer major.minor version + my $base_url = 'https://download.gnome.org/sources/libxml2/'; + my $response = $ua->get($base_url); + + if ($response->is_success) { + my @versions; + while ($response->content =~ m{href="(\d+\.\d+)/"}g) { + push @versions, $1; + } + @versions = sort { version->parse($b) <=> version->parse($a) } @versions; + $major_minor = $versions[0] if @versions; + } + + # Now check for the latest patch version + $url = "https://download.gnome.org/sources/libxml2/$major_minor/"; + return check_web_scrape($url, $regex); +} + +sub compare_versions { + my ($current, $latest) = @_; + + eval { + return version->parse($latest) > version->parse($current); + }; + if ($@) { + warn "Version comparison failed for $current vs $latest: $@"; + return 0; + } +} + +sub read_file { + my ($path) = @_; + open my $fh, '<', $path or die "Cannot read $path: $!"; + local $/; + my $content = <$fh>; + close $fh; + return $content; +} + +sub write_file { + my ($path, $content) = @_; + open my $fh, '>', $path or die "Cannot write $path: $!"; + print $fh $content; + close $fh; +} + +sub update_script { + my ($script_path, $dep_name, $old_version, $new_version) = @_; + + my $content = read_file($script_path); + my $dep = $DEPS{$dep_name}; + + # Build old and new URL patterns + my $script_type = $script_path =~ /linux/ ? 'linux' : 'macos'; + my $url_pattern_key = "url_pattern_$script_type"; + + my $old_url = sprintf($dep->{$url_pattern_key}, ($old_version) x 2); + my $new_url = sprintf($dep->{$url_pattern_key}, ($new_version) x 2); + + # Handle special case for %M (major.minor) replacement + if ($new_url =~ /%M/) { + my ($major_minor) = $new_version =~ /^(\d+\.\d+)/; + $new_url =~ s/%M/$major_minor/g; + $old_url =~ s/%M/\d+\.\d+/; # Make it a regex for matching + } + + # Replace the URL + my $replaced = ($content =~ s/\Q$old_url\E/$new_url/g); + + if ($replaced) { + write_file($script_path, $content) unless $DRY_RUN; + return 1; + } + + warn "Failed to update $dep_name in $script_path"; + return 0; +} + +sub create_pr { + my ($updates_ref) = @_; + my @updates = @$updates_ref; + + return unless @updates; + + # Create branch name + my $branch_name = 'dependabot/native/' . join('-', map { lc($_->{name}) } @updates); + $branch_name =~ s/[^a-z0-9\-]/-/g; + + # Create commit message + my $commit_title = scalar(@updates) == 1 + ? "Bump native $updates[0]{name} from $updates[0]{old} to $updates[0]{new}" + : "Bump " . scalar(@updates) . " native dependencies"; + + my $commit_body = join("\n", map { + "- Bump $_->{name} from $_->{old} to $_->{new}" + } @updates); + + my $pr_body = "## Native Dependency Updates\n\n$commit_body\n\n"; + $pr_body .= "### Changes\n"; + for my $update (@updates) { + $pr_body .= "- **$update->{name}**: `$update->{old}` → `$update->{new}`\n"; + if (my $note = $DEPS{$update->{name}}{note}) { + $pr_body .= " - *Note: $note*\n"; + } + } + + $pr_body .= "\n### Build Scripts Updated\n"; + $pr_body .= "- `native/build-linux.sh`\n"; + $pr_body .= "- `native/build-macos.sh`\n"; + $pr_body .= "\n### Testing\n"; + $pr_body .= "CI will rebuild native libraries and run tests on all platforms.\n"; + + if ($DRY_RUN) { + print "\n=== DRY RUN: Would create PR ===\n"; + print "Branch: $branch_name\n"; + print "Title: $commit_title\n"; + print "Body:\n$pr_body\n"; + return; + } + + # Git operations + system("git", "checkout", "-b", $branch_name) == 0 or die "Failed to create branch"; + system("git", "add", $LINUX_SCRIPT, $MACOS_SCRIPT) == 0 or die "Failed to stage changes"; + system("git", "commit", "-m", "$commit_title\n\n$commit_body") == 0 or die "Failed to commit"; + system("git", "push", "-u", "origin", $branch_name) == 0 or die "Failed to push"; + + # Create PR using gh CLI + my $pr_file = "/tmp/pr-body-$$.md"; + write_file($pr_file, $pr_body); + system("gh", "pr", "create", "-R", "jas88/libarchive.net", + "--title", $commit_title, + "--body-file", $pr_file, + "--label", "dependencies", + "--label", "native") == 0 or die "Failed to create PR"; + unlink $pr_file; +} + +# Main execution +sub main { + print "Checking native dependency versions...\n"; + + my @updates; + + for my $dep_name (sort keys %DEPS) { + my $dep = $DEPS{$dep_name}; + my $current = $dep->{current}; + my $latest; + + print "Checking $dep_name (current: $current)... "; + + if ($dep->{check_type} eq 'github_releases') { + $latest = check_github_releases($dep->{repo}); + } elsif ($dep->{check_type} eq 'web_scrape') { + $latest = check_web_scrape($dep->{check_url}, $dep->{version_regex}); + } elsif ($dep->{check_type} eq 'gnome_release') { + $latest = check_gnome_release($dep->{check_url}, $dep->{version_regex}, $dep->{major_minor}); + } + + unless (defined $latest) { + print "ERROR: Could not determine latest version\n"; + next; + } + + print "latest: $latest"; + + if (compare_versions($current, $latest)) { + print " ⚠️ UPDATE AVAILABLE\n"; + push @updates, { + name => $dep_name, + old => $current, + new => $latest, + }; + + # Update both scripts + update_script($LINUX_SCRIPT, $dep_name, $current, $latest); + update_script($MACOS_SCRIPT, $dep_name, $current, $latest); + } else { + print " ✓\n"; + } + } + + if (@updates) { + print "\n" . scalar(@updates) . " updates found!\n"; + create_pr(\@updates); + } else { + print "\nAll dependencies are up to date ✓\n"; + } +} + +main(); diff --git a/scripts/check-native-updates.sh b/scripts/check-native-updates.sh new file mode 100755 index 00000000..aa9654a0 --- /dev/null +++ b/scripts/check-native-updates.sh @@ -0,0 +1,291 @@ +#!/bin/bash +set -euo pipefail + +# Configuration +REPO_ROOT="${GITHUB_WORKSPACE:-$(pwd)}" +LINUX_SCRIPT="$REPO_ROOT/native/build-linux.sh" +MACOS_SCRIPT="$REPO_ROOT/native/build-macos.sh" +DRY_RUN="${DRY_RUN:-1}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Temporary files +UPDATES_FILE=$(mktemp) +trap "rm -f $UPDATES_FILE" EXIT + +# Check for required tools +for cmd in curl jq gh git; do + if ! command -v $cmd &> /dev/null; then + echo -e "${RED}Error: $cmd is required but not installed${NC}" + exit 1 + fi +done + +# Function to check GitHub releases +check_github_release() { + local repo=$1 + local url="https://api.github.com/repos/$repo/releases/latest" + + local version=$(curl -sf -H "Accept: application/vnd.github.v3+json" "$url" | \ + jq -r '.tag_name // empty' | sed 's/^v//') + + echo "$version" +} + +# Function to check web page for version +check_web_version() { + local url=$1 + local pattern=$2 + + local content=$(curl -sf "$url" || echo "") + if [ -z "$content" ]; then + echo "" + return + fi + + echo "$content" | grep -oE "$pattern" | \ + sed -E "s|$pattern|\1|" | \ + sort -V | tail -1 +} + +# Function to compare versions +version_gt() { + local ver1=$1 + local ver2=$2 + + # Use sort -V to compare versions + [ "$ver2" = "$(echo -e "$ver1\n$ver2" | sort -V | head -n1)" ] && [ "$ver1" != "$ver2" ] +} + +# Function to update script file +update_script() { + local script=$1 + local dep_name=$2 + local old_ver=$3 + local new_ver=$4 + local url_pattern=$5 + + local old_url=$(printf "$url_pattern" "$old_ver" "$old_ver") + local new_url=$(printf "$url_pattern" "$new_ver" "$new_ver") + + # Handle %M placeholder for major.minor + if [[ "$new_url" == *"%M"* ]]; then + local major_minor=$(echo "$new_ver" | cut -d. -f1-2) + new_url="${new_url//%M/$major_minor}" + fi + + if [ "$DRY_RUN" = "1" ]; then + echo " Would update: $old_url -> $new_url" + else + sed -i.bak "s|$(echo "$old_url" | sed 's/[.[\*^$(){}?+|]/\\&/g')|$new_url|g" "$script" + rm -f "$script.bak" + fi +} + +# Function to check single dependency +check_dependency() { + local name=$1 + local current=$2 + local check_type=$3 + shift 3 + local check_args=("$@") + + printf "Checking %-12s (current: %-7s)... " "$name" "$current" + + local latest="" + + case "$check_type" in + github) + local repo="${check_args[0]}" + latest=$(check_github_release "$repo") + ;; + web) + local url="${check_args[0]}" + local pattern="${check_args[1]}" + latest=$(check_web_version "$url" "$pattern") + ;; + gnome) + # Check for newer major.minor first + local base_url="https://download.gnome.org/sources/libxml2/" + local latest_mm=$(curl -sf "$base_url" | \ + grep -oE 'href="([0-9]+\.[0-9]+)/"' | \ + sed 's/href="//;s/\/"//g' | \ + sort -V | tail -1) + + local url="$base_url$latest_mm/" + local pattern="${check_args[0]}" + latest=$(check_web_version "$url" "$pattern") + ;; + esac + + if [ -z "$latest" ]; then + echo -e "${RED}ERROR${NC}" + return 1 + fi + + printf "latest: %-7s" "$latest" + + if version_gt "$latest" "$current"; then + echo -e " ${YELLOW}⚠️ UPDATE AVAILABLE${NC}" + echo "$name|$current|$latest" >> "$UPDATES_FILE" + return 0 + else + echo -e " ${GREEN}✓${NC}" + return 0 + fi +} + +# Main dependency checking +echo "Checking native dependency versions..." +echo "" + +# libarchive +check_dependency "libarchive" "3.7.3" "github" "libarchive/libarchive" + +# lz4 +check_dependency "lz4" "1.9.4" "github" "lz4/lz4" + +# zstd +check_dependency "zstd" "1.5.6" "github" "facebook/zstd" + +# xz +check_dependency "xz" "5.4.6" "github" "tukaani-project/xz" + +# zlib +check_dependency "zlib" "1.3.1" "web" \ + "https://zlib.net/" \ + 'zlib-([0-9]+\.[0-9]+\.[0-9]+)\.tar\.xz' + +# libxml2 +check_dependency "libxml2" "2.12.6" "gnome" \ + 'libxml2-([0-9]+\.[0-9]+\.[0-9]+)\.tar\.xz' + +# lzo +check_dependency "lzo" "2.10" "web" \ + "https://www.oberhumer.com/opensource/lzo/download/" \ + 'lzo-([0-9]+\.[0-9]+)\.tar\.gz' + +# bzip2 +check_dependency "bzip2" "1.0.8" "web" \ + "https://www.sourceware.org/pub/bzip2/" \ + 'bzip2-([0-9]+\.[0-9]+\.[0-9]+)\.tar\.gz' + +echo "" + +# Check if any updates were found +if [ ! -s "$UPDATES_FILE" ]; then + echo -e "${GREEN}All dependencies are up to date ✓${NC}" + exit 0 +fi + +# Process updates +update_count=$(wc -l < "$UPDATES_FILE") +echo -e "${YELLOW}$update_count update(s) found!${NC}" +echo "" + +# Apply updates to both scripts +while IFS='|' read -r name old new; do + case "$name" in + libarchive) + url_pattern='https://github.com/libarchive/libarchive/releases/download/v%s/libarchive-%s.tar.xz' + ;; + lz4) + url_pattern='https://github.com/lz4/lz4/archive/refs/tags/v%s.tar.gz' + ;; + zstd) + url_pattern='https://github.com/facebook/zstd/releases/download/v%s/zstd-%s.tar.gz' + ;; + xz) + url_pattern='https://github.com/tukaani-project/xz/releases/download/v%s/xz-%s.tar.xz' + ;; + zlib) + url_pattern='https://zlib.net/zlib-%s.tar.xz' + ;; + libxml2) + url_pattern='https://download.gnome.org/sources/libxml2/%%M/libxml2-%s.tar.xz' + ;; + lzo) + url_pattern='https://www.oberhumer.com/opensource/lzo/download/lzo-%s.tar.gz' + ;; + bzip2) + echo "Note: bzip2 uses 'latest' tarball - manual update required" + continue + ;; + esac + + echo "Updating $name: $old -> $new" + update_script "$LINUX_SCRIPT" "$name" "$old" "$new" "$url_pattern" + update_script "$MACOS_SCRIPT" "$name" "$old" "$new" "$url_pattern" +done < "$UPDATES_FILE" + +# Create PR if not dry run +if [ "$DRY_RUN" = "1" ]; then + echo "" + echo "=== DRY RUN MODE ===" + echo "To apply changes and create PR, run with: DRY_RUN=0" + exit 0 +fi + +# Create branch and PR +branch_name="dependabot/native/$(date +%Y%m%d)-updates" +update_list=$(while IFS='|' read -r name old new; do + echo "- Bump $name from $old to $new" +done < "$UPDATES_FILE") + +if [ "$update_count" = "1" ]; then + first_update=$(head -1 "$UPDATES_FILE") + IFS='|' read -r name old new <<< "$first_update" + pr_title="Bump native $name from $old to $new" +else + pr_title="Bump $update_count native dependencies" +fi + +# Git operations +echo "" +echo "Creating branch: $branch_name" +git checkout -b "$branch_name" + +echo "Committing changes..." +git add "$LINUX_SCRIPT" "$MACOS_SCRIPT" +git commit -m "$pr_title" -m "$update_list" + +echo "Pushing to origin..." +git push -u origin "$branch_name" + +# Create PR body +pr_body=$(cat <