From f7251b0967b6b8fa1efa05bcff946e8b32720734 Mon Sep 17 00:00:00 2001 From: Be Wilson Date: Tue, 26 Jul 2022 02:26:32 -0500 Subject: [PATCH 1/3] handle file paths to libraries 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}" --- src/lib.rs | 238 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 185 insertions(+), 53 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d52c6ad..e509519 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,6 +95,7 @@ pub struct Config { pub struct Library { pub libs: Vec, pub link_paths: Vec, + pub link_files: Vec, pub frameworks: Vec, pub framework_paths: Vec, pub include_paths: Vec, @@ -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(), @@ -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; } @@ -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); + } + } + } + } + } } } @@ -776,60 +865,103 @@ fn split_flags(output: &[u8]) -> Vec { 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"); + } } From ec0c408ee8c9f5fd0adb0e23274f3152b48cc81b Mon Sep 17 00:00:00 2001 From: Be Wilson Date: Fri, 21 Oct 2022 17:20:48 -0500 Subject: [PATCH 2/3] document fields of Library struct --- src/lib.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index e509519..0778b69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,15 +93,26 @@ pub struct Config { #[derive(Clone, Debug)] pub struct Library { + /// Libraries specified by -l pub libs: Vec, + /// Library search paths specified by -L pub link_paths: Vec, + /// Library file paths specified without -l pub link_files: Vec, + /// Darwin frameworks specified by -framework pub frameworks: Vec, + /// Darwin framework search paths specified by -F pub framework_paths: Vec, + /// C/C++ header include paths specified by -I pub include_paths: Vec, + /// Linker options specified by -Wl pub ld_args: Vec>, + /// C/C++ definitions specified by -D pub defines: HashMap>, + /// 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: (), } From a9d0132be76205cd92d282bf67dc135a06074608 Mon Sep 17 00:00:00 2001 From: Be Wilson Date: Sat, 22 Oct 2022 10:07:18 -0500 Subject: [PATCH 3/3] remove `extern crate pkg_config` from examples No longer needed as of Rust 2018 --- src/lib.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0778b69..e9395be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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(); //! } @@ -49,8 +47,6 @@ //! recommended): //! //! ```no_run -//! extern crate pkg_config; -//! //! fn main() { //! pkg_config::probe_library("foo").unwrap(); //! } @@ -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(); //! }