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`.
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`.
112//!
113//! At this point, any remaining assets should work on your platform, so if there's more than one
114//! match, it attempts to pick the best one.
115//!
116//! - If it finds both 64-bit and 32-bit assets and you are on a 64-bit platform, it filters out the
117//!   32-bit assets.
118//! - If you've provided a string to [`UbiBuilder::matching`], this is used as a filter at this
119//!   point.
120//! - If your platform is macOS on ARM64 and there are assets for both x86-64 and ARM64, it filters
121//!   out the non-ARM64 assets.
122//!
123//! Finally, if there are still multiple assets left, it sorts them by file name and picks the first
124//! one. The sorting is done to make sure it always picks the same one every time it's run .
125//!
126//! ## How `ubi` Finds the Right Executable in an Archive File
127//!
128//! If the selected release artifact is an archive file (a tarball or zip file), then `ubi` will
129//! look inside the archive to find the right executable.
130//!
131//! It first tries to find a file matching the exact name of the project (plus an extension on
132//! Windows). So for example, if you're installing
133//! [`houseabsolute/precious`](https://github.com/houseabsolute/precious), it will look in the
134//! archive for a file named `precious` on Unix-like systems and `precious.bat` or `precious.exe` on
135//! Windows. Note that if it finds an exact match, it does not check the file's mode.
136//!
137//! If it can't find an exact match it will look for a file that _starts with_ the project
138//! name. This is mostly to account for projects that include things like platforms or release names
139//! in their executables. Using
140//! [`houseabsolute/precious`](https://github.com/houseabsolute/precious) as an example again, it
141//! will match a file named `precious-linux-amd64` or `precious-v1.2.3`. In this case, it will
142//! _rename_ the extracted file to `precious`. On Unix-like systems, these partial matches will only
143//! be considered if the file's mode includes an executable bit. On Windows, it looks for a partial
144//! match that is a `.bat` or `.exe` file, and the extracted file will be renamed to `precious.bat`
145//! or `precious.exe`.
146//!
147//! ## Features
148//!
149//! This crate offers several features to control the TLS dependency used by `reqwest`:
150//!
151#![doc = document_features::document_features!()]
152
153mod arch;
154mod archive;
155mod builder;
156mod extension;
157mod forge;
158mod forgejo;
159mod github;
160mod gitlab;
161mod installer;
162mod os;
163mod picker;
164#[cfg(test)]
165mod test;
166#[cfg(test)]
167mod test_case;
168mod ubi;
169
170pub use crate::{builder::UbiBuilder, forge::ForgeType, ubi::Ubi};
171
172// The version of the `ubi` crate.
173pub const VERSION: &str = env!("CARGO_PKG_VERSION");
174
175#[cfg(feature = "logging")]
176use fern::{
177    colors::{Color, ColoredLevelConfig},
178    Dispatch,
179};
180
181/// This function initializes logging for the application. It's public for the sake of the `ubi`
182/// binary, but it lives in the library crate so that test code can also enable logging.
183///
184/// # Errors
185///
186/// This can return a `log::SetLoggerError` error.
187#[cfg(feature = "logging")]
188pub fn init_logger(level: log::LevelFilter) -> Result<(), log::SetLoggerError> {
189    let line_colors = ColoredLevelConfig::new()
190        .error(Color::Red)
191        .warn(Color::Yellow)
192        .info(Color::BrightBlack)
193        .debug(Color::BrightBlack)
194        .trace(Color::BrightBlack);
195    let level_colors = line_colors.info(Color::Green).debug(Color::Black);
196
197    Dispatch::new()
198        .format(move |out, message, record| {
199            out.finish(format_args!(
200                "{color_line}[{target}][{level}{color_line}] {message}\x1B[0m",
201                color_line = format_args!(
202                    "\x1B[{}m",
203                    line_colors.get_color(&record.level()).to_fg_str()
204                ),
205                target = record.target(),
206                level = level_colors.color(record.level()),
207                message = message,
208            ));
209        })
210        .level(level)
211        // This is very noisy.
212        .level_for("hyper", log::LevelFilter::Error)
213        .chain(std::io::stderr())
214        .apply()
215}