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 uv build
uv sync --extra x11
- name: Compile
run: python -m py_compile src/*.py tests/*.py
- name: Unit tests
run: python -m unittest discover -s tests -p 'test_*.py'
- name: Build artifacts
run: python -m build
- name: Release quality checks
run: make release-check
- name: Build Debian package
run: make package-deb
- name: Build Arch package inputs
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
outputs/
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.
## [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
### Added

View file

@ -1,10 +1,13 @@
PYTHON ?= python3
CONFIG := $(HOME)/.config/aman/config.json
.PHONY: run doctor self-check install sync test check
DIST_DIR := $(CURDIR)/dist
BUILD_DIR := $(CURDIR)/build
RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
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))),)
.PHONY: $(RUN_ARGS)
$(RUN_ARGS):
@ -24,14 +27,43 @@ sync:
uv sync
test:
python3 -m unittest discover -s tests -p 'test_*.py'
$(PYTHON) -m unittest discover -s tests -p 'test_*.py'
check:
python3 -m py_compile src/*.py
$(PYTHON) -m py_compile src/*.py
$(MAKE) test
install:
uv pip install --user .
build:
$(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
systemctl --user daemon-reload
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.
## 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
- `sounddevice` (PortAudio)
- `faster-whisper`
- `llama-cpp-python`
- 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`.
- PortAudio runtime (`libportaudio2` or distro equivalent)
- GTK3 and AppIndicator runtime (`gtk3`, `libayatana-appindicator3`)
- Python GTK and X11 bindings (`python3-gi`/`python-gobject`, `python-xlib`)
<details>
<summary>Ubuntu/Debian</summary>
```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>
@ -28,7 +66,7 @@ sudo apt install -y portaudio19-dev libportaudio2 python3-gi gir1.2-gtk-3.0 liba
<summary>Arch Linux</summary>
```bash
sudo pacman -S --needed portaudio gtk3 libayatana-appindicator
sudo pacman -S --needed portaudio gtk3 libayatana-appindicator python-gobject python-xlib
```
</details>
@ -37,7 +75,7 @@ sudo pacman -S --needed portaudio gtk3 libayatana-appindicator
<summary>Fedora</summary>
```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>
@ -46,25 +84,15 @@ sudo dnf install -y portaudio portaudio-devel gtk3 libayatana-appindicator-gtk3
<summary>openSUSE</summary>
```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>
## Python Daemon
Install Python deps:
X11 (supported):
```bash
uv sync --extra x11
```
## Quickstart
```bash
uv run aman run
aman run
```
On first launch, Aman opens a graphical settings window automatically.
@ -191,15 +219,13 @@ STT hinting:
## systemd user service
```bash
uv pip install --user .
cp systemd/aman.service ~/.config/systemd/user/aman.service
systemctl --user daemon-reload
systemctl --user enable --now aman
make install-service
```
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`.
## Usage
@ -228,21 +254,50 @@ AI processing:
- Default local llama.cpp model.
- 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:
```bash
make run
make run config.example.json
make doctor
make self-check
make check
```
CLI (internal/support fallback, mostly for automation/tests):
Developer setup (optional, `uv` workflow):
```bash
uv sync --extra x11
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
uv run aman init --config ~/.config/aman/config.json --force
```
Developer setup (optional, `pip` workflow):
```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.
2. Bump `project.version` in `pyproject.toml`.
3. Run:
- `python3 -m py_compile src/*.py tests/*.py`
- `python3 -m unittest discover -s tests -p 'test_*.py'`
4. Build artifacts:
- `python3 -m build`
5. Tag release:
3. Run quality and build gates:
- `make release-check`
4. Build packaging artifacts:
- `make package`
5. Verify artifacts:
- `dist/*.whl`
- `dist/*.tar.gz`
- `dist/*.deb`
- `dist/arch/PKGBUILD`
6. Tag release:
- `git tag 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}"