rake-deveiate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,675 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'pathname'
5
+ require 'etc'
6
+
7
+ begin
8
+ gem 'rdoc'
9
+ rescue Gem::LoadError
10
+ end unless defined?(RDoc)
11
+
12
+ begin
13
+ gem 'rake'
14
+ rescue Gem::LoadError
15
+ end unless defined?(Rake)
16
+
17
+
18
+ require 'rake'
19
+ require 'rake/tasklib'
20
+ require 'rake/clean'
21
+ require 'rdoc'
22
+ require 'rdoc/markdown'
23
+ require 'tty/prompt'
24
+ require 'tty/table'
25
+ require 'pastel'
26
+ require 'rubygems/request_set'
27
+
28
+
29
+ # A task library for maintaining an open-source library.
30
+ class Rake::DevEiate < Rake::TaskLib
31
+ include Rake::TraceOutput
32
+
33
+ # Pattern for extracting a version constant
34
+ VERSION_PATTERN = /VERSION\s*=\s*(?<quote>['"])(?<version>\d+(\.\d+){2}.*)\k<quote>/
35
+
36
+ # The version of this library
37
+ VERSION = '0.1.0'
38
+
39
+ # The server to release to by default
40
+ DEFAULT_GEMSERVER = 'https://rubygems.org/'
41
+
42
+ # The description to use if none is set
43
+ DEFAULT_DESCRIPTION = "A gem of some sort."
44
+
45
+ # The version to use if one cannot be read from the source
46
+ DEFAULT_VERSION = '0.1.0'
47
+
48
+ # Paths
49
+ PROJECT_DIR = Pathname( '.' )
50
+
51
+ DOCS_DIR = PROJECT_DIR + 'docs'
52
+ LIB_DIR = PROJECT_DIR + 'lib'
53
+ EXT_DIR = PROJECT_DIR + 'ext'
54
+ SPEC_DIR = PROJECT_DIR + 'spec'
55
+ DATA_DIR = PROJECT_DIR + 'data'
56
+ CERTS_DIR = PROJECT_DIR + 'certs'
57
+ PKG_DIR = PROJECT_DIR + 'pkg'
58
+ CHECKSUM_DIR = PROJECT_DIR + 'checksum'
59
+
60
+ DEFAULT_MANIFEST_FILE = PROJECT_DIR + 'Manifest.txt'
61
+ DEFAULT_README_FILE = PROJECT_DIR + 'README.md'
62
+ DEFAULT_HISTORY_FILE = PROJECT_DIR + 'History.md'
63
+ DEFAULT_PROJECT_FILES = Rake::FileList[
64
+ '*.{rdoc,md,txt}',
65
+ 'bin/*',
66
+ 'lib/*.rb', 'lib/**/*.rb',
67
+ 'ext/*.[ch]', 'ext/**/*.[ch]',
68
+ 'data/**/*'
69
+ ]
70
+
71
+ # The default license for the project in SPDX form: https://spdx.org/licenses
72
+ DEFAULT_LICENSE = 'BSD-3-Clause'
73
+
74
+ # The file that contains the project's dependencies
75
+ GEMDEPS_FILE = PROJECT_DIR + 'gem.deps.rb'
76
+
77
+ # The file suffixes to include in documentation
78
+ DOCUMENTATION_SUFFIXES = %w[
79
+ .rb
80
+ .c
81
+ .h
82
+ .md
83
+ .rdoc
84
+ .txt
85
+ ]
86
+
87
+ # The path to the data directory for the Prestigio library.
88
+ DEVEIATE_DATADIR = if ENV['DEVEIATE_DATADIR']
89
+ Pathname( ENV['DEVEIATE_DATADIR'] )
90
+ elsif Gem.loaded_specs['rake-deveiate'] &&
91
+ File.directory?( Gem.loaded_specs['rake-deveiate'].datadir )
92
+ Pathname( Gem.loaded_specs['rake-deveiate'].datadir )
93
+ else
94
+ Pathname( __FILE__ ).dirname.parent.parent + 'data/rake-deveiate'
95
+ end
96
+
97
+
98
+ # Autoload utility classes
99
+ autoload :GemDepFinder, 'rake/deveiate/gem_dep_finder'
100
+
101
+
102
+ ### Declare an attribute that should be cast to a Pathname when set.
103
+ def self::attr_pathname( name ) # :nodoc:
104
+ attr_reader( name )
105
+ define_method( "#{name}=" ) do |new_value|
106
+ instance_variable_set( "@#{name}", Pathname(new_value) )
107
+ end
108
+ end
109
+
110
+
111
+ ### Set up common development tasks
112
+ def self::setup( name, **options, &block )
113
+ tasklib = self.new( name, **options, &block )
114
+ tasklib.define_tasks
115
+ return tasklib
116
+ end
117
+
118
+
119
+
120
+ ### Create the devEiate tasks for a gem with the given +name+.
121
+ def initialize( name, **options, &block )
122
+ @name = validate_gemname( name )
123
+ @options = options
124
+
125
+ @manifest_file = DEFAULT_MANIFEST_FILE.dup
126
+ @project_files = self.read_manifest
127
+ @version = self.find_version || DEFAULT_VERSION
128
+ @readme_file = self.find_readme
129
+ @history_file = self.find_history_file
130
+ @readme = self.parse_readme
131
+ @rdoc_files = self.make_rdoc_filelist
132
+ @cert_files = Rake::FileList[ CERTS_DIR + '*.pem' ]
133
+ @licenses = [ DEFAULT_LICENSE ]
134
+
135
+ @docs_dir = DOCS_DIR.dup
136
+
137
+ @title = self.extract_default_title
138
+ @authors = self.extract_authors
139
+ @homepage = self.extract_homepage
140
+ @description = self.extract_description || DEFAULT_DESCRIPTION
141
+ @summary = nil
142
+ @dependencies = self.find_dependencies
143
+
144
+ @gemserver = DEFAULT_GEMSERVER
145
+
146
+ super()
147
+
148
+ self.load_task_libraries
149
+
150
+ if block
151
+ if block.arity.nonzero?
152
+ block.call( self )
153
+ else
154
+ self.instance_exec( self, &block )
155
+ end
156
+ end
157
+ end
158
+
159
+
160
+ ######
161
+ public
162
+ ######
163
+
164
+ ##
165
+ # The name of the gem the task will build
166
+ attr_reader :name
167
+
168
+ ##
169
+ # The options Hash the task lib was created with
170
+ attr_reader :options
171
+
172
+ ##
173
+ # The descriotion of the gem
174
+ attr_accessor :description
175
+
176
+ ##
177
+ # The summary description of the gem.
178
+ attr_accessor :summary
179
+
180
+ ##
181
+ # The Gem::Version of the current library, extracted from the top-level
182
+ # namespace.
183
+ attr_reader :version
184
+
185
+ ##
186
+ # The README of the project as an RDoc::Markup::Document
187
+ attr_accessor :readme
188
+
189
+ ##
190
+ # The title of the library for things like docs, gemspec, etc.
191
+ attr_accessor :title
192
+
193
+ ##
194
+ # The file that will be the main page of documentation
195
+ attr_pathname :readme_file
196
+
197
+ ##
198
+ # The file that provides high-level change history
199
+ attr_pathname :history_file
200
+
201
+ ##
202
+ # The file to read the list of distribution files from
203
+ attr_pathname :manifest_file
204
+
205
+ ##
206
+ # The files which should be distributed with the project as a Rake::FileList
207
+ attr_accessor :project_files
208
+
209
+ ##
210
+ # The files which should be used to generate documentation as a Rake::FileList
211
+ attr_accessor :rdoc_files
212
+
213
+ ##
214
+ # The public cetificates that can be used to verify signed gems
215
+ attr_accessor :cert_files
216
+
217
+ ##
218
+ # The licenses the project is distributed under; usual practice is to list the
219
+ # SPDX name: https://spdx.org/licenses
220
+ attr_accessor :licenses
221
+
222
+ ##
223
+ # The gem's authors in the form of strings in the format: `Name <email>`
224
+ attr_accessor :authors
225
+
226
+ ##
227
+ # The URI of the project's homepage as a String
228
+ attr_accessor :homepage
229
+
230
+ ##
231
+ # The Gem::RequestSet that describes the gem's dependencies
232
+ attr_accessor :dependencies
233
+
234
+ ##
235
+ # The gemserver to push gems to
236
+ attr_accessor :gemserver
237
+
238
+
239
+ #
240
+ # Task definition
241
+ #
242
+
243
+ ### Load the deveiate task libraries.
244
+ def load_task_libraries
245
+ taskdir = Pathname( __FILE__.delete_suffix('.rb') )
246
+ tasklibs = Rake::FileList[ taskdir + '*.rb' ].pathmap( '%-2d/%n' )
247
+
248
+ self.trace( "Loading task libs: %p" % [ tasklibs ] )
249
+ tasklibs.each do |lib|
250
+ require( lib )
251
+ end
252
+
253
+ self.class.constants.
254
+ map {|c| self.class.const_get(c) }.
255
+ select {|c| c.respond_to?(:instance_methods) }.
256
+ select {|c| c.instance_methods(false).include?(:define_tasks) }.
257
+ each do |mod|
258
+ self.trace "Loading tasks from %p" % [ mod ]
259
+ extend( mod )
260
+ end
261
+
262
+ self.setup( self.name, **self.options )
263
+ end
264
+
265
+
266
+ ### Post-loading callback.
267
+ def setup( name, **options )
268
+ # No-op
269
+ end
270
+
271
+
272
+ ### Task-definition hook.
273
+ def define_tasks
274
+ self.define_default_tasks
275
+ self.define_debug_tasks
276
+
277
+ super if defined?( super )
278
+ end
279
+
280
+
281
+ ### Set up a simple default task
282
+ def define_default_tasks
283
+ desc "The task that runs by default"
284
+ task( :default => :spec )
285
+
286
+ desc "Check in the current changes"
287
+ task :checkin => :precheckin
288
+ task :commit => :checkin
289
+ task :ci => :checkin
290
+ task :precheckin => [ :check, :gemspec, :spec ]
291
+
292
+ desc "Sanity-check the project"
293
+ task :check
294
+
295
+ desc "Update the history file"
296
+ task :update_history
297
+
298
+ desc "Package up and push a release"
299
+ task :release => [ :prerelease, :release_gem, :postrelease ]
300
+ task :prerelease
301
+ task :release_gem
302
+ task :postrelease
303
+
304
+ task :spec
305
+ task :test => :spec
306
+ end
307
+
308
+
309
+ ### Set up tasks for debugging the task library.
310
+ def define_debug_tasks
311
+ task( :base_debug ) do
312
+ self.output_documentation_debugging
313
+ self.output_project_files_debugging
314
+ self.output_dependency_debugging
315
+ self.output_release_debugging
316
+ end
317
+
318
+ task :debug => :base_debug
319
+ end
320
+
321
+
322
+ #
323
+ # Utility methods
324
+ #
325
+
326
+ ### Fetch the TTY-Prompt, creating it if necessary.
327
+ def prompt
328
+ return @prompt ||= TTY::Prompt.new( output: $stderr )
329
+ end
330
+
331
+
332
+ ### Fetch the Pastel object, creating it if necessary.
333
+ def pastel
334
+ return @pastel ||= begin
335
+ pastel = Pastel.new( enabled: $stdout.tty? )
336
+ pastel.alias_color( :headline, :bold, :white, :on_black )
337
+ pastel.alias_color( :success, :bold, :green )
338
+ pastel.alias_color( :error, :bold, :red )
339
+ pastel.alias_color( :warning, :yellow )
340
+ pastel.alias_color( :added, :green )
341
+ pastel.alias_color( :removed, :red )
342
+ pastel.alias_color( :prompt, :cyan )
343
+ pastel.alias_color( :even_row, :bold )
344
+ pastel.alias_color( :odd_row, :reset )
345
+ pastel
346
+ end
347
+ end
348
+
349
+
350
+ ### Output +args+ to $stderr if tracing is enabled.
351
+ def trace( *args )
352
+ Rake.application.trace( *args ) if Rake.application.options.trace
353
+ end
354
+
355
+
356
+ ### Extract the default title from the README if possible, or derive it from the
357
+ ### gem name.
358
+ def extract_default_title
359
+ return self.name unless self.readme&.table_of_contents&.first
360
+ title = self.readme.table_of_contents.first.text
361
+ title ||= self.name
362
+ end
363
+
364
+
365
+ ### Extract a summary from the README if possible. Returns +nil+ if not.
366
+ def extract_summary
367
+ return self.description.split( /(?<=\.)\s+/ ).first.gsub( /\n/, ' ' )
368
+ end
369
+
370
+
371
+ ### Extract a description from the README if possible. Returns +nil+ if not.
372
+ def extract_description
373
+ parts = self.readme&.parts or return nil
374
+ return parts.find {|part| part.is_a?(RDoc::Markup::Paragraph) }&.text
375
+ end
376
+
377
+
378
+ ### Return just the name parts of the library's authors setting.
379
+ def author_names
380
+ return self.authors.map do |author|
381
+ author[ /^(.*?) </, 1 ]
382
+ end
383
+ end
384
+
385
+
386
+ ### Extract authors in the form `Firstname Lastname <email@address>` from the README.
387
+ def extract_authors
388
+ return [] unless self.readme
389
+
390
+ heading, list = self.readme.parts.each_cons( 2 ).find do |heading, list|
391
+ heading.is_a?( RDoc::Markup::Heading ) && heading.text =~ /^authors?/i &&
392
+ list.is_a?( RDoc::Markup::List )
393
+ end
394
+
395
+ unless list
396
+ self.trace "Couldn't find an Author(s) section of the readme."
397
+ return []
398
+ end
399
+
400
+ return list.items.map do |item|
401
+ # unparse the name + email
402
+ raw = item.parts.first.text or next
403
+ name, email = raw.split( ' mailto:', 2 )
404
+ "%s <%s>" % [ name, email ]
405
+ end
406
+ end
407
+
408
+
409
+ ### Extract the URI of the homepage from the `home` item of the first NOTE-type
410
+ ### list in the README. Returns +nil+ if no such URI could be found.
411
+ def extract_homepage
412
+ return fail_extraction(:homepage, "no README") unless self.readme
413
+
414
+ list = self.readme.parts.find {|part| RDoc::Markup::List === part && part.type == :NOTE } or
415
+ return fail_extraction(:homepage, "No NOTE list")
416
+ item = list.items.find {|item| item.label.include?('home') } or
417
+ return fail_extraction(:homepage, "No `home` item")
418
+
419
+ return item.parts.first.text
420
+ end
421
+
422
+
423
+ ### Find the file that contains the VERSION constant and return it as a
424
+ ### Gem::Version.
425
+ def find_version
426
+ version_file = LIB_DIR + "%s.rb" % [ self.name.gsub(/-/, '/') ]
427
+
428
+ unless version_file.readable?
429
+ self.prompt.warn "Version could not be read from %s" % [ version_file ]
430
+ return nil
431
+ end
432
+
433
+ version_line = version_file.readlines.find {|l| l =~ VERSION_PATTERN } or
434
+ abort "Can't read the VERSION from #{version_file}!"
435
+ version = version_line[ VERSION_PATTERN, :version ] or
436
+ abort "Couldn't find a semantic version in %p" % [ version_line ]
437
+
438
+ return Gem::Version.new( version )
439
+ end
440
+
441
+
442
+ ### Returns +true+ if the manifest file exists and is readable.
443
+ def has_manifest?
444
+ return self.manifest_file.readable?
445
+ end
446
+
447
+
448
+ ### Read the manifest file if there is one, falling back to a default list if
449
+ ### there isn't a manifest.
450
+ def read_manifest
451
+ if self.has_manifest?
452
+ entries = self.manifest_file.readlines.map( &:chomp )
453
+ return Rake::FileList[ *entries ]
454
+ else
455
+ self.prompt.warn "No manifest (%s): falling back to a default list" %
456
+ [ self.manifest_file ]
457
+ return DEFAULT_PROJECT_FILES.dup
458
+ end
459
+ end
460
+
461
+
462
+ ### Make a Rake::FileList of the files that should be used to generate
463
+ ### documentation.
464
+ def make_rdoc_filelist
465
+ list = self.project_files.dup
466
+
467
+ list.exclude do |fn|
468
+ fn =~ %r:^(spec|data)/: || !fn.end_with?( *DOCUMENTATION_SUFFIXES )
469
+ end
470
+
471
+ return list
472
+ end
473
+
474
+
475
+ ### Find the README file in the list of project files and return it as a
476
+ ### Pathname.
477
+ def find_readme
478
+ file = self.project_files.find {|file| file =~ /^README\.(md|rdoc)$/ }
479
+ if file
480
+ return Pathname( file )
481
+ else
482
+ self.prompt.warn "No README found in the project files."
483
+ return DEFAULT_README_FILE
484
+ end
485
+ end
486
+
487
+
488
+ ### Find the history file in the list of project files and return it as a
489
+ ### Pathname.
490
+ def find_history_file
491
+ file = self.project_files.find {|file| file =~ /^History\.(md|rdoc)$/ }
492
+ if file
493
+ return Pathname( file )
494
+ else
495
+ self.prompt.warn "No History.{md,rdoc} found in the project files."
496
+ return DEFAULT_HISTORY_FILE
497
+ end
498
+ end
499
+
500
+
501
+ ### Generate a TTY::Table from the current project files and return it.
502
+ def generate_project_files_table
503
+ columns = [
504
+ self.project_files.sort,
505
+ self.rdoc_files.sort
506
+ ]
507
+
508
+ max_length = columns.map( &:length ).max
509
+ columns.each do |col|
510
+ self.trace "Filling out columns %d-%d" % [ col.length, max_length ]
511
+ next if col.length == max_length
512
+ col.fill( '', col.length .. max_length - 1 )
513
+ end
514
+
515
+ table = TTY::Table.new(
516
+ header: ['Project', 'Documentation'],
517
+ rows: columns.transpose,
518
+ )
519
+
520
+ return table
521
+ end
522
+
523
+
524
+ ### Generate a TTY::Table from the current dependency list and return it.
525
+ def generate_dependencies_table
526
+ table = TTY::Table.new( header: ['Gem', 'Version', 'Type'] )
527
+
528
+ self.dependencies.each do |dep|
529
+ table << [ dep.name, dep.requirement.to_s, dep.type ]
530
+ end
531
+
532
+ return table
533
+ end
534
+
535
+
536
+ ### Parse the README into an RDoc::Markup::Document and return it
537
+ def parse_readme
538
+ return nil unless self.readme_file.readable?
539
+
540
+ case self.readme_file.extname
541
+ when '.md'
542
+ return RDoc::Markdown.parse( self.readme_file.read )
543
+ when '.rdoc'
544
+ return RDoc::Markup.parse( self.readme_file.read )
545
+ else
546
+ raise "Can't parse %s: unhandled format %p" % [ self.readme_file, README_FILE.extname ]
547
+ end
548
+ end
549
+
550
+
551
+ ### Load the gemdeps file if it exists, and return a Gem::RequestSet with the
552
+ ### regular dependencies contained in it.
553
+ def find_dependencies
554
+ unless GEMDEPS_FILE.readable?
555
+ self.prompt.warn "Deps file (%s) is missing or unreadable, assuming no dependencies." %
556
+ [ GEMDEPS_FILE ]
557
+ return []
558
+ end
559
+
560
+ finder = Rake::DevEiate::GemDepFinder.new( GEMDEPS_FILE )
561
+ finder.load
562
+ return finder.dependencies
563
+ end
564
+
565
+
566
+ ### Return the character used to build headings give the filename of the file to
567
+ ### be generated.
568
+ def header_char_for( filename )
569
+ case File.extname( filename )
570
+ when '.md' then return '#'
571
+ when '.rdoc' then return '='
572
+ else
573
+ raise "Don't know what header character is appropriate for %s" % [ filename ]
574
+ end
575
+ end
576
+
577
+
578
+ ### Read a template with the given +name+ from the data directory and return it
579
+ ### as an ERB object.
580
+ def read_template( name )
581
+ name = "%s.erb" % [ name ] unless name.to_s.end_with?( '.erb' )
582
+ template_path = DEVEIATE_DATADIR + name
583
+ template_src = template_path.read( encoding: 'utf-8' )
584
+
585
+ return ERB.new( template_src, trim_mode: '-' )
586
+ end
587
+
588
+
589
+ ### Load the template at the specified +template_path+, and render it with suitable
590
+ ### settings for the given +target_filename+.
591
+ def load_and_render_template( template_path, target_filename )
592
+ template = self.read_template( template_path )
593
+ header_char = self.header_char_for( target_filename )
594
+
595
+ return template.result_with_hash(
596
+ header_char: header_char,
597
+ project: self
598
+ )
599
+ end
600
+
601
+
602
+ ### Output debugging information about documentation.
603
+ def output_documentation_debugging
604
+ summary = self.extract_summary
605
+ description = self.extract_description
606
+
607
+ self.prompt.say( "Documentation", color: :bright_green )
608
+ self.prompt.say( "Authors:" )
609
+ self.authors.each do |author|
610
+ self.prompt.say( " • " )
611
+ self.prompt.say( author, color: :bold )
612
+ end
613
+ self.prompt.say( "Summary: " )
614
+ self.prompt.say( summary, color: :bold )
615
+ self.prompt.say( "Description:" )
616
+ self.prompt.say( description, color: :bold )
617
+ self.prompt.say( "\n" )
618
+ end
619
+
620
+
621
+ ### Output debugging info related to the list of project files the build
622
+ ### operates on.
623
+ def output_project_files_debugging
624
+ self.prompt.say( "Project files:", color: :bright_green )
625
+ table = self.generate_project_files_table
626
+ if table.empty?
627
+ self.prompt.warn( "None." )
628
+ else
629
+ self.prompt.say( table.render(:unicode, padding: [0,1]) )
630
+ end
631
+ self.prompt.say( "\n" )
632
+ end
633
+
634
+
635
+ ### Output debugging about the project's dependencies.
636
+ def output_dependency_debugging
637
+ self.prompt.say( "Dependencies", color: :bright_green )
638
+ table = self.generate_dependencies_table
639
+ if table.empty?
640
+ self.prompt.warn( "None." )
641
+ else
642
+ self.prompt.say( table.render(:unicode, padding: [0,1]) )
643
+ end
644
+ self.prompt.say( "\n" )
645
+ end
646
+
647
+
648
+ ### Output debugging regarding where releases will be posted.
649
+ def output_release_debugging
650
+ self.prompt.say( "Will push releases to:", color: :bright_green )
651
+ self.prompt.say( " #{self.gemserver}" )
652
+ self.prompt.say( "\n" )
653
+ end
654
+
655
+
656
+ #######
657
+ private
658
+ #######
659
+
660
+ ### Ensure the given +gemname+ is valid, raising if it isn't.
661
+ def validate_gemname( gemname )
662
+ raise ScriptError, "invalid gem name" unless
663
+ Gem::SpecificationPolicy::VALID_NAME_PATTERN.match?( gemname )
664
+ return gemname.freeze
665
+ end
666
+
667
+
668
+ ### Log a reason that extraction of the specified +item+ failed for the given
669
+ ### +reason+ and then return +nil+.
670
+ def fail_extraction( item, reason )
671
+ self.prompt.warn "Extraction of %s failed: %s" % [ item, reason ]
672
+ return nil
673
+ end
674
+
675
+ end # class Rake::DevEiate
data.tar.gz.sig ADDED
Binary file