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.
Files changed (3) hide show
  1. data/tinygem +3 -0
  2. data/tinygem.rb +293 -0
  3. metadata +59 -0
data/tinygem ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require "tinygem"
3
+ TinyGem.new(ARGV[0]).package
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: