diff --git a/CHANGELOG.md b/CHANGELOG.md index cc79f4550fe8..4c6c03ee4d39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Compatibility: * Add `Refinement#refined_class` (#3039, @itarato). * Add `rb_hash_new_capa` function (#3039, @itarato). * Fix `Encoding::Converter#primitive_convert` and raise `FrozenError` when a destination buffer argument is frozen (@andrykonchin). +* Ensure symlinked paths aren't loaded more than once (@rwstauner). Performance: diff --git a/spec/ruby/core/kernel/require_relative_spec.rb b/spec/ruby/core/kernel/require_relative_spec.rb index 5999855de644..91542dc9f74e 100644 --- a/spec/ruby/core/kernel/require_relative_spec.rb +++ b/spec/ruby/core/kernel/require_relative_spec.rb @@ -235,6 +235,24 @@ features.should_not include(canonical_path) end + ruby_version_is "3.1" do + it "does not require symlinked target file twice" do + symlink_path = "#{@symlink_basename}/load_fixture.rb" + absolute_path = "#{tmp("")}#{symlink_path}" + canonical_path = "#{CODE_LOADING_DIR}/load_fixture.rb" + touch(@requiring_file) { |f| + f.puts "require_relative #{symlink_path.inspect}" + f.puts "require_relative #{canonical_path.inspect}" + } + load(@requiring_file) + ScratchPad.recorded.should == [:loaded] + + features = $LOADED_FEATURES.select { |path| path.end_with?('load_fixture.rb') } + features.should include(absolute_path) + features.should_not include(canonical_path) + end + end + it "stores the same path that __FILE__ returns in the required file" do symlink_path = "#{@symlink_basename}/load_fixture_and__FILE__.rb" touch(@requiring_file) { |f| diff --git a/src/main/ruby/truffleruby/core/kernel.rb b/src/main/ruby/truffleruby/core/kernel.rb index c391a591c08b..43726df7c8b8 100644 --- a/src/main/ruby/truffleruby/core/kernel.rb +++ b/src/main/ruby/truffleruby/core/kernel.rb @@ -231,7 +231,7 @@ def autoload?(name) when :feature_loaded false when :feature_found - Primitive.load_feature(feature, path) + Truffle::FeatureLoader.load_unless_realpath_loaded(feature, path) when :not_found raise Truffle::KernelOperations.load_error(feature) end @@ -257,7 +257,7 @@ def require(feature) when :feature_loaded false when :feature_found - Primitive.load_feature(feature, path) + Truffle::FeatureLoader.load_unless_realpath_loaded(feature, path) when :not_found if lazy_rubygems Truffle::KernelOperations.loading_rubygems = true @@ -290,7 +290,7 @@ def require_relative(feature) false when :feature_found # The first argument needs to be the expanded path here for patching to work correctly - Primitive.load_feature(path, path) + Truffle::FeatureLoader.load_unless_realpath_loaded(path, path) when :not_found raise Truffle::KernelOperations.load_error(feature) end diff --git a/src/main/ruby/truffleruby/core/truffle/feature_loader.rb b/src/main/ruby/truffleruby/core/truffle/feature_loader.rb index 0d97ff4fd461..6e3e202f6eb9 100644 --- a/src/main/ruby/truffleruby/core/truffle/feature_loader.rb +++ b/src/main/ruby/truffleruby/core/truffle/feature_loader.rb @@ -35,6 +35,9 @@ module FeatureLoader # A snapshot of $LOADED_FEATURES, to check if the @loaded_features_index cache is up to date. @loaded_features_version = -1 + @features_realpath_cache = {} + @loaded_features_realpaths = {} + @expanded_load_path = [] # A snapshot of $LOAD_PATH, to check if the @expanded_load_path cache is up to date. @load_path_version = -1 @@ -140,6 +143,15 @@ def self.find_feature_or_file(feature, use_feature_provided = true) end end + def self.load_unless_realpath_loaded(feature, path) + realpath = (@features_realpath_cache[path] ||= File.realpath(path) || path) + return false if @loaded_features_realpaths.key?(realpath) + + result = Primitive.load_feature(feature, path) + @loaded_features_realpaths[realpath] = path + result + end + def self.expanded_path_provided(path, ext, use_feature_provided) if use_feature_provided && feature_provided?(path, true) [:feature_loaded, path, ext] @@ -292,6 +304,9 @@ def self.get_loaded_features_index unless @loaded_features_version == $LOADED_FEATURES.version raise '$LOADED_FEATURES is frozen; cannot append feature' if $LOADED_FEATURES.frozen? @loaded_features_index.clear + previous_realpaths = @features_realpath_cache.dup + @features_realpath_cache.clear + @loaded_features_realpaths.clear $LOADED_FEATURES.map! do |val| val = StringValue(val) #val.freeze # TODO freeze these but post-boot.rb issue using replace @@ -299,6 +314,9 @@ def self.get_loaded_features_index end $LOADED_FEATURES.each_with_index do |val, idx| features_index_add(val, idx) + realpath = previous_realpaths[val] || (File.exist?(val) ? File.realpath(val) : val) + @features_realpath_cache[val] = realpath + @loaded_features_realpaths[val] = realpath end @loaded_features_version = $LOADED_FEATURES.version end