mirror of
https://github.com/containers/ramalama.git
synced 2026-02-05 06:46:39 +01:00
macOS installer: build and install an app bundle
Switch from building a onefile executable to a onedir executable and app bundle. The onefile executable is less efficient because it needs to extract resources into a temp directory on every run, and it will no longer be supported as part of an app bundle in future versions of pyinstaller. Skip installing config and inference files under /usr, they will be referenced from within the app bundle. Use a plist when calling pkgbuild to avoid the macOS installer "relocating" the app bundle to arbitrary locations, which breaks the "ramalama" symlink. Signed-off-by: Mike Bonnet <mikeb@redhat.com>
This commit is contained in:
2
.github/workflows/build-macos-installer.yml
vendored
2
.github/workflows/build-macos-installer.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
pip3 install pyinstaller
|
||||
pip3 install -e .
|
||||
pip3 install .
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
|
||||
@@ -30,11 +30,11 @@ sudo installer -pkg RamaLama-VERSION-macOS-Installer.pkg -target /
|
||||
|
||||
The installer places files in:
|
||||
- `/usr/local/bin/ramalama` - Main executable
|
||||
- `/usr/local/share/ramalama/` - Configuration files
|
||||
- `/usr/local/share/man/` - Man pages
|
||||
- `/usr/local/share/bash-completion/` - Bash completions
|
||||
- `/usr/local/share/fish/` - Fish completions
|
||||
- `/usr/local/share/zsh/` - Zsh completions
|
||||
- `/Applications/ramalama.app` - App bundle containing configuration files, resources, Python dependencies, etc.
|
||||
|
||||
### Verify Installation
|
||||
|
||||
@@ -124,8 +124,10 @@ To remove RamaLama:
|
||||
# Remove the executable
|
||||
sudo rm /usr/local/bin/ramalama
|
||||
|
||||
# Remove the app bundle
|
||||
sudo rm -rf /Applications/ramalama.app
|
||||
|
||||
# Remove configuration and data files (optional)
|
||||
sudo rm -rf /usr/local/share/ramalama
|
||||
rm -rf ~/.local/share/ramalama
|
||||
rm -rf ~/.config/ramalama
|
||||
|
||||
|
||||
BIN
logos/ICNS/ramalama.icns
Normal file
BIN
logos/ICNS/ramalama.icns
Normal file
Binary file not shown.
@@ -15,8 +15,6 @@ from pathlib import Path
|
||||
sys.path.insert(0, str(Path.cwd() / 'ramalama'))
|
||||
from version import version as get_version
|
||||
|
||||
block_cipher = None
|
||||
|
||||
# Get the project root directory
|
||||
project_root = Path.cwd()
|
||||
|
||||
@@ -80,28 +78,21 @@ a = Analysis(
|
||||
'pandas',
|
||||
'PIL',
|
||||
],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='ramalama',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=False, # Disabled for Apple Silicon compatibility
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
@@ -110,11 +101,20 @@ exe = EXE(
|
||||
entitlements_file=None,
|
||||
)
|
||||
|
||||
# Create app bundle for macOS
|
||||
app = BUNDLE(
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=False,
|
||||
upx_exclude=[],
|
||||
name='ramalama',
|
||||
)
|
||||
|
||||
app = BUNDLE(
|
||||
coll,
|
||||
name='ramalama.app',
|
||||
icon=None,
|
||||
icon='logos/ICNS/ramalama.icns',
|
||||
bundle_identifier='com.github.containers.ramalama',
|
||||
version=app_version,
|
||||
info_plist={
|
||||
@@ -123,8 +123,7 @@ app = BUNDLE(
|
||||
'CFBundleIdentifier': 'com.github.containers.ramalama',
|
||||
'CFBundleVersion': app_version,
|
||||
'CFBundleShortVersionString': app_version,
|
||||
'NSHumanReadableCopyright': 'Copyright © 2024 The Containers Organization',
|
||||
'NSHumanReadableCopyright': 'Copyright © 2026 The Containers Organization',
|
||||
'CFBundleExecutable': 'ramalama',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -57,24 +57,18 @@ rm -rf "$PROJECT_ROOT/dist"
|
||||
rm -rf "$PROJECT_ROOT/build"
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
# Build standalone executable with PyInstaller
|
||||
echo "Building standalone executable..."
|
||||
# Build app bundle with PyInstaller
|
||||
echo "Building app bundle..."
|
||||
cd "$PROJECT_ROOT"
|
||||
pyinstaller ramalama.spec --clean --noconfirm
|
||||
|
||||
# Verify build - PyInstaller creates either a single file or directory
|
||||
BUILT_EXECUTABLE=""
|
||||
if [ -f "$PROJECT_ROOT/dist/ramalama" ]; then
|
||||
BUILT_EXECUTABLE="$PROJECT_ROOT/dist/ramalama"
|
||||
echo "Found single-file executable: $BUILT_EXECUTABLE"
|
||||
elif [ -f "$PROJECT_ROOT/dist/ramalama/ramalama" ]; then
|
||||
BUILT_EXECUTABLE="$PROJECT_ROOT/dist/ramalama/ramalama"
|
||||
echo "Found executable in bundle: $BUILT_EXECUTABLE"
|
||||
elif [ -d "$PROJECT_ROOT/dist/ramalama.app" ]; then
|
||||
BUILT_EXECUTABLE="$PROJECT_ROOT/dist/ramalama.app/Contents/MacOS/ramalama"
|
||||
echo "Found app bundle: $PROJECT_ROOT/dist/ramalama.app"
|
||||
# Verify build - PyInstaller creates a .app directory
|
||||
BUNDLE_DIR="$PROJECT_ROOT/dist/ramalama.app"
|
||||
BUILT_EXECUTABLE="$BUNDLE_DIR/Contents/MacOS/ramalama"
|
||||
if [ -d "$BUNDLE_DIR" ]; then
|
||||
echo "Found app bundle: $BUNDLE_DIR"
|
||||
else
|
||||
echo "Error: PyInstaller build failed - no executable found in dist/"
|
||||
echo "Error: PyInstaller build failed - no bundle found in dist/"
|
||||
echo "Contents of dist/:"
|
||||
ls -la "$PROJECT_ROOT/dist/" || true
|
||||
exit 1
|
||||
@@ -86,46 +80,24 @@ if [ ! -x "$BUILT_EXECUTABLE" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Standalone executable built successfully: $BUILT_EXECUTABLE"
|
||||
echo "App bundle built successfully: $BUNDLE_DIR"
|
||||
|
||||
# Create package structure
|
||||
echo "Creating package structure..."
|
||||
PKG_ROOT="$BUILD_DIR/package-root"
|
||||
mkdir -p "$PKG_ROOT/usr/local/bin"
|
||||
mkdir -p "$PKG_ROOT/usr/local/share/ramalama"
|
||||
mkdir -p "$PKG_ROOT/usr/local/share/man/man1"
|
||||
mkdir -p "$PKG_ROOT/usr/local/share/man/man5"
|
||||
mkdir -p "$PKG_ROOT/usr/local/share/man/man7"
|
||||
mkdir -p "$PKG_ROOT/usr/local/share/bash-completion/completions"
|
||||
mkdir -p "$PKG_ROOT/usr/local/share/fish/vendor_completions.d"
|
||||
mkdir -p "$PKG_ROOT/usr/local/share/zsh/site-functions"
|
||||
mkdir -p "$PKG_ROOT/Applications"
|
||||
|
||||
# Copy executable
|
||||
echo "Copying executable..."
|
||||
if [ -d "$PROJECT_ROOT/dist/ramalama.app" ]; then
|
||||
# For app bundle, copy the executable from inside
|
||||
cp "$BUILT_EXECUTABLE" "$PKG_ROOT/usr/local/bin/ramalama"
|
||||
else
|
||||
# For single file or directory bundle
|
||||
cp "$BUILT_EXECUTABLE" "$PKG_ROOT/usr/local/bin/ramalama"
|
||||
fi
|
||||
chmod +x "$PKG_ROOT/usr/local/bin/ramalama"
|
||||
|
||||
# Verify the copy worked
|
||||
if [ ! -x "$PKG_ROOT/usr/local/bin/ramalama" ]; then
|
||||
echo "Error: Failed to copy executable to package root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy configuration files
|
||||
echo "Copying configuration files..."
|
||||
cp "$PROJECT_ROOT/shortnames/shortnames.conf" "$PKG_ROOT/usr/local/share/ramalama/"
|
||||
cp "$PROJECT_ROOT/docs/ramalama.conf" "$PKG_ROOT/usr/local/share/ramalama/"
|
||||
|
||||
# Copy inference spec files
|
||||
mkdir -p "$PKG_ROOT/usr/local/share/ramalama/inference"
|
||||
cp "$PROJECT_ROOT"/inference-spec/schema/*.json "$PKG_ROOT/usr/local/share/ramalama/inference/"
|
||||
cp "$PROJECT_ROOT"/inference-spec/engines/* "$PKG_ROOT/usr/local/share/ramalama/inference/"
|
||||
# Copy bundle
|
||||
echo "Copying bundle..."
|
||||
cp -a "$BUNDLE_DIR" "$PKG_ROOT/Applications"
|
||||
ln -s ../../../Applications/ramalama.app/Contents/MacOS/ramalama "$PKG_ROOT/usr/local/bin/ramalama"
|
||||
|
||||
# Copy man pages
|
||||
echo "Copying documentation..."
|
||||
@@ -205,6 +177,7 @@ pkgbuild \
|
||||
--version "$VERSION" \
|
||||
--scripts "$SCRIPTS_DIR" \
|
||||
--install-location "/" \
|
||||
--component-plist "$SCRIPT_DIR/macos-installer/ramalama-pkg.plist" \
|
||||
"$PKG_OUTPUT"
|
||||
|
||||
# Copy installer resource files from templates
|
||||
@@ -243,4 +216,3 @@ echo "To distribute:"
|
||||
echo " 1. Sign the package (recommended for public distribution)"
|
||||
echo " 2. Upload to GitHub Releases"
|
||||
echo " 3. Optionally notarize with Apple"
|
||||
|
||||
|
||||
30
scripts/macos-installer/ramalama-pkg.plist
Normal file
30
scripts/macos-installer/ramalama-pkg.plist
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<array>
|
||||
<dict>
|
||||
<key>BundleHasStrictIdentifier</key>
|
||||
<true/>
|
||||
<key>BundleIsRelocatable</key>
|
||||
<false/>
|
||||
<key>BundleIsVersionChecked</key>
|
||||
<true/>
|
||||
<key>BundleOverwriteAction</key>
|
||||
<string>upgrade</string>
|
||||
<key>RootRelativeBundlePath</key>
|
||||
<string>Applications/ramalama.app</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>BundleHasStrictIdentifier</key>
|
||||
<true/>
|
||||
<key>BundleIsRelocatable</key>
|
||||
<false/>
|
||||
<key>BundleIsVersionChecked</key>
|
||||
<true/>
|
||||
<key>BundleOverwriteAction</key>
|
||||
<string>upgrade</string>
|
||||
<key>RootRelativeBundlePath</key>
|
||||
<string>Applications/ramalama.app/Contents/Frameworks/Python.framework</string>
|
||||
</dict>
|
||||
</array>
|
||||
</plist>
|
||||
Reference in New Issue
Block a user