How to list ruby production only dependencies using Gemfile.lock and LockfileParser class

纵饮孤独 提交于 2021-02-04 08:27:38

问题


I have a tool that analyze some ruby projects having Gemfile and Gemfile.lock files. This tool given in input the path where ruby project is, list all its dependencies.

My problem is that I only need to print production dependencies excluding development and test. Today I find out that my code does not exclude them and I do not know how to modify it for my purpose. How I can remove development and test dependencies from the list?

Here a simplified version of the ruby code I use to list dependencies:

project_path = ARGV.map {|path| File.expand_path(path) }

ENV['BUNDLE_GEMFILE'] = project_path+"/Gemfile"

lockfile_path=project_path+"/Gemfile.lock"

lockfile_contents = File.read(lockfile_path)
parser = Bundler::LockfileParser.new(lockfile_contents)

lockfile_contents = File.read(lockfile_path)
parser = Bundler::LockfileParser.new(lockfile_contents)

to_fetch = []
parser.specs.each do |spec|
    gem_basename = "#{spec.name},#{spec.version}"
    to_fetch << gem_basename
end

to_fetch.uniq!
to_fetch.sort!

The variable to_fetch contains the dependencies.

Here the Gemfile:

source "https://rubygems.org"

gem "cf-message-bus", git: "https://github.com/cloudfoundry/cf-message-bus.git"
gem "vcap_common", git: "https://github.com/cloudfoundry/vcap-common.git"
gem "aws-sdk", '~> 2', require: false
gem "steno"
gem "httparty"

group :test do
    gem 'codeclimate-test-reporter', require: false
    gem "rake"
    gem "rspec"
    gem "ci_reporter"
    gem "timecop"
    gem "webmock"
end

Let me know if you need also the Gemfile.lock that is 149 lines long.


回答1:


I will leave my other answer in case it helps someone else: Here is the revised version based on what I think you want

require 'bundler'

class DependencyTree
  attr_reader :definition
  def initialize(gemfile,lockfile)
    @gemfile = gemfile
    @definition = Bundler::Definition.build(gemfile,lockfile,nil)
  end

  def all_dependencies
    return @all_dependencies if @all_dependencies
    collect_dependencies
  end

  def inspect
    "#<#{self.class.name}:#{self.object_id} Gemfile: #{Pathname.new(@gemfile).expand_path} >"
  end

  def lock_file
    @definition.locked_gems
  end

  def to_h
    lock_file.specs.each_with_object(Hash.new {|h,k| h[k] = []}) do |lock,obj|
      gem_file_dep = all_dependencies.detect {|dep| dep[:name] == lock.name} || {group: :unknown}
      name = lock.full_name.dup
      name << " (#{gem_file_dep[:error]})" if gem_file_dep[:error]
      obj[gem_file_dep[:group]] << name                    
    end
  end

  private 
    def groupify(dep)
      dep.groups.map do |g| 
        a = [{group: g, name: dep.name}] 
        begin
          a << runtime_dependencies(g,dep.to_spec)
        rescue Gem::LoadError => e
          a[-1] =  {group: g, name: dep.name,error: 'NOT INSTALLED'}
        end
      end
    end
    def collect_dependencies
      @all_dependencies = @definition.dependencies.map do |dep|
          groupify(dep)
      end.flatten
      group_missing
      @all_dependencies.uniq!
    end
    def runtime_dependencies(group,spec)
      spec.dependencies.select { |dep| dep.type == :runtime}.map do |dep|
          a = {group: group, name: dep.name}
          dep.to_spec.dependencies.empty? ? a : [a] << runtime_dependencies(group,dep.to_spec)
      end
    end
    def group_missing
      all_locks.cycle(2) do |a|
        deep_dep = @all_dependencies.find_all {|h| a.include?(h[:name])}.uniq
        a.each do |k|
          deep_dep.each do |h|
            all_dependencies << {group: h[:group], name: k, error: 'NOT INSTALLED'}
          end
        end
      end
    end
    def all_locks
      lock_file.specs.map do |spec|
        spec.to_lock.delete(' ').split("\n").map do |s| 
          s.slice(/^[\w\-]+/)
        end
      end
    end
end  

the usage is:

 dt = DependencyTree.new('Gemfile','Gemfile.lock') 
 dt.to_h

output Snippet:

{:default=>
  ["actionmailer-4.2.5.2 (NOT INSTALLED)",
   "actionpack-4.2.5.2",
   "actionview-4.2.5.2",
   "activejob-4.2.5.2 (NOT INSTALLED)",
   "activemodel-4.2.5.2",
   "activerecord-4.2.5.2",
   "activerecord-sqlserver-adapter-4.2.17",
   "activesupport-4.2.5.2",
   "arel-6.0.3 (NOT INSTALLED)",
   "axlsx-2.0.1 (NOT INSTALLED)",
   "binding_of_caller-0.7.2 (NOT INSTALLED)",
   "builder-3.2.3",
   "coffee-rails-4.1.1 (NOT INSTALLED)",
   "coffee-script-2.4.1 (NOT INSTALLED)",
   "coffee-script-source-1.12.2 (NOT INSTALLED)",
   "concurrent-ruby-1.0.4",
   "debug_inspector-0.0.2 (NOT INSTALLED)",
   "erubis-2.7.0",
   "execjs-2.7.0"],
:development=>
  ["airbrussh-1.1.2",
   "byebug-9.0.6 (NOT INSTALLED)",
   "capistrano-3.7.2"],
:doc => ["sdoc-0.4.2 (NOT INSTALLED)"]}

production gems will be in :default development gems would be :default + :development




回答2:


How about this

require 'bundler'

dependencies = Bundler::Definition.build('Gemfile','Gemfile.lock',nil).
     dependencies.each_with_object(Hash.new { |h,k| h[k] = [] }) do |dep,obj| 
       dep.groups.each do |g|
         obj[g] << {name: dep.name,
          required_version: dep.requirement.requirements.join,
          actual_version: dep.to_spec.version.to_s
          }
       end
end

This will group all the dependencies by their group and place them in Hashes containing their name, their required version and their specific version. For example:

{:default=>
  [{:name=>"rake", :required_version=>"=11.3.0", :actual_version=>"11.3.0"},
   {:name=>"rails", :required_version=>"=4.2.5.2", :actual_version=>"4.2.5.2"},
   {:name=>"arel", :required_version=>"=6.0.3", :actual_version=>"6.0.3"},
   {:name=>"activerecord",
    :required_version=>"~>4.2.0",
    :actual_version=>"4.2.8"},
   {:name=>"activerecord-sqlserver-adapter",
    :required_version=>"~>4.2.0",
    :actual_version=>"4.2.18"},
   {:name=>"tiny_tds", :required_version=>"~>0.7.0", :actual_version=>"0.7.0"}],
 :doc=>
  [{:name=>"sdoc", :required_version=>"~>0.4.0", :actual_version=>"0.4.2"}],
 :development=>
  [{:name=>"byebug", :required_version=>">=0", :actual_version=>"9.0.6"},
   {:name=>"web-console",
    :required_version=>"~>2.0",
    :actual_version=>"2.3.0"},
   {:name=>"capistrano",
    :required_version=>"=3.7.2",
    :actual_version=>"3.7.2"}],
 :test=>
  [{:name=>"byebug", :required_version=>">=0", :actual_version=>"9.0.6"}]}

BTW :default is production (might need to expand further to handle dependencies of dependencies I will see what I can do)

Update 1: Similar formatting lists :runtime dependencies of dependencies (not uniq)

  def dep_to_hash(dep)
    {name: dep.name,
      required_version: dep.requirement.requirements.join,
      actual_version: dep.to_spec.version.to_s,
      dependencies: dep.to_spec.dependencies.select {|d| d.type == :runtime}.each_with_object(Hash.new { |h,k| h[k] = [] }) do |dep2,obj|
        obj[dep2.type] << dep_to_hash(dep2)
      end
      }
  end


  deps = Bundler::Definition.build('Gemfile','Gemfile.lock',nil).
     dependencies.each_with_object(Hash.new { |h,k| h[k] = [] }) do |dep,obj| 
       dep.groups.each do |g|
         obj[g] << dep_to_hash(dep)
       end
  end

Output snippet:

 {:default=>
  [{:name=>"rails",
    :required_version=>"=4.2.5.2",
    :actual_version=>"4.2.5.2",
    :dependencies=>
     {:runtime=>
       [{:name=>"activesupport",
         :required_version=>"=4.2.5.2",
         :actual_version=>"4.2.5.2",
         :dependencies=>
          {:runtime=>
            [{:name=>"i18n",
              :required_version=>"~>0.7",
              :actual_version=>"0.8.1",
              :dependencies=>{}},
             {:name=>"json",
              :required_version=>">=1.7.7~>1.7",
              :actual_version=>"1.8.6",
              :dependencies=>{}},

Update 2 (More like what you seem to be targeting now and is uniq by "group")

    require 'bundler'
    def add_to_dep(dep,top_level)
      deps = dep.to_spec.dependencies.select {|d| d.type == :runtime}
      deps.each do |dep|
        add_to_dep(dep,top_level) 
      end
      # handle existing dependencies by using highest version
      exists = top_level.grep(/#{dep.name}/)[0]
      if exists
        version = exists.split(',').last
        new_version = dep.to_spec.version.to_s
        if new_version > version
          top_level.delete_at(top_level.index(exists))
          top_level << "#{dep.name}, #{dep.to_spec.version}" 
        end
      else
        top_level << "#{dep.name}, #{dep.to_spec.version}"
      end  
    end


    deps = Bundler::Definition.build('Gemfile','Gemfile.lock',nil).
       dependencies.each_with_object(Hash.new { |h,k| h[k] = [] }) do |dep,obj| 
         dep.groups.each do |g|
           add_to_dep(dep,obj[g])
         end
    end.each {|_k,v| v.sort!}

Output Snippet:

{:default=>
  ["actionmailer, 4.2.5.2",
   "actionpack, 4.2.8",
   "actionview, 4.2.8",
   "activejob, 4.2.5.2",
   "activemodel, 4.2.8",
   "activerecord, 4.2.8",
   "activerecord-sqlserver-adapter, 4.2.18",
   "activesupport, 4.2.8",
   "arel, 6.0.4",
   "axlsx, 2.0.1",
   "builder, 3.2.3",
   "bundler, 1.14.6",
   "coffee-rails, 4.1.1",
   "coffee-script, 2.4.1",
   "coffee-script-source, 1.12.2",
   "concurrent-ruby, 1.0.5",
   "erubis, 2.7.0",
   "execjs, 2.7.0",
   "globalid, 0.4.0",
   "hash-deep-merge, 0.1.1",
   "htmlentities, 4.3.4",
   "i18n, 0.8.1",
   "jbuilder, 2.6.2",
   "jquery-rails, 4.2.2",
   "jquery-ui-rails, 6.0.1",
   "json, 1.8.6",
   "lazy_high_charts, 1.5.6",
   "loofah, 2.0.3",
   "mail, 2.6.5",
   "mime-types, 3.1",
   "mime-types-data, 3.2016.0521",


来源:https://stackoverflow.com/questions/44681231/how-to-list-ruby-production-only-dependencies-using-gemfile-lock-and-lockfilepar

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!