# 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\.\d\.\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