Skip to content

handle file paths to libraries #134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 25, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 196 additions & 59 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@
//! Find the system library named `foo`, with minimum version 1.2.3:
//!
//! ```no_run
//! extern crate pkg_config;
//!
//! fn main() {
//! pkg_config::Config::new().atleast_version("1.2.3").probe("foo").unwrap();
//! }
Expand All @@ -49,8 +47,6 @@
//! recommended):
//!
//! ```no_run
//! extern crate pkg_config;
//!
//! fn main() {
//! pkg_config::probe_library("foo").unwrap();
//! }
Expand All @@ -59,8 +55,6 @@
//! Configure how library `foo` is linked to.
//!
//! ```no_run
//! extern crate pkg_config;
//!
//! fn main() {
//! pkg_config::Config::new().atleast_version("1.2.3").statik(true).probe("foo").unwrap();
//! }
Expand Down Expand Up @@ -93,14 +87,26 @@ pub struct Config {

#[derive(Clone, Debug)]
pub struct Library {
/// Libraries specified by -l
pub libs: Vec<String>,
/// Library search paths specified by -L
pub link_paths: Vec<PathBuf>,
/// Library file paths specified without -l
pub link_files: Vec<PathBuf>,
/// Darwin frameworks specified by -framework
pub frameworks: Vec<String>,
/// Darwin framework search paths specified by -F
pub framework_paths: Vec<PathBuf>,
/// C/C++ header include paths specified by -I
pub include_paths: Vec<PathBuf>,
/// Linker options specified by -Wl
pub ld_args: Vec<Vec<String>>,
/// C/C++ definitions specified by -D
pub defines: HashMap<String, Option<String>>,
/// Version specified by .pc file's Version field
pub version: String,
/// Ensure that this struct can only be created via its private `[Library::new]` constructor.
/// Users of this crate can only access the struct via `[Config::probe]`.
_priv: (),
}

Expand Down Expand Up @@ -558,6 +564,7 @@ impl Library {
Library {
libs: Vec::new(),
link_paths: Vec::new(),
link_files: Vec::new(),
include_paths: Vec::new(),
ld_args: Vec::new(),
frameworks: Vec::new(),
Expand All @@ -568,9 +575,67 @@ impl Library {
}
}

/// Extract the &str to pass to cargo:rustc-link-lib from a filename (just the file name, not including directories)
/// using target-specific logic.
fn extract_lib_from_filename<'a>(target: &str, filename: &'a str) -> Option<&'a str> {
fn test_suffixes<'b>(filename: &'b str, suffixes: &[&str]) -> Option<&'b str> {
for suffix in suffixes {
if filename.ends_with(suffix) {
return Some(&filename[..filename.len() - suffix.len()]);
}
}
None
}

let prefix = "lib";
if target.contains("msvc") {
// According to link.exe documentation:
// https://learn.microsoft.com/en-us/cpp/build/reference/link-input-files?view=msvc-170
//
// LINK doesn't use file extensions to make assumptions about the contents of a file.
// Instead, LINK examines each input file to determine what kind of file it is.
//
// However, rustc appends `.lib` to the string it receives from the -l command line argument,
// which it receives from Cargo via cargo:rustc-link-lib:
// https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L828
// https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L843
// So the only file extension that works for MSVC targets is `.lib`
return test_suffixes(filename, &[".lib"]);
} else if target.contains("windows") && target.contains("gnu") {
// GNU targets for Windows, including gnullvm, use `LinkerFlavor::Gcc` internally in rustc,
// which tells rustc to use the GNU linker. rustc does not prepend/append to the string it
// receives via the -l command line argument before passing it to the linker:
// https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L446
// https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L457
// GNU ld can work with more types of files than just the .lib files that MSVC's link.exe needs.
// GNU ld will prepend the `lib` prefix to the filename if necessary, so it is okay to remove
// the `lib` prefix from the filename. The `.a` suffix *requires* the `lib` prefix.
// https://sourceware.org/binutils/docs-2.39/ld.html#index-direct-linking-to-a-dll
if filename.starts_with(prefix) {
let filename = &filename[prefix.len()..];
return test_suffixes(filename, &[".dll.a", ".dll", ".lib", ".a"]);
} else {
return test_suffixes(filename, &[".dll.a", ".dll", ".lib"]);
}
} else if target.contains("apple") {
if filename.starts_with(prefix) {
let filename = &filename[prefix.len()..];
return test_suffixes(filename, &[".a", ".so", ".dylib"]);
}
return None;
} else {
if filename.starts_with(prefix) {
let filename = &filename[prefix.len()..];
return test_suffixes(filename, &[".a", ".so"]);
}
return None;
}
}

