set up docker compose setup

This commit is contained in:
2025-10-18 00:36:55 -05:00
committed by Jason Ross
parent 3b5a77da83
commit 7d15397a9b
7 changed files with 393 additions and 138 deletions
+56
View File
@@ -0,0 +1,56 @@
# Git
.git/
.gitignore
.gitattributes
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
dist/
build/
*.egg
.pytest_cache/
.mypy_cache/
.coverage
htmlcov/
# Virtual environments
venv/
ENV/
env/
.venv
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Build artifacts
dist_*/
pkg_dist_*/
*.deb
*.rpm
*.pkg.tar.zst
*.exe
*.asc
*.sha256
# Platform tools (downloaded at runtime)
src/platform-tools/
# Documentation
*.md
!CLAUDE.md
# CI/CD
.github/
# Other
.DS_Store
*.log
+5 -123
View File
@@ -135,50 +135,13 @@ jobs:
with: with:
ref: main ref: main
- name: Build executable - name: Build and package Debian
run: | run: |
# Container has Poetry and all dependencies pre-installed # Container has Poetry and all dependencies pre-installed
# Python build script handles both PyInstaller build and fpm packaging
poetry install --no-interaction poetry install --no-interaction
poetry run python scripts/build_package_linux.py poetry run python scripts/build_package_linux.py
- name: Package .deb (fpm)
shell: bash
run: |
set -euo pipefail
if ! VERSION="$(poetry version -s 2>&1)"; then
echo "::error::Failed to read version from pyproject.toml"
echo "::error::Poetry output: $VERSION"
exit 1
fi
if [ -z "$VERSION" ]; then
echo "::error::Version is empty in pyproject.toml"
exit 1
fi
# Validate semver format
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?(\+[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$ ]]; then
echo "::error::Invalid version format in pyproject.toml: $VERSION"
echo "::error::Expected semantic version format (e.g., 1.2.3, 1.2.3-beta.1, 1.2.3+build.123)"
exit 1
fi
echo "Using version: $VERSION"
PKG_DIR="pkg_dist_debian"
mkdir -p dist
echo "Packaging from $PKG_DIR"
ls -la "$PKG_DIR" || true
ICON_PATH="$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png"
PKG_ITEMS=( "usr/local/bin/android-file-handler" "usr/share/applications/android-file-handler.desktop" )
if [ -f "$ICON_PATH" ]; then
PKG_ITEMS+=( "usr/share/icons/hicolor/256x256/apps/android-file-handler.png" )
else
echo "Note: icon not present, packaging without icon"
fi
fpm -s dir -t deb -n android-file-handler -v "$VERSION" \
--architecture amd64 --deb-user root --deb-group root \
--after-install scripts/debian_postinst.sh \
-p "dist/android-file-handler_${VERSION}_amd64.deb" -C "$PKG_DIR" "${PKG_ITEMS[@]}"
- name: Import GPG key - name: Import GPG key
shell: bash shell: bash
run: | run: |
@@ -247,7 +210,7 @@ jobs:
- name: Pull Docker image - name: Pull Docker image
run: docker pull ghcr.io/jmr-dev/android-file-handler-arch-builder:latest run: docker pull ghcr.io/jmr-dev/android-file-handler-arch-builder:latest
- name: Build executable inside container - name: Build and package Arch
run: | run: |
docker run --rm \ docker run --rm \
-v ${{ github.workspace }}:/workspace \ -v ${{ github.workspace }}:/workspace \
@@ -258,52 +221,6 @@ jobs:
ghcr.io/jmr-dev/android-file-handler-arch-builder:latest \ ghcr.io/jmr-dev/android-file-handler-arch-builder:latest \
sh -c "poetry install --no-interaction && poetry run python scripts/build_package_linux.py" sh -c "poetry install --no-interaction && poetry run python scripts/build_package_linux.py"
- name: Package pacman (fpm) inside container
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
-w /workspace \
-e FPM_VERSION=${{ env.FPM_VERSION }} \
ghcr.io/jmr-dev/android-file-handler-arch-builder:latest \
sh -c 'set -euo pipefail && \
if ! VERSION="$(poetry version -s 2>&1)"; then \
echo "::error::Failed to read version from pyproject.toml" >&2 && \
echo "::error::Poetry output: $VERSION" >&2 && \
exit 1; \
fi && \
if [ -z "$VERSION" ]; then \
echo "::error::Version is empty in pyproject.toml" >&2 && \
exit 1; \
fi && \
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?(\+[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$ ]]; then \
echo "::error::Invalid version format in pyproject.toml: $VERSION" >&2 && \
echo "::error::Expected semantic version format (e.g., 1.2.3, 1.2.3-beta.1, 1.2.3+build.123)" >&2 && \
exit 1; \
fi && \
echo "Using version: $VERSION" && \
PKG_DIR="pkg_dist_arch" && \
mkdir -p dist && \
echo "Packaging from $PKG_DIR" && \
ls -la "$PKG_DIR" || true && \
ICON_PATH="$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png" && \
if [ -f "$ICON_PATH" ]; then \
fpm -s dir -t pacman -n android-file-handler -v "$VERSION" \
--architecture x86_64 \
-p "dist/android-file-handler-${VERSION}-1-x86_64.pkg.tar.zst" \
-C "$PKG_DIR" \
"usr/bin/android-file-handler" \
"usr/share/applications/android-file-handler.desktop" \
"usr/share/icons/hicolor/256x256/apps/android-file-handler.png"; \
else \
echo "Note: icon not present, packaging without icon" && \
fpm -s dir -t pacman -n android-file-handler -v "$VERSION" \
--architecture x86_64 \
-p "dist/android-file-handler-${VERSION}-1-x86_64.pkg.tar.zst" \
-C "$PKG_DIR" \
"usr/bin/android-file-handler" \
"usr/share/applications/android-file-handler.desktop"; \
fi'
- name: Import GPG key - name: Import GPG key
shell: bash shell: bash
run: | run: |
@@ -367,48 +284,13 @@ jobs:
with: with:
ref: main ref: main
- name: Build RHEL package - name: Build and package RHEL
run: | run: |
set -euo pipefail
# Container has Poetry and all dependencies pre-installed # Container has Poetry and all dependencies pre-installed
# Python build script handles both PyInstaller build and fpm packaging
poetry install --no-interaction poetry install --no-interaction
poetry run python scripts/build_package_linux.py poetry run python scripts/build_package_linux.py
- name: Package RHEL (fpm)
shell: bash
run: |
set -euo pipefail
if ! VERSION="$(poetry version -s 2>&1)"; then
echo "::error::Failed to read version from pyproject.toml"
echo "::error::Poetry output: $VERSION"
exit 1
fi
if [ -z "$VERSION" ]; then
echo "::error::Version is empty in pyproject.toml"
exit 1
fi
# Validate semver format
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?(\+[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$ ]]; then
echo "::error::Invalid version format in pyproject.toml: $VERSION"
echo "::error::Expected semantic version format (e.g., 1.2.3, 1.2.3-beta.1, 1.2.3+build.123)"
exit 1
fi
echo "Using version: $VERSION"
PKG_DIR="pkg_dist_rhel"
mkdir -p dist
echo "Packaging from $PKG_DIR (version=$VERSION)"
ls -la "$PKG_DIR" || true
ICON_PATH="$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png"
PKG_ITEMS=( "usr/bin/android-file-handler" "usr/share/applications/android-file-handler.desktop" )
if [ -f "$ICON_PATH" ]; then
PKG_ITEMS+=( "usr/share/icons/hicolor/256x256/apps/android-file-handler.png" )
else
echo "Note: icon not present, packaging without icon"
fi
fpm -s dir -t rpm -n android-file-handler -v "$VERSION" --architecture x86_64 --prefix /usr/bin --after-install scripts/rhel_postinst.sh -p "dist/android-file-handler-${VERSION}.x86_64.rpm" -C "$PKG_DIR" "${PKG_ITEMS[@]}"
- name: Import GPG key - name: Import GPG key
shell: bash shell: bash
run: | run: |
+20
View File
@@ -54,9 +54,29 @@ poetry run mypy src/
#### Local Development Build (Linux) #### Local Development Build (Linux)
```sh ```sh
# Interactive build (prompts for distro selection)
poetry run python scripts/build_package_linux.py poetry run python scripts/build_package_linux.py
``` ```
#### Docker Compose Build (Recommended for Linux)
```sh
# Build all distributions (Debian, Arch, RHEL)
docker compose up --build
# Build specific distribution
docker compose up --build debian
docker compose up --build arch
docker compose up --build rhel
# Build all in parallel
docker compose up --build --parallel
# Clean build artifacts
docker compose down -v && rm -rf dist pkg_dist_* dist_*
```
See [scripts/docker/README.md](scripts/docker/README.md) for detailed Docker build documentation.
#### Platform-Specific Builds #### Platform-Specific Builds
```sh ```sh
# Windows executable (PyInstaller) # Windows executable (PyInstaller)
+72
View File
@@ -0,0 +1,72 @@
# Docker Compose configuration for local multi-platform builds
# Matches the exact images and configurations from .github/workflows/release.yml
#
# Usage:
# Build all distributions: docker-compose up --build
# Build specific distro: docker-compose up --build debian
#
# Each service builds a distribution package and outputs to:
# - dist/ - Final packaged files (.deb, .rpm, .pkg.tar.zst)
# - pkg_dist_{distro}/ - Staging directory for package contents
# - dist_{distro}/ - PyInstaller build output
services:
debian:
image: ghcr.io/jmr-dev/android-file-handler-debian-builder:debian13-trixie
build:
context: .
dockerfile: scripts/docker/Dockerfile.debian
args:
FPM_VERSION: "1.16.0"
volumes:
- .:/workspace
# Exclude host .venv to prevent conflicts with container Python
- /workspace/.venv
working_dir: /workspace
environment:
- CI_CD=true
- DISTRO_TYPE=debian
- FPM_VERSION=1.16.0
- POETRY_VIRTUALENVS_IN_PROJECT=false
- POETRY_VIRTUALENVS_PATH=/tmp/poetry-cache
command: sh -c "poetry install --no-interaction && poetry run python scripts/build_package_linux.py"
arch:
image: ghcr.io/jmr-dev/android-file-handler-arch-builder:latest
build:
context: .
dockerfile: scripts/docker/Dockerfile.arch
args:
FPM_VERSION: "1.16.0"
volumes:
- .:/workspace
# Exclude host .venv to prevent conflicts with container Python
- /workspace/.venv
working_dir: /workspace
environment:
- CI_CD=true
- DISTRO_TYPE=arch
- FPM_VERSION=1.16.0
- POETRY_VIRTUALENVS_IN_PROJECT=false
- POETRY_VIRTUALENVS_PATH=/tmp/poetry-cache
command: sh -c "poetry install --no-interaction && poetry run python scripts/build_package_linux.py"
rhel:
image: ghcr.io/jmr-dev/android-file-handler-rhel-builder:fedora42
build:
context: .
dockerfile: scripts/docker/Dockerfile.rhel
args:
FPM_VERSION: "1.16.0"
volumes:
- .:/workspace
# Exclude host .venv to prevent conflicts with container Python
- /workspace/.venv
working_dir: /workspace
environment:
- CI_CD=true
- DISTRO_TYPE=rhel
- FPM_VERSION=1.16.0
- POETRY_VIRTUALENVS_IN_PROJECT=false
- POETRY_VIRTUALENVS_PATH=/tmp/poetry-cache
command: sh -c "poetry install --no-interaction && poetry run python scripts/build_package_linux.py"
+118 -14
View File
@@ -4,6 +4,7 @@ import os
import subprocess import subprocess
import shutil import shutil
import sys import sys
import re
from pathlib import Path from pathlib import Path
from enum import Enum from enum import Enum
from typing import List from typing import List
@@ -35,24 +36,130 @@ def get_distro_config(distro_type: DistroType) -> dict:
"name": "Debian", "name": "Debian",
"bin_path": "usr/local/bin", "bin_path": "usr/local/bin",
"pkg_suffix": "debian", "pkg_suffix": "debian",
"spec_file": "scripts/spec_scripts/android-file-handler-debian.spec" "spec_file": "scripts/spec_scripts/android-file-handler-debian.spec",
"pkg_type": "deb",
"architecture": "amd64",
"postinstall": "scripts/debian_postinst.sh"
}, },
DistroType.ARCH: { DistroType.ARCH: {
"name": "Arch", "name": "Arch",
"bin_path": "usr/bin", "bin_path": "usr/bin",
"pkg_suffix": "arch", "pkg_suffix": "arch",
"spec_file": "scripts/spec_scripts/android-file-handler-arch.spec" "spec_file": "scripts/spec_scripts/android-file-handler-arch.spec",
"pkg_type": "pacman",
"architecture": "x86_64",
"postinstall": None
}, },
DistroType.RHEL: { DistroType.RHEL: {
"name": "RHEL", "name": "RHEL",
"bin_path": "usr/bin", "bin_path": "usr/bin",
"pkg_suffix": "rhel", "pkg_suffix": "rhel",
"spec_file": "scripts/spec_scripts/android-file-handler-rhel.spec" "spec_file": "scripts/spec_scripts/android-file-handler-rhel.spec",
"pkg_type": "rpm",
"architecture": "x86_64",
"postinstall": "scripts/rhel_postinst.sh"
} }
} }
return configs[distro_type] return configs[distro_type]
def validate_version(version: str) -> bool:
"""Validate semantic version format."""
semver_pattern = r'^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?(\+[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$'
return bool(re.match(semver_pattern, version))
def get_version() -> str:
"""Get version from Poetry and validate it."""
result = subprocess.run(
["poetry", "version", "-s"],
capture_output=True,
text=True,
check=True
)
version = result.stdout.strip()
if not version:
print("ERROR: Version is empty in pyproject.toml")
sys.exit(1)
if not validate_version(version):
print(f"ERROR: Invalid version format in pyproject.toml: {version}")
print("Expected semantic version format (e.g., 1.2.3, 1.2.3-beta.1, 1.2.3+build.123)")
sys.exit(1)
return version
def package_with_fpm(distro_type: DistroType, version: str, project_root: Path) -> None:
"""Package the built application using fpm."""
config = get_distro_config(distro_type)
pkg_dir = project_root / f"pkg_dist_{config['pkg_suffix']}"
dist_dir = project_root / "dist"
print(f"\n=== Packaging {config['name']} with fpm ===")
# Ensure dist directory exists
dist_dir.mkdir(exist_ok=True)
# Check if icon exists
icon_path = pkg_dir / "usr/share/icons/hicolor/256x256/apps/android-file-handler.png"
icon_included = icon_path.exists()
# Build package items list
pkg_items = [
f"{config['bin_path']}/android-file-handler",
"usr/share/applications/android-file-handler.desktop"
]
if icon_included:
pkg_items.append("usr/share/icons/hicolor/256x256/apps/android-file-handler.png")
else:
print("Note: icon not present, packaging without icon")
# Build fpm command based on distro type
fpm_cmd = [
"fpm",
"-s", "dir",
"-t", config["pkg_type"],
"-n", "android-file-handler",
"-v", version,
"--architecture", config["architecture"],
"-C", str(pkg_dir)
]
# Add distro-specific options
if distro_type == DistroType.DEBIAN:
output_file = dist_dir / f"android-file-handler_{version}_{config['architecture']}.deb"
fpm_cmd.extend([
"--deb-user", "root",
"--deb-group", "root",
"--after-install", config["postinstall"],
"-p", str(output_file)
])
elif distro_type == DistroType.ARCH:
output_file = dist_dir / f"android-file-handler-{version}-1-{config['architecture']}.pkg.tar.zst"
fpm_cmd.extend([
"-p", str(output_file)
])
elif distro_type == DistroType.RHEL:
output_file = dist_dir / f"android-file-handler-{version}.{config['architecture']}.rpm"
fpm_cmd.extend([
"--prefix", "/usr/bin",
"--after-install", config["postinstall"],
"-p", str(output_file)
])
# Add package items
fpm_cmd.extend(pkg_items)
# Run fpm
print(f"Creating package: {output_file}")
run_command(fpm_cmd, working_dir=str(project_root))
print(f"Package created successfully: {output_file}")
def prompt_distro_selection() -> List[DistroType]: def prompt_distro_selection() -> List[DistroType]:
"""Prompt user for distro selection.""" """Prompt user for distro selection."""
print("Select distribution(s) to build for:") print("Select distribution(s) to build for:")
@@ -60,7 +167,7 @@ def prompt_distro_selection() -> List[DistroType]:
print("2. Arch") print("2. Arch")
print("3. RHEL") print("3. RHEL")
print("4. All distributions") print("4. All distributions")
while True: while True:
choice = input("Enter choice (1-4): ").strip() choice = input("Enter choice (1-4): ").strip()
if choice == "1": if choice == "1":
@@ -161,7 +268,7 @@ StartupNotify=true
pkg_items.append("usr/share/icons/hicolor/256x256/apps/android-file-handler.png") pkg_items.append("usr/share/icons/hicolor/256x256/apps/android-file-handler.png")
# Debug listing # Debug listing
print(f"Packaging the following items for {config['name']} (relative to {pkg_dir}):") print(f"Prepared items for {config['name']} (relative to {pkg_dir}):")
for item in pkg_items: for item in pkg_items:
print(f" - {item}") print(f" - {item}")
item_path = pkg_dir / item item_path = pkg_dir / item
@@ -171,6 +278,9 @@ StartupNotify=true
else: else:
print(f" (missing) {item_path}") print(f" (missing) {item_path}")
# Package with fpm
package_with_fpm(distro_type, version, project_root)
def main() -> None: def main() -> None:
# Get project root (parent of scripts directory) # Get project root (parent of scripts directory)
@@ -211,14 +321,8 @@ def main() -> None:
run_command(["poetry", "lock"]) run_command(["poetry", "lock"])
run_command(["poetry", "install"]) run_command(["poetry", "install"])
# Get version from Poetry # Get and validate version from Poetry
result = subprocess.run( version = get_version()
["poetry", "version", "-s"],
capture_output=True,
text=True,
check=True
)
version = result.stdout.strip()
print(f"Version: {version}") print(f"Version: {version}")
# Build for selected distributions # Build for selected distributions
+1 -1
View File
@@ -48,7 +48,7 @@ ENV PATH="$PYENV_ROOT/bin:$PATH"
RUN git clone https://github.com/pyenv/pyenv.git /root/.pyenv RUN git clone https://github.com/pyenv/pyenv.git /root/.pyenv
# Install Python 3.12 via pyenv with tkinter support # Install Python 3.13 via pyenv with tkinter support
# The tk8-devel package must be installed before this step for _tkinter to be compiled # The tk8-devel package must be installed before this step for _tkinter to be compiled
RUN eval "$(pyenv init -)" && \ RUN eval "$(pyenv init -)" && \
LDFLAGS="-L/usr/lib64" \ LDFLAGS="-L/usr/lib64" \
+121
View File
@@ -0,0 +1,121 @@
# Docker Build Environment
This directory contains Dockerfiles for building the Android File Handler on different Linux distributions. These images match exactly the images used in the CI/CD pipeline.
## Quick Start
### Using Docker Compose (Recommended)
Build for all distributions:
```bash
docker-compose up --build
```
Build for a specific distribution:
```bash
docker-compose up --build debian
docker-compose up --build arch
docker-compose up --build rhel
```
Build all distributions in parallel:
```bash
docker-compose up --build --parallel
```
### Manual Docker Build
Build the image:
```bash
# Debian
docker build -f scripts/docker/Dockerfile.debian -t android-file-handler-debian-builder .
# Arch
docker build -f scripts/docker/Dockerfile.arch -t android-file-handler-arch-builder .
# RHEL/Fedora
docker build -f scripts/docker/Dockerfile.rhel -t android-file-handler-rhel-builder .
```
Run the build:
```bash
# Debian
docker run --rm -v $(pwd):/workspace -w /workspace android-file-handler-debian-builder
# Arch
docker run --rm -v $(pwd):/workspace -w /workspace android-file-handler-arch-builder
# RHEL/Fedora
docker run --rm -v $(pwd):/workspace -w /workspace android-file-handler-rhel-builder
```
## Output
After building, you'll find:
- `dist/` - Final packaged files (.deb, .rpm, .pkg.tar.zst)
- `pkg_dist_{distro}/` - Staging directories for package contents
- `dist_{distro}/` - PyInstaller build outputs
## Images
### Debian Builder
- **Image**: `ghcr.io/jmr-dev/android-file-handler-debian-builder:debian13-trixie`
- **Base**: `debian:13`
- **Python**: 3.13 (via pyenv)
- **Tools**: Poetry, fpm, PyInstaller
### Arch Builder
- **Image**: `ghcr.io/jmr-dev/android-file-handler-arch-builder:latest`
- **Base**: `archlinux:latest`
- **Python**: 3.13 (via pyenv)
- **Tools**: Poetry, fpm, PyInstaller
### RHEL Builder
- **Image**: `ghcr.io/jmr-dev/android-file-handler-rhel-builder:fedora42`
- **Base**: `fedora:42`
- **Python**: 3.13 (via pyenv)
- **Tools**: Poetry, fpm, PyInstaller
## Troubleshooting
### Virtualenv Conflicts
The Docker Compose configuration automatically excludes the host's `.venv` directory to prevent conflicts between the host Python environment and the container Python environment. Each container creates its own virtualenv in `/tmp/poetry-cache`.
If you encounter virtualenv-related errors, ensure you're using the latest docker-compose.yml configuration.
## Cleaning Up
Remove build artifacts:
```bash
rm -rf dist pkg_dist_* dist_*
```
Remove Docker volumes and containers:
```bash
docker compose down -v
```
## Customization
### Override FPM Version
Build with a specific fpm version:
```bash
docker-compose build --build-arg FPM_VERSION=1.15.0 debian
```
### Environment Variables
All builds use these environment variables:
- `CI_CD=true` - Runs in CI/CD mode
- `DISTRO_TYPE` - Set to `debian`, `arch`, or `rhel`
- `FPM_VERSION` - Version of fpm to use (default: 1.16.0)
## Notes
- All images use Python 3.13 built from source with tkinter support
- The builds are identical to what runs in GitHub Actions
- Poetry and fpm are pre-installed in all images
- The Python build script handles both PyInstaller and fpm packaging