Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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