rake-deveiate 0.1.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.
@@ -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