Skip to content

Commit

Permalink
feat: complete compilation including homebrew formulae generation
Browse files Browse the repository at this point in the history
  • Loading branch information
SethBurkart123 committed Nov 13, 2024
1 parent 4c98108 commit d6f2b31
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 1 deletion.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ __pycache__/
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
Expand Down Expand Up @@ -171,3 +170,6 @@ cython_debug/

**/*.xcodeproj/*
.aider*

.DS_Store
.vscode
18 changes: 18 additions & 0 deletions Formula/exo.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
cask "exo" do
version "0.1.0"
sha256 "57fc9b838688a4dbd4842db4a96888f7627d5df16fd633bf2401340a7388cba6"

url "http://localhost:8000/exo-0.1.0-darwin-arm64.zip"
name "Exo"
desc "MLX-powered AI assistant"
homepage "https://github.com/exo-explorer/exo"

depends_on macos: ">= :ventura"
depends_on arch: :arm64

binary "#{staged_path}/exo-0.1.0-darwin-arm64/exo"

postflight do
set_permissions "#{staged_path}/exo-0.1.0-darwin-arm64/exo", "0755"
end
end
62 changes: 62 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/bin/bash
set -e

# Configuration
VERSION="0.1.0"
APP_NAME="exo"
DIST_DIR="dist"
PACKAGE_NAME="${APP_NAME}-${VERSION}-darwin-arm64"

# 1. Clean previous builds
echo "Cleaning previous builds..."
rm -rf build dist

# 2. Run PyInstaller
echo "Building with PyInstaller..."
pyinstaller exo.spec

# 3. Create a clean distribution directory
echo "Creating distribution package..."
mkdir -p "${DIST_DIR}/${PACKAGE_NAME}"
cp -r "dist/${APP_NAME}/"* "${DIST_DIR}/${PACKAGE_NAME}/"

# 4. Create ZIP file
echo "Creating ZIP archive..."
cd "${DIST_DIR}"
zip -r "${PACKAGE_NAME}.zip" "${PACKAGE_NAME}"
cd ..

# 5. Calculate SHA256
echo "Calculating SHA256..."
SHA256=$(shasum -a 256 "${DIST_DIR}/${PACKAGE_NAME}.zip" | cut -d' ' -f1)

# 6. Generate Homebrew Cask formula
echo "Generating Homebrew formula..."
cat > Formula/exo.rb << EOL
cask "exo" do
version "${VERSION}"
sha256 "${SHA256}"
url "https://github.com/sethburkart123/exo/releases/download/test/exo-0.1.0-darwin-arm64.zip"
name "Exo"
desc "MLX-powered AI assistant"
homepage "https://github.com/exo-explorer/exo"
depends_on macos: ">= :ventura"
depends_on arch: :arm64
binary "#{staged_path}/exo-${VERSION}-darwin-arm64/exo"
postflight do
set_permissions "#{staged_path}/exo-${VERSION}-darwin-arm64/exo", "0755"
end
end
EOL

echo "Done! Package created at: ${DIST_DIR}/${PACKAGE_NAME}.zip"
echo "SHA256: ${SHA256}"
echo ""
echo "Next steps:"
echo "1. Upload ${PACKAGE_NAME}.zip to GitHub releases"
echo "2. Update the URL in the formula with your actual GitHub repository"
echo "3. Test the formula locally with: brew install --cask ./Formula/exo.rb"
201 changes: 201 additions & 0 deletions exo.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# -*- mode: python ; coding: utf-8 -*-
import sys
import os
import shutil
from PyInstaller.utils.hooks import collect_all, collect_submodules, copy_metadata

# Basic Configuration
block_cipher = None
name = os.environ.get('EXO_NAME', 'exo')
spec_dir = os.path.dirname(os.path.abspath(SPEC))
root_dir = os.path.abspath(os.path.join(spec_dir))

# Get Python library path dynamically
python_exec = sys.executable
python_prefix = sys.prefix
if sys.platform.startswith('darwin'):
# On macOS, construct library path based on current Python version
version = f"{sys.version_info.major}.{sys.version_info.minor}"
lib_patterns = [
os.path.join(python_prefix, 'lib', f'libpython{version}.dylib'),
os.path.join(python_prefix, 'lib', f'libpython{version}m.dylib'),
os.path.join(os.path.dirname(python_exec), '..', 'lib', f'libpython{version}.dylib'),
]

PYTHON_LIB = None
for pattern in lib_patterns:
if os.path.exists(pattern):
PYTHON_LIB = pattern
break

if not PYTHON_LIB:
raise FileNotFoundError(f"Could not find Python library for Python {version}")
print(f"Using Python library: {PYTHON_LIB}")

# Create a local copy of the Python library
local_lib_dir = os.path.join(spec_dir, 'lib')
os.makedirs(local_lib_dir, exist_ok=True)
local_python_lib = os.path.join(local_lib_dir, 'libpython3.12.dylib')
shutil.copy2(PYTHON_LIB, local_python_lib)
print(f"Copied Python library to: {local_python_lib}")

# Model Collection
models_dir = os.path.join(root_dir, 'exo', 'inference', 'mlx', 'models')
model_files = []
for root, dirs, files in os.walk(models_dir):
for file in files:
if file.endswith('.py') and file not in ['__init__.py', 'base.py']:
model_files.append(os.path.join(root, file))

model_imports = [
f"exo.inference.mlx.models.{os.path.basename(f)[:-3]}"
for f in model_files
if '__pycache__' not in f
]

# Data Collection
datas = [
(os.path.join(root_dir, 'exo/tinychat'), 'exo/tinychat'),
(os.path.join(root_dir, 'exo'), 'exo'),
(local_python_lib, '.'),
]

# Collect Transformers Data
print("Collecting transformers data...")
try:
trans_datas, _, _ = collect_all('transformers')
filtered_datas = [(src, dst) for src, dst in trans_datas
if not any(x in dst.lower() for x in ['.git', 'test', 'examples'])]
datas.extend(filtered_datas)
datas.extend(copy_metadata('transformers'))
except Exception as e:
print(f"Warning: Could not collect transformers data: {e}")

# MLX Integration
if sys.platform.startswith('darwin'):
print("Configuring macOS specific settings...")
mlx_locations = [
'/opt/homebrew/Caskroom/miniconda/base/envs/exo/lib/python3.12/site-packages/mlx',
os.path.join(root_dir, 'venv/lib/python3.12/site-packages/mlx'),
os.path.join(python_prefix, 'lib/python3.12/site-packages/mlx'), # Added new search path
]

mlx_path = None
for loc in mlx_locations:
if os.path.exists(loc):
mlx_path = os.path.abspath(loc)
print(f"Found MLX at: {mlx_path}")
break

if mlx_path:
datas.append((mlx_path, 'mlx'))
# Search for metallib in multiple possible locations
metallib_locations = [
os.path.join(mlx_path, 'backend/metal/kernels/mlx.metallib'),
os.path.join(mlx_path, 'mlx/backend/metal/kernels/mlx.metallib'),
os.path.join(python_prefix, 'lib/python3.12/site-packages/mlx/backend/metal/kernels/mlx.metallib'),
]

metallib_found = False
for metallib_path in metallib_locations:
if os.path.exists(metallib_path):
print(f"Found metallib at: {metallib_path}")
# Add metallib to both the root and the MLX directory structure
datas.extend([
(metallib_path, '.'),
(metallib_path, 'mlx/backend/metal/kernels')
])
metallib_found = True
break

if not metallib_found:
print("ERROR: Could not find mlx.metallib in any expected location!")
print("Searched locations:", "\n".join(metallib_locations))
sys.exit(1)
else:
print("ERROR: MLX package not found in expected locations")
sys.exit(1)

# Initial binaries list with Python library
binaries = []
if sys.platform.startswith('darwin'):
binaries.append((local_python_lib, '.'))

# Analysis Configuration
a = Analysis(
[os.path.join(root_dir, 'exo/main.py')],
pathex=[root_dir],
binaries=binaries,
datas=datas,
hiddenimports=[
'transformers',
'safetensors',
'safetensors.torch',
'exo',
'packaging.version',
'packaging.specifiers',
'packaging.requirements',
'packaging.markers',
'charset_normalizer',
'requests',
'urllib3',
'certifi',
'idna',
'mlx',
'mlx.core',
'mlx.nn',
'mlx.backend',
'mlx.backend.metal',
'mlx.backend.metal.kernels',
'_sysconfigdata__darwin_darwin',
] + model_imports,
hookspath=[],
hooksconfig={
'urllib3': {'ssl': True},
'transformers': {'module': True}
},
runtime_hooks=[],
excludes=[
'pytest',
'sentry_sdk'
],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)

# Make sure Python library is included in both datas and binaries
a.datas = list(dict.fromkeys(a.datas + [(os.path.basename(local_python_lib), local_python_lib, 'DATA')]))
a.binaries = list(dict.fromkeys(a.binaries + [(os.path.basename(local_python_lib), local_python_lib, 'BINARY')]))

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name=name,
debug=False, # Enable debug mode
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch='arm64',
codesign_identity=None,
entitlements_file=None,
)
# Create the collection
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name=name,
)

0 comments on commit d6f2b31

Please sign in to comment.