Skip to content

Commit aaeadb5

Browse files
committed
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}"
1 parent 28b8442 commit aaeadb5

File tree

1 file changed

+186
-53
lines changed

1 file changed

+186
-53
lines changed

src/lib.rs

Lines changed: 186 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ pub struct Config {
9595
pub struct Library {
9696
pub libs: Vec<String>,
9797
pub link_paths: Vec<PathBuf>,
98+
pub link_files: Vec<PathBuf>,
9899
pub frameworks: Vec<String>,
99100
pub framework_paths: Vec<PathBuf>,
100101
pub include_paths: Vec<PathBuf>,
@@ -558,6 +559,7 @@ impl Library {
558559
Library {
559560
libs: Vec::new(),
560561
link_paths: Vec::new(),
562+
link_files: Vec::new(),
561563
include_paths: Vec::new(),
562564
ld_args: Vec::new(),
563565
frameworks: Vec::new(),
@@ -568,9 +570,67 @@ impl Library {
568570
}
569571
}
570572

573+
/// Extract the &str to pass to cargo:rustc-link-lib from a filename (just the file name, not including directories)
574+
/// using target-specific logic.
575+
fn extract_lib_from_filename<'a>(target: &str, filename: &'a str) -> Option<&'a str> {
576+
fn test_suffixes<'b>(filename: &'b str, suffixes: &[&str]) -> Option<&'b str> {
577+
for suffix in suffixes {
578+
if filename.ends_with(suffix) {
579+
return Some(&filename[..filename.len() - suffix.len()]);
580+
}
581+
}
582+
None
583+
}
584+
585+
let prefix = "lib";
586+
if target.contains("msvc") {
587+
// According to link.exe documentation:
588+
// https://learn.microsoft.com/en-us/cpp/build/reference/link-input-files?view=msvc-170
589+
//
590+
// LINK doesn't use file extensions to make assumptions about the contents of a file.
591+
// Instead, LINK examines each input file to determine what kind of file it is.
592+
//
593+
// However, rustc appends `.lib` to the string it receives from the -l command line argument,
594+
// which it receives from Cargo via cargo:rustc-link-lib:
595+
// https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L828
596+
// https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L843
597+
// So the only file extension that works for MSVC targets is `.lib`
598+
return test_suffixes(filename, &[".lib"]);
599+
} else if target.contains("windows") && target.contains("gnu") {
600+
// GNU targets for Windows, including gnullvm, use `LinkerFlavor::Gcc` internally in rustc,
601+
// which tells rustc to use the GNU linker. rustc does not prepend/append to the string it
602+
// receives via the -l command line argument before passing it to the linker:
603+
// https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L446
604+
// https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L457
605+
// GNU ld can work with more types of files than just the .lib files that MSVC's link.exe needs.
606+
// GNU ld will prepend the `lib` prefix to the filename if necessary, so it is okay to remove
607+
// the `lib` prefix from the filename. The `.a` suffix *requires* the `lib` prefix.
608+
// https://sourceware.org/binutils/docs-2.39/ld.html#index-direct-linking-to-a-dll
609+
if filename.starts_with(prefix) {
610+
let filename = &filename[prefix.len()..];
611+
return test_suffixes(filename, &[".dll.a", ".dll", ".lib", ".a"]);
612+
} else {
613+
return test_suffixes(filename, &[".dll.a", ".dll", ".lib"]);
614+
}
615+
} else if target.contains("apple") {
616+
if filename.starts_with(prefix) {
617+
let filename = &filename[prefix.len()..];
618+
return test_suffixes(filename, &[".a", ".so", ".dylib"]);
619+
}
620+
return None;
621+
} else {
622+
if filename.starts_with(prefix) {
623+
let filename = &filename[prefix.len()..];
624+
return test_suffixes(filename, &[".a", ".so"]);
625+
}
626+
return None;
627+
}
628+
}
629+
571630
fn parse_libs_cflags(&mut self, name: &str, output: &[u8], config: &Config) {
572631
let mut is_msvc = false;
573-
if let Ok(target) = env::var("TARGET") {
632+
let target = env::var("TARGET");
633+
if let Ok(target) = &target {
574634
if target.contains("msvc") {
575635
is_msvc = true;
576636
}
@@ -670,7 +730,36 @@ impl Library {
670730
self.include_paths.push(PathBuf::from(inc));
671731
}
672732
}
673-
_ => (),
733+
_ => {
734+
let path = std::path::Path::new(part);
735+
if path.is_file() {
736+
// Cargo doesn't have a means to directly specify a file path to link,
737+
// so split up the path into the parent directory and library name.
738+
// TODO: pass file path directly when link-arg library type is stabilized
739+
// https://github.com/rust-lang/rust/issues/99427
740+
if let (Some(dir), Some(file_name), Ok(target)) =
741+
(path.parent(), path.file_name(), &target)
742+
{
743+
match Self::extract_lib_from_filename(
744+
target,
745+
&file_name.to_string_lossy(),
746+
) {
747+
Some(lib_basename) => {
748+
let link_search =
749+
format!("rustc-link-search={}", dir.display());
750+
config.print_metadata(&link_search);
751+
752+
let link_lib = format!("rustc-link-lib={}", lib_basename);
753+
config.print_metadata(&link_lib);
754+
self.link_files.push(PathBuf::from(path));
755+
}
756+
None => {
757+
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);
758+
}
759+
}
760+
}
761+
}
762+
}
674763
}
675764
}
676765

