name: Build Platforms

# Reusable workflow for building RustNet on all platforms.
# Called by both test-platform-builds.yml and release.yml

on:
  workflow_call:
    inputs:
      create-archives:
        description: 'Create release archives with README/LICENSE (true for release, false for test)'
        type: boolean
        default: false
      strip-symbols:
        description: 'Strip debug symbols from binaries'
        type: boolean
        default: false
      version:
        description: 'Version string for archive naming (e.g., v0.3.0)'
        type: string
        default: ''

env:
  RUST_BACKTRACE: 1
  CARGO_TERM_COLOR: always

jobs:
  build:
    permissions:
      contents: read
    name: build-${{ matrix.build }}
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        build:
          - linux-x64-gnu
          - linux-aarch64-gnu
          - linux-armv7-gnueabihf
          - macos-aarch64
          - macos-x64
          - windows-x64-msvc
          - windows-x86-msvc
        include:
          - os: ubuntu-22.04
          - cargo: cargo
          - build: linux-x64-gnu
            target: x86_64-unknown-linux-gnu
            run-tests: true
          - build: linux-aarch64-gnu
            target: aarch64-unknown-linux-gnu
            cargo: cross
          - build: linux-armv7-gnueabihf
            target: armv7-unknown-linux-gnueabihf
            cargo: cross
          - build: macos-aarch64
            os: macos-14
            target: aarch64-apple-darwin
            run-tests: true
          - build: macos-x64
            os: macos-14
            target: x86_64-apple-darwin
          - build: windows-x64-msvc
            os: windows-latest
            target: x86_64-pc-windows-msvc
          - build: windows-x86-msvc
            os: windows-latest
            target: i686-pc-windows-msvc

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Setup Linux dependencies
        if: matrix.os == 'ubuntu-22.04'
        uses: ./.github/actions/setup-linux-deps

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
          components: rustfmt

      - name: Install cross
        if: matrix.cargo == 'cross'
        run: cargo install cross@0.2.5

      - name: Build
        uses: ./.github/actions/build-rustnet
        with:
          target: ${{ matrix.target }}
          cargo-command: ${{ matrix.cargo }}
          default-features: ${{ contains(matrix.target, 'linux') && 'true' || 'false' }}
          strip-symbols: ${{ inputs.strip-symbols && 'true' || 'false' }}

      - name: Run tests
        if: matrix.run-tests
        run: cargo test --verbose

      # For test builds: upload raw binary
      - name: Upload binary artifact
        if: ${{ !inputs.create-archives }}
        uses: actions/upload-artifact@v7
        with:
          name: rustnet-${{ matrix.build }}
          path: target/${{ matrix.target }}/release/rustnet${{ contains(matrix.os, 'windows') && '.exe' || '' }}
          if-no-files-found: error

      # For release builds: create archive with README/LICENSE
      - name: Create release archive
        if: ${{ inputs.create-archives }}
        shell: bash
        env:
          RUSTNET_BIN: ${{ contains(matrix.os, 'windows') && 'rustnet.exe' || 'rustnet' }}
        run: |
          staging="rustnet-${{ inputs.version }}-${{ matrix.target }}"
          mkdir -p "$staging"

          cp "target/${{ matrix.target }}/release/$RUSTNET_BIN" "$staging/"

          if [ -d "assets" ] && [ -f "assets/services" ]; then
            mkdir -p "$staging/assets"
            cp "assets/services" "$staging/assets/"
          fi

          cp README.md "$staging/"
          cp LICENSE "$staging/" 2>/dev/null || true

          if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
            7z a "$staging.zip" "$staging"
            echo "ASSET=$staging.zip" >> $GITHUB_ENV
          else
            tar czf "$staging.tar.gz" "$staging"
            echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV
          fi

      - name: Upload release archive
        if: ${{ inputs.create-archives }}
        uses: actions/upload-artifact@v7
        with:
          name: build-${{ matrix.target }}
          path: ${{ env.ASSET }}
          if-no-files-found: error

  build-static:
    permissions:
      contents: read
    name: build-linux-static-${{ matrix.arch }}
    runs-on: ${{ matrix.runner }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - arch: x86_64
            runner: ubuntu-latest
            target: x86_64-unknown-linux-musl
          - arch: aarch64
            runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
            features: '--features default'
          - arch: aarch64-android
            runner: ubuntu-24.04-arm
            target: aarch64-linux-android-musl # Note: it's not a real rust target but we rename the artifact
            features: '--no-default-features'
          - arch: x86_64-android
            runner: ubuntu-latest
            target: x86_64-linux-android-musl
            features: '--no-default-features'
          - arch: armv7-android
            runner: ubuntu-latest
            target: armv7-linux-android-musl
            features: '--no-default-features'
          - arch: x86-android
            runner: ubuntu-latest
            target: i686-linux-android-musl
            features: '--no-default-features'
    container:
      image: rust:alpine
    steps:
      # x86_64: use standard checkout action
      - name: Checkout repository
        if: matrix.arch == 'x86_64' || matrix.arch == 'x86_64-android' || matrix.arch == 'armv7-android' || matrix.arch == 'x86-android'
        uses: actions/checkout@v6

      # aarch64: JS actions don't work in Alpine containers on ARM runners
      - name: Checkout repository (ARM workaround)
        if: contains(matrix.arch, 'aarch64')
        run: |
          apk add --no-cache git
          git config --global --add safe.directory /__w/rustnet/rustnet
          git clone --depth 1 https://github.com/${{ github.repository }}.git .
          if [[ "${{ github.ref }}" == refs/tags/* ]]; then
            git fetch --depth 1 origin tag "${{ github.ref_name }}"
            git checkout "${{ github.ref_name }}"
          fi

      # x86_64: use composite action
      - name: Build static binary
        if: matrix.arch == 'x86_64'
        uses: ./.github/actions/build-static

      # aarch64 & android: inline build (composite actions don't work with ARM workaround/zig wrapper easily here)
      - name: Install dependencies (ARM & Android)
        if: matrix.arch != 'x86_64'
        run: |
          apk add --no-cache \
            musl-dev libpcap-dev pkgconfig build-base perl \
            elfutils-dev zlib-dev zlib-static zstd-dev zstd-static \
            clang llvm linux-headers curl github-cli
          rustup component add rustfmt

      - name: Install cross-compiler and libpcap (armv7)
        if: matrix.arch == 'armv7-android'
        run: |
          rustup target add armv7-unknown-linux-musleabihf
          apk add zig flex bison bash

          printf '%s\n' \
            '#!/bin/bash' \
            'args=()' \
            'for arg in "$@"; do' \
            '  case "$arg" in --target=*) ;; *) args+=("$arg") ;; esac' \
            'done' \
            'exec zig cc -target arm-linux-musleabihf "${args[@]}"' \
            > /usr/local/bin/arm-linux-musleabihf-gcc
          chmod +x /usr/local/bin/arm-linux-musleabihf-gcc

          curl -fsSL --retry 3 --retry-delay 5 \
            -o libpcap.tar.gz https://www.tcpdump.org/release/libpcap-1.10.5.tar.gz
          tar xzf libpcap.tar.gz
          cd libpcap-1.10.5
          CC=arm-linux-musleabihf-gcc \
            ./configure --host=arm-linux-musleabihf \
              --disable-shared --enable-static \
              --disable-usb --disable-dbus --disable-bluetooth --disable-remote \
              --prefix=/arm-sysroot
          make -j$(nproc) && make install
          cd ..

      - name: Install cross-compiler and libpcap (x86/i686)
        if: matrix.arch == 'x86-android'
        run: |
          rustup target add i686-unknown-linux-musl
          apk add zig flex bison bash

          printf '%s\n' \
            '#!/bin/bash' \
            'args=()' \
            'for arg in "$@"; do' \
            '  case "$arg" in --target=*) ;; -Wl,-melf_i386) ;; *) args+=("$arg") ;; esac' \
            'done' \
            'exec zig cc -target x86-linux-musl "${args[@]}"' \
            > /usr/local/bin/i686-linux-musl-gcc
          chmod +x /usr/local/bin/i686-linux-musl-gcc

          curl -fsSL --retry 3 --retry-delay 5 \
            -o libpcap.tar.gz https://www.tcpdump.org/release/libpcap-1.10.5.tar.gz
          tar xzf libpcap.tar.gz
          cd libpcap-1.10.5
          CC=i686-linux-musl-gcc \
            ./configure --host=i686-linux-musl \
              --disable-shared --enable-static \
              --disable-usb --disable-dbus --disable-bluetooth --disable-remote \
              --prefix=/x86-sysroot
          make -j$(nproc) && make install
          cd ..

      - name: Build static binary (AArch64 / AArch64 Android)
        if: matrix.arch == 'aarch64' || matrix.arch == 'aarch64-android'
        env:
          # -mno-outline-atomics: Prevent GCC from generating calls to __aarch64_ldadd4_sync etc.
          # which aren't in Alpine's libatomic. Uses inline atomics instead.
          CFLAGS: "-mno-outline-atomics"
          CXXFLAGS: "-mno-outline-atomics"
          RUSTFLAGS: "-C strip=symbols -C link-arg=-l:libzstd.a"
        run: cargo build --release ${{ matrix.features }}

      - name: Build static binary (x86_64 Android)
        if: matrix.arch == 'x86_64-android'
        env:
          RUSTFLAGS: "-C strip=symbols -C link-arg=-l:libzstd.a"
        run: cargo build --release ${{ matrix.features }}

      - name: Build static binary (armv7-android)
        if: matrix.arch == 'armv7-android'
        env:
          RUSTFLAGS: "-C strip=symbols -C link-self-contained=no"
          CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER: arm-linux-musleabihf-gcc
          PKG_CONFIG_PATH: /arm-sysroot/lib/pkgconfig
          PKG_CONFIG_SYSROOT_DIR: /arm-sysroot
          PKG_CONFIG_ALLOW_CROSS: "1"
        run: cargo build --release --target armv7-unknown-linux-musleabihf ${{ matrix.features }}

      - name: Build static binary (x86-android)
        if: matrix.arch == 'x86-android'
        env:
          RUSTFLAGS: "-C strip=symbols -C link-self-contained=no"
          CARGO_TARGET_I686_UNKNOWN_LINUX_MUSL_LINKER: i686-linux-musl-gcc
          PKG_CONFIG_PATH: /x86-sysroot/lib/pkgconfig
          PKG_CONFIG_SYSROOT_DIR: /x86-sysroot
          PKG_CONFIG_ALLOW_CROSS: "1"
        run: cargo build --release --target i686-unknown-linux-musl ${{ matrix.features }}

      - name: Verify static linking (ARM & Android)
        if: matrix.arch != 'x86_64'
        run: |
          BIN="${{ (matrix.arch == 'armv7-android' && 'target/armv7-unknown-linux-musleabihf/release/rustnet') || (matrix.arch == 'x86-android' && 'target/i686-unknown-linux-musl/release/rustnet') || 'target/release/rustnet' }}"
          file "$BIN"
          file "$BIN" | grep -q "static.* linked" || \
            (echo "ERROR: Binary is not statically linked" && exit 1)

      # For test builds: upload raw binary
      - name: Upload binary artifact
        if: ${{ !inputs.create-archives }}
        continue-on-error: ${{ matrix.arch != 'x86_64' }}
        uses: actions/upload-artifact@v7
        with:
          name: rustnet-linux-static-${{ matrix.arch }}
          path: ${{ (matrix.arch == 'armv7-android' && 'target/armv7-unknown-linux-musleabihf/release/rustnet') || (matrix.arch == 'x86-android' && 'target/i686-unknown-linux-musl/release/rustnet') || 'target/release/rustnet' }}
          if-no-files-found: error

      # For release builds: create archive
      - name: Create release archive
        if: ${{ inputs.create-archives }}
        run: |
          BIN="${{ (matrix.arch == 'armv7-android' && 'target/armv7-unknown-linux-musleabihf/release/rustnet') || (matrix.arch == 'x86-android' && 'target/i686-unknown-linux-musl/release/rustnet') || 'target/release/rustnet' }}"

          staging="rustnet-${{ inputs.version }}-${{ matrix.target }}"
          mkdir -p "$staging/assets"

          cp "$BIN" "$staging/rustnet"
          cp assets/services "$staging/assets/" 2>/dev/null || true
          cp README.md "$staging/"
          cp LICENSE "$staging/" 2>/dev/null || true

          tar czf "$staging.tar.gz" "$staging"

      - name: Upload release archive
        if: ${{ inputs.create-archives }}
        uses: actions/upload-artifact@v7
        with:
          name: build-${{ matrix.target }}
          path: rustnet-${{ inputs.version }}-${{ matrix.target }}.tar.gz
          if-no-files-found: error

