How do I find the location of the gem? - ruby

I'm developing a library that provides access to gem metadata, including it's location on the file system. The idea was to let gem authors set it to a relative path from any script:
# $root/example.gemspec
Example::Gem.root '.' # => $root/
# $root/lib/example/gem.rb
Example::Gem.root '../..' # => $root/
Then, the path of the current script would be used to compute the absolute path. My implementation is currently as follows:
def root(relative_to = nil, file = __FILE__)
unless relative_to.nil?
#root = File.expand_path relative_to, File.dirname(file)
end
#root
end
I thought __FILE__ would return the path to the caller's script, but that assumption is wrong.
It worked within the library itself, but broke down when I tried to integrate it with one of my other gems; the generated path was always relative to the support library itself.
How can I implement this without having to pass the current __FILE__ on every call? Otherwise, there isn't much value to be gained; writing root('../..', __FILE__) is almost the same as writing an actual method to do the same thing.
If it's possible to figure out the path without having to specify anything, that would be even better, but I couldn't think of anything. How does Rails do it?
By the way, I'm aware of Gem::Specification#gem_dir, but it always returns paths relative to the installation directory, even if the gem is not actually there, which makes it useless in a development environment.

You can always make use of the backtrace facility provided:
caller.first
It produces an amalgam of file and line but is usually separated by :. I'd be careful to allow for filenames or paths that may contain colon for whatever reason by ignoring the line information but preserving the rest. In other words, do not split but sub:
caller.first.sub(/:\d+:in .*$/, '')

Related

Dir.chdir(File.dirname(__FILE__)) throws Errno::ENOENT

I got a method where I use Dir.chdir(File.dirname(__FILE__)) inside. I am using it so that I can run the ruby file from anywhere and not get this error: No such file or directory # rb_sysopen - 'filename' (Errno::ENOENT).
Using the method for the first time works fine but using it for the second time throws an error. See method and exact error below.
def meth(string)
Dir.chdir(File.dirname(__FILE__))
hash = JSON.parse(File.read("file.json"))
# do something with hash and string
# return some value
end
meth("first string") # this returns what is expected
meth("second string") # this second usage of the method throws the error
Error sample pinpointing the line where I used Dir.chdir(File.dirname(__FILE__)):
dir/decoder.rb:44:in `chdir': No such file or directory # dir_s_chdir - lib (Errno::ENOENT)
Not sure if OS plays a role here, I am using an m1 BigSur on version 11.2.3.
Why is this happening?
What needs to be done so that the method` can be used as much as needed without running into the error?
Your problem here seems to be that __FILE__ is a relative path like dir/decoder.rb and that path becomes invalid after the first time Dir.chdir is used, because that command changes the working directory of your entire Ruby process. I think the solution would be to do something like this in your decoder.rb file:
DecoderDir = File.realpath(File.dirname(__FILE__))
def meth
Dir.chdir(DecoderDir)
# ...
end
I'm guessing that the first time the Ruby interpreter processes the file, that is early enough that the relative path in __FILE__ still refers to the right place. So, at that time, we generate an absolute path for future use.
By the way, a well-behaved library should not run Dir.chdir because it will affect all use of relative paths throughout your entire Ruby process. I pretty much only run Dir.chdir once and I run it near the beginning of my top-level script. If you're making a reusable library, you might want to do something like this to calculate the absolute path of the file you want to open:
DecoderJson = File.join(DecoderDir, 'file.json')

Requiring files from a required file

I have a file required.rb required by other files main.rb and tester.rb, each of which is invoked separately and run separately.
Within required.rb, I want to require all files in a subdirectory of the required file. The whole thing looks something like this:
main.rb
lib/
required.rb
req_files/
req1.rb
req2.rb
req3.rb
tester/
tester.rb
The code to import the required files looks like:
Dir[Dir.pwd + "/req_files/*.rb"].each do |file|
require file
end
In suggested strategies I have seen, be it utilizing Dir.pwd or __FILE__, the context applied to required.rb's location is the context of whichever original file required it in the first place, which means that I can't support requiring from both of those files separately with the current setup.
Is there a way to denote a path relative to the actual required.rb?
EDIT :
It's not though, because changing require to require_relative doesn't change the fact that Dir[Dir.pwd + "/req_files/*.rb"] and more specifically Dir.pwd resolves with respect to the original file (main or tester), so it cannot be expressed as is in required and work for both entry points
Also note that required.rb is required via require_relative already from both main.rb and tester.rb.
Is there a way to denote a path relative to the actual required.rb
Yes, kinda. There's another method for this.
http://ruby-doc.org/core-2.4.2/Kernel.html#method-i-require_relative
require_relative(string) → true or false
Ruby tries to load the library named string relative to the requiring file’s path. If the file’s path cannot be determined a LoadError is raised. If a file is loaded true is returned and false otherwise.
I was incorrect regarding __FILE__; Using File.dirname(__FILE__) instead of Dir.pwd works for giving the directory of the actual file versus the directory of the invoking file.

Relative File Path in RSpec

