Skip to content

Build and Release

Build and Release #4

Workflow file for this run

name: Build and Release
on:
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., v1.0.0)'
required: true
release_notes:
description: 'Release notes'
required: true
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
include:
- os: windows-latest
output_name: VoiceCommand.exe
asset_name: VoiceCommand-Windows.exe
data_separator: ";"
- os: ubuntu-latest
output_name: VoiceCommand
asset_name: VoiceCommand-Linux
data_separator: ":"
- os: macos-latest
output_name: VoiceCommand
asset_name: VoiceCommand-MacOS
data_separator: ":"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'pip'
- name: Install dependencies
if: matrix.os != 'ubuntu-latest'
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pyinstaller
# Print Python environment info for debugging
- name: Print Python environment info
run: |
python --version
pip --version
pip list
echo "Python executable: $(which python || where python)"
# Install system dependencies for Linux
- name: Install Linux system dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
# Install comprehensive list of audio libraries and dependencies
sudo apt-get install -y \
portaudio19-dev \
python3-pyaudio \
libxcb-xinerama0 \
libasound2-dev \
libpulse-dev \
libsndfile1-dev \
libdbus-1-dev \
libglib2.0-dev \
pkg-config \
build-essential \
ffmpeg
# Special handling for PyAudio on macOS
- name: Install PyAudio on macOS
if: matrix.os == 'macos-latest'
run: |
brew install portaudio
pip install pyaudio
# Special handling for PyAudio on Windows
- name: Install PyAudio on Windows
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
# Windows typically doesn't need special handling for PyAudio if pre-built wheels are available
pip install pyaudio
# Special handling for requirements on Linux
- name: Install dependencies on Linux
if: matrix.os == 'ubuntu-latest'
run: |
# Link to the system-installed PyAudio
python -m pip install --upgrade pip
# Create modified requirements file without PyAudio
grep -v "pyaudio" requirements.txt > requirements_no_pyaudio.txt
# Install other requirements
pip install -r requirements_no_pyaudio.txt
pip install pyinstaller
echo "Using system-installed PyAudio"
python -c "import pyaudio; print('PyAudio version:', pyaudio.__version__)" || {
echo "System PyAudio not working. Attempting to compile PyAudio directly."
export PYTHONPATH=/usr/lib/python3/dist-packages:$PYTHONPATH
# Attempt to compile PyAudio from source with more verbose output
pip install --verbose --force-reinstall pyaudio || {
echo "Failed to compile PyAudio. Creating a stub module."
mkdir -p $(python -c "import site; print(site.getsitepackages()[0])")/pyaudio
echo "__version__ = 'stub-0.0.0'" > $(python -c "import site; print(site.getsitepackages()[0])")/pyaudio/__init__.py
echo "class PyAudio: pass" >> $(python -c "import site; print(site.getsitepackages()[0])")/pyaudio/__init__.py
}
}
echo "Python environment information:"
python -c "import sys; print(sys.path)"
# Build with PyInstaller, using the appropriate data separator for each OS
- name: List directories (Linux/macOS)
if: runner.os != 'Windows'
shell: bash
run: |
echo "Current directory contents:"
ls -la
echo "Public directory contents:"
ls -la public
- name: List directories (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
echo "Current directory contents:"
dir
echo "Public directory contents:"
dir public
- name: Build with PyInstaller (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
# Create PyAudio stub if needed
if ! python -c "import pyaudio" 2>/dev/null; then
echo "Creating PyAudio stub because import failed"
mkdir -p pyaudio_stub
echo "__version__ = 'stub-0.0.0'" > pyaudio_stub/__init__.py
echo "class PyAudio: pass" >> pyaudio_stub/__init__.py
PYTHONPATH="$PWD:$PYTHONPATH"
fi
python -m pip install --verbose pyinstaller
# Create a stub main script that catches PyAudio errors
cat > app_wrapper.py << 'EOL'
try:
import app
except ImportError as e:
print(f"Import error: {e}")
if "pyaudio" in str(e).lower():
print("PyAudio import failed - creating mock")
import sys
import types
sys.modules['pyaudio'] = types.ModuleType('pyaudio')
sys.modules['pyaudio'].__version__ = 'stub-0.0.0'
sys.modules['pyaudio'].PyAudio = type('PyAudio', (), {})
import app
EOL
# Use --hidden-import to ensure audio libraries are included and --exclude to avoid PyAudio if needed
pyinstaller --debug=all --onefile \
--add-data "public${{ matrix.data_separator }}public" \
--icon=public/favicon.ico \
--hidden-import=pyaudio \
--hidden-import=speech_recognition \
--hidden-import=typing \
--hidden-import=_portaudio \
--hidden-import=sounddevice \
app_wrapper.py \
--name VoiceCommand
echo "Dist directory contents after build:"
ls -la dist
- name: Build with PyInstaller (macOS)
if: runner.os == 'macOS'
shell: bash
run: |
python -m pip install --verbose pyinstaller
pyinstaller --onefile --add-data "public${{ matrix.data_separator }}public" --icon=public/favicon.ico --name VoiceCommand app.py
echo "Dist directory contents after build:"
ls -la dist
- name: Build with PyInstaller (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
python -m pip install --verbose pyinstaller
pyinstaller --onefile --add-data "public${{ matrix.data_separator }}public" --icon=public/favicon.ico --name VoiceCommand app.py
echo "Dist directory contents after build:"
dir dist
# Set executable permissions for Linux and macOS
- name: Set executable permissions
if: matrix.os != 'windows-latest'
shell: bash
run: |
chmod +x dist/${{ matrix.output_name }}
# Upload artifacts (separate from release for diagnostic purposes)
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.asset_name }}
path: dist/${{ matrix.output_name }}
# Create a release with all builds attached
create-release:
needs: build
runs-on: ubuntu-latest
if: always() && !contains(needs.build.result, 'cancelled')
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: List downloaded artifacts
shell: bash
run: |
echo "Downloaded artifacts:"
find artifacts -type f | sort
- name: Prepare release files
shell: bash
id: prepare_files
run: |
FILES=""
# Check if Windows build succeeded
if [ -f "artifacts/VoiceCommand-Windows.exe/VoiceCommand.exe" ]; then
FILES="${FILES}artifacts/VoiceCommand-Windows.exe/VoiceCommand.exe\n"
echo "Windows build found"
else
echo "Windows build not found or failed"
fi
# Check if Linux build succeeded
if [ -f "artifacts/VoiceCommand-Linux/VoiceCommand" ]; then
FILES="${FILES}artifacts/VoiceCommand-Linux/VoiceCommand\n"
echo "Linux build found"
else
echo "Linux build not found or failed"
fi
# Check if macOS build succeeded
if [ -f "artifacts/VoiceCommand-MacOS/VoiceCommand" ]; then
FILES="${FILES}artifacts/VoiceCommand-MacOS/VoiceCommand\n"
echo "macOS build found"
else
echo "macOS build not found or failed"
fi
# Trim the trailing newline
FILES=$(echo -e "${FILES}" | sed -e 's/\\n$//')
# Set output for next step
echo "FILES<<EOF" >> $GITHUB_OUTPUT
echo -e "${FILES}" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.event.inputs.version }}
name: Release ${{ github.event.inputs.version }}
body: ${{ github.event.inputs.release_notes }}
draft: false
prerelease: false
files: ${{ steps.prepare_files.outputs.FILES }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}