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
Use realpaths in Cache @path_obj #136
Conversation
Hopefully this fixes a bunch of
Please advise how should I proceed with this PR. |
IIRC |
Hmm, could you elaborate where how exactly we can have mismatch? AFAIU files are always resolved using realpath, but I understand that |
And of course I'm happy to rework this PR if other solution is more suitable, but unfortunately I haven't found such solution. |
The main case in which this is an issue is:
For example, let's say there's a file One file might require If Here's some exploration of this issue, though it doesn't precisely reproduce the case described here: require 'fileutils'
$info = ->(indent, msg) do
STDERR.puts(format("%*s\x1b[1;34m%% \x1b[1;37m%s\x1b[0m", indent, "", msg))
end
$real_app_root = 'realpath'
$symlinked_app_root = 'expanded_path'
$load_path_entry = 'expanded_path/lib'
$some_file = 'expanded_path/lib/some-file.rb'
at_exit do
File.unlink($some_file) rescue nil
FileUtils.rmdir($load_path_entry) rescue nil
File.unlink($symlinked_app_root) rescue nil
FileUtils.rmdir($real_app_root) rescue nil
end
FileUtils.mkdir($real_app_root)
FileUtils.ln_s($real_app_root, $symlinked_app_root)
FileUtils.mkdir($load_path_entry)
File.write($some_file, 'puts "\x1b[1;31m!\x1b[0;31m evaluated contents\x1b[0m"')
$LOAD_PATH.unshift(File.expand_path($load_path_entry))
alias :req :require
def require(f)
$info.(0, "require(#{f.inspect})")
req(f)
end
def (RubyVM::InstructionSequence).load_iseq(f)
$info.(2, "load_iseq(#{f.inspect})")
end
require 'some-file'
puts $LOADED_FEATURES.grep(/some-file/)
require_relative './expanded_path/lib/some-file'
puts $LOADED_FEATURES.grep(/some-file/)
require_relative './realpath/lib/some-file'
puts $LOADED_FEATURES.grep(/some-file/) It might be possible to work around this by also hooking |
Idea to override |
Forget it, doesn't help here. |
I think it might not be completely crazy to add that overhead to One other possibility would be to take the |
Something like this might do the trick: require 'English'
module RealpathCache
@cache = {}
class << self
def call(path)
call_rec(File.expand_path(path))
end
private
def call_rec(path)
if found = @cache[path]
return found
end
# base case: FS root
return path if File.dirname(path) == path
if File.lstat(path).symlink?
link_target = File.readlink(path)
link_base = File.dirname(path)
expanded = File.expand_path(link_target, link_base)
return @cache[path] = call_rec(expanded)
end
File.join(
call_rec(File.dirname(path)),
File.basename(path)
)
end
end
end
if __FILE__ == $PROGRAM_NAME
puts RealpathCache.call(ARGV.first)
end |
085fa8b
to
ecf7444
Compare
Sorry for delay (been lazy/other stuff). Do we care that required file itself can be a symlink? |
ah, looks like we care. |
Quick question, fellas — is there any way that this is related to this Ruby bug, require_relative and require should be compatible with each other when symlinks are used? It must not be the root cause, because it looks like fixes were merged before the release of v2.5.0, but the revision that fixed it sounds like it should be awful relevant:
Changes were committed to Ruby trunk September of last year (5754f15 & b6d3927), but I'm having trouble making out how exactly this patches the |
We're hijacking |
Added tests for |
Pushed fixed version. |
As I commented in #147 (comment), I still have an error for my case. |
Pushed version that iterates over @yskkin please check if it works for you. |
It works. 🎉 Thank you @ojab! |
I've pushed this as 1.3.0.beta, but it broke Shopify boot. I'll try to dig into the issue. |
@wpolicarpo could you create a new issue with description/backtrace and cc me? |
@@ -9,7 +9,7 @@ def initialize(store, path_obj, development_mode: false) | |||
@development_mode = development_mode | |||
@store = store | |||
@mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped. | |||
@path_obj = path_obj | |||
@path_obj = path_obj.map { |f| File.exist?(f) ? File.realpath(f) : f } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this is the problem! @path_obj
needs to continue to be an actual reference to $LOAD_PATH
or to ActiveSupport::Dependencies.autoload_paths
, and any change to it needs to trigger a corresponding event in ChangeObserver
.
One way to do this here would be to:
path_obj.map! { |f| File.exist?(f) ? File.realpath(f) : f }
@path_obj = path_obj
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be best too, at this point, to either:
- Make the
ChangeObserver
understandmap!
; or - Make this line blow up if the
ChangeObserver
has bound to this object`.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ouch, understood, will take a look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAICS ChangeObserver already understands map!
, so .map!
and we should be fine?
Any hints how it can be reproduced locally?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh great then.
To reproduce, I guess:
- load bootsnap
- Add a new dir to the load path via, say,
$LOAD_PATH.unshift
- Observe that bootsnap will not load features from this path since the
ChangeObserver
hook did not fire.
An easy way to catch this is to just log or whatever from the hooked method in ChangeObserver
.
@wpolicarpo could you try |
|
@ojab it's working fine now. Thanks! |
Am having this issue any help.
|
It was added in #136 to handle a very specific scenario: ``` /app/lib/ - real directory /symlink/ - symlink to /app/ ```
It was added in #136 to handle a very specific scenario: ``` /app/lib/ - real directory /symlink/ - symlink to /app/ ```
It was added in #136 to handle a very specific scenario: ``` /app/lib/ - real directory /symlink/ - symlink to /app/ ```
Ref: #402 Ref: https://bugs.ruby-lang.org/issues/10222 Ref: ruby/ruby@5754f15 Ref: ruby/ruby@b6d3927 Up to Ruby 2.3, `require` would resolve symlinks, but `require_relative` wouldn't: ```ruby require 'fileutils' FileUtils.mkdir_p("realpath") File.write("realpath/a.rb", "p :a_loaded") File.symlink("realpath", "symlink") rescue nil $LOAD_PATH.unshift(File.realpath(__dir__) + "/symlink") require "a.rb" # load symlink/a.rb in 2.3 and older, load realpath/a.rb on 2.4 and newer require_relative "realpath/a.rb" # noop on 2.4+ ``` This would easily cause double loading issue when `require` and `require_relative` were mixed, but was fixed in 2.4 (https://bugs.ruby-lang.org/issues/10222). The problem is that `Bootsnap` kinda negated this fix, because `realpath()` wouldn't be applied to absolute paths: ```ruby require 'fileutils' FileUtils.mkdir_p("realpath") File.write("realpath/a.rb", "p :a_loaded") File.symlink("realpath", "symlink") rescue nil $LOAD_PATH.unshift(File.realpath(__dir__) + "/symlink") require File.expand_path("symlink/a.rb") # load symlink/a.rb in 3.0 and older, load realpath/a.rb on 3.1 and newer require_relative "realpath/a.rb" # noop on 3.1+ ``` And for performance reasons, Bootsnap tried really hard not to call `realpath`, as it's a syscall, instead it used `expand_path`, which is entirely in use space and doesn't reach to the file system. So if you had a `symlink` in `$LOAD_PATH`, `bootcsnap` would perpetuate this bug, which led to the addition of #136. This was ultimately fixed in Ruby 3.1 (https://bugs.ruby-lang.org/issues/17885), now `realpath` is applied even on absolute paths. While `realpath` is indeed expensive, I think the performance impact is ok if we only call it for `$LOAD_PATH` members, rather than for all requirable files. So if you have X gems, it's going to be more or less X `realpath` calls. It would stay a problem if a gem actually contained symlinks and used `require_relative`, but it's quite the stretch, and with 3.1 now handling it, it's not worth keeping such workaround. See: #402
It was added in #136 to handle a very specific scenario: ``` /app/lib/ - real directory /symlink/ - symlink to /app/ ```
It was added in #136 to handle a very specific scenario: ``` /app/lib/ - real directory /symlink/ - symlink to /app/ ```
It was added in #136 to handle a very specific scenario: ``` /app/lib/ - real directory /symlink/ - symlink to /app/ ```
Ref: #402 Ref: https://bugs.ruby-lang.org/issues/10222 Ref: ruby/ruby@5754f15 Ref: ruby/ruby@b6d3927 Up to Ruby 2.3, `require` would resolve symlinks, but `require_relative` wouldn't: ```ruby require 'fileutils' FileUtils.mkdir_p("realpath") File.write("realpath/a.rb", "p :a_loaded") File.symlink("realpath", "symlink") rescue nil $LOAD_PATH.unshift(File.realpath(__dir__) + "/symlink") require "a.rb" # load symlink/a.rb in 2.3 and older, load realpath/a.rb on 2.4 and newer require_relative "realpath/a.rb" # noop on 2.4+ ``` This would easily cause double loading issue when `require` and `require_relative` were mixed, but was fixed in 2.4 (https://bugs.ruby-lang.org/issues/10222). The problem is that `Bootsnap` kinda negated this fix, because `realpath()` wouldn't be applied to absolute paths: ```ruby require 'fileutils' FileUtils.mkdir_p("realpath") File.write("realpath/a.rb", "p :a_loaded") File.symlink("realpath", "symlink") rescue nil $LOAD_PATH.unshift(File.realpath(__dir__) + "/symlink") require File.expand_path("symlink/a.rb") # load symlink/a.rb in 3.0 and older, load realpath/a.rb on 3.1 and newer require_relative "realpath/a.rb" # noop on 3.1+ ``` And for performance reasons, Bootsnap tried really hard not to call `realpath`, as it's a syscall, instead it used `expand_path`, which is entirely in use space and doesn't reach to the file system. So if you had a `symlink` in `$LOAD_PATH`, `bootcsnap` would perpetuate this bug, which led to the addition of #136. This was ultimately fixed in Ruby 3.1 (https://bugs.ruby-lang.org/issues/17885), now `realpath` is applied even on absolute paths. While `realpath` is indeed expensive, I think the performance impact is ok if we only call it for `$LOAD_PATH` members, rather than for all requirable files. So if you have X gems, it's going to be more or less X `realpath` calls. It would stay a problem if a gem actually contained symlinks and used `require_relative`, but it's quite the stretch, and with 3.1 now handling it, it's not worth keeping such workaround. See: #402
Ref: #402 Ref: https://bugs.ruby-lang.org/issues/10222 Ref: ruby/ruby@5754f15 Ref: ruby/ruby@b6d3927 Up to Ruby 2.3, `require` would resolve symlinks, but `require_relative` wouldn't: ```ruby require 'fileutils' FileUtils.mkdir_p("realpath") File.write("realpath/a.rb", "p :a_loaded") File.symlink("realpath", "symlink") rescue nil $LOAD_PATH.unshift(File.realpath(__dir__) + "/symlink") require "a.rb" # load symlink/a.rb in 2.3 and older, load realpath/a.rb on 2.4 and newer require_relative "realpath/a.rb" # noop on 2.4+ ``` This would easily cause double loading issue when `require` and `require_relative` were mixed, but was fixed in 2.4 (https://bugs.ruby-lang.org/issues/10222). The problem is that `Bootsnap` kinda negated this fix, because `realpath()` wouldn't be applied to absolute paths: ```ruby require 'fileutils' FileUtils.mkdir_p("realpath") File.write("realpath/a.rb", "p :a_loaded") File.symlink("realpath", "symlink") rescue nil $LOAD_PATH.unshift(File.realpath(__dir__) + "/symlink") require File.expand_path("symlink/a.rb") # load symlink/a.rb in 3.0 and older, load realpath/a.rb on 3.1 and newer require_relative "realpath/a.rb" # noop on 3.1+ ``` And for performance reasons, Bootsnap tried really hard not to call `realpath`, as it's a syscall, instead it used `expand_path`, which is entirely in use space and doesn't reach to the file system. So if you had a `symlink` in `$LOAD_PATH`, `bootcsnap` would perpetuate this bug, which led to the addition of #136. This was ultimately fixed in Ruby 3.1 (https://bugs.ruby-lang.org/issues/17885), now `realpath` is applied even on absolute paths. While `realpath` is indeed expensive, I think the performance impact is ok if we only call it for `$LOAD_PATH` members, rather than for all requirable files. So if you have X gems, it's going to be more or less X `realpath` calls. It would stay a problem if a gem actually contained symlinks and used `require_relative`, but it's quite the stretch, and with 3.1 now handling it, it's not worth keeping such workaround. See: #402
If gems are loaded from symlinked directory, $LOAD_PATH has mixed symlinked
and real paths, while subsequent
require
s are resolved to real path, so we canend up in situation when we rerequire the same gem with different path,
require
loads this gem again and things go BOOM. for example: