Skip to content

Commit

Permalink
handle file paths to libraries
Browse files Browse the repository at this point in the history
Typically pkgconfig files specify cflags for linking with -L and -l,
however, pkgconfig files can also specify paths to library files. For
example, building Qt5 statically on macOS generates the following
pkgconfig file. Notice the absolute path to libqtpcre.a in Libs.private:

prefix=/Users/be/qt5-installed
exec_prefix=${prefix}
libdir=${prefix}/lib
includedir=${prefix}/include

host_bins=${prefix}/bin
qt_config=debug_and_release release debug build_all c++11 c++14 c++17 c++1z concurrent dbus no-pkg-config reduce_exports release_tools static stl

Name: Qt5 Core
Description: Qt Core module
Version: 5.15.5
Libs: -L${libdir} -lQt5Core
Libs.private: -framework DiskArbitration -framework IOKit -lm -framework AppKit -framework Security -framework ApplicationServices -framework CoreServices -framework CoreFoundation -framework Foundation -lz /Users/be/sw/qt-everywhere-src-5.15.5/qtbase/lib/libqtpcre2.a
Cflags: -DQT_CORE_LIB -I${includedir}/QtCore -I${includedir}

Building Qt5 statically on macOS with vcpkg generates this pkgconfig
file which has a handful of file paths for libraries:

prefix=${pcfiledir}/../..

exec_prefix=${prefix}
libdir=${prefix}/lib
includedir=${prefix}/include/qt5

host_bins=${prefix}/tools/qt5/bin
qt_config=release c++11 c++14 c++17 c++1z concurrent dbus no-pkg-config reduce_exports static stl properties animation textcodec big_codecs codecs itemmodel proxymodel concatenatetablesproxymodel textdate datestring doubleconversion filesystemiterator filesystemwatcher gestures identityproxymodel library mimetype process statemachine regularexpression settings sharedmemory sortfilterproxymodel stringlistmodel systemsemaphore temporaryfile translation transposeproxymodel xmlstream xmlstreamreader xmlstreamwriter

Name: Qt5 Core
Description: Qt Core module
Version: 5.15.3

Libs: -L"${libdir}" -lQt5Core  -L"${prefix}/lib" -L"${prefix}/lib/manual-link" -framework DiskArbitration -framework IOKit -lm -framework AppKit -framework Security -framework ApplicationServices -framework CoreServices -framework CoreFoundation -framework Foundation ${prefix}/lib/libz.a -ldouble-conversion ${prefix}/lib/libicui18n.a ${prefix}/lib/libicutu.a ${prefix}/lib/libicuuc.a ${prefix}/lib/libicuio.a ${prefix}/lib/libicudata.a ${prefix}/lib/libpcre2-16.a -lzstd  ${prefix}/lib/libbz2.a ${prefix}/lib/libpng16.a ${prefix}/lib/libicui18n.a ${prefix}/lib/libicutu.a ${prefix}/lib/libicuuc.a ${prefix}/lib/libicuio.a ${prefix}/lib/libicudata.a ${prefix}/lib/libzstd.a
Cflags: -DQT_CORE_LIB -I"${includedir}/QtCore" -I"${includedir}"
  • Loading branch information
Be-ing committed Oct 25, 2022
1 parent 28b8442 commit f7251b0
Showing 1 changed file with 185 additions and 53 deletions.
238 changes: 185 additions & 53 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ pub struct Config {
pub struct Library {
pub libs: Vec<String>,
pub link_paths: Vec<PathBuf>,
pub link_files: Vec<PathBuf>,
pub frameworks: Vec<String>,
pub framework_paths: Vec<PathBuf>,
pub include_paths: Vec<PathBuf>,
Expand Down Expand Up @@ -558,6 +559,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 +570,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 +730,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 +865,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("/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("/usr/local/lib")]
&[PathBuf::from(
"/Library/Ruby/Gems/2.0.0/gems/ffi-1.9.10/ext/ffi_c/libffi-x86_64/.libs"
)]
));

let libpng = Config::new()
.range_version("1".."99")
.probe("libpng16")
.unwrap();
assert!(libpng.version.find('\n').is_none());
// 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());
}
}

#[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");
}
}

0 comments on commit f7251b0

Please sign in to comment.