self_update 1.3.0

Self updates for standalone executables
Documentation
# Version comparison and target detection (reference)

Status: implemented

## Scope

This spec documents two related mechanisms: (1) how the crate parses and compares
semver version strings (`src/version.rs`), and (2) how the running platform target
is determined and how a release asset is matched to it (`src/lib.rs`,
`src/update.rs`, `src/macros.rs`, `src/backends/common.rs`). It covers the default
asset matcher, the `target(...)` and `asset_matcher(...)` overrides, and the
tie-breaking/ordering used when multiple releases or assets qualify.

## Behavior

### Version parsing and comparison

All comparison helpers parse both operands with `semver::Version::parse` and operate
on the parsed `Version` (`src/version.rs:bump_is_greater`). They take bare `&str` version strings:

- `bump_is_greater(current, other)` returns `Ok(true)` iff `other > current` under
  full semver ordering, including prerelease and build-metadata rules
  (`src/version.rs:bump_is_greater`). This is the predicate that drives "is there an update".
- `bump_is_compatible(current, other)` (`src/version.rs:bump_is_compatible`) encodes the crate's
  own compatibility policy (caret-like, with special-casing when `current` is a
  prerelease and when both majors are 0); a compatible bump must not itself be a
  prerelease except in the prerelease-current branch.
- `bump_is_major` / `bump_is_minor` / `bump_is_patch` compare the corresponding
  numeric fields (`src/version.rs:bump_is_major`, `src/version.rs:bump_is_minor`,
  `src/version.rs:bump_is_patch`).
- `cmp_versions(a, b) -> Result<Ordering>` parses each version once and
  returns a true total order, including real `Equal` for equal versions (unlike the
  boolean `bump_is_greater`, which collapses equal/less into `false`). The shared
  release comparator `cmp_releases_newest_first(a, b) -> Ordering` builds on it for a
  newest-first order that places unparseable versions deterministically last (two
  unparseable compare `Equal`); it backs the selection sort in `choose_latest_release`
  and `s3::sort_newer` / `pick_latest`, so all three agree on "newest".

Parsing rules come entirely from the `semver` crate: strings must be `MAJOR.MINOR.PATCH`
with optional `-prerelease` and `+build` segments. Prerelease identifiers order below
the same core version; build metadata is ignored for ordering (standard semver).

No helper in `version.rs` strips a leading `v`. Prefix handling happens at the backend
boundary: the GitHub backend trims a single leading `v` from the release tag before it
becomes `Release.version` (`src/backends/github.rs:into_release`), so the value reaching these
helpers is already a bare semver string. A `Release` built via `Release::builder()` is
expected to carry a bare semver `version` (`src/update.rs:Release::builder`).

Error cases: any unparseable operand surfaces the underlying `semver::Error` converted
to `Error::SemVer` (boxed, opaque, source-preserving) via `From<semver::Error>`
(`src/errors.rs:SemVer`, `src/errors.rs:SemVer`). The helpers return `Result<bool>`, so a
parse failure propagates as `Err(Error::SemVer(_))` rather than a boolean.

Where parse errors are swallowed vs propagated:

- `Releases::is_update_available` calls `bump_is_greater` with `?` and short-circuits
  on the first strictly-newer release; a found update wins over a later parse error, but
  the first release *reached* with an unparseable version propagates `Error::SemVer`
  (`src/update.rs:Releases::is_update_available`).
- `choose_latest_release` is lenient: its filter and sort comparator use
  `bump_is_greater(...).unwrap_or(false)` / treat a comparator error as "not greater",
  so a release with an unparseable version is dropped rather than failing the update
  (`src/update.rs:choose_latest_release`, `src/update.rs:choose_latest_release`).

### Target string

`get_target()` returns the compile-time target triple as `&'static str` via
`env!("TARGET")` (`src/lib.rs:get_target`), e.g. `x86_64-unknown-linux-gnu` or
`i686-pc-windows-msvc`. It is the build target of the crate, captured at compile time
in `build.rs`; it is not recomputed at runtime.

The effective target used during an update is resolved at builder `build()` time:
`CommonBuilderConfig.target` is an `Option<String>` (`src/backends/common.rs:CommonBuilderConfig`) that,
when unset, defaults to `get_target().to_owned()`, and when set is used verbatim
(`src/backends/common.rs:CommonBuilderConfig::build`). The resolved `CommonConfig.target` is a plain
`String` (`src/backends/common.rs:CommonConfig`) exposed through the `target()` accessor
(`src/macros.rs:target`).

### Asset matching and overrides

Asset selection happens in `resolve_and_confirm` (`src/update.rs:resolve_and_confirm`):

- If a custom `asset_matcher` is set, it is called with `&release.assets` and its
  `Option<ReleaseAsset>` result is used directly; the built-in `target`/`identifier`
  matching is bypassed entirely (`src/update.rs:resolve_and_confirm`).
