tinygem 1.0.0

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