I have a RSpec test for a class in /lib/classes which needs access to a zip file (no upload). The file is stored in /spec/fixtures/files/test.zip. How do I input the correct path so it's environment agnostic, i.e. without absolute path?
Rails.root will give you the app root, so
Rails.root.join "spec/fixtures/files/test.zip"
will give you the absolute path of your file, agnostic of the location of the app on your hard drive.
I've tackled this issue recently and here's what I came up with:
Define a constant in your spec_helper.rb (or equivalent) pointing to RSpec root:
RSPEC_ROOT = File.dirname __FILE__
Use this variable in your *_spec.rb (or equivalent) test files:
require 'spec_helper'
File.open("#{RSPEC_ROOT}/resources/example_data.json")
Why this solution?
It doesn't make use of the location of the test file which may be
subject to change (spec_helper is likely not)
It doesn't require any additions to test files other than the already existing
require 'spec_helper'
It doesn't depend on Rails (unlike Rails.root)
Here's a Gist: https://gist.github.com/thisismydesign/9dc142f89b82a07e413a45a5d2983b07
As far as I know (from looking every month or two) there is no better way that building something in spec_helper that uses the __FILE__ value to grab the path to a know bit of content, then build your own helpers on top of that.
You can obviously use a path relative to __FILE__ in the individual *_spec.rb file as well.

File.expand_path("../../Gemfile", __FILE__) How does this work? Where is the file?

ENV["BUNDLE_GEMFILE"] = File.expand_path("../../Gemfile", __FILE__)
I'm just trying to access a .rb file from the some directory and a tutorial is telling me to use this code but I don't see how it is finding the gem file.
File.expand_path('../../Gemfile', __FILE__)
is a somewhat ugly Ruby idiom for getting the absolute path to a file when you know the path relative to the current file. Another way of writing it is this:
File.expand_path('../Gemfile', File.dirname(__FILE__))
both are ugly, but the first variant is shorter. The first variant is, however, also very non-intuitive until you get the hang of it. Why the extra ..? (but the second variant may give a clue as to why it is needed).
This is how it works: File.expand_path returns the absolute path of the first argument, relative to the second argument (which defaults to the current working directory). __FILE__ is the path to the file the code is in. Since the second argument in this case is a path to a file, and File.expand_path assumes a directory, we have to stick an extra .. in the path to get the path right. This is how it works:
File.expand_path is basically implemented like this (in the following code path will have the value of ../../Gemfile and relative_to will have the value of /path/to/file.rb):
def File.expand_path(path, relative_to=Dir.getwd)
# first the two arguments are concatenated, with the second argument first
absolute_path = File.join(relative_to, path)
while absolute_path.include?('..')
# remove the first occurrence of /<something>/..
absolute_path = absolute_path.sub(%r{/[^/]+/\.\.}, '')
end
absolute_path
end
(there's a little bit more to it, it expands ~ to the home directory and so on -- there are probably also some other issues with the code above)
Stepping through a call to the code above absolute_path will first get the value /path/to/file.rb/../../Gemfile, then for each round in the loop the first .. will be removed, along with the path component before it. First /file.rb/.. is removed, then on the next round /to/.. is removed, and we get /path/Gemfile.
To make a long story short, File.expand_path('../../Gemfile', __FILE__) is a trick to get the absolute path of a file when you know the path relative to the current file. The extra .. in the relative path is to eliminate the name of the file in __FILE__.
In Ruby 2.0 there is a Kernel function called __dir__ that is implemented as File.dirname(File.realpath(__FILE__)).
Two references:
File::expand_path method documentation
How does __FILE__ work in Ruby
I stumbled across this today:
boot.rb commit in the Rails Github
If you go up two directories from boot.rb in the directory tree:
/railties/lib/rails/generators/rails/app/templates
you see Gemfile, which leads me to believe that File.expand_path("../../Gemfile", __FILE__) references following file: /path/to/this/file/../../Gemfile

A Ruby method for resolving relative paths while keeping absolute paths?

I am working on writing a rake build scrip which will work cross platform ( Mac OSX, Linux , Windows ). The build script will be consumed by a CI server.
I want the logic of my script to be as follows:
If the path is determined to be relative, make it absolute by making output_path = FOO_HOME + user_supplied_relative_path
If the path is determined to be absolute, take it as-is
I'm currently using Pathname.new(location).absolute? but it's not working correctly on windows.
What approach would you suggest for this?
require 'pathname'
(Pathname.new "/foo").absolute? # => true
(Pathname.new "foo").absolute? # => false
The method you're looking for is realpath.
Essentially you do this:
absolute_path = Pathname.new(path).realpath
N.B.: The Pathname module states that usage is experimental on machines that do not have unix like pathnames. So it's implementation dependent. Looks like JRuby should work on Windows.
There is a built-in function that covers both cases and does exactly what you want:
output_path = File.absolute_path(user_supplied_path, FOO_HOME)
The trick is supplying a second argument. It servers as a base directory if (and only if) the first argument is a relative path.
Pathname can do all that for you
require "pathname"
home= Pathname.new("/home/foo")
home + Pathname.new("/bin") # => #<Pathname:/bin>
home + Pathname.new("documents") # => #<Pathname:/home/foo/documents>
I am not sure about this on windows though.
You could also use File.expand_path if the relative directory is relative to the current working directory.
I checked on Linux and windows and didn't have any issues.
Assuming FOO_HOME is the working directory, the code would be:
output_path = File.expand_path user_supplied_relative_path

Resources