Ruby: How to deduce the "current" gem name?
Asked Answered
T

2

5

I'm aware of the following to grab a Gem's specification for interrogation:

spec = Gem::Specification.find_by_name('my_gem')

Is there a way to programmatically identify "this" gem's name such that the above could be rewritten in a reusable manner?

In other words, how can you get the parent gem's name from some executing Ruby code at runtime?

Tracheo answered 13/12, 2012 at 12:11 Comment(8)
Let's say, code from gem Foo is called by gem Bar. Which one should be returned as "this gem"? :)Pastrami
If Foo contains the "this gem" code then Foo else Bar. There's no reason why this information shouldn't be available, it entirely depends on whether they've included the feature in the Gem API. Since code is loaded from a gem the ruby runtime could easily be aware of the parent gem for an execution context.Tracheo
Let's say you're calling a method from gem Foo which has been mixed in to a gem Bar which calls super for gem Baz and has been monkeypatched by gem Qux? Which one is the "parent" gem?Grovel
@Mark it doesn't matter how complex the mixin/import/include scenario is, it's still feasible that the runtime can be aware of the parent gem for the execution context containing the parent gem name reference. This is possible thanks to runtime gem resolution. So if the reference is being printed out in Foo then it'd be Foo, if it's in Bar then Bar etc. As I said before, it simply depends on whether they've coded the runtime to provide this information.Tracheo
I guess what I was trying to illustrate is that, unlike a Class hierarchy, there isn't a Gem hierarchy, and there is no concept of "current" gem or "parent" gem. So I disagree with the statement that the ruby runtime could easily be aware of such a thing in the execution context.Grovel
That's down to the runtime implementation; if you're confident the runtime doesn't record such information, or at least the API doesn't expose it either way, then feel free to answer and I'll mark it as correct.Tracheo
It all depends on what you mean by "parent gem". If you mean the gem that called require on the current one, then you could probably parse it out of the backtrace.Akim
That's a fair response - thank you for the info. You deserve a tick for your time; looks like no-one else is going to respond (presumably there's no simple API route to this) so please post the comment as an answer and I'll accept it.Tracheo
A
8

To find the gem specification for the current source file (assuming it's a source file in the lib dir):

require 'rubygems'

searcher = if Gem::Specification.respond_to? :find
  # ruby 2.0
  Gem::Specification
elsif Gem.respond_to? :searcher
  # ruby 1.8/1.9
  Gem.searcher.init_gemspecs
end
spec = unless searcher.nil?
  searcher.find do |spec|
    File.fnmatch(File.join(spec.full_gem_path,'*'), __FILE__)
  end
end

You could make this reusable by passing in the __FILE__ from the source file you're actually interested in, or by examining the caller stack (but that's probably a bad idea).

Actionable answered 2/3, 2013 at 8:9 Comment(1)
Dang... you'd think this value would get loaded at runtime eliminating the need to dig through files and compare paths. This is good though.Lock
R
0

I used the safe navigation operator on the solution provided by @JacobLukas, and wrapped the code into a method.

I also changed the File.fnmatch pattern from * to **, which means to match directories recursively or files expansively. This makes the method return the proper Gem::Specification when pointed at any file in any directory within a gem.

Tested with Ruby version 3.1.0p0.

# @param file must be a fully qualified file name
# @return Gem::Specification of gem that file points into, or nil if not called from a gem
def current_spec(file)
  searcher = if Gem::Specification.respond_to?(:find)
               Gem::Specification
             elsif Gem.respond_to?(:searcher)
               Gem.searcher.init_gemspecs
             end

  searcher&.find do |spec|
    File.fnmatch(File.join(spec.full_gem_path, '**'), file)
  end
end

A simpler, and more familiar, way to accomplish the same file path match is:

file.start_with? spec.full_gem_path

This version assumes that the file exists. You could be explicit about that:

file.exist?

You could rewrite the method using the above like this:

# @param file must be a fully qualified directory name pointing to an installed gem, or within it,
#             or a file name within an installed gem
# @return Gem::Specification of gem that file points into,
# or nil if no gem exists at the given file
def current_spec(file)
  return nil unless File.exist? file

  searcher = if Gem::Specification.respond_to?(:find)
               Gem::Specification
             elsif Gem.respond_to?(:searcher)
               Gem.searcher.init_gemspecs
             end

  searcher&.find do |spec|
    spec.full_gem_path.start_with? file
  end
end

def gem_path(file)
  spec = self.current_spec2(file)
  spec&.full_gem_path
end

See https://www.mslinn.com/ruby/6550-gem-navel.html#self_discovery

Call either version the same to obtain the complete Gem Specification:

current_spec __FILE__
current_spec2 __FILE__

Obtain the absolute path to the installed gem:

gem_path __FILE__
Rolfrolfe answered 30/3, 2023 at 15:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.