tinygem 1.0.0
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/tinygem +3 -0
- data/tinygem.rb +293 -0
- metadata +59 -0
data/tinygem
ADDED
data/tinygem.rb
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
|
|
2
|
+
require "tmpdir"
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "ripper"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
#### Public Interface
|
|
9
|
+
|
|
10
|
+
# `TinyGem.new` takes a source `source_path` to turn into a gem. This path, of course,
|
|
11
|
+
# should specify a file that is formatted in the manner that TinyGem` expects. TinyGem
|
|
12
|
+
# is built using itself, so you can use it as an example.
|
|
13
|
+
class TinyGem
|
|
14
|
+
|
|
15
|
+
# Initialize TinyGem with a path pointing at an appropriately formatted source file.
|
|
16
|
+
def initialize(source_path)
|
|
17
|
+
@source_path = Pathname(source_path).expand_path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Given an optional `output_dir`, which defaults to the current `pwd`, generate a
|
|
21
|
+
# RubyGem out of the source that TinyGem was initialized with.
|
|
22
|
+
def package(output_dir = Pathname(Dir.pwd).expand_path)
|
|
23
|
+
# Check the source
|
|
24
|
+
check_source!
|
|
25
|
+
# Turn the source into component parts to build a gem out of
|
|
26
|
+
gem_parts = read_source_parts
|
|
27
|
+
# Write these parts to a directory
|
|
28
|
+
gem_dir = write_gem_dir(gem_parts)
|
|
29
|
+
# Build a .gem file from this directory, and leave it in the `output_dir`
|
|
30
|
+
build_package(gem_dir, output_dir)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Name the gem we're building the same thing as the filename of the code being turned
|
|
36
|
+
# into a gem, without the ".rb" extension.
|
|
37
|
+
def gem_name
|
|
38
|
+
@gem_name ||= @source_path.sub_ext("").basename.to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Perform some quick sanity checks to make sure the source path is readable, and also
|
|
42
|
+
# contains syntactically valid Ruby code. No point continuing, otherwise.
|
|
43
|
+
def check_source!
|
|
44
|
+
raise "#{@source_path} is not readable" unless @source_path.readable?
|
|
45
|
+
output = %x[ruby -c #{@source_path} 2>&1]
|
|
46
|
+
raise "#{@source_path} is not valid ruby:\n#{output}" unless $?.success?
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Take the source and chunk it into its component parts, so we can use them to build
|
|
51
|
+
# a valid RubyGem.
|
|
52
|
+
def read_source_parts
|
|
53
|
+
@source_parts ||= begin
|
|
54
|
+
chunked_source = ChunkedSource.new(@source_path.open("r"))
|
|
55
|
+
metadata_extractor = TinyGem::MetadataExtractor.new(chunked_source, 'name' => gem_name)
|
|
56
|
+
{spec: metadata_extractor.as_gemspec, library: chunked_source.library,
|
|
57
|
+
readme: chunked_source.readme, executable: metadata_extractor.executable_code}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Create a temporary directory to write the various gem parts into, so that the `gem`
|
|
62
|
+
# command can work against it to build a RubyGem.
|
|
63
|
+
def write_gem_dir(gem_parts)
|
|
64
|
+
tmp = Pathname(Dir.mktmpdir)
|
|
65
|
+
warn("Using #{tmp} as a place to build the gem")
|
|
66
|
+
# Write out the gemspec
|
|
67
|
+
(tmp + "#{gem_name}.gemspec").open("w") {|f| f << gem_parts[:spec]}
|
|
68
|
+
# Write out the readme
|
|
69
|
+
(tmp + "README.md").open("w") {|f| f << gem_parts[:readme]}
|
|
70
|
+
# Write out the library itself
|
|
71
|
+
(tmp + "#{gem_name}.rb").open("w") {|f| f << gem_parts[:library]}
|
|
72
|
+
# If a chunk of code has been specified to build a command line executable
|
|
73
|
+
# from, write that out
|
|
74
|
+
if gem_parts[:executable]
|
|
75
|
+
(tmp + gem_name).open("w") do |f|
|
|
76
|
+
# Flip the executable bit on the binary file
|
|
77
|
+
f.chmod(f.stat.mode | 0100)
|
|
78
|
+
f << "#!/usr/bin/env ruby\n"
|
|
79
|
+
f << "require #{gem_name.inspect}\n"
|
|
80
|
+
f << "#{gem_parts[:executable]}\n"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
tmp
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Build a .gem from the given gem_dir, and move it to the output_dir.
|
|
87
|
+
def build_package(gem_dir, output_dir)
|
|
88
|
+
curdir = Dir.pwd
|
|
89
|
+
Dir.chdir(gem_dir.to_s)
|
|
90
|
+
output = `gem build #{gem_name}.gemspec 2>&1`
|
|
91
|
+
raise "Couldn't build gem: #{output}" unless $?.success?
|
|
92
|
+
warn(output)
|
|
93
|
+
filename = output.match(/^\s+File: ([^\n]+)$/)[1]
|
|
94
|
+
# Move the built .gem to the output_dir, since `gem` doesn't seem to
|
|
95
|
+
# provide a way to specify the output location.
|
|
96
|
+
FileUtils.mv(filename, output_dir + filename)
|
|
97
|
+
Dir.chdir(curdir)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A `TinyGem::ChunkedSource` object takes a string or something 'IO-ish' that is
|
|
102
|
+
# then chunked into parts for use by TinyGem.
|
|
103
|
+
class TinyGem::ChunkedSource
|
|
104
|
+
# The YAML trailing document separator, `---`, is used to distinguish the separation
|
|
105
|
+
# between the YAML metadata part of the first multi-line comment in the file, and
|
|
106
|
+
# the rest of the multi-line comment that contains the README.
|
|
107
|
+
README_SEPARATOR = /\A---\s*\z/
|
|
108
|
+
|
|
109
|
+
def initialize(source_io)
|
|
110
|
+
@source_io = source_io
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def metadata; chunked[:metadata]; end
|
|
114
|
+
def readme; chunked[:readme]; end
|
|
115
|
+
def library; chunked[:library]; end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def chunked
|
|
120
|
+
return @chunked if @chunked
|
|
121
|
+
metadata, readme, library = '', '', ''
|
|
122
|
+
state = :seeking_brief
|
|
123
|
+
|
|
124
|
+
lexed_source = Ripper.lex(@source_io)
|
|
125
|
+
lexed_source.each do |((line,col),type,token)|
|
|
126
|
+
case state
|
|
127
|
+
# Start off looking for the first multiline comment
|
|
128
|
+
when :seeking_brief then
|
|
129
|
+
# Switch state to read the metadata from the brief when it's found
|
|
130
|
+
state = :read_metadata if type == :on_embdoc_beg
|
|
131
|
+
when :read_metadata then
|
|
132
|
+
metadata << token if type == :on_embdoc
|
|
133
|
+
# Slurp lines until the separator between the YAML metadata and the readme
|
|
134
|
+
# is found, then switch state
|
|
135
|
+
state = :read_readme if token =~ README_SEPARATOR
|
|
136
|
+
state = :read_library if type == :on_embdoc_end
|
|
137
|
+
when :read_readme then
|
|
138
|
+
readme << token if type == :on_embdoc
|
|
139
|
+
# Slurp readme lines until the end of the multi-line comment is found,
|
|
140
|
+
# then switch state to read the rest of the file contents in as the actual
|
|
141
|
+
# library code.
|
|
142
|
+
state = :read_library if type == :on_embdoc_end
|
|
143
|
+
when :read_library then
|
|
144
|
+
library << token
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
@chunked = {metadata: metadata, readme: readme, library: library}
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Extracts metadata from the file used to build the gemspec, amongst other things.
|
|
153
|
+
# It does this by first checking for any explicit values in the YAML metadata
|
|
154
|
+
# supplied - for any omitted values, it tries to infer them in various ways.
|
|
155
|
+
class TinyGem::MetadataExtractor
|
|
156
|
+
# The keys valid for use in the YAML metadata
|
|
157
|
+
SPEC_KEYS = %w[author email name version summary description homepage]
|
|
158
|
+
VERSION_MATCH = %r[(Version:?|v)\s* # A version string starting with Version or v
|
|
159
|
+
(\d+\.\d+\.\d+) # A int.int.int version number
|
|
160
|
+
]xi
|
|
161
|
+
HOMEPAGE_MATCH = %r[^\s* # A line starting with any or no whitespace
|
|
162
|
+
\[?Home(page)?:? # Home, Homepage:, optionally starting a Markdown link
|
|
163
|
+
\s*(\]\()? # Some more optional Markdown link formatting
|
|
164
|
+
(https?:\/\/[^\)\n]+)\)?\s* # Anything url-ish
|
|
165
|
+
]xi
|
|
166
|
+
SUMMARY_MATCH = /^.*[:alnum:]+.*$/
|
|
167
|
+
|
|
168
|
+
# Takes some already chunked source, and some default values to use if no explicit
|
|
169
|
+
# metadata is available, or none can be inferred.
|
|
170
|
+
def initialize(chunked_source, defaults = {})
|
|
171
|
+
@chunked_source = chunked_source
|
|
172
|
+
@defaults = defaults
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Spits out a gemspec.
|
|
176
|
+
def as_gemspec
|
|
177
|
+
spec_values = SPEC_KEYS.inject({}) do |keys,spec_key|
|
|
178
|
+
keys[spec_key] = metadata_hash[spec_key] || default_or_inferred_for(spec_key)
|
|
179
|
+
keys
|
|
180
|
+
end
|
|
181
|
+
gemspec_from_values(spec_values)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def has_executable?
|
|
185
|
+
!executable_code.nil?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# A bit of code to call in a binary that ships with the gem can be specified using
|
|
189
|
+
# the `executable` key.
|
|
190
|
+
def executable_code
|
|
191
|
+
metadata_hash['executable']
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
# Given a hash, we build a nicely formatted gemspec.
|
|
197
|
+
def gemspec_from_values(spec_values)
|
|
198
|
+
%Q[Gem::Specification.new do |gem|
|
|
199
|
+
gem.author = #{spec_values['author'].inspect}
|
|
200
|
+
gem.email = #{spec_values['email'].inspect}
|
|
201
|
+
gem.name = #{spec_values['name'].inspect}
|
|
202
|
+
gem.version = #{spec_values['version'].inspect}
|
|
203
|
+
gem.summary = #{spec_values['summary'].inspect}
|
|
204
|
+
gem.description = #{spec_values['description'].strip.inspect}
|
|
205
|
+
gem.homepage = #{spec_values['homepage'].inspect}
|
|
206
|
+
gem.files = [#{"#{spec_values['name']}.rb".inspect}]
|
|
207
|
+
gem.require_paths = ["."]
|
|
208
|
+
gem.bindir = "."
|
|
209
|
+
#{"gem.executables = [#{spec_values['name'].inspect}]" if has_executable?}
|
|
210
|
+
end].gsub(/^\s{7}/, "")
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Check the defaults passed in during initialization, or try and get an inferred
|
|
214
|
+
# one, or fail.
|
|
215
|
+
def default_or_inferred_for(key_name)
|
|
216
|
+
@defaults[key_name] || send("default_value_for_#{key_name}") || \
|
|
217
|
+
raise("No default value for: #{key_name}")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Try to pull an author name out of the git global config.
|
|
221
|
+
def default_value_for_author
|
|
222
|
+
git_global_config_for("user.name") do |author_val|
|
|
223
|
+
warn("Using author from git as: #{author_val}")
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Try to pull an author email out of the git global config.
|
|
228
|
+
def default_value_for_email
|
|
229
|
+
git_global_config_for("user.email") do |email_val|
|
|
230
|
+
warn("Using email from git as: #{email_val}")
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Searches the readme for something that looks like a version string to use
|
|
235
|
+
# as a version number.
|
|
236
|
+
def default_value_for_version
|
|
237
|
+
positional_match_or_nil(@chunked_source.readme, VERSION_MATCH, 2) do |str|
|
|
238
|
+
warn("Using version from README: #{str}")
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Searches the readme for something that looks like a homepage.
|
|
243
|
+
def default_value_for_homepage
|
|
244
|
+
positional_match_or_nil(@chunked_source.readme, HOMEPAGE_MATCH, 3) do |str|
|
|
245
|
+
warn("Using homepage from README: #{str}")
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Uses the first non-blank line from the readme as a summary.
|
|
250
|
+
def default_value_for_summary
|
|
251
|
+
positional_match_or_nil(@chunked_source.readme, SUMMARY_MATCH, 0) do |str|
|
|
252
|
+
warn("Using summary from README: #{str}")
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Uses the entire contents of the readme as a description.
|
|
257
|
+
def default_value_for_description
|
|
258
|
+
warn("Using README as description")
|
|
259
|
+
# RubyGems refuses to build a gem if the description contains `FIXME` or `TODO`,
|
|
260
|
+
# which are perfectly valid words to use in a description, but alas.
|
|
261
|
+
@chunked_source.readme.gsub(/FIXME/i, "FIZZIX-ME").gsub(/TODO/i, "TOODLES")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The metadata specified in the YAML part of the brief.
|
|
265
|
+
def metadata_hash
|
|
266
|
+
@metadata_hash ||= YAML.load(@chunked_source.metadata) || {}
|
|
267
|
+
rescue Psych::SyntaxError
|
|
268
|
+
msg = "Bad metadata hash - are you sure it's valid YAML?\n#{@chunked_source.metadata}"
|
|
269
|
+
raise SyntaxError, msg
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Fetches global config values out of git.
|
|
273
|
+
def git_global_config_for(config_key)
|
|
274
|
+
return nil unless system_has_git?
|
|
275
|
+
value = %x[git config --global #{config_key}]
|
|
276
|
+
conf_val = value.squeeze.strip.empty? ? nil : value.strip
|
|
277
|
+
yield(conf_val) if conf_val
|
|
278
|
+
conf_val
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# When given some text, a regexp with captures, and a capture position, get back the
|
|
282
|
+
# matched text at that position and yield it if a block is given, or return nil.
|
|
283
|
+
def positional_match_or_nil(source, re, position)
|
|
284
|
+
md = source.match(re)
|
|
285
|
+
matched_substr = md && md[position]
|
|
286
|
+
yield(matched_substr) if matched_substr
|
|
287
|
+
matched_substr
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def system_has_git?
|
|
291
|
+
system("which git 2>&1 1>/dev/null")
|
|
292
|
+
end
|
|
293
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: tinygem
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Gabriel Gironda
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: .
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-05-10 00:00:00.000000000 Z
|
|
13
|
+
dependencies: []
|
|
14
|
+
description: ! "# TinyGem: A tiny gem build tool\n\nVersion 1.0.0\n\nHome: https://github.com/gabrielg/tinygem\n\n`tinygem`
|
|
15
|
+
is a tool to build a RubyGem from a single file. `tinygem` itself is\nbuilt using
|
|
16
|
+
`tinygem`.\n\n## Dependencies\n\n`tinygem` has no library dependencies. It requires
|
|
17
|
+
Ruby 1.9.\n\n## Install\n\n`gem install tinygem`\n\n## Examples\n\nHere's an example
|
|
18
|
+
file to create a gem from:\n\n =begin\n author: Gabriel Gironda\n email:
|
|
19
|
+
gabriel@gironda.org\n version: 1.0.0\n summary: A tinygem example\n description:
|
|
20
|
+
Example gem created using tinygem\n homepage: http://www.example.com/\n executable:
|
|
21
|
+
puts(ARGV.inspect)\n ---\n\n # Example gem\n\n ## TOODLES - Write README\n\n
|
|
22
|
+
\ =end\n\n class ExampleGem\n # Do something\n end\n\nSave this in
|
|
23
|
+
`example.rb`, then run `tinygem example.rb`.\n\n## TOODLES\n\n* Add support for
|
|
24
|
+
specifying runtime dependencies\n* Write better docs"
|
|
25
|
+
email: gabriel@gironda.org
|
|
26
|
+
executables:
|
|
27
|
+
- tinygem
|
|
28
|
+
extensions: []
|
|
29
|
+
extra_rdoc_files: []
|
|
30
|
+
files:
|
|
31
|
+
- tinygem.rb
|
|
32
|
+
- !binary |-
|
|
33
|
+
Li90aW55Z2Vt
|
|
34
|
+
homepage: https://github.com/gabrielg/tinygem
|
|
35
|
+
licenses: []
|
|
36
|
+
post_install_message:
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
require_paths:
|
|
39
|
+
- .
|
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
|
+
none: false
|
|
42
|
+
requirements:
|
|
43
|
+
- - ! '>='
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0'
|
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
|
+
none: false
|
|
48
|
+
requirements:
|
|
49
|
+
- - ! '>='
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
requirements: []
|
|
53
|
+
rubyforge_project:
|
|
54
|
+
rubygems_version: 1.8.11
|
|
55
|
+
signing_key:
|
|
56
|
+
specification_version: 3
|
|
57
|
+
summary: ! '# TinyGem: A tiny gem build tool'
|
|
58
|
+
test_files: []
|
|
59
|
+
has_rdoc:
|