@@ -776,60 +865,104 @@ fn split_flags(output: &[u8]) -> Vec<String> {
776865
words
777866
}
778867

779-
#[test]
780-
#[cfg(target_os = "macos")]
781-
fn system_library_mac_test() {
782-
use std::path::Path;
783-
784-
let system_roots = vec![PathBuf::from("/Library"), PathBuf::from("/System")];
785-
786-
assert!(!is_static_available(
787-
"PluginManager",
788-
&system_roots,
789-
&[PathBuf::from("/Library/Frameworks")]
790-
));
791-
assert!(!is_static_available(
792-
"python2.7",
793-
&system_roots,
794-
&[PathBuf::from(
795-
"/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config"
796-
)]
797-
));
798-
assert!(!is_static_available(
799-
"ffi_convenience",
800-
&system_roots,
801-
&[PathBuf::from(
802-
"/Library/Ruby/Gems/2.0.0/gems/ffi-1.9.10/ext/ffi_c/libffi-x86_64/.libs"
803-
)]
804-
));
805-
806-
// Homebrew is in /usr/local, and it's not a part of the OS
807-
if Path::new("/usr/local/lib/libpng16.a").exists() {
808-
assert!(is_static_available(
809-
"png16",
868+
#[cfg(test)]
869+
mod tests {
870+
use super::*;
871+
872+
#[test]
873+
#[cfg(target_os = "macos")]
874+
fn system_library_mac_test() {
875+
use std::path::Path;
876+
877+
let system_roots = vec![PathBuf::from("/Library"), PathBuf::from("/System")];
878+
879+
assert!(!is_static_available(
880+
"PluginManager",
881+
&system_roots,
882+
&[PathBuf::from("/Library/Frameworks")]
883+
));
884+
assert!(!is_static_available(
885+
"python2.7",
886+
&system_roots,
887+
&[PathBuf::from(
888+
"/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config"
889+
)]
890+
));
891+
assert!(!is_static_available(
892+
"ffi_convenience",
810893
&system_roots,
811-
&[PathBuf::from("/usr/local/lib")]
894+
&[PathBuf::from(
895+
"/Library/Ruby/Gems/2.0.0/gems/ffi-1.9.10/ext/ffi_c/libffi-x86_64/.libs"
896+
)]
812897
));
813898

814-
let libpng = Config::new()
815-
.range_version("1".."99")
816-
.probe("libpng16")
817-
.unwrap();
818-
assert!(libpng.version.find('\n').is_none());
899+
// Homebrew is in /usr/local, and it's not a part of the OS
900+
if Path::new("/usr/local/lib/libpng16.a").exists() {
901+
assert!(is_static_available(
902+
"png16",
903+
&system_roots,
904+
&[PathBuf::from("/usr/local/lib")]
905+
));
906+
907+
let libpng = Config::new()
908+
.range_version("1".."99")
909+
.probe("libpng16")
910+
.unwrap();
911+
assert!(libpng.version.find('\n').is_none());
912+
}
913+
}
914+
915+
#[test]
916+
#[cfg(target_os = "linux")]
917+
fn system_library_linux_test() {
918+
assert!(!is_static_available(
919+
"util",
920+
&[PathBuf::from("/usr")],
921+
&[PathBuf::from("/usr/lib/x86_64-linux-gnu")]
922+
));
923+
assert!(!is_static_available(
924+
"dialog",
925+
&[PathBuf::from("/usr")],
926+
&[PathBuf::from("/usr/lib")]
927+
));
819928
}
820-
}
821929

822-
#[test]
823-
#[cfg(target_os = "linux")]
824-
fn system_library_linux_test() {
825-
assert!(!is_static_available(
826-
"util",
827-
&[PathBuf::from("/usr")],
828-
&[PathBuf::from("/usr/lib/x86_64-linux-gnu")]
829-
));
830-
assert!(!is_static_available(
831-
"dialog",
832-
&[PathBuf::from("/usr")],
833-
&[PathBuf::from("/usr/lib")]
834-
));
930+
#[track_caller]
931+
fn test_library_filename(target: &str, filename: &str) {
932+
assert_eq!(
933+
Library::extract_lib_from_filename(target, filename),
934+
Some("foo")
935+
);
936+
}
937+
938+
#[test]
939+
fn link_filename_linux() {
940+
let target = "x86_64-unknown-linux-gnu";
941+
test_library_filename(target, "libfoo.a");
942+
test_library_filename(target, "libfoo.so");
943+
}
944+
945+
#[test]
946+
fn link_filename_apple() {
947+
let target = "x86_64-apple-darwin";
948+
test_library_filename(target, "libfoo.a");
949+
test_library_filename(target, "libfoo.so");
950+
test_library_filename(target, "libfoo.dylib");
951+
}
952+
953+
#[test]
954+
fn link_filename_msvc() {
955+
let target = "x86_64-pc-windows-msvc";
956+
// static and dynamic libraries have the same .lib suffix
957+
test_library_filename(target, "foo.lib");
958+
}
959+
960+
#[test]
961+
fn link_filename_mingw() {
962+
let target = "x86_64-pc-windows-gnu";
963+
test_library_filename(target, "foo.lib");
964+
test_library_filename(target, "libfoo.a");
965+
test_library_filename(target, "foo.dll");
966+
test_library_filename(target, "foo.dll.a");
967+
}
835968
}

0 commit comments

Comments
 (0)