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 <