aim/.plans/002-per-distro-installation/2026-03-19-per-distro-installation-design.md

276 lines
No EOL
9 KiB
Markdown

# Per-Distro Installation Design
## Goal
Implement actual AppImage installation for `aim` across a broad Linux distro set, while preserving a single reusable install engine in `aim-core`, keeping `aim-cli` thin, and respecting distro policy constraints instead of fighting them.
## Agreed Product Shape
### Supported distro families
- Debian
- Fedora
- Arch
- openSUSE
- Alpine
- Nix
- Immutable
- Generic Linux fallback
### Detection model
- Detect distro family from `/etc/os-release`
- Detect policy constraints such as immutable or Nix-style environments
- Probe runtime capabilities such as directory writability and optional helper availability
- Let distro family define intended policy, then let capabilities refine the safe final action
### Support posture
- Broad Linux coverage is the target
- Deep integration is preferred for mainstream distro families
- Immutable systems should get best-effort user installs with explicit warnings
- System installs should prefer native shared integration locations when policy allows, with fallback to `aim`-managed locations where appropriate
## Recommended Approach
Use a hybrid family-plus-capability model.
This was chosen over a family-only matrix because distro identity alone is not enough to determine what the current host will allow. It was also chosen over capability-only probing because distro family still matters for native path expectations and default policy.
The model is:
- distro family selects the intended install policy
- host capabilities determine whether that policy can be executed fully, degraded, or not at all
- one generic install engine executes the workflow using the resolved policy
## Install Policy Model
### Debian, Fedora, Arch, openSUSE
- `--user` installs use `aim`-managed user paths
- `--system` prefers native shared desktop integration locations and system icon locations
- AppImage payloads remain in an `aim`-managed system payload directory unless a later distro-specific need proves otherwise
- Desktop caches are refreshed only when helper tools are present
### Alpine
- Treat Alpine as a lightweight Linux with shallower integration assumptions
- Prefer `aim`-managed paths for both user and system installs
- Run optional helpers only when present
### Nix
- Default to conservative behavior
- Allow `--user` installs into `aim`-managed user paths, with a warning that the install is outside declarative package management
- Deny `--system` installs until a Nix-native strategy exists
- Never attempt to write into the Nix store or emulate a declarative install
### Immutable
- Treat `--user` as the primary supported path
- If `--system` is requested, first test whether the host genuinely permits the required writes
- If policy blocks system writes, either downgrade to user scope with an explicit warning or fail clearly when downgrade is not allowed by policy
### Generic fallback
- Use `aim`-managed paths only
- Apply standards-based XDG desktop integration only
- Skip distro-specific helper assumptions
### Cross-cutting rule
Payload storage and integration targets are separate concerns.
For example, a system install may place the AppImage payload under `/opt/aim/appimages`, while writing the `.desktop` file to `/usr/share/applications` and icons to `/usr/share/icons`. This keeps artifact replacement and rollback simple while still integrating natively.
## Execution Pipeline
Use one install engine with policy injected into it rather than separate installers per distro.
### 1. Resolve policy
- Detect distro family
- Detect capabilities and policy markers
- Resolve an `InstallPolicy` describing:
- allowed scope
- payload root
- desktop entry root
- icon root
- integration mode
- helper refresh actions
- warnings and fallback notes
### 2. Stage artifact
- Download into a temporary staging path
- Validate that the payload is an AppImage
- Derive stable identity and final filenames before touching permanent locations
- Mark the staged artifact executable before final commit
### 3. Commit payload atomically
- Move the staged AppImage into the final managed payload location
- Use replacement semantics suitable for safe updates
- Keep the old installed payload until the new one is validated and committed
### 4. Commit integration
- Generate a normalized `.desktop` file
- Extract and install icons when available
- Write integration artifacts into policy-selected locations
- Keep these artifacts generated by `aim`, not manually managed
### 5. Refresh caches best-effort
- Run optional helpers only when present and relevant
- Cache refresh failures should produce warnings, not rollback a valid install
### 6. Persist registry last
- Only persist the final `AppRecord` after payload and required integration steps succeed
- The registry should represent completed installs, not partial attempts
## Reliability Model
### Preflight failures
Stop early for:
- unsupported scope for the resolved host policy
- unwritable required target directories with no allowed fallback
- payloads that are not valid AppImages
- missing required normalized metadata for integration
### Transaction boundary
Treat install as three phases:
- preflight and staging
- payload commit
- integration commit
Nothing is persisted in the registry until the required phases succeed.
### Rollback rules
- If staging fails, clean temporary files only
- If payload commit succeeds but required integration fails, remove the new payload unless degraded payload-only success was explicitly allowed
- If integration partially succeeds, remove generated `.desktop` and icon artifacts before returning failure
- Cache refresh failures do not trigger rollback
### Degraded success
Success with warnings is acceptable only when:
- optional cache refresh helpers are missing or fail
- desktop integration was optional and the host lacks the helper stack to finish niceties
- immutable or policy-heavy systems forced a user-scope fallback that policy explicitly allows
### Update safety
- Updates stage side-by-side and replace atomically
- The previous working AppImage stays active until the new one is validated and committed
- Failed updates should leave the previous installed version intact
## Verification Strategy
### Unit tests
- distro detection
- immutable and Nix marker detection
- helper capability probing
- writable path probing
- install policy resolution per family and scope
- degraded and denied policy decisions
### Integration tests
- successful user install
- successful system install using managed payload plus native integration paths
- denied Nix system install
- immutable system fallback to user install with warning
- payload commit failure cleanup
- integration failure rollback
- cache refresh warning-only behavior
Fixture-driven filesystem tests should cover most of this. First implementation should not depend on real distro images in CI.
## Architecture Slice
### Core types
#### `DistroFamily`
- `Debian`
- `Fedora`
- `Arch`
- `OpenSuse`
- `Alpine`
- `Nix`
- `Immutable`
- `Generic`
#### `HostCapabilities`
- parsed `os-release` identity
- immutable and Nix policy markers
- writable status for candidate directories
- desktop-session presence
- helper availability such as `update-desktop-database` and `gtk-update-icon-cache`
#### `InstallPolicy`
- resolved scope
- payload path root
- desktop entry root
- icon path root
- integration mode: `full`, `degraded`, `payload-only`, or `denied`
- fallback notes and warnings for UI surfaces
#### `InstallPlan`
- selected artifact URL and expected identity
- staging paths
- final payload path
- desktop file path
- icon path
- helper refresh actions to attempt after commit
#### `InstallOutcome`
- installed scope
- resolved mode
- written paths
- warnings
- rollback status
### Recommended module layout
Within `crates/aim-core/src/`:
- `platform/distro.rs`
- `platform/capabilities.rs`
- `integration/policy.rs`
- `integration/install.rs`
- `integration/desktop.rs`
- `integration/refresh.rs`
Responsibilities:
- `platform/distro.rs` detects distro families and policy markers
- `platform/capabilities.rs` probes writability and helper availability
- `integration/policy.rs` converts requested scope plus host facts into `InstallPolicy`
- `integration/install.rs` owns the transactional install pipeline
- `integration/desktop.rs` owns desktop file generation and icon handling
- `integration/refresh.rs` owns best-effort helper execution
### CLI behavior
- `aim <QUERY>` should become a real install path rather than registry-only tracking
- The CLI should print the resolved install mode before commit
- If the plan degrades, the CLI should explain that before confirmation
- Bare `aim` can later reuse the same policy summary when showing pending updates
## Summary
The agreed design is a single install engine in `aim-core` driven by a resolved per-host `InstallPolicy`. Distro families provide intended policy, runtime capabilities decide what is safe, and transactional install semantics keep installs recoverable. This preserves the thin-CLI architecture while allowing broad Linux coverage without scattering distro-specific conditionals across the entire codebase.