#!/usr/bin/env bash set -euo pipefail repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" cd "${repo_root}" platform="${1:-all}" build_number="${BUILD_NUMBER:-${RELEASE_BUILD_NUMBER:-${RELEASE_REF:-manual}}}" out_root="${BURROW_RELEASE_OUT:-${repo_root}/dist/builds/${build_number}}" mkdir -p "${out_root}" sha256_file() { local file="$1" if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$file" > "${file}.sha256" else sha256sum "$file" > "${file}.sha256" fi } burrow_crate_version() { awk -F '"' '/^version = / { print $2; exit }' burrow/Cargo.toml } package_release_suffix() { printf '%s' "$build_number" | tr -c 'A-Za-z0-9.+~' '.' } arch_package_suffix() { printf '%s' "$build_number" | tr -c 'A-Za-z0-9.+_' '.' } deb_arch_for_target() { case "$1" in x86_64-unknown-linux-gnu) printf 'amd64' ;; aarch64-unknown-linux-gnu) printf 'arm64' ;; riscv64gc-unknown-linux-gnu) printf 'riscv64' ;; *) return 1 ;; esac } rpm_arch_for_target() { case "$1" in x86_64-unknown-linux-gnu) printf 'x86_64' ;; aarch64-unknown-linux-gnu) printf 'aarch64' ;; riscv64gc-unknown-linux-gnu) printf 'riscv64' ;; *) return 1 ;; esac } pacman_arch_for_target() { case "$1" in x86_64-unknown-linux-gnu) printf 'x86_64' ;; aarch64-unknown-linux-gnu) printf 'aarch64' ;; riscv64gc-unknown-linux-gnu) printf 'riscv64' ;; *) return 1 ;; esac } copy_license_readme() { local dst="$1" cp README.md "$dst/README.md" if [[ -f LICENSE ]]; then cp LICENSE "$dst/LICENSE" elif [[ -f LICENSE.md ]]; then cp LICENSE.md "$dst/LICENSE.md" fi } package_deb() { local target binary deb_arch pkg_version suffix staging deb_file installed_size target="$1" binary="$2" if ! deb_arch="$(deb_arch_for_target "$target")"; then echo "warning: no Debian architecture mapping for ${target}; skipping .deb package" >&2 return 0 fi if ! command -v dpkg-deb >/dev/null 2>&1; then echo "warning: dpkg-deb is not available; skipping .deb package" >&2 return 0 fi suffix="$(package_release_suffix)" pkg_version="${BURROW_PACKAGE_VERSION:-$(burrow_crate_version)+${suffix}}" staging="${out_root}/linux/pkg-deb/burrow_${pkg_version}_${deb_arch}" rm -rf "$staging" mkdir -p \ "$staging/DEBIAN" \ "$staging/usr/bin" \ "$staging/usr/lib/systemd/system" \ "$staging/usr/share/doc/burrow" \ "$staging/usr/share/licenses/burrow" install -m 0755 "$binary" "$staging/usr/bin/burrow" install -m 0644 systemd/burrow.service "$staging/usr/lib/systemd/system/burrow.service" install -m 0644 systemd/burrow.socket "$staging/usr/lib/systemd/system/burrow.socket" install -m 0644 README.md "$staging/usr/share/doc/burrow/README.md" install -m 0644 LICENSE.md "$staging/usr/share/licenses/burrow/LICENSE.md" installed_size="$(du -sk "$staging/usr" | awk '{ print $1 }')" cat > "$staging/DEBIAN/control" < Installed-Size: ${installed_size} Depends: systemd Description: Burrow VPN daemon and CLI Burrow installs the daemon entrypoint and systemd socket used by the desktop UI. EOF deb_file="${out_root}/linux/burrow_${pkg_version}_${deb_arch}.deb" dpkg-deb --build --root-owner-group "$staging" "$deb_file" sha256_file "$deb_file" } package_rpm() { local target binary rpm_arch target="$1" binary="$2" if ! rpm_arch="$(rpm_arch_for_target "$target")"; then echo "warning: no RPM architecture mapping for ${target}; skipping .rpm package" >&2 return 0 fi if ! command -v cargo-generate-rpm >/dev/null 2>&1; then echo "warning: cargo-generate-rpm is not available; skipping .rpm package" >&2 return 0 fi mkdir -p target/release cp "$binary" target/release/burrow if cargo generate-rpm -p burrow; then find target/generate-rpm -type f -name '*.rpm' -exec cp {} "${out_root}/linux/" \; 2>/dev/null || true find "${out_root}/linux" -maxdepth 1 -type f -name '*.rpm' -print | while read -r rpm; do sha256_file "$rpm" done else echo "warning: cargo-generate-rpm failed for ${rpm_arch}; skipping .rpm package" >&2 fi } package_pacman() { local target binary pacman_arch pkg_version pkgrel work source_sum service_sum socket_sum readme_sum license_sum target="$1" binary="$2" if ! pacman_arch="$(pacman_arch_for_target "$target")"; then echo "warning: no pacman architecture mapping for ${target}; skipping Arch package" >&2 return 0 fi if ! command -v makepkg >/dev/null 2>&1; then echo "warning: makepkg is not available; skipping Arch package" >&2 return 0 fi pkg_version="${BURROW_ARCH_PKGVER:-$(burrow_crate_version)_$(arch_package_suffix)}" pkgrel="${BURROW_ARCH_PKGREL:-1}" work="${out_root}/linux/pkg-arch/${pacman_arch}" rm -rf "$work" mkdir -p "$work" install -m 0755 "$binary" "$work/burrow" install -m 0644 systemd/burrow.service "$work/burrow.service" install -m 0644 systemd/burrow.socket "$work/burrow.socket" install -m 0644 README.md "$work/README.md" install -m 0644 LICENSE.md "$work/LICENSE.md" source_sum="$(sha256sum "$work/burrow" | awk '{ print $1 }')" service_sum="$(sha256sum "$work/burrow.service" | awk '{ print $1 }')" socket_sum="$(sha256sum "$work/burrow.socket" | awk '{ print $1 }')" readme_sum="$(sha256sum "$work/README.md" | awk '{ print $1 }')" license_sum="$(sha256sum "$work/LICENSE.md" | awk '{ print $1 }')" cat > "$work/PKGBUILD" < pkgname=burrow pkgver=${pkg_version} pkgrel=${pkgrel} pkgdesc="Burrow VPN daemon and CLI" arch=("${pacman_arch}") url="https://git.burrow.net/hackclub/burrow" license=("GPL-3.0-or-later") depends=("systemd") source=("burrow" "burrow.service" "burrow.socket" "README.md" "LICENSE.md") sha256sums=("${source_sum}" "${service_sum}" "${socket_sum}" "${readme_sum}" "${license_sum}") package() { install -Dm755 "\${srcdir}/burrow" "\${pkgdir}/usr/bin/burrow" install -Dm644 "\${srcdir}/burrow.service" "\${pkgdir}/usr/lib/systemd/system/burrow.service" install -Dm644 "\${srcdir}/burrow.socket" "\${pkgdir}/usr/lib/systemd/system/burrow.socket" install -Dm644 "\${srcdir}/README.md" "\${pkgdir}/usr/share/doc/burrow/README.md" install -Dm644 "\${srcdir}/LICENSE.md" "\${pkgdir}/usr/share/licenses/burrow/LICENSE.md" } EOF ( cd "$work" makepkg --force --nodeps --noconfirm ) find "$work" -maxdepth 1 -type f -name '*.pkg.tar.*' -exec cp {} "${out_root}/linux/" \; find "${out_root}/linux" -maxdepth 1 -type f -name '*.pkg.tar.*' -print | while read -r package; do sha256_file "$package" done } package_linux() { local target host archive staging host="$(rustc -vV | awk '/^host:/ { print $2 }')" target="${BURROW_LINUX_TARGET:-$host}" if [[ "$target" != *-linux-* && -z "${BURROW_LINUX_TARGET:-}" ]]; then mkdir -p "${out_root}/linux" echo "warning: host target ${target} is not Linux; skipping Linux artifact on this host" >&2 printf 'Linux build was skipped on host target %s. Run this lane on a Linux Namespace runner or set BURROW_LINUX_TARGET explicitly.\n' "$target" > "${out_root}/linux/README.txt" return 0 fi staging="${out_root}/linux/burrow-${build_number}-${target}" mkdir -p "$staging" cargo build --locked --release -p burrow --bin burrow --target "$target" install -m 0755 "target/${target}/release/burrow" "$staging/burrow" copy_license_readme "$staging" archive="${out_root}/linux/burrow-${build_number}-${target}.tar.gz" tar -C "${out_root}/linux" -czf "$archive" "$(basename "$staging")" sha256_file "$archive" package_deb "$target" "target/${target}/release/burrow" package_rpm "$target" "target/${target}/release/burrow" package_pacman "$target" "target/${target}/release/burrow" } package_windows_stub_unix() { local target exe archive staging target="${BURROW_WINDOWS_TARGET:-x86_64-pc-windows-gnu}" staging="${out_root}/windows/burrow-${build_number}-${target}" mkdir -p "$staging" target_libdir="$(rustc --print target-libdir --target "$target" 2>/dev/null || true)" if [[ -z "$target_libdir" || ! -d "$target_libdir" ]]; then echo "warning: Rust target ${target} is not installed on this host" >&2 printf 'Windows stub build for %s was skipped on this host. Use the Namespace Windows lane for the native artifact.\n' "$target" > "${out_root}/windows/README.txt" return 0 fi if cargo build --locked --release -p burrow-windows-stub --target "$target"; then exe="target/${target}/release/burrow.exe" else echo "warning: unable to build the Windows release stub on this host" >&2 printf 'Windows stub build for %s was skipped on this host. Use the Namespace Windows lane for the native artifact.\n' "$target" > "${out_root}/windows/README.txt" return 0 fi install -m 0755 "$exe" "$staging/burrow.exe" copy_license_readme "$staging" printf 'Burrow Windows stub build %s\n' "$build_number" > "$staging/README-Windows.txt" archive="${out_root}/windows/burrow-${build_number}-${target}.zip" (cd "${out_root}/windows" && zip -qr "$archive" "$(basename "$staging")") sha256_file "$archive" } package_sparkle_unsigned() { local macos_dir appcast channel dmg channel="${SPARKLE_CHANNEL:-build-${build_number}}" macos_dir="${out_root}/macos" appcast="${out_root}/sparkle/${channel}/appcast.xml" mkdir -p "$(dirname "$appcast")" dmg="$(find "$macos_dir" -maxdepth 1 -type f -name '*.dmg' -print 2>/dev/null | sort | tail -n 1 || true)" if [[ -z "$dmg" ]]; then echo "No macOS DMG exists yet; Sparkle appcast generation is waiting on the macOS signed artifact." > "${out_root}/sparkle/${channel}/README.txt" return 0 fi local name length pubdate url name="$(basename "$dmg")" length="$(wc -c < "$dmg" | tr -d ' ')" pubdate="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')" url="${SPARKLE_DOWNLOAD_PREFIX:-https://releases.burrow.net/sparkle}/${channel}/${name}" cat > "$appcast" < Burrow ${channel} Burrow ${build_number} ${pubdate} ${build_number} 0.1 EOF cp "$dmg" "${out_root}/sparkle/${channel}/" } case "$platform" in all) package_linux package_windows_stub_unix if [[ "$(uname -s)" == "Darwin" ]] && command -v xcodebuild >/dev/null 2>&1; then Scripts/ci/package-apple-artifacts.sh all else mkdir -p "${out_root}/apple" printf 'Apple artifacts require a macOS runner with Xcode. The release workflow builds them in the dedicated Apple lane.\n' > "${out_root}/apple/README.txt" fi package_sparkle_unsigned ;; linux) package_linux ;; windows-stub|windows) package_windows_stub_unix ;; sparkle) package_sparkle_unsigned ;; apple) Scripts/ci/package-apple-artifacts.sh all ;; ios|macos) Scripts/ci/package-apple-artifacts.sh "$platform" ;; *) echo "unknown release platform: $platform" >&2 exit 64 ;; esac find "$out_root" -type f -print | sort