diff --git a/.changeset/eleven-ghosts-bow.md b/.changeset/eleven-ghosts-bow.md new file mode 100644 index 00000000..a95734a1 --- /dev/null +++ b/.changeset/eleven-ghosts-bow.md @@ -0,0 +1,5 @@ +--- +"linux-tools-mac": major +--- + +feat: upgrade linux-tools-mac to latest toolsets of gnu-tar, lzip, makedepend, glib, libgsf, libtool, pcre, gettext, binutils diff --git a/.github/workflows/build-linux-tools-mac.yaml b/.github/workflows/build-linux-tools-mac.yaml new file mode 100644 index 00000000..4fcfbd63 --- /dev/null +++ b/.github/workflows/build-linux-tools-mac.yaml @@ -0,0 +1,29 @@ +name: Build macOS linux-tools-mac bundle + +on: + workflow_dispatch: + workflow_call: + +jobs: + mac: + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + runner: [macos-26-intel, macos-26] + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Build + run: | + bash ./packages/linux-tools-mac/build.sh + + - name: Upload linux-tools-mac artifact + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 + with: + name: linux-tools-mac-${{ matrix.runner }} + path: ./packages/linux-tools-mac/out/**/*.tar.gz + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index cc529b9b..1faa3392 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -74,7 +74,12 @@ jobs: if: contains(needs.detect.outputs.matrix, '"dmg-builder"') uses: ./.github/workflows/build-dmg-builder.yaml needs: detect - + + linux-tools-mac: + if: contains(needs.detect.outputs.matrix, '"linux-tools-mac"') + uses: ./.github/workflows/build-linux-tools-mac.yaml + needs: detect + squirrel: if: contains(needs.detect.outputs.matrix, '"squirrel.windows"') uses: ./.github/workflows/build-squirrel.yaml @@ -89,6 +94,7 @@ jobs: ran, nsis, dmg-builder, + linux-tools-mac, squirrel ] if: | diff --git a/packages/linux-tools-mac/assets/build-mac.sh b/packages/linux-tools-mac/assets/build-mac.sh new file mode 100755 index 00000000..51d78b62 --- /dev/null +++ b/packages/linux-tools-mac/assets/build-mac.sh @@ -0,0 +1,323 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "โŒ Must be run on macOS" + exit 1 +fi + +XCODE_VER="$(xcodebuild -version 2>/dev/null | awk '/^Xcode /{print $2}')" +XCODE_MAJOR="${XCODE_VER%%.*}" +if [[ -z "$XCODE_MAJOR" || "$XCODE_MAJOR" -lt 26 ]]; then + echo "โŒ Xcode 26+ required (found: ${XCODE_VER:-none})" + echo " install_name_tool in Xcode < 26 rejects Homebrew ld_prime binaries." + echo " Use a macos-26 runner." + exit 1 +fi + +### ================================ +### ARGS +### ================================ +ARCH="" +OUTPUT_DIR="" +ROOT="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --arch) ARCH="$2"; shift 2 ;; + --output-dir) OUTPUT_DIR="$2"; shift 2 ;; + --root) ROOT="$2"; shift 2 ;; + *) echo "Unknown argument: $1"; exit 1 ;; + esac +done + +ROOT="${ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +OUTPUT_DIR="${OUTPUT_DIR:-${ROOT}/out/linux-tools-mac}" +ARCH="${ARCH:-$(uname -m)}" + +if [[ "$ARCH" != "arm64" && "$ARCH" != "x86_64" ]]; then + echo "โŒ Unsupported ARCH: $ARCH" + exit 1 +fi + +### ================================ +### CONFIG +### ================================ +TMP_DIR="/tmp/linux-tools-mac-build-${ARCH}" +BUNDLE_DIR="${TMP_DIR}/linux-tools-mac" +BIN_DIR="${BUNDLE_DIR}/bin" +LIB_DIR="${BUNDLE_DIR}/lib" + +cleanup() { rm -rf "$TMP_DIR"; } +trap cleanup EXIT INT TERM + +# Binaries to collect per formula โ€” "formula:bin1 bin2 bin3" entries +FORMULA_BINS=( + "gnu-tar:gtar" + "lzip:lzip" + "makedepend:makedepend" + "glib:gapplication gdbus gdbus-codegen gio gio-querymodules glib-compile-resources glib-compile-schemas glib-genmarshal glib-gettextize glib-mkenums gobject-query gresource gsettings gtester gtester-report" + "libgsf:gsf gsf-office-thumbnailer gsf-vba-dump" + "libtool:glibtool glibtoolize" + "pcre:pcre-config pcregrep pcretest" + "gettext:autopoint envsubst gettext gettext.sh gettextize msgattrib msgcat msgcmp msgcomm msgconv msgen msgexec msgfilter msgfmt msggrep msginit msgmerge msgunfmt msguniq ngettext recode-sr-latin xgettext" + "binutils:gar ar" +) + +echo "๐Ÿ› ๏ธ linux-tools-mac macOS bundle" +echo " Arch: $ARCH" +echo " Xcode: $XCODE_VER" +echo " Output: $OUTPUT_DIR" + +### ================================ +### CLEAN +### ================================ +rm -rf "$TMP_DIR" +mkdir -p "$BIN_DIR" "$LIB_DIR" + +### ================================ +### INSTALL DEPENDENCIES +### ================================ +echo "" +echo "๐Ÿ“ฆ Installing brew formulas..." +brew install gnu-tar lzip makedepend glib libgsf libtool pcre gettext binutils + +### ================================ +### COPY BINARIES +### ================================ +echo "" +echo "๐Ÿ“‹ Copying binaries..." + +for entry in "${FORMULA_BINS[@]}"; do + formula="${entry%%:*}" + bins="${entry#*:}" + prefix="$(brew --prefix "$formula" 2>/dev/null || true)" + if [[ -z "$prefix" ]]; then + echo " โš ๏ธ Could not find prefix for $formula, skipping" + continue + fi + for bin in $bins; do + src="$prefix/bin/$bin" + if [[ -f "$src" || -L "$src" ]]; then + echo " โž• $formula/$bin" + cp -L "$src" "$BIN_DIR/$bin" + chmod u+w,a+x "$BIN_DIR/$bin" + else + echo " โญ๏ธ $formula/$bin not found, skipping" + fi + done +done + +### ================================ +### COPY LICENSE FILES +### ================================ +echo "" +echo "๐Ÿ“„ Copying license files..." +mkdir -p "$BUNDLE_DIR/LICENSES" + +for entry in "${FORMULA_BINS[@]}"; do + formula="${entry%%:*}" + license_prefix="$(brew --prefix "$formula" 2>/dev/null || true)" + if [[ -z "$license_prefix" ]]; then + echo " โš ๏ธ Could not find prefix for $formula, skipping licenses" + continue + fi + + license_dir="$BUNDLE_DIR/LICENSES/$formula" + mkdir -p "$license_dir" + + found=0 + for name in COPYING LICENSE LICENCE COPYING.LIB AUTHORS; do + src="$license_prefix/$name" + if [[ -f "$src" ]]; then + cp "$src" "$license_dir/$name" + echo " ๐Ÿ“„ $formula/$name" + found=1 + fi + done + if [[ "$found" -eq 0 ]]; then + echo " โš ๏ธ No license file found for $formula in $license_prefix" + fi +done + +### ================================ +### COLLECT DYLIB DEPENDENCIES +### ================================ +echo "" +echo "๐Ÿ“š Collecting dylib dependencies..." + +should_skip_lib() { + local dep="$1" + [[ "$dep" == /usr/lib/* ]] && return 0 + [[ "$dep" == /System/* ]] && return 0 + [[ "$dep" == @* ]] && return 0 + return 1 +} + +is_macho() { + file -b "$1" 2>/dev/null | grep -q '^Mach-O' +} + +collect_deps() { + local binary="$1" + is_macho "$binary" || return 0 + otool -L "$binary" 2>/dev/null | awk 'NR>1 {print $1}' | while read -r dep; do + should_skip_lib "$dep" && continue + [[ ! -f "$dep" ]] && continue + + local dep_name + dep_name="$(basename "$dep")" + local dest="$LIB_DIR/$dep_name" + + if [[ ! -f "$dest" ]]; then + echo " ๐Ÿ“ฅ $dep_name" + cp -L "$dep" "$dest" + chmod u+w "$dest" + # Recurse into this library's own deps + collect_deps "$dest" + fi + done +} + +find "$BIN_DIR" -type f | while read -r bin; do + collect_deps "$bin" +done + +# Also collect deps of libs (they may have deps we missed) +find "$LIB_DIR" -name "*.dylib" -type f | while read -r lib; do + collect_deps "$lib" +done + +### ================================ +### PATCH DYLIB REFERENCES +### ================================ +echo "" +echo "๐Ÿ”ง Patching dylib references..." + +patch_binary() { + local binary="$1" + local is_lib="${2:-false}" + + is_macho "$binary" || return 0 + + # Set install name for dylibs + if [[ "$is_lib" == "true" ]]; then + local lib_name + lib_name="$(basename "$binary")" + install_name_tool -id "@loader_path/$lib_name" "$binary" 2>/dev/null || true + fi + + # Patch all absolute non-system dep references + otool -L "$binary" 2>/dev/null | awk 'NR>1 {print $1}' | while read -r dep; do + should_skip_lib "$dep" && continue + + local dep_name + dep_name="$(basename "$dep")" + local dest="$LIB_DIR/$dep_name" + + if [[ -f "$dest" ]]; then + local new_path + if [[ "$is_lib" == "true" ]]; then + new_path="@loader_path/$dep_name" + else + new_path="@loader_path/../lib/$dep_name" + fi + local it_err + it_err="$(install_name_tool -change "$dep" "$new_path" "$binary" 2>&1)" \ + || echo " โš ๏ธ patch failed [$(basename "$binary")] $dep: $it_err" + fi + done +} + +find "$BIN_DIR" -type f | while read -r bin; do + echo " ๐Ÿ” bin/$(basename "$bin")" + patch_binary "$bin" "false" +done + +find "$LIB_DIR" -name "*.dylib" -type f | while read -r lib; do + echo " ๐Ÿ” lib/$(basename "$lib")" + patch_binary "$lib" "true" +done + +### ================================ +### STRIP SYMBOLS +### ================================ +echo "" +echo "โœ‚๏ธ Stripping symbols..." +find "$BUNDLE_DIR" -type f | while read -r f; do + strip -x "$f" 2>/dev/null || true +done + +### ================================ +### AD-HOC CODESIGN +### ================================ +echo "" +echo "๐Ÿ” Code signing..." + +# Sign libs first (executables may load them) +find "$LIB_DIR" -name "*.dylib" -type f | while read -r lib; do + codesign --force --sign - "$lib" 2>/dev/null || true +done + +find "$BIN_DIR" -type f | while read -r bin; do + codesign --force --sign - "$bin" 2>/dev/null || true +done + +### ================================ +### VERSION.txt +### ================================ +echo "" +echo "๐Ÿ“ Writing VERSION.txt..." +{ + echo "platform: darwin" + echo "arch: $ARCH" + echo "created_at: $(date -u +"%Y-%m-%dT%H:%M:%SZ")" + echo "" + for formula in gnu-tar lzip makedepend glib libgsf libtool pcre gettext binutils; do + ver="$(brew info --json "$formula" 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['versions']['stable'])" 2>/dev/null || echo "unknown")" + echo "$formula: $ver" + done +} > "$BUNDLE_DIR/VERSION.txt" + +### ================================ +### ARCHIVE +### ================================ +echo "" +echo "๐Ÿ“ฆ Creating archive..." +mkdir -p "$OUTPUT_DIR" + +ARCHIVE_ARCH="$ARCH" +ARCHIVE_NAME="linux-tools-mac-darwin-${ARCHIVE_ARCH}.tar.gz" +ARCHIVE_PATH="$OUTPUT_DIR/$ARCHIVE_NAME" + +tar -czf "$ARCHIVE_PATH" -C "$TMP_DIR" linux-tools-mac +shasum -a 256 "$ARCHIVE_PATH" > "${ARCHIVE_PATH}.sha256" + +rm -rf "$TMP_DIR" + +### ================================ +### TEST +### ================================ +echo "" +echo "๐Ÿงช Running tests..." + +VERIFY_DIR="/tmp/linux-tools-mac-verify-$$" +mkdir -p "$VERIFY_DIR" +tar -xzf "$ARCHIVE_PATH" -C "$VERIFY_DIR" + +bash "$ROOT/assets/test.sh" --bundle-dir "$VERIFY_DIR/linux-tools-mac" + +rm -rf "$VERIFY_DIR" + +### ================================ +### DONE +### ================================ +SIZE="$(du -sh "$ARCHIVE_PATH" | cut -f1)" +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "โœ… DONE" +echo "โ€ข Arch: $ARCH" +echo "โ€ข Archive: $ARCHIVE_NAME" +echo "โ€ข Size: $SIZE" +echo "โ€ข Path: $ARCHIVE_PATH" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" diff --git a/packages/linux-tools-mac/assets/test.sh b/packages/linux-tools-mac/assets/test.sh new file mode 100755 index 00000000..b0944e68 --- /dev/null +++ b/packages/linux-tools-mac/assets/test.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +set -euo pipefail + +### ================================ +### ARGS +### ================================ +BUNDLE_DIR="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --bundle-dir) BUNDLE_DIR="$2"; shift 2 ;; + *) echo "Unknown argument: $1"; exit 1 ;; + esac +done + +if [[ -z "$BUNDLE_DIR" ]]; then + echo "Usage: $0 --bundle-dir " + exit 1 +fi + +if [[ ! -d "$BUNDLE_DIR/bin" ]]; then + echo "โŒ No bin/ directory found in $BUNDLE_DIR" + exit 1 +fi + +BIN="$BUNDLE_DIR/bin" +LIB="$BUNDLE_DIR/lib" + +# Use bundled dylibs, not system/Homebrew ones +export DYLD_FALLBACK_LIBRARY_PATH="$LIB${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}" + +### ================================ +### HELPERS +### ================================ +pass=0 +fail=0 + +ok() { + echo " โœ… $1" + pass=$((pass + 1)) +} + +fail_bin() { + echo " โŒ $1" + fail=$((fail + 1)) +} + +run_test() { + local label="$1"; shift + if "$@" >/dev/null 2>&1; then + ok "$label" + else + fail_bin "$label" + fi +} + +# Check a binary exists and is executable +assert_exists() { + local bin="$BIN/$1" + if [[ ! -x "$bin" ]]; then + fail_bin "$1 (missing or not executable)" + return 1 + fi + return 0 +} + +# Verify no absolute Homebrew paths remain in a binary's load commands +check_paths() { + local label="$1" + local bin="$BIN/$2" + local bad + bad="$(otool -L "$bin" 2>/dev/null | awk 'NR>1 {print $1}' | grep -E '^/(opt/homebrew|usr/local)' || true)" + if [[ -n "$bad" ]]; then + fail_bin "$label (absolute Homebrew paths found: $bad)" + else + ok "$label (no absolute paths)" + fi +} + +### ================================ +### BINARY TESTS +### ================================ +echo "๐Ÿงช Testing linux-tools-mac bundle at: $BUNDLE_DIR" +echo "" + +echo "โ”€โ”€ gnu-tar โ”€โ”€" +assert_exists gtar && run_test "gtar --version" "$BIN/gtar" --version +check_paths "gtar paths" gtar + +echo "" +echo "โ”€โ”€ lzip โ”€โ”€" +assert_exists lzip && run_test "lzip --version" "$BIN/lzip" --version +check_paths "lzip paths" lzip + +echo "" +echo "โ”€โ”€ makedepend โ”€โ”€" +assert_exists makedepend && run_test "makedepend (no args, expect non-crash)" bash -c "'$BIN/makedepend' || true" +check_paths "makedepend paths" makedepend + +echo "" +echo "โ”€โ”€ glib โ”€โ”€" +assert_exists gdbus && run_test "gdbus --help" "$BIN/gdbus" --help +assert_exists gdbus-codegen && run_test "gdbus-codegen --help" "$BIN/gdbus-codegen" --help +assert_exists gio && run_test "gio version" "$BIN/gio" version +assert_exists gio-querymodules && run_test "gio-querymodules (no crash)" bash -c "'$BIN/gio-querymodules' /nonexistent 2>/dev/null || true" +assert_exists glib-compile-resources && run_test "glib-compile-resources --version" "$BIN/glib-compile-resources" --version +assert_exists glib-compile-schemas && run_test "glib-compile-schemas --version" "$BIN/glib-compile-schemas" --version +assert_exists glib-genmarshal && run_test "glib-genmarshal --version" "$BIN/glib-genmarshal" --version +assert_exists glib-gettextize && run_test "glib-gettextize --version" bash -c "'$BIN/glib-gettextize' --version 2>/dev/null || true" +assert_exists glib-mkenums && run_test "glib-mkenums --version" "$BIN/glib-mkenums" --version +assert_exists gobject-query && run_test "gobject-query (no crash)" bash -c "'$BIN/gobject-query' --help 2>/dev/null || true" +assert_exists gresource && run_test "gresource --help" bash -c "'$BIN/gresource' --help 2>/dev/null || true" +assert_exists gsettings && run_test "gsettings --help" bash -c "'$BIN/gsettings' --help 2>/dev/null || true" +assert_exists gtester && run_test "gtester --version" "$BIN/gtester" --version +assert_exists gtester-report && run_test "gtester-report (no crash)" bash -c "'$BIN/gtester-report' --help 2>/dev/null || true" +check_paths "gdbus paths" gdbus +check_paths "gio paths" gio +check_paths "glib-compile-schemas paths" glib-compile-schemas + +echo "" +echo "โ”€โ”€ libgsf โ”€โ”€" +assert_exists gsf && run_test "gsf (no crash)" bash -c "'$BIN/gsf' --help 2>/dev/null || true" +check_paths "gsf paths" gsf + +echo "" +echo "โ”€โ”€ libtool โ”€โ”€" +assert_exists glibtool && run_test "glibtool --version" "$BIN/glibtool" --version +assert_exists glibtoolize && run_test "glibtoolize --version" "$BIN/glibtoolize" --version +check_paths "glibtool paths" glibtool + +echo "" +echo "โ”€โ”€ pcre โ”€โ”€" +assert_exists pcre-config && run_test "pcre-config --version" "$BIN/pcre-config" --version +assert_exists pcregrep && run_test "pcregrep --version" "$BIN/pcregrep" --version +assert_exists pcretest && run_test "pcretest (no crash)" bash -c "echo | '$BIN/pcretest' 2>/dev/null || true" +check_paths "pcregrep paths" pcregrep + +echo "" +echo "โ”€โ”€ gettext โ”€โ”€" +assert_exists gettext && run_test "gettext --version" "$BIN/gettext" --version +assert_exists msgfmt && run_test "msgfmt --version" "$BIN/msgfmt" --version +assert_exists msgmerge && run_test "msgmerge --version" "$BIN/msgmerge" --version +assert_exists envsubst && run_test "envsubst --version" "$BIN/envsubst" --version +assert_exists xgettext && run_test "xgettext --version" "$BIN/xgettext" --version +check_paths "msgfmt paths" msgfmt + +echo "" +echo "โ”€โ”€ binutils โ”€โ”€" +assert_exists gar && run_test "gar --version" "$BIN/gar" --version +assert_exists ar && run_test "ar --version" "$BIN/ar" --version +check_paths "ar paths" ar + +### ================================ +### SUMMARY +### ================================ +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "Results: $pass passed, $fail failed" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + +[[ "$fail" -eq 0 ]] || exit 1 diff --git a/packages/linux-tools-mac/build.sh b/packages/linux-tools-mac/build.sh new file mode 100755 index 00000000..ca94a581 --- /dev/null +++ b/packages/linux-tools-mac/build.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +OUTPUT_DIR="${ROOT}/out/linux-tools-mac" +ARCH="$(uname -m)" + +rm -rf "${OUTPUT_DIR}" +mkdir -p "${OUTPUT_DIR}" + +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "๐Ÿ—๏ธ Building linux-tools-mac for ${ARCH}" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +bash "$ROOT/assets/build-mac.sh" \ + --arch "$ARCH" \ + --output-dir "$OUTPUT_DIR" \ + --root "$ROOT" diff --git a/packages/linux-tools-mac/package.json b/packages/linux-tools-mac/package.json new file mode 100644 index 00000000..d0ca445f --- /dev/null +++ b/packages/linux-tools-mac/package.json @@ -0,0 +1,5 @@ +{ + "name": "linux-tools-mac", + "version": "0.0.0", + "description": "Portable macOS bundle of GNU/Linux-compatible tools (gtar, lzip, glib, gettext, libgsf, libtool, pcre, makedepend, binutils) used by electron-builder when packaging Linux targets on macOS." +} diff --git a/packages/linux-tools/package.json b/packages/linux-tools/package.json deleted file mode 100644 index d6092507..00000000 --- a/packages/linux-tools/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "linux-tools", - "version": "0.0.0" -} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 736ea46e..909ddcb0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,7 +38,7 @@ importers: packages/fpm: {} - packages/linux-tools: {} + packages/linux-tools-mac: {} packages/nsis: {}