ubi/
lib.rs

1//! A library for downloading and installing pre-built binaries from GitHub.
2//!
3//! UBI stands for "Universal Binary Installer". It downloads and installs pre-built binaries from
4//! GitHub releases. It is designed to be used in shell scripts and other automation.
5//!
6//! This project also ships a CLI tool named `ubi`. See [the project's GitHub
7//! repo](https://github.com/houseabsolute/ubi) for more details on installing and using this tool.
8//!
9//! The main entry point for programmatic use is the [`UbiBuilder`] struct. Here is an example of its
10//! usage:
11//!
12//! ```ignore
13//! use ubi::UbiBuilder;
14//!
15//! #[tokio::main]
16//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
17//!     let ubi = UbiBuilder::new()
18//!         .project("houseabsolute/precious")
19//!         .install_dir("/usr/local/bin")
20//!         .build()?;
21//!
22//!     ubi.install_binary().await?;
23//!
24//!     Ok(())
25//! }
26//! ```
27//!
28//! ## Installed Executable Naming
29//!
30//! If the release is in the form of a tarball or zip file, `ubi` will look in that archive file for
31//! a file that matches the value given for the `exe` field, if any. Otherwise it looks for a file
32//! with the same name as the project. In either case, the file will be installed with the name it
33//! has in the archive file.
34//!
35//! If the release is in the form of a bare executable or a compressed executable, then the
36//! installed executable will use the name of the project instead. For files with a `.exe`, `.jar`,
37//! `.phar`, `.py`, `.pyz` `.sh`, or `.AppImage`, the installed executable will be
38//! `$project_name.$extension`.
39//!
40//! This is a bit inconsistent, but it's how `ubi` has behaved since it was created, and I find this
41//! to be the sanest behavior. Some projects, for example `rust-analyzer`, provide releases as
42//! executables with names like `rust-analyzer-x86_64-apple-darwin` and
43//! `rust-analyzer-x86_64-unknown-linux-musl`, so installing these as `rust-analyzer` seems like
44//! better behavior.
45//!
46//! ## How `ubi` Finds the Right Release Artifact
47//!
48//! <div class="warning">Note that the exact set of steps that are followed to find a release
49//! artifacts is not considered part of the API, and may change in any future release.</div>
50//!
51//! If you work on a project and you'd like to make sure that `ubi` can install it, please see [my
52//! blog post, Naming Your Binary Executable
53//! Releases](https://blog.urth.org/2023/04/16/naming-your-binary-executable-releases/) for more
54//! details.
55//!
56//! When you call [`Ubi::install_binary`], it looks at the release assets (downloadable files) for a
57//! project and tries to find the "right" asset for the platform it's running on. The matching logic
58//! currently works like this:
59//!
60//! First it filters out assets with extensions it doesn't recognize. Right now this is anything that
61//! doesn't match one of the following:
62//!
63//! - `.7z`
64//! - `.AppImage` (Linux only)
65//! - `.bat` (Windows only)
66//! - `.bz`
67//! - `.bz2`
68//! - `.exe` (Windows only)
69//! - `.gz`
70//! - `.jar`
71//! - `.phar`
72//! - `.py`
73//! - `.pyz`
74//! - `.sh`
75//! - `.tar`
76//! - `.tar.bz`
77//! - `.tar.bz2`
78//! - `.tar.gz`
79//! - `.tar.xz`
80//! - `.tbz`
81//! - `.tgz`
82//! - `.txz`
83//! - `.xz`
84//! - `.zip`
85//! - No extension
86//!
87//! It tries to be careful about what constitutes an extension. It's common for release filenames to
88//! include a dot (`.`) in the filename before something that's _not_ intended as an extension, for
89//! example `some-tool.linux.amd64` or `some-tools-linux-x86-64-1.3.5.tar.gz`.
90//!
91//! If, after filtering for extensions, there's only one asset, it will try to install this one, on
92//! the assumption that this project releases assets which are not platform-specific (like a shell
93//! script) _or_ that this project only releases for one platform and you're running `ubi` on that
94//! platform.
95//!
96//! If there are multiple matching assets, it will first filter them based on your platform. It does
97//! this in several stages:
98//!
99//! - First it filters based on your OS, which is something like Linux, macOS, Windows, FreeBSD,
100//!   etc. It looks at the asset filenames to see which ones match your OS, using a (hopefully
101//!   complete) regex.
102//! - Next it filters based on your CPU architecture, which is something like x86-64, ARM64, `PowerPC`,
103//!   etc. Again, this is done with a regex.
104//! - If you are running on a Linux system using musl as its libc, it will also filter out anything
105//!   _not_ compiled against musl. This filter looks to see if the file name contains an indication
106//!   of which libc it was compiled against. Typically, this is something like "-gnu" or "-musl". If
107//!   it does contain this indicator, names that are _not_ musl are filtered out. However, if there
108//!   is no libc indicator, the asset will still be included. You can use the
109//!   [`UbiBuilder::is_musl`] method to explicitly say that the platform is using musl. If this
110//!   isn't set, then it will try to detect if you are using musl by looking at the output of `ldd
111//!   /bin/ls`. However, if there is no libc indicator, the asset will still be included, but musl
112//!   assets will be preferred over assets with no indication of which libc they use.
113//!
114//! At this point, any remaining assets should work on your platform, so if there's more than one
115//! match, it attempts to pick the best one.
116//!
117//! - If it finds both 64-bit and 32-bit assets and you are on a 64-bit platform, it filters out the
118//!   32-bit assets.
119//! - If you've provided a string to [`UbiBuilder::matching`], this is used as a filter at this
120//!   point.
121//! - If your platform is macOS on ARM64 and there are assets for both x86-64 and ARM64, it filters
122//!   out the non-ARM64 assets.
123//!
124//! Finally, if there are still multiple assets left, it sorts them by file name and picks the first
125//! one. The sorting is done to make sure it always picks the same one every time it's run .
126//!
127//! ## How `ubi` Finds the Right Executable in an Archive File
128//!
129//! If the selected release artifact is an archive file (a tarball or zip file), then `ubi` will
130//! look inside the archive to find the right executable.
131//!
132//! It first tries to find a file matching the exact name of the project (plus an extension on
133//! Windows). So for example, if you're installing
134//! [`houseabsolute/precious`](https://github.com/houseabsolute/precious), it will look in the
135//! archive for a file named `precious` on Unix-like systems and `precious.bat` or `precious.exe` on
136//! Windows. Note that if it finds an exact match, it does not check the file's mode.
137//!
138//! If it can't find an exact match it will look for a file that _starts with_ the project
139//! name. This is mostly to account for projects that include things like platforms or release names
140//! in their executables. Using
141//! [`houseabsolute/precious`](https://github.com/houseabsolute/precious) as an example again, it
142//! will match a file named `precious-linux-amd64` or `precious-v1.2.3`. In this case, it will
143//! _rename_ the extracted file to `precious`. On Unix-like systems, these partial matches will only
144//! be considered if the file's mode includes an executable bit. On Windows, it looks for a partial
145//! match that is a `.bat` or `.exe` file, and the extracted file will be renamed to `precious.bat`
146//! or `precious.exe`.
147//!
148//! ## Features
149//!
150//! This crate offers several features to control the TLS dependency used by `reqwest`:
151//!
152#![doc = document_features::document_features!()]
153
154mod arch;
155mod archive;
156mod builder;
157mod extension;
158mod forge;
159mod forgejo;
160mod github;
161mod gitlab;
162mod installer;
163mod os;
164mod picker;
165#[cfg(test)]
166mod test;
167#[cfg(test)]
168mod test_log;
169mod ubi;
170
171pub use crate::{builder::UbiBuilder, forge::ForgeType, ubi::Ubi};
172
173// The version of the `ubi` crate.
174pub const VERSION: &str = env!("CARGO_PKG_VERSION");
175
176#[cfg(feature = "logging")]
177use fern::{
178    colors::{Color, ColoredLevelConfig},
179    Dispatch,
180};
181
182/// This function initializes logging for the application. It's public for the sake of the `ubi`
183/// binary, but it lives in the library crate so that test code can also enable logging.
184///
185/// # Errors
186///
187/// This can return a `log::SetLoggerError` error.
188#[cfg(feature = "logging")]
189pub fn init_logger(level: log::LevelFilter) -> Result<(), log::SetLoggerError> {
190    let line_colors = ColoredLevelConfig::new()
191        .error(Color::Red)
192        .warn(Color::Yellow)
193        .info(Color::BrightBlack)
194        .debug(Color::BrightBlack)
195        .trace(Color::BrightBlack);
196    let level_colors = line_colors.info(Color::Green).debug(Color::Black);
197
198    Dispatch::new()
199        .format(move |out, message, record| {
200            out.finish(format_args!(
201                "{color_line}[{target}][{level}{color_line}] {message}\x1B[0m",
202                color_line = format_args!(
203                    "\x1B[{}m",
204                    line_colors.get_color(&record.level()).to_fg_str()
205                ),
206                target = record.target(),
207                level = level_colors.color(record.level()),
208                message = message,
209            ));
210        })
211        .level(level)
212        // This is very noisy.
213        .level_for("hyper", log::LevelFilter::Error)
214        .chain(std::io::stderr())
215        .apply()
216}