Compare commits

...

2 Commits

Author SHA1 Message Date
openhands
944530a72d Fix PyInstaller --target-arch issue with spec files
PyInstaller doesn't allow --target-arch flag when using .spec files.
Instead, modify the spec file directly to set target_arch parameter.

- Dynamically modify spec file to set target_arch for macOS builds
- Restore original spec file after build completes
- Remove --target-arch command line argument usage

This fixes the CI build failure for macOS ARM64 architecture.
2025-10-10 16:54:49 +00:00
openhands
09de78d426 Add multi-architecture support for CLI binary builds
- Expand CI matrix to build for x86_64 and ARM64 on both Linux and macOS
- Add --target-arch parameter to build.py for macOS cross-compilation
- Update artifact naming to include platform and architecture
- Enhance release asset preparation for multi-arch binaries

Fixes #11320

Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-10 16:48:06 +00:00
2 changed files with 78 additions and 12 deletions

View File

@@ -25,7 +25,23 @@ jobs:
name: Build and test binary executable
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
include:
# Linux x86_64
- os: ubuntu-latest
arch: x86_64
platform: linux
# Linux ARM64
- os: ubuntu-latest-arm64
arch: arm64
platform: linux
# macOS x86_64 (Intel)
- os: macos-13
arch: x86_64
platform: macos
# macOS ARM64 (Apple Silicon)
- os: macos-latest
arch: arm64
platform: macos
runs-on: ${{ matrix.os }}
steps:
@@ -52,7 +68,14 @@ jobs:
- name: Build binary executable
working-directory: openhands-cli
run: |
./build.sh --install-pyinstaller | tee output.log
# Set build arguments based on platform and architecture
BUILD_ARGS="--install-pyinstaller"
if [ "${{ matrix.platform }}" = "macos" ]; then
BUILD_ARGS="$BUILD_ARGS --target-arch ${{ matrix.arch }}"
fi
echo "🔨 Building with args: $BUILD_ARGS"
./build.sh $BUILD_ARGS | tee output.log
echo "Full output:"
cat output.log
@@ -67,7 +90,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
uses: actions/upload-artifact@v4
with:
name: openhands-cli-${{ matrix.os }}
name: openhands-cli-${{ matrix.platform }}-${{ matrix.arch }}
path: openhands-cli/dist/openhands*
retention-days: 30
@@ -88,13 +111,24 @@ jobs:
- name: Prepare release assets
run: |
mkdir -p release-assets
# Rename binaries to include OS in filename
if [ -f artifacts/openhands-cli-ubuntu-latest/openhands ]; then
cp artifacts/openhands-cli-ubuntu-latest/openhands release-assets/openhands-linux
fi
if [ -f artifacts/openhands-cli-macos-latest/openhands ]; then
cp artifacts/openhands-cli-macos-latest/openhands release-assets/openhands-macos
fi
# Rename binaries to include platform and architecture in filename
for artifact_dir in artifacts/openhands-cli-*; do
if [ -d "$artifact_dir" ]; then
# Extract platform and arch from directory name
# Format: openhands-cli-{platform}-{arch}
dir_name=$(basename "$artifact_dir")
platform_arch=${dir_name#openhands-cli-}
if [ -f "$artifact_dir/openhands" ]; then
cp "$artifact_dir/openhands" "release-assets/openhands-$platform_arch"
echo "✅ Copied $artifact_dir/openhands to release-assets/openhands-$platform_arch"
else
echo "⚠️ No openhands binary found in $artifact_dir"
fi
fi
done
echo "📁 Release assets prepared:"
ls -la release-assets/
- name: Create GitHub Release

View File

@@ -72,6 +72,7 @@ def check_pyinstaller() -> bool:
def build_executable(
spec_file: str = 'openhands.spec',
clean: bool = True,
target_arch: str = None,
) -> bool:
"""Build the executable using PyInstaller."""
if clean:
@@ -83,8 +84,27 @@ def build_executable(
print(f'🔨 Building executable using {spec_file}...')
# Handle target architecture for macOS by modifying the spec file
original_spec_content = None
if target_arch and sys.platform == 'darwin':
print(f'🎯 Building for macOS target architecture: {target_arch}')
# Read the original spec file
with open(spec_file, 'r') as f:
original_spec_content = f.read()
# Replace target_arch=None with target_arch='<arch>'
modified_spec_content = original_spec_content.replace(
'target_arch=None',
f"target_arch='{target_arch}'"
)
# Write the modified spec file
with open(spec_file, 'w') as f:
f.write(modified_spec_content)
try:
# Run PyInstaller with uv
# Run PyInstaller with uv (no --target-arch flag needed with spec file)
cmd = ['uv', 'run', 'pyinstaller', spec_file, '--clean']
print(f'Running: {" ".join(cmd)}')
@@ -113,6 +133,12 @@ def build_executable(
if e.stderr:
print('STDERR:', e.stderr)
return False
finally:
# Restore the original spec file if we modified it
if original_spec_content is not None:
with open(spec_file, 'w') as f:
f.write(original_spec_content)
print(f'🔄 Restored original {spec_file}')
# =================================================
@@ -254,6 +280,10 @@ def main() -> int:
action='store_true',
help='Install PyInstaller using uv before building',
)
parser.add_argument(
'--target-arch',
help='Target architecture for macOS builds (x86_64, arm64, universal2)',
)
parser.add_argument(
'--no-build', action='store_true', help='Skip testing the built executable'
@@ -270,7 +300,9 @@ def main() -> int:
return 1
# Build the executable
if not args.no_build and not build_executable(args.spec, clean=not args.no_clean):
if not args.no_build and not build_executable(
args.spec, clean=not args.no_clean, target_arch=args.target_arch
):
return 1
# Test the executable