1
0
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:
Mike Bonnet
2026-01-13 08:39:00 -08:00
parent 6e261e9d55
commit c72493f24a
6 changed files with 65 additions and 62 deletions

View File

@@ -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

View File

@@ -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

Binary file not shown.

View File

@@ -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',
},
)

View File

@@ -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"

View 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>