Class: Licensed::Sources::Bundler

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

Defined Under Namespace

Classes: BundlerSpecification, Dependency, 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



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/licensed/sources/bundler.rb', line 203

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.



276
277
278
279
280
281
282
283
# File 'lib/licensed/sources/bundler.rb', line 276

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



242
243
244
# File 'lib/licensed/sources/bundler.rb', line 242

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

#enabled?Boolean

Returns:

  • (Boolean)


80
81
82
83
84
85
# File 'lib/licensed/sources/bundler.rb', line 80

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



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/licensed/sources/bundler.rb', line 87

def enumerate_dependencies
  with_local_configuration do
    specs.map do |spec|
      error = spec.error if spec.respond_to?(:error)
      Dependency.new(
        name: spec.name,
        version: spec.version.to_s,
        path: spec.gem_dir,
        loaded_from: spec.loaded_from,
        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)


192
193
194
195
196
197
198
199
# File 'lib/licensed/sources/bundler.rb', line 192

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



255
256
257
258
259
260
261
# File 'lib/licensed/sources/bundler.rb', line 255

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.



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/licensed/sources/bundler.rb', line 147

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 matching_spec(dependency) || 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



264
265
266
267
# File 'lib/licensed/sources/bundler.rb', line 264

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



248
249
250
# File 'lib/licensed/sources/bundler.rb', line 248

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)


173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/licensed/sources/bundler.rb', line 173

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



270
271
272
273
# File 'lib/licensed/sources/bundler.rb', line 270

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

#matching_spec(dependency) ⇒ Object

Loads a dependency specification using rubygems’ built-in ‘Dependency#matching_specs` and `Dependency#to_spec`, from the original gem environment



229
230
231
232
233
234
235
236
237
238
239
# File 'lib/licensed/sources/bundler.rb', line 229

def matching_spec(dependency)
  begin
    ::Bundler.with_original_env do
      ::Bundler.rubygems.clear_paths
      return unless dependency.matching_specs(true).any?
      BundlerSpecification.new(dependency.to_spec)
    end
  ensure
    ::Bundler.configure
  end
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



122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/licensed/sources/bundler.rb', line 122

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.



287
288
289
290
# File 'lib/licensed/sources/bundler.rb', line 287

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



108
109
110
111
112
113
114
115
116
117
118
# File 'lib/licensed/sources/bundler.rb', line 108

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.



139
140
141
142
143
144
# File 'lib/licensed/sources/bundler.rb', line 139

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