require "./config" require "./error" module Crystal struct CrystalPath class NotFoundError < Crystal::Error getter filename getter relative_to def initialize(@filename : String, @relative_to : String?) if relative_to = @relative_to super "can't find #{@filename.inspect} relative to #{relative_to.inspect}" else super "can't find #{@filename.inspect}" end end end private DEFAULT_LIB_PATH = "lib" 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 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(@entries : Array(String) = CrystalPath.default_paths, codegen_target = Config.host_target) add_target_path(codegen_target) @current_dir = Dir.current end private def add_target_path(codegen_target) target = "#{codegen_target.architecture}-#{codegen_target.os_name}" @entries.each do |path| path = File.join(path, "lib_c", target) if Dir.exists?(path) @entries << path unless @entries.includes?(path) return end end end def find(filename, relative_to = nil) : Array(String) relative_to = File.dirname(relative_to) if relative_to.is_a?(String) if filename.starts_with? '.' result = find_in_path_relative_to_dir(filename, relative_to) else result = find_in_crystal_path(filename) end unless result raise NotFoundError.new(filename, relative_to) end result = [result] if result.is_a?(String) result end private def find_in_path_relative_to_dir(filename, relative_to) return unless relative_to.is_a?(String) # Check if it's a wildcard. if filename.ends_with?("/*") || (recursive = filename.ends_with?("/**")) filename_dir_index = filename.rindex!('/') filename_dir = filename[0..filename_dir_index] relative_dir = "#{relative_to}/#{filename_dir}" if File.exists?(relative_dir) files = [] of String gather_dir_files(relative_dir, files, recursive) return files end return nil end each_file_expansion(filename, relative_to) do |path| absolute_path = File.expand_path(path, dir: @current_dir) return absolute_path if File.file?(absolute_path) end nil end def each_file_expansion(filename, relative_to, &) relative_filename = "#{relative_to}/#{filename}" # Check if .cr file exists. yield relative_filename.ensure_suffix(".cr") filename_is_relative = filename.starts_with?('.') shard_name, _, shard_path = filename.partition("/") shard_path = shard_path.presence if !filename_is_relative && shard_path shard_src = "#{relative_to}/#{shard_name}/src" shard_path_stem = shard_path.rchop(".cr") # If it's "foo/bar/baz", check if "foo/src/bar/baz.cr" exists (for a shard, non-namespaced structure) yield "#{shard_src}/#{shard_path_stem}.cr" # Then check if "foo/src/foo/bar/baz.cr" exists (for a shard, namespaced structure) yield "#{shard_src}/#{shard_name}/#{shard_path_stem}.cr" # If it's "foo/bar/baz", check if "foo/bar/baz/baz.cr" exists (std, nested) basename = File.basename(relative_filename, ".cr") yield "#{relative_filename}/#{basename}.cr" # If it's "foo/bar/baz", check if "foo/src/foo/bar/baz/baz.cr" exists (shard, non-namespaced, nested) yield "#{shard_src}/#{shard_path}/#{shard_path_stem}.cr" # If it's "foo/bar/baz", check if "foo/src/foo/bar/baz/baz.cr" exists (shard, namespaced, nested) yield "#{shard_src}/#{shard_name}/#{shard_path}/#{shard_path_stem}.cr" else basename = File.basename(relative_filename, ".cr") # If it's "foo", check if "foo/foo.cr" exists (for the std, nested) yield "#{relative_filename}/#{basename}.cr" unless filename_is_relative # If it's "foo", check if "foo/src/foo.cr" exists (for a shard) yield "#{relative_filename}/src/#{basename}.cr" end end end private def gather_dir_files(dir, files_accumulator, recursive) files = [] of String dirs = [] of String Dir.each_child(dir) do |filename| full_name = "#{dir}/#{filename}" if File.directory?(full_name) if recursive dirs << filename end else if filename.ends_with?(".cr") files << full_name end end end files.sort! dirs.sort! files.each do |file| files_accumulator << File.expand_path(file, dir: @current_dir) end dirs.each do |subdir| gather_dir_files("#{dir}/#{subdir}", files_accumulator, recursive) end end private def find_in_crystal_path(filename) @entries.each do |path| required = find_in_path_relative_to_dir(filename, path) return required if required end nil end end end