fn parse_libs_cflags(&mut self, name: &str, output: &[u8], config: &Config) {
let mut is_msvc = false;
if let Ok(target) = env::var("TARGET") {
let target = env::var("TARGET");
if let Ok(target) = &target {
if target.contains("msvc") {
is_msvc = true;
}
Expand Down Expand Up @@ -670,7 +735,36 @@ impl Library {
self.include_paths.push(PathBuf::from(inc));
}
}
_ => (),
_ => {
let path = std::path::Path::new(part);
if path.is_file() {
// Cargo doesn't have a means to directly specify a file path to link,
// so split up the path into the parent directory and library name.
// TODO: pass file path directly when link-arg library type is stabilized
// https://github.com/rust-lang/rust/issues/99427
if let (Some(dir), Some(file_name), Ok(target)) =
(path.parent(), path.file_name(), &target)
{
match Self::extract_lib_from_filename(
target,
&file_name.to_string_lossy(),
) {
Some(lib_basename) => {
let link_search =
format!("rustc-link-search={}", dir.display());
config.print_metadata(&link_search);

let link_lib = format!("rustc-link-lib={}", lib_basename);
config.print_metadata(&link_lib);
self.link_files.push(PathBuf::from(path));
}
None => {
println!("cargo:warning=File path {} found in pkg-config file for {}, but could not extract library base name to pass to linker command line", path.display(), name);
}
}
}
}
}
}
}

Expand Down Expand Up @@ -776,60 +870,103 @@ fn split_flags(output: &[u8]) -> Vec<String> {
words
}

#[test]
#[cfg(target_os = "macos")]
fn system_library_mac_test() {
use std::path::Path;

let system_roots = vec![PathBuf::from("/Library"), PathBuf::from("/System")];

assert!(!is_static_available(
"PluginManager",
&system_roots,
&[PathBuf::from("/Library/Frameworks")]
));
assert!(!is_static_available(
"python2.7",
&system_roots,
&[PathBuf::from(
"/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config"
)]
));
assert!(!is_static_available(
"ffi_convenience",
&system_roots,
&[PathBuf::from(
"/Library/Ruby/Gems/2.0.0/gems/ffi-1.9.10/ext/ffi_c/libffi-x86_64/.libs"
)]
));

// Homebrew is in /usr/local, and it's not a part of the OS
if Path::new("/usr/local/lib/libpng16.a").exists() {
assert!(is_static_available(
"png16",
#[cfg(test)]
mod tests {
use super::*;

#[test]
#[cfg(target_os = "macos")]
fn system_library_mac_test() {
use std::path::Path;

let system_roots = vec![PathBuf::from("/Library"), PathBuf::from("/System")];

assert!(!is_static_available(
"PluginManager",
&system_roots,
&[PathBuf::from("/usr/local/lib")]
&[PathBuf::from("/Library/Frameworks")]
));
assert!(!is_static_available(
"python2.7",
&system_roots,
&[PathBuf::from(
"/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config"
)]
));
assert!(!is_static_available(
"ffi_convenience",
&system_roots,
&[PathBuf::from(
"/Library/Ruby/Gems/2.0.0/gems/ffi-1.9.10/ext/ffi_c/libffi-x86_64/.libs"
)]
));

// Homebrew is in /usr/local, and it's not a part of the OS
if Path::new("/usr/local/lib/libpng16.a").exists() {
assert!(is_static_available(
"png16",
&system_roots,
&[PathBuf::from("/usr/local/lib")]
));

let libpng = Config::new()
.range_version("1".."99")
.probe("libpng16")
.unwrap();
assert!(libpng.version.find('\n').is_none());
}
}

let libpng = Config::new()
.range_version("1".."99")
.probe("libpng16")
.unwrap();
assert!(libpng.version.find('\n').is_none());
#[test]
#[cfg(target_os = "linux")]
fn system_library_linux_test() {
assert!(!is_static_available(
"util",
&[PathBuf::from("/usr")],
&[PathBuf::from("/usr/lib/x86_64-linux-gnu")]
));
assert!(!is_static_available(
"dialog",
&[PathBuf::from("/usr")],
&[PathBuf::from("/usr/lib")]
));
}
}

#[test]
#[cfg(target_os = "linux")]
fn system_library_linux_test() {
assert!(!is_static_available(
"util",
&[PathBuf::from("/usr")],
&[PathBuf::from("/usr/lib/x86_64-linux-gnu")]
));
assert!(!is_static_available(
"dialog",
&[PathBuf::from("/usr")],
&[PathBuf::from("/usr/lib")]
));
fn test_library_filename(target: &str, filename: &str) {
assert_eq!(
Library::extract_lib_from_filename(target, filename),
Some("foo")
);
}

#[test]
fn link_filename_linux() {
let target = "x86_64-unknown-linux-gnu";
test_library_filename(target, "libfoo.a");
test_library_filename(target, "libfoo.so");
}

#[test]
fn link_filename_apple() {
let target = "x86_64-apple-darwin";
test_library_filename(target, "libfoo.a");
test_library_filename(target, "libfoo.so");
test_library_filename(target, "libfoo.dylib");
}

#[test]
fn link_filename_msvc() {
let target = "x86_64-pc-windows-msvc";
// static and dynamic libraries have the same .lib suffix
test_library_filename(target, "foo.lib");
}

#[test]
fn link_filename_mingw() {
let target = "x86_64-pc-windows-gnu";
test_library_filename(target, "foo.lib");
test_library_filename(target, "libfoo.a");
test_library_filename(target, "foo.dll");
test_library_filename(target, "foo.dll.a");
}
}