- Otherwise `release.asset_for(target, asset_identifier())` runs the default matcher.
- Either way, `None` becomes `Error::NoReleaseFound { target: Some(..) }`
  (`resolve_and_confirm`, `src/update.rs`).

Default matcher `Release::asset_for` (`src/update.rs`) is substring-based and
tries three passes in order, returning the **first** matching asset (cloned):

1. First asset whose `name` contains `target` and (if set) `identifier`.
2. Else first asset whose `name` contains both the arch and os tokens derived from the
   configured `target` string by `target_arch_os(target)` (`src/update.rs`) - not the
   build host's `std::env::consts` values, so an explicitly configured cross-target
   selects its own assets - and (if set) `identifier`.
3. Else, only if `identifier` is set, the first asset whose `name` contains
   `identifier`.

`identifier` (set via `asset_identifier(...)`, `src/macros.rs:asset_identifier`) disambiguates when
multiple assets match the same target; if unset, the first target match wins.
`has_target_asset` is the related `any(name.contains(target))` predicate the GitHub
backend uses to pre-filter releases (`src/update.rs:has_target_asset`, `src/backends/github.rs:ReleaseList::fetch`).

Overrides:

- `target(&str)` (`src/macros.rs:target`) overrides the platform string used by the
  default matcher and the `{target}` URL substitution.
- `asset_matcher(closure)` (`src/macros.rs:asset_matcher`) installs an
  `Fn(&[ReleaseAsset]) -> Option<ReleaseAsset>` (boxed as `DynAssetMatcher`,
  `src/lib.rs:DynAssetMatcher`) that fully replaces the default substring heuristic.

### Tie-breaking and ordering

- Multiple matching assets: the default matcher returns the **first** asset in list
  order that satisfies the earliest-succeeding pass; passes are ordered
  target+identifier, then OS+ARCH+identifier, then identifier-only.
- Multiple releases: `choose_latest_release` filters to strictly-newer releases,
  sorts them semver-descending (newest first) independent of source order, prefers the
  first *compatible* release, and falls back to the first (newest) release overall if
  none is compatible (`src/update.rs:choose_latest_release`).

## Public surface

- `self_update::get_target() -> &'static str` (`src/lib.rs:get_target`).
- `self_update::version::{bump_is_greater, bump_is_compatible, bump_is_major,
  bump_is_minor, bump_is_patch}(current, other) -> Result<bool>` (`src/version.rs`).
- `Release::has_target_asset(target)`, `Release::asset_for(target, identifier)`
  (`src/update.rs:has_target_asset`, `src/update.rs:asset_for`).
- Builder setters (each backend, via macro): `.target(&str)`,
  `.asset_identifier(&str)`, `.asset_matcher(closure)`
  (`src/macros.rs:target`, `src/macros.rs:asset_identifier`, `src/macros.rs:asset_matcher`).

## Invariants and regression checklist

- Comparison helpers never strip `v`; the `v`-trim lives in the GitHub backend
  (`src/backends/github.rs:into_release`). Backend `Release.version` must be bare semver.
- An unparseable version propagates as `Error::SemVer`, never a silent `false`, in
  `bump_is_greater`/`is_update_available`; but `choose_latest_release` deliberately
  drops unparseable releases via `unwrap_or(false)`.
- Unset `target` resolves to `get_target()`; a set `target` is used verbatim
  (`src/backends/common.rs:CommonBuilderConfig::build`).
- A custom `asset_matcher` fully bypasses `asset_for`; no asset selected ->
  `Error::Release`.
- Default matcher pass order (target+id, OS+ARCH+id, id-only) and first-match
  semantics are stable.
- Release selection is order-independent: candidates are re-sorted descending before
  the compatible-first / newest-fallback choice.

## Tests

- `src/version.rs:test_bump_greater`, `src/version.rs:test_bump_is_compatible`,
  `src/version.rs:test_bump_is_major`, `src/version.rs:test_bump_is_minor`,
  `src/version.rs:test_bump_is_patch` cover the comparison matrix.
- `src/errors.rs:semver_error_is_opaque_with_source`,
  `src/errors.rs:semver_error_display_includes_prefix_and_inner_message`:
  `Error::SemVer` is opaque, source-preserving, and keeps the
  `SemVerError:` Display prefix.
- `src/backends/common.rs:build_resolves_target_and_install_path_defaults`
  pins unset-target-defaults-to-build-target and set-target-used-verbatim.
- `src/backends/github.rs:asset_matcher_overrides_default_selection`.
- `src/update.rs:choose_latest_release_ignores_unparseable_versions`: documents that an
  unparseable version is dropped by the leading
  `bump_is_greater(...).unwrap_or(false)` in `choose_latest_release`.

## Related

- `custom-asset-matching.md`
- `choose-latest-release-sort.md`
- `ref-release-model.md`
- `ref-update-pipeline.md`
- `error-variant-granularity.md`
- `ref-github-backend.md`