# frozen_string_literal: true require 'shopify_cli' module Rails class Gem include SmartProperties property :ctx, accepts: ShopifyCli::Context, required: true property :name, converts: :to_s, required: true property :version, converts: :to_s class << self def install(ctx, *args) name = args.shift version = args.shift gem = new(ctx: ctx, name: name, version: version) ctx.debug(ctx.message('rails.gem.installed_debug', name, gem.installed?)) gem.installed? ? true : gem.install! end def binary_path_for(ctx, binary) path_to_binary = File.join(gem_home(ctx), 'bin', binary) File.exist?(path_to_binary) ? path_to_binary : binary end def gem_home(ctx) ctx.getenv('GEM_HOME') || apply_gem_home(ctx) end def gem_path(ctx) ctx.getenv('GEM_PATH') || apply_gem_path(ctx) end private def apply_gem_home(ctx) path = '' # extract GEM_HOME from `gem environment home` command out, stat = ctx.capture2e('gem', 'environment', 'home') path = out&.empty? ? '' : out.strip if stat.success? # fallback if return from `gem environment home` is empty (somewhat unlikely) path = fallback_gem_home_path(ctx) if path.empty? # fallback if path isn't writable (if using a system installed ruby) path = fallback_gem_home_path(ctx) unless File.writable?(path) ctx.mkdir_p(path) unless Dir.exist?(path) ctx.debug(ctx.message('rails.gem.setting_gem_home', path)) ctx.setenv('GEM_HOME', path) end def apply_gem_path(ctx) path = '' out, stat = ctx.capture2e('gem', 'environment', 'path') path = out&.empty? ? '' : out.strip if stat.success? # usually GEM_PATH already contains GEM_HOME # if gem_home() falls back to our fallback path, we need to add it path = gem_home(ctx) + File::PATH_SEPARATOR + path unless path.include?(gem_home(ctx)) ctx.debug(ctx.message('rails.gem.setting_gem_path', path)) ctx.setenv('GEM_PATH', path) end def fallback_gem_home_path(ctx) File.join(ctx.getenv('HOME'), '.gem', 'ruby', RUBY_VERSION) end end def installed? found = false paths = self.class.gem_path(ctx).split(File::PATH_SEPARATOR) paths.each do |path| ctx.debug(ctx.message('rails.gem.checking_installation_path', "#{path}/gems/", name)) found = !!Dir.glob("#{path}/gems/#{name}-*").detect do |f| gem_satisfies_version?(f) end break if found end found end def install! spin = CLI::UI::SpinGroup.new spin.add(ctx.message('rails.gem.installing', name)) do |spinner| args = %w(gem install) args.push(name) unless version.nil? if ctx.windows? && version.include?('~') args.push('-v', "\"#{version}\"") else args.push('-v', version) end end ctx.system(*args) spinner.update_title(ctx.message('rails.gem.installed', name)) end spin.wait end def gem_satisfies_version?(path) if version # there was a specific version given during new(), so # check version of gem found to determine match require 'semantic/semantic' found_version, _ = path.match(%r{/#{Regexp.quote(name)}-([\d\.]+)})&.captures found_version.nil? ? false : Semantic::Version.new(found_version).satisfies?(version) else # otherwise ignore the actual version number, # just check there's an initial digit %r{/#{Regexp.quote(name)}-\d}.match?(path) end end end end