Skip to content

Commit

Permalink
Make it loose coupling between RubyGems and RDoc
Browse files Browse the repository at this point in the history
\### 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.
  • Loading branch information
mterada1228 committed Sep 5, 2024
1 parent a576ff8 commit 9e2e1bf
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 247 deletions.
255 changes: 12 additions & 243 deletions lib/rdoc/rubygems_hook.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 9e2e1bf

Please sign in to comment.