Class: Licensed::Sources::Bundler

Inherits:
Source
  • Object
show all
Defined in:
lib/licensed/sources/bundler.rb

Defined Under Namespace

Classes: MissingSpecification

Constant Summary collapse

GEMFILES =
%w{Gemfile gems.rb}.freeze
DEFAULT_WITHOUT_GROUPS =
%i{development test}

Instance Attribute Summary

Attributes inherited from Source

#config

Instance Method Summary collapse

Methods inherited from Source

#dependencies, #ignored?, inherited, #initialize, type

Constructor Details

This class inherits a constructor from Licensed::Sources::Source

Instance Method Details

#bundle_exec_gem_spec(name) ⇒ Object

Load a gem specification from the YAML returned from ‘gem specification` This is a last resort when licensed can’t obtain a specification from other means



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/licensed/sources/bundler.rb', line 164

def bundle_exec_gem_spec(name)
  # `gem` must be available to run `gem specification`
  return unless Licensed::Shell.tool_available?("gem")

  # use `gem specification` with a clean ENV and clean Gem.dir paths
  # to get gem specification at the right directory
  begin
    ::Bundler.with_original_env do
      ::Bundler.rubygems.clear_paths
      yaml = Licensed::Shell.execute(*ruby_command_args("gem", "specification", name))
      spec = Gem::Specification.from_yaml(yaml)
      # this is horrible, but it will cache the gem_dir using the clean env
      # so that it can be used outside of this block
      spec.gem_dir
      spec
    end
  rescue Licensed::Shell::Error
    # return nil
  ensure
    ::Bundler.configure
  end
end

#bundler_exeObject

Returns the configured bundler executable to use, or “bundle” by default.



222
223
224
225
226
227
228
229
# File 'lib/licensed/sources/bundler.rb', line 222

def bundler_exe
  @bundler_exe ||= begin
    exe = @config.dig("bundler", "bundler_exe")
    return "bundle" unless exe
    return exe if Licensed::Shell.tool_available?(exe)
    @config.root.join(exe)
  end
end

#definitionObject

Build the bundler definition



188
189
190
# File 'lib/licensed/sources/bundler.rb', line 188

def definition
  @definition ||= ::Bundler::Definition.build(gemfile_path, lockfile_path, nil)
end

#enabled?Boolean

Returns:

  • (Boolean)


42
43
44
45
46
47
# File 'lib/licensed/sources/bundler.rb', line 42

def enabled?
  # running a ruby-packer-built licensed exe when ruby isn't available
  # could lead to errors if the host ruby doesn't exist
  return false if ruby_packer? && !Licensed::Shell.tool_available?("ruby")
  defined?(::Bundler) && lockfile_path && lockfile_path.exist?
end

#enumerate_dependenciesObject



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/licensed/sources/bundler.rb', line 49

def enumerate_dependencies
  with_local_configuration do
    specs.map do |spec|
      error = spec.error if spec.respond_to?(:error)
      Licensed::Dependency.new(
        name: spec.name,
        version: spec.version.to_s,
        path: spec.gem_dir,
        errors: Array(error),
        metadata: {
          "type"     => Bundler.type,
          "summary"  => spec.summary,
          "homepage" => spec.homepage
        }
      )
    end
  end
end

#exclude_development_dependencies?Boolean

Returns whether development dependencies should be excluded

Returns:

  • (Boolean)


153
154
155
156
157
158
159
160
# File 'lib/licensed/sources/bundler.rb', line 153

def exclude_development_dependencies?
  @include_development ||= begin
    # check whether the development dependency group is explicitly removed
    # or added via bundler and licensed configurations
    groups = [:development] - Array(::Bundler.settings[:without]) + Array(::Bundler.settings[:with]) - exclude_groups
    !groups.include?(:development)
  end
end

#exclude_groupsObject

Returns any groups to exclude specified from both licensed configuration and bundler configuration. Defaults to [:development, :test] + ::Bundler.settings



201
202
203
204
205
206
207
# File 'lib/licensed/sources/bundler.rb', line 201

def exclude_groups
  @exclude_groups ||= begin
    exclude = Array(@config.dig("bundler", "without"))
    exclude = DEFAULT_WITHOUT_GROUPS if exclude.empty?
    exclude.uniq.map(&:to_sym)
  end
end

#gem_spec(dependency) ⇒ Object

Returns a Gem::Specification for the provided gem argument.



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/licensed/sources/bundler.rb', line 108

def gem_spec(dependency)
  return unless dependency

  # find a specifiction from the resolved ::Bundler::Definition specs
  spec = definition.resolve.find { |s| s.satisfies?(dependency) }

  # a nil spec should be rare, generally only seen from bundler
  return bundle_exec_gem_spec(dependency.name) if spec.nil?

  # try to find a non-lazy specification that matches `spec`
  # spec.source.specs gives access to specifications with more
  # information than spec itself, including platform-specific gems.
  # these objects should have all the information needed to detect license metadata
  source_spec = spec.source.specs.find { |s| s.name == spec.name && s.version == spec.version }
  return source_spec if source_spec

  # look for a specification at the bundler specs path
  spec_path = ::Bundler.specs_path.join("#{spec.full_name}.gemspec")
  return Gem::Specification.load(spec_path.to_s) if File.exist?(spec_path.to_s)

  # if the specification file doesn't exist, get the specification using
  # the bundler and gem CLI
  bundle_exec_gem_spec(dependency.name)
end

#gemfile_pathObject

Returns the path to the Bundler Gemfile



210
211
212
213
# File 'lib/licensed/sources/bundler.rb', line 210

def gemfile_path
  @gemfile_path ||= GEMFILES.map { |g| @config.pwd.join g }
                            .find { |f| f.exist? }
end

#groupsObject

Returns the bundle definition groups, removing “without” groups, and including “with” groups



194
195
196
# File 'lib/licensed/sources/bundler.rb', line 194

def groups
  definition.groups - Array(::Bundler.settings[:without]) + Array(::Bundler.settings[:with]) - exclude_groups
end

#include?(dependency, source) ⇒ Boolean

Returns whether a dependency should be included in the final

Returns:

  • (Boolean)


134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/licensed/sources/bundler.rb', line 134

def include?(dependency, source)
  # ::Bundler::Dependency has an extra `should_include?`
  return false unless dependency.should_include? if dependency.respond_to?(:should_include?)

  # Don't return gems added from `add_development_dependency` in a gemspec
  # if the :development group is excluded
  gemspec_source = source.is_a?(::Bundler::Source::Gemspec)
  return false if dependency.type == :development && (!gemspec_source || exclude_development_dependencies?)

  # Gem::Dependency don't have groups - in our usage these objects always
  # come as child-dependencies and are never directly from a Gemfile.
  # We assume that all Gem::Dependencies are ok at this point
  return true if dependency.groups.nil?

  # check if the dependency is in any groups we're interested in
  (dependency.groups & groups).any?
end

#lockfile_pathObject

Returns the path to the Bundler Gemfile.lock



216
217
218
219
# File 'lib/licensed/sources/bundler.rb', line 216

def lockfile_path
  return unless gemfile_path
  @lockfile_path ||= gemfile_path.dirname.join("#{gemfile_path.basename}.lock")
end

#recursive_specs(specs, results = Set.new) ⇒ Object

Recursively finds the dependencies for Gem specifications. Returns a ‘Set` containing the package names for all dependencies



83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/licensed/sources/bundler.rb', line 83

def recursive_specs(specs, results = Set.new)
  return [] if specs.nil? || specs.empty?

  new_specs = Set.new(specs) - results.to_a
  return [] if new_specs.empty?

  results.merge new_specs

  dependency_specs = new_specs.flat_map { |s| specs_for_dependencies(s.dependencies, s.source) }

  return results if dependency_specs.empty?

  results.merge recursive_specs(dependency_specs, results)
end

#ruby_command_args(*args) ⇒ Object

Determines if the configured bundler executable is available and returns shell command args with or without ‘bundle exec` depending on availability.



233
234
235
236
# File 'lib/licensed/sources/bundler.rb', line 233

def ruby_command_args(*args)
  return Array(args) unless Licensed::Shell.tool_available?(bundler_exe)
  [bundler_exe, "exec", *args]
end

#specsObject

Returns an array of Gem::Specifications for all gem dependencies



69
70
71
72
73
74
75
76
77
78
79
# File 'lib/licensed/sources/bundler.rb', line 69

def specs
  # get the specifications for all dependencies in a Gemfile
  root_dependencies = definition.dependencies.select { |d| include?(d, nil) }
  root_specs = specs_for_dependencies(root_dependencies, nil).compact

  # recursively find the remaining specifications
  all_specs = recursive_specs(root_specs)

  # delete any specifications loaded from a gemspec
  all_specs.delete_if { |s| s.source.is_a?(::Bundler::Source::Gemspec) }
end

#specs_for_dependencies(dependencies, source) ⇒ Object

Returns the specs for dependencies that pass the checks in ‘include?`. Returns a `MissingSpecification` if a gem specification isn’t found.



100
101
102
103
104
105
# File 'lib/licensed/sources/bundler.rb', line 100

def specs_for_dependencies(dependencies, source)
  included_dependencies = dependencies.select { |d| include?(d, source) }
  included_dependencies.map do |dep|
    gem_spec(dep) || MissingSpecification.new(name: dep.name, requirement: dep.requirement)
  end
end