lib/licensed/sources/bundler.rb in licensed-2.15.2 vs lib/licensed/sources/bundler.rb in licensed-3.0.0

- old
+ new

@@ -1,59 +1,22 @@ # frozen_string_literal: true require "delegate" begin require "bundler" + require "licensed/sources/bundler/missing_specification" rescue LoadError end module Licensed module Sources class Bundler < Source - class MissingSpecification < Gem::BasicSpecification - attr_reader :name, :requirement - alias_method :version, :requirement - def initialize(name:, requirement:) - @name = name - @requirement = requirement - end - - def dependencies - [] - end - - def source - nil - end - - def platform; end - def gem_dir; end - def gems_dir - Gem.dir - end - def summary; end - def homepage; end - - def error - "could not find #{name} (#{requirement}) in any sources" - end - end - - class BundlerSpecification < ::SimpleDelegator - def gem_dir - dir = super - return dir if File.exist?(dir) - - File.join(Gem.dir, "gems", full_name) - end - end - class Dependency < Licensed::Dependency attr_reader :loaded_from - def initialize(name:, version:, path:, loaded_from:, errors: [], metadata: {}) + def initialize(name:, version:, path:, loaded_from:, search_root:, errors: [], metadata: {}) @loaded_from = loaded_from - super name: name, version: version, path: path, errors: errors, metadata: metadata + super name: name, version: version, path: path, errors: errors, metadata: metadata, search_root: search_root end # Load a package manager file from the base Licensee::Projects::FsProject # or from a gem specification file. def package_file @@ -74,27 +37,34 @@ end end GEMFILES = { "Gemfile" => "Gemfile.lock", "gems.rb" => "gems.locked" } DEFAULT_WITHOUT_GROUPS = %i{development test} + RUBY_PACKER_ERROR = "The bundler source cannot be used from the executable built with ruby-packer. Please install licensed using `gem install` or using bundler." 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 def enumerate_dependencies + raise Licensed::Sources::Source::Error.new(RUBY_PACKER_ERROR) if ruby_packer? + with_local_configuration do specs.map do |spec| + next if spec.name == "bundler" && !include_bundler? + next if spec.name == config["name"] + 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, + search_root: spec_root(spec), errors: Array(error), metadata: { "type" => Bundler.type, "summary" => spec.summary, "homepage" => spec.homepage @@ -104,143 +74,37 @@ end end # Returns an array of Gem::Specifications for all gem dependencies 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) } + @specs ||= definition.specs_for(groups) end - # Recursively finds the dependencies for Gem specifications. - # Returns a `Set` containing the package names for all dependencies - 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 - - # Returns the specs for dependencies that pass the checks in `include?`. - # Returns a `MissingSpecification` if a gem specification isn't found. - 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) + # Returns whether to include bundler as a listed dependency of the project + def include_bundler? + @include_bundler ||= begin + # include if bundler is listed as a direct dependency that should be included + requested_dependencies = definition.dependencies.select { |d| (d.groups & groups).any? && d.should_include? } + return true if requested_dependencies.any? { |d| d.name == "bundler" } + # include if bundler is an indirect dependency + return true if specs.flat_map(&:dependencies).any? { |d| d.name == "bundler" } + false end end - # Returns a Gem::Specification for the provided gem argument. - def gem_spec(dependency) - return unless dependency + # Returns a search root for a specification, one of: + # - the local bundler gem location + # - the system rubygems install gem location + # - nil + def spec_root(spec) + return if spec.gem_dir.nil? + root = [Gem.default_dir, Gem.dir].find { |dir| spec.gem_dir.start_with?(dir) } + return unless root - # 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, dependency.requirement) 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, dependency.requirement) + "#{root}/gems/#{spec.full_name}" end - # Returns whether a dependency should be included in the final - 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 - - # Returns whether development dependencies should be excluded - 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 - - # 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 - def bundle_exec_gem_spec(name, requirement) - # `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, "-v", requirement.to_s)) - 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 when running from - # the ruby packer executable environment - spec.gem_dir if ruby_packer? - spec - end - rescue Licensed::Shell::Error - # return nil - ensure - ::Bundler.configure - end - end - - # Loads a dependency specification using rubygems' built-in - # `Dependency#matching_specs` and `Dependency#to_spec`, from the original - # gem environment - 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 - # Build the bundler definition def definition @definition ||= ::Bundler::Definition.build(gemfile_path, lockfile_path, nil) end @@ -281,73 +145,36 @@ def lockfile_path return unless gemfile_path @lockfile_path ||= gemfile_path.dirname.join(GEMFILES[gemfile_path.basename.to_s]) end - # Returns the configured bundler executable to use, or "bundle" by default. - 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 - - # Determines if the configured bundler executable is available and returns - # shell command args with or without `bundle exec` depending on availability. - def ruby_command_args(*args) - return Array(args) unless Licensed::Shell.tool_available?(bundler_exe) - [bundler_exe, "exec", *args] - end - private # helper to clear all bundler environment around a yielded block def with_local_configuration # force bundler to use the local gem file original_bundle_gemfile, ENV["BUNDLE_GEMFILE"] = ENV["BUNDLE_GEMFILE"], gemfile_path.to_s - if ruby_packer? - # if running under ruby-packer, set environment from host + # silence any bundler warnings while running licensed + bundler_ui, ::Bundler.ui = ::Bundler.ui, ::Bundler::UI::Silent.new - # hack: setting this ENV var allows licensed to use Gem paths outside - # of the ruby-packer filesystem. this is needed to find spec sources - # from the host filesystem - ENV["ENCLOSE_IO_RUBYC_1ST_PASS"] = "1" - ruby_version = Gem::ConfigMap[:ruby_version] - # set the ruby version in Gem::ConfigMap to the ruby version from the host. - # this helps Bundler find the correct spec sources and paths - Gem::ConfigMap[:ruby_version] = host_ruby_version - end - # reset all bundler configuration ::Bundler.reset! # and re-configure with settings for current directory ::Bundler.configure yield ensure - if ruby_packer? - # if running under ruby-packer, restore environment after block is finished - ENV.delete("ENCLOSE_IO_RUBYC_1ST_PASS") - Gem::ConfigMap[:ruby_version] = ruby_version - end - ENV["BUNDLE_GEMFILE"] = original_bundle_gemfile + ::Bundler.ui = bundler_ui + # restore bundler configuration - ::Bundler.reset! ::Bundler.configure end # Returns whether the current licensed execution is running ruby-packer def ruby_packer? @ruby_packer ||= RbConfig::TOPDIR =~ /__enclose_io_memfs__/ - end - - # Returns the ruby version found in the bundler environment - def host_ruby_version - Licensed::Shell.execute(*ruby_command_args("ruby", "-e", "puts Gem::ConfigMap[:ruby_version]")) end end end end