diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c168124d..f0420247 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -28,3 +28,7 @@ updates: directory: "/crates/normalize-path" schedule: interval: "daily" + - package-ecosystem: "cargo" + directory: "/crates/detect-target" + schedule: + interval: "daily" diff --git a/Cargo.lock b/Cargo.lock index a72e95bd..5d9985a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,11 +86,11 @@ dependencies = [ "clap", "compact_str", "crates_io_api", + "detect-targets", "env_logger", "flate2", "flock", "futures-util", - "guess_host_triple", "home", "itertools", "jobserver", @@ -350,6 +350,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "detect-targets" +version = "0.1.0" +dependencies = [ + "cfg-if", + "guess_host_triple", + "tokio", +] + [[package]] name = "detect-wasi" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 80872e3a..8cbee532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/detect-wasi", "crates/flock", "crates/normalize-path", + "crates/detect-targets", ] [profile.release] diff --git a/crates/bin/src/entry.rs b/crates/bin/src/entry.rs index 5d23a316..0091b017 100644 --- a/crates/bin/src/entry.rs +++ b/crates/bin/src/entry.rs @@ -2,6 +2,7 @@ use std::{fs, path::Path, sync::Arc, time::Duration}; use binstall::{ errors::BinstallError, + get_desired_targets, helpers::{ jobserver_client::LazyJobserverClient, remote::create_reqwest_client, tasks::AutoAbortJoinHandle, @@ -13,7 +14,6 @@ use binstall::{ self, resolve::{CrateName, Resolution, VersionReqExt}, }, - targets::get_desired_targets, }; use log::{debug, error, info, warn, LevelFilter}; use miette::{miette, Result, WrapErr}; @@ -29,7 +29,7 @@ pub async fn install_crates(mut args: Args, jobserver_client: LazyJobserverClien }; // Launch target detection - let desired_targets = get_desired_targets(&args.targets); + let desired_targets = get_desired_targets(args.targets.as_deref()); // Initialize reqwest client let client = create_reqwest_client(args.secure, args.min_tls_version.map(|v| v.into()))?; diff --git a/crates/detect-targets/Cargo.toml b/crates/detect-targets/Cargo.toml new file mode 100644 index 00000000..e389b996 --- /dev/null +++ b/crates/detect-targets/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "detect-targets" +description = "Detect the target of the env at runtime" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/detect-target" +version = "0.1.0" +rust-version = "1.61.0" +authors = ["Jiahao XU "] +edition = "2021" +license = "Apache-2.0 OR MIT" + +[dependencies] +tokio = { version = "1.20.1", features = ["rt", "process", "sync"], default-features = false } +cfg-if = "1.0.0" + +[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] +guess_host_triple = "0.1.3" + +[dev-dependencies] +tokio = { version = "1.20.1", features = ["macros"], default-features = false } diff --git a/crates/detect-targets/LICENSE-APACHE b/crates/detect-targets/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/detect-targets/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/detect-targets/LICENSE-MIT b/crates/detect-targets/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/detect-targets/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/crates/lib/build.rs b/crates/detect-targets/build.rs similarity index 100% rename from crates/lib/build.rs rename to crates/detect-targets/build.rs diff --git a/crates/detect-targets/src/desired_targets.rs b/crates/detect-targets/src/desired_targets.rs new file mode 100644 index 00000000..42acfefd --- /dev/null +++ b/crates/detect-targets/src/desired_targets.rs @@ -0,0 +1,59 @@ +use crate::detect_targets; + +use std::sync::Arc; + +use tokio::sync::OnceCell; + +#[derive(Debug)] +enum DesiredTargetsInner { + AutoDetect(Arc>>), + Initialized(Vec), +} + +#[derive(Debug)] +pub struct DesiredTargets(DesiredTargetsInner); + +impl DesiredTargets { + fn initialized(targets: Vec) -> Self { + Self(DesiredTargetsInner::Initialized(targets)) + } + + fn auto_detect() -> Self { + let arc = Arc::new(OnceCell::new()); + + let once_cell = arc.clone(); + tokio::spawn(async move { + once_cell.get_or_init(detect_targets).await; + }); + + Self(DesiredTargetsInner::AutoDetect(arc)) + } + + pub async fn get(&self) -> &[String] { + use DesiredTargetsInner::*; + + match &self.0 { + Initialized(targets) => targets, + + // This will mostly just wait for the spawned task, + // on rare occausion though, it will poll the future + // returned by `detect_targets`. + AutoDetect(once_cell) => once_cell.get_or_init(detect_targets).await, + } + } +} + +/// If opts_targets is `Some`, then it will be parsed in the format of +/// `$target1,$target2,...`. +/// Otherwise, call `detect_targets` using `tokio::spawn` to detect targets. +/// +/// Since `detect_targets` internally spawns a process and wait for it, +/// it's pretty costy, it is recommended to run this fn ASAP and +/// reuse the result. +pub fn get_desired_targets(opts_targets: Option<&str>) -> DesiredTargets { + if let Some(targets) = opts_targets { + DesiredTargets::initialized(targets.split(',').map(|t| t.to_string()).collect()) + } else { + DesiredTargets::auto_detect() + } +} diff --git a/crates/detect-targets/src/detect.rs b/crates/detect-targets/src/detect.rs new file mode 100644 index 00000000..da023365 --- /dev/null +++ b/crates/detect-targets/src/detect.rs @@ -0,0 +1,88 @@ +use std::{ + io::{BufRead, Cursor}, + process::{Output, Stdio}, +}; + +use cfg_if::cfg_if; +use tokio::process::Command; + +cfg_if! { + if #[cfg(target_os = "linux")] { + mod linux; + } else if #[cfg(target_os = "macos")] { + mod macos; + } else if #[cfg(target_os = "windows")] { + mod windows; + } +} + +/// Detect the targets supported at runtime, +/// which might be different from `TARGET` which is detected +/// at compile-time. +/// +/// Return targets supported in the order of preference. +/// If target_os is linux and it support gnu, then it is preferred +/// to musl. +/// +/// If target_os is mac and it is aarch64, then aarch64 is preferred +/// to x86_64. +/// +/// Check [this issue](https://github.com/ryankurte/cargo-binstall/issues/155) +/// for more information. +pub async fn detect_targets() -> Vec { + if let Some(target) = get_target_from_rustc().await { + let mut v = vec![target]; + + cfg_if! { + if #[cfg(target_os = "linux")] { + if v[0].contains("gnu") { + v.push(v[0].replace("gnu", "musl")); + } + } else if #[cfg(target_os = "macos")] { + if &*v[0] == macos::AARCH64 { + v.push(macos::X86.into()); + } + } else if #[cfg(target_os = "windows")] { + v.extend(windows::detect_alternative_targets(&v[0])); + } + } + + v + } else { + cfg_if! { + if #[cfg(target_os = "linux")] { + linux::detect_targets_linux().await + } else if #[cfg(target_os = "macos")] { + macos::detect_targets_macos() + } else if #[cfg(target_os = "windows")] { + windows::detect_targets_windows() + } else { + vec![TARGET.into()] + } + } + } +} + +/// Figure out what the host target is using `rustc`. +/// If `rustc` is absent, then it would return `None`. +async fn get_target_from_rustc() -> Option { + let Output { status, stdout, .. } = Command::new("rustc") + .arg("-vV") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .ok()? + .wait_with_output() + .await + .ok()?; + + if !status.success() { + return None; + } + + Cursor::new(stdout) + .lines() + .filter_map(|line| line.ok()) + .find_map(|line| line.strip_prefix("host: ").map(|host| host.to_owned())) +} diff --git a/crates/detect-targets/src/detect/linux.rs b/crates/detect-targets/src/detect/linux.rs new file mode 100644 index 00000000..ebe635a6 --- /dev/null +++ b/crates/detect-targets/src/detect/linux.rs @@ -0,0 +1,86 @@ +use crate::TARGET; + +use std::process::{Output, Stdio}; + +use tokio::process::Command; + +pub(super) async fn detect_targets_linux() -> Vec { + let (abi, libc) = parse_abi_and_libc(); + + if let Libc::Glibc = libc { + // Glibc can only be dynamically linked. + // If we can run this binary, then it means that the target + // supports both glibc and musl. + return create_targets_str(&["gnu", "musl"], abi); + } + + if let Ok(Output { + status: _, + stdout, + stderr, + }) = Command::new("ldd") + .arg("--version") + .stdin(Stdio::null()) + .output() + .await + { + let libc_version = if let Some(libc_version) = parse_libc_version_from_ldd_output(&stdout) { + libc_version + } else if let Some(libc_version) = parse_libc_version_from_ldd_output(&stderr) { + libc_version + } else { + return vec![create_target_str("musl", abi)]; + }; + + if libc_version == "gnu" { + return create_targets_str(&["gnu", "musl"], abi); + } + } + + // Fallback to using musl + vec![create_target_str("musl", abi)] +} + +fn parse_libc_version_from_ldd_output(output: &[u8]) -> Option<&'static str> { + let s = String::from_utf8_lossy(output); + if s.contains("musl libc") { + Some("musl") + } else if s.contains("GLIBC") { + Some("gnu") + } else { + None + } +} + +enum Libc { + Glibc, + Musl, +} + +fn parse_abi_and_libc() -> (&'static str, Libc) { + let last = TARGET.rsplit_once('-').unwrap().1; + + if let Some(libc_version) = last.strip_prefix("musl") { + (libc_version, Libc::Musl) + } else if let Some(libc_version) = last.strip_prefix("gnu") { + (libc_version, Libc::Glibc) + } else { + panic!("Unrecognized libc") + } +} + +fn create_target_str(libc_version: &str, abi: &str) -> String { + let prefix = TARGET + .rsplit_once('-') + .expect("unwrap: TARGET always has a -") + .0; + + format!("{prefix}-{libc_version}{abi}") +} + +fn create_targets_str(libc_versions: &[&str], abi: &str) -> Vec { + libc_versions + .iter() + .map(|libc_version| create_target_str(libc_version, abi)) + .collect() +} diff --git a/crates/detect-targets/src/detect/macos.rs b/crates/detect-targets/src/detect/macos.rs new file mode 100644 index 00000000..67e31ccf --- /dev/null +++ b/crates/detect-targets/src/detect/macos.rs @@ -0,0 +1,15 @@ +use crate::TARGET; +use guess_host_triple::guess_host_triple; + +pub(super) const AARCH64: &str = "aarch64-apple-darwin"; +pub(super) const X86: &str = "x86_64-apple-darwin"; + +pub(super) fn detect_targets_macos() -> Vec { + let mut targets = vec![guess_host_triple().unwrap_or(TARGET).to_string()]; + + if targets[0] == AARCH64 { + targets.push(X86.into()); + } + + targets +} diff --git a/crates/detect-targets/src/detect/windows.rs b/crates/detect-targets/src/detect/windows.rs new file mode 100644 index 00000000..4efd5f2c --- /dev/null +++ b/crates/detect-targets/src/detect/windows.rs @@ -0,0 +1,17 @@ +use crate::TARGET; +use guess_host_triple::guess_host_triple; + +pub(super) fn detect_alternative_targets(target: &str) -> Option { + let (prefix, abi) = target.rsplit_once('-')?; + + // detect abi in ["gnu", "gnullvm", ...] + (abi != "msvc").then(|| format!("{prefix}-msvc")) +} + +pub(super) fn detect_targets_windows() -> Vec { + let mut targets = vec![guess_host_triple().unwrap_or(TARGET).to_string()]; + + targets.extend(detect_alternative_targets(&targets[0])); + + targets +} diff --git a/crates/detect-targets/src/lib.rs b/crates/detect-targets/src/lib.rs new file mode 100644 index 00000000..1ba7831b --- /dev/null +++ b/crates/detect-targets/src/lib.rs @@ -0,0 +1,65 @@ +//! Detect the target at the runtime. +//! +//! Example use cases: +//! - The binary is built with musl libc to run on anywhere, but +//! the runtime supports glibc. +//! - The binary is built for x86_64-apple-darwin, but run on +//! aarch64-apple-darwin. +//! +//! This crate provides two API: +//! - [`detect_targets`] provides the API to get the target +//! at runtime, but the code is run on the current thread. +//! - [`get_desired_targets`] provides the API to either +//! parse `$target1,$target2,...` override provided by the users, +//! or run [`detect_targets`] in the background using [`tokio::spawn`]. +//! +//! # Example +//! +//! `detect_targets`: +//! +//! ```rust +//! use detect_targets::detect_targets; +//! # #[tokio::main(flavor = "current_thread")] +//! # async fn main() { +//! +//! let targets = detect_targets().await; +//! eprintln!("Your platform supports targets: {targets:#?}"); +//! # } +//! ``` +//! +//! `get_desired_targets` with user override: +//! +//! ```rust +//! use detect_targets::get_desired_targets; +//! # #[tokio::main(flavor = "current_thread")] +//! # async fn main() { +//! +//! assert_eq!( +//! get_desired_targets(Some("x86_64-apple-darwin,aarch64-apple-darwin")).get().await, +//! &["x86_64-apple-darwin", "aarch64-apple-darwin"], +//! ); +//! # } +//! ``` +//! +//! `get_desired_targets` without user override: +//! +//! ```rust +//! use detect_targets::get_desired_targets; +//! # #[tokio::main(flavor = "current_thread")] +//! # async fn main() { +//! +//! eprintln!( +//! "Your platform supports targets: {:#?}", +//! get_desired_targets(None).get().await +//! ); +//! # } +//! ``` + +mod detect; +pub use detect::detect_targets; + +mod desired_targets; +pub use desired_targets::{get_desired_targets, DesiredTargets}; + +/// Compiled target triple, used as default for binary fetching +pub const TARGET: &str = env!("TARGET"); diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 977e82b5..6634a31b 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -17,6 +17,7 @@ cargo_toml = "0.11.5" clap = { version = "3.2.17", features = ["derive"] } compact_str = { version = "0.5.2", features = ["serde"] } crates_io_api = { version = "0.8.0", default-features = false } +detect-targets = { version = "0.1.0", path = "../detect-targets" } flate2 = { version = "1.0.24", default-features = false } flock = { version = "0.1.0", path = "../flock" } futures-util = { version = "0.3.23", default-features = false } @@ -57,9 +58,6 @@ zip = { version = "0.6.2", default-features = false, features = ["deflate", "bzi # otherwise there will be a link conflict. zstd = { version = "0.10.0", default-features = false } -[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] -guess_host_triple = "0.1.3" - [dev-dependencies] env_logger = "0.9.0" diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 261a008b..273f4d97 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -6,4 +6,5 @@ pub mod fs; pub mod helpers; pub mod manifests; pub mod ops; -pub mod targets; + +pub use detect_targets::{get_desired_targets, DesiredTargets}; diff --git a/crates/lib/src/manifests/binstall_crates_v1.rs b/crates/lib/src/manifests/binstall_crates_v1.rs index 6593b81a..78b9ab02 100644 --- a/crates/lib/src/manifests/binstall_crates_v1.rs +++ b/crates/lib/src/manifests/binstall_crates_v1.rs @@ -170,9 +170,10 @@ impl<'a> IntoIterator for &'a Records { #[cfg(test)] mod test { use super::*; - use crate::{manifests::crate_info::CrateSource, targets::TARGET}; + use crate::manifests::crate_info::CrateSource; use compact_str::CompactString; + use detect_targets::TARGET; use semver::Version; use tempfile::NamedTempFile; diff --git a/crates/lib/src/manifests/cargo_crates_v1.rs b/crates/lib/src/manifests/cargo_crates_v1.rs index d197e8b4..2f9b8296 100644 --- a/crates/lib/src/manifests/cargo_crates_v1.rs +++ b/crates/lib/src/manifests/cargo_crates_v1.rs @@ -130,8 +130,9 @@ pub enum CratesTomlParseError { #[cfg(test)] mod tests { use super::*; - use crate::{manifests::crate_info::CrateSource, targets::TARGET}; + use crate::manifests::crate_info::CrateSource; + use detect_targets::TARGET; use semver::Version; use tempfile::TempDir; diff --git a/crates/lib/src/ops.rs b/crates/lib/src/ops.rs index 4274df96..b7475750 100644 --- a/crates/lib/src/ops.rs +++ b/crates/lib/src/ops.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use semver::VersionReq; -use crate::{manifests::cargo_toml_binstall::PkgOverride, targets::DesiredTargets}; +use crate::{manifests::cargo_toml_binstall::PkgOverride, DesiredTargets}; pub mod install; pub mod resolve; diff --git a/crates/lib/src/targets.rs b/crates/lib/src/targets.rs deleted file mode 100644 index 57723bba..00000000 --- a/crates/lib/src/targets.rs +++ /dev/null @@ -1,253 +0,0 @@ -use std::{ - io::{BufRead, Cursor}, - process::Output, - sync::Arc, -}; - -use tokio::{process::Command, sync::OnceCell}; - -/// Compiled target triple, used as default for binary fetching -pub const TARGET: &str = env!("TARGET"); - -#[derive(Debug)] -enum DesiredTargetsInner { - AutoDetect(Arc>>), - Initialized(Vec), -} - -#[derive(Debug)] -pub struct DesiredTargets(DesiredTargetsInner); - -impl DesiredTargets { - fn initialized(targets: Vec) -> Self { - Self(DesiredTargetsInner::Initialized(targets)) - } - - fn auto_detect() -> Self { - let arc = Arc::new(OnceCell::new()); - - let once_cell = arc.clone(); - tokio::spawn(async move { - once_cell.get_or_init(detect_targets).await; - }); - - Self(DesiredTargetsInner::AutoDetect(arc)) - } - - pub async fn get(&self) -> &[String] { - use DesiredTargetsInner::*; - - match &self.0 { - Initialized(targets) => targets, - - // This will mostly just wait for the spawned task, - // on rare occausion though, it will poll the future - // returned by `detect_targets`. - AutoDetect(once_cell) => once_cell.get_or_init(detect_targets).await, - } - } -} - -/// If opts_targets is `Some`, then it will be used. -/// Otherwise, call `detect_targets` using `tokio::spawn` to detect targets. -/// -/// Since `detect_targets` internally spawns a process and wait for it, -/// it's pretty costy. -/// -/// Calling it through `tokio::spawn` would enable other tasks, such as -/// fetching the crate tarballs, to be executed concurrently. -pub fn get_desired_targets(opts_targets: &Option) -> DesiredTargets { - if let Some(targets) = opts_targets.as_ref() { - DesiredTargets::initialized(targets.split(',').map(|t| t.to_string()).collect()) - } else { - DesiredTargets::auto_detect() - } -} - -/// Detect the targets supported at runtime, -/// which might be different from `TARGET` which is detected -/// at compile-time. -/// -/// Return targets supported in the order of preference. -/// If target_os is linux and it support gnu, then it is preferred -/// to musl. -/// -/// If target_os is mac and it is aarch64, then aarch64 is preferred -/// to x86_64. -/// -/// Check [this issue](https://github.com/ryankurte/cargo-binstall/issues/155) -/// for more information. -pub async fn detect_targets() -> Vec { - if let Some(target) = get_target_from_rustc().await { - let mut v = vec![target]; - - #[cfg(target_os = "linux")] - if v[0].contains("gnu") { - v.push(v[0].replace("gnu", "musl")); - } - - #[cfg(target_os = "macos")] - if &*v[0] == macos::AARCH64 { - v.push(macos::X86.into()); - } - - #[cfg(target_os = "windows")] - v.extend(windows::detect_alternative_targets(&v[0])); - - v - } else { - #[cfg(target_os = "linux")] - { - linux::detect_targets_linux().await - } - #[cfg(target_os = "macos")] - { - macos::detect_targets_macos() - } - #[cfg(target_os = "windows")] - { - windows::detect_targets_windows() - } - #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] - { - vec![TARGET.into()] - } - } -} - -/// Figure out what the host target is using `rustc`. -/// If `rustc` is absent, then it would return `None`. -async fn get_target_from_rustc() -> Option { - let Output { status, stdout, .. } = Command::new("rustc").arg("-vV").output().await.ok()?; - if !status.success() { - return None; - } - - Cursor::new(stdout) - .lines() - .filter_map(|line| line.ok()) - .find_map(|line| line.strip_prefix("host: ").map(|host| host.to_owned())) -} - -#[cfg(target_os = "linux")] -mod linux { - use super::{Command, Output, TARGET}; - - pub(super) async fn detect_targets_linux() -> Vec { - let (abi, libc) = parse_abi_and_libc(); - - if let Libc::Glibc = libc { - // Glibc can only be dynamically linked. - // If we can run this binary, then it means that the target - // supports both glibc and musl. - return create_targets_str(&["gnu", "musl"], abi); - } - - if let Ok(Output { - status: _, - stdout, - stderr, - }) = Command::new("ldd").arg("--version").output().await - { - let libc_version = - if let Some(libc_version) = parse_libc_version_from_ldd_output(&stdout) { - libc_version - } else if let Some(libc_version) = parse_libc_version_from_ldd_output(&stderr) { - libc_version - } else { - return vec![create_target_str("musl", abi)]; - }; - - if libc_version == "gnu" { - return create_targets_str(&["gnu", "musl"], abi); - } - } - - // Fallback to using musl - vec![create_target_str("musl", abi)] - } - - fn parse_libc_version_from_ldd_output(output: &[u8]) -> Option<&'static str> { - let s = String::from_utf8_lossy(output); - if s.contains("musl libc") { - Some("musl") - } else if s.contains("GLIBC") { - Some("gnu") - } else { - None - } - } - - enum Libc { - Glibc, - Musl, - } - - fn parse_abi_and_libc() -> (&'static str, Libc) { - let last = TARGET.rsplit_once('-').unwrap().1; - - if let Some(libc_version) = last.strip_prefix("musl") { - (libc_version, Libc::Musl) - } else if let Some(libc_version) = last.strip_prefix("gnu") { - (libc_version, Libc::Glibc) - } else { - panic!("Unrecognized libc") - } - } - - fn create_target_str(libc_version: &str, abi: &str) -> String { - let prefix = TARGET - .rsplit_once('-') - .expect("unwrap: TARGET always has a -") - .0; - - format!("{prefix}-{libc_version}{abi}") - } - - fn create_targets_str(libc_versions: &[&str], abi: &str) -> Vec { - libc_versions - .iter() - .map(|libc_version| create_target_str(libc_version, abi)) - .collect() - } -} - -#[cfg(target_os = "macos")] -mod macos { - use super::TARGET; - use guess_host_triple::guess_host_triple; - - pub(super) const AARCH64: &str = "aarch64-apple-darwin"; - pub(super) const X86: &str = "x86_64-apple-darwin"; - - pub(super) fn detect_targets_macos() -> Vec { - let mut targets = vec![guess_host_triple().unwrap_or(TARGET).to_string()]; - - if targets[0] == AARCH64 { - targets.push(X86.into()); - } - - targets - } -} - -#[cfg(target_os = "windows")] -mod windows { - use super::TARGET; - use guess_host_triple::guess_host_triple; - - pub(super) fn detect_alternative_targets(target: &str) -> Option { - let (prefix, abi) = target.rsplit_once('-').expect("Invalid target triple"); - - // detect abi in ["gnu", "gnullvm", ...] - (abi != "msvc").then(|| format!("{prefix}-msvc")) - } - - pub(super) fn detect_targets_windows() -> Vec { - let mut targets = vec![guess_host_triple().unwrap_or(TARGET).to_string()]; - - targets.extend(detect_alternative_targets(&targets[0])); - - targets - } -}