From f690598a79e9fc4e40c856823653cdad6953d729 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Tue, 23 May 2023 16:12:58 +0800 Subject: [PATCH] Support `-static` and `-dynamic` `.lib` suffixes on Windows (#13473) --- src/compiler/crystal/compiler.cr | 31 ++++++++--- src/compiler/crystal/loader/msvc.cr | 57 ++++++++++++++++----- src/crystal/system/win32/library_archive.cr | 7 ++- 3 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src/compiler/crystal/compiler.cr b/src/compiler/crystal/compiler.cr index af573e817a3d..9d8e3b156975 100644 --- a/src/compiler/crystal/compiler.cr +++ b/src/compiler/crystal/compiler.cr @@ -366,14 +366,29 @@ module Crystal @link_flags.try { |flags| link_args << flags } {% if flag?(:msvc) %} - if program.has_flag?("preview_dll") && !program.has_flag?("no_win32_delay_load") - # "LINK : warning LNK4199: /DELAYLOAD:foo.dll ignored; no imports found from foo.dll" - # it is harmless to skip this error because not all import libraries are always used, much - # less the individual DLLs they refer to - link_args << "/IGNORE:4199" - - Loader.search_dlls(Process.parse_arguments_windows(link_args.join(' '))).each do |dll| - link_args << "/DELAYLOAD:#{dll}" + unless @cross_compile + extra_suffix = program.has_flag?("preview_dll") ? "-dynamic" : "-static" + search_result = Loader.search_libraries(Process.parse_arguments_windows(link_args.join(' ').gsub('\n', ' ')), extra_suffix: extra_suffix) + if not_found = search_result.not_found? + error "Cannot locate the .lib files for the following libraries: #{not_found.join(", ")}" + end + + link_args = search_result.remaining_args.concat(search_result.library_paths).map { |arg| Process.quote_windows(arg) } + + if !program.has_flag?("no_win32_delay_load") + # "LINK : warning LNK4199: /DELAYLOAD:foo.dll ignored; no imports found from foo.dll" + # it is harmless to skip this error because not all import libraries are always used, much + # less the individual DLLs they refer to + link_args << "/IGNORE:4199" + + dlls = Set(String).new + search_result.library_paths.each do |library_path| + Crystal::System::LibraryArchive.imported_dlls(library_path).each do |dll| + dlls << dll.downcase + end + end + dlls.delete "kernel32.dll" + dlls.each { |dll| link_args << "/DELAYLOAD:#{dll}" } end end {% end %} diff --git a/src/compiler/crystal/loader/msvc.cr b/src/compiler/crystal/loader/msvc.cr index 54c3bd5d41af..9f9a592477f2 100644 --- a/src/compiler/crystal/loader/msvc.cr +++ b/src/compiler/crystal/loader/msvc.cr @@ -32,28 +32,53 @@ class Crystal::Loader end end - # Returns the list of DLLs imported from the libraries specified in the given - # linker arguments. Used by the compiler for delay-loaded DLL support. - def self.search_dlls(args : Array(String), *, search_paths : Array(String) = default_search_paths) : Set(String) - search_paths, libnames = parse_args(args, search_paths) - dlls = Set(String).new + struct SearchLibResult + getter library_paths = [] of String + getter remaining_args = [] of String + getter(not_found) { [] of String } + + def not_found? + @not_found + end + end + + # Extracts the command-line arguments from *args* that add libraries and + # expands them to their absolute paths. Returns a `SearchLibResult` with those + # expanded paths, plus unused arguments and libraries that were not found. + def self.search_libraries(args : Array(String), *, search_paths : Array(String) = default_search_paths, extra_suffix : String? = nil) : SearchLibResult + result = SearchLibResult.new + search_paths, libnames = parse_args(args, search_paths, remaining: result.remaining_args) libnames.each do |libname| - search_paths.each do |directory| - library_path = File.join(directory, library_filename(libname)) - next unless File.file?(library_path) + if found_path = search_library(libname, search_paths, extra_suffix) + result.library_paths << found_path + else + result.not_found << libname + end + end + + result + end - Crystal::System::LibraryArchive.imported_dlls(library_path).each do |dll| - dlls << dll unless dll.compare("kernel32.dll", case_insensitive: true).zero? + private def self.search_library(libname, search_paths, extra_suffix) + if ::Path::SEPARATORS.any? { |separator| libname.includes?(separator) } + libname = File.expand_path(libname) + library_path = library_filename(libname) + return library_path if File.file?(library_path) + else + search_paths.each do |directory| + if extra_suffix + library_path = File.join(directory, library_filename(libname + extra_suffix)) + return library_path if File.file?(library_path) end - break + + library_path = File.join(directory, library_filename(libname)) + return library_path if File.file?(library_path) end end - - dlls end - private def self.parse_args(args, search_paths) + def self.parse_args(args, search_paths, *, remaining = nil) libnames = [] of String # NOTE: `/LIBPATH`s are prepended before the default paths: @@ -68,10 +93,14 @@ class Crystal::Loader extra_search_paths << lib_path elsif !arg.starts_with?('/') && (name = arg.rchop?(".lib")) libnames << name + elsif remaining + remaining << arg end end search_paths = extra_search_paths + search_paths + search_paths.uniq! &.downcase + libnames.uniq! &.downcase {search_paths, libnames} end diff --git a/src/crystal/system/win32/library_archive.cr b/src/crystal/system/win32/library_archive.cr index a1ea8c275443..e1487fb907d9 100644 --- a/src/crystal/system/win32/library_archive.cr +++ b/src/crystal/system/win32/library_archive.cr @@ -70,8 +70,11 @@ module Crystal::System::LibraryArchive sig2 = io.read_bytes(UInt16, IO::ByteFormat::LittleEndian) return unless sig2 == 0xFFFF - # version(2) + machine(2) + time(4) + size(4) + ordinal/hint(2) + flags(2) - io.skip(16) + version = io.read_bytes(UInt16, IO::ByteFormat::LittleEndian) + return unless version == 0 # 1 and 2 are used by object files (ANON_OBJECT_HEADER) + + # machine(2) + time(4) + size(4) + ordinal/hint(2) + flags(2) + io.skip(14) # TODO: is there a way to do this without constructing a temporary string, # but with the optimizations present in `IO#gets`?