diff --git a/Makefile b/Makefile index 8e465e846327..ac3a8b670ae3 100644 --- a/Makefile +++ b/Makefile @@ -31,13 +31,16 @@ SPEC_SOURCES := $(shell find spec -name '*.cr') override FLAGS += $(if $(release),--release )$(if $(stats),--stats )$(if $(progress),--progress )$(if $(threads),--threads $(threads) )$(if $(debug),-d )$(if $(static),--static )$(if $(LDFLAGS),--link-flags="$(LDFLAGS)" )$(if $(target),--cross-compile --target $(target) ) SPEC_WARNINGS_OFF := --exclude-warnings spec/std --exclude-warnings spec/compiler SPEC_FLAGS := $(if $(verbose),-v )$(if $(junit_output),--junit_output $(junit_output) ) -CRYSTAL_CONFIG_LIBRARY_PATH := $(shell bin/crystal env CRYSTAL_LIBRARY_PATH 2> /dev/null) +CRYSTAL_CONFIG_LIBRARY_PATH := '$$ORIGIN/../lib/crystal' CRYSTAL_CONFIG_BUILD_COMMIT := $(shell git rev-parse --short HEAD 2> /dev/null) +CRYSTAL_CONFIG_PATH := '$$ORIGIN/../share/crystal/src' SOURCE_DATE_EPOCH := $(shell (git show -s --format=%ct HEAD || stat -c "%Y" Makefile || stat -f "%m" Makefile) 2> /dev/null) EXPORTS := \ - CRYSTAL_CONFIG_LIBRARY_PATH="$(CRYSTAL_CONFIG_LIBRARY_PATH)" \ CRYSTAL_CONFIG_BUILD_COMMIT="$(CRYSTAL_CONFIG_BUILD_COMMIT)" \ + CRYSTAL_CONFIG_PATH=$(CRYSTAL_CONFIG_PATH) \ SOURCE_DATE_EPOCH="$(SOURCE_DATE_EPOCH)" +EXPORTS_BUILD := \ + CRYSTAL_CONFIG_LIBRARY_PATH=$(CRYSTAL_CONFIG_LIBRARY_PATH) SHELL = sh LLVM_CONFIG := $(shell src/llvm/ext/find-llvm-config) LLVM_EXT_DIR = src/llvm/ext @@ -116,7 +119,7 @@ $(O)/compiler_spec: $(DEPS) $(SOURCES) $(SPEC_SOURCES) $(O)/crystal: $(DEPS) $(SOURCES) @mkdir -p $(O) - $(EXPORTS) ./bin/crystal build $(FLAGS) -o $@ src/compiler/crystal.cr -D without_openssl -D without_zlib + $(EXPORTS) $(EXPORTS_BUILD) ./bin/crystal build $(FLAGS) -o $@ src/compiler/crystal.cr -D without_openssl -D without_zlib $(LLVM_EXT_OBJ): $(LLVM_EXT_DIR)/llvm_ext.cc $(CXX) -c $(CXXFLAGS) -o $@ $< $(shell $(LLVM_CONFIG) --cxxflags) diff --git a/bin/crystal b/bin/crystal index ec70846e497d..db27e2a050a8 100755 --- a/bin/crystal +++ b/bin/crystal @@ -147,11 +147,13 @@ export CRYSTAL_HAS_WRAPPER=true export CRYSTAL="${CRYSTAL:-"crystal"}" -if [ -z "$CRYSTAL_CONFIG_LIBRARY_PATH" ]; then - export CRYSTAL_CONFIG_LIBRARY_PATH="$( - export PATH="$(remove_path_item "$(remove_path_item "$PATH" "$SCRIPT_ROOT")" "bin")" - crystal env CRYSTAL_LIBRARY_PATH || echo "" - )" +if [ -z "$CRYSTAL_CONFIG_LIBRARY_PATH" ] || [ -z "$CRYSTAL_LIBRARY_PATH" ]; then + CRYSTAL_INSTALLED_LIBRARY_PATH="$( + export PATH="$(remove_path_item "$(remove_path_item "$PATH" "$SCRIPT_ROOT")" "bin")" + crystal env CRYSTAL_LIBRARY_PATH || echo "" + )" + export CRYSTAL_LIBRARY_PATH=${CRYSTAL_LIBRARY_PATH:-$CRYSTAL_INSTALLED_LIBRARY_PATH} + export CRYSTAL_CONFIG_LIBRARY_PATH=${CRYSTAL_CONFIG_LIBRARY_PATH:-$CRYSTAL_INSTALLED_LIBRARY_PATH} fi if [ -x "$CRYSTAL_DIR/crystal" ]; then diff --git a/man/crystal.1 b/man/crystal.1 index 2c54fee9f60c..541163934fc2 100644 --- a/man/crystal.1 +++ b/man/crystal.1 @@ -234,6 +234,11 @@ Please see .Sm "ENVIRONMENT VARIABLES". .Pp .It +.It Sy CRYSTAL_LIBRARY_PATH +Please see +.Sm "ENVIRONMENT VARIABLES". +.Pp +.It .It Sy CRYSTAL_PATH Please see .Sm "ENVIRONMENT VARIABLES". @@ -384,8 +389,17 @@ Show version. Defines path where Crystal caches partial compilation results for faster subsequent builds. This path is also used to temporarily store executables when Crystal programs are run with 'crystal run' rather than 'crystal build'. .Pp .It +.It Sy CRYSTAL_LIBRARY_PATH +Defines paths where Crystal searches for (binary) libraries. Multiple paths can be separated by ":". +These paths are passed to the linker as `-L` flags. +.Pp +The pattern '$ORIGIN' at the start of the path expands to the directory where the compiler binary is located. For example, '$ORIGIN/../lib/crystal' resolves the standard library path relative to the compiler location in a generic way, independent of the absolute paths (assuming the relative location is correct). +.Pp +.It .It Sy CRYSTAL_PATH -Defines paths where Crystal searches for required files. +Defines paths where Crystal searches for required source files. Multiple paths can be separated by ":". +.Pp +The pattern '$ORIGIN' at the start of the path expands to the directory where the compiler binary is located. For example, '$ORIGIN/../share/crystal/src' resolves the standard library path relative to the compiler location in a generic way, independent of the absolute paths (assuming the relative location is correct). .Pp .It .It Sy CRYSTAL_OPTS diff --git a/spec/compiler/crystal_path/crystal_path_spec.cr b/spec/compiler/crystal_path/crystal_path_spec.cr index 8e5960dc54e2..058f2ecd156c 100644 --- a/spec/compiler/crystal_path/crystal_path_spec.cr +++ b/spec/compiler/crystal_path/crystal_path_spec.cr @@ -4,7 +4,7 @@ require "spec/helpers/iterate" private def assert_finds(search, results, relative_to = nil, path = __DIR__, file = __FILE__, line = __LINE__) it "finds #{search.inspect}", file, line do - crystal_path = Crystal::CrystalPath.new(path) + crystal_path = Crystal::CrystalPath.new([path]) results = results.map { |result| ::Path[__DIR__, result].normalize.to_s } Dir.cd(__DIR__) do matches = crystal_path.find search, relative_to: relative_to @@ -15,7 +15,7 @@ end private def assert_doesnt_find(search, relative_to = nil, path = __DIR__, expected_relative_to = nil, file = __FILE__, line = __LINE__) it "doesn't finds #{search.inspect}", file, line do - crystal_path = Crystal::CrystalPath.new(path) + crystal_path = Crystal::CrystalPath.new([path]) Dir.cd(__DIR__) do error = expect_raises Crystal::CrystalPath::NotFoundError do crystal_path.find search, relative_to: relative_to @@ -188,4 +188,24 @@ describe Crystal::CrystalPath do crystal_path.entries.should eq(%w(foo bar)) end end + + it ".expand_paths" do + paths = ["$ORIGIN/../foo"] + Crystal::CrystalPath.expand_paths(paths, "/usr/bin/") + paths.should eq ["/usr/bin/../foo"] + paths = ["./$ORIGIN/../foo"] + Crystal::CrystalPath.expand_paths(paths, "/usr/bin/") + paths.should eq ["./$ORIGIN/../foo"] + paths = ["$ORIGINfoo"] + Crystal::CrystalPath.expand_paths(paths, "/usr/bin/") + paths.should eq ["$ORIGINfoo"] + paths = ["lib", "$ORIGIN/../foo"] + Crystal::CrystalPath.expand_paths(paths, "/usr/bin/") + paths.should eq ["lib", "/usr/bin/../foo"] + + paths = ["$ORIGIN/../foo"] + expect_raises(Exception, "Missing executable path to expand $ORIGIN path") do + Crystal::CrystalPath.expand_paths(paths, nil) + end + end end diff --git a/src/compiler/crystal/codegen/link.cr b/src/compiler/crystal/codegen/link.cr index 86f021a4247b..13994aa32905 100644 --- a/src/compiler/crystal/codegen/link.cr +++ b/src/compiler/crystal/codegen/link.cr @@ -81,12 +81,20 @@ module Crystal end class CrystalLibraryPath + def self.default_paths : Array(String) + paths = ENV.fetch("CRYSTAL_LIBRARY_PATH", Crystal::Config.library_path).split(Process::PATH_DELIMITER, remove_empty: true) + + CrystalPath.expand_paths(paths) + + paths + end + def self.default_path : String - ENV.fetch("CRYSTAL_LIBRARY_PATH", Crystal::Config.library_path) + default_paths.join(Process::PATH_DELIMITER) end class_getter paths : Array(String) do - default_path.split(Process::PATH_DELIMITER, remove_empty: true) + default_paths end end diff --git a/src/compiler/crystal/crystal_path.cr b/src/compiler/crystal/crystal_path.cr index 5cb2ffae8fd7..e79beabb8ea5 100644 --- a/src/compiler/crystal/crystal_path.cr +++ b/src/compiler/crystal/crystal_path.cr @@ -13,22 +13,61 @@ module Crystal private DEFAULT_LIB_PATH = "lib" - def self.default_path - ENV["CRYSTAL_PATH"]? || begin - if Crystal::Config.path.blank? - DEFAULT_LIB_PATH - elsif Crystal::Config.path.split(Process::PATH_DELIMITER).includes?(DEFAULT_LIB_PATH) - Crystal::Config.path + def self.default_paths : Array(String) + if path = ENV["CRYSTAL_PATH"]? + path_array = path.split(Process::PATH_DELIMITER, remove_empty: true) + elsif path = Crystal::Config.path.presence + path_array = path.split(Process::PATH_DELIMITER, remove_empty: true) + unless path_array.includes?(DEFAULT_LIB_PATH) + path_array.unshift DEFAULT_LIB_PATH + end + else + path_array = [DEFAULT_LIB_PATH] + end + + expand_paths(path_array) + + path_array + end + + def self.default_path : String + default_paths.join(Process::PATH_DELIMITER) + end + + # Expand `$ORIGIN` in the paths to the directory where the compiler binary + # is located (at runtime). + # For install locations like + # `/path/prefix/bin/crystal` for the compiler + # `/path/prefix/share/crystal/src` for the standard library + # the path `$ORIGIN/../share/crystal/src` resolves to + # the standard library location. + # This generic path can be passed into the compiler via CRYSTAL_CONFIG_PATH + # to produce a portable binary that resolves the standard library path + # relative to the compiler location, independent of the absolute path. + def self.expand_paths(paths, origin) + paths.map! do |path| + if (chopped = path.lchop?("$ORIGIN")) && chopped[0].in?(::Path::SEPARATORS) + if origin.nil? + raise "Missing executable path to expand $ORIGIN path" + end + File.join(origin, chopped) else - {DEFAULT_LIB_PATH, Crystal::Config.path}.join(Process::PATH_DELIMITER) + path end end end + def self.expand_paths(paths) + origin = nil + if executable_path = Process.executable_path + origin = File.dirname(executable_path) + end + expand_paths(paths, origin) + end + property entries : Array(String) - def initialize(path = CrystalPath.default_path, codegen_target = Config.host_target) - @entries = path.split(Process::PATH_DELIMITER).reject &.empty? + def initialize(@entries : Array(String) = CrystalPath.default_paths, codegen_target = Config.host_target) add_target_path(codegen_target) end