doc 0.0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.markdown +80 -1
- data/Rakefile +6 -2
- data/VERSION +1 -1
- data/bin/docr +43 -0
- data/doc.gemspec +42 -9
- data/lib/doc.rb +11 -0
- data/lib/doc/base_task.rb +76 -0
- data/lib/doc/builder.rb +101 -0
- data/lib/doc/command.rb +40 -0
- data/lib/doc/config_error.rb +11 -0
- data/lib/doc/config_object.rb +80 -0
- data/lib/doc/configurator.rb +38 -0
- data/lib/doc/configurator/gems.rb +70 -0
- data/lib/doc/configurator/paths.rb +90 -0
- data/lib/doc/configurator/rails.rb +98 -0
- data/lib/doc/configurator/ruby.rb +167 -0
- data/lib/doc/configurator/ruby/path_info.rb +38 -0
- data/lib/doc/configurator/ruby/source.rb +216 -0
- data/lib/doc/configurator/ruby/stdlib.rb +32 -0
- data/lib/doc/configurator/ruby/version_specifier.rb +39 -0
- data/lib/doc/core_ext.rb +75 -0
- data/lib/doc/documentor.rb +62 -0
- data/lib/doc/merger.rb +40 -0
- data/lib/doc/root_config.rb +29 -0
- data/lib/doc/root_merger.rb +14 -0
- data/lib/doc/tasks.rb +52 -0
- metadata +93 -17
@@ -0,0 +1,38 @@
|
|
1
|
+
module Doc
|
2
|
+
class Configurator
|
3
|
+
class Ruby
|
4
|
+
class PathInfo < Struct.new(:path, :name, :type, :full_version, :parts)
|
5
|
+
private_class_method :new
|
6
|
+
def self.latest_matching(version, paths)
|
7
|
+
paths.map(&method(:for_path)).compact.grep(version).sort.last
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.for_path(path)
|
11
|
+
name = path.basename.to_s
|
12
|
+
if name =~ /^ruby-(\d+\.\d+\.\d+-p\d+)(?i:\.(tar\.(?:gz|bz2)|tgz|tbz|zip))?$/
|
13
|
+
extension = $2 ? $2.downcase : :dir
|
14
|
+
type = ({'tar.bz2' => 'tbz', 'tar.gz' => 'tgz'}[extension] || extension).to_sym
|
15
|
+
new(path, name, type, $1, $1.scan(/\d+/).map(&:to_i))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def type_priority
|
20
|
+
@type_priority ||= {:zip => 0, :tgz => 1, :tbz => 2, :dir => 3}[type]
|
21
|
+
end
|
22
|
+
|
23
|
+
def sort_by
|
24
|
+
@sort_by ||= [parts, type_priority]
|
25
|
+
end
|
26
|
+
|
27
|
+
include Comparable
|
28
|
+
def <=>(other)
|
29
|
+
sort_by <=> other.sort_by
|
30
|
+
end
|
31
|
+
|
32
|
+
def ===(other)
|
33
|
+
parts === other.parts
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
require 'net/ftp'
|
2
|
+
require 'net/ftp/list'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'tmpdir'
|
5
|
+
require 'timeout'
|
6
|
+
|
7
|
+
module Doc
|
8
|
+
class Configurator
|
9
|
+
class Ruby
|
10
|
+
module Source
|
11
|
+
def by_binary(binary, update)
|
12
|
+
binary = (binary || 'ruby').to_s
|
13
|
+
version = VersionSpecifier.new(`#{binary} -e 'print "\#{RUBY_VERSION}-p\#{RUBY_PATCHLEVEL}"'`)
|
14
|
+
if $?.success?
|
15
|
+
if version.valid?
|
16
|
+
by_version(version, update)
|
17
|
+
else
|
18
|
+
raise "invalid version from `#{binary}`: #{version.to_s.inspect}"
|
19
|
+
end
|
20
|
+
else
|
21
|
+
raise "failed to get version from `#{binary}`"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def by_version(version, update)
|
26
|
+
version = VersionSpecifier.new(version)
|
27
|
+
if version.valid?
|
28
|
+
if (update && !version.full_version?) || !path_for_version(version)
|
29
|
+
download_version(version)
|
30
|
+
end
|
31
|
+
if path = path_for_version(version)
|
32
|
+
path.directory? ? from_dir(path) : from_archive(path)
|
33
|
+
else
|
34
|
+
raise "can't get ruby #{version}"
|
35
|
+
end
|
36
|
+
else
|
37
|
+
raise "version should be in format X.Y, X.Y.Z or X.Y.Z-pPPP, download archive if you need release candidate or other version"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def from_archive(path)
|
42
|
+
path = FSPath(path)
|
43
|
+
if path.file?
|
44
|
+
if path.basename.to_s =~ /^(.*)(?i:\.(tar\.(?:gz|bz2)|tgz|tbz|zip))$/
|
45
|
+
dir, extension = sources_dir / $1, $2.downcase
|
46
|
+
unless dir.exist?
|
47
|
+
Dir.mktmpdir 'ruby' do |d|
|
48
|
+
begin
|
49
|
+
case extension
|
50
|
+
when 'tbz', 'tar.bz2'
|
51
|
+
Command.run('tar', '-xjf', path, '-C', d)
|
52
|
+
when 'tgz', 'tar.gz'
|
53
|
+
Command.run('tar', '-xzf', path, '-C', d)
|
54
|
+
when 'zip'
|
55
|
+
Command.run('unzip', '-q', path, '-d', d)
|
56
|
+
end
|
57
|
+
|
58
|
+
children = FSPath(d).children
|
59
|
+
if children.length == 1
|
60
|
+
children.first.rename(dir)
|
61
|
+
else
|
62
|
+
dir.mkpath
|
63
|
+
FileUtils.mv children, dir
|
64
|
+
end
|
65
|
+
rescue SystemExit => e
|
66
|
+
raise "got #{e} trying to extract archive"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
from_dir(dir)
|
71
|
+
else
|
72
|
+
raise "#{path} doesn't seem to be archive of known type"
|
73
|
+
end
|
74
|
+
else
|
75
|
+
raise "#{path} is not a file"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def from_dir(path)
|
80
|
+
path = FSPath(path)
|
81
|
+
if path.directory?
|
82
|
+
version_path = path / 'version.h'
|
83
|
+
if version_path.file? && version_path.read['RUBY_VERSION']
|
84
|
+
dot_document_path = path / '.document'
|
85
|
+
if dot_document_path.size?
|
86
|
+
path.expand_path
|
87
|
+
else
|
88
|
+
raise "#{path} doesn't contain .document file or it is empty"
|
89
|
+
end
|
90
|
+
else
|
91
|
+
raise "#{path} doesn't contain version.h file or it has no RUBY_VERSION in it"
|
92
|
+
end
|
93
|
+
else
|
94
|
+
raise "#{path} is not a directory"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def path_for_version(version)
|
101
|
+
if path_info = PathInfo.latest_matching(version, sources_dir.children)
|
102
|
+
path_info.path
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def tempfile_for(dst)
|
107
|
+
Tempfile.open 'ruby' do |f|
|
108
|
+
path = FSPath(f.path)
|
109
|
+
yield path
|
110
|
+
path.rename(dst)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def latest_version_from_tag_list(command, regexp, version)
|
115
|
+
IO.popen(command, &:readlines).map do |line|
|
116
|
+
if line.strip =~ regexp
|
117
|
+
VersionSpecifier.new($1)
|
118
|
+
end
|
119
|
+
end.compact.sort.grep(version).last
|
120
|
+
end
|
121
|
+
|
122
|
+
def tmpdir_for_latest_version_from_tag_list(command, regexp, version)
|
123
|
+
if tag_version = latest_version_from_tag_list(command, regexp, version)
|
124
|
+
unless path_for_version(tag_version)
|
125
|
+
Dir.mktmpdir 'ruby' do |d|
|
126
|
+
tmp_dir = FSPath(d) / tag_version.dir_name
|
127
|
+
if yield(tmp_dir, tag_version)
|
128
|
+
tmp_dir.rename(sources_dir / tag_version.dir_name)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
path_for_version(tag_version)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def download_version(version)
|
137
|
+
download_version_via(:git_pull, version) || download_version_via(:git_tarball, version)
|
138
|
+
# git doesn't always have latest version so fast download from there, but verify latest from ftp/svn
|
139
|
+
download_version_via(:ftp, version) || download_version_via(:svn, version)
|
140
|
+
end
|
141
|
+
|
142
|
+
def download_version_via(type, version)
|
143
|
+
$stderr.puts "Checking/downloading ruby #{version} via #{type}"
|
144
|
+
Timeout.timeout(90) do
|
145
|
+
send("download_version_via_#{type}", version)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
FTP_HOST = 'ftp.ruby-lang.org'
|
150
|
+
SVN_TAGS_URL = 'http://svn.ruby-lang.org/repos/ruby/tags/'
|
151
|
+
SVN_TAG_LIST_COMMAND = "svn list --non-interactive #{SVN_TAGS_URL}"
|
152
|
+
SVN_TAG_REGEXP = /^(v\d+(?:_\d+){3})\/$/
|
153
|
+
GIT_BARE_URL = 'github.com/ruby/ruby'
|
154
|
+
GIT_URL = "git://#{GIT_BARE_URL}.git"
|
155
|
+
GIT_TAG_LIST_COMMAND = "git ls-remote -t #{GIT_URL}"
|
156
|
+
GIT_TAG_REGEXP = /^.*\t(refs\/tags\/v\d+(?:_\d+){3})$/
|
157
|
+
|
158
|
+
def download_version_via_ftp(version)
|
159
|
+
Net::FTP.open(FTP_HOST) do |ftp|
|
160
|
+
ftp.passive = true
|
161
|
+
ftp.login
|
162
|
+
|
163
|
+
ftp_dir = FSPath('/pub/ruby') / version.parts[0, 2].join('.')
|
164
|
+
entries = ftp.list(ftp_dir.to_s).map{ |e| Net::FTP::List.parse(e) }
|
165
|
+
if archive_info = PathInfo.latest_matching(version, entries.select(&:file?))
|
166
|
+
unless path_for_version(archive_info)
|
167
|
+
entry = archive_info.path
|
168
|
+
tempfile_for(sources_dir / entry.basename) do |f|
|
169
|
+
ftp.getbinaryfile(ftp_dir / entry.basename, f)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
path_for_version(archive_info)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def download_version_via_svn(version)
|
178
|
+
tmpdir_for_latest_version_from_tag_list(SVN_TAG_LIST_COMMAND, SVN_TAG_REGEXP, version) do |tmp_dir, tag_version|
|
179
|
+
Command.run *%W[svn export -q --non-interactive #{SVN_TAGS_URL}#{tag_version}/ #{tmp_dir}]
|
180
|
+
$?.success?
|
181
|
+
end
|
182
|
+
rescue SystemExit
|
183
|
+
end
|
184
|
+
|
185
|
+
def download_version_via_git_pull(version)
|
186
|
+
tmpdir_for_latest_version_from_tag_list(GIT_TAG_LIST_COMMAND, GIT_TAG_REGEXP, version) do |tmp_dir, tag_version|
|
187
|
+
tmp_dir.mkpath
|
188
|
+
Dir.chdir(tmp_dir) do
|
189
|
+
Command.run 'git init -q'
|
190
|
+
Command.run *%W[git pull -q --depth=1 #{GIT_URL} #{tag_version}]
|
191
|
+
(tmp_dir / '.git').rmtree
|
192
|
+
end
|
193
|
+
$?.success?
|
194
|
+
end
|
195
|
+
rescue SystemExit
|
196
|
+
end
|
197
|
+
|
198
|
+
def download_version_via_git_tarball(version)
|
199
|
+
if tag_version = latest_version_from_tag_list(GIT_TAG_LIST_COMMAND, GIT_TAG_REGEXP, version)
|
200
|
+
unless path_for_version(tag_version)
|
201
|
+
tempfile_for(sources_dir / "#{tag_version.dir_name}.tgz") do |f|
|
202
|
+
Command.run *%W[
|
203
|
+
curl -L -s
|
204
|
+
http://#{GIT_BARE_URL}/tarball/#{tag_version.to_s.split('/').last}
|
205
|
+
-o #{f}
|
206
|
+
]
|
207
|
+
end
|
208
|
+
end
|
209
|
+
path_for_version(tag_version)
|
210
|
+
end
|
211
|
+
rescue SystemExit
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Doc
|
6
|
+
class Configurator
|
7
|
+
class Ruby
|
8
|
+
module Stdlib
|
9
|
+
STDLIB_CONFIG_URL = 'http://stdlib-doc.rubyforge.org/svn/trunk/data/gendoc.yaml'
|
10
|
+
|
11
|
+
def stdlib_config(update)
|
12
|
+
if update || !read_stdlib_config
|
13
|
+
download_stdlib_config
|
14
|
+
end
|
15
|
+
read_stdlib_config
|
16
|
+
end
|
17
|
+
|
18
|
+
def stdlib_config_path
|
19
|
+
sources_dir / 'stdlib-config.yaml'
|
20
|
+
end
|
21
|
+
|
22
|
+
def read_stdlib_config
|
23
|
+
YAML.load_file stdlib_config_path if stdlib_config_path.readable?
|
24
|
+
end
|
25
|
+
|
26
|
+
def download_stdlib_config
|
27
|
+
stdlib_config_path.write(Net::HTTP.get(URI.parse(STDLIB_CONFIG_URL)))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Doc
|
2
|
+
class Configurator
|
3
|
+
class Ruby
|
4
|
+
class VersionSpecifier
|
5
|
+
attr_reader :str, :parts
|
6
|
+
alias_method :to_s, :str
|
7
|
+
def initialize(o)
|
8
|
+
@str = o.to_s
|
9
|
+
@parts = str.scan(/\d+/).map(&:to_i)
|
10
|
+
end
|
11
|
+
|
12
|
+
def valid?
|
13
|
+
str =~ /^\d+\.\d+(?:\.\d+(?:-p\d+)?)?$/
|
14
|
+
end
|
15
|
+
|
16
|
+
def full_version?
|
17
|
+
valid? && parts.length == 4
|
18
|
+
end
|
19
|
+
|
20
|
+
def dir_name
|
21
|
+
'ruby-%d.%d.%d-p%d' % parts
|
22
|
+
end
|
23
|
+
|
24
|
+
include Comparable
|
25
|
+
def <=>(other)
|
26
|
+
parts <=> other.parts
|
27
|
+
end
|
28
|
+
|
29
|
+
def ===(other)
|
30
|
+
if other.respond_to?(:parts)
|
31
|
+
parts == other.parts[0, parts.length]
|
32
|
+
else
|
33
|
+
str === other
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/doc/core_ext.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'fspath'
|
2
|
+
|
3
|
+
def glob_require(*globs)
|
4
|
+
globs.each do |glob|
|
5
|
+
$LOAD_PATH.each do |load_dir|
|
6
|
+
FSPath.glob(File.join(load_dir, glob)).each do |path|
|
7
|
+
require path.relative_path_from(FSPath(load_dir)).to_s
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Integer
|
14
|
+
def minutes
|
15
|
+
60 * self
|
16
|
+
end
|
17
|
+
alias_method :minute, :minutes
|
18
|
+
|
19
|
+
def hours
|
20
|
+
60 * minutes
|
21
|
+
end
|
22
|
+
alias_method :hour, :hours
|
23
|
+
|
24
|
+
def days
|
25
|
+
24 * hours
|
26
|
+
end
|
27
|
+
alias_method :day, :days
|
28
|
+
|
29
|
+
def weeks
|
30
|
+
7 * days
|
31
|
+
end
|
32
|
+
alias_method :week, :weeks
|
33
|
+
end
|
34
|
+
|
35
|
+
class String
|
36
|
+
def underscore
|
37
|
+
split(/::/).map{ |part| part.split(/(?=[A-Z])/).join('_') }.join('/').downcase
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Module
|
42
|
+
def smart_autoload(*names)
|
43
|
+
names.each do |name|
|
44
|
+
autoload name, "#{self}::#{name}".underscore
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def abstract_method(*names)
|
49
|
+
names.each do |name|
|
50
|
+
class_eval <<-RUBY, __FILE__, __LINE__
|
51
|
+
def #{name}(*_)
|
52
|
+
raise NotImplementedError.new("\#{self.class.name} has no implementation for method `#{name}`")
|
53
|
+
end
|
54
|
+
RUBY
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Array
|
60
|
+
def select!(&block)
|
61
|
+
replace(select(&block))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class FSPath
|
66
|
+
def touch(atime = nil, mtime = nil)
|
67
|
+
open('w'){} unless exist?
|
68
|
+
utime(atime ||= Time.now, mtime || atime)
|
69
|
+
end
|
70
|
+
|
71
|
+
def rmtree_verbose
|
72
|
+
require 'fileutils'
|
73
|
+
FileUtils.rm_r(@path, :verbose => true)
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'fspath'
|
2
|
+
require 'progress'
|
3
|
+
|
4
|
+
module Doc
|
5
|
+
class Documentor
|
6
|
+
attr_reader :title, :min_update_interval, :clean_after, :configurators
|
7
|
+
attr_reader :base_dir, :sources_dir, :docs_dir, :public_dir
|
8
|
+
def initialize(*arguments, &block)
|
9
|
+
config = RootConfig.new(self, *arguments, &block)
|
10
|
+
config.check_options!([], [:title, :min_update_interval, :clean_after, :public_dir])
|
11
|
+
|
12
|
+
@title = config[:title] || 'ruby documentation'
|
13
|
+
@min_update_interval = config[:min_update_interval] || 1.hour
|
14
|
+
@clean_after = config[:clean_after]
|
15
|
+
@configurators = config.configurators
|
16
|
+
|
17
|
+
@base_dir = FSPath('.').expand_path
|
18
|
+
@sources_dir = base_dir / 'sources'
|
19
|
+
@docs_dir = base_dir / 'docs'
|
20
|
+
if config[:public_dir]
|
21
|
+
config[:public_dir] = FSPath(config[:public_dir]).cleanpath.to_s
|
22
|
+
if config[:public_dir] == '.'
|
23
|
+
raise ConfigError.new(self, "can't use base dir: #{config[:public_dir].inspect}")
|
24
|
+
end
|
25
|
+
if config[:public_dir].split(FSPath::SEPARATOR_PAT).include?('..')
|
26
|
+
raise ConfigError.new(self, ".. in public_dir: #{config[:public_dir].inspect}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
@public_dir = base_dir / (config[:public_dir] || 'public')
|
30
|
+
end
|
31
|
+
|
32
|
+
def config(update = false)
|
33
|
+
last_updated_path = base_dir / '.last_updated'
|
34
|
+
update ||= last_updated_path.exist? ? (Time.now > last_updated_path.mtime + min_update_interval) : true
|
35
|
+
|
36
|
+
configurators.with_progress('config/update').each do |configurator|
|
37
|
+
configurator.configure(update)
|
38
|
+
end
|
39
|
+
last_updated_path.touch if update
|
40
|
+
|
41
|
+
RootMerger.new(self, {
|
42
|
+
:title => title,
|
43
|
+
:tasks => configurators.with_progress('tasks').map(&:tasks).flatten
|
44
|
+
})
|
45
|
+
end
|
46
|
+
|
47
|
+
def build(update = false)
|
48
|
+
started = Time.now
|
49
|
+
root_task = config(update)
|
50
|
+
|
51
|
+
root_task.run
|
52
|
+
|
53
|
+
if clean_after
|
54
|
+
(sources_dir.directory? ? sources_dir.children : [] + docs_dir.children).each do |dir|
|
55
|
+
if started - dir.mtime > clean_after
|
56
|
+
dir.rmtree_verbose
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|