Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support CRYSTAL_LIBRARY_RPATH for adding dynamic library lookup paths #13499

Merged
7 changes: 4 additions & 3 deletions spec/std/spec_helper.cr
Expand Up @@ -81,9 +81,10 @@ def compile_file(source_file, *, bin_name = "executable_file", flags = %w(), fil
args = ["build"] + flags + ["-o", executable_file, source_file]
output = IO::Memory.new
status = Process.run(compiler, args, env: {
"CRYSTAL_PATH" => Crystal::PATH,
"CRYSTAL_LIBRARY_PATH" => Crystal::LIBRARY_PATH,
"CRYSTAL_CACHE_DIR" => Crystal::CACHE_DIR,
"CRYSTAL_PATH" => Crystal::PATH,
"CRYSTAL_LIBRARY_PATH" => Crystal::LIBRARY_PATH,
"CRYSTAL_LIBRARY_RPATH" => Crystal::LIBRARY_RPATH,
"CRYSTAL_CACHE_DIR" => Crystal::CACHE_DIR,
}, output: output, error: output)

unless status.success?
Expand Down
55 changes: 54 additions & 1 deletion src/compiler/crystal/codegen/link.cr
Expand Up @@ -85,7 +85,7 @@ module Crystal
end
end

class CrystalLibraryPath
module CrystalLibraryPath
def self.default_paths : Array(String)
paths = ENV.fetch("CRYSTAL_LIBRARY_PATH", Crystal::Config.library_path).split(Process::PATH_DELIMITER, remove_empty: true)

Expand All @@ -98,6 +98,59 @@ module Crystal
default_paths.join(Process::PATH_DELIMITER)
end

def self.default_rpath : String
# do not call `CrystalPath.expand_paths`, as `$ORIGIN` inside this env
# variable is always expanded at run time
ENV.fetch("CRYSTAL_LIBRARY_RPATH", "")
end

# Adds the compiler itself's RPATH to the environment for the duration of
# the block. `$ORIGIN` in the compiler's RPATH is expanded immediately, but
# `$ORIGIN`s in the existing environment variable are not expanded. For
# example, on Linux:
#
# - CRYSTAL_LIBRARY_RPATH of the compiler: `$ORIGIN/so`
# - Current $CRYSTAL_LIBRARY_RPATH: `/home/foo:$ORIGIN/mylibs`
# - Compiler's full path: `/opt/crystal`
# - Generated executable's Crystal::LIBRARY_RPATH: `/home/foo:$ORIGIN/mylibs:/opt/so`
#
# On Windows we additionally append the compiler's parent directory to the
# list, as if by appending `$ORIGIN` to the compiler's RPATH. This directory
# is effectively the first search entry on any Windows executable. Example:
#
# - CRYSTAL_LIBRARY_RPATH of the compiler: `$ORIGIN\dll`
# - Current %CRYSTAL_LIBRARY_RPATH%: `C:\bar;$ORIGIN\mylibs`
# - Compiler's full path: `C:\foo\crystal.exe`
# - Generated executable's Crystal::LIBRARY_RPATH: `C:\bar;$ORIGIN\mylibs;C:\foo\dll;C:\foo`
#
# Combining RPATHs multiple times has no effect; the `CRYSTAL_LIBRARY_RPATH`
# environment variable at compiler startup is used, not really the "current"
# one. This can happen when running a program that also uses macro `run`s.
def self.add_compiler_rpath(&)
executable_path = Process.executable_path
compiler_origin = File.dirname(executable_path) if executable_path

current_rpaths = ORIGINAL_CRYSTAL_LIBRARY_RPATH.try &.split(Process::PATH_DELIMITER, remove_empty: true)
compiler_rpaths = Crystal::LIBRARY_RPATH.split(Process::PATH_DELIMITER, remove_empty: true)
CrystalPath.expand_paths(compiler_rpaths, compiler_origin)

rpaths = compiler_rpaths
rpaths.concat(current_rpaths) if current_rpaths
{% if flag?(:win32) %}
rpaths << compiler_origin if compiler_origin
{% end %}

old_env = ENV["CRYSTAL_LIBRARY_RPATH"]?
ENV["CRYSTAL_LIBRARY_RPATH"] = rpaths.join(Process::PATH_DELIMITER)
begin
yield
ensure
ENV["CRYSTAL_LIBRARY_RPATH"] = old_env
end
end

private ORIGINAL_CRYSTAL_LIBRARY_RPATH = ENV["CRYSTAL_LIBRARY_RPATH"]?

class_getter paths : Array(String) do
default_paths
end
Expand Down
8 changes: 5 additions & 3 deletions src/compiler/crystal/command.cr
Expand Up @@ -331,10 +331,11 @@ class Crystal::Command
specified_output : Bool,
hierarchy_exp : String?,
cursor_location : String?,
output_format : String? do
output_format : String?,
combine_rpath : Bool do
def compile(output_filename = self.output_filename)
compiler.emit_base_filename = output_filename.rchop(File.extname(output_filename))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is equivalent and more concise:

Suggested change
compiler.emit_base_filename = output_filename.rchop(File.extname(output_filename))
compiler.emit_base_filename = Path[output_filename].stem

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from a prior PR. That PR actually changed the behavior of -o so that it also affects --emit; previously those emitted files would always be in the current working directory and ignore the name given to -o. Thus output_filename could now contain directory separators, and calling Path#stem would remove them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the name filename is misleading then if it's actually a path.
But okay, don't need to fix that here.

compiler.compile sources, output_filename
compiler.compile sources, output_filename, combine_rpath: combine_rpath
end

def top_level_semantic
Expand Down Expand Up @@ -548,7 +549,8 @@ class Crystal::Command
error "can't use `#{output_filename}` as output filename because it's a directory"
end

@config = CompilerConfig.new compiler, sources, output_filename, arguments, specified_output, hierarchy_exp, cursor_location, output_format
combine_rpath = run && !no_codegen
@config = CompilerConfig.new compiler, sources, output_filename, arguments, specified_output, hierarchy_exp, cursor_location, output_format, combine_rpath
end

private def gather_sources(filenames)
Expand Down
11 changes: 6 additions & 5 deletions src/compiler/crystal/command/env.cr
Expand Up @@ -18,11 +18,12 @@ class Crystal::Command
end

vars = {
"CRYSTAL_CACHE_DIR" => CacheDir.instance.dir,
"CRYSTAL_PATH" => CrystalPath.default_path,
"CRYSTAL_VERSION" => Config.version || "",
"CRYSTAL_LIBRARY_PATH" => CrystalLibraryPath.default_path,
"CRYSTAL_OPTS" => ENV.fetch("CRYSTAL_OPTS", ""),
"CRYSTAL_CACHE_DIR" => CacheDir.instance.dir,
"CRYSTAL_PATH" => CrystalPath.default_path,
"CRYSTAL_VERSION" => Config.version || "",
"CRYSTAL_LIBRARY_PATH" => CrystalLibraryPath.default_path,
"CRYSTAL_LIBRARY_RPATH" => CrystalLibraryPath.default_rpath,
"CRYSTAL_OPTS" => ENV.fetch("CRYSTAL_OPTS", ""),
}

if var_names.empty?
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/command/eval.cr
Expand Up @@ -26,7 +26,7 @@ class Crystal::Command

output_filename = Crystal.temp_executable "eval"

compiler.compile sources, output_filename
compiler.compile sources, output_filename, combine_rpath: true
execute output_filename, program_args, compiler
end
end
2 changes: 1 addition & 1 deletion src/compiler/crystal/command/spec.cr
Expand Up @@ -95,7 +95,7 @@ class Crystal::Command
output_filename = Crystal.temp_executable "spec"

ENV["CRYSTAL_SPEC_COMPILER_BIN"] ||= Process.executable_path
compiler.compile sources, output_filename
compiler.compile sources, output_filename, combine_rpath: true
report_warnings
execute output_filename, options, compiler, error_on_exit: warnings_fail_on_exit?
end
Expand Down
11 changes: 10 additions & 1 deletion src/compiler/crystal/compiler.cr
Expand Up @@ -150,12 +150,21 @@ module Crystal
# Compiles the given *source*, with *output_filename* as the name
# of the generated executable.
#
# If *combine_rpath* is true, add the compiler itself's RPATH to the
# generated executable via `CrystalLibraryPath.add_compiler_rpath`. This is
# used by the `run` / `eval` / `spec` commands as well as the macro `run`
# (via `Crystal::Program#macro_compile`), and never during cross-compiling.
#
# Raises `Crystal::CodeError` if there's an error in the
# source code.
#
# Raises `InvalidByteSequenceError` if the source code is not
# valid UTF-8.
def compile(source : Source | Array(Source), output_filename : String) : Result
def compile(source : Source | Array(Source), output_filename : String, *, combine_rpath : Bool = false) : Result
if combine_rpath
return CrystalLibraryPath.add_compiler_rpath { compile(source, output_filename, combine_rpath: false) }
end

source = [source] unless source.is_a?(Array)
program = new_program(source)
node = parse program, source
Expand Down
1 change: 1 addition & 0 deletions src/compiler/crystal/loader/msvc.cr
Expand Up @@ -149,6 +149,7 @@ class Crystal::Loader
end

private def open_library(path : String)
# TODO: respect Crystal::LIBRARY_RPATH (#13490)
LibC.LoadLibraryExW(System.to_wstr(path), nil, 0)
end

Expand Down
5 changes: 5 additions & 0 deletions src/compiler/crystal/loader/unix.cr
Expand Up @@ -137,11 +137,16 @@ class Crystal::Loader
def self.default_search_paths : Array(String)
default_search_paths = [] of String

# TODO: respect the compiler's DT_RPATH (#13490)

if env_library_path = ENV[{{ flag?(:darwin) ? "DYLD_LIBRARY_PATH" : "LD_LIBRARY_PATH" }}]?
# TODO: Expand tokens $ORIGIN, $LIB, $PLATFORM
default_search_paths.concat env_library_path.split(Process::PATH_DELIMITER, remove_empty: true)
end

# TODO: respect the compiler's DT_RUNPATH
# TODO: respect $DYLD_FALLBACK_LIBRARY_PATH and the compiler's LC_RPATH on darwin

{% if (flag?(:linux) && !flag?(:android)) || flag?(:bsd) %}
read_ld_conf(default_search_paths)
{% end %}
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/macros/macros.cr
Expand Up @@ -93,7 +93,7 @@ class Crystal::Program
return CompiledMacroRun.new(executable_path, elapsed_time, true)
end

result = host_compiler.compile Compiler::Source.new(filename, source), executable_path
result = host_compiler.compile Compiler::Source.new(filename, source), executable_path, combine_rpath: true

# Write the new files from which 'filename' depends into the cache dir
# (here we store how to obtain these files, because a require might use
Expand Down
1 change: 1 addition & 0 deletions src/compiler/crystal/program.cr
Expand Up @@ -278,6 +278,7 @@ module Crystal
define_crystal_string_constant "DESCRIPTION", Crystal::Config.description
define_crystal_string_constant "PATH", Crystal::CrystalPath.default_path
define_crystal_string_constant "LIBRARY_PATH", Crystal::CrystalLibraryPath.default_path
define_crystal_string_constant "LIBRARY_RPATH", Crystal::CrystalLibraryPath.default_rpath
define_crystal_string_constant "VERSION", Crystal::Config.version
define_crystal_string_constant "LLVM_VERSION", Crystal::Config.llvm_version
end
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/tools/doc/generator.cr
Expand Up @@ -227,7 +227,7 @@ class Crystal::Doc::Generator

{"BUILD_COMMIT", "BUILD_DATE", "CACHE_DIR", "DEFAULT_PATH",
"DESCRIPTION", "PATH", "VERSION", "LLVM_VERSION",
"LIBRARY_PATH"}.each do |name|
"LIBRARY_PATH", "LIBRARY_RPATH"}.each do |name|
return true if type == crystal_type.types[name]?
end

Expand Down
14 changes: 14 additions & 0 deletions src/crystal/main.cr
@@ -1,3 +1,17 @@
require "process/executable_path" # Process::PATH_DELIMITER

module Crystal
{% unless Crystal.has_constant?("LIBRARY_RPATH") %}
LIBRARY_RPATH = {{ env("CRYSTAL_LIBRARY_RPATH") || "" }}
{% end %}
end

{% if flag?(:unix) && !flag?(:darwin) %}
{% unless Crystal::LIBRARY_RPATH.empty? %}
# TODO: is there a better way to quote this?
@[Link(ldflags: {{ "'-Wl,-rpath,#{Crystal::LIBRARY_RPATH.id}'" }})]
{% end %}
{% end %}
lib LibCrystalMain
@[Raises]
fun __crystal_main(argc : Int32, argv : UInt8**)
Expand Down