pivotal-legacy/LicenseFinder

View on GitHub
lib/license_finder/package_managers/conda.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

require 'json'

module LicenseFinder
  class Conda < PackageManager
    attr_reader :conda_bash_setup_script

    def initialize(options = {})
      @conda_bash_setup_script = options[:conda_bash_setup_script] || Pathname("#{ENV['HOME']}/miniconda3/etc/profile.d/conda.sh")
      super
    end

    # This command is *not* directly executable. See .conda() below.
    def prepare_command
      "conda env create -f #{detected_package_path}"
    end

    def prepare
      return if environment_exists?

      prep_cmd = prepare_command
      _stdout, stderr, status = Dir.chdir(project_path) { conda(prep_cmd) }
      return if status.success?

      log_errors stderr
      raise "Prepare command '#{prep_cmd}' failed" unless @prepare_no_fail
    end

    def current_packages
      conda_list.map do |entry|
        case entry['channel']
        when 'pypi'
          # PyPI is much faster than `conda search`, use it when we can.
          PipPackage.new(entry['name'], entry['version'], PyPI.definition(entry['name'], entry['version']))
        else
          CondaPackage.new(conda_search_info(entry))
        end
      end.compact
    end

    def possible_package_paths
      [project_path.join('environment.yaml'), project_path.join('environment.yml')]
    end

    private

    def environment_exists?
      environments.grep(environment_name).any?
    end

    def environments
      command = 'conda env list'
      stdout, stderr, status = conda command

      environments = []
      if status.success?
        environments = stdout.split("\n").grep_v(/^#/).map { |line| line.split.first }
      else
        log_errors_with_cmd command, stderr
      end
      environments
    end

    def environment_file
      detected_package_path
    end

    def environment_name
      @environment_name ||= YAML.load_file(environment_file).fetch('name')
    end

    def conda(command)
      Open3.capture3('bash', '-c', "source #{conda_bash_setup_script} && #{command}")
    end

    def activated_conda(command)
      Open3.capture3('bash', '-c', "source #{conda_bash_setup_script} && conda activate #{environment_name} && #{command}")
    end

    # Algorithm is based on
    # https://bioinformatics.stackexchange.com/a/11226
    # but completely recoded in Ruby. Like the poster, if the package is
    # actually managed by conda, we assume that all the potential infos (for
    # various architectures, versions of python, etc) have the same license.
    def conda_list
      command = 'conda list'
      stdout, stderr, status = activated_conda(command)

      if status.success?
        conda_list = []
        stdout.each_line do |line|
          next if /^\s*#/.match?(line)

          name, version, build, channel = line.split
          conda_list << {
            'name' => name,
            'version' => version,
            'build' => build,
            'channel' => channel
          }
        end
        conda_list
      else
        log_errors_with_cmd command, stderr
        []
      end
    end

    def conda_search_info(list_entry)
      command = 'conda search --info --json '
      command += "--channel #{list_entry['channel']} " if list_entry['channel'] && !list_entry['channel'].empty?
      command += "'#{list_entry['name']} #{list_entry['version']}'"

      # Errors from conda (in --json mode, at least) show up in stdout, not stderr
      stdout, _stderr, status = activated_conda(command)

      name = list_entry['name']

      if status.success?
        JSON(stdout).fetch(name).first
      else
        log_errors_with_cmd command, stdout
        list_entry
      end
    rescue KeyError
      logger.info('Conda', "Key error trying to find #{name} in\n#{JSON(stdout)}")
      list_entry
    end
  end
end