update_rs/lib.rs
1//! Self-contained, in-place self-updates for Rust applications.
2//!
3//! `update-rs` lets a Rust application replace its own binary on disk with a
4//! newer release and relaunch into it — without an installer, package manager,
5//! or external updater process. It was extracted from the Sierra Softworks
6//! [Git-Tool](https://github.com/SierraSoftworks/git-tool) project, where it
7//! powers the `gt update` command.
8//!
9//! # The three-phase update
10//!
11//! A running executable can't reliably overwrite itself (Windows holds an
12//! exclusive lock on a running image), so the update is performed across three
13//! phases, each running from a *different* binary and relaunching the next:
14//!
15//! 1. **Prepare** — the running application downloads the new release to a
16//! temporary file next to it, marks it executable (on Unix), and launches
17//! that temporary binary to perform the next phase.
18//! 2. **Replace** — the temporary binary deletes the original application file
19//! and copies itself over it (retrying while the old process exits), then
20//! launches the freshly replaced original.
21//! 3. **Cleanup** — the updated original deletes the leftover temporary binary
22//! and returns control to the application.
23//!
24//! The phases are threaded together by relaunching the binary with
25//! [`RESUME_FLAG`] followed by a serialized [`UpdateState`]. This design is
26//! described in more detail in
27//! [*Building self-updating applications*](https://sierrasoftworks.com/2019/10/15/app-updates/#cleanup).
28//!
29//! # Quick start
30//!
31//! Build an [`UpdateManager`] around a [`Source`] (the crate ships
32//! [`GitHubSource`]), detect the [`RESUME_FLAG`] **before** any other argument
33//! parsing, and otherwise offer the newest release. The asset to download is
34//! chosen by a glob pattern; the [`naming`] helpers build one for the current
35//! platform.
36//!
37//! ```no_run
38//! use update_rs::{naming, GitHubSource, Release, UpdateManager, RESUME_FLAG};
39//!
40//! #[tokio::main]
41//! async fn main() -> Result<(), update_rs::Error> {
42//! let manager = UpdateManager::new(
43//! // e.g. matches "yourapp-linux-amd64", "yourapp-windows-amd64.exe", ...
44//! GitHubSource::new("yourorg/yourapp", naming::go("yourapp"))
45//! .with_release_tag_prefix("v"), // strips the leading v in vX.Y.Z tags
46//! );
47//!
48//! // The updater relaunches your application between phases, passing the
49//! // serialized update state after `RESUME_FLAG`. Detect it first and hand
50//! // control back to the library.
51//! let args: Vec<String> = std::env::args().collect();
52//! if let Some(i) = args.iter().position(|a| a == RESUME_FLAG) {
53//! if manager.resume_from_arg(&args[i + 1]).await? {
54//! return Ok(()); // a phase was launched; exit so it can take over
55//! }
56//! }
57//!
58//! // Otherwise, look for the newest release with a binary for this platform.
59//! let releases = manager.get_releases().await?;
60//! let latest = Release::get_latest(releases.iter().filter(|r| r.get_variant().is_some()));
61//! if let Some(latest) = latest {
62//! if manager.update(latest).await? {
63//! println!("Shutting down to complete the update.");
64//! return Ok(()); // exit promptly so the new binary can take over
65//! }
66//! }
67//!
68//! // ... your normal application logic ...
69//! Ok(())
70//! }
71//! ```
72//!
73//! # Selecting the release asset
74//!
75//! [`GitHubSource::new`] takes a glob pattern (`*` and `?` wildcards) that is
76//! matched against each release's asset file names, so your project can name its
77//! assets however it likes. Build the pattern by hand —
78//! `format!("yourapp-{OS}-{ARCH}{EXE_SUFFIX}")` using [`std::env::consts`] — or
79//! with a [`naming`] helper: [`naming::go`] for Go-style names
80//! (`yourapp-linux-amd64`) or [`naming::rust`] for the Rust target triple
81//! (`yourapp-x86_64-unknown-linux-gnu`).
82//!
83//! # Customising the relaunch
84//!
85//! The updater relaunches your binary between phases through a [`Launcher`]. The
86//! default, [`DefaultLauncher`], passes the [`RESUME_FLAG`] and serialized state
87//! and spawns a detached child — and can thread your own arguments and
88//! environment variables through to the relaunched process via its
89//! [`with_arg`](DefaultLauncher::with_arg) /
90//! [`with_env`](DefaultLauncher::with_env) builders, so the common cases need no
91//! custom launcher. For more control, install any [`Launcher`] with
92//! [`with_launcher`](UpdateManager::with_launcher): override
93//! [`resume_args`](Launcher::resume_args) to hand the state to a sub-command, or
94//! [`launch`](Launcher::launch) for the whole command. (The `opentelemetry`
95//! feature below already propagates the trace context for you, so that case needs
96//! no wiring.)
97//!
98//! # Observability
99//!
100//! By default the crate emits its diagnostic events through the lightweight
101//! [`log`](https://docs.rs/log) facade, which does nothing until the application
102//! installs a logger. Two opt-in features build on that:
103//!
104//! - **`tracing`** routes diagnostics through
105//! [`tracing`](https://docs.rs/tracing) instead of `log`, adding
106//! `#[instrument]` spans and structured events for each step of the update.
107//! - **`opentelemetry`** (which implies `tracing`) carries the active
108//! OpenTelemetry trace context *inside the serialized [`UpdateState`]* — not
109//! as an extra command-line argument — so the three phases, which each run in
110//! a separate process, stitch together into one distributed trace. It reads
111//! and writes only the **global** propagator
112//! (`opentelemetry::global::get_text_map_propagator`), so the host
113//! application stays in full control of how (and whether) traces are exported;
114//! if no propagator is installed, the feature is a no-op.
115//!
116//! There is nothing extra to wire up: detect [`RESUME_FLAG`] and call
117//! [`resume_from_arg`](UpdateManager::resume_from_arg) as usual, and the trace
118//! context rides along with the update state automatically.
119//!
120//! # Windows: avoiding UAC / Error 740
121//!
122//! Because the temporary binary is named like `yourapp-<tag>.exe`, Windows'
123//! installer-detection heuristic can decide it's an installer and demand UAC
124//! elevation — which fails the relaunch with `ERROR_ELEVATION_REQUIRED`
125//! (Win32 error 740). The fix is to ship your binary with an `asInvoker`
126//! application manifest. This is a property of the consuming *binary* (a library
127//! can't embed a manifest), so `update-rs` ships a ready-to-copy `build.rs` and
128//! manifest template under `examples/windows-manifest/`; see the project README
129//! for details.
130
131mod cmd;
132mod fs;
133mod glob;
134mod manager;
135pub mod naming;
136mod release;
137mod source;
138mod state;
139
140pub use cmd::{DefaultLauncher, Launcher};
141pub use human_errors::Error;
142pub use manager::UpdateManager;
143pub use release::{Release, ReleaseVariant};
144/// Re-export of the [`reqwest`] version this crate is built against, so callers
145/// can construct a [`reqwest::Client`] of the exact type
146/// [`GitHubSource::with_client`] expects.
147pub use reqwest;
148pub use source::{GitHubSource, Source};
149pub use state::{UpdatePhase, UpdateState};
150
151/// The command-line flag the library uses to relaunch the consuming binary
152/// between update phases.
153///
154/// A consuming `main()` **must** detect this flag (before any other argument
155/// parsing), pass the JSON value that follows it to
156/// [`UpdateManager::resume_from_arg`], and exit immediately if it returns
157/// `Ok(true)` — so that the next phase's process can take ownership of the
158/// binary.
159pub const RESUME_FLAG: &str = "--update-resume-internal";
160
161/// The Rust target triple this crate was compiled for (e.g.
162/// `x86_64-unknown-linux-gnu`), captured at build time. Used by
163/// [`naming::rust`] to build release asset names.
164pub const TARGET: &str = env!("UPDATE_RS_TARGET");