Add package-first build and distribution workflow

This commit is contained in:
Thales Maciel 2026-02-27 15:06:57 -03:00
parent 4a69c3d333
commit 993f51712b
13 changed files with 462 additions and 52 deletions

View file

@ -17,9 +17,18 @@ jobs:
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install uv build python -m pip install uv build
uv sync --extra x11 uv sync --extra x11
- name: Compile - name: Release quality checks
run: python -m py_compile src/*.py tests/*.py run: make release-check
- name: Unit tests - name: Build Debian package
run: python -m unittest discover -s tests -p 'test_*.py' run: make package-deb
- name: Build artifacts - name: Build Arch package inputs
run: python -m build run: make package-arch
- name: Upload packaging artifacts
uses: actions/upload-artifact@v4
with:
name: aman-artifacts
path: |
dist/*.whl
dist/*.tar.gz
dist/*.deb
dist/arch/PKGBUILD

2
.gitignore vendored
View file

@ -4,3 +4,5 @@ __pycache__/
*.pyc *.pyc
outputs/ outputs/
models/ models/
build/
dist/

View file

@ -4,6 +4,17 @@ All notable changes to Aman will be documented in this file.
The format is based on Keep a Changelog and this project follows Semantic Versioning. The format is based on Keep a Changelog and this project follows Semantic Versioning.
## [Unreleased]
### Added
- Packaging scripts and templates for Debian (`.deb`) and Arch (`PKGBUILD` + source tarball).
- Make targets for build/package/release-check workflows.
- Persona and distribution policy documentation.
### Changed
- README now documents package-first installation for non-technical users.
- Release checklist now includes packaging artifacts.
## [0.1.0] - 2026-02-26 ## [0.1.0] - 2026-02-26
### Added ### Added

View file

@ -1,10 +1,13 @@
PYTHON ?= python3
CONFIG := $(HOME)/.config/aman/config.json CONFIG := $(HOME)/.config/aman/config.json
DIST_DIR := $(CURDIR)/dist
.PHONY: run doctor self-check install sync test check BUILD_DIR := $(CURDIR)/build
RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
RUN_CONFIG := $(if $(RUN_ARGS),$(abspath $(firstword $(RUN_ARGS))),$(CONFIG)) RUN_CONFIG := $(if $(RUN_ARGS),$(abspath $(firstword $(RUN_ARGS))),$(CONFIG))
.PHONY: run doctor self-check sync test check build package package-deb package-arch release-check install-local install-service install clean-dist clean-build clean
ifneq ($(filter run,$(firstword $(MAKECMDGOALS))),) ifneq ($(filter run,$(firstword $(MAKECMDGOALS))),)
.PHONY: $(RUN_ARGS) .PHONY: $(RUN_ARGS)
$(RUN_ARGS): $(RUN_ARGS):
@ -24,14 +27,43 @@ sync:
uv sync uv sync
test: test:
python3 -m unittest discover -s tests -p 'test_*.py' $(PYTHON) -m unittest discover -s tests -p 'test_*.py'
check: check:
python3 -m py_compile src/*.py $(PYTHON) -m py_compile src/*.py
$(MAKE) test $(MAKE) test
install: build:
uv pip install --user . $(PYTHON) -m build --no-isolation
package: package-deb package-arch
package-deb:
./scripts/package_deb.sh
package-arch:
./scripts/package_arch.sh
release-check:
$(PYTHON) -m py_compile src/*.py tests/*.py
$(MAKE) test
$(MAKE) build
install-local:
$(PYTHON) -m pip install --user ".[x11]"
install-service:
mkdir -p $(HOME)/.config/systemd/user
cp systemd/aman.service $(HOME)/.config/systemd/user/aman.service cp systemd/aman.service $(HOME)/.config/systemd/user/aman.service
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user enable --now aman systemctl --user enable --now aman
install: install-local install-service
clean-dist:
rm -rf $(DIST_DIR)
clean-build:
rm -rf $(BUILD_DIR)
clean: clean-build

123
README.md
View file

@ -3,23 +3,61 @@
Python X11 STT daemon that records audio, runs Whisper, applies local AI cleanup, and injects text. Python X11 STT daemon that records audio, runs Whisper, applies local AI cleanup, and injects text.
## Requirements ## Target User
The canonical Aman user is a desktop professional who wants dictation and
rewriting features without learning Python tooling.
- End-user path: native OS package install.
- Developer path: Python/uv workflows.
Persona details and distribution policy are documented in
[`docs/persona-and-distribution.md`](docs/persona-and-distribution.md).
## Install (Recommended)
End users do not need `uv`.
### Debian/Ubuntu (`.deb`)
Download a release artifact and install it:
```bash
sudo apt install ./aman_<version>_<arch>.deb
```
Then enable the user service:
```bash
systemctl --user daemon-reload
systemctl --user enable --now aman
```
### Arch Linux
Use the generated packaging inputs (`PKGBUILD` + source tarball) in `dist/arch/`
or your own packaging pipeline.
## Distribution Matrix
| Channel | Audience | Status |
| --- | --- | --- |
| Debian package (`.deb`) | End users on Ubuntu/Debian | Canonical |
| Arch `PKGBUILD` + source tarball | Arch maintainers/power users | Supported |
| Python wheel/sdist | Developers/integrators | Supported |
## Runtime Dependencies
- X11 - X11
- `sounddevice` (PortAudio) - PortAudio runtime (`libportaudio2` or distro equivalent)
- `faster-whisper` - GTK3 and AppIndicator runtime (`gtk3`, `libayatana-appindicator3`)
- `llama-cpp-python` - Python GTK and X11 bindings (`python3-gi`/`python-gobject`, `python-xlib`)
- Tray icon deps: `gtk3`, `libayatana-appindicator3`
- Python deps (core): `numpy`, `pillow`, `faster-whisper`, `llama-cpp-python`, `sounddevice`
- X11 extras: `PyGObject`, `python-xlib`
System packages (example names): `portaudio`/`libportaudio2`.
<details> <details>
<summary>Ubuntu/Debian</summary> <summary>Ubuntu/Debian</summary>
```bash ```bash
sudo apt install -y portaudio19-dev libportaudio2 python3-gi gir1.2-gtk-3.0 libayatana-appindicator3-1 sudo apt install -y libportaudio2 python3-gi python3-xlib gir1.2-gtk-3.0 libayatana-appindicator3-1
``` ```
</details> </details>
@ -28,7 +66,7 @@ sudo apt install -y portaudio19-dev libportaudio2 python3-gi gir1.2-gtk-3.0 liba
<summary>Arch Linux</summary> <summary>Arch Linux</summary>
```bash ```bash
sudo pacman -S --needed portaudio gtk3 libayatana-appindicator sudo pacman -S --needed portaudio gtk3 libayatana-appindicator python-gobject python-xlib
``` ```
</details> </details>
@ -37,7 +75,7 @@ sudo pacman -S --needed portaudio gtk3 libayatana-appindicator
<summary>Fedora</summary> <summary>Fedora</summary>
```bash ```bash
sudo dnf install -y portaudio portaudio-devel gtk3 libayatana-appindicator-gtk3 sudo dnf install -y portaudio gtk3 libayatana-appindicator-gtk3 python3-gobject python3-xlib
``` ```
</details> </details>
@ -46,25 +84,15 @@ sudo dnf install -y portaudio portaudio-devel gtk3 libayatana-appindicator-gtk3
<summary>openSUSE</summary> <summary>openSUSE</summary>
```bash ```bash
sudo zypper install -y portaudio portaudio-devel gtk3 libayatana-appindicator3-1 sudo zypper install -y portaudio gtk3 libayatana-appindicator3-1 python3-gobject python3-python-xlib
``` ```
</details> </details>
## Python Daemon
Install Python deps:
X11 (supported):
```bash
uv sync --extra x11
```
## Quickstart ## Quickstart
```bash ```bash
uv run aman run aman run
``` ```
On first launch, Aman opens a graphical settings window automatically. On first launch, Aman opens a graphical settings window automatically.
@ -191,15 +219,13 @@ STT hinting:
## systemd user service ## systemd user service
```bash ```bash
uv pip install --user . make install-service
cp systemd/aman.service ~/.config/systemd/user/aman.service
systemctl --user daemon-reload
systemctl --user enable --now aman
``` ```
Service notes: Service notes:
- The user unit launches `aman` from `PATH`; ensure `~/.local/bin` is present in your user PATH. - The user unit launches `aman` from `PATH`.
- Package installs should provide the `aman` command automatically.
- Inspect failures with `systemctl --user status aman` and `journalctl --user -u aman -f`. - Inspect failures with `systemctl --user status aman` and `journalctl --user -u aman -f`.
## Usage ## Usage
@ -228,21 +254,50 @@ AI processing:
- Default local llama.cpp model. - Default local llama.cpp model.
- Optional external API provider through `llm.provider=external_api`. - Optional external API provider through `llm.provider=external_api`.
Build and packaging (maintainers):
```bash
make build
make package
make package-deb
make package-arch
make release-check
```
`make package-deb` installs Python dependencies while creating the package.
For offline packaging, set `AMAN_WHEELHOUSE_DIR` to a directory containing the
required wheels.
Control: Control:
```bash ```bash
make run make run
make run config.example.json
make doctor make doctor
make self-check make self-check
make check make check
``` ```
CLI (internal/support fallback, mostly for automation/tests): Developer setup (optional, `uv` workflow):
```bash ```bash
uv sync --extra x11
uv run aman run --config ~/.config/aman/config.json uv run aman run --config ~/.config/aman/config.json
uv run aman doctor --config ~/.config/aman/config.json --json ```
uv run aman self-check --config ~/.config/aman/config.json --json
uv run aman version Developer setup (optional, `pip` workflow):
uv run aman init --config ~/.config/aman/config.json --force
```bash
make install-local
aman run --config ~/.config/aman/config.json
```
CLI (internal/support fallback):
```bash
aman run --config ~/.config/aman/config.json
aman doctor --config ~/.config/aman/config.json --json
aman self-check --config ~/.config/aman/config.json --json
aman version
aman init --config ~/.config/aman/config.json --force
``` ```

View file

@ -0,0 +1,50 @@
# Aman Target Persona and Distribution Strategy
## Primary Persona: Desktop Professional
This is the canonical Aman user.
- Uses Linux desktop daily (X11 today), mostly Ubuntu/Debian.
- Wants fast dictation and rewriting without learning Python tooling.
- Prefers GUI setup and tray usage over CLI.
- Expects normal install/uninstall/update behavior from system packages.
Design implications:
- End-user install path must not require `uv`.
- Runtime defaults should work with minimal input.
- Documentation should prioritize package install first.
## Secondary Persona: Power User
- Comfortable with CLI, package internals, and model customization.
- Uses advanced config, external APIs, or custom models.
- Can run diagnostics and debug logs when needed.
Design implications:
- Keep Make and Python workflows available.
- Keep explicit expert-mode knobs in settings and config.
- Keep docs for development separate from standard install docs.
## Supported Distribution Path (Current)
Tiered distribution model:
1. Canonical: Debian package (`.deb`) for Ubuntu/Debian users.
2. Secondary: Arch package inputs (`PKGBUILD` + source tarball).
3. Developer: wheel/sdist from `python -m build`.
## Out of Scope for Initial Packaging
- Wayland production support.
- Flatpak/snap-first distribution.
- Cross-platform desktop installers outside Linux.
## Release and Support Policy
- App versioning follows SemVer (`0.y.z` until API/UX stabilizes).
- Config schema versioning is independent (`config_version` in config).
- Packaging docs must always separate:
- End-user install path (package-first)
- Developer setup path (uv/pip/build workflows)

View file

@ -2,11 +2,16 @@
1. Update `CHANGELOG.md` with final release notes. 1. Update `CHANGELOG.md` with final release notes.
2. Bump `project.version` in `pyproject.toml`. 2. Bump `project.version` in `pyproject.toml`.
3. Run: 3. Run quality and build gates:
- `python3 -m py_compile src/*.py tests/*.py` - `make release-check`
- `python3 -m unittest discover -s tests -p 'test_*.py'` 4. Build packaging artifacts:
4. Build artifacts: - `make package`
- `python3 -m build` 5. Verify artifacts:
5. Tag release: - `dist/*.whl`
- `dist/*.tar.gz`
- `dist/*.deb`
- `dist/arch/PKGBUILD`
6. Tag release:
- `git tag vX.Y.Z` - `git tag vX.Y.Z`
- `git push origin vX.Y.Z` - `git push origin vX.Y.Z`
7. Publish release and upload package artifacts from `dist/`.

View file

@ -0,0 +1,33 @@
# Maintainer: Aman Maintainers <maintainers@example.com>
pkgname=aman
pkgver=__VERSION__
pkgrel=1
pkgdesc="Local amanuensis daemon for X11 desktops"
arch=('x86_64')
url="https://github.com/example/aman"
license=('MIT')
depends=('python' 'python-pip' 'python-setuptools' 'portaudio' 'gtk3' 'libayatana-appindicator' 'python-gobject' 'python-xlib')
makedepends=('python-build' 'python-installer' 'python-wheel')
source=("__TARBALL_NAME__")
sha256sums=('__TARBALL_SHA256__')
prepare() {
cd "${srcdir}/aman-${pkgver}"
python -m build --wheel
}
package() {
cd "${srcdir}/aman-${pkgver}"
install -dm755 "${pkgdir}/opt/aman"
python -m venv --system-site-packages "${pkgdir}/opt/aman/venv"
"${pkgdir}/opt/aman/venv/bin/python" -m pip install --upgrade pip
"${pkgdir}/opt/aman/venv/bin/python" -m pip install "dist/aman-${pkgver}-"*.whl
install -Dm755 /dev/stdin "${pkgdir}/usr/bin/aman" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
exec /opt/aman/venv/bin/aman "$@"
EOF
install -Dm644 "systemd/aman.service" "${pkgdir}/usr/lib/systemd/user/aman.service"
}

11
packaging/deb/control.in Normal file
View file

@ -0,0 +1,11 @@
Package: __PACKAGE_NAME__
Version: __VERSION__
Section: utils
Priority: optional
Architecture: __ARCH__
Maintainer: Aman Maintainers <maintainers@example.com>
Depends: python3, python3-venv, python3-gi, python3-xlib, libportaudio2, gir1.2-gtk-3.0, libayatana-appindicator3-1
Description: Aman local amanuensis daemon for X11 desktops
Aman records microphone input, transcribes speech, optionally rewrites output,
and injects text into the focused desktop app. Includes tray controls and a
first-run graphical settings UI.

10
packaging/deb/postinst Executable file
View file

@ -0,0 +1,10 @@
#!/bin/sh
set -eu
cat <<'EOF'
Aman installed.
To enable auto-start for your user session:
systemctl --user daemon-reload
systemctl --user enable --now aman
EOF

44
scripts/package_arch.sh Executable file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
source "${SCRIPT_DIR}/package_common.sh"
require_command git
require_command sha256sum
VERSION="$(project_version)"
PACKAGE_NAME="$(project_name)"
ARCH_DIST_DIR="${DIST_DIR}/arch"
TARBALL_NAME="${PACKAGE_NAME}-${VERSION}.tar.gz"
TARBALL_PATH="${ARCH_DIST_DIR}/${TARBALL_NAME}"
PKGBUILD_PATH="${ARCH_DIST_DIR}/PKGBUILD"
mkdir -p "${ARCH_DIST_DIR}"
git -C "${ROOT_DIR}" archive \
--format=tar.gz \
--prefix="${PACKAGE_NAME}-${VERSION}/" \
HEAD \
> "${TARBALL_PATH}"
TARBALL_SHA256="$(sha256sum "${TARBALL_PATH}" | awk '{print $1}')"
render_template \
"${ROOT_DIR}/packaging/arch/PKGBUILD.in" \
"${PKGBUILD_PATH}" \
"VERSION=${VERSION}" \
"TARBALL_NAME=${TARBALL_NAME}" \
"TARBALL_SHA256=${TARBALL_SHA256}"
echo "generated ${PKGBUILD_PATH}"
echo "generated ${TARBALL_PATH}"
if [[ "${BUILD_ARCH_PACKAGE:-0}" == "1" ]]; then
require_command makepkg
(
cd "${ARCH_DIST_DIR}"
makepkg -f
)
fi

84
scripts/package_common.sh Executable file
View file

@ -0,0 +1,84 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
DIST_DIR="${ROOT_DIR}/dist"
BUILD_DIR="${ROOT_DIR}/build"
APP_NAME="aman"
mkdir -p "${DIST_DIR}" "${BUILD_DIR}"
require_command() {
local cmd="$1"
if command -v "${cmd}" >/dev/null 2>&1; then
return
fi
echo "missing required command: ${cmd}" >&2
exit 1
}
project_version() {
require_command python3
python3 - <<'PY'
from pathlib import Path
import re
text = Path("pyproject.toml").read_text(encoding="utf-8")
match = re.search(r'(?m)^version\s*=\s*"([^"]+)"\s*$', text)
if not match:
raise SystemExit("project version not found in pyproject.toml")
print(match.group(1))
PY
}
project_name() {
require_command python3
python3 - <<'PY'
from pathlib import Path
import re
text = Path("pyproject.toml").read_text(encoding="utf-8")
match = re.search(r'(?m)^name\s*=\s*"([^"]+)"\s*$', text)
if not match:
raise SystemExit("project name not found in pyproject.toml")
print(match.group(1))
PY
}
build_wheel() {
require_command python3
python3 -m build --wheel --no-isolation
}
latest_wheel_path() {
require_command python3
python3 - <<'PY'
from pathlib import Path
import re
text = Path("pyproject.toml").read_text(encoding="utf-8")
name_match = re.search(r'(?m)^name\s*=\s*"([^"]+)"\s*$', text)
version_match = re.search(r'(?m)^version\s*=\s*"([^"]+)"\s*$', text)
if not name_match or not version_match:
raise SystemExit("project metadata not found in pyproject.toml")
name = name_match.group(1).replace("-", "_")
version = version_match.group(1)
candidates = sorted(Path("dist").glob(f"{name}-{version}-*.whl"))
if not candidates:
raise SystemExit("no wheel artifact found in dist/")
print(candidates[-1])
PY
}
render_template() {
local template_path="$1"
local output_path="$2"
shift 2
cp "${template_path}" "${output_path}"
for mapping in "$@"; do
local key="${mapping%%=*}"
local value="${mapping#*=}"
sed -i "s|__${key}__|${value}|g" "${output_path}"
done
}

64
scripts/package_deb.sh Executable file
View file

@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
source "${SCRIPT_DIR}/package_common.sh"
require_command dpkg-deb
require_command python3
VERSION="$(project_version)"
PACKAGE_NAME="$(project_name)"
ARCH="${DEB_ARCH:-}"
if [[ -z "${ARCH}" ]]; then
if command -v dpkg >/dev/null 2>&1; then
ARCH="$(dpkg --print-architecture)"
else
ARCH="amd64"
fi
fi
build_wheel
WHEEL_PATH="$(latest_wheel_path)"
STAGE_DIR="${BUILD_DIR}/deb/${PACKAGE_NAME}_${VERSION}_${ARCH}"
PACKAGE_BASENAME="${PACKAGE_NAME}_${VERSION}_${ARCH}"
DEB_PATH="${DIST_DIR}/${PACKAGE_BASENAME}.deb"
VENV_DIR="${STAGE_DIR}/opt/${PACKAGE_NAME}/venv"
PIP_ARGS=()
if [[ -n "${AMAN_WHEELHOUSE_DIR:-}" ]]; then
PIP_ARGS+=(--no-index --find-links "${AMAN_WHEELHOUSE_DIR}")
fi
rm -rf "${STAGE_DIR}"
mkdir -p "${STAGE_DIR}/DEBIAN"
mkdir -p "${STAGE_DIR}/usr/bin"
mkdir -p "${STAGE_DIR}/usr/lib/systemd/user"
mkdir -p "${STAGE_DIR}/opt/${PACKAGE_NAME}"
render_template \
"${ROOT_DIR}/packaging/deb/control.in" \
"${STAGE_DIR}/DEBIAN/control" \
"PACKAGE_NAME=${PACKAGE_NAME}" \
"VERSION=${VERSION}" \
"ARCH=${ARCH}"
cp "${ROOT_DIR}/packaging/deb/postinst" "${STAGE_DIR}/DEBIAN/postinst"
chmod 0755 "${STAGE_DIR}/DEBIAN/postinst"
python3 -m venv --system-site-packages "${VENV_DIR}"
"${VENV_DIR}/bin/python" -m pip install "${PIP_ARGS[@]}" "${WHEEL_PATH}"
cat >"${STAGE_DIR}/usr/bin/${PACKAGE_NAME}" <<EOF
#!/usr/bin/env bash
set -euo pipefail
exec /opt/${PACKAGE_NAME}/venv/bin/${PACKAGE_NAME} "\$@"
EOF
chmod 0755 "${STAGE_DIR}/usr/bin/${PACKAGE_NAME}"
cp "${ROOT_DIR}/systemd/aman.service" "${STAGE_DIR}/usr/lib/systemd/user/aman.service"
rm -f "${DEB_PATH}"
dpkg-deb --build --root-owner-group "${STAGE_DIR}" "${DEB_PATH}"
echo "built ${DEB_PATH}"