tinygem 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|