lib/yard/rubygems/hook.rb



# frozen_string_literal: true
require 'rubygems'
require 'rubygems/user_interaction'
require 'fileutils'

##
# Gem::YARDoc provides methods to generate YARDoc and yri data for installed gems
# upon gem installation.
#
# This file is automatically required by RubyGems 1.9 and newer.

module YARD
  class RubygemsHook
    include Gem::UserInteraction
    extend Gem::UserInteraction

    @yard_version = nil

    ##
    # Force installation of documentation?

    attr_accessor :force

    ##
    # Generate yard?

    attr_accessor :generate_yard

    ##
    # Generate yri data?

    attr_accessor :generate_yri

    class << self
      ##
      # Loaded version of YARD. Set by ::load_yard

      attr_reader :yard_version
    end

    ##
    # Post installs hook that generates documentation for each specification in
    # +specs+

    def self.generation_hook(installer, specs)
      start = Time.now
      types = installer.document

      generate_yard = types.include?('yardoc') || types.include?('yard')
      generate_yri = types.include? 'yri'

      specs.each do |spec|
        gen_yard = generate_yard
        gen_yri = generate_yri
        gen_yri = false if gen_yard # never generate both, no need
        unless types.empty? # --no-document is not in effect
          # look at spec.metadata['yard.run'] for override
          run_yard = spec.metadata['yard.run']
          gen_yard = true if run_yard && run_yard != 'yri'
          gen_yri = true if run_yard == 'yri'
        end

        new(spec, gen_yard, gen_yri).generate
      end

      return unless generate_yard || generate_yri

      duration = (Time.now - start).to_i
      names = specs.map(&:name).join ', '

      say "Done installing documentation for #{names} after #{duration} seconds"
    end

    ##
    # Pre uninstalls hook that removes documentation
    #

    def self.removal_hook(uninstaller)
      new(uninstaller.spec).remove
    end

    ##
    # Loads the YARD generator

    def self.load_yard
      return if @yard_version

      require 'yard'

      @yard_version = Gem::Version.new ::YARD::VERSION
    end

    def initialize(spec, generate_yard = false, generate_yri = true)
      @doc_dir = spec.doc_dir
      @force = false
      @spec = spec

      @generate_yard = generate_yard
      @generate_yri = generate_yri

      @yard_dir = spec.doc_dir('yard')
      @yri_dir = spec.doc_dir('.yardoc')
    end

    def run_yardoc(*args)
      args << '--quiet' unless Gem.configuration.really_verbose
      args << '--backtrace' if Gem.configuration.backtrace
      unless File.file?(File.join(@spec.full_gem_path, '.yardopts'))
        args << @spec.require_paths
        unless @spec.extra_rdoc_files.empty?
          args << '-'
          args += @spec.extra_rdoc_files
        end
      end
      args = args.flatten.map(&:to_s)

      Dir.chdir(@spec.full_gem_path) do
        YARD::CLI::Yardoc.run(*args)
      end
    rescue Errno::EACCES => e
      dirname = File.dirname e.message.split("-")[1].strip
      raise Gem::FilePermissionError, dirname
    rescue => ex
      alert_error "While generating documentation for #{@spec.full_name}"
      ui.errs.puts "... MESSAGE:   #{ex}"
      ui.errs.puts "... YARDOC args: #{args.join(' ')}"
      ui.errs.puts "\t#{ex.backtrace.join("\n\t")}" if Gem.configuration.backtrace
      ui.errs.puts "(continuing with the rest of the installation)"
    end

    def install_yard
      FileUtils.rm_rf @yard_dir

      say "Installing YARD documentation for #{@spec.full_name}..."
      run_yardoc '--no-progress', '--db', @yri_dir, '-o', @yard_dir
    end

    def install_yri
      FileUtils.rm_rf @yri_dir

      say "Building YARD (yri) index for #{@spec.full_name}..."
      run_yardoc '--no-progress', '-c', '-n', '--db', @yri_dir
    end

    ##
    # Generates YARD and yri data

    def generate
      return if @spec.default_gem?
      return unless @generate_yri || @generate_yard

      setup

      install_yri if @generate_yri && (@force || !File.exist?(@yri_dir))

      install_yard if @generate_yard && (@force || !File.exist?(@yard_dir))
    end

    ##
    # Prepares the spec for documentation generation

    def setup
      self.class.load_yard

      if File.exist?(@doc_dir)
        raise Gem::FilePermissionError, @doc_dir unless File.writable?(@doc_dir)
      else
        FileUtils.mkdir_p @doc_dir
      end
    end

    def uninstall_yard
      if File.exist?(@yard_dir)
        raise Gem::FilePermissionError, @yard_dir unless File.writable?(@yard_dir)
        FileUtils.rm_rf @yard_dir
      end
    end

    def uninstall_yri
      if File.exist?(@yri_dir)
        raise Gem::FilePermissionError, @yri_dir unless File.writable?(@yri_dir)
        FileUtils.rm_rf @yri_dir
      end
    end

    ##
    # Removes YARD and yri data

    def remove
      uninstall_yri
      uninstall_yard
    end
  end
end

Gem.done_installing(&YARD::RubygemsHook.method(:generation_hook))
Gem.pre_uninstall(&YARD::RubygemsHook.method(:removal_hook))