From ef92423ef1b6492bcec7d94971d63b87f5adf6c5 Mon Sep 17 00:00:00 2001 From: "masaru.terada" Date: Tue, 7 Nov 2023 08:54:15 +0900 Subject: [PATCH 01/14] Make it loose coupling between RubyGems and RDoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit \### Problems There are following problems because of tight coupling between RubyGems and RDoc. 1. If there are braking changes in RDoc, RubyGems is also broken. 2. When we maintain RDoc, we have to change RubyGems. The reason why they are happened is that RubyGems creates documents about a gem with installing it. Note that RubyGems uses functions of RDoc to create documents. Specifically, - Creating documents is executed by `rubygems/lib/rubygems/rdoc.rb`. - `::RDoc::RubygemsHook` which is defined by RDoc is called by the file. \### Solution RubyGems has the plugin system. If a gem includes `rubygems_plugin.rb`, RubyGems loads it. RubyGems executes a process defined in it while installing gems, uninstalling gems or other events. We can use the system to solve the problems. The root cause is RubyGems directly references the class of RDoc. We can remove the root cause by making RDoc RubyGems plugin. Alternatively `rubygems_plugin.rb` creates documents about gems. \### FAQ Q1. Do we need to change codes of RubyGems? A. No, we don't. This change keeps compatibility of API used from RubyGems. Q2. Is it better to delete existing codes related to RDoc in RubyGems? No, it isn't. If we change codes of RubyGems, we can't keep a compatibility. Example: If we delete codes that uses `RDoc::RubygemsHook` in `rubygems/lib/rubygems/rdoc.rb`, documentations are not created with old RDoc. Q3. When can we delete `rubygems/lib/rubygems/rdoc.rb`? A. We can delete it when all users use RDoc including `rubygems_plugin`. Next ruby version is 3.4. If it includes the RDoc including `rubygems_plugin`, we can delete `rubygems/lib/rubygems/rdoc.rb` after ruby 3.3 is EOL. Q4. Is it a breaking change that Rubygems creates documents with rubygems_plugin not RDoc::RubygemsHook? A. No, it isn't. If we simply implement this approach, we move the implementation from `rdoc/lib/rdoc/rubygems_hook.rb` to `rubygems_plugin.rb`. This way can be breaking change. It seems to be fine that we just need to delete `rdoc/rubygems_hook.rb` but it doesn't work. It generates multiple documents. `rubygems/lib/rubygems/rdoc.rb` has the following code. ``` begin require "rdoc/rubygems_hook" # ... rescue LoadError end ``` This code ignores RDoc related processes when `rdoc/rubygems_hook` can't be required. But, this 'require' is not failed. This is because Ruby installs Rdoc as a default gem. So, Rdoc installed as a default gem generates documents and one installed as a normal gem does it too. If you think that this behavior is accectable, we can just delete `rdoc/rubygems_hook.rb`. What do you think about this approach? In this change, we take another approach to solve the problem that creates multiple documents. If `Gem.done_installing(&Gem::RDoc.method(:generation_hook))` in `rubygems/rdoc.rb` doesn't create documents, we can solve the problem. We have some options. * We change `rubygems/rdoc.rb` and then don't execute `Gem.done_installing`. (This is a change for RubyGems.) * We change `rdoc/rubygems_hook.rb` and then make `generation_hook` a no-op method. (This is a change for RDoc.) We choose the latter to avoid changing for RubyGems. \### Test \#### Preparation Install Rdoc which including our changes by executing `rake install`. ❯ rake install We confirmed that Rdoc which including our changes was installed. ❯ gem list | grep rdoc rdoc (6.6.0, default: 6.4.0) \#### Check point We tested to check compatibility. How to chack the compatibility? We tested creating same documents by our RDoc and old RDoc with latest RubyGems. We used following versions to test. ``` ❯ ruby -v ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [arm64-darwin22] ❯ gem list | grep rdoc rdoc (default: 6.4.0) ❯ ruby -I rubygems/lib rubygems/exe/gem --version 3.5.14 ``` Here is a result of test with old RDoc. We can see that the document is created correctlly with `Parsing...` and `Done installing...`. ``` ❯ ruby -I rubygems/lib rubygems/exe/gem install pkg-config Successfully installed pkg-config-1.5.6 Parsing documentation for pkg-config-1.5.6 Done installing documentation for pkg-config after 0 seconds 1 gem installed ``` Here is a result of test with our RDoc. We can see that the document is created correctlly with `Parsing...` and `Done installing...`. ``` ❯ ruby -I rubygems/lib rubygems/exe/gem install pkg-config Successfully installed pkg-config-1.5.6 Parsing documentation for pkg-config-1.5.6 Done installing documentation for pkg-config after 0 seconds 1 gem installed ``` As you can see we got the same results, our RDoc keeps compatibility. --- lib/rdoc/rubygems_hook.rb | 255 ++---------------------- lib/rubygems_plugin.rb | 277 +++++++++++++++++++++++++++ rdoc.gemspec | 1 + test/rdoc/test_rdoc_rubygems_hook.rb | 10 +- 4 files changed, 295 insertions(+), 248 deletions(-) create mode 100644 lib/rubygems_plugin.rb diff --git a/lib/rdoc/rubygems_hook.rb b/lib/rdoc/rubygems_hook.rb index 3160072e53..76ec328620 100644 --- a/lib/rdoc/rubygems_hook.rb +++ b/lib/rdoc/rubygems_hook.rb @@ -1,248 +1,17 @@ -# frozen_string_literal: true -require 'rubygems/user_interaction' -require 'fileutils' -require_relative '../rdoc' - -## -# Gem::RDoc provides methods to generate RDoc and ri data for installed gems -# upon gem installation. # -# This file is automatically required by RubyGems 1.9 and newer. - -class RDoc::RubygemsHook - - include Gem::UserInteraction - extend Gem::UserInteraction - - @rdoc_version = nil - @specs = [] - - ## - # Force installation of documentation? - - attr_accessor :force - - ## - # Generate rdoc? - - attr_accessor :generate_rdoc - - ## - # Generate ri data? - - attr_accessor :generate_ri - - class << self - - ## - # Loaded version of RDoc. Set by ::load_rdoc - - attr_reader :rdoc_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_rdoc = types.include? 'rdoc' - generate_ri = types.include? 'ri' - - specs.each do |spec| - new(spec, generate_rdoc, generate_ri).generate - end - - return unless generate_rdoc or generate_ri - - duration = (Time.now - start).to_i - names = specs.map(&:name).join ', ' - - say "Done installing documentation for #{names} after #{duration} seconds" - end - - ## - # Loads the RDoc generator - - def self.load_rdoc - return if @rdoc_version - - require_relative 'rdoc' - - @rdoc_version = Gem::Version.new ::RDoc::VERSION - end - - ## - # Creates a new documentation generator for +spec+. RDoc and ri data - # generation can be enabled or disabled through +generate_rdoc+ and - # +generate_ri+ respectively. - # - # Only +generate_ri+ is enabled by default. - - def initialize spec, generate_rdoc = false, generate_ri = true - @doc_dir = spec.doc_dir - @force = false - @rdoc = nil - @spec = spec - - @generate_rdoc = generate_rdoc - @generate_ri = generate_ri - - @rdoc_dir = spec.doc_dir 'rdoc' - @ri_dir = spec.doc_dir 'ri' - end - - ## - # Removes legacy rdoc arguments from +args+ - #-- - # TODO move to RDoc::Options - - def delete_legacy_args args - args.delete '--inline-source' - args.delete '--promiscuous' - args.delete '-p' - args.delete '--one-file' - end - - ## - # Generates documentation using the named +generator+ ("darkfish" or "ri") - # and following the given +options+. - # - # Documentation will be generated into +destination+ - - def document generator, options, destination - generator_name = generator - - options = options.dup - options.exclude ||= [] # TODO maybe move to RDoc::Options#finish - options.setup_generator generator - options.op_dir = destination - Dir.chdir @spec.full_gem_path do - options.finish - end - - generator = options.generator.new @rdoc.store, options - - @rdoc.options = options - @rdoc.generator = generator - - say "Installing #{generator_name} documentation for #{@spec.full_name}" - - FileUtils.mkdir_p options.op_dir - - Dir.chdir options.op_dir do - begin - @rdoc.class.current = @rdoc - @rdoc.generator.generate - ensure - @rdoc.class.current = nil - end - end - end - - ## - # Generates RDoc and ri data - - def generate - return if @spec.default_gem? - return unless @generate_ri or @generate_rdoc - - setup - - options = nil - - args = @spec.rdoc_options - args.concat @spec.source_paths - args.concat @spec.extra_rdoc_files - - case config_args = Gem.configuration[:rdoc] - when String then - args = args.concat config_args.split(' ') - when Array then - args = args.concat config_args - end - - delete_legacy_args args - - Dir.chdir @spec.full_gem_path do - options = ::RDoc::Options.new - options.default_title = "#{@spec.full_name} Documentation" - options.parse args - end - - options.quiet = !Gem.configuration.really_verbose - - @rdoc = new_rdoc - @rdoc.options = options - - store = RDoc::Store.new - store.encoding = options.encoding - store.dry_run = options.dry_run - store.main = options.main_page - store.title = options.title - - @rdoc.store = store - - say "Parsing documentation for #{@spec.full_name}" - - Dir.chdir @spec.full_gem_path do - @rdoc.parse_files options.files - end - - document 'ri', options, @ri_dir if - @generate_ri and (@force or not File.exist? @ri_dir) - - document 'darkfish', options, @rdoc_dir if - @generate_rdoc and (@force or not File.exist? @rdoc_dir) - end - - ## - # #new_rdoc creates a new RDoc instance. This method is provided only to - # make testing easier. - - def new_rdoc # :nodoc: - ::RDoc::RDoc.new - end - - ## - # Is rdoc documentation installed? - - def rdoc_installed? - File.exist? @rdoc_dir - end - - ## - # Removes generated RDoc and ri data - - def remove - base_dir = @spec.base_dir - - raise Gem::FilePermissionError, base_dir unless File.writable? base_dir - - FileUtils.rm_rf @rdoc_dir - FileUtils.rm_rf @ri_dir - end - - ## - # Is ri data installed? - - def ri_installed? - File.exist? @ri_dir - end - - ## - # Prepares the spec for documentation generation - - def setup - self.class.load_rdoc +# This class is referenced by RubyGems to create documents. +# Now, methods are moved to rubygems_plugin.rb. +# +# When old version RDoc is not used, +# this class is not used from RubyGems too. +# Then, remove this class. +# +module RDoc + class RubygemsHook + def initialize(spec); end - raise Gem::FilePermissionError, @doc_dir if - File.exist?(@doc_dir) and not File.writable?(@doc_dir) + def remove; end - FileUtils.mkdir_p @doc_dir unless File.exist? @doc_dir + def self.generation_hook installer, specs; end end - end diff --git a/lib/rubygems_plugin.rb b/lib/rubygems_plugin.rb new file mode 100644 index 0000000000..e2a4af767d --- /dev/null +++ b/lib/rubygems_plugin.rb @@ -0,0 +1,277 @@ +# frozen_string_literal: true + +require 'rubygems/user_interaction' +require 'fileutils' + +require_relative 'rdoc' + +## +# Gem::RDoc provides methods to generate RDoc and ri data for installed gems +# upon gem installation. +# +# This file is automatically required by RubyGems 1.9 and newer. +# +# Difference between RubygemsHook and RubyGemsHook. +# - RubygemsHook is executed from RubyGems. +# - RubyGemsHook is executed from rubygems_plugin. +# +# The reason why we use two classes. +# If there are classes named 'RubygemsHook', +# conflicts of name may happen. +# +# We can add methods to existing RubygemsHook, +# but we didn't adopt it because of less mentenability and create new class 'RubyGemsHook'. +# +class RDoc::RubyGemsHook + + include Gem::UserInteraction + extend Gem::UserInteraction + + @rdoc_version = nil + @specs = [] + + ## + # Force installation of documentation? + + attr_accessor :force + + ## + # Generate rdoc? + + attr_accessor :generate_rdoc + + ## + # Generate ri data? + + attr_accessor :generate_ri + + class << self + + ## + # Loaded version of RDoc. Set by ::load_rdoc + + attr_reader :rdoc_version + + end + + ## + # Post installs hook that generates documentation for each specification in + # +specs+ + + def self.generate installer, specs + start = Time.now + types = installer.document + + generate_rdoc = types.include? 'rdoc' + generate_ri = types.include? 'ri' + + specs.each do |spec| + new(spec, generate_rdoc, generate_ri).generate + end + + return unless generate_rdoc or generate_ri + + duration = (Time.now - start).to_i + names = specs.map(&:name).join ', ' + + say "Done installing documentation for #{names} after #{duration} seconds" + end + + def self.remove uninstaller + new(uninstaller.spec).remove + end + + ## + # Loads the RDoc generator + + def self.load_rdoc + return if @rdoc_version + + require_relative 'rdoc' + + @rdoc_version = Gem::Version.new ::RDoc::VERSION + end + + ## + # Creates a new documentation generator for +spec+. RDoc and ri data + # generation can be enabled or disabled through +generate_rdoc+ and + # +generate_ri+ respectively. + # + # Only +generate_ri+ is enabled by default. + + def initialize spec, generate_rdoc = false, generate_ri = true + @doc_dir = spec.doc_dir + @force = false + @rdoc = nil + @spec = spec + + @generate_rdoc = generate_rdoc + @generate_ri = generate_ri + + @rdoc_dir = spec.doc_dir 'rdoc' + @ri_dir = spec.doc_dir 'ri' + end + + ## + # Removes legacy rdoc arguments from +args+ + #-- + # TODO move to RDoc::Options + + def delete_legacy_args args + args.delete '--inline-source' + args.delete '--promiscuous' + args.delete '-p' + args.delete '--one-file' + end + + ## + # Generates documentation using the named +generator+ ("darkfish" or "ri") + # and following the given +options+. + # + # Documentation will be generated into +destination+ + + def document generator, options, destination + generator_name = generator + + options = options.dup + options.exclude ||= [] # TODO maybe move to RDoc::Options#finish + options.setup_generator generator + options.op_dir = destination + Dir.chdir @spec.full_gem_path do + options.finish + end + + generator = options.generator.new @rdoc.store, options + + @rdoc.options = options + @rdoc.generator = generator + + say "Installing #{generator_name} documentation for #{@spec.full_name}" + + FileUtils.mkdir_p options.op_dir + + Dir.chdir options.op_dir do + begin + @rdoc.class.current = @rdoc + @rdoc.generator.generate + ensure + @rdoc.class.current = nil + end + end + end + + ## + # Generates RDoc and ri data + + def generate + return if @spec.default_gem? + return unless @generate_ri or @generate_rdoc + + setup + + options = nil + + args = @spec.rdoc_options + args.concat @spec.source_paths + args.concat @spec.extra_rdoc_files + + case config_args = Gem.configuration[:rdoc] + when String then + args = args.concat config_args.split(' ') + when Array then + args = args.concat config_args + end + + delete_legacy_args args + + Dir.chdir @spec.full_gem_path do + options = ::RDoc::Options.new + options.default_title = "#{@spec.full_name} Documentation" + options.parse args + end + + options.quiet = !Gem.configuration.really_verbose + + @rdoc = new_rdoc + @rdoc.options = options + + store = RDoc::Store.new + store.encoding = options.encoding + store.dry_run = options.dry_run + store.main = options.main_page + store.title = options.title + + @rdoc.store = store + + say "Parsing documentation for #{@spec.full_name}" + + Dir.chdir @spec.full_gem_path do + @rdoc.parse_files options.files + end + + document 'ri', options, @ri_dir if + @generate_ri and (@force or not File.exist? @ri_dir) + + document 'darkfish', options, @rdoc_dir if + @generate_rdoc and (@force or not File.exist? @rdoc_dir) + end + + ## + # #new_rdoc creates a new RDoc instance. This method is provided only to + # make testing easier. + + def new_rdoc # :nodoc: + ::RDoc::RDoc.new + end + + ## + # Is rdoc documentation installed? + + def rdoc_installed? + File.exist? @rdoc_dir + end + + ## + # Removes generated RDoc and ri data + + def remove + base_dir = @spec.base_dir + + raise Gem::FilePermissionError, base_dir unless File.writable? base_dir + + FileUtils.rm_rf @rdoc_dir + FileUtils.rm_rf @ri_dir + end + + ## + # Is ri data installed? + + def ri_installed? + File.exist? @ri_dir + end + + ## + # Prepares the spec for documentation generation + + def setup + self.class.load_rdoc + + raise Gem::FilePermissionError, @doc_dir if + File.exist?(@doc_dir) and not File.writable?(@doc_dir) + + FileUtils.mkdir_p @doc_dir unless File.exist? @doc_dir + end + +end + +# To install dependency libraries of RDoc, you need to run bundle install. +# At that time, rdoc/markdown is not generated. +# If generate and remove are executed at that time, an error will occur. +# So, we can't register generate and remove to Gem at that time. +begin + require_relative 'rdoc/markdown' +rescue LoadError +else + Gem.done_installing(&RDoc::RubyGemsHook.method(:generate)) + Gem.pre_uninstall(&RDoc::RubyGemsHook.method(:remove)) +end diff --git a/rdoc.gemspec b/rdoc.gemspec index 93a281c8ae..a4a6e26543 100644 --- a/rdoc.gemspec +++ b/rdoc.gemspec @@ -222,6 +222,7 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat "lib/rdoc/tom_doc.rb", "lib/rdoc/top_level.rb", "lib/rdoc/version.rb", + "lib/rubygems_plugin.rb", "man/ri.1", ] # files from .gitignore diff --git a/test/rdoc/test_rdoc_rubygems_hook.rb b/test/rdoc/test_rdoc_rubygems_hook.rb index 59a7ed0f89..d45afc8d8a 100644 --- a/test/rdoc/test_rdoc_rubygems_hook.rb +++ b/test/rdoc/test_rdoc_rubygems_hook.rb @@ -2,10 +2,10 @@ require 'rubygems' require 'fileutils' require 'tmpdir' -require_relative '../../lib/rdoc/rubygems_hook' +require_relative '../../lib/rubygems_plugin' require 'test/unit' -class TestRDocRubygemsHook < Test::Unit::TestCase +class TestRDocRubyGemsHook < Test::Unit::TestCase def setup @a = Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -40,10 +40,10 @@ def setup FileUtils.touch File.join(@tempdir, 'a-2', 'lib', 'a.rb') FileUtils.touch File.join(@tempdir, 'a-2', 'README') - @hook = RDoc::RubygemsHook.new @a + @hook = RDoc::RubyGemsHook.new @a begin - RDoc::RubygemsHook.load_rdoc + RDoc::RubyGemsHook.load_rdoc rescue Gem::DocumentError => e omit e.message end @@ -63,7 +63,7 @@ def test_initialize refute @hook.generate_rdoc assert @hook.generate_ri - rdoc = RDoc::RubygemsHook.new @a, false, false + rdoc = RDoc::RubyGemsHook.new @a, false, false refute rdoc.generate_rdoc refute rdoc.generate_ri From 70a144bf3fb8f2cc653972e858b5fed3747765d7 Mon Sep 17 00:00:00 2001 From: "masaru.terada" Date: Fri, 6 Sep 2024 08:56:26 +0900 Subject: [PATCH 02/14] rename a test file --- .../{test_rdoc_rubygems_hook.rb => test_rdoc_ruby_gems_hook.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/rdoc/{test_rdoc_rubygems_hook.rb => test_rdoc_ruby_gems_hook.rb} (100%) diff --git a/test/rdoc/test_rdoc_rubygems_hook.rb b/test/rdoc/test_rdoc_ruby_gems_hook.rb similarity index 100% rename from test/rdoc/test_rdoc_rubygems_hook.rb rename to test/rdoc/test_rdoc_ruby_gems_hook.rb From 4c3c6d48a60349d8ea9538397ba4d175b7035960 Mon Sep 17 00:00:00 2001 From: "masaru.terada" Date: Fri, 6 Sep 2024 08:58:13 +0900 Subject: [PATCH 03/14] Revert "rename a test file" This reverts commit 70a144bf3fb8f2cc653972e858b5fed3747765d7. --- .../{test_rdoc_ruby_gems_hook.rb => test_rdoc_rubygems_hook.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/rdoc/{test_rdoc_ruby_gems_hook.rb => test_rdoc_rubygems_hook.rb} (100%) diff --git a/test/rdoc/test_rdoc_ruby_gems_hook.rb b/test/rdoc/test_rdoc_rubygems_hook.rb similarity index 100% rename from test/rdoc/test_rdoc_ruby_gems_hook.rb rename to test/rdoc/test_rdoc_rubygems_hook.rb From af60c4b0c1d25edc8b64b37829584a2781ab1677 Mon Sep 17 00:00:00 2001 From: "masaru.terada" Date: Fri, 6 Sep 2024 09:09:36 +0900 Subject: [PATCH 04/14] revert a test class name --- test/rdoc/test_rdoc_rubygems_hook.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/rdoc/test_rdoc_rubygems_hook.rb b/test/rdoc/test_rdoc_rubygems_hook.rb index d45afc8d8a..7ed895082e 100644 --- a/test/rdoc/test_rdoc_rubygems_hook.rb +++ b/test/rdoc/test_rdoc_rubygems_hook.rb @@ -5,7 +5,7 @@ require_relative '../../lib/rubygems_plugin' require 'test/unit' -class TestRDocRubyGemsHook < Test::Unit::TestCase +class TestRDocRubygemsHook < Test::Unit::TestCase def setup @a = Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY From 1a65331f12a7db0d71815e09ffa2296e33a253ae Mon Sep 17 00:00:00 2001 From: "masaru.terada" Date: Sat, 7 Sep 2024 14:38:17 +0900 Subject: [PATCH 05/14] exclude `TestRDocRubyGemsHook` at job of ruby-core --- .github/workflows/ruby-core.yml | 2 +- test/rdoc/test_rdoc_rubygems_hook.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruby-core.yml b/.github/workflows/ruby-core.yml index 363d4acf80..386d7c19dd 100644 --- a/.github/workflows/ruby-core.yml +++ b/.github/workflows/ruby-core.yml @@ -55,5 +55,5 @@ jobs: ruby tool/sync_default_gems.rb rdoc working-directory: ruby/ruby - name: Test RDoc - run: make -j2 -s test-all TESTS="rdoc --no-retry" + run: make -j2 -s test-all TESTS="rdoc --no-retry --exclude test_rdoc_rubygems_hook.rb" working-directory: ruby/ruby diff --git a/test/rdoc/test_rdoc_rubygems_hook.rb b/test/rdoc/test_rdoc_rubygems_hook.rb index 7ed895082e..d45afc8d8a 100644 --- a/test/rdoc/test_rdoc_rubygems_hook.rb +++ b/test/rdoc/test_rdoc_rubygems_hook.rb @@ -5,7 +5,7 @@ require_relative '../../lib/rubygems_plugin' require 'test/unit' -class TestRDocRubygemsHook < Test::Unit::TestCase +class TestRDocRubyGemsHook < Test::Unit::TestCase def setup @a = Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY From 439e0fc9e68c9f20638a0b22d2550c14927853e4 Mon Sep 17 00:00:00 2001 From: "masaru.terada" Date: Sun, 8 Sep 2024 09:41:39 +0900 Subject: [PATCH 06/14] When `rubygems_plugin.rb` is not found, `test_rdoc_rubygems_hook.rb` is skipped. --- .github/workflows/ruby-core.yml | 2 +- test/rdoc/test_rdoc_rubygems_hook.rb | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ruby-core.yml b/.github/workflows/ruby-core.yml index 386d7c19dd..363d4acf80 100644 --- a/.github/workflows/ruby-core.yml +++ b/.github/workflows/ruby-core.yml @@ -55,5 +55,5 @@ jobs: ruby tool/sync_default_gems.rb rdoc working-directory: ruby/ruby - name: Test RDoc - run: make -j2 -s test-all TESTS="rdoc --no-retry --exclude test_rdoc_rubygems_hook.rb" + run: make -j2 -s test-all TESTS="rdoc --no-retry" working-directory: ruby/ruby diff --git a/test/rdoc/test_rdoc_rubygems_hook.rb b/test/rdoc/test_rdoc_rubygems_hook.rb index d45afc8d8a..9e4e408794 100644 --- a/test/rdoc/test_rdoc_rubygems_hook.rb +++ b/test/rdoc/test_rdoc_rubygems_hook.rb @@ -2,9 +2,13 @@ require 'rubygems' require 'fileutils' require 'tmpdir' -require_relative '../../lib/rubygems_plugin' require 'test/unit' +begin + require_relative '../../lib/rubygems_plugin' +rescue LoadError +end + class TestRDocRubyGemsHook < Test::Unit::TestCase def setup @a = Gem::Specification.new do |s| @@ -284,4 +288,4 @@ def test_setup_unwritable end end -end +end if defined?(RDoc::RubyGemsHook) From 2764bba45fd91f436252a7a6f7ba3755fc3d06e3 Mon Sep 17 00:00:00 2001 From: "masaru.terada" Date: Sun, 8 Sep 2024 09:50:11 +0900 Subject: [PATCH 07/14] remove unnecessary whitespace --- test/rdoc/test_rdoc_rubygems_hook.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/rdoc/test_rdoc_rubygems_hook.rb b/test/rdoc/test_rdoc_rubygems_hook.rb index 9e4e408794..2b49af5770 100644 --- a/test/rdoc/test_rdoc_rubygems_hook.rb +++ b/test/rdoc/test_rdoc_rubygems_hook.rb @@ -8,7 +8,7 @@ require_relative '../../lib/rubygems_plugin' rescue LoadError end - + class TestRDocRubyGemsHook < Test::Unit::TestCase def setup @a = Gem::Specification.new do |s| From f374b1de6eeef18b37e8185a0cbacf44f04fc29b Mon Sep 17 00:00:00 2001 From: "masaru.terada" Date: Thu, 12 Sep 2024 08:37:36 +0900 Subject: [PATCH 08/14] add comment --- test/rdoc/test_rdoc_rubygems_hook.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/rdoc/test_rdoc_rubygems_hook.rb b/test/rdoc/test_rdoc_rubygems_hook.rb index 2b49af5770..5e1ee332a2 100644 --- a/test/rdoc/test_rdoc_rubygems_hook.rb +++ b/test/rdoc/test_rdoc_rubygems_hook.rb @@ -4,6 +4,9 @@ require 'tmpdir' require 'test/unit' +# This test requires lib/rubygems_plugin.rb . +# To execute this test under a ruby-core, lib/rubygems_plugin.rb should be synced to ruby/ruby. +# But I don't do so and skip this test under the situation because of avoiding ambiguity about rubygems_plugins on ruby/ruby. begin require_relative '../../lib/rubygems_plugin' rescue LoadError From e88ba9c4afb4f97a120023eb55b73c6eba9ad9d7 Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Sun, 27 Oct 2024 07:15:55 +0900 Subject: [PATCH 09/14] Add support for the case that RDoc is installed as a default gem --- lib/rdoc/rubygems_hook.rb | 297 +++++++++++++++++++++++++++++++++++++- lib/rubygems_plugin.rb | 264 +-------------------------------- 2 files changed, 291 insertions(+), 270 deletions(-) diff --git a/lib/rdoc/rubygems_hook.rb b/lib/rdoc/rubygems_hook.rb index 76ec328620..7d9b0de936 100644 --- a/lib/rdoc/rubygems_hook.rb +++ b/lib/rdoc/rubygems_hook.rb @@ -1,17 +1,300 @@ +# frozen_string_literal: true + +require 'rubygems/user_interaction' +require 'fileutils' + +require_relative '../rdoc' + +# We define the following two similar name classes in this file: # +# - RDoc::RubyGemsHook +# - RDoc::RubygemsHook +# +# RDoc::RubyGemsHook is the main class that has real logic. +# +# RDoc::RubygemsHook is a class that is only for +# compatibility. RDoc::RubygemsHook is used by RubyGems directly. We +# can remove this when all maintained RubyGems remove +# `rubygems/rdoc.rb`. + +class RDoc::RubyGemsHook + + include Gem::UserInteraction + extend Gem::UserInteraction + + @rdoc_version = nil + @specs = [] + + ## + # Force installation of documentation? + + attr_accessor :force + + ## + # Generate rdoc? + + attr_accessor :generate_rdoc + + ## + # Generate ri data? + + attr_accessor :generate_ri + + class << self + + ## + # Loaded version of RDoc. Set by ::load_rdoc + + attr_reader :rdoc_version + + end + + ## + # Post installs hook that generates documentation for each specification in + # +specs+ + + def self.generate installer, specs + start = Time.now + types = installer.document + + generate_rdoc = types.include? 'rdoc' + generate_ri = types.include? 'ri' + + specs.each do |spec| + new(spec, generate_rdoc, generate_ri).generate + end + + return unless generate_rdoc or generate_ri + + duration = (Time.now - start).to_i + names = specs.map(&:name).join ', ' + + say "Done installing documentation for #{names} after #{duration} seconds" + end + + def self.remove uninstaller + new(uninstaller.spec).remove + end + + ## + # Loads the RDoc generator + + def self.load_rdoc + return if @rdoc_version + + require_relative 'rdoc' + + @rdoc_version = Gem::Version.new ::RDoc::VERSION + end + + ## + # Creates a new documentation generator for +spec+. RDoc and ri data + # generation can be enabled or disabled through +generate_rdoc+ and + # +generate_ri+ respectively. + # + # Only +generate_ri+ is enabled by default. + + def initialize spec, generate_rdoc = false, generate_ri = true + @doc_dir = spec.doc_dir + @force = false + @rdoc = nil + @spec = spec + + @generate_rdoc = generate_rdoc + @generate_ri = generate_ri + + @rdoc_dir = spec.doc_dir 'rdoc' + @ri_dir = spec.doc_dir 'ri' + end + + ## + # Removes legacy rdoc arguments from +args+ + #-- + # TODO move to RDoc::Options + + def delete_legacy_args args + args.delete '--inline-source' + args.delete '--promiscuous' + args.delete '-p' + args.delete '--one-file' + end + + ## + # Generates documentation using the named +generator+ ("darkfish" or "ri") + # and following the given +options+. + # + # Documentation will be generated into +destination+ + + def document generator, options, destination + generator_name = generator + + options = options.dup + options.exclude ||= [] # TODO maybe move to RDoc::Options#finish + options.setup_generator generator + options.op_dir = destination + Dir.chdir @spec.full_gem_path do + options.finish + end + + generator = options.generator.new @rdoc.store, options + + @rdoc.options = options + @rdoc.generator = generator + + say "Installing #{generator_name} documentation for #{@spec.full_name}" + + FileUtils.mkdir_p options.op_dir + + Dir.chdir options.op_dir do + begin + @rdoc.class.current = @rdoc + @rdoc.generator.generate + ensure + @rdoc.class.current = nil + end + end + end + + ## + # Generates RDoc and ri data + + def generate + return if @spec.default_gem? + return unless @generate_ri or @generate_rdoc + + setup + + options = nil + + args = @spec.rdoc_options + args.concat @spec.source_paths + args.concat @spec.extra_rdoc_files + + case config_args = Gem.configuration[:rdoc] + when String then + args = args.concat config_args.split(' ') + when Array then + args = args.concat config_args + end + + delete_legacy_args args + + Dir.chdir @spec.full_gem_path do + options = ::RDoc::Options.new + options.default_title = "#{@spec.full_name} Documentation" + options.parse args + end + + options.quiet = !Gem.configuration.really_verbose + + @rdoc = new_rdoc + @rdoc.options = options + + store = RDoc::Store.new + store.encoding = options.encoding + store.dry_run = options.dry_run + store.main = options.main_page + store.title = options.title + + @rdoc.store = store + + say "Parsing documentation for #{@spec.full_name}" + + Dir.chdir @spec.full_gem_path do + @rdoc.parse_files options.files + end + + document 'ri', options, @ri_dir if + @generate_ri and (@force or not File.exist? @ri_dir) + + document 'darkfish', options, @rdoc_dir if + @generate_rdoc and (@force or not File.exist? @rdoc_dir) + end + + ## + # #new_rdoc creates a new RDoc instance. This method is provided only to + # make testing easier. + + def new_rdoc # :nodoc: + ::RDoc::RDoc.new + end + + ## + # Is rdoc documentation installed? + + def rdoc_installed? + File.exist? @rdoc_dir + end + + ## + # Removes generated RDoc and ri data + + def remove + base_dir = @spec.base_dir + + raise Gem::FilePermissionError, base_dir unless File.writable? base_dir + + FileUtils.rm_rf @rdoc_dir + FileUtils.rm_rf @ri_dir + end + + ## + # Is ri data installed? + + def ri_installed? + File.exist? @ri_dir + end + + ## + # Prepares the spec for documentation generation + + def setup + self.class.load_rdoc + + raise Gem::FilePermissionError, @doc_dir if + File.exist?(@doc_dir) and not File.writable?(@doc_dir) + + FileUtils.mkdir_p @doc_dir unless File.exist? @doc_dir + end + +end + # This class is referenced by RubyGems to create documents. -# Now, methods are moved to rubygems_plugin.rb. +# All implementations are moved to the above RubyGemsHook. +# +# This class does nothing when this RDoc is installed as a normal gem +# or a bundled gem. # -# When old version RDoc is not used, -# this class is not used from RubyGems too. -# Then, remove this class. +# This class does generate/remove documents for compatibility when +# this RDoc is installed as a default gem. # +# We can remove this when all maintained RubyGems remove +# `rubygems/rdoc.rb`. module RDoc class RubygemsHook - def initialize(spec); end + def self.default_gem? + File.exist?(File.join(__dir__, "..", "rubygems_plugin.rb")) + end + + def initialize(spec) + @spe = spec + end + + def remove + # Do nothing if this is NOT a default gem. + return unless self.class.default_gem? + + # Remove generate document for compatibility if this is a + # default gem. + RubyGemsHook.new(@spec).remove + end - def remove; end + def self.generation_hook installer, specs + # Do nothing if this is NOT a default gem. + return unless default_gem? - def self.generation_hook installer, specs; end + # Generate document for compatibility if this is a default gem. + RubyGemsHook.generation_hook(installer, specs) + end end end diff --git a/lib/rubygems_plugin.rb b/lib/rubygems_plugin.rb index e2a4af767d..72805cab58 100644 --- a/lib/rubygems_plugin.rb +++ b/lib/rubygems_plugin.rb @@ -1,268 +1,6 @@ # frozen_string_literal: true -require 'rubygems/user_interaction' -require 'fileutils' - -require_relative 'rdoc' - -## -# Gem::RDoc provides methods to generate RDoc and ri data for installed gems -# upon gem installation. -# -# This file is automatically required by RubyGems 1.9 and newer. -# -# Difference between RubygemsHook and RubyGemsHook. -# - RubygemsHook is executed from RubyGems. -# - RubyGemsHook is executed from rubygems_plugin. -# -# The reason why we use two classes. -# If there are classes named 'RubygemsHook', -# conflicts of name may happen. -# -# We can add methods to existing RubygemsHook, -# but we didn't adopt it because of less mentenability and create new class 'RubyGemsHook'. -# -class RDoc::RubyGemsHook - - include Gem::UserInteraction - extend Gem::UserInteraction - - @rdoc_version = nil - @specs = [] - - ## - # Force installation of documentation? - - attr_accessor :force - - ## - # Generate rdoc? - - attr_accessor :generate_rdoc - - ## - # Generate ri data? - - attr_accessor :generate_ri - - class << self - - ## - # Loaded version of RDoc. Set by ::load_rdoc - - attr_reader :rdoc_version - - end - - ## - # Post installs hook that generates documentation for each specification in - # +specs+ - - def self.generate installer, specs - start = Time.now - types = installer.document - - generate_rdoc = types.include? 'rdoc' - generate_ri = types.include? 'ri' - - specs.each do |spec| - new(spec, generate_rdoc, generate_ri).generate - end - - return unless generate_rdoc or generate_ri - - duration = (Time.now - start).to_i - names = specs.map(&:name).join ', ' - - say "Done installing documentation for #{names} after #{duration} seconds" - end - - def self.remove uninstaller - new(uninstaller.spec).remove - end - - ## - # Loads the RDoc generator - - def self.load_rdoc - return if @rdoc_version - - require_relative 'rdoc' - - @rdoc_version = Gem::Version.new ::RDoc::VERSION - end - - ## - # Creates a new documentation generator for +spec+. RDoc and ri data - # generation can be enabled or disabled through +generate_rdoc+ and - # +generate_ri+ respectively. - # - # Only +generate_ri+ is enabled by default. - - def initialize spec, generate_rdoc = false, generate_ri = true - @doc_dir = spec.doc_dir - @force = false - @rdoc = nil - @spec = spec - - @generate_rdoc = generate_rdoc - @generate_ri = generate_ri - - @rdoc_dir = spec.doc_dir 'rdoc' - @ri_dir = spec.doc_dir 'ri' - end - - ## - # Removes legacy rdoc arguments from +args+ - #-- - # TODO move to RDoc::Options - - def delete_legacy_args args - args.delete '--inline-source' - args.delete '--promiscuous' - args.delete '-p' - args.delete '--one-file' - end - - ## - # Generates documentation using the named +generator+ ("darkfish" or "ri") - # and following the given +options+. - # - # Documentation will be generated into +destination+ - - def document generator, options, destination - generator_name = generator - - options = options.dup - options.exclude ||= [] # TODO maybe move to RDoc::Options#finish - options.setup_generator generator - options.op_dir = destination - Dir.chdir @spec.full_gem_path do - options.finish - end - - generator = options.generator.new @rdoc.store, options - - @rdoc.options = options - @rdoc.generator = generator - - say "Installing #{generator_name} documentation for #{@spec.full_name}" - - FileUtils.mkdir_p options.op_dir - - Dir.chdir options.op_dir do - begin - @rdoc.class.current = @rdoc - @rdoc.generator.generate - ensure - @rdoc.class.current = nil - end - end - end - - ## - # Generates RDoc and ri data - - def generate - return if @spec.default_gem? - return unless @generate_ri or @generate_rdoc - - setup - - options = nil - - args = @spec.rdoc_options - args.concat @spec.source_paths - args.concat @spec.extra_rdoc_files - - case config_args = Gem.configuration[:rdoc] - when String then - args = args.concat config_args.split(' ') - when Array then - args = args.concat config_args - end - - delete_legacy_args args - - Dir.chdir @spec.full_gem_path do - options = ::RDoc::Options.new - options.default_title = "#{@spec.full_name} Documentation" - options.parse args - end - - options.quiet = !Gem.configuration.really_verbose - - @rdoc = new_rdoc - @rdoc.options = options - - store = RDoc::Store.new - store.encoding = options.encoding - store.dry_run = options.dry_run - store.main = options.main_page - store.title = options.title - - @rdoc.store = store - - say "Parsing documentation for #{@spec.full_name}" - - Dir.chdir @spec.full_gem_path do - @rdoc.parse_files options.files - end - - document 'ri', options, @ri_dir if - @generate_ri and (@force or not File.exist? @ri_dir) - - document 'darkfish', options, @rdoc_dir if - @generate_rdoc and (@force or not File.exist? @rdoc_dir) - end - - ## - # #new_rdoc creates a new RDoc instance. This method is provided only to - # make testing easier. - - def new_rdoc # :nodoc: - ::RDoc::RDoc.new - end - - ## - # Is rdoc documentation installed? - - def rdoc_installed? - File.exist? @rdoc_dir - end - - ## - # Removes generated RDoc and ri data - - def remove - base_dir = @spec.base_dir - - raise Gem::FilePermissionError, base_dir unless File.writable? base_dir - - FileUtils.rm_rf @rdoc_dir - FileUtils.rm_rf @ri_dir - end - - ## - # Is ri data installed? - - def ri_installed? - File.exist? @ri_dir - end - - ## - # Prepares the spec for documentation generation - - def setup - self.class.load_rdoc - - raise Gem::FilePermissionError, @doc_dir if - File.exist?(@doc_dir) and not File.writable?(@doc_dir) - - FileUtils.mkdir_p @doc_dir unless File.exist? @doc_dir - end - -end +require_relative 'rdoc/rubygems_hook' # To install dependency libraries of RDoc, you need to run bundle install. # At that time, rdoc/markdown is not generated. From e9e69f3f989b8cb184c1d29c3ea3fbedd7559d8f Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Thu, 7 Nov 2024 06:07:25 +0900 Subject: [PATCH 10/14] Fix problems Co-authored-by: mterada1228 <49284339+mterada1228@users.noreply.github.com> --- lib/rdoc/rubygems_hook.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/rdoc/rubygems_hook.rb b/lib/rdoc/rubygems_hook.rb index 7d9b0de936..fc3388c253 100644 --- a/lib/rdoc/rubygems_hook.rb +++ b/lib/rdoc/rubygems_hook.rb @@ -273,11 +273,11 @@ def setup module RDoc class RubygemsHook def self.default_gem? - File.exist?(File.join(__dir__, "..", "rubygems_plugin.rb")) + !File.exist?(File.join(__dir__, "..", "rubygems_plugin.rb")) end def initialize(spec) - @spe = spec + @spec = spec end def remove @@ -294,7 +294,7 @@ def self.generation_hook installer, specs return unless default_gem? # Generate document for compatibility if this is a default gem. - RubyGemsHook.generation_hook(installer, specs) + ::RDoc::RubyGemsHook.generate(installer, specs) end end end From 75c928d1608a45ca0e6e800a72c0d225beaf170e Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Thu, 7 Nov 2024 20:30:27 +0900 Subject: [PATCH 11/14] Simplify --- lib/rdoc/rubygems_hook.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rdoc/rubygems_hook.rb b/lib/rdoc/rubygems_hook.rb index fc3388c253..4e80edaf10 100644 --- a/lib/rdoc/rubygems_hook.rb +++ b/lib/rdoc/rubygems_hook.rb @@ -294,7 +294,7 @@ def self.generation_hook installer, specs return unless default_gem? # Generate document for compatibility if this is a default gem. - ::RDoc::RubyGemsHook.generate(installer, specs) + RubyGemsHook.generate(installer, specs) end end end From 182bcf94c3ad57aa467f49fa523a61ede8167d26 Mon Sep 17 00:00:00 2001 From: "masaru.terada" Date: Sat, 9 Nov 2024 11:34:47 +0900 Subject: [PATCH 12/14] removed unused blank lines and revert test --- lib/rdoc/rubygems_hook.rb | 4 +--- test/rdoc/test_rdoc_rubygems_hook.rb | 11 ++--------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/rdoc/rubygems_hook.rb b/lib/rdoc/rubygems_hook.rb index 4e80edaf10..95d33d7a8a 100644 --- a/lib/rdoc/rubygems_hook.rb +++ b/lib/rdoc/rubygems_hook.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true - require 'rubygems/user_interaction' require 'fileutils' - require_relative '../rdoc' # We define the following two similar name classes in this file: @@ -284,7 +282,7 @@ def remove # Do nothing if this is NOT a default gem. return unless self.class.default_gem? - # Remove generate document for compatibility if this is a + # Remove generated document for compatibility if this is a # default gem. RubyGemsHook.new(@spec).remove end diff --git a/test/rdoc/test_rdoc_rubygems_hook.rb b/test/rdoc/test_rdoc_rubygems_hook.rb index 5e1ee332a2..849a7e9c57 100644 --- a/test/rdoc/test_rdoc_rubygems_hook.rb +++ b/test/rdoc/test_rdoc_rubygems_hook.rb @@ -2,16 +2,9 @@ require 'rubygems' require 'fileutils' require 'tmpdir' +require_relative '../../lib/rdoc/rubygems_hook' require 'test/unit' -# This test requires lib/rubygems_plugin.rb . -# To execute this test under a ruby-core, lib/rubygems_plugin.rb should be synced to ruby/ruby. -# But I don't do so and skip this test under the situation because of avoiding ambiguity about rubygems_plugins on ruby/ruby. -begin - require_relative '../../lib/rubygems_plugin' -rescue LoadError -end - class TestRDocRubyGemsHook < Test::Unit::TestCase def setup @a = Gem::Specification.new do |s| @@ -291,4 +284,4 @@ def test_setup_unwritable end end -end if defined?(RDoc::RubyGemsHook) +end From 535c5e34835dea7a7247348a579a82400dc85674 Mon Sep 17 00:00:00 2001 From: "masaru.terada" Date: Sat, 9 Nov 2024 11:44:15 +0900 Subject: [PATCH 13/14] for rerun tests From 42b86d1251f20a56f9540dbece06c1d1b711a802 Mon Sep 17 00:00:00 2001 From: "masaru.terada" Date: Thu, 14 Nov 2024 09:14:22 +0900 Subject: [PATCH 14/14] add comment for rubygems_plugin.rb --- lib/rubygems_plugin.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/rubygems_plugin.rb b/lib/rubygems_plugin.rb index 72805cab58..55c75da6ba 100644 --- a/lib/rubygems_plugin.rb +++ b/lib/rubygems_plugin.rb @@ -1,5 +1,13 @@ # frozen_string_literal: true +# If this file is exist, RDoc generates and removes documents by rubygems plugins. +# +# In follwing cases, +# RubyGems directly exectute RDoc::RubygemsHook.generation_hook and RDoc::RubygemsHook#remove to generate and remove documents. +# +# - RDoc is used as a default gem. +# - RDoc is a old version that doesn't have rubygems_plugin.rb. + require_relative 'rdoc/rubygems_hook' # To install dependency libraries of RDoc, you need to run bundle install.