refactor: rename aim to upm and extract appimage module
This commit is contained in:
parent
af13e98eb3
commit
863c57e473
117 changed files with 2622 additions and 887 deletions
|
|
@ -2,20 +2,22 @@
|
|||
|
||||
## Workspace Shape
|
||||
|
||||
`aim` is a Rust workspace with two main crates:
|
||||
`upm` is a Rust workspace with three main crates:
|
||||
|
||||
- `crates/aim-core`: source normalization, provider adapters, install/update planning, payload installation, registry persistence, and desktop integration.
|
||||
- `crates/aim-cli`: argument parsing, config loading, terminal UX, prompting, progress reporting, and summary rendering.
|
||||
- `crates/upm-core`: source normalization, add/update orchestration, registry persistence, install policies, desktop integration, and the provider-composition seam.
|
||||
- `crates/upm`: argument parsing, config loading, terminal UX, prompting, progress reporting, summary rendering, and provider assembly.
|
||||
- `crates/upm-appimage`: AppImageHub transport, search-provider behavior, and exact add-resolution for AppImage-backed installs.
|
||||
|
||||
The split keeps product logic in `aim-core` so additional frontends can reuse the same install and update pipeline.
|
||||
The split keeps frontend-agnostic logic in `upm-core`, while concrete package-source behavior is composed at the CLI boundary. That keeps the headless layer reusable for future frontends without making provider behavior a permanent core dependency.
|
||||
|
||||
## Core Flow
|
||||
|
||||
The main execution path is:
|
||||
|
||||
1. Parse CLI input and load runtime config in `aim-cli`.
|
||||
2. Resolve the query into a normalized source in `aim-core`.
|
||||
3. Build an add or update plan through provider adapters and artifact selection.
|
||||
1. Parse CLI input and load runtime config in `upm`.
|
||||
2. Assemble a `ProviderRegistry` in `crates/upm/src/providers.rs`.
|
||||
3. Resolve the query into a normalized source in `upm-core`.
|
||||
4. Build an add or update plan through core orchestration plus any registered external providers.
|
||||
4. Download the selected AppImage into a staged path.
|
||||
5. Verify integrity metadata when available.
|
||||
6. Commit the payload into the managed install location.
|
||||
|
|
@ -33,7 +35,16 @@ Supported source classes currently include:
|
|||
- direct URLs
|
||||
- local file imports
|
||||
|
||||
Provider-specific resolution lives in `crates/aim-core/src/adapters` and `crates/aim-core/src/source`.
|
||||
Core source normalization and orchestration live in `crates/upm-core`. AppImageHub-specific transport and provider behavior live in `crates/upm-appimage` and are injected through `ProviderRegistry` rather than hardcoded into core entrypoints.
|
||||
|
||||
## Runtime Interface
|
||||
|
||||
The rename to `upm` is a hard cutover:
|
||||
|
||||
- runtime overrides use `UPM_*`
|
||||
- legacy `AIM_*` runtime overrides are not read
|
||||
- default config, registry, payload, and desktop-entry paths use `upm` names
|
||||
- helper audit logging uses `UPM_DEBUG_EXTERNAL_HELPERS=1`
|
||||
|
||||
## Security Hardening State
|
||||
|
||||
|
|
@ -51,9 +62,9 @@ The remaining deferred AppImageHub host-trust concern is tracked in `security-is
|
|||
|
||||
## Persistence And Integration
|
||||
|
||||
- Registry writes are atomic and live under the registry store implementation in `aim-core`.
|
||||
- Registry writes are atomic and live under the registry store implementation in `upm-core`.
|
||||
- Managed payload, desktop entry, and icon paths are resolved from install policy and scope.
|
||||
- Desktop integration refresh uses external helpers when available and now supports env-gated audit logging through `AIM_DEBUG_EXTERNAL_HELPERS=1`.
|
||||
- Desktop integration refresh uses external helpers when available and now supports env-gated audit logging through `UPM_DEBUG_EXTERNAL_HELPERS=1`.
|
||||
|
||||
## Planning And Audit Artifacts
|
||||
|
||||
|
|
|
|||
208
.architecture/roadmap.md
Normal file
208
.architecture/roadmap.md
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
# UPM Roadmap
|
||||
|
||||
## Direction
|
||||
|
||||
The project is evolving from a focused AppImage manager into `upm`, a modular universal package manager. The target system manages multiple package sources through a shared headless core, keeps the CLI thin, and leaves room for future GUI frontends.
|
||||
|
||||
The initial rename-and-extraction slice has now landed as a hard cutover: the runtime surface is `upm`, the shared core is `upm-core`, and AppImage support is composed through `upm-appimage` instead of being embedded directly into the core.
|
||||
|
||||
The near-term goal is a Linux-first platform with honest cross-platform architecture. Phase 2 will implement Linux package sources while establishing the abstractions needed for later macOS support and possible Windows support.
|
||||
|
||||
## Confirmed Product Decisions
|
||||
|
||||
- `aim-cli` becomes `upm`.
|
||||
- `aim-core` stops being the all-in-one backend and is split.
|
||||
- AppImage support becomes an installable module named `upm-appimage`.
|
||||
- Shared orchestration, config, state, resolution, ranking, and frontend-facing APIs move into `upm-core`.
|
||||
- The `upm` crate becomes a thin CLI client over `upm-core`.
|
||||
- The rename is a hard cutover. Legacy `AIM_*` runtime interfaces are removed rather than preserved.
|
||||
- The declarative package file starts in a hybrid mode and is intended to become the source of truth over time.
|
||||
- Every `upm` invocation should detect drift between declared state and observed system state, then auto-sync metadata as needed.
|
||||
- Phase 2 is Linux implementation first, with macOS-oriented provider abstractions and packaging seams designed now.
|
||||
- Feature delivery should happen as vertical slices rather than a single large refactor.
|
||||
- The `upm` branch is the effective trunk for this evolution work and should be treated as the integration base for future UPM feature branches and worktrees.
|
||||
|
||||
## Architectural Destination
|
||||
|
||||
### Workspace Shape
|
||||
|
||||
The intended workspace shape after the initial refactor is:
|
||||
|
||||
- `upm`: thin CLI frontend, ratatui config UI, command routing, presentation.
|
||||
- `upm-core`: headless application layer, provider registry, resolution pipeline, state model, declarative sync engine, ranking, policies, and frontend-agnostic APIs.
|
||||
- `upm-appimage`: AppImage provider module extracted from the current `aim-core` implementation.
|
||||
- future provider modules: `upm-pacman`, `upm-aur`, `upm-flatpak`, `upm-cargo`, `upm-npm`, and later macOS or Windows-specific modules.
|
||||
|
||||
### Module Model
|
||||
|
||||
UPM should stay modular in both code and packaging:
|
||||
|
||||
- modules can be enabled or disabled by config
|
||||
- providers can be ranked by user priority
|
||||
- distro packaging can offer grouped installs such as `upm-full`
|
||||
- lighter installs can ship only the core and selected modules
|
||||
|
||||
This means provider capabilities, discovery, search, install, remove, inspect, and sync behavior need stable interfaces in `upm-core` rather than provider-specific branching in the CLI.
|
||||
|
||||
### State Model
|
||||
|
||||
The long-term model is declarative and config-first, but Phase 2 begins with a hybrid approach:
|
||||
|
||||
- `upm`-managed actions update the declarative config directly
|
||||
- `upm update` and normal command entrypoints can inspect live system state
|
||||
- drift detection reconciles unmanaged or changed packages into the config representation
|
||||
- over time the config becomes authoritative and reconciliation becomes stricter
|
||||
|
||||
## Phase 2 Milestones
|
||||
|
||||
### Milestone 0: Rename And Split Foundation
|
||||
|
||||
Deliver the naming and ownership transition without changing product scope yet.
|
||||
|
||||
Goals:
|
||||
|
||||
- rename workspace crates and package outputs from `aim` to `upm`
|
||||
- create `upm-core` by extracting reusable infrastructure from `aim-core`
|
||||
- reduce the CLI crate to a frontend over headless APIs
|
||||
- isolate current AppImage-specific logic into `upm-appimage`
|
||||
- compose provider behavior in the CLI through `ProviderRegistry` rather than hardcoded AppImage paths in `upm-core`
|
||||
- preserve current AppImage functionality and tests during the move
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `upm` binary replaces `aim`
|
||||
- workspace builds under new crate names
|
||||
- AppImage flows still work end-to-end through the new layering
|
||||
|
||||
### Milestone 1: AppImage On The New Core
|
||||
|
||||
Make the current AppImage implementation the first real module on the modular architecture.
|
||||
|
||||
Goals:
|
||||
|
||||
- validate the provider module contract using AppImage as the reference implementation
|
||||
- move search, add, install, update, show, and remove behaviors behind core provider APIs
|
||||
- prove the CLI can treat AppImage as just one enabled source
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- AppImage support is no longer special-cased as the whole product
|
||||
- provider registration and capability discovery exist in `upm-core`
|
||||
|
||||
### Milestone 2: Linux Native Sources
|
||||
|
||||
Add the first non-AppImage providers as Linux-focused vertical slices.
|
||||
|
||||
Initial supported sources:
|
||||
|
||||
- pacman
|
||||
- AUR
|
||||
- Flatpak
|
||||
- cargo global installs
|
||||
- npm global installs
|
||||
|
||||
Goals:
|
||||
|
||||
- implement provider discovery and capability coverage for each source
|
||||
- normalize package identity, version, installed state, and update candidates across providers
|
||||
- support search, inspect, install, remove, and update planning where each provider can do so safely
|
||||
- capture provider limitations explicitly rather than faking uniformity
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- multi-source search works across enabled providers
|
||||
- installs and removals work through a consistent command model
|
||||
- provider-specific metadata is normalized enough for ranking and sync
|
||||
|
||||
### Milestone 3: Provider Priority And Config UX
|
||||
|
||||
Expose modularity directly in the terminal interface.
|
||||
|
||||
Goals:
|
||||
|
||||
- build a ratatui configuration menu
|
||||
- allow enablement and disablement per provider
|
||||
- allow explicit search and install priority ordering
|
||||
- allow configuration of module weight, source preference, and future policy toggles
|
||||
- make search ranking obey configured priority instead of hard-coded source bias
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- users can manage provider selection and ranking from the TUI
|
||||
- search results are explainable in terms of configured preference order
|
||||
|
||||
### Milestone 4: Declarative Package State And Drift Sync
|
||||
|
||||
Introduce the nix-like experience incrementally.
|
||||
|
||||
Goals:
|
||||
|
||||
- define the declarative package file format in `config.toml` or a closely related managed file
|
||||
- track installed packages by provider in a normalized state model
|
||||
- implement `upm update` as both package refresh and state reconciliation
|
||||
- scan currently installed packages from supported providers and build or refresh the declared package set
|
||||
- auto-sync detected drift during any `upm` command invocation
|
||||
|
||||
Phase 2 intent:
|
||||
|
||||
- begin in hybrid mode
|
||||
- move steadily toward config-first behavior
|
||||
- avoid destructive reconciliation until provider semantics are trustworthy
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- users can bootstrap a declarative package definition from the current machine state
|
||||
- repeated `upm` runs keep declared and observed state aligned
|
||||
- state drift is surfaced clearly and reconciled predictably
|
||||
|
||||
### Milestone 5: Packaging, Distribution, And Platform Seams
|
||||
|
||||
Make the modular architecture real at packaging time, not just in code.
|
||||
|
||||
Goals:
|
||||
|
||||
- define packaging layout for standalone core, selected modules, and full installs
|
||||
- support distro-level grouped packages such as `upm-full`
|
||||
- ensure unsupported modules degrade cleanly on the wrong OS or distro
|
||||
- add macOS provider and packaging seams to `upm-core` even if Linux remains the only implemented provider set in Phase 2
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- module packaging strategy is documented and testable
|
||||
- cross-platform abstractions exist without blocking Linux delivery
|
||||
|
||||
## Phase 2 Non-Goals
|
||||
|
||||
The following are explicitly not required to complete Phase 2:
|
||||
|
||||
- full macOS provider implementation
|
||||
- Windows provider implementation
|
||||
- GUI frontend delivery
|
||||
- forcing strict config-authoritative reconciliation before provider behavior is stable
|
||||
- shipping every conceivable Linux package manager in the first expansion
|
||||
|
||||
## Success Criteria
|
||||
|
||||
Phase 2 is successful when the project can credibly be described as a modular package manager rather than an AppImage manager with extra adapters.
|
||||
|
||||
That means:
|
||||
|
||||
- the product name, workspace shape, and binary identity are all `upm`
|
||||
- AppImage support is only one module among several
|
||||
- Linux users can manage packages from the first targeted provider set
|
||||
- ranking and enablement are user-controlled through config and TUI
|
||||
- declarative state exists, is importable from the live system, and stays synchronized through normal use
|
||||
- the architecture is ready for GUI and later platform expansion without another major rewrite
|
||||
|
||||
## Immediate Planning Order
|
||||
|
||||
Implementation plans should follow this order:
|
||||
|
||||
1. rename and crate extraction
|
||||
2. provider API definition and AppImage migration onto `upm-core`
|
||||
3. Linux provider onboarding in a stable order, likely `pacman` then `Flatpak`, then `AUR`, then `cargo`, then `npm`
|
||||
4. ratatui configuration and ranking UX
|
||||
5. declarative state model, drift detection, and `update` sync behavior
|
||||
6. packaging layout and `upm-full` distribution strategy
|
||||
|
||||
This order keeps the refactor defensible, gives each slice a usable product outcome, and avoids locking future provider work into AppImage-era assumptions.
|
||||
|
|
@ -0,0 +1,410 @@
|
|||
# UPM Rename And Core Extraction Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Rename the product from `aim` to `upm`, remove legacy `aim` runtime interfaces, extract the shared headless backend into `upm-core`, and move AppImage-specific transport and provider logic into a separate `upm-appimage` module without regressing current AppImage workflows.
|
||||
|
||||
**Architecture:** Execute this in vertical slices. First rename the workspace, binary, paths, environment interfaces, and tests to `upm` without carrying legacy `aim` compatibility. Next introduce a narrow provider-composition seam in `upm-core` so AppImage-specific add and search logic can move into `upm-appimage` without creating a dependency cycle. Finally rewire the `upm` CLI to assemble built-in providers, update docs, and run full verification.
|
||||
|
||||
**Tech Stack:** Rust workspace, Cargo manifests, clap CLI, ratatui frontend crate, core domain/app modules, fixture-backed provider tests, workspace-wide `cargo test` and `cargo clippy`.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Rename the workspace, binary, and default runtime paths to `upm`
|
||||
|
||||
**Files:**
|
||||
- Modify: `Cargo.toml`
|
||||
- Rename: `crates/aim-cli` -> `crates/upm`
|
||||
- Rename: `crates/aim-core` -> `crates/upm-core`
|
||||
- Modify: `crates/upm/Cargo.toml`
|
||||
- Modify: `crates/upm/src/main.rs`
|
||||
- Modify: `crates/upm/src/lib.rs`
|
||||
- Modify: `crates/upm/src/cli/args.rs`
|
||||
- Modify: `crates/upm/src/config.rs`
|
||||
- Modify: `crates/upm/src/cli/config.rs`
|
||||
- Modify: `crates/upm-core/Cargo.toml`
|
||||
- Modify: `crates/upm-core/src/platform/mod.rs`
|
||||
- Modify: `crates/upm-core/src/integration/paths.rs`
|
||||
- Modify: `crates/upm-core/src/integration/policy.rs`
|
||||
- Test: `crates/upm/tests/cli_smoke.rs`
|
||||
- Test: `crates/upm/tests/cli_commands.rs`
|
||||
- Test: `crates/upm/tests/config_loading.rs`
|
||||
- Test: `crates/upm-core/tests/install_paths.rs`
|
||||
- Test: `crates/upm-core/tests/install_policy.rs`
|
||||
|
||||
**Step 1: Write the failing rename expectations**
|
||||
|
||||
Update the selected tests to assert:
|
||||
|
||||
- the binary name is `upm`
|
||||
- clap parses `upm` instead of `aim`
|
||||
- default config path is `~/.config/upm/config.toml`
|
||||
- default registry path is `~/.local/share/upm/registry.toml`
|
||||
- default managed payload roots are `.local/lib/upm/appimages` and `/opt/upm/appimages`
|
||||
- desktop entry filenames use `upm-<stable-id>.desktop`
|
||||
|
||||
**Step 2: Run the focused tests to verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test --package aim-cli --test cli_smoke
|
||||
cargo test --package aim-cli --test config_loading
|
||||
cargo test --package aim-core --test install_paths
|
||||
cargo test --package aim-core --test install_policy
|
||||
```
|
||||
|
||||
Expected: FAIL because the workspace still exposes `aim`, `aim-cli`, `aim-core`, and `aim` default paths.
|
||||
|
||||
**Step 3: Perform the crate and manifest rename**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git mv crates/aim-cli crates/upm
|
||||
git mv crates/aim-core crates/upm-core
|
||||
```
|
||||
|
||||
Then update:
|
||||
|
||||
- workspace members and default members in `Cargo.toml`
|
||||
- package names to `upm` and `upm-core`
|
||||
- binary name to `upm`
|
||||
- crate imports from `aim_core` to `upm_core`
|
||||
- crate imports from `aim_cli` to `upm`
|
||||
- clap command name from `aim` to `upm`
|
||||
- default config, registry, payload-root, and desktop-entry paths to `upm`
|
||||
|
||||
**Step 4: Run the focused tests to verify pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test --package upm --test cli_smoke
|
||||
cargo test --package upm --test cli_commands
|
||||
cargo test --package upm --test config_loading
|
||||
cargo test --package upm-core --test install_paths
|
||||
cargo test --package upm-core --test install_policy
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add Cargo.toml crates/upm crates/upm-core
|
||||
git commit -m "refactor: rename workspace to upm"
|
||||
```
|
||||
|
||||
### Task 2: Remove remaining `aim`-named runtime interfaces
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/upm/src/config.rs`
|
||||
- Modify: `crates/upm/src/cli/config.rs`
|
||||
- Modify: `crates/upm/src/lib.rs`
|
||||
- Modify: `crates/upm/src/ui/prompt.rs`
|
||||
- Modify: `crates/upm-core/src/platform/mod.rs`
|
||||
- Modify: `crates/upm-core/src/source/github.rs`
|
||||
- Modify: `crates/upm-core/src/source/appimagehub.rs`
|
||||
- Modify: `crates/upm-core/src/integration/refresh.rs`
|
||||
- Test: `crates/upm/tests/config_loading.rs`
|
||||
|
||||
**Step 1: Write the failing strict-rename expectations**
|
||||
|
||||
Update representative tests to cover:
|
||||
|
||||
- config lookup uses `UPM_CONFIG_PATH`
|
||||
- registry lookup uses `UPM_REGISTRY_PATH`
|
||||
- old `AIM_*` config and registry overrides are ignored
|
||||
- tracking preference uses `UPM_TRACKING_PREFERENCE`
|
||||
- old `AIM_TRACKING_PREFERENCE` is ignored
|
||||
- provider fixture execution uses the renamed `UPM_*` interfaces through CLI-facing tests
|
||||
- managed install and summary output use `upm` paths and desktop prefixes
|
||||
|
||||
**Step 2: Run the focused tests to verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test --package upm --test config_loading
|
||||
cargo test --package upm --test search_cli
|
||||
cargo test --package upm --test end_to_end_cli
|
||||
cargo test --package upm --test ui_summary
|
||||
```
|
||||
|
||||
Expected: FAIL because representative CLI and config flows still depend on old `aim` names.
|
||||
|
||||
**Step 3: Remove the remaining `aim` interfaces**
|
||||
|
||||
Update the codebase so renamed runtime interfaces are consistently `upm`:
|
||||
|
||||
- environment variable names use `UPM_*`
|
||||
- helper/debug prefixes print `[upm]`
|
||||
- GitHub user agent identifies as `upm/0.1`
|
||||
- old `aim` compatibility reads are removed instead of preserved
|
||||
|
||||
**Step 4: Run the focused tests to verify pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test --package upm --test config_loading
|
||||
cargo test --package upm --test search_cli
|
||||
cargo test --package upm --test end_to_end_cli
|
||||
cargo test --package upm --test ui_summary
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/upm/src/config.rs crates/upm/src/cli/config.rs crates/upm/src/lib.rs crates/upm/src/ui/prompt.rs crates/upm-core/src/platform/mod.rs crates/upm-core/src/source/github.rs crates/upm-core/src/source/appimagehub.rs crates/upm-core/src/integration/refresh.rs crates/upm/tests/config_loading.rs
|
||||
git commit -m "refactor: remove remaining aim runtime interfaces"
|
||||
```
|
||||
|
||||
### Task 3: Add a provider-composition seam in `upm-core`
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/upm-core/src/app/providers.rs`
|
||||
- Modify: `crates/upm-core/src/app/mod.rs`
|
||||
- Modify: `crates/upm-core/src/app/add.rs`
|
||||
- Modify: `crates/upm-core/src/app/search.rs`
|
||||
- Modify: `crates/upm-core/src/lib.rs`
|
||||
- Create: `crates/upm-core/tests/provider_registry.rs`
|
||||
|
||||
**Step 1: Write the failing provider-composition tests**
|
||||
|
||||
Create `crates/upm-core/tests/provider_registry.rs` with two focused tests:
|
||||
|
||||
- `build_search_results_with_registered_providers_uses_external_hits` using a stub external search provider
|
||||
- `build_add_plan_with_registered_providers_delegates_appimagehub_like_sources` using a stub external add provider that returns a fixed artifact and release
|
||||
|
||||
The tests should prove that `upm-core` orchestration can consume provider-supplied search and add behavior without hardcoding AppImage-specific modules.
|
||||
|
||||
**Step 2: Run the focused tests to verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test --package upm-core --test provider_registry
|
||||
```
|
||||
|
||||
Expected: FAIL because the orchestration layer still hardcodes AppImageHub in `app/add.rs` and `app/search.rs`.
|
||||
|
||||
**Step 3: Introduce the narrow composition API**
|
||||
|
||||
Create `crates/upm-core/src/app/providers.rs` with minimal types:
|
||||
|
||||
- `pub trait ExternalAddProvider`
|
||||
- `pub struct ExternalAddResolution`
|
||||
- `pub struct ProviderRegistry<'a>`
|
||||
|
||||
Requirements:
|
||||
|
||||
- `ProviderRegistry` carries `search_providers: Vec<&'a dyn SearchProvider>` and `external_add_providers: Vec<&'a dyn ExternalAddProvider>`
|
||||
- `build_search_results` can delegate to `build_search_results_with` using providers supplied by the caller
|
||||
- `build_add_plan_with_reporter_and_policy` gets a sibling entrypoint that accepts a `ProviderRegistry`
|
||||
- core built-ins remain in `upm-core`; only AppImage-specific exact-resolution and search logic should move behind the new registry seam
|
||||
|
||||
Keep the interface intentionally small. Do not attempt plugin loading or dynamic discovery yet.
|
||||
|
||||
**Step 4: Run the focused tests to verify pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test --package upm-core --test provider_registry
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/upm-core/src/app/providers.rs crates/upm-core/src/app/mod.rs crates/upm-core/src/app/add.rs crates/upm-core/src/app/search.rs crates/upm-core/src/lib.rs crates/upm-core/tests/provider_registry.rs
|
||||
git commit -m "refactor: add provider composition seam to upm-core"
|
||||
```
|
||||
|
||||
### Task 4: Extract AppImage-specific logic into `upm-appimage`
|
||||
|
||||
**Files:**
|
||||
- Modify: `Cargo.toml`
|
||||
- Create: `crates/upm-appimage/Cargo.toml`
|
||||
- Create: `crates/upm-appimage/src/lib.rs`
|
||||
- Create: `crates/upm-appimage/src/add.rs`
|
||||
- Create: `crates/upm-appimage/src/search.rs`
|
||||
- Create: `crates/upm-appimage/src/source/mod.rs`
|
||||
- Create: `crates/upm-appimage/src/source/appimagehub.rs`
|
||||
- Modify: `crates/upm-core/src/adapters/mod.rs`
|
||||
- Modify: `crates/upm-core/src/source/mod.rs`
|
||||
- Modify: `crates/upm-core/src/app/add.rs`
|
||||
- Modify: `crates/upm-core/src/app/search.rs`
|
||||
- Create: `crates/upm-appimage/tests/appimagehub_search.rs`
|
||||
- Modify: `crates/upm-core/tests/adapter_contract.rs`
|
||||
- Modify: `crates/upm-core/tests/adapter_smoke.rs`
|
||||
|
||||
**Step 1: Write the failing extracted-module test**
|
||||
|
||||
Create `crates/upm-appimage/tests/appimagehub_search.rs` by moving the current AppImageHub search expectations out of `upm-core` and updating imports to target the new crate.
|
||||
|
||||
Also update the affected `upm-core` tests so they no longer import `AppImageHubAdapter` from `upm-core` directly.
|
||||
|
||||
**Step 2: Run the focused tests to verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test --package upm-appimage --test appimagehub_search
|
||||
```
|
||||
|
||||
Expected: FAIL because the new crate does not exist yet.
|
||||
|
||||
**Step 3: Create the new crate and move AppImageHub implementation into it**
|
||||
|
||||
Move the AppImageHub-specific code into the new crate:
|
||||
|
||||
- transport and fixture logic from `upm-core/src/source/appimagehub.rs`
|
||||
- AppImage-backed exact-resolution logic into `crates/upm-appimage/src/add.rs` implementing `ExternalAddProvider`
|
||||
- AppImageHub search provider logic out of `upm-core/src/app/search.rs` into `crates/upm-appimage/src/search.rs`
|
||||
|
||||
`upm-appimage` should depend on `upm-core`, not the other way around.
|
||||
|
||||
Leave `SourceKind::AppImageHub`, `SourceInputKind::AppImageHub*`, and `NormalizedSourceKind::AppImageHub` in `upm-core` for this milestone. The deeper provider/domain generalization belongs to the next milestone.
|
||||
|
||||
**Step 4: Remove direct AppImageHub wiring from `upm-core`**
|
||||
|
||||
Update `upm-core` so it no longer declares:
|
||||
|
||||
- `pub mod appimagehub;` in `src/adapters/mod.rs`
|
||||
- `pub mod appimagehub;` in `src/source/mod.rs`
|
||||
- built-in AppImageHub search-provider construction in `src/app/search.rs`
|
||||
- direct `AppImageHubAdapter` imports in `src/app/add.rs`
|
||||
|
||||
After this step, AppImage behavior should exist only through the provider registry seam from Task 3.
|
||||
|
||||
**Step 5: Run the focused tests to verify pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test --package upm-appimage --test appimagehub_search
|
||||
cargo test --package upm-core --test adapter_contract
|
||||
cargo test --package upm-core --test adapter_smoke
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add Cargo.toml crates/upm-appimage crates/upm-core/src/adapters/mod.rs crates/upm-core/src/source/mod.rs crates/upm-core/src/app/add.rs crates/upm-core/src/app/search.rs crates/upm-core/tests/adapter_contract.rs crates/upm-core/tests/adapter_smoke.rs
|
||||
git commit -m "refactor: extract appimage support into upm-appimage"
|
||||
```
|
||||
|
||||
### Task 5: Rewire the `upm` CLI to assemble built-in providers from modules
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/upm/Cargo.toml`
|
||||
- Create: `crates/upm/src/providers.rs`
|
||||
- Modify: `crates/upm/src/lib.rs`
|
||||
- Test: `crates/upm/tests/search_cli.rs`
|
||||
- Test: `crates/upm/tests/end_to_end_cli.rs`
|
||||
- Test: `crates/upm/tests/ui_summary.rs`
|
||||
|
||||
**Step 1: Write the failing CLI integration expectations**
|
||||
|
||||
Update CLI integration tests to prove that:
|
||||
|
||||
- `upm search firefox` still includes AppImageHub results
|
||||
- direct `upm appimagehub/2338455` install flow still succeeds through the CLI
|
||||
- the final summary output still renders the new `upm`-prefixed paths and desktop-entry names
|
||||
|
||||
**Step 2: Run the focused tests to verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test --package upm --test search_cli
|
||||
cargo test --package upm --test end_to_end_cli
|
||||
cargo test --package upm --test ui_summary
|
||||
```
|
||||
|
||||
Expected: FAIL because `upm` does not yet assemble AppImage providers through the extracted module.
|
||||
|
||||
**Step 3: Add CLI-side provider assembly**
|
||||
|
||||
Create `crates/upm/src/providers.rs` that:
|
||||
|
||||
- builds the `ProviderRegistry` for `upm-core`
|
||||
- registers the `upm-appimage` search provider
|
||||
- registers the `upm-appimage` external add provider
|
||||
|
||||
Update `crates/upm/src/lib.rs` so dispatch paths call the provider-aware core entrypoints instead of hardcoded core defaults.
|
||||
|
||||
Do not move progress rendering or config loading into `upm-core`; the CLI remains the presentation layer.
|
||||
|
||||
**Step 4: Run the focused tests to verify pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test --package upm --test search_cli
|
||||
cargo test --package upm --test end_to_end_cli
|
||||
cargo test --package upm --test ui_summary
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/upm/Cargo.toml crates/upm/src/providers.rs crates/upm/src/lib.rs crates/upm/tests/search_cli.rs crates/upm/tests/end_to_end_cli.rs crates/upm/tests/ui_summary.rs
|
||||
git commit -m "refactor: compose providers from upm modules"
|
||||
```
|
||||
|
||||
### Task 6: Update docs and run full workspace verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
- Modify: `.architecture/overview.md`
|
||||
- Modify: `.architecture/roadmap.md`
|
||||
|
||||
**Step 1: Update product and architecture docs**
|
||||
|
||||
Document:
|
||||
|
||||
- the workspace rename to `upm`
|
||||
- `upm-core` as the headless application layer
|
||||
- `upm-appimage` as the first installable provider module
|
||||
- compatibility behavior for existing `aim` config and registry locations
|
||||
- the fact that provider composition now happens in the CLI rather than through hardcoded AppImage paths in `upm-core`
|
||||
|
||||
**Step 2: Verify the docs mention the new structure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n "upm-core|upm-appimage|legacy aim|ProviderRegistry|upm" README.md .architecture/overview.md .architecture/roadmap.md
|
||||
```
|
||||
|
||||
Expected: matches showing the renamed crates, provider split, and compatibility note.
|
||||
|
||||
**Step 3: Run the full verification suite**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add README.md .architecture/overview.md .architecture/roadmap.md
|
||||
git commit -m "docs: describe upm core and module split"
|
||||
```
|
||||
|
|
@ -10,4 +10,8 @@ Audits are to live in `.audits` with a good name slug plus time & date.
|
|||
IMPORTANT TO CHECK BEFORE ANY COMMIT!!
|
||||
Architecture under `.architecture` must be maintained with each change.
|
||||
- Security issues stumbled upon or noticed during execution **already in code** must live in `security-issues.md`. Newly added issues during execution or planning should be raised to the user and/or dealt with, instead of growing the list.
|
||||
- An overview of the workspace, should live in `overview.md`.
|
||||
- An overview of the workspace, should live in `overview.md`.
|
||||
- A roadmap of the planned direction & features in `roadmap.md`.
|
||||
|
||||
## UPM work
|
||||
We are currently using the `upm` branch as our "main" for all `upm` work, meaning feature branches/worktrees will merge back into here.
|
||||
81
Cargo.lock
generated
81
Cargo.lock
generated
|
|
@ -11,41 +11,6 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aim-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aim-core",
|
||||
"assert_cmd",
|
||||
"clap",
|
||||
"console 0.16.3",
|
||||
"crossterm",
|
||||
"dialoguer",
|
||||
"indicatif",
|
||||
"libc",
|
||||
"predicates",
|
||||
"ratatui",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aim-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"fs2",
|
||||
"md5",
|
||||
"quick-xml",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
|
|
@ -1966,6 +1931,52 @@ version = "0.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "upm"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"assert_cmd",
|
||||
"clap",
|
||||
"console 0.16.3",
|
||||
"crossterm",
|
||||
"dialoguer",
|
||||
"indicatif",
|
||||
"libc",
|
||||
"predicates",
|
||||
"ratatui",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"toml",
|
||||
"upm-appimage",
|
||||
"upm-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "upm-appimage"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"upm-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "upm-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"fs2",
|
||||
"md5",
|
||||
"quick-xml",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"crates/aim-core",
|
||||
"crates/aim-cli",
|
||||
"crates/upm-core",
|
||||
"crates/upm-appimage",
|
||||
"crates/upm",
|
||||
]
|
||||
default-members = [
|
||||
"crates/aim-cli",
|
||||
"crates/upm",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
|||
50
README.md
50
README.md
|
|
@ -1,24 +1,25 @@
|
|||
# aim
|
||||
AppImage Manager
|
||||
# upm
|
||||
Universal Package Manager
|
||||
|
||||
`aim` is a Rust Cargo workspace for managing AppImages from multiple source types.
|
||||
`upm` is a Rust Cargo workspace for a modular package manager with a shared headless core and provider crates.
|
||||
|
||||
## Workspace
|
||||
|
||||
- `crates/aim-core`: business logic, source adapters, registry, install/update planning
|
||||
- `crates/aim-cli`: thin terminal frontend for parsing, prompting, and rendering
|
||||
- `crates/upm-core`: headless application layer for query normalization, resolution, planning, registry persistence, install/update orchestration, and provider-facing APIs
|
||||
- `crates/upm`: thin terminal frontend for argument parsing, config loading, prompting, progress reporting, and summary rendering
|
||||
- `crates/upm-appimage`: AppImageHub transport, search, and add-provider integration composed into the CLI through `ProviderRegistry`
|
||||
|
||||
The split is intentional so a future GUI client can reuse `aim-core` without moving logic out of the shared library.
|
||||
The split is intentional so future frontends can reuse `upm-core`, while package-source behavior stays modular instead of being hardcoded into the core.
|
||||
|
||||
## Commands
|
||||
|
||||
```text
|
||||
aim <QUERY>
|
||||
aim
|
||||
aim update
|
||||
aim list
|
||||
aim search <QUERY>
|
||||
aim remove <QUERY>
|
||||
upm <QUERY>
|
||||
upm
|
||||
upm update
|
||||
upm list
|
||||
upm search <QUERY>
|
||||
upm remove <QUERY>
|
||||
```
|
||||
|
||||
## Query Forms
|
||||
|
|
@ -36,22 +37,22 @@ aim remove <QUERY>
|
|||
|
||||
## Search
|
||||
|
||||
`aim search <QUERY>` is part of v0.9 finalisation.
|
||||
`upm search <QUERY>` is part of the initial modular provider surface.
|
||||
|
||||
- search is provider-extensible and currently includes GitHub plus AppImageHub
|
||||
- search results should resolve to install-ready queries such as `owner/repo` and `appimagehub/<id>`
|
||||
- the search model is provider-extensible for future phases
|
||||
- provider composition happens in `crates/upm/src/providers.rs`, not through AppImageHub-specific wiring inside `upm-core`
|
||||
|
||||
## Scope Overrides
|
||||
|
||||
By default `aim` auto-detects whether to use user or system scope. Override that with:
|
||||
By default `upm` auto-detects whether to use user or system scope. Override that with:
|
||||
|
||||
- `--user`
|
||||
- `--system`
|
||||
|
||||
## Config
|
||||
|
||||
Runtime config is loaded from `~/.config/aim/config.toml` or `$XDG_CONFIG_HOME/aim/config.toml`.
|
||||
Runtime config is loaded from `~/.config/upm/config.toml` or `$XDG_CONFIG_HOME/upm/config.toml`.
|
||||
|
||||
Example:
|
||||
|
||||
|
|
@ -63,13 +64,20 @@ allow_http = true
|
|||
- `allow_http` only permits user-supplied `http://` inputs such as direct URL installs or updates from previously installed direct HTTP origins
|
||||
- provider-resolved downloads such as AppImageHub artifacts remain HTTPS-only even when `allow_http = true`
|
||||
|
||||
## Breaking Rename
|
||||
|
||||
- `upm` is a hard rename from `aim`
|
||||
- runtime overrides now use `UPM_*` names such as `UPM_CONFIG_PATH` and `UPM_REGISTRY_PATH`
|
||||
- old `AIM_*` runtime overrides are intentionally ignored
|
||||
- default config and registry locations now live under `upm` paths
|
||||
|
||||
## Current Flow Shape
|
||||
|
||||
- `aim <QUERY>` installs direct provider matches when available, otherwise falls back to search results, shows live progress on stderr, prints an `Installation Summary` on stdout for installs, and renders an `Installation Review` when tracking needs confirmation
|
||||
- bare `aim` prints an `Update Review` without mutating the registry
|
||||
- `aim update` executes the pending updates, streams live status on stderr, then prints an `Update Summary`
|
||||
- `aim list` renders either `Installed Apps` or `No installed apps yet`
|
||||
- `aim remove <QUERY>` resolves a registered application name, streams removal progress on stderr, then prints a `Removal Summary`
|
||||
- `upm <QUERY>` installs direct provider matches when available, otherwise falls back to search results, shows live progress on stderr, prints an `Installation Summary` on stdout for installs, and renders an `Installation Review` when tracking needs confirmation
|
||||
- bare `upm` prints an `Update Review` without mutating the registry
|
||||
- `upm update` executes the pending updates, streams live status on stderr, then prints an `Update Summary`
|
||||
- `upm list` renders either `Installed Apps` or `No installed apps yet`
|
||||
- `upm remove <QUERY>` resolves a registered application name, streams removal progress on stderr, then prints a `Removal Summary`
|
||||
|
||||
## Terminal UX
|
||||
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
use aim_cli::config::{CliConfig, ConfigError, SearchConfig, load_from_path};
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn missing_config_file_returns_defaults() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("config.toml");
|
||||
|
||||
let config = load_from_path(&path).unwrap();
|
||||
|
||||
assert_eq!(config, CliConfig::default());
|
||||
assert_eq!(config.search, SearchConfig::default());
|
||||
assert!(!config.allow_http);
|
||||
assert!(config.search.bottom_to_top);
|
||||
assert!(!config.search.skip_confirmation);
|
||||
assert_eq!(config.theme.accent, "#b388ff");
|
||||
assert_eq!(config.theme.accent_secondary, "#d5c2ff");
|
||||
assert_eq!(config.theme.dim, "#7f7396");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_section_overrides_defaults() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("config.toml");
|
||||
std::fs::write(
|
||||
&path,
|
||||
"allow_http = true\n\n[search]\nbottom_to_top = false\nskip_confirmation = true\n\n[theme]\naccent = \"#9f6bff\"\naccent_secondary = \"#efe7ff\"\ndim = \"#6b6480\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config = load_from_path(&path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config,
|
||||
CliConfig {
|
||||
allow_http: true,
|
||||
search: SearchConfig {
|
||||
bottom_to_top: false,
|
||||
skip_confirmation: true,
|
||||
},
|
||||
theme: aim_cli::config::ThemeConfig {
|
||||
accent: "#9f6bff".to_owned(),
|
||||
accent_secondary: "#efe7ff".to_owned(),
|
||||
dim: "#6b6480".to_owned(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_toml_returns_path_aware_error() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("config.toml");
|
||||
std::fs::write(&path, "[search\nskip_confirmation = true\n").unwrap();
|
||||
|
||||
let error = load_from_path(&path).unwrap_err();
|
||||
|
||||
match error {
|
||||
ConfigError::Parse {
|
||||
path: error_path, ..
|
||||
} => {
|
||||
assert_eq!(error_path, path);
|
||||
}
|
||||
other => panic!("expected parse error, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
use std::path::Path;
|
||||
|
||||
use aim_core::domain::app::InstallScope;
|
||||
use aim_core::integration::paths::{desktop_entry_path, managed_appimage_path};
|
||||
|
||||
#[test]
|
||||
fn user_scope_path_lands_under_home_managed_dir() {
|
||||
let path = managed_appimage_path(Path::new("/home/test"), InstallScope::User, "bat");
|
||||
|
||||
assert_eq!(
|
||||
path,
|
||||
Path::new("/home/test/.local/lib/aim/appimages/bat.AppImage")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_scope_desktop_entry_uses_system_prefix() {
|
||||
let path = desktop_entry_path(Path::new("/home/test"), InstallScope::System, "bat");
|
||||
|
||||
assert_eq!(path, Path::new("/usr/share/applications/aim-bat.desktop"));
|
||||
}
|
||||
14
crates/upm-appimage/Cargo.toml
Normal file
14
crates/upm-appimage/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "upm-appimage"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
quick-xml.workspace = true
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
upm-core = { path = "../upm-core" }
|
||||
163
crates/upm-appimage/src/add.rs
Normal file
163
crates/upm-appimage/src/add.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
use crate::source::appimagehub::{
|
||||
AppImageHubError, AppImageHubTransport, resolve_appimagehub_item, resolve_appimagehub_item_with,
|
||||
};
|
||||
use upm_core::adapters::traits::{
|
||||
AdapterCapabilities, AdapterError, AdapterResolution, AdapterResolveOutcome, SourceAdapter,
|
||||
};
|
||||
use upm_core::app::providers::{ExternalAddProvider, ExternalAddResolution};
|
||||
use upm_core::app::query::resolve_query;
|
||||
use upm_core::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
||||
use upm_core::domain::update::{
|
||||
ArtifactCandidate, ChannelPreference, UpdateChannelKind, UpdateStrategy,
|
||||
};
|
||||
|
||||
pub struct AppImageHubAdapter;
|
||||
|
||||
impl AppImageHubAdapter {
|
||||
pub fn resolve_source_with<T: AppImageHubTransport + ?Sized>(
|
||||
&self,
|
||||
source: &SourceRef,
|
||||
transport: &T,
|
||||
) -> Result<AdapterResolveOutcome, AdapterError> {
|
||||
if source.kind != SourceKind::AppImageHub {
|
||||
return Err(AdapterError::UnsupportedSource);
|
||||
}
|
||||
|
||||
let resolved = resolve_appimagehub_item_with(source, transport)
|
||||
.map_err(|error| AdapterError::ResolutionFailed(render_appimagehub_error(&error)))?;
|
||||
|
||||
match resolved {
|
||||
Some(item) => Ok(AdapterResolveOutcome::Resolved(AdapterResolution {
|
||||
source: item.source,
|
||||
release: ResolvedRelease {
|
||||
version: item.version,
|
||||
prerelease: false,
|
||||
},
|
||||
})),
|
||||
None => Ok(AdapterResolveOutcome::NoInstallableArtifact {
|
||||
source: source.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceAdapter for AppImageHubAdapter {
|
||||
fn id(&self) -> &'static str {
|
||||
"appimagehub"
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
AdapterCapabilities {
|
||||
supports_search: true,
|
||||
supports_exact_resolution: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn repository_source_kind(&self) -> Option<SourceKind> {
|
||||
Some(SourceKind::AppImageHub)
|
||||
}
|
||||
|
||||
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError> {
|
||||
let source = resolve_query(query).map_err(|_| AdapterError::UnsupportedQuery)?;
|
||||
if source.kind != SourceKind::AppImageHub {
|
||||
return Err(AdapterError::UnsupportedQuery);
|
||||
}
|
||||
|
||||
Ok(source)
|
||||
}
|
||||
|
||||
fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||
match resolve_appimagehub_item(source)
|
||||
.map_err(|error| AdapterError::ResolutionFailed(render_appimagehub_error(&error)))?
|
||||
{
|
||||
Some(item) => Ok(AdapterResolution {
|
||||
source: item.source,
|
||||
release: ResolvedRelease {
|
||||
version: item.version,
|
||||
prerelease: false,
|
||||
},
|
||||
}),
|
||||
None => Err(AdapterError::ResolutionFailed(
|
||||
"appimagehub item has no installable AppImage artifact".to_owned(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_supported_source(
|
||||
&self,
|
||||
source: &SourceRef,
|
||||
) -> Result<AdapterResolveOutcome, AdapterError> {
|
||||
let transport = crate::source::appimagehub::default_transport();
|
||||
self.resolve_source_with(source, transport.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppImageHubAddProvider<'a, T: AppImageHubTransport + ?Sized> {
|
||||
transport: &'a T,
|
||||
}
|
||||
|
||||
impl<'a, T: AppImageHubTransport + ?Sized> AppImageHubAddProvider<'a, T> {
|
||||
pub fn new(transport: &'a T) -> Self {
|
||||
Self { transport }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AppImageHubTransport + ?Sized> ExternalAddProvider for AppImageHubAddProvider<'_, T> {
|
||||
fn id(&self) -> &'static str {
|
||||
"appimagehub"
|
||||
}
|
||||
|
||||
fn resolve(&self, source: &SourceRef) -> Result<Option<ExternalAddResolution>, AdapterError> {
|
||||
if source.kind != SourceKind::AppImageHub {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let adapter = AppImageHubAdapter;
|
||||
let resolution = match adapter.resolve_source_with(source, self.transport)? {
|
||||
AdapterResolveOutcome::Resolved(resolution) => resolution,
|
||||
AdapterResolveOutcome::NoInstallableArtifact { .. } => return Ok(None),
|
||||
};
|
||||
let Some(resolved_item) = resolve_appimagehub_item_with(source, self.transport)
|
||||
.map_err(|error| AdapterError::ResolutionFailed(format!("{error:?}")))?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(ExternalAddResolution {
|
||||
resolution,
|
||||
selected_artifact: ArtifactCandidate {
|
||||
url: resolved_item.download.url.clone(),
|
||||
version: resolved_item.version.clone(),
|
||||
arch: resolved_item.download.arch.clone(),
|
||||
trusted_checksum: None,
|
||||
weak_checksum_md5: resolved_item.download.md5sum.clone(),
|
||||
selection_reason: "provider-release".to_owned(),
|
||||
},
|
||||
update_strategy: UpdateStrategy {
|
||||
preferred: ChannelPreference {
|
||||
kind: UpdateChannelKind::DirectAsset,
|
||||
locator: resolved_item.download.url.clone(),
|
||||
reason: "provider-release".to_owned(),
|
||||
},
|
||||
alternates: Vec::new(),
|
||||
},
|
||||
display_name_hint: Some(resolved_item.title),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn render_appimagehub_error(error: &AppImageHubError) -> String {
|
||||
match error {
|
||||
AppImageHubError::FixtureItemMissing(id) => {
|
||||
format!("missing appimagehub fixture item {id}")
|
||||
}
|
||||
AppImageHubError::InsecureDownloadUrl(url) => {
|
||||
format!("insecure appimagehub download url: {url}")
|
||||
}
|
||||
AppImageHubError::Parse(error) => error.to_string(),
|
||||
AppImageHubError::Transport(error) => error.to_string(),
|
||||
AppImageHubError::UnsupportedSource(locator) => {
|
||||
format!("unsupported appimagehub source: {locator}")
|
||||
}
|
||||
}
|
||||
}
|
||||
6
crates/upm-appimage/src/lib.rs
Normal file
6
crates/upm-appimage/src/lib.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod add;
|
||||
pub mod search;
|
||||
pub mod source;
|
||||
|
||||
pub use add::{AppImageHubAdapter, AppImageHubAddProvider};
|
||||
pub use search::AppImageHubSearchProvider;
|
||||
103
crates/upm-appimage/src/search.rs
Normal file
103
crates/upm-appimage/src/search.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
use crate::source::appimagehub::{
|
||||
AppImageHubSearchError, AppImageHubTransport, search_appimagehub_with,
|
||||
};
|
||||
use upm_core::app::search::{SearchProvider, SearchProviderError};
|
||||
use upm_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult};
|
||||
|
||||
pub struct AppImageHubSearchProvider<'a, T: AppImageHubTransport + ?Sized> {
|
||||
transport: &'a T,
|
||||
}
|
||||
|
||||
impl<'a, T: AppImageHubTransport + ?Sized> AppImageHubSearchProvider<'a, T> {
|
||||
pub fn new(transport: &'a T) -> Self {
|
||||
Self { transport }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AppImageHubTransport + ?Sized> SearchProvider for AppImageHubSearchProvider<'_, T> {
|
||||
fn search(&self, query: &SearchQuery) -> Result<Vec<SearchResult>, SearchProviderError> {
|
||||
let hits = search_appimagehub_with(&query.text, query.remote_limit, self.transport)
|
||||
.map_err(|error| {
|
||||
SearchProviderError::new("appimagehub", &render_appimagehub_search_error(&error))
|
||||
})?;
|
||||
|
||||
let normalized_query = normalize_lookup(&query.text);
|
||||
let mut ranked_hits = hits
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, hit)| {
|
||||
(
|
||||
appimagehub_remote_match_rank(
|
||||
&normalized_query,
|
||||
&hit.name,
|
||||
hit.summary.as_deref(),
|
||||
),
|
||||
index,
|
||||
hit,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
ranked_hits.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1)));
|
||||
|
||||
Ok(ranked_hits
|
||||
.into_iter()
|
||||
.map(|(_, _, hit)| SearchResult {
|
||||
provider_id: "appimagehub".to_owned(),
|
||||
display_name: hit.name,
|
||||
description: hit.summary,
|
||||
source_locator: hit.detail_page,
|
||||
install_query: format!("appimagehub/{}", hit.id),
|
||||
canonical_locator: hit.id,
|
||||
version: Some(hit.version),
|
||||
install_status: SearchInstallStatus::Available,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_lookup(value: &str) -> String {
|
||||
value.trim().to_ascii_lowercase()
|
||||
}
|
||||
|
||||
fn appimagehub_remote_match_rank(query: &str, name: &str, summary: Option<&str>) -> u8 {
|
||||
let name = normalize_lookup(name);
|
||||
let summary = summary.map(normalize_lookup);
|
||||
|
||||
if name == query {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if name.starts_with(query) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if name.contains(query) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if summary
|
||||
.as_deref()
|
||||
.map(|summary| summary.starts_with(query))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
if summary
|
||||
.as_deref()
|
||||
.map(|summary| summary.contains(query))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
|
||||
5
|
||||
}
|
||||
|
||||
fn render_appimagehub_search_error(error: &AppImageHubSearchError) -> String {
|
||||
match error {
|
||||
AppImageHubSearchError::Parse(inner) => inner.to_string(),
|
||||
AppImageHubSearchError::Transport(inner) => inner.to_string(),
|
||||
}
|
||||
}
|
||||
517
crates/upm-appimage/src/source/appimagehub.rs
Normal file
517
crates/upm-appimage/src/source/appimagehub.rs
Normal file
|
|
@ -0,0 +1,517 @@
|
|||
use std::env;
|
||||
use std::time::Duration;
|
||||
|
||||
use upm_core::domain::source::SourceRef;
|
||||
|
||||
const DEFAULT_APPIMAGEHUB_API_BASE: &str = "https://api.appimagehub.com/ocs/v1/content";
|
||||
const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE";
|
||||
const FIXTURE_MODE_ENV: &str = "UPM_APPIMAGEHUB_FIXTURE_MODE";
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct AppImageHubDownload {
|
||||
pub url: String,
|
||||
pub name: String,
|
||||
pub package_type: Option<String>,
|
||||
pub arch: Option<String>,
|
||||
pub md5sum: Option<String>,
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct AppImageHubItem {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub summary: Option<String>,
|
||||
pub detail_page: String,
|
||||
pub tags: Vec<String>,
|
||||
pub downloads: Vec<AppImageHubDownload>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct AppImageHubSearchHit {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub summary: Option<String>,
|
||||
pub detail_page: String,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ResolvedAppImageHubItem {
|
||||
pub source: SourceRef,
|
||||
pub title: String,
|
||||
pub version: String,
|
||||
pub download: AppImageHubDownload,
|
||||
}
|
||||
|
||||
pub trait AppImageHubTransport {
|
||||
fn fetch_item(&self, id: &str) -> Result<AppImageHubItem, AppImageHubError>;
|
||||
|
||||
fn search_items(
|
||||
&self,
|
||||
query: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError>;
|
||||
}
|
||||
|
||||
pub fn default_transport() -> Box<dyn AppImageHubTransport> {
|
||||
if env::var(GLOBAL_FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
|
||||
|| env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
|
||||
{
|
||||
Box::new(FixtureAppImageHubTransport)
|
||||
} else {
|
||||
Box::new(ReqwestAppImageHubTransport::new())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_appimagehub_item(
|
||||
source: &SourceRef,
|
||||
) -> Result<Option<ResolvedAppImageHubItem>, AppImageHubError> {
|
||||
let transport = default_transport();
|
||||
resolve_appimagehub_item_with(source, transport.as_ref())
|
||||
}
|
||||
|
||||
pub fn resolve_appimagehub_item_with<T: AppImageHubTransport + ?Sized>(
|
||||
source: &SourceRef,
|
||||
transport: &T,
|
||||
) -> Result<Option<ResolvedAppImageHubItem>, AppImageHubError> {
|
||||
let item = transport.fetch_item(source_id(source)?)?;
|
||||
let Some(download) = item
|
||||
.downloads
|
||||
.iter()
|
||||
.find(|download| is_appimage_download(download))
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
validate_download_url(&download.url)?;
|
||||
|
||||
Ok(Some(ResolvedAppImageHubItem {
|
||||
source: source.clone(),
|
||||
title: item.name.clone(),
|
||||
version: resolved_version(&item, download),
|
||||
download: download.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn search_appimagehub(
|
||||
query: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
|
||||
let transport = default_transport();
|
||||
search_appimagehub_with(query, limit, transport.as_ref())
|
||||
}
|
||||
|
||||
pub fn search_appimagehub_with<T: AppImageHubTransport + ?Sized>(
|
||||
query: &str,
|
||||
limit: usize,
|
||||
transport: &T,
|
||||
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
|
||||
transport.search_items(query, limit)
|
||||
}
|
||||
|
||||
pub struct ReqwestAppImageHubTransport {
|
||||
client: reqwest::blocking::Client,
|
||||
api_base: String,
|
||||
}
|
||||
|
||||
impl Default for ReqwestAppImageHubTransport {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ReqwestAppImageHubTransport {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
client: reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.expect("reqwest client should build"),
|
||||
api_base: env::var("UPM_APPIMAGEHUB_API_BASE")
|
||||
.unwrap_or_else(|_| DEFAULT_APPIMAGEHUB_API_BASE.to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppImageHubTransport for ReqwestAppImageHubTransport {
|
||||
fn fetch_item(&self, id: &str) -> Result<AppImageHubItem, AppImageHubError> {
|
||||
let url = format!("{}/data/{id}", self.api_base);
|
||||
let xml = self
|
||||
.client
|
||||
.get(url)
|
||||
.send()
|
||||
.map_err(AppImageHubError::Transport)?
|
||||
.error_for_status()
|
||||
.map_err(AppImageHubError::Transport)?
|
||||
.text()
|
||||
.map_err(AppImageHubError::Transport)?;
|
||||
|
||||
parse_item_xml(&xml)
|
||||
}
|
||||
|
||||
fn search_items(
|
||||
&self,
|
||||
query: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
|
||||
let url = format!("{}/data", self.api_base);
|
||||
let xml = self
|
||||
.client
|
||||
.get(url)
|
||||
.query(&[("search", query), ("pagesize", &limit.to_string())])
|
||||
.send()
|
||||
.map_err(AppImageHubSearchError::Transport)?
|
||||
.error_for_status()
|
||||
.map_err(AppImageHubSearchError::Transport)?
|
||||
.text()
|
||||
.map_err(AppImageHubSearchError::Transport)?;
|
||||
|
||||
parse_search_xml(&xml)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct FixtureAppImageHubTransport;
|
||||
|
||||
impl AppImageHubTransport for FixtureAppImageHubTransport {
|
||||
fn fetch_item(&self, id: &str) -> Result<AppImageHubItem, AppImageHubError> {
|
||||
fixture_item(id).ok_or_else(|| AppImageHubError::FixtureItemMissing(id.to_owned()))
|
||||
}
|
||||
|
||||
fn search_items(
|
||||
&self,
|
||||
query: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
|
||||
Ok(fixture_search_results(query, limit))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AppImageHubError {
|
||||
FixtureItemMissing(String),
|
||||
InsecureDownloadUrl(String),
|
||||
Parse(quick_xml::DeError),
|
||||
Transport(reqwest::Error),
|
||||
UnsupportedSource(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AppImageHubSearchError {
|
||||
Parse(quick_xml::DeError),
|
||||
Transport(reqwest::Error),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct OcsSingleResponse {
|
||||
data: OcsSingleData,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct OcsSingleData {
|
||||
content: OcsContent,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct OcsSearchResponse {
|
||||
data: OcsSearchData,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct OcsSearchData {
|
||||
#[serde(default)]
|
||||
content: Vec<OcsContent>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct OcsContent {
|
||||
id: String,
|
||||
name: String,
|
||||
version: Option<String>,
|
||||
summary: Option<String>,
|
||||
detailpage: Option<String>,
|
||||
tags: Option<String>,
|
||||
downloadlink1: Option<String>,
|
||||
downloadname1: Option<String>,
|
||||
download_package_type1: Option<String>,
|
||||
download_package_arch1: Option<String>,
|
||||
downloadmd5sum1: Option<String>,
|
||||
download_version1: Option<String>,
|
||||
downloadlink2: Option<String>,
|
||||
downloadname2: Option<String>,
|
||||
download_package_type2: Option<String>,
|
||||
download_package_arch2: Option<String>,
|
||||
downloadmd5sum2: Option<String>,
|
||||
download_version2: Option<String>,
|
||||
downloadlink3: Option<String>,
|
||||
downloadname3: Option<String>,
|
||||
download_package_type3: Option<String>,
|
||||
download_package_arch3: Option<String>,
|
||||
downloadmd5sum3: Option<String>,
|
||||
download_version3: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_item_xml(xml: &str) -> Result<AppImageHubItem, AppImageHubError> {
|
||||
let parsed =
|
||||
quick_xml::de::from_str::<OcsSingleResponse>(xml).map_err(AppImageHubError::Parse)?;
|
||||
Ok(content_to_item(parsed.data.content))
|
||||
}
|
||||
|
||||
fn parse_search_xml(xml: &str) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
|
||||
if !xml.contains("<id>") {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let parsed =
|
||||
quick_xml::de::from_str::<OcsSearchResponse>(xml).map_err(AppImageHubSearchError::Parse)?;
|
||||
Ok(parsed
|
||||
.data
|
||||
.content
|
||||
.into_iter()
|
||||
.map(|content| AppImageHubSearchHit {
|
||||
id: content.id,
|
||||
name: content.name,
|
||||
version: normalize_version_text(content.version.as_deref()),
|
||||
summary: content.summary,
|
||||
detail_page: content
|
||||
.detailpage
|
||||
.unwrap_or_else(|| "https://www.appimagehub.com".to_owned()),
|
||||
tags: split_tags(content.tags.as_deref()),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn content_to_item(content: OcsContent) -> AppImageHubItem {
|
||||
let detail_page = content
|
||||
.detailpage
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://www.appimagehub.com".to_owned());
|
||||
let summary = content.summary.clone();
|
||||
let tags = split_tags(content.tags.as_deref());
|
||||
let downloads = collect_downloads(&content);
|
||||
|
||||
AppImageHubItem {
|
||||
id: content.id,
|
||||
name: content.name,
|
||||
version: normalize_version_text(content.version.as_deref()),
|
||||
summary,
|
||||
detail_page,
|
||||
tags,
|
||||
downloads,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_download_url(url: &str) -> Result<(), AppImageHubError> {
|
||||
if !url.starts_with("https://") {
|
||||
return Err(AppImageHubError::InsecureDownloadUrl(url.to_owned()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_downloads(content: &OcsContent) -> Vec<AppImageHubDownload> {
|
||||
let mut downloads = Vec::new();
|
||||
|
||||
for download in [
|
||||
download_slot(
|
||||
content.downloadlink1.as_deref(),
|
||||
content.downloadname1.as_deref(),
|
||||
content.download_package_type1.as_deref(),
|
||||
content.download_package_arch1.as_deref(),
|
||||
content.downloadmd5sum1.as_deref(),
|
||||
content.download_version1.as_deref(),
|
||||
),
|
||||
download_slot(
|
||||
content.downloadlink2.as_deref(),
|
||||
content.downloadname2.as_deref(),
|
||||
content.download_package_type2.as_deref(),
|
||||
content.download_package_arch2.as_deref(),
|
||||
content.downloadmd5sum2.as_deref(),
|
||||
content.download_version2.as_deref(),
|
||||
),
|
||||
download_slot(
|
||||
content.downloadlink3.as_deref(),
|
||||
content.downloadname3.as_deref(),
|
||||
content.download_package_type3.as_deref(),
|
||||
content.download_package_arch3.as_deref(),
|
||||
content.downloadmd5sum3.as_deref(),
|
||||
content.download_version3.as_deref(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
downloads.push(download);
|
||||
}
|
||||
|
||||
downloads
|
||||
}
|
||||
|
||||
fn download_slot(
|
||||
link: Option<&str>,
|
||||
name: Option<&str>,
|
||||
package_type: Option<&str>,
|
||||
arch: Option<&str>,
|
||||
md5sum: Option<&str>,
|
||||
version: Option<&str>,
|
||||
) -> Option<AppImageHubDownload> {
|
||||
let url = link?.trim();
|
||||
if url.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(AppImageHubDownload {
|
||||
url: url.to_owned(),
|
||||
name: name.unwrap_or("download").trim().to_owned(),
|
||||
package_type: trim_optional(package_type),
|
||||
arch: trim_optional(arch),
|
||||
md5sum: trim_optional(md5sum),
|
||||
version: trim_optional(version),
|
||||
})
|
||||
}
|
||||
|
||||
fn trim_optional(value: Option<&str>) -> Option<String> {
|
||||
value
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn normalize_version_text(value: Option<&str>) -> String {
|
||||
let value = value.map(str::trim).filter(|value| !value.is_empty());
|
||||
match value {
|
||||
Some("Latest") | Some("latest") | None => "latest".to_owned(),
|
||||
Some(other) => other.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn split_tags(tags: Option<&str>) -> Vec<String> {
|
||||
tags.unwrap_or_default()
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|tag| !tag.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn source_id(source: &SourceRef) -> Result<&str, AppImageHubError> {
|
||||
source
|
||||
.canonical_locator
|
||||
.as_deref()
|
||||
.or_else(|| source.locator.rsplit('/').next())
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| AppImageHubError::UnsupportedSource(source.locator.clone()))
|
||||
}
|
||||
|
||||
fn is_appimage_download(download: &AppImageHubDownload) -> bool {
|
||||
download
|
||||
.package_type
|
||||
.as_deref()
|
||||
.map(|kind| kind.eq_ignore_ascii_case("appimage"))
|
||||
.unwrap_or(false)
|
||||
|| download.name.ends_with(".AppImage")
|
||||
}
|
||||
|
||||
fn resolved_version(item: &AppImageHubItem, download: &AppImageHubDownload) -> String {
|
||||
download
|
||||
.version
|
||||
.as_deref()
|
||||
.map(|value| normalize_version_text(Some(value)))
|
||||
.filter(|value| value != "latest")
|
||||
.unwrap_or_else(|| item.version.clone())
|
||||
}
|
||||
|
||||
fn fixture_item(id: &str) -> Option<AppImageHubItem> {
|
||||
let insecure_http = env::var("UPM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP")
|
||||
.ok()
|
||||
.as_deref()
|
||||
== Some("1");
|
||||
let bad_md5 = env::var("UPM_APPIMAGEHUB_FIXTURE_BAD_MD5").ok().as_deref() == Some("1");
|
||||
|
||||
match id {
|
||||
"2338455" => Some(AppImageHubItem {
|
||||
id: "2338455".to_owned(),
|
||||
name: "Firefox by Mozilla - Official AppImage Edition".to_owned(),
|
||||
version: "latest".to_owned(),
|
||||
summary: Some("Take control of your internet with the Firefox browser".to_owned()),
|
||||
detail_page: "https://www.appimagehub.com/p/2338455".to_owned(),
|
||||
tags: vec![
|
||||
"appimage".to_owned(),
|
||||
"x86-64".to_owned(),
|
||||
"desktop".to_owned(),
|
||||
"release-stable".to_owned(),
|
||||
],
|
||||
downloads: vec![AppImageHubDownload {
|
||||
url: if insecure_http {
|
||||
"http://files06.pling.com/api/files/download/firefox-x86-64.AppImage".to_owned()
|
||||
} else {
|
||||
"https://files06.pling.com/api/files/download/firefox-x86-64.AppImage"
|
||||
.to_owned()
|
||||
},
|
||||
name: "firefox-x86-64.AppImage".to_owned(),
|
||||
package_type: Some("appimage".to_owned()),
|
||||
arch: Some("x86-64".to_owned()),
|
||||
md5sum: Some(if bad_md5 {
|
||||
"00000000000000000000000000000000".to_owned()
|
||||
} else {
|
||||
"2a685cf45213d5a2a243273fa68dafa6".to_owned()
|
||||
}),
|
||||
version: None,
|
||||
}],
|
||||
}),
|
||||
"2337998" => Some(AppImageHubItem {
|
||||
id: "2337998".to_owned(),
|
||||
name: "Example Non-AppImage Package".to_owned(),
|
||||
version: "latest".to_owned(),
|
||||
summary: Some("An item that does not expose an AppImage download".to_owned()),
|
||||
detail_page: "https://www.appimagehub.com/p/2337998".to_owned(),
|
||||
tags: vec!["desktop".to_owned()],
|
||||
downloads: vec![AppImageHubDownload {
|
||||
url: "https://files06.pling.com/api/files/download/example.deb".to_owned(),
|
||||
name: "example.deb".to_owned(),
|
||||
package_type: Some("debian-package".to_owned()),
|
||||
arch: Some("x86-64".to_owned()),
|
||||
md5sum: None,
|
||||
version: Some("2.1.1".to_owned()),
|
||||
}],
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn fixture_search_results(query: &str, limit: usize) -> Vec<AppImageHubSearchHit> {
|
||||
let query = query.trim().to_ascii_lowercase();
|
||||
let fixtures = [
|
||||
AppImageHubSearchHit {
|
||||
id: "2338455".to_owned(),
|
||||
name: "Firefox by Mozilla - Official AppImage Edition".to_owned(),
|
||||
version: "latest".to_owned(),
|
||||
summary: Some("Take control of your internet with the Firefox browser".to_owned()),
|
||||
detail_page: "https://www.appimagehub.com/p/2338455".to_owned(),
|
||||
tags: vec!["browser".to_owned(), "appimage".to_owned()],
|
||||
},
|
||||
AppImageHubSearchHit {
|
||||
id: "2338484".to_owned(),
|
||||
name: "Waterfox".to_owned(),
|
||||
version: "latest".to_owned(),
|
||||
summary: Some("Open Source, Private Browsing".to_owned()),
|
||||
detail_page: "https://www.appimagehub.com/p/2338484".to_owned(),
|
||||
tags: vec!["browser".to_owned(), "appimage".to_owned()],
|
||||
},
|
||||
];
|
||||
|
||||
fixtures
|
||||
.into_iter()
|
||||
.filter(|item| {
|
||||
item.name.to_ascii_lowercase().contains(&query)
|
||||
|| item
|
||||
.tags
|
||||
.iter()
|
||||
.any(|tag| tag.to_ascii_lowercase().contains(&query))
|
||||
})
|
||||
.take(limit)
|
||||
.collect()
|
||||
}
|
||||
1
crates/upm-appimage/src/source/mod.rs
Normal file
1
crates/upm-appimage/src/source/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod appimagehub;
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
use aim_core::app::search::{
|
||||
AppImageHubSearchProvider, GitHubSearchProvider, SearchProvider, SearchProviderError,
|
||||
build_search_results_with,
|
||||
use upm_appimage::add::{AppImageHubAdapter, AppImageHubAddProvider};
|
||||
use upm_appimage::search::AppImageHubSearchProvider;
|
||||
use upm_appimage::source::appimagehub::FixtureAppImageHubTransport;
|
||||
use upm_core::adapters::traits::AdapterResolveOutcome;
|
||||
use upm_core::app::providers::ExternalAddProvider;
|
||||
use upm_core::app::query::resolve_query;
|
||||
use upm_core::app::search::{
|
||||
GitHubSearchProvider, SearchProvider, SearchProviderError, build_search_results_with,
|
||||
};
|
||||
use aim_core::domain::app::AppRecord;
|
||||
use aim_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult};
|
||||
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
|
||||
use aim_core::source::appimagehub::FixtureAppImageHubTransport;
|
||||
use aim_core::source::github::FixtureGitHubTransport;
|
||||
use upm_core::domain::app::AppRecord;
|
||||
use upm_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult};
|
||||
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
|
||||
use upm_core::source::github::FixtureGitHubTransport;
|
||||
|
||||
struct StubProvider {
|
||||
hit: SearchResult,
|
||||
|
|
@ -106,3 +110,40 @@ fn search_can_merge_github_and_appimagehub_providers() {
|
|||
.any(|hit| hit.provider_id == "appimagehub")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appimagehub_adapter_resolves_installable_items_through_fixture_transport() {
|
||||
let adapter = AppImageHubAdapter;
|
||||
let source = resolve_query("appimagehub/2338455").unwrap();
|
||||
|
||||
let resolution = adapter
|
||||
.resolve_source_with(&source, &FixtureAppImageHubTransport)
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
resolution,
|
||||
AdapterResolveOutcome::Resolved(resolution)
|
||||
if resolution.source.kind == SourceKind::AppImageHub
|
||||
&& resolution.source.canonical_locator.as_deref() == Some("2338455")
|
||||
&& resolution.release.version == "latest"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appimagehub_add_provider_resolves_external_add_plan() {
|
||||
let provider = AppImageHubAddProvider::new(&FixtureAppImageHubTransport);
|
||||
let source = resolve_query("appimagehub/2338455").unwrap();
|
||||
|
||||
let resolution = provider.resolve(&source).unwrap().unwrap();
|
||||
|
||||
assert_eq!(resolution.resolution.source.kind, SourceKind::AppImageHub);
|
||||
assert_eq!(resolution.resolution.release.version, "latest");
|
||||
assert_eq!(
|
||||
resolution.selected_artifact.url,
|
||||
"https://files06.pling.com/api/files/download/firefox-x86-64.AppImage"
|
||||
);
|
||||
assert_eq!(
|
||||
resolution.display_name_hint.as_deref(),
|
||||
Some("Firefox by Mozilla - Official AppImage Edition")
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "aim-core"
|
||||
name = "upm-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
pub mod appimagehub;
|
||||
pub mod direct_url;
|
||||
pub mod github;
|
||||
pub mod gitlab;
|
||||
|
|
@ -11,14 +10,7 @@ use crate::adapters::traits::SourceAdapter;
|
|||
use crate::domain::source::SourceRef;
|
||||
|
||||
pub fn all_adapter_kinds() -> Vec<&'static str> {
|
||||
vec![
|
||||
"appimagehub",
|
||||
"github",
|
||||
"gitlab",
|
||||
"direct-url",
|
||||
"zsync",
|
||||
"sourceforge",
|
||||
]
|
||||
vec!["github", "gitlab", "direct-url", "zsync", "sourceforge"]
|
||||
}
|
||||
|
||||
pub fn supports_source<A: SourceAdapter + ?Sized>(adapter: &A, source: &SourceRef) -> bool {
|
||||
|
|
@ -3,7 +3,6 @@ use std::fs::{self, File};
|
|||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::adapters::appimagehub::AppImageHubAdapter;
|
||||
use crate::adapters::direct_url::DirectUrlAdapter;
|
||||
use crate::adapters::gitlab::GitLabAdapter;
|
||||
use crate::adapters::sourceforge::SourceForgeAdapter;
|
||||
|
|
@ -14,6 +13,7 @@ use crate::app::interaction::{InteractionKind, InteractionRequest};
|
|||
use crate::app::progress::{
|
||||
NoopReporter, OperationEvent, OperationKind, OperationStage, ProgressReporter,
|
||||
};
|
||||
use crate::app::providers::{ExternalAddResolution, ProviderRegistry};
|
||||
use crate::app::query::{ResolveQueryError, resolve_query};
|
||||
use crate::app::scope::{ScopeOverride, resolve_install_scope_with_default};
|
||||
use crate::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
|
|
@ -25,14 +25,14 @@ use crate::integration::install::{
|
|||
use crate::integration::policy::{IntegrationMode, resolve_install_policy};
|
||||
use crate::metadata::parse_document;
|
||||
use crate::platform::probe_live_host;
|
||||
use crate::source::appimagehub::resolve_appimagehub_item;
|
||||
use crate::source::github::{
|
||||
GitHubDiscoveryError, GitHubTransport, discover_github_candidates_with, http_client_policy,
|
||||
};
|
||||
use crate::update::channels::build_channels;
|
||||
use crate::update::ranking::{rank_channels, select_artifact, to_preference};
|
||||
|
||||
const FIXTURE_MODE_ENV: &str = "AIM_GITHUB_FIXTURE_MODE";
|
||||
const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE";
|
||||
const FIXTURE_MODE_ENV: &str = "UPM_GITHUB_FIXTURE_MODE";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct AddSecurityPolicy {
|
||||
|
|
@ -42,11 +42,12 @@ pub struct AddSecurityPolicy {
|
|||
pub fn build_add_plan(query: &str) -> Result<AddPlan, BuildAddPlanError> {
|
||||
let transport = crate::source::github::default_transport();
|
||||
let mut reporter = NoopReporter;
|
||||
build_add_plan_with_reporter_and_policy(
|
||||
build_add_plan_with_reporter_and_policy_and_registry(
|
||||
query,
|
||||
transport.as_ref(),
|
||||
&mut reporter,
|
||||
AddSecurityPolicy::default(),
|
||||
&ProviderRegistry::default(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -55,11 +56,12 @@ pub fn build_add_plan_with<T: GitHubTransport + ?Sized>(
|
|||
transport: &T,
|
||||
) -> Result<AddPlan, BuildAddPlanError> {
|
||||
let mut reporter = NoopReporter;
|
||||
build_add_plan_with_reporter_and_policy(
|
||||
build_add_plan_with_reporter_and_policy_and_registry(
|
||||
query,
|
||||
transport,
|
||||
&mut reporter,
|
||||
AddSecurityPolicy::default(),
|
||||
&ProviderRegistry::default(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -68,11 +70,40 @@ pub fn build_add_plan_with_reporter<T: GitHubTransport + ?Sized>(
|
|||
transport: &T,
|
||||
reporter: &mut impl ProgressReporter,
|
||||
) -> Result<AddPlan, BuildAddPlanError> {
|
||||
build_add_plan_with_reporter_and_policy(
|
||||
build_add_plan_with_reporter_and_policy_and_registry(
|
||||
query,
|
||||
transport,
|
||||
reporter,
|
||||
AddSecurityPolicy::default(),
|
||||
&ProviderRegistry::default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_add_plan_with_registered_providers<T: GitHubTransport + ?Sized>(
|
||||
query: &str,
|
||||
transport: &T,
|
||||
providers: &ProviderRegistry<'_>,
|
||||
policy: AddSecurityPolicy,
|
||||
) -> Result<AddPlan, BuildAddPlanError> {
|
||||
let mut reporter = NoopReporter;
|
||||
build_add_plan_with_reporter_and_policy_and_registry(
|
||||
query,
|
||||
transport,
|
||||
&mut reporter,
|
||||
policy,
|
||||
providers,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_add_plan_with_reporter_and_registered_providers<T: GitHubTransport + ?Sized>(
|
||||
query: &str,
|
||||
transport: &T,
|
||||
reporter: &mut impl ProgressReporter,
|
||||
providers: &ProviderRegistry<'_>,
|
||||
policy: AddSecurityPolicy,
|
||||
) -> Result<AddPlan, BuildAddPlanError> {
|
||||
build_add_plan_with_reporter_and_policy_and_registry(
|
||||
query, transport, reporter, policy, providers,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -81,6 +112,22 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
|
|||
transport: &T,
|
||||
reporter: &mut impl ProgressReporter,
|
||||
policy: AddSecurityPolicy,
|
||||
) -> Result<AddPlan, BuildAddPlanError> {
|
||||
build_add_plan_with_reporter_and_policy_and_registry(
|
||||
query,
|
||||
transport,
|
||||
reporter,
|
||||
policy,
|
||||
&ProviderRegistry::default(),
|
||||
)
|
||||
}
|
||||
|
||||
fn build_add_plan_with_reporter_and_policy_and_registry<T: GitHubTransport + ?Sized>(
|
||||
query: &str,
|
||||
transport: &T,
|
||||
reporter: &mut impl ProgressReporter,
|
||||
policy: AddSecurityPolicy,
|
||||
providers: &ProviderRegistry<'_>,
|
||||
) -> Result<AddPlan, BuildAddPlanError> {
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::ResolveQuery,
|
||||
|
|
@ -91,8 +138,7 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
|
|||
|
||||
let mut interactions = Vec::new();
|
||||
let mut parsed_metadata = Vec::new();
|
||||
let mut display_name_hint = None;
|
||||
let (resolution, selected_artifact, update_strategy) = match source.kind {
|
||||
let (resolution, selected_artifact, update_strategy, display_name_hint) = match source.kind {
|
||||
SourceKind::GitHub => {
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::DiscoverRelease,
|
||||
|
|
@ -148,6 +194,7 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
|
|||
},
|
||||
artifact,
|
||||
strategy,
|
||||
None,
|
||||
)
|
||||
}
|
||||
SourceKind::GitLab => {
|
||||
|
|
@ -188,59 +235,29 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
|
|||
selection_reason: "provider-release".to_owned(),
|
||||
};
|
||||
|
||||
(resolution, artifact, strategy)
|
||||
(resolution, artifact, strategy, None)
|
||||
}
|
||||
SourceKind::AppImageHub => {
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::DiscoverRelease,
|
||||
message: "discovering release".to_owned(),
|
||||
});
|
||||
let adapter = AppImageHubAdapter;
|
||||
let resolution = match adapter
|
||||
.resolve_source(&source)
|
||||
.map_err(|error| BuildAddPlanError::Adapter("appimagehub", error))?
|
||||
if let Some(external_resolution) =
|
||||
resolve_registered_external_add_provider(&source, providers)?
|
||||
{
|
||||
AdapterResolveOutcome::Resolved(resolution) => resolution,
|
||||
AdapterResolveOutcome::NoInstallableArtifact { source } => {
|
||||
return Err(BuildAddPlanError::NoInstallableArtifact { source });
|
||||
}
|
||||
};
|
||||
let resolved_item = resolve_appimagehub_item(&resolution.source)
|
||||
.map_err(|error| {
|
||||
BuildAddPlanError::Adapter(
|
||||
"appimagehub",
|
||||
crate::adapters::traits::AdapterError::ResolutionFailed(format!(
|
||||
"{error:?}"
|
||||
)),
|
||||
)
|
||||
})?
|
||||
.ok_or(BuildAddPlanError::NoInstallableArtifact {
|
||||
source: resolution.source.clone(),
|
||||
})?;
|
||||
display_name_hint = Some(resolved_item.title.clone());
|
||||
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::SelectArtifact,
|
||||
message: "selecting artifact".to_owned(),
|
||||
});
|
||||
let artifact = ArtifactCandidate {
|
||||
url: resolved_item.download.url.clone(),
|
||||
version: resolved_item.version.clone(),
|
||||
arch: resolved_item.download.arch.clone(),
|
||||
trusted_checksum: None,
|
||||
weak_checksum_md5: resolved_item.download.md5sum.clone(),
|
||||
selection_reason: "provider-release".to_owned(),
|
||||
};
|
||||
let strategy = UpdateStrategy {
|
||||
preferred: crate::domain::update::ChannelPreference {
|
||||
kind: crate::domain::update::UpdateChannelKind::DirectAsset,
|
||||
locator: resolved_item.download.url.clone(),
|
||||
reason: "provider-release".to_owned(),
|
||||
},
|
||||
alternates: Vec::new(),
|
||||
};
|
||||
|
||||
(resolution, artifact, strategy)
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::SelectArtifact,
|
||||
message: "selecting artifact".to_owned(),
|
||||
});
|
||||
(
|
||||
external_resolution.resolution,
|
||||
external_resolution.selected_artifact,
|
||||
external_resolution.update_strategy,
|
||||
external_resolution.display_name_hint,
|
||||
)
|
||||
} else {
|
||||
return Err(BuildAddPlanError::NoInstallableArtifact { source });
|
||||
}
|
||||
}
|
||||
SourceKind::DirectUrl => {
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
|
|
@ -274,7 +291,7 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
|
|||
alternates: Vec::new(),
|
||||
};
|
||||
|
||||
(resolution, artifact, strategy)
|
||||
(resolution, artifact, strategy, None)
|
||||
}
|
||||
SourceKind::SourceForge => {
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
|
|
@ -315,7 +332,7 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
|
|||
alternates: Vec::new(),
|
||||
};
|
||||
|
||||
(resolution, artifact, strategy)
|
||||
(resolution, artifact, strategy, None)
|
||||
}
|
||||
_ => {
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
|
|
@ -345,7 +362,7 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
|
|||
},
|
||||
alternates: Vec::new(),
|
||||
};
|
||||
(resolution, artifact, strategy)
|
||||
(resolution, artifact, strategy, None)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -359,6 +376,21 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
|
|||
})
|
||||
}
|
||||
|
||||
fn resolve_registered_external_add_provider(
|
||||
source: &crate::domain::source::SourceRef,
|
||||
providers: &ProviderRegistry<'_>,
|
||||
) -> Result<Option<ExternalAddResolution>, BuildAddPlanError> {
|
||||
for provider in &providers.external_add_providers {
|
||||
match provider.resolve(source) {
|
||||
Ok(Some(resolution)) => return Ok(Some(resolution)),
|
||||
Ok(None) => continue,
|
||||
Err(error) => return Err(BuildAddPlanError::Adapter(provider.id(), error)),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn prefer_latest_tracking(mut plan: AddPlan) -> AddPlan {
|
||||
if let Some(index) = plan
|
||||
.update_strategy
|
||||
|
|
@ -465,7 +497,7 @@ pub fn install_app_with_reporter(
|
|||
install_home,
|
||||
&policy
|
||||
.desktop_entry_root
|
||||
.join(format!("aim-{}.desktop", record.stable_id)),
|
||||
.join(format!("upm-{}.desktop", record.stable_id)),
|
||||
);
|
||||
let icon_path = resolve_target_path(
|
||||
install_home,
|
||||
|
|
@ -475,7 +507,7 @@ pub fn install_app_with_reporter(
|
|||
stage: OperationStage::DownloadArtifact,
|
||||
message: "downloading artifact".to_owned(),
|
||||
});
|
||||
let staging_root = install_home.join(".local/share/aim/staging");
|
||||
let staging_root = install_home.join(".local/share/upm/staging");
|
||||
let staged_payload_path = staged_appimage_path(&staging_root, &record.stable_id);
|
||||
let artifact_size_bytes = download_artifact_to_staged_path_with_reporter(
|
||||
&plan.selected_artifact.url,
|
||||
|
|
@ -630,7 +662,9 @@ fn download_artifact_to_staged_path_with_reporter(
|
|||
) -> Result<u64, InstallAppError> {
|
||||
let policy = http_client_policy();
|
||||
|
||||
if env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1") {
|
||||
if env::var(GLOBAL_FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
|
||||
|| env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
|
||||
{
|
||||
let bytes = b"\x7fELFAppImage\x89PNG\r\n\x1a\nicondataIEND\xaeB`\x82";
|
||||
return download_to_staged_path_with_retries(staged_payload_path, reporter, policy, || {
|
||||
Ok((
|
||||
|
|
@ -3,6 +3,7 @@ pub mod identity;
|
|||
pub mod interaction;
|
||||
pub mod list;
|
||||
pub mod progress;
|
||||
pub mod providers;
|
||||
pub mod query;
|
||||
pub mod remove;
|
||||
pub mod scope;
|
||||
24
crates/upm-core/src/app/providers.rs
Normal file
24
crates/upm-core/src/app/providers.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use crate::adapters::traits::{AdapterError, AdapterResolution};
|
||||
use crate::app::search::SearchProvider;
|
||||
use crate::domain::source::SourceRef;
|
||||
use crate::domain::update::{ArtifactCandidate, UpdateStrategy};
|
||||
|
||||
pub trait ExternalAddProvider {
|
||||
fn id(&self) -> &'static str;
|
||||
|
||||
fn resolve(&self, source: &SourceRef) -> Result<Option<ExternalAddResolution>, AdapterError>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ExternalAddResolution {
|
||||
pub resolution: AdapterResolution,
|
||||
pub selected_artifact: ArtifactCandidate,
|
||||
pub update_strategy: UpdateStrategy,
|
||||
pub display_name_hint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ProviderRegistry<'a> {
|
||||
pub search_providers: Vec<&'a dyn SearchProvider>,
|
||||
pub external_add_providers: Vec<&'a dyn ExternalAddProvider>,
|
||||
}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
use crate::app::providers::ProviderRegistry;
|
||||
use crate::domain::app::AppRecord;
|
||||
use crate::domain::search::{
|
||||
InstalledSearchMatch, SearchInstallStatus, SearchQuery, SearchResult, SearchResults,
|
||||
SearchWarning,
|
||||
};
|
||||
use crate::source::appimagehub::{
|
||||
AppImageHubSearchError, AppImageHubTransport, search_appimagehub_with,
|
||||
};
|
||||
use crate::source::github::{
|
||||
GitHubSearchError, GitHubTransport, TransportRelease, default_transport,
|
||||
search_github_repositories_with,
|
||||
|
|
@ -40,17 +38,26 @@ pub fn build_search_results(
|
|||
query: &SearchQuery,
|
||||
installed_apps: &[AppRecord],
|
||||
) -> Result<SearchResults, SearchError> {
|
||||
let github_transport = default_transport();
|
||||
let appimagehub_transport = crate::source::appimagehub::default_transport();
|
||||
let github_provider = GitHubSearchProvider::new(github_transport.as_ref());
|
||||
let appimagehub_provider = AppImageHubSearchProvider::new(appimagehub_transport.as_ref());
|
||||
build_search_results_with(
|
||||
build_search_results_with_registered_providers(
|
||||
query,
|
||||
installed_apps,
|
||||
&[&github_provider, &appimagehub_provider],
|
||||
&ProviderRegistry::default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_search_results_with_registered_providers(
|
||||
query: &SearchQuery,
|
||||
installed_apps: &[AppRecord],
|
||||
providers: &ProviderRegistry<'_>,
|
||||
) -> Result<SearchResults, SearchError> {
|
||||
let github_transport = default_transport();
|
||||
let github_provider = GitHubSearchProvider::new(github_transport.as_ref());
|
||||
let mut resolved_providers = vec![&github_provider as &dyn SearchProvider];
|
||||
resolved_providers.extend(providers.search_providers.iter().copied());
|
||||
|
||||
build_search_results_with(query, installed_apps, &resolved_providers)
|
||||
}
|
||||
|
||||
pub fn build_search_results_with(
|
||||
query: &SearchQuery,
|
||||
installed_apps: &[AppRecord],
|
||||
|
|
@ -94,58 +101,6 @@ impl<'a, T: GitHubTransport + ?Sized> GitHubSearchProvider<'a, T> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct AppImageHubSearchProvider<'a, T: AppImageHubTransport + ?Sized> {
|
||||
transport: &'a T,
|
||||
}
|
||||
|
||||
impl<'a, T: AppImageHubTransport + ?Sized> AppImageHubSearchProvider<'a, T> {
|
||||
pub fn new(transport: &'a T) -> Self {
|
||||
Self { transport }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AppImageHubTransport + ?Sized> SearchProvider for AppImageHubSearchProvider<'_, T> {
|
||||
fn search(&self, query: &SearchQuery) -> Result<Vec<SearchResult>, SearchProviderError> {
|
||||
let hits = search_appimagehub_with(&query.text, query.remote_limit, self.transport)
|
||||
.map_err(|error| {
|
||||
SearchProviderError::new("appimagehub", &render_appimagehub_search_error(&error))
|
||||
})?;
|
||||
|
||||
let normalized_query = normalize_lookup(&query.text);
|
||||
let mut ranked_hits = hits
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, hit)| {
|
||||
(
|
||||
appimagehub_remote_match_rank(
|
||||
&normalized_query,
|
||||
&hit.name,
|
||||
hit.summary.as_deref(),
|
||||
),
|
||||
index,
|
||||
hit,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
ranked_hits.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1)));
|
||||
|
||||
Ok(ranked_hits
|
||||
.into_iter()
|
||||
.map(|(_, _, hit)| SearchResult {
|
||||
provider_id: "appimagehub".to_owned(),
|
||||
display_name: hit.name,
|
||||
description: hit.summary,
|
||||
source_locator: hit.detail_page,
|
||||
install_query: format!("appimagehub/{}", hit.id),
|
||||
canonical_locator: hit.id,
|
||||
version: Some(hit.version),
|
||||
install_status: SearchInstallStatus::Available,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: GitHubTransport + ?Sized> SearchProvider for GitHubSearchProvider<'_, T> {
|
||||
fn search(&self, query: &SearchQuery) -> Result<Vec<SearchResult>, SearchProviderError> {
|
||||
let name_only_query = format!("{} in:name", query.text);
|
||||
|
|
@ -392,45 +347,3 @@ fn render_github_search_error(error: &GitHubSearchError) -> String {
|
|||
GitHubSearchError::Transport(inner) => inner.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn appimagehub_remote_match_rank(query: &str, name: &str, summary: Option<&str>) -> u8 {
|
||||
let name = normalize_lookup(name);
|
||||
let summary = summary.map(normalize_lookup);
|
||||
|
||||
if name == query {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if name.starts_with(query) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if name.contains(query) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if summary
|
||||
.as_deref()
|
||||
.map(|summary| summary.starts_with(query))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
if summary
|
||||
.as_deref()
|
||||
.map(|summary| summary.contains(query))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
|
||||
5
|
||||
}
|
||||
|
||||
fn render_appimagehub_search_error(error: &AppImageHubSearchError) -> String {
|
||||
match error {
|
||||
AppImageHubSearchError::Parse(inner) => inner.to_string(),
|
||||
AppImageHubSearchError::Transport(inner) => inner.to_string(),
|
||||
}
|
||||
}
|
||||
|
|
@ -291,7 +291,7 @@ fn stage_existing_installation(
|
|||
}
|
||||
|
||||
let stage_dir = install_home
|
||||
.join(".local/share/aim/rollback")
|
||||
.join(".local/share/upm/rollback")
|
||||
.join(&app.stable_id);
|
||||
fs::create_dir_all(&stage_dir)
|
||||
.map_err(|error| format!("failed to create rollback staging directory: {error}"))?;
|
||||
|
|
@ -11,7 +11,7 @@ pub fn managed_appimage_path(home_dir: &Path, scope: InstallScope, app_id: &str)
|
|||
}
|
||||
|
||||
pub fn desktop_entry_path(home_dir: &Path, scope: InstallScope, app_id: &str) -> PathBuf {
|
||||
scope_applications_dir(home_dir, scope).join(format!("aim-{app_id}.desktop"))
|
||||
scope_applications_dir(home_dir, scope).join(format!("upm-{app_id}.desktop"))
|
||||
}
|
||||
|
||||
pub fn icon_path(home_dir: &Path, scope: InstallScope, app_id: &str) -> PathBuf {
|
||||
|
|
@ -37,7 +37,7 @@ pub fn resolve_install_policy(
|
|||
(DistroFamily::Immutable, InstallScope::System) if capabilities.is_immutable => {
|
||||
Ok(InstallPolicy {
|
||||
scope: InstallScope::User,
|
||||
payload_root: PathBuf::from(".local/lib/aim/appimages"),
|
||||
payload_root: PathBuf::from(".local/lib/upm/appimages"),
|
||||
desktop_entry_root: PathBuf::from(".local/share/applications"),
|
||||
icon_root: PathBuf::from(".local/share/icons/hicolor/256x256/apps"),
|
||||
integration_mode: IntegrationMode::Degraded,
|
||||
|
|
@ -57,7 +57,7 @@ pub fn resolve_install_policy(
|
|||
}),
|
||||
_ => Ok(InstallPolicy {
|
||||
scope: InstallScope::User,
|
||||
payload_root: PathBuf::from(".local/lib/aim/appimages"),
|
||||
payload_root: PathBuf::from(".local/lib/upm/appimages"),
|
||||
desktop_entry_root: PathBuf::from(".local/share/applications"),
|
||||
icon_root: PathBuf::from(".local/share/icons/hicolor/256x256/apps"),
|
||||
integration_mode: if capabilities.has_desktop_session {
|
||||
|
|
@ -62,7 +62,7 @@ fn icon_theme_root(icon_path: &Path) -> PathBuf {
|
|||
}
|
||||
|
||||
fn audit_helper(helper: &Path, args: &[&Path]) {
|
||||
if env::var("AIM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") {
|
||||
if env::var("UPM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ fn audit_helper(helper: &Path, args: &[&Path]) {
|
|||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
eprintln!(
|
||||
"[aim] helper exec: {}{}{}",
|
||||
"[upm] helper exec: {}{}{}",
|
||||
helper.display(),
|
||||
if rendered_args.is_empty() { "" } else { " " },
|
||||
rendered_args
|
||||
|
|
@ -80,23 +80,23 @@ fn audit_helper(helper: &Path, args: &[&Path]) {
|
|||
}
|
||||
|
||||
fn audit_helper_status(helper: &Path, code: Option<i32>) {
|
||||
if env::var("AIM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") {
|
||||
if env::var("UPM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") {
|
||||
return;
|
||||
}
|
||||
|
||||
match code {
|
||||
Some(code) => eprintln!("[aim] helper exit: {} code={code}", helper.display()),
|
||||
Some(code) => eprintln!("[upm] helper exit: {} code={code}", helper.display()),
|
||||
None => eprintln!(
|
||||
"[aim] helper exit: {} terminated by signal",
|
||||
"[upm] helper exit: {} terminated by signal",
|
||||
helper.display()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn audit_helper_failure(helper: &Path, error: &str) {
|
||||
if env::var("AIM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") {
|
||||
if env::var("UPM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") {
|
||||
return;
|
||||
}
|
||||
|
||||
eprintln!("[aim] helper failure: {} error={error}", helper.display());
|
||||
eprintln!("[upm] helper failure: {} error={error}", helper.display());
|
||||
}
|
||||
|
|
@ -7,3 +7,5 @@ pub mod platform;
|
|||
pub mod registry;
|
||||
pub mod source;
|
||||
pub mod update;
|
||||
|
||||
pub use app::providers::{ExternalAddProvider, ExternalAddResolution, ProviderRegistry};
|
||||
|
|
@ -71,7 +71,7 @@ fn is_writable_dir(path: &Path) -> bool {
|
|||
return false;
|
||||
}
|
||||
|
||||
let probe_path = path.join(".aim-write-test");
|
||||
let probe_path = path.join(".upm-write-test");
|
||||
let result = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
|
|
@ -10,11 +10,11 @@ pub use crate::domain::app::InstallScope;
|
|||
pub use capabilities::{DesktopHelpers, HostCapabilities, WritableRoots};
|
||||
pub use distro::{DistroFamily, detect_distro_family};
|
||||
|
||||
const OS_RELEASE_PATH_ENV: &str = "AIM_OS_RELEASE_PATH";
|
||||
const HELPER_PATHS_ENV: &str = "AIM_HELPER_PATHS";
|
||||
const OS_RELEASE_PATH_ENV: &str = "UPM_OS_RELEASE_PATH";
|
||||
const HELPER_PATHS_ENV: &str = "UPM_HELPER_PATHS";
|
||||
|
||||
pub fn user_managed_appimages_dir(home_dir: &Path) -> PathBuf {
|
||||
home_dir.join(".local/lib/aim/appimages")
|
||||
home_dir.join(".local/lib/upm/appimages")
|
||||
}
|
||||
|
||||
pub fn user_applications_dir(home_dir: &Path) -> PathBuf {
|
||||
|
|
@ -26,7 +26,7 @@ pub fn user_icons_dir(home_dir: &Path) -> PathBuf {
|
|||
}
|
||||
|
||||
pub fn system_managed_appimages_dir() -> PathBuf {
|
||||
PathBuf::from("/opt/aim/appimages")
|
||||
PathBuf::from("/opt/upm/appimages")
|
||||
}
|
||||
|
||||
pub fn system_applications_dir() -> PathBuf {
|
||||
|
|
@ -4,7 +4,8 @@ use std::time::Duration;
|
|||
use crate::domain::source::SourceRef;
|
||||
|
||||
const DEFAULT_APPIMAGEHUB_API_BASE: &str = "https://api.appimagehub.com/ocs/v1/content";
|
||||
const FIXTURE_MODE_ENV: &str = "AIM_APPIMAGEHUB_FIXTURE_MODE";
|
||||
const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE";
|
||||
const FIXTURE_MODE_ENV: &str = "UPM_APPIMAGEHUB_FIXTURE_MODE";
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct AppImageHubDownload {
|
||||
|
|
@ -56,9 +57,8 @@ pub trait AppImageHubTransport {
|
|||
}
|
||||
|
||||
pub fn default_transport() -> Box<dyn AppImageHubTransport> {
|
||||
if env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
|
||||
|| env::var("AIM_GITHUB_FIXTURE_MODE").ok().as_deref() == Some("1")
|
||||
{
|
||||
if env::var(GLOBAL_FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
|
||||
|| env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1") {
|
||||
Box::new(FixtureAppImageHubTransport)
|
||||
} else {
|
||||
Box::new(ReqwestAppImageHubTransport::new())
|
||||
|
|
@ -129,7 +129,7 @@ impl ReqwestAppImageHubTransport {
|
|||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.expect("reqwest client should build"),
|
||||
api_base: env::var("AIM_APPIMAGEHUB_API_BASE")
|
||||
api_base: env::var("UPM_APPIMAGEHUB_API_BASE")
|
||||
.unwrap_or_else(|_| DEFAULT_APPIMAGEHUB_API_BASE.to_owned()),
|
||||
}
|
||||
}
|
||||
|
|
@ -424,11 +424,11 @@ fn resolved_version(item: &AppImageHubItem, download: &AppImageHubDownload) -> S
|
|||
}
|
||||
|
||||
fn fixture_item(id: &str) -> Option<AppImageHubItem> {
|
||||
let insecure_http = env::var("AIM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP")
|
||||
let insecure_http = env::var("UPM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP")
|
||||
.ok()
|
||||
.as_deref()
|
||||
== Some("1");
|
||||
let bad_md5 = env::var("AIM_APPIMAGEHUB_FIXTURE_BAD_MD5").ok().as_deref() == Some("1");
|
||||
let bad_md5 = env::var("UPM_APPIMAGEHUB_FIXTURE_BAD_MD5").ok().as_deref() == Some("1");
|
||||
|
||||
match id {
|
||||
"2338455" => Some(AppImageHubItem {
|
||||
|
|
@ -5,7 +5,8 @@ use crate::domain::source::{ResolvedRelease, SourceRef};
|
|||
use crate::metadata::MetadataDocument;
|
||||
|
||||
const DEFAULT_GITHUB_API_BASE: &str = "https://api.github.com";
|
||||
const FIXTURE_MODE_ENV: &str = "AIM_GITHUB_FIXTURE_MODE";
|
||||
const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE";
|
||||
const FIXTURE_MODE_ENV: &str = "UPM_GITHUB_FIXTURE_MODE";
|
||||
const DEFAULT_HTTP_TIMEOUT_SECS: u64 = 30;
|
||||
const DEFAULT_HTTP_MAX_RETRIES: usize = 3;
|
||||
|
||||
|
|
@ -176,7 +177,9 @@ pub fn search_github_repositories_with<T: GitHubTransport + ?Sized>(
|
|||
}
|
||||
|
||||
pub fn default_transport() -> Box<dyn GitHubTransport> {
|
||||
if env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1") {
|
||||
if env::var(GLOBAL_FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
|
||||
|| env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
|
||||
{
|
||||
Box::new(FixtureGitHubTransport)
|
||||
} else {
|
||||
Box::new(ReqwestGitHubTransport::new())
|
||||
|
|
@ -200,13 +203,13 @@ impl ReqwestGitHubTransport {
|
|||
let mut default_headers = reqwest::header::HeaderMap::new();
|
||||
default_headers.insert(
|
||||
reqwest::header::USER_AGENT,
|
||||
reqwest::header::HeaderValue::from_static("aim/0.1"),
|
||||
reqwest::header::HeaderValue::from_static("upm/0.1"),
|
||||
);
|
||||
default_headers.insert(
|
||||
reqwest::header::ACCEPT,
|
||||
reqwest::header::HeaderValue::from_static("application/vnd.github+json"),
|
||||
);
|
||||
if let Some(token) = env::var("AIM_GITHUB_TOKEN")
|
||||
if let Some(token) = env::var("UPM_GITHUB_TOKEN")
|
||||
.ok()
|
||||
.or_else(|| env::var("GITHUB_TOKEN").ok())
|
||||
&& let Ok(value) = reqwest::header::HeaderValue::from_str(&format!("Bearer {token}"))
|
||||
|
|
@ -220,7 +223,7 @@ impl ReqwestGitHubTransport {
|
|||
.timeout(policy.timeout)
|
||||
.build()
|
||||
.expect("reqwest client should build"),
|
||||
api_base: env::var("AIM_GITHUB_API_BASE")
|
||||
api_base: env::var("UPM_GITHUB_API_BASE")
|
||||
.unwrap_or_else(|_| DEFAULT_GITHUB_API_BASE.to_owned()),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,2 @@
|
|||
pub mod appimagehub;
|
||||
pub mod github;
|
||||
pub mod input;
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
use aim_core::adapters::appimagehub::AppImageHubAdapter;
|
||||
use aim_core::adapters::direct_url::DirectUrlAdapter;
|
||||
use aim_core::adapters::github::GitHubAdapter;
|
||||
use aim_core::adapters::gitlab::GitLabAdapter;
|
||||
use aim_core::adapters::sourceforge::SourceForgeAdapter;
|
||||
use aim_core::adapters::traits::{
|
||||
use upm_core::adapters::direct_url::DirectUrlAdapter;
|
||||
use upm_core::adapters::github::GitHubAdapter;
|
||||
use upm_core::adapters::gitlab::GitLabAdapter;
|
||||
use upm_core::adapters::sourceforge::SourceForgeAdapter;
|
||||
use upm_core::adapters::traits::{
|
||||
AdapterCapabilities, AdapterError, AdapterResolution, AdapterResolveOutcome, SourceAdapter,
|
||||
};
|
||||
use aim_core::app::query::resolve_query;
|
||||
use aim_core::domain::source::{
|
||||
use upm_core::app::query::resolve_query;
|
||||
use upm_core::domain::source::{
|
||||
NormalizedSourceKind, ResolvedRelease, SourceInputKind, SourceKind, SourceRef,
|
||||
};
|
||||
use aim_core::source::appimagehub::FixtureAppImageHubTransport;
|
||||
|
||||
struct FileArtifactAdapter;
|
||||
|
||||
|
|
@ -61,60 +59,6 @@ fn adapter_capabilities_can_report_exact_resolution_only() {
|
|||
assert!(!capabilities.supports_search);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appimagehub_adapter_reports_search_and_exact_resolution_capabilities() {
|
||||
let adapter = AppImageHubAdapter;
|
||||
|
||||
assert_eq!(adapter.id(), "appimagehub");
|
||||
assert_eq!(
|
||||
adapter.repository_source_kind(),
|
||||
Some(SourceKind::AppImageHub)
|
||||
);
|
||||
assert_eq!(adapter.exact_source_kind(), None);
|
||||
assert_eq!(
|
||||
adapter.capabilities(),
|
||||
AdapterCapabilities {
|
||||
supports_search: true,
|
||||
supports_exact_resolution: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appimagehub_adapter_resolves_installable_items_through_fixture_transport() {
|
||||
let adapter = AppImageHubAdapter;
|
||||
let source = resolve_query("appimagehub/2338455").unwrap();
|
||||
|
||||
let resolution = adapter
|
||||
.resolve_source_with(&source, &FixtureAppImageHubTransport)
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
resolution,
|
||||
AdapterResolveOutcome::Resolved(AdapterResolution {
|
||||
source,
|
||||
release: ResolvedRelease { version, .. },
|
||||
}) if source.kind == SourceKind::AppImageHub
|
||||
&& source.canonical_locator.as_deref() == Some("2338455")
|
||||
&& version == "latest"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn appimagehub_adapter_reports_no_installable_artifact_for_non_appimage_items() {
|
||||
let adapter = AppImageHubAdapter;
|
||||
let source = resolve_query("appimagehub/2337998").unwrap();
|
||||
|
||||
let resolution = adapter
|
||||
.resolve_source_with(&source, &FixtureAppImageHubTransport)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
resolution,
|
||||
AdapterResolveOutcome::NoInstallableArtifact { source }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repository_backed_resolvers_accept_only_their_own_source_kind() {
|
||||
let github_source = resolve_query("sharkdp/bat").unwrap();
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
use aim_core::adapters::all_adapter_kinds;
|
||||
use upm_core::adapters::all_adapter_kinds;
|
||||
|
||||
#[test]
|
||||
fn all_expected_adapter_kinds_are_registered() {
|
||||
let kinds = all_adapter_kinds();
|
||||
|
||||
assert!(kinds.contains(&"appimagehub"));
|
||||
assert!(kinds.contains(&"github"));
|
||||
assert!(kinds.contains(&"gitlab"));
|
||||
assert!(kinds.contains(&"direct-url"));
|
||||
assert!(kinds.contains(&"zsync"));
|
||||
assert!(kinds.contains(&"sourceforge"));
|
||||
assert!(!kinds.contains(&"appimagehub"));
|
||||
assert!(!kinds.contains(&"custom-json"));
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
use std::fs;
|
||||
|
||||
use aim_core::integration::install::{InstallRequest, PayloadInstallError, execute_install};
|
||||
use aim_core::platform::DesktopHelpers;
|
||||
use tempfile::tempdir;
|
||||
use upm_core::integration::install::{InstallRequest, PayloadInstallError, execute_install};
|
||||
use upm_core::platform::DesktopHelpers;
|
||||
|
||||
const VALID_FIXTURE_SHA512: &str =
|
||||
"ZZma4ZD+9XB4GGTHCNZu8I92OY02YrEvIG89ZtRNi99W8SZKwWkmGZz/QyNBxqAt0XeiKtcR80/dMnKlwpcIWw==";
|
||||
|
|
@ -2,15 +2,15 @@ use std::fs;
|
|||
use std::io::{self, Cursor, Read};
|
||||
use std::time::Duration;
|
||||
|
||||
use aim_core::app::add::{
|
||||
use tempfile::tempdir;
|
||||
use upm_core::app::add::{
|
||||
InstallAppError, download_to_staged_path_with_retries,
|
||||
stream_payload_to_staged_file_with_reporter,
|
||||
};
|
||||
use aim_core::app::progress::{NoopReporter, OperationEvent};
|
||||
use aim_core::integration::install::{InstallRequest, execute_install};
|
||||
use aim_core::platform::DesktopHelpers;
|
||||
use aim_core::source::github::HttpClientPolicy;
|
||||
use tempfile::tempdir;
|
||||
use upm_core::app::progress::{NoopReporter, OperationEvent};
|
||||
use upm_core::integration::install::{InstallRequest, execute_install};
|
||||
use upm_core::platform::DesktopHelpers;
|
||||
use upm_core::source::github::HttpClientPolicy;
|
||||
|
||||
#[test]
|
||||
fn payload_streaming_writes_staged_file_and_reports_progress() {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use aim_core::app::add::{build_add_plan_with, materialize_app_record, prefer_latest_tracking};
|
||||
use aim_core::app::query::resolve_query;
|
||||
use aim_core::source::github::FixtureGitHubTransport;
|
||||
use upm_core::app::add::{build_add_plan_with, materialize_app_record, prefer_latest_tracking};
|
||||
use upm_core::app::query::resolve_query;
|
||||
use upm_core::source::github::FixtureGitHubTransport;
|
||||
|
||||
#[test]
|
||||
fn github_adapter_can_normalize_owner_repo_source() {
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
use aim_core::app::query::resolve_query;
|
||||
use aim_core::source::github::{
|
||||
use std::time::Duration;
|
||||
use upm_core::app::query::resolve_query;
|
||||
use upm_core::source::github::{
|
||||
FixtureGitHubTransport, discover_github_candidates_with, http_client_policy,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn discovery_reports_appimage_assets_and_latest_linux_yml() {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use aim_core::app::identity::{IdentityFallback, resolve_identity};
|
||||
use aim_core::domain::app::IdentityConfidence;
|
||||
use upm_core::app::identity::{IdentityFallback, resolve_identity};
|
||||
use upm_core::domain::app::IdentityConfidence;
|
||||
|
||||
#[test]
|
||||
fn unresolved_identity_can_fall_back_to_url() {
|
||||
|
|
@ -42,6 +42,6 @@ fn identifiers_containing_dot_dot_are_rejected() {
|
|||
|
||||
assert_eq!(
|
||||
error,
|
||||
aim_core::app::identity::ResolveIdentityError::InvalidStableId
|
||||
upm_core::app::identity::ResolveIdentityError::InvalidStableId
|
||||
);
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
use aim_core::app::add::{BuildAddPlanError, build_add_plan_with};
|
||||
use aim_core::app::query::ResolveQueryError;
|
||||
use aim_core::app::update::execute_updates;
|
||||
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
use aim_core::domain::source::SourceKind;
|
||||
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceRef};
|
||||
use aim_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install};
|
||||
use aim_core::platform::DesktopHelpers;
|
||||
use aim_core::source::github::FixtureGitHubTransport;
|
||||
use std::fs;
|
||||
use std::sync::Mutex;
|
||||
use tempfile::tempdir;
|
||||
use upm_core::app::add::{BuildAddPlanError, build_add_plan_with};
|
||||
use upm_core::app::query::ResolveQueryError;
|
||||
use upm_core::app::update::execute_updates;
|
||||
use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
use upm_core::domain::source::SourceKind;
|
||||
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceRef};
|
||||
use upm_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install};
|
||||
use upm_core::platform::DesktopHelpers;
|
||||
use upm_core::source::github::FixtureGitHubTransport;
|
||||
|
||||
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ fn integration_failure_removes_new_payload_and_generated_files() {
|
|||
fs::write(&staged_path, b"\x7fELFAppImage").unwrap();
|
||||
|
||||
let final_payload_path = payload_root.join("bat.AppImage");
|
||||
let desktop_entry_path = blocking_path.join("aim-bat.desktop");
|
||||
let desktop_entry_path = blocking_path.join("upm-bat.desktop");
|
||||
let error = execute_install(&InstallRequest {
|
||||
staged_payload_path: &staged_path,
|
||||
final_payload_path: &final_payload_path,
|
||||
|
|
@ -85,13 +85,13 @@ fn failed_update_restores_tracked_desktop_and_icon_files() {
|
|||
let root = tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("DISPLAY", ":99");
|
||||
std::env::set_var("XDG_CURRENT_DESKTOP", "test");
|
||||
}
|
||||
|
||||
let payload_path = root.path().join("tracked/team-app.AppImage");
|
||||
let desktop_path = root.path().join("tracked/aim-team-app.desktop");
|
||||
let desktop_path = root.path().join("tracked/upm-team-app.desktop");
|
||||
let icon_path = root.path().join("tracked/team-app.png");
|
||||
fs::create_dir_all(payload_path.parent().unwrap()).unwrap();
|
||||
fs::write(&payload_path, b"previous-payload").unwrap();
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
use aim_core::app::add::{build_add_plan_with_reporter, install_app_with_reporter};
|
||||
use aim_core::app::progress::{OperationEvent, OperationStage};
|
||||
use aim_core::domain::app::InstallScope;
|
||||
use aim_core::domain::source::{NormalizedSourceKind, SourceKind};
|
||||
use aim_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install};
|
||||
use aim_core::platform::DesktopHelpers;
|
||||
use aim_core::source::github::FixtureGitHubTransport;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use tempfile::tempdir;
|
||||
use upm_core::app::add::{build_add_plan_with_reporter, install_app_with_reporter};
|
||||
use upm_core::app::progress::{OperationEvent, OperationStage};
|
||||
use upm_core::domain::app::InstallScope;
|
||||
use upm_core::domain::source::{NormalizedSourceKind, SourceKind};
|
||||
use upm_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install};
|
||||
use upm_core::platform::DesktopHelpers;
|
||||
use upm_core::source::github::FixtureGitHubTransport;
|
||||
|
||||
fn write_staged_payload(root: &std::path::Path, name: &str, bytes: &[u8]) -> std::path::PathBuf {
|
||||
let staged_path = root.join("staging").join(format!("{name}.download"));
|
||||
|
|
@ -32,7 +32,7 @@ fn install_writes_desktop_entry_and_reports_refresh_warning_only() {
|
|||
trusted_checksum: None,
|
||||
weak_checksum_md5: None,
|
||||
desktop: Some(DesktopIntegrationRequest {
|
||||
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
|
||||
desktop_entry_path: &desktop_root.join("upm-bat.desktop"),
|
||||
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
|
||||
icon_path: None,
|
||||
icon_bytes: None,
|
||||
|
|
@ -86,7 +86,7 @@ fn install_executes_refresh_helpers_when_available() {
|
|||
trusted_checksum: None,
|
||||
weak_checksum_md5: None,
|
||||
desktop: Some(DesktopIntegrationRequest {
|
||||
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
|
||||
desktop_entry_path: &desktop_root.join("upm-bat.desktop"),
|
||||
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
|
||||
icon_path: Some(&icon_root.join("bat.png")),
|
||||
icon_bytes: None,
|
||||
|
|
@ -128,7 +128,7 @@ fn install_extracts_icon_from_appimage_payload_when_icon_path_is_requested() {
|
|||
trusted_checksum: None,
|
||||
weak_checksum_md5: None,
|
||||
desktop: Some(DesktopIntegrationRequest {
|
||||
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
|
||||
desktop_entry_path: &desktop_root.join("upm-bat.desktop"),
|
||||
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
|
||||
icon_path: Some(&icon_root.join("bat.png")),
|
||||
icon_bytes: None,
|
||||
|
|
@ -152,7 +152,7 @@ fn install_app_reports_operation_stages_in_order() {
|
|||
let mut events: Vec<OperationEvent> = Vec::new();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
}
|
||||
|
||||
let mut reporter = |event: &OperationEvent| events.push(event.clone());
|
||||
|
|
@ -249,7 +249,7 @@ fn install_app_sanitizes_desktop_entry_display_names() {
|
|||
let mut reporter = Vec::new();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
}
|
||||
|
||||
let mut capture = |event: &OperationEvent| reporter.push(event.clone());
|
||||
|
|
@ -349,7 +349,7 @@ fn gitlab_install_preserves_truthful_gitlab_origin() {
|
|||
let root = tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
}
|
||||
|
||||
let mut reporter = |_event: &OperationEvent| {};
|
||||
|
|
@ -399,7 +399,7 @@ fn direct_url_install_preserves_truthful_direct_url_origin() {
|
|||
let root = tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
}
|
||||
|
||||
let mut reporter = |_event: &OperationEvent| {};
|
||||
|
|
@ -484,7 +484,7 @@ fn sourceforge_latest_download_install_preserves_truthful_origin() {
|
|||
let root = tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
}
|
||||
|
||||
let mut reporter = |_event: &OperationEvent| {};
|
||||
|
|
@ -514,7 +514,7 @@ fn sourceforge_release_folder_install_preserves_truthful_origin() {
|
|||
let root = tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
}
|
||||
|
||||
let mut reporter = |_event: &OperationEvent| {};
|
||||
|
|
@ -577,7 +577,7 @@ fn sourceforge_file_like_release_download_install_preserves_input_but_stores_rel
|
|||
let root = tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
}
|
||||
|
||||
let mut reporter = |_event: &OperationEvent| {};
|
||||
28
crates/upm-core/tests/install_paths.rs
Normal file
28
crates/upm-core/tests/install_paths.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use std::path::Path;
|
||||
|
||||
use upm_core::domain::app::InstallScope;
|
||||
use upm_core::integration::paths::{desktop_entry_path, managed_appimage_path};
|
||||
|
||||
#[test]
|
||||
fn user_scope_path_lands_under_home_managed_dir() {
|
||||
let path = managed_appimage_path(Path::new("/home/test"), InstallScope::User, "bat");
|
||||
|
||||
assert_eq!(
|
||||
path,
|
||||
Path::new("/home/test/.local/lib/upm/appimages/bat.AppImage")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_scope_path_lands_under_opt_upm_dir() {
|
||||
let path = managed_appimage_path(Path::new("/home/test"), InstallScope::System, "bat");
|
||||
|
||||
assert_eq!(path, Path::new("/opt/upm/appimages/bat.AppImage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_scope_desktop_entry_uses_upm_prefix() {
|
||||
let path = desktop_entry_path(Path::new("/home/test"), InstallScope::System, "bat");
|
||||
|
||||
assert_eq!(path, Path::new("/usr/share/applications/upm-bat.desktop"));
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use aim_core::integration::install::stage_and_commit_payload;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use tempfile::tempdir;
|
||||
use upm_core::integration::install::stage_and_commit_payload;
|
||||
|
||||
#[test]
|
||||
fn payload_commit_moves_staged_appimage_into_final_location() {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use aim_core::integration::policy::{IntegrationMode, resolve_install_policy};
|
||||
use aim_core::platform::{DistroFamily, HostCapabilities, InstallScope};
|
||||
use std::path::Path;
|
||||
use upm_core::integration::policy::{IntegrationMode, resolve_install_policy};
|
||||
use upm_core::platform::{DistroFamily, HostCapabilities, InstallScope};
|
||||
|
||||
#[test]
|
||||
fn immutable_system_request_downgrades_to_user_when_allowed() {
|
||||
|
|
@ -36,7 +36,7 @@ fn system_policy_uses_managed_payload_and_native_integration_roots() {
|
|||
.unwrap();
|
||||
|
||||
assert_eq!(policy.scope, InstallScope::System);
|
||||
assert_eq!(policy.payload_root, Path::new("/opt/aim/appimages"));
|
||||
assert_eq!(policy.payload_root, Path::new("/opt/upm/appimages"));
|
||||
assert_eq!(
|
||||
policy.desktop_entry_root,
|
||||
Path::new("/usr/share/applications")
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use aim_core::app::scope::{ScopeOverride, resolve_install_scope};
|
||||
use aim_core::domain::app::InstallScope;
|
||||
use upm_core::app::scope::{ScopeOverride, resolve_install_scope};
|
||||
use upm_core::domain::app::InstallScope;
|
||||
|
||||
#[test]
|
||||
fn explicit_scope_override_beats_effective_user() {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use aim_core::domain::update::ParsedMetadataKind;
|
||||
use aim_core::metadata::{MetadataDocument, parse_document};
|
||||
use upm_core::domain::update::ParsedMetadataKind;
|
||||
use upm_core::metadata::{MetadataDocument, parse_document};
|
||||
|
||||
#[test]
|
||||
fn unknown_document_returns_typed_warning_not_panic() {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use aim_core::domain::update::ParsedMetadataKind;
|
||||
use aim_core::metadata::{MetadataDocument, parse_document};
|
||||
use upm_core::domain::update::ParsedMetadataKind;
|
||||
use upm_core::metadata::{MetadataDocument, parse_document};
|
||||
|
||||
#[test]
|
||||
fn parses_latest_linux_yml_into_download_hints() {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use aim_core::domain::update::ParsedMetadataKind;
|
||||
use aim_core::metadata::{MetadataDocument, parse_document};
|
||||
use upm_core::domain::update::ParsedMetadataKind;
|
||||
use upm_core::metadata::{MetadataDocument, parse_document};
|
||||
|
||||
#[test]
|
||||
fn parses_zsync_document_into_channel_hints() {
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
use aim_core::platform::capabilities::{probe_desktop_helpers, probe_writable_roots};
|
||||
use aim_core::platform::distro::{DistroFamily, detect_distro_family};
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use tempfile::tempdir;
|
||||
use upm_core::platform::capabilities::{probe_desktop_helpers, probe_writable_roots};
|
||||
use upm_core::platform::distro::{DistroFamily, detect_distro_family};
|
||||
|
||||
#[test]
|
||||
fn detects_fedora_family_from_os_release() {
|
||||
159
crates/upm-core/tests/provider_registry.rs
Normal file
159
crates/upm-core/tests/provider_registry.rs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
use upm_core::app::add::{AddSecurityPolicy, build_add_plan_with_registered_providers};
|
||||
use upm_core::app::providers::{ExternalAddProvider, ExternalAddResolution, ProviderRegistry};
|
||||
use upm_core::app::search::{SearchProvider, build_search_results_with_registered_providers};
|
||||
use upm_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult};
|
||||
use upm_core::domain::source::{
|
||||
NormalizedSourceKind, ResolvedRelease, SourceInputKind, SourceKind, SourceRef,
|
||||
};
|
||||
use upm_core::domain::update::{
|
||||
ArtifactCandidate, ChannelPreference, UpdateChannelKind, UpdateStrategy,
|
||||
};
|
||||
use upm_core::source::github::FixtureGitHubTransport;
|
||||
|
||||
struct StubSearchProvider;
|
||||
|
||||
impl SearchProvider for StubSearchProvider {
|
||||
fn search(
|
||||
&self,
|
||||
_query: &SearchQuery,
|
||||
) -> Result<Vec<SearchResult>, upm_core::app::search::SearchProviderError> {
|
||||
Ok(vec![SearchResult {
|
||||
provider_id: "external-search".to_owned(),
|
||||
display_name: "Firefox Nightly".to_owned(),
|
||||
description: Some("Provided by external registry".to_owned()),
|
||||
source_locator: "https://example.invalid/firefox-nightly".to_owned(),
|
||||
install_query: "external/firefox-nightly".to_owned(),
|
||||
canonical_locator: "external/firefox-nightly".to_owned(),
|
||||
version: Some("2026.03.21".to_owned()),
|
||||
install_status: SearchInstallStatus::Available,
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
struct StubExternalAddProvider;
|
||||
|
||||
impl ExternalAddProvider for StubExternalAddProvider {
|
||||
fn id(&self) -> &'static str {
|
||||
"stub-appimage"
|
||||
}
|
||||
|
||||
fn resolve(
|
||||
&self,
|
||||
source: &SourceRef,
|
||||
) -> Result<Option<ExternalAddResolution>, upm_core::adapters::traits::AdapterError> {
|
||||
Ok(
|
||||
(source.kind == SourceKind::AppImageHub).then(|| ExternalAddResolution {
|
||||
resolution: upm_core::adapters::traits::AdapterResolution {
|
||||
source: SourceRef {
|
||||
kind: SourceKind::AppImageHub,
|
||||
locator: source.locator.clone(),
|
||||
input_kind: SourceInputKind::AppImageHubShorthand,
|
||||
normalized_kind: NormalizedSourceKind::AppImageHub,
|
||||
canonical_locator: Some("2338455".to_owned()),
|
||||
requested_tag: None,
|
||||
requested_asset_name: None,
|
||||
tracks_latest: true,
|
||||
},
|
||||
release: ResolvedRelease {
|
||||
version: "stable".to_owned(),
|
||||
prerelease: false,
|
||||
},
|
||||
},
|
||||
selected_artifact: ArtifactCandidate {
|
||||
url: "https://downloads.example.invalid/firefox.AppImage".to_owned(),
|
||||
version: "stable".to_owned(),
|
||||
arch: Some("x86_64".to_owned()),
|
||||
trusted_checksum: None,
|
||||
weak_checksum_md5: Some("deadbeef".to_owned()),
|
||||
selection_reason: "provider-release".to_owned(),
|
||||
},
|
||||
update_strategy: UpdateStrategy {
|
||||
preferred: ChannelPreference {
|
||||
kind: UpdateChannelKind::DirectAsset,
|
||||
locator: "https://downloads.example.invalid/firefox.AppImage".to_owned(),
|
||||
reason: "provider-release".to_owned(),
|
||||
},
|
||||
alternates: Vec::new(),
|
||||
},
|
||||
display_name_hint: Some(
|
||||
"Firefox by Mozilla - Official AppImage Edition".to_owned(),
|
||||
),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_search_results_with_registered_providers_uses_external_hits() {
|
||||
let query = SearchQuery::new("firefox");
|
||||
let search_provider = StubSearchProvider;
|
||||
let providers = ProviderRegistry {
|
||||
search_providers: vec![&search_provider],
|
||||
external_add_providers: Vec::new(),
|
||||
};
|
||||
|
||||
let results = build_search_results_with_registered_providers(&query, &[], &providers).unwrap();
|
||||
|
||||
let external_hit = results
|
||||
.remote_hits
|
||||
.iter()
|
||||
.find(|hit| hit.provider_id == "external-search")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(external_hit.install_query, "external/firefox-nightly");
|
||||
assert!(
|
||||
results
|
||||
.remote_hits
|
||||
.iter()
|
||||
.all(|hit| hit.provider_id != "appimagehub")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_add_plan_with_registered_providers_requires_external_provider_for_appimagehub() {
|
||||
let registry = ProviderRegistry::default();
|
||||
|
||||
let error = build_add_plan_with_registered_providers(
|
||||
"appimagehub/2338455",
|
||||
&FixtureGitHubTransport,
|
||||
®istry,
|
||||
AddSecurityPolicy::default(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
upm_core::app::add::BuildAddPlanError::NoInstallableArtifact { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_add_plan_with_registered_providers_delegates_appimagehub_like_sources() {
|
||||
let provider = StubExternalAddProvider;
|
||||
let registry = ProviderRegistry {
|
||||
search_providers: Vec::new(),
|
||||
external_add_providers: vec![&provider],
|
||||
};
|
||||
|
||||
let plan = build_add_plan_with_registered_providers(
|
||||
"appimagehub/2338455",
|
||||
&FixtureGitHubTransport,
|
||||
®istry,
|
||||
AddSecurityPolicy::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(plan.resolution.source.kind, SourceKind::AppImageHub);
|
||||
assert_eq!(
|
||||
plan.resolution.source.canonical_locator.as_deref(),
|
||||
Some("2338455")
|
||||
);
|
||||
assert_eq!(
|
||||
plan.selected_artifact.url,
|
||||
"https://downloads.example.invalid/firefox.AppImage"
|
||||
);
|
||||
assert_eq!(
|
||||
plan.display_name_hint.as_deref(),
|
||||
Some("Firefox by Mozilla - Official AppImage Edition")
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use aim_core::app::query::resolve_query;
|
||||
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind};
|
||||
use upm_core::app::query::resolve_query;
|
||||
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind};
|
||||
|
||||
#[test]
|
||||
fn owner_repo_defaults_to_github() {
|
||||
|
|
@ -233,21 +233,21 @@ fn classifies_single_segment_sourceforge_release_download_with_query_as_candidat
|
|||
fn rejects_malformed_gitlab_url() {
|
||||
let error = resolve_query("https://gitlab.com/example").unwrap_err();
|
||||
|
||||
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
|
||||
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unsupported_gitlab_url_shape() {
|
||||
let error = resolve_query("https://gitlab.com/example/team-app/-/issues").unwrap_err();
|
||||
|
||||
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
|
||||
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unsupported_gitlab_nested_resource_url() {
|
||||
let error = resolve_query("https://gitlab.com/example/team-app/issues").unwrap_err();
|
||||
|
||||
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
|
||||
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -255,14 +255,14 @@ fn rejects_unsupported_gitlab_release_permalink_url() {
|
|||
let error = resolve_query("https://gitlab.com/example/team-app/-/releases/permalink/latest")
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
|
||||
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unsupported_gitlab_issue_detail_url() {
|
||||
let error = resolve_query("https://gitlab.com/example/team-app/issues/1").unwrap_err();
|
||||
|
||||
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
|
||||
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -270,7 +270,7 @@ fn rejects_unsupported_gitlab_blob_url() {
|
|||
let error =
|
||||
resolve_query("https://gitlab.com/example/team-app/blob/main/README.md").unwrap_err();
|
||||
|
||||
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
|
||||
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -291,28 +291,28 @@ fn classifies_ambiguous_gitlab_deep_reserved_segment_as_candidate() {
|
|||
fn rejects_unsupported_gitlab_packages_url() {
|
||||
let error = resolve_query("https://gitlab.com/example/team-app/packages").unwrap_err();
|
||||
|
||||
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
|
||||
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_malformed_sourceforge_url() {
|
||||
let error = resolve_query("https://sourceforge.net/projects/").unwrap_err();
|
||||
|
||||
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
|
||||
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_malformed_appimagehub_shorthand() {
|
||||
let error = resolve_query("appimagehub/firefox").unwrap_err();
|
||||
|
||||
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
|
||||
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unsupported_sourceforge_url_shape() {
|
||||
let error = resolve_query("https://sourceforge.net/projects/team-app/rss").unwrap_err();
|
||||
|
||||
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
|
||||
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -335,7 +335,7 @@ fn rejects_unsupported_sourceforge_folder_download_shape() {
|
|||
let error = resolve_query("https://sourceforge.net/projects/team-app/files/releases/download")
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
|
||||
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use aim_core::registry::store::RegistryStore;
|
||||
use tempfile::tempdir;
|
||||
use upm_core::registry::store::RegistryStore;
|
||||
|
||||
#[test]
|
||||
fn registry_round_trips_app_records() {
|
||||
|
|
@ -13,28 +13,28 @@ fn registry_round_trips_app_records() {
|
|||
fn registry_round_trips_update_strategy_and_alternates() {
|
||||
let dir = tempdir().unwrap();
|
||||
let store = RegistryStore::new(dir.path().join("registry.toml"));
|
||||
let registry = aim_core::registry::model::Registry {
|
||||
let registry = upm_core::registry::model::Registry {
|
||||
version: 1,
|
||||
apps: vec![aim_core::domain::app::AppRecord {
|
||||
apps: vec![upm_core::domain::app::AppRecord {
|
||||
stable_id: "t3code".to_owned(),
|
||||
display_name: "T3 Code".to_owned(),
|
||||
source_input: Some("pingdotgg/t3code".to_owned()),
|
||||
source: None,
|
||||
installed_version: Some("0.0.11".to_owned()),
|
||||
update_strategy: Some(aim_core::domain::update::UpdateStrategy {
|
||||
preferred: aim_core::domain::update::ChannelPreference {
|
||||
kind: aim_core::domain::update::UpdateChannelKind::DirectAsset,
|
||||
update_strategy: Some(upm_core::domain::update::UpdateStrategy {
|
||||
preferred: upm_core::domain::update::ChannelPreference {
|
||||
kind: upm_core::domain::update::UpdateChannelKind::DirectAsset,
|
||||
locator: "https://example.test/app.AppImage".to_owned(),
|
||||
reason: "install-origin-match".to_owned(),
|
||||
},
|
||||
alternates: vec![
|
||||
aim_core::domain::update::ChannelPreference {
|
||||
kind: aim_core::domain::update::UpdateChannelKind::GitHubReleases,
|
||||
upm_core::domain::update::ChannelPreference {
|
||||
kind: upm_core::domain::update::UpdateChannelKind::GitHubReleases,
|
||||
locator: "pingdotgg/t3code".to_owned(),
|
||||
reason: "heuristic-match".to_owned(),
|
||||
},
|
||||
aim_core::domain::update::ChannelPreference {
|
||||
kind: aim_core::domain::update::UpdateChannelKind::ElectronBuilder,
|
||||
upm_core::domain::update::ChannelPreference {
|
||||
kind: upm_core::domain::update::UpdateChannelKind::ElectronBuilder,
|
||||
locator: "https://example.test/latest-linux.yml".to_owned(),
|
||||
reason: "metadata-guided".to_owned(),
|
||||
},
|
||||
|
|
@ -57,9 +57,9 @@ fn registry_round_trips_update_strategy_and_alternates() {
|
|||
fn registry_round_trips_install_metadata() {
|
||||
let dir = tempdir().unwrap();
|
||||
let store = RegistryStore::new(dir.path().join("registry.toml"));
|
||||
let registry = aim_core::registry::model::Registry {
|
||||
let registry = upm_core::registry::model::Registry {
|
||||
version: 1,
|
||||
apps: vec![aim_core::domain::app::AppRecord {
|
||||
apps: vec![upm_core::domain::app::AppRecord {
|
||||
stable_id: "t3code".to_owned(),
|
||||
display_name: "T3 Code".to_owned(),
|
||||
source_input: Some("pingdotgg/t3code".to_owned()),
|
||||
|
|
@ -67,13 +67,13 @@ fn registry_round_trips_install_metadata() {
|
|||
installed_version: Some("0.0.11".to_owned()),
|
||||
update_strategy: None,
|
||||
metadata: Vec::new(),
|
||||
install: Some(aim_core::domain::app::InstallMetadata {
|
||||
scope: aim_core::domain::app::InstallScope::User,
|
||||
install: Some(upm_core::domain::app::InstallMetadata {
|
||||
scope: upm_core::domain::app::InstallScope::User,
|
||||
payload_path: Some(
|
||||
"/tmp/install-home/.local/lib/aim/appimages/t3code.AppImage".to_owned(),
|
||||
"/tmp/install-home/.local/lib/upm/appimages/t3code.AppImage".to_owned(),
|
||||
),
|
||||
desktop_entry_path: Some(
|
||||
"/tmp/install-home/.local/share/applications/aim-t3code.desktop".to_owned(),
|
||||
"/tmp/install-home/.local/share/applications/upm-t3code.desktop".to_owned(),
|
||||
),
|
||||
icon_path: Some(
|
||||
"/tmp/install-home/.local/share/icons/hicolor/256x256/apps/t3code.png"
|
||||
|
|
@ -87,14 +87,14 @@ fn registry_round_trips_install_metadata() {
|
|||
let loaded = store.load().unwrap();
|
||||
|
||||
let install = loaded.apps[0].install.as_ref().unwrap();
|
||||
assert_eq!(install.scope, aim_core::domain::app::InstallScope::User);
|
||||
assert_eq!(install.scope, upm_core::domain::app::InstallScope::User);
|
||||
assert_eq!(
|
||||
install.payload_path.as_deref(),
|
||||
Some("/tmp/install-home/.local/lib/aim/appimages/t3code.AppImage")
|
||||
Some("/tmp/install-home/.local/lib/upm/appimages/t3code.AppImage")
|
||||
);
|
||||
assert_eq!(
|
||||
install.desktop_entry_path.as_deref(),
|
||||
Some("/tmp/install-home/.local/share/applications/aim-t3code.desktop")
|
||||
Some("/tmp/install-home/.local/share/applications/upm-t3code.desktop")
|
||||
);
|
||||
assert_eq!(
|
||||
install.icon_path.as_deref(),
|
||||
|
|
@ -106,18 +106,18 @@ fn registry_round_trips_install_metadata() {
|
|||
fn registry_round_trips_source_identity_for_new_provider_kinds() {
|
||||
let dir = tempdir().unwrap();
|
||||
let store = RegistryStore::new(dir.path().join("registry.toml"));
|
||||
let registry = aim_core::registry::model::Registry {
|
||||
let registry = upm_core::registry::model::Registry {
|
||||
version: 1,
|
||||
apps: vec![
|
||||
aim_core::domain::app::AppRecord {
|
||||
upm_core::domain::app::AppRecord {
|
||||
stable_id: "example-team-app".to_owned(),
|
||||
display_name: "team-app".to_owned(),
|
||||
source_input: Some("https://gitlab.com/example/team-app".to_owned()),
|
||||
source: Some(aim_core::domain::source::SourceRef {
|
||||
kind: aim_core::domain::source::SourceKind::GitLab,
|
||||
source: Some(upm_core::domain::source::SourceRef {
|
||||
kind: upm_core::domain::source::SourceKind::GitLab,
|
||||
locator: "https://gitlab.com/example/team-app".to_owned(),
|
||||
input_kind: aim_core::domain::source::SourceInputKind::GitLabUrl,
|
||||
normalized_kind: aim_core::domain::source::NormalizedSourceKind::GitLab,
|
||||
input_kind: upm_core::domain::source::SourceInputKind::GitLabUrl,
|
||||
normalized_kind: upm_core::domain::source::NormalizedSourceKind::GitLab,
|
||||
canonical_locator: Some("example/team-app".to_owned()),
|
||||
requested_tag: None,
|
||||
requested_asset_name: None,
|
||||
|
|
@ -128,18 +128,18 @@ fn registry_round_trips_source_identity_for_new_provider_kinds() {
|
|||
metadata: Vec::new(),
|
||||
install: None,
|
||||
},
|
||||
aim_core::domain::app::AppRecord {
|
||||
upm_core::domain::app::AppRecord {
|
||||
stable_id: "team-app".to_owned(),
|
||||
display_name: "team-app".to_owned(),
|
||||
source_input: Some(
|
||||
"https://sourceforge.net/projects/team-app/files/latest/download".to_owned(),
|
||||
),
|
||||
source: Some(aim_core::domain::source::SourceRef {
|
||||
kind: aim_core::domain::source::SourceKind::SourceForge,
|
||||
source: Some(upm_core::domain::source::SourceRef {
|
||||
kind: upm_core::domain::source::SourceKind::SourceForge,
|
||||
locator: "https://sourceforge.net/projects/team-app/files/latest/download"
|
||||
.to_owned(),
|
||||
input_kind: aim_core::domain::source::SourceInputKind::SourceForgeUrl,
|
||||
normalized_kind: aim_core::domain::source::NormalizedSourceKind::SourceForge,
|
||||
input_kind: upm_core::domain::source::SourceInputKind::SourceForgeUrl,
|
||||
normalized_kind: upm_core::domain::source::NormalizedSourceKind::SourceForge,
|
||||
canonical_locator: Some("team-app".to_owned()),
|
||||
requested_tag: None,
|
||||
requested_asset_name: None,
|
||||
|
|
@ -150,15 +150,15 @@ fn registry_round_trips_source_identity_for_new_provider_kinds() {
|
|||
metadata: Vec::new(),
|
||||
install: None,
|
||||
},
|
||||
aim_core::domain::app::AppRecord {
|
||||
upm_core::domain::app::AppRecord {
|
||||
stable_id: "url-example.com-downloads-team-app.appimage".to_owned(),
|
||||
display_name: "https://example.com/downloads/team-app.AppImage".to_owned(),
|
||||
source_input: Some("https://example.com/downloads/team-app.AppImage".to_owned()),
|
||||
source: Some(aim_core::domain::source::SourceRef {
|
||||
kind: aim_core::domain::source::SourceKind::DirectUrl,
|
||||
source: Some(upm_core::domain::source::SourceRef {
|
||||
kind: upm_core::domain::source::SourceKind::DirectUrl,
|
||||
locator: "https://example.com/downloads/team-app.AppImage".to_owned(),
|
||||
input_kind: aim_core::domain::source::SourceInputKind::DirectUrl,
|
||||
normalized_kind: aim_core::domain::source::NormalizedSourceKind::DirectUrl,
|
||||
input_kind: upm_core::domain::source::SourceInputKind::DirectUrl,
|
||||
normalized_kind: upm_core::domain::source::NormalizedSourceKind::DirectUrl,
|
||||
canonical_locator: None,
|
||||
requested_tag: None,
|
||||
requested_asset_name: None,
|
||||
|
|
@ -213,9 +213,9 @@ fn registry_save_is_atomic_and_cleans_up_temp_file() {
|
|||
let store = RegistryStore::new(registry_path.clone());
|
||||
|
||||
store
|
||||
.save(&aim_core::registry::model::Registry {
|
||||
.save(&upm_core::registry::model::Registry {
|
||||
version: 1,
|
||||
apps: vec![aim_core::domain::app::AppRecord {
|
||||
apps: vec![upm_core::domain::app::AppRecord {
|
||||
stable_id: "bat".to_owned(),
|
||||
display_name: "Bat".to_owned(),
|
||||
source_input: None,
|
||||
|
|
@ -242,7 +242,7 @@ fn registry_exclusive_lock_rejects_second_mutator() {
|
|||
|
||||
assert!(matches!(
|
||||
error,
|
||||
aim_core::registry::store::RegistryStoreError::LockUnavailable
|
||||
upm_core::registry::store::RegistryStoreError::LockUnavailable
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -251,9 +251,9 @@ fn registry_mutate_exclusive_reloads_and_writes_latest_state() {
|
|||
let dir = tempdir().unwrap();
|
||||
let store = RegistryStore::new(dir.path().join("registry.toml"));
|
||||
store
|
||||
.save(&aim_core::registry::model::Registry {
|
||||
.save(&upm_core::registry::model::Registry {
|
||||
version: 1,
|
||||
apps: vec![aim_core::domain::app::AppRecord {
|
||||
apps: vec![upm_core::domain::app::AppRecord {
|
||||
stable_id: "bat".to_owned(),
|
||||
display_name: "Bat".to_owned(),
|
||||
source_input: None,
|
||||
|
|
@ -268,7 +268,7 @@ fn registry_mutate_exclusive_reloads_and_writes_latest_state() {
|
|||
|
||||
store
|
||||
.mutate_exclusive(|registry| {
|
||||
registry.apps.push(aim_core::domain::app::AppRecord {
|
||||
registry.apps.push(upm_core::domain::app::AppRecord {
|
||||
stable_id: "t3code".to_owned(),
|
||||
display_name: "T3 Code".to_owned(),
|
||||
source_input: None,
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
use aim_core::app::interaction::{InteractionKind, InteractionRequest};
|
||||
use aim_core::app::list::build_list_rows;
|
||||
use aim_core::app::progress::{OperationEvent, OperationStage};
|
||||
use aim_core::app::remove::{
|
||||
build_removal_plan, remove_registered_app_with_reporter, resolve_registered_app,
|
||||
};
|
||||
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
|
||||
use std::path::Path;
|
||||
use tempfile::tempdir;
|
||||
use upm_core::app::interaction::{InteractionKind, InteractionRequest};
|
||||
use upm_core::app::list::build_list_rows;
|
||||
use upm_core::app::progress::{OperationEvent, OperationStage};
|
||||
use upm_core::app::remove::{
|
||||
build_removal_plan, remove_registered_app_with_reporter, resolve_registered_app,
|
||||
};
|
||||
use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
|
||||
|
||||
#[test]
|
||||
fn remove_flow_rejects_unknown_app_names() {
|
||||
|
|
@ -74,7 +74,7 @@ fn ambiguous_remove_matches_include_stable_ids_for_client_choice() {
|
|||
|
||||
assert_eq!(
|
||||
error,
|
||||
aim_core::app::remove::ResolveRegisteredAppError::Ambiguous {
|
||||
upm_core::app::remove::ResolveRegisteredAppError::Ambiguous {
|
||||
request: InteractionRequest {
|
||||
key: "select-registered-app".to_owned(),
|
||||
kind: InteractionKind::SelectRegisteredApp {
|
||||
|
|
@ -98,8 +98,8 @@ fn removal_plan_prefers_persisted_install_metadata_paths() {
|
|||
metadata: Vec::new(),
|
||||
install: Some(InstallMetadata {
|
||||
scope: InstallScope::System,
|
||||
payload_path: Some("/opt/aim/appimages/bat.AppImage".to_owned()),
|
||||
desktop_entry_path: Some("/usr/share/applications/aim-bat.desktop".to_owned()),
|
||||
payload_path: Some("/opt/upm/appimages/bat.AppImage".to_owned()),
|
||||
desktop_entry_path: Some("/usr/share/applications/upm-bat.desktop".to_owned()),
|
||||
icon_path: Some("/usr/share/icons/hicolor/256x256/apps/bat.png".to_owned()),
|
||||
}),
|
||||
};
|
||||
|
|
@ -110,8 +110,8 @@ fn removal_plan_prefers_persisted_install_metadata_paths() {
|
|||
assert_eq!(
|
||||
plan.artifact_paths,
|
||||
vec![
|
||||
"/opt/aim/appimages/bat.AppImage".to_owned(),
|
||||
"/usr/share/applications/aim-bat.desktop".to_owned(),
|
||||
"/opt/upm/appimages/bat.AppImage".to_owned(),
|
||||
"/usr/share/applications/upm-bat.desktop".to_owned(),
|
||||
"/usr/share/icons/hicolor/256x256/apps/bat.png".to_owned(),
|
||||
]
|
||||
);
|
||||
|
|
@ -135,8 +135,8 @@ fn removal_plan_falls_back_to_derived_managed_user_paths() {
|
|||
assert_eq!(
|
||||
plan.artifact_paths,
|
||||
vec![
|
||||
"/home/test/.local/lib/aim/appimages/bat.AppImage".to_owned(),
|
||||
"/home/test/.local/share/applications/aim-bat.desktop".to_owned(),
|
||||
"/home/test/.local/lib/upm/appimages/bat.AppImage".to_owned(),
|
||||
"/home/test/.local/share/applications/upm-bat.desktop".to_owned(),
|
||||
"/home/test/.local/share/icons/hicolor/256x256/apps/bat.png".to_owned(),
|
||||
]
|
||||
);
|
||||
|
|
@ -158,7 +158,7 @@ fn remove_flow_reports_resolution_and_cleanup_events() {
|
|||
payload_path: Some(
|
||||
install_home
|
||||
.path()
|
||||
.join(".local/lib/aim/appimages/bat.AppImage")
|
||||
.join(".local/lib/upm/appimages/bat.AppImage")
|
||||
.display()
|
||||
.to_string(),
|
||||
),
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
use aim_core::app::search::{
|
||||
use upm_core::app::search::{
|
||||
GitHubSearchProvider, SearchProvider, SearchProviderError, build_search_results_with,
|
||||
};
|
||||
use aim_core::domain::app::AppRecord;
|
||||
use aim_core::domain::search::{SearchInstallStatus, SearchQuery};
|
||||
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
|
||||
use aim_core::source::github::{FixtureGitHubTransport, search_github_repositories_with};
|
||||
use upm_core::domain::app::AppRecord;
|
||||
use upm_core::domain::search::{SearchInstallStatus, SearchQuery};
|
||||
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
|
||||
use upm_core::source::github::{FixtureGitHubTransport, search_github_repositories_with};
|
||||
|
||||
#[test]
|
||||
fn github_fixtures_return_normalized_remote_hits() {
|
||||
|
|
@ -206,7 +206,7 @@ impl SearchProvider for FailingProvider {
|
|||
fn search(
|
||||
&self,
|
||||
_query: &SearchQuery,
|
||||
) -> Result<Vec<aim_core::domain::search::SearchResult>, SearchProviderError> {
|
||||
) -> Result<Vec<upm_core::domain::search::SearchResult>, SearchProviderError> {
|
||||
Err(SearchProviderError::new("github", "fixture rate limit"))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
use aim_core::app::show::{build_show_result, build_show_result_with};
|
||||
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
use aim_core::domain::show::{ShowResult, ShowResultError};
|
||||
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
|
||||
use aim_core::domain::update::{
|
||||
use upm_core::app::show::{build_show_result, build_show_result_with};
|
||||
use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
use upm_core::domain::show::{ShowResult, ShowResultError};
|
||||
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
|
||||
use upm_core::domain::update::{
|
||||
ChannelPreference, MetadataHints, ParsedMetadata, ParsedMetadataKind, UpdateChannelKind,
|
||||
UpdateStrategy,
|
||||
};
|
||||
use aim_core::source::github::FixtureGitHubTransport;
|
||||
use upm_core::source::github::FixtureGitHubTransport;
|
||||
|
||||
#[test]
|
||||
fn exact_installed_match_returns_installed_details() {
|
||||
|
|
@ -48,8 +48,8 @@ fn exact_installed_match_returns_installed_details() {
|
|||
install: Some(InstallMetadata {
|
||||
scope: InstallScope::User,
|
||||
payload_path: Some("/tmp/bat.AppImage".to_owned()),
|
||||
desktop_entry_path: Some("/tmp/aim-bat.desktop".to_owned()),
|
||||
icon_path: Some("/tmp/aim-bat.png".to_owned()),
|
||||
desktop_entry_path: Some("/tmp/upm-bat.desktop".to_owned()),
|
||||
icon_path: Some("/tmp/upm-bat.png".to_owned()),
|
||||
}),
|
||||
}];
|
||||
|
||||
|
|
@ -180,7 +180,7 @@ fn remote_show_projects_tracking_preference_interaction() {
|
|||
ShowResult::Remote(remote) => {
|
||||
assert!(remote.interactions.iter().any(|interaction| matches!(
|
||||
interaction,
|
||||
aim_core::domain::show::RemoteInteractionSummary::ChooseTrackingPreference { .. }
|
||||
upm_core::domain::show::RemoteInteractionSummary::ChooseTrackingPreference { .. }
|
||||
)));
|
||||
}
|
||||
other => panic!("expected remote result, got {other:?}"),
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
use aim_core::app::add::AddSecurityPolicy;
|
||||
use aim_core::app::progress::{NoopReporter, OperationEvent, OperationStage};
|
||||
use aim_core::app::update::{
|
||||
build_update_plan, execute_updates, execute_updates_with_reporter,
|
||||
execute_updates_with_reporter_and_policy,
|
||||
};
|
||||
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
|
||||
use aim_core::domain::update::{ChannelPreference, UpdateChannelKind, UpdateStrategy};
|
||||
use aim_core::integration::paths::managed_appimage_path;
|
||||
use std::fs;
|
||||
use std::sync::Mutex;
|
||||
use tempfile::tempdir;
|
||||
use upm_core::app::add::AddSecurityPolicy;
|
||||
use upm_core::app::progress::{NoopReporter, OperationEvent, OperationStage};
|
||||
use upm_core::app::update::{
|
||||
build_update_plan, execute_updates, execute_updates_with_reporter,
|
||||
execute_updates_with_reporter_and_policy,
|
||||
};
|
||||
use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
|
||||
use upm_core::domain::update::{ChannelPreference, UpdateChannelKind, UpdateStrategy};
|
||||
use upm_core::integration::paths::managed_appimage_path;
|
||||
|
||||
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ fn update_execution_rebuilds_gitlab_source_without_rewriting_origin() {
|
|||
let install_home = tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
}
|
||||
|
||||
let previous = AppRecord {
|
||||
|
|
@ -253,7 +253,7 @@ fn update_execution_rebuilds_sourceforge_release_folder_without_rewriting_origin
|
|||
let install_home = tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
}
|
||||
|
||||
let previous = AppRecord {
|
||||
|
|
@ -323,7 +323,7 @@ fn direct_http_updates_are_rejected_by_default() {
|
|||
let install_home = tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
}
|
||||
|
||||
let previous = AppRecord {
|
||||
|
|
@ -357,7 +357,7 @@ fn direct_http_updates_are_rejected_by_default() {
|
|||
assert_eq!(result.failed_count(), 1);
|
||||
assert!(matches!(
|
||||
&result.items[0].status,
|
||||
aim_core::domain::update::UpdateExecutionStatus::Failed { reason }
|
||||
upm_core::domain::update::UpdateExecutionStatus::Failed { reason }
|
||||
if reason.contains("InsecureHttpSource")
|
||||
));
|
||||
}
|
||||
|
|
@ -370,7 +370,7 @@ fn direct_http_updates_can_be_allowed_by_policy() {
|
|||
let install_home = tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
}
|
||||
|
||||
let previous = AppRecord {
|
||||
|
|
@ -417,7 +417,7 @@ fn update_execution_uses_stored_sourceforge_releases_root_for_file_like_inputs()
|
|||
let install_home = tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
}
|
||||
|
||||
let previous = AppRecord {
|
||||
|
|
@ -482,7 +482,7 @@ fn failed_update_restores_previous_payload_contents() {
|
|||
let install_home = tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("DISPLAY", ":99");
|
||||
std::env::set_var("XDG_CURRENT_DESKTOP", "test");
|
||||
}
|
||||
|
|
@ -536,7 +536,7 @@ fn successful_update_removes_rollback_staging_directory() {
|
|||
let install_home = tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
|
||||
std::env::remove_var("DISPLAY");
|
||||
std::env::remove_var("WAYLAND_DISPLAY");
|
||||
std::env::remove_var("XDG_CURRENT_DESKTOP");
|
||||
|
|
@ -578,7 +578,7 @@ fn successful_update_removes_rollback_staging_directory() {
|
|||
assert!(
|
||||
!install_home
|
||||
.path()
|
||||
.join(".local/share/aim/rollback")
|
||||
.join(".local/share/upm/rollback")
|
||||
.exists()
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "aim-cli"
|
||||
name = "upm"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
|
@ -8,7 +8,7 @@ license.workspace = true
|
|||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "aim"
|
||||
name = "upm"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
|
|
@ -21,7 +21,8 @@ libc.workspace = true
|
|||
ratatui.workspace = true
|
||||
serde.workspace = true
|
||||
toml.workspace = true
|
||||
aim-core = { path = "../aim-core" }
|
||||
upm-appimage = { path = "../upm-appimage" }
|
||||
upm-core = { path = "../upm-core" }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd.workspace = true
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
use clap::Parser;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "aim")]
|
||||
#[command(about = "AppImage Manager")]
|
||||
#[command(name = "upm")]
|
||||
#[command(about = "Universal Package Manager")]
|
||||
pub struct Cli {
|
||||
#[arg(global = true, long = "system", conflicts_with = "user")]
|
||||
pub system: bool,
|
||||
|
|
@ -52,10 +52,10 @@ struct FileThemeConfig {
|
|||
|
||||
impl AppConfig {
|
||||
pub fn load() -> LoadedConfig {
|
||||
let system_path = Some(PathBuf::from("/etc/aim/config.toml"));
|
||||
let system_path = Some(PathBuf::from("/etc/upm/config.toml"));
|
||||
let user_path = env::var_os("HOME")
|
||||
.map(PathBuf::from)
|
||||
.map(|home| home.join(".config/aim/config.toml"));
|
||||
.map(|home| home.join(".config/upm/config.toml"));
|
||||
Self::load_from_paths(system_path.as_deref(), user_path.as_deref())
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue