Skip to content

Commit

Permalink
Support dll parameter in @[Link] (#14131)
Browse files Browse the repository at this point in the history
Co-authored-by: Johannes Müller <[email protected]>
  • Loading branch information
HertzDevil and straight-shoota authored Dec 23, 2023
1 parent 15010d3 commit 899ec69
Show file tree
Hide file tree
Showing 15 changed files with 166 additions and 5 deletions.
50 changes: 49 additions & 1 deletion spec/compiler/semantic/lib_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,55 @@ describe "Semantic: lib" do
lib LibFoo
end
),
"unknown link argument: 'boo' (valid arguments are 'lib', 'ldflags', 'static', 'pkg_config', 'framework', and 'wasm_import_module')"
"unknown link argument: 'boo' (valid arguments are 'lib', 'ldflags', 'static', 'pkg_config', 'framework', 'wasm_import_module', and 'dll')"
end

it "allows dll argument" do
assert_no_errors <<-CRYSTAL
@[Link(dll: "foo.dll")]
lib LibFoo
end
CRYSTAL

assert_no_errors <<-CRYSTAL
@[Link(dll: "BAR.DLL")]
lib LibFoo
end
CRYSTAL
end

it "errors if dll argument contains directory separators" do
assert_error <<-CRYSTAL, "'dll' link argument must not include directory separators"
@[Link(dll: "foo/bar.dll")]
lib LibFoo
end
CRYSTAL

assert_error <<-CRYSTAL, "'dll' link argument must not include directory separators"
@[Link(dll: %q(foo\\bar.dll))]
lib LibFoo
end
CRYSTAL
end

it "errors if dll argument does not end with '.dll'" do
assert_error <<-CRYSTAL, "'dll' link argument must use a '.dll' file extension"
@[Link(dll: "foo")]
lib LibFoo
end
CRYSTAL

assert_error <<-CRYSTAL, "'dll' link argument must use a '.dll' file extension"
@[Link(dll: "foo.dylib")]
lib LibFoo
end
CRYSTAL

assert_error <<-CRYSTAL, "'dll' link argument must use a '.dll' file extension"
@[Link(dll: "")]
lib LibFoo
end
CRYSTAL
end

it "errors if lib already specified with positional argument" do
Expand Down
9 changes: 8 additions & 1 deletion src/annotations.cr
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ end
annotation Flags
end

# A `lib` can be marked with `@[Link(lib : String, *, ldflags : String, framework : String, pkg_config : String)]`
# A `lib` can be marked with `@[Link(lib : String, *, ldflags : String, static : Bool, framework : String, pkg_config : String, wasm_import_module : String, dll : String)]`
# to declare the library that should be linked when compiling the program.
#
# At least one of the *lib*, *ldflags*, *framework* arguments needs to be specified.
Expand All @@ -45,6 +45,13 @@ end
#
# `@[Link(framework: "Cocoa")]` will pass `-framework Cocoa` to the linker.
#
# `@[Link(dll: "gc.dll")]` will copy `gc.dll` to any built program. The DLL name
# must use `.dll` as its file extension and cannot contain any directory
# separators. The actual DLL is searched among `CRYSTAL_LIBRARY_PATH`, the
# compiler's own directory, and `PATH` in that order; a warning is printed if
# the DLL isn't found, although it might still run correctly if the DLLs are
# available in other DLL search paths on the system.
#
# When an `-l` option is passed to the linker, it will lookup the libraries in
# paths passed with the `-L` option. Any paths in `CRYSTAL_LIBRARY_PATH` are
# added by default. Custom paths can be passed using `ldflags`:
Expand Down
53 changes: 50 additions & 3 deletions src/compiler/crystal/codegen/link.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ module Crystal
getter ldflags : String?
getter framework : String?
getter wasm_import_module : String?
getter dll : String?

def initialize(@lib = nil, @pkg_config = @lib, @ldflags = nil, @static = false, @framework = nil, @wasm_import_module = nil)
def initialize(@lib = nil, @pkg_config = @lib, @ldflags = nil, @static = false, @framework = nil, @wasm_import_module = nil, @dll = nil)
end

def static?
Expand All @@ -27,6 +28,7 @@ module Crystal
lib_pkg_config = nil
lib_framework = nil
lib_wasm_import_module = nil
lib_dll = nil
count = 0

args.each do |arg|
Expand Down Expand Up @@ -76,12 +78,21 @@ module Crystal
when "wasm_import_module"
named_arg.raise "'wasm_import_module' link argument must be a String" unless value.is_a?(StringLiteral)
lib_wasm_import_module = value.value
when "dll"
named_arg.raise "'dll' link argument must be a String" unless value.is_a?(StringLiteral)
lib_dll = value.value
unless lib_dll.size >= 4 && lib_dll[-4..].compare(".dll", case_insensitive: true) == 0
named_arg.raise "'dll' link argument must use a '.dll' file extension"
end
if ::Path.separators(::Path::Kind::WINDOWS).any? { |separator| lib_dll.includes?(separator) }
named_arg.raise "'dll' link argument must not include directory separators"
end
else
named_arg.raise "unknown link argument: '#{named_arg.name}' (valid arguments are 'lib', 'ldflags', 'static', 'pkg_config', 'framework', and 'wasm_import_module')"
named_arg.raise "unknown link argument: '#{named_arg.name}' (valid arguments are 'lib', 'ldflags', 'static', 'pkg_config', 'framework', 'wasm_import_module', and 'dll')"
end
end

new(lib_name, lib_pkg_config, lib_ldflags, lib_static, lib_framework, lib_wasm_import_module)
new(lib_name, lib_pkg_config, lib_ldflags, lib_static, lib_framework, lib_wasm_import_module, lib_dll)
end
end

Expand Down Expand Up @@ -221,6 +232,42 @@ module Crystal
flags.join(" ")
end

def each_dll_path(& : String, Bool ->)
executable_path = nil
compiler_origin = nil
paths = nil

link_annotations.each do |ann|
next unless dll = ann.dll

dll_path = CrystalLibraryPath.paths.each do |path|
full_path = File.join(path, dll)
break full_path if File.file?(full_path)
end

unless dll_path
executable_path ||= Process.executable_path
compiler_origin ||= File.dirname(executable_path) if executable_path

if compiler_origin
full_path = File.join(compiler_origin, dll)
dll_path = full_path if File.file?(full_path)
end
end

unless dll_path
paths ||= ENV["PATH"]?.try &.split(Process::PATH_DELIMITER, remove_empty: true)

dll_path = paths.try &.each do |path|
full_path = File.join(path, dll)
break full_path if File.file?(full_path)
end
end

yield dll_path || dll, !dll_path.nil?
end
end

PKG_CONFIG_PATH = Process.find_executable("pkg-config")

# Returns the result of running `pkg-config mod` but returns nil if
Expand Down
23 changes: 23 additions & 0 deletions src/compiler/crystal/compiler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,10 @@ module Crystal
{% if flag?(:darwin) %}
run_dsymutil(output_filename) unless debug.none?
{% end %}

{% if flag?(:windows) %}
copy_dlls(program, output_filename) if program.has_flag?("preview_dll")
{% end %}
end

CacheDir.instance.cleanup if @cleanup
Expand All @@ -345,6 +349,25 @@ module Crystal
end
end

private def copy_dlls(program, output_filename)
not_found = nil
output_directory = File.dirname(output_filename)

program.each_dll_path do |path, found|
if found
FileUtils.cp(path, output_directory)
else
not_found ||= [] of String
not_found << path
end
end

if not_found
stderr << "Warning: The following DLLs are required at run time, but Crystal is unable to locate them in CRYSTAL_LIBRARY_PATH, the compiler's directory, or PATH: "
not_found.sort!.join(stderr, ", ")
end
end

private def cross_compile(program, units, output_filename)
unit = units.first
llvm_mod = unit.llvm_mod
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/crystal/ffi/lib_ffi.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
module Crystal
@[Link("ffi")]
{% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %}
@[Link(dll: "libffi.dll")]
{% end %}
lib LibFFI
{% begin %}
enum ABI
Expand Down
3 changes: 3 additions & 0 deletions src/crystal/lib_iconv.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ require "c/stddef"
{% end %}

@[Link("iconv")]
{% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %}
@[Link(dll: "libiconv.dll")]
{% end %}
lib LibIconv
type IconvT = Void*

Expand Down
3 changes: 3 additions & 0 deletions src/gc/boehm.cr
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
@[Link("gc")]
{% end %}

{% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %}
@[Link(dll: "gc.dll")]
{% end %}
lib LibGC
alias Int = LibC::Int
alias SizeT = LibC::SizeT
Expand Down
3 changes: 3 additions & 0 deletions src/lib_z/lib_z.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
@[Link("z")]
{% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %}
@[Link(dll: "zlib1.dll")]
{% end %}
lib LibZ
alias Char = LibC::Char
alias Int = LibC::Int
Expand Down
3 changes: 3 additions & 0 deletions src/llvm/lib_llvm.cr
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
{% llvm_ldflags = lines[2] %}

@[Link("llvm")]
{% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %}
@[Link(dll: "LLVM-C.dll")]
{% end %}
lib LibLLVM
end
{% else %}
Expand Down
4 changes: 4 additions & 0 deletions src/openssl/lib_crypto.cr
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
{% else %}
@[Link(ldflags: "`command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libcrypto || printf %s '-lcrypto'`")]
{% end %}
{% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %}
# TODO: if someone brings their own OpenSSL 1.x.y on Windows, will this have a different name?
@[Link(dll: "libcrypto-3-x64.dll")]
{% end %}
lib LibCrypto
alias Char = LibC::Char
alias Int = LibC::Int
Expand Down
5 changes: 5 additions & 0 deletions src/openssl/lib_ssl.cr
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ require "./lib_crypto"
{% else %}
@[Link(ldflags: "`command -v pkg-config > /dev/null && pkg-config --libs --silence-errors libssl || printf %s '-lssl -lcrypto'`")]
{% end %}
{% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %}
# TODO: if someone brings their own OpenSSL 1.x.y on Windows, will this have a different name?
@[Link(dll: "libssl-3-x64.dll")]
@[Link(dll: "libcrypto-3-x64.dll")]
{% end %}
lib LibSSL
alias Int = LibC::Int
alias Char = LibC::Char
Expand Down
3 changes: 3 additions & 0 deletions src/regex/lib_pcre.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
@[Link("pcre")]
{% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %}
@[Link(dll: "pcre.dll")]
{% end %}
lib LibPCRE
alias Int = LibC::Int

Expand Down
3 changes: 3 additions & 0 deletions src/regex/lib_pcre2.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
@[Link("pcre2-8")]
{% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %}
@[Link(dll: "pcre2-8.dll")]
{% end %}
lib LibPCRE2
alias Int = LibC::Int

Expand Down
3 changes: 3 additions & 0 deletions src/xml/libxml2.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ require "./html_parser_options"
require "./save_options"

@[Link("xml2", pkg_config: "libxml-2.0")]
{% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %}
@[Link(dll: "libxml2.dll")]
{% end %}
lib LibXML
alias Int = LibC::Int

Expand Down
3 changes: 3 additions & 0 deletions src/yaml/lib_yaml.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
require "./enums"

@[Link("yaml", pkg_config: "yaml-0.1")]
{% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %}
@[Link(dll: "yaml.dll")]
{% end %}
lib LibYAML
alias Int = LibC::Int

Expand Down

0 comments on commit 899ec69

Please sign in to comment.