xhtmldiff 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.
data/README ADDED
@@ -0,0 +1,25 @@
1
+ = XHTML Diff
2
+
3
+ == Purpose
4
+
5
+ To transform two revisions of an XHTML document into a single valid XHMTL
6
+ document with redlines, using the <ins> and <del> tags.
7
+
8
+ == Requirements
9
+
10
+ * Diff::LCS
11
+ * REXML (Standard Library in 1.8)
12
+ * Delegate (Standard Library)
13
+
14
+ == Installing
15
+
16
+ run setup.rb:
17
+
18
+ ruby setup.rb config
19
+ ruby setup.rb setup
20
+ ruby setup.rb install
21
+
22
+ == That's it.
23
+
24
+ == Author
25
+ Aredridel <aredridel@nbtsc.org>
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'xhtmldiff'
4
+
5
+ include REXML
6
+
7
+ if ARGV.size != 2
8
+ puts "usage: #{File.basename($0)} old new > output.html"
9
+ exit 255
10
+ end
11
+
12
+ odoc = Document.new
13
+ odoc << (Element.new 'html')
14
+ odoc.root << (head = Element.new 'head')
15
+ head << (title = Element.new 'title')
16
+ title << Text.new("Diff of #{ARGV[0]} and #{ARGV[1]}")
17
+ odoc.root << (body = Element.new 'body')
18
+
19
+ hd = XHTMLDiff.new(body)
20
+
21
+ a = HashableElementDelegator.new(XPath.first(Document.new(File.read(ARGV[0])), '/html/body'))
22
+ b = HashableElementDelegator.new(XPath.first(Document.new(File.read(ARGV[1])), '/html/body'))
23
+
24
+ Diff::LCS.traverse_balanced(a, b, hd)
25
+
26
+ odoc.write($stdout, 0, false, true)
27
+
@@ -0,0 +1,10 @@
1
+ <html>
2
+ <head>
3
+ <title>An example page</title>
4
+ </head>
5
+ <body>
6
+ <p>I like Ruby.</p>
7
+
8
+ <p>I like Ruby a lot.</p>
9
+ </body>
10
+ </html>
@@ -0,0 +1,12 @@
1
+ <html>
2
+ <head>
3
+ <title>An example page</title>
4
+ </head>
5
+ <body>
6
+ <p>I like Ruby.</p>
7
+
8
+ <p>You like Ruby!</p>
9
+
10
+ <p>We all love Ruby!</p>
11
+ </body>
12
+ </html>
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'diff/lcs'
4
+ require 'rexml/document'
5
+ require 'delegate'
6
+
7
+ def Math.max(a, b)
8
+ a > b ? a : b
9
+ end
10
+
11
+ module Kernel
12
+ def debug
13
+ yield if $DEBUG
14
+ end
15
+ end
16
+
17
+ module REXML
18
+
19
+ class Text
20
+ def deep_clone
21
+ clone
22
+ end
23
+ end
24
+
25
+ class HashableElementDelegator < DelegateClass(Element)
26
+ def initialize(sub)
27
+ super sub
28
+ end
29
+ def == other
30
+ res = other.to_s.strip == self.to_s.strip
31
+ res
32
+ end
33
+
34
+ def eql? other
35
+ self == other
36
+ end
37
+
38
+ def[](k)
39
+ r = super
40
+ if r.kind_of? __getobj__.class
41
+ self.class.new(r)
42
+ else
43
+ r
44
+ end
45
+ end
46
+
47
+ def hash
48
+ r = __getobj__.to_s.hash
49
+ r
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+ class XHTMLDiff
56
+ include REXML
57
+ attr_accessor :output
58
+
59
+ class << self
60
+ BLOCK_CONTAINERS = ['div', 'ul', 'li']
61
+ def diff(a, b)
62
+ if a == b
63
+ return a.deep_clone
64
+ end
65
+ if REXML::HashableElementDelegator === a and REXML::HashableElementDelegator === b
66
+ o = REXML::Element.new(a.name)
67
+ o.add_attributes a.attributes
68
+ hd = self.new(o)
69
+ Diff::LCS.traverse_balanced(a, b, hd)
70
+ o
71
+ else
72
+ raise ArgumentError.new("both arguments must be equal or both be elements. a is #{a.class.name} and b is #{b.class.name}")
73
+ end
74
+ end
75
+ end
76
+
77
+ def diff(a, b)
78
+ self.class.diff(a,b)
79
+ end
80
+
81
+ def initialize(output)
82
+ @output = output
83
+ end
84
+
85
+ # This will be called with both elements are the same
86
+ def match(event)
87
+ debug { $stderr.puts event.inspect }
88
+ @output << event.old_element.deep_clone if event.old_element
89
+ end
90
+
91
+ # This will be called when there is an element in A that isn't in B
92
+ def discard_a(event)
93
+ @output << wrap(event.old_element, 'del')
94
+ end
95
+
96
+ def change(event)
97
+ begin
98
+ sd = diff(event.old_element, event.new_element)
99
+ rescue ArgumentError
100
+ debug { $stderr.puts "Not subdiffable: #{$!.message}" }
101
+ sd = nil
102
+ end
103
+ if sd and ((rs = Float(sd.to_s.size)) / bs = Math.max(event.old_element.to_s.size, event.new_element.to_s.size)) < 2
104
+ debug { $stderr.puts "Chose recursed: rs = #{rs}, bs = #{bs}" }
105
+ @output << sd
106
+ else
107
+ debug { $stderr.puts "Chose block: rs = #{rs}, bs = #{bs}" }
108
+ @output << wrap(event.old_element, 'del')
109
+ @output << wrap(event.new_element, 'ins')
110
+ end
111
+ end
112
+
113
+ # This will be called when there is an element in B that isn't in A
114
+ def discard_b(event)
115
+ @output << wrap(event.new_element, 'ins')
116
+ end
117
+
118
+ def choose_event(event, element, tag)
119
+ end
120
+
121
+ def wrap(element, tag)
122
+ el = Element.new tag
123
+ el << element.deep_clone
124
+ el
125
+ end
126
+
127
+ end
128
+
129
+ if $0 == __FILE__
130
+
131
+ $stderr.puts "No tests available yet"
132
+ exit(1)
133
+
134
+ end
@@ -0,0 +1,1312 @@
1
+ #!/usr/bin/env ruby -w
2
+ #
3
+ # This file is automatically generated. DO NOT MODIFY!
4
+ #
5
+ # setup.rb
6
+ #
7
+ # Copyright (c) 2000-2003 Minero Aoki <aamine@loveruby.net>
8
+ #
9
+ # This program is free software.
10
+ # You can distribute/modify this program under the terms of
11
+ # the GNU Lesser General Public License version 2.
12
+ #
13
+
14
+ def multipackage_install?
15
+ FileTest.directory?(File.dirname($0) + '/packages')
16
+ end
17
+
18
+ #
19
+ # compat.rb
20
+ #
21
+
22
+ unless Enumerable.method_defined?(:map)
23
+ module Enumerable
24
+ alias map collect
25
+ end
26
+ end
27
+
28
+ unless Enumerable.method_defined?(:select)
29
+ module Enumerable
30
+ alias select find_all
31
+ end
32
+ end
33
+
34
+ unless Enumerable.method_defined?(:reject)
35
+ module Enumerable
36
+ def reject
37
+ result = []
38
+ each do |i|
39
+ result.push i unless yield(i)
40
+ end
41
+ result
42
+ end
43
+ end
44
+ end
45
+
46
+ unless Enumerable.method_defined?(:inject)
47
+ module Enumerable
48
+ def inject(result)
49
+ each do |i|
50
+ result = yield(result, i)
51
+ end
52
+ result
53
+ end
54
+ end
55
+ end
56
+
57
+ unless Enumerable.method_defined?(:any?)
58
+ module Enumerable
59
+ def any?
60
+ each do |i|
61
+ return true if yield(i)
62
+ end
63
+ false
64
+ end
65
+ end
66
+ end
67
+
68
+ unless File.respond_to?(:read)
69
+ def File.read(fname)
70
+ File.open(fname) {|f|
71
+ return f.read
72
+ }
73
+ end
74
+ end
75
+
76
+ #
77
+ # fileop.rb
78
+ #
79
+
80
+ module FileOperations
81
+
82
+ def mkdir_p(dirname, prefix = nil)
83
+ dirname = prefix + dirname if prefix
84
+ $stderr.puts "mkdir -p #{dirname}" if verbose?
85
+ return if no_harm?
86
+
87
+ # does not check '/'... it's too abnormal case
88
+ dirs = dirname.split(%r<(?=/)>)
89
+ if /\A[a-z]:\z/i =~ dirs[0]
90
+ disk = dirs.shift
91
+ dirs[0] = disk + dirs[0]
92
+ end
93
+ dirs.each_index do |idx|
94
+ path = dirs[0..idx].join('')
95
+ Dir.mkdir path unless File.dir?(path)
96
+ end
97
+ end
98
+
99
+ def rm_f(fname)
100
+ $stderr.puts "rm -f #{fname}" if verbose?
101
+ return if no_harm?
102
+
103
+ if File.exist?(fname) or File.symlink?(fname)
104
+ File.chmod 0777, fname
105
+ File.unlink fname
106
+ end
107
+ end
108
+
109
+ def rm_rf(dn)
110
+ $stderr.puts "rm -rf #{dn}" if verbose?
111
+ return if no_harm?
112
+
113
+ Dir.chdir dn
114
+ Dir.foreach('.') do |fn|
115
+ next if fn == '.'
116
+ next if fn == '..'
117
+ if File.dir?(fn)
118
+ verbose_off {
119
+ rm_rf fn
120
+ }
121
+ else
122
+ verbose_off {
123
+ rm_f fn
124
+ }
125
+ end
126
+ end
127
+ Dir.chdir '..'
128
+ Dir.rmdir dn
129
+ end
130
+
131
+ def move_file(src, dest)
132
+ File.unlink dest if File.exist?(dest)
133
+ begin
134
+ File.rename src, dest
135
+ rescue
136
+ File.open(dest, 'wb') {|f| f.write File.read(src) }
137
+ File.chmod File.stat(src).mode, dest
138
+ File.unlink src
139
+ end
140
+ end
141
+
142
+ def install(from, dest, mode, prefix = nil)
143
+ $stderr.puts "install #{from} #{dest}" if verbose?
144
+ return if no_harm?
145
+
146
+ realdest = prefix + dest if prefix
147
+ realdest += '/' + File.basename(from) if File.dir?(realdest)
148
+ str = File.read(from)
149
+ if diff?(str, realdest)
150
+ verbose_off {
151
+ rm_f realdest if File.exist?(realdest)
152
+ }
153
+ File.open(realdest, 'wb') {|f| f.write str }
154
+ File.chmod mode, realdest
155
+
156
+ File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| f.puts dest }
157
+ end
158
+ end
159
+
160
+ def diff?(orig, targ)
161
+ return true unless File.exist?(targ)
162
+ orig != File.read(targ)
163
+ end
164
+
165
+ def command(str)
166
+ $stderr.puts str if verbose?
167
+ system str or raise RuntimeError, "'system #{str}' failed"
168
+ end
169
+
170
+ def ruby(str)
171
+ command config('ruby-prog') + ' ' + str
172
+ end
173
+
174
+ def make(task = '')
175
+ command config('make-prog') + ' ' + task
176
+ end
177
+
178
+ def extdir?(dir)
179
+ File.exist?(dir + '/MANIFEST')
180
+ end
181
+
182
+ def all_files_in(dirname)
183
+ Dir.open(dirname) {|d|
184
+ return d.select {|ent| File.file?("#{dirname}/#{ent}") }
185
+ }
186
+ end
187
+
188
+ REJECT_DIRS = %w(
189
+ CVS SCCS RCS CVS.adm .svn
190
+ )
191
+
192
+ def all_dirs_in(dirname)
193
+ Dir.open(dirname) {|d|
194
+ return d.select {|n| File.dir?("#{dirname}/#{n}") } - %w(. ..) - REJECT_DIRS
195
+ }
196
+ end
197
+
198
+ end
199
+
200
+ class File
201
+
202
+ def File.dir?(path)
203
+ # for corrupted windows stat()
204
+ File.directory?((path[-1,1] == '/') ? path : path + '/')
205
+ end
206
+
207
+ end
208
+
209
+ #
210
+ # config.rb
211
+ #
212
+
213
+ if idx = ARGV.index(/\A--rbconfig=/)
214
+ require ARGV.delete_at(idx).split(/=/, 2)[1]
215
+ else
216
+ require 'rbconfig'
217
+ end
218
+
219
+ class ConfigTable
220
+
221
+ c = ::Config::CONFIG
222
+
223
+ rubypath = c['bindir'] + '/' + c['ruby_install_name']
224
+
225
+ major = c['MAJOR'].to_i
226
+ minor = c['MINOR'].to_i
227
+ teeny = c['TEENY'].to_i
228
+ version = "#{major}.#{minor}"
229
+
230
+ # ruby ver. >= 1.4.4?
231
+ newpath_p = ((major >= 2) or
232
+ ((major == 1) and
233
+ ((minor >= 5) or
234
+ ((minor == 4) and (teeny >= 4)))))
235
+
236
+ subprefix = lambda {|path|
237
+ path.sub(/\A#{Regexp.quote(c['prefix'])}/o, '$prefix')
238
+ }
239
+
240
+ if c['rubylibdir']
241
+ # V < 1.6.3
242
+ stdruby = subprefix.call(c['rubylibdir'])
243
+ siteruby = subprefix.call(c['sitedir'])
244
+ versite = subprefix.call(c['sitelibdir'])
245
+ sodir = subprefix.call(c['sitearchdir'])
246
+ elsif newpath_p
247
+ # 1.4.4 <= V <= 1.6.3
248
+ stdruby = "$prefix/lib/ruby/#{version}"
249
+ siteruby = subprefix.call(c['sitedir'])
250
+ versite = siteruby + '/' + version
251
+ sodir = "$site-ruby/#{c['arch']}"
252
+ else
253
+ # V < 1.4.4
254
+ stdruby = "$prefix/lib/ruby/#{version}"
255
+ siteruby = "$prefix/lib/ruby/#{version}/site_ruby"
256
+ versite = siteruby
257
+ sodir = "$site-ruby/#{c['arch']}"
258
+ end
259
+
260
+ common_descripters = [
261
+ [ 'prefix', [ c['prefix'],
262
+ 'path',
263
+ 'path prefix of target environment' ] ],
264
+ [ 'std-ruby', [ stdruby,
265
+ 'path',
266
+ 'the directory for standard ruby libraries' ] ],
267
+ [ 'site-ruby-common', [ siteruby,
268
+ 'path',
269
+ 'the directory for version-independent non-standard ruby libraries' ] ],
270
+ [ 'site-ruby', [ versite,
271
+ 'path',
272
+ 'the directory for non-standard ruby libraries' ] ],
273
+ [ 'bin-dir', [ '$prefix/bin',
274
+ 'path',
275
+ 'the directory for commands' ] ],
276
+ [ 'rb-dir', [ '$site-ruby',
277
+ 'path',
278
+ 'the directory for ruby scripts' ] ],
279
+ [ 'so-dir', [ sodir,
280
+ 'path',
281
+ 'the directory for ruby extentions' ] ],
282
+ [ 'data-dir', [ '$prefix/share',
283
+ 'path',
284
+ 'the directory for shared data' ] ],
285
+ [ 'ruby-path', [ rubypath,
286
+ 'path',
287
+ 'path to set to #! line' ] ],
288
+ [ 'ruby-prog', [ rubypath,
289
+ 'name',
290
+ 'the ruby program using for installation' ] ],
291
+ [ 'make-prog', [ 'make',
292
+ 'name',
293
+ 'the make program to compile ruby extentions' ] ],
294
+ [ 'without-ext', [ 'no',
295
+ 'yes/no',
296
+ 'does not compile/install ruby extentions' ] ]
297
+ ]
298
+ multipackage_descripters = [
299
+ [ 'with', [ '',
300
+ 'name,name...',
301
+ 'package names that you want to install',
302
+ 'ALL' ] ],
303
+ [ 'without', [ '',
304
+ 'name,name...',
305
+ 'package names that you do not want to install',
306
+ 'NONE' ] ]
307
+ ]
308
+ if multipackage_install?
309
+ DESCRIPTER = common_descripters + multipackage_descripters
310
+ else
311
+ DESCRIPTER = common_descripters
312
+ end
313
+
314
+ SAVE_FILE = 'config.save'
315
+
316
+ def ConfigTable.each_name(&block)
317
+ keys().each(&block)
318
+ end
319
+
320
+ def ConfigTable.keys
321
+ DESCRIPTER.map {|name, *dummy| name }
322
+ end
323
+
324
+ def ConfigTable.each_definition(&block)
325
+ DESCRIPTER.each(&block)
326
+ end
327
+
328
+ def ConfigTable.get_entry(name)
329
+ name, ent = DESCRIPTER.assoc(name)
330
+ ent
331
+ end
332
+
333
+ def ConfigTable.get_entry!(name)
334
+ get_entry(name) or raise ArgumentError, "no such config: #{name}"
335
+ end
336
+
337
+ def ConfigTable.add_entry(name, vals)
338
+ ConfigTable::DESCRIPTER.push [name,vals]
339
+ end
340
+
341
+ def ConfigTable.remove_entry(name)
342
+ get_entry(name) or raise ArgumentError, "no such config: #{name}"
343
+ DESCRIPTER.delete_if {|n, arr| n == name }
344
+ end
345
+
346
+ def ConfigTable.config_key?(name)
347
+ get_entry(name) ? true : false
348
+ end
349
+
350
+ def ConfigTable.bool_config?(name)
351
+ ent = get_entry(name) or return false
352
+ ent[1] == 'yes/no'
353
+ end
354
+
355
+ def ConfigTable.value_config?(name)
356
+ ent = get_entry(name) or return false
357
+ ent[1] != 'yes/no'
358
+ end
359
+
360
+ def ConfigTable.path_config?(name)
361
+ ent = get_entry(name) or return false
362
+ ent[1] == 'path'
363
+ end
364
+
365
+
366
+ class << self
367
+ alias newobj new
368
+ end
369
+
370
+ def ConfigTable.new
371
+ c = newobj()
372
+ c.initialize_from_table
373
+ c
374
+ end
375
+
376
+ def ConfigTable.load
377
+ c = newobj()
378
+ c.initialize_from_file
379
+ c
380
+ end
381
+
382
+ def initialize_from_table
383
+ @table = {}
384
+ DESCRIPTER.each do |k, (default, vname, desc, default2)|
385
+ @table[k] = default
386
+ end
387
+ end
388
+
389
+ def initialize_from_file
390
+ unless File.file?(SAVE_FILE)
391
+ raise InstallError, "#{File.basename $0} config first"
392
+ end
393
+ @table = {}
394
+ File.foreach(SAVE_FILE) do |line|
395
+ k, v = line.split(/=/, 2)
396
+ @table[k] = v.strip
397
+ end
398
+ end
399
+
400
+ def save
401
+ File.open(SAVE_FILE, 'w') {|f|
402
+ @table.each do |k, v|
403
+ f.printf "%s=%s\n", k, v if v
404
+ end
405
+ }
406
+ end
407
+
408
+ def []=(k, v)
409
+ unless ConfigTable.config_key?(k) then
410
+ raise InstallError, "unknown config option #{k}"
411
+ end
412
+
413
+ @table[k] = v
414
+ end
415
+
416
+ def [](key)
417
+ return nil unless @table[key]
418
+ @table[key].gsub(%r<\$([^/]+)>) { self[$1] }
419
+ end
420
+
421
+ def set_raw(key, val)
422
+ @table[key] = val
423
+ end
424
+
425
+ def get_raw(key)
426
+ @table[key]
427
+ end
428
+
429
+ end
430
+
431
+ module MetaConfigAPI
432
+
433
+ def eval_file_ifexist(fname)
434
+ instance_eval File.read(fname), fname, 1 if File.file?(fname)
435
+ end
436
+
437
+ def config_names
438
+ ConfigTable.keys
439
+ end
440
+
441
+ def config?(name)
442
+ ConfigTable.config_key?(name)
443
+ end
444
+
445
+ def bool_config?(name)
446
+ ConfigTable.bool_config?(name)
447
+ end
448
+
449
+ def value_config?(name)
450
+ ConfigTable.value_config?(name)
451
+ end
452
+
453
+ def path_config?(name)
454
+ ConfigTable.path_config?(name)
455
+ end
456
+
457
+ def add_config(name, argname, default, desc)
458
+ ConfigTable.add_entry name,[default,argname,desc]
459
+ end
460
+
461
+ def add_path_config(name, default, desc)
462
+ add_config name, 'path', default, desc
463
+ end
464
+
465
+ def add_bool_config(name, default, desc)
466
+ add_config name, 'yes/no', default ? 'yes' : 'no', desc
467
+ end
468
+
469
+ def set_config_default(name, default)
470
+ if bool_config?(name)
471
+ ConfigTable.get_entry!(name)[0] = (default ? 'yes' : 'no')
472
+ else
473
+ ConfigTable.get_entry!(name)[0] = default
474
+ end
475
+ end
476
+
477
+ def remove_config(name)
478
+ ent = ConfigTable.get_entry(name)
479
+ ConfigTable.remove_entry name
480
+ ent
481
+ end
482
+
483
+ end
484
+
485
+ #
486
+ # base.rb
487
+ #
488
+
489
+ require 'rbconfig'
490
+
491
+ class InstallError < StandardError; end
492
+
493
+ module HookUtils
494
+
495
+ def run_hook(name)
496
+ try_run_hook "#{curr_srcdir()}/#{name}" or
497
+ try_run_hook "#{curr_srcdir()}/#{name}.rb"
498
+ end
499
+
500
+ def try_run_hook(fname)
501
+ return false unless File.file?(fname)
502
+ begin
503
+ instance_eval File.read(fname), fname, 1
504
+ rescue
505
+ raise InstallError, "hook #{fname} failed:\n" + $!.message
506
+ end
507
+ true
508
+ end
509
+
510
+ end
511
+
512
+ module HookScriptAPI
513
+
514
+ def get_config(key)
515
+ @config[key]
516
+ end
517
+
518
+ alias config get_config
519
+
520
+ def set_config(key, val)
521
+ @config[key] = val
522
+ end
523
+
524
+ #
525
+ # srcdir/objdir (works only in the package directory)
526
+ #
527
+
528
+ #abstract srcdir_root
529
+ #abstract objdir_root
530
+ #abstract relpath
531
+
532
+ def curr_srcdir
533
+ "#{srcdir_root()}/#{relpath()}"
534
+ end
535
+
536
+ def curr_objdir
537
+ "#{objdir_root()}/#{relpath()}"
538
+ end
539
+
540
+ def srcfile(path)
541
+ "#{curr_srcdir()}/#{path}"
542
+ end
543
+
544
+ def srcexist?(path)
545
+ File.exist?(srcfile(path))
546
+ end
547
+
548
+ def srcdirectory?(path)
549
+ File.dir?(srcfile(path))
550
+ end
551
+
552
+ def srcfile?(path)
553
+ File.file? srcfile(path)
554
+ end
555
+
556
+ def srcentries(path = '.')
557
+ Dir.open("#{curr_srcdir()}/#{path}") {|d|
558
+ return d.to_a - %w(. ..)
559
+ }
560
+ end
561
+
562
+ def srcfiles(path = '.')
563
+ srcentries(path).select {|fname|
564
+ File.file?(File.join(curr_srcdir(), path, fname))
565
+ }
566
+ end
567
+
568
+ def srcdirectories(path = '.')
569
+ srcentries(path).select {|fname|
570
+ File.dir?(File.join(curr_srcdir(), path, fname))
571
+ }
572
+ end
573
+
574
+ end
575
+
576
+ class Installer
577
+
578
+ FILETYPES = %w( bin lib ext data )
579
+
580
+ include HookScriptAPI
581
+ include HookUtils
582
+ include FileOperations
583
+
584
+ def initialize(config, opt, srcroot, objroot)
585
+ @config = config
586
+ @options = opt
587
+ @srcdir = File.expand_path(srcroot)
588
+ @objdir = File.expand_path(objroot)
589
+ @currdir = '.'
590
+ end
591
+
592
+ def inspect
593
+ "#<#{self.class} #{File.basename(@srcdir)}>"
594
+ end
595
+
596
+ #
597
+ # Hook Script API bases
598
+ #
599
+
600
+ def srcdir_root
601
+ @srcdir
602
+ end
603
+
604
+ def objdir_root
605
+ @objdir
606
+ end
607
+
608
+ def relpath
609
+ @currdir
610
+ end
611
+
612
+ #
613
+ # configs/options
614
+ #
615
+
616
+ def no_harm?
617
+ @options['no-harm']
618
+ end
619
+
620
+ def verbose?
621
+ @options['verbose']
622
+ end
623
+
624
+ def verbose_off
625
+ begin
626
+ save, @options['verbose'] = @options['verbose'], false
627
+ yield
628
+ ensure
629
+ @options['verbose'] = save
630
+ end
631
+ end
632
+
633
+ #
634
+ # TASK config
635
+ #
636
+
637
+ def exec_config
638
+ exec_task_traverse 'config'
639
+ end
640
+
641
+ def config_dir_bin(rel)
642
+ end
643
+
644
+ def config_dir_lib(rel)
645
+ end
646
+
647
+ def config_dir_ext(rel)
648
+ extconf if extdir?(curr_srcdir())
649
+ end
650
+
651
+ def extconf
652
+ opt = @options['config-opt'].join(' ')
653
+ command "#{config('ruby-prog')} #{curr_srcdir()}/extconf.rb #{opt}"
654
+ end
655
+
656
+ def config_dir_data(rel)
657
+ end
658
+
659
+ #
660
+ # TASK setup
661
+ #
662
+
663
+ def exec_setup
664
+ exec_task_traverse 'setup'
665
+ end
666
+
667
+ def setup_dir_bin(rel)
668
+ all_files_in(curr_srcdir()).each do |fname|
669
+ adjust_shebang "#{curr_srcdir()}/#{fname}"
670
+ end
671
+ end
672
+
673
+ # modify: #!/usr/bin/ruby
674
+ # modify: #! /usr/bin/ruby
675
+ # modify: #!ruby
676
+ # not modify: #!/usr/bin/env ruby
677
+ SHEBANG_RE = /\A\#!\s*\S*ruby\S*/
678
+
679
+ def adjust_shebang(path)
680
+ return if no_harm?
681
+
682
+ tmpfile = File.basename(path) + '.tmp'
683
+ begin
684
+ File.open(path) {|r|
685
+ File.open(tmpfile, 'w') {|w|
686
+ first = r.gets
687
+ return unless SHEBANG_RE =~ first
688
+
689
+ $stderr.puts "adjusting shebang: #{File.basename path}" if verbose?
690
+ w.print first.sub(SHEBANG_RE, '#!' + config('ruby-path'))
691
+ w.write r.read
692
+ }
693
+ }
694
+ move_file tmpfile, File.basename(path)
695
+ ensure
696
+ File.unlink tmpfile if File.exist?(tmpfile)
697
+ end
698
+ end
699
+
700
+ def setup_dir_lib(rel)
701
+ end
702
+
703
+ def setup_dir_ext(rel)
704
+ make if extdir?(curr_srcdir())
705
+ end
706
+
707
+ def setup_dir_data(rel)
708
+ end
709
+
710
+ #
711
+ # TASK install
712
+ #
713
+
714
+ def exec_install
715
+ exec_task_traverse 'install'
716
+ end
717
+
718
+ def install_dir_bin(rel)
719
+ install_files collect_filenames_auto(), config('bin-dir') + '/' + rel, 0755
720
+ end
721
+
722
+ def install_dir_lib(rel)
723
+ install_files ruby_scripts(), config('rb-dir') + '/' + rel, 0644
724
+ end
725
+
726
+ def install_dir_ext(rel)
727
+ return unless extdir?(curr_srcdir())
728
+ install_files ruby_extentions('.'),
729
+ config('so-dir') + '/' + File.dirname(rel),
730
+ 0555
731
+ end
732
+
733
+ def install_dir_data(rel)
734
+ install_files collect_filenames_auto(), config('data-dir') + '/' + rel, 0644
735
+ end
736
+
737
+ def install_files(list, dest, mode)
738
+ mkdir_p dest, @options['install-prefix']
739
+ list.each do |fname|
740
+ install fname, dest, mode, @options['install-prefix']
741
+ end
742
+ end
743
+
744
+ def ruby_scripts
745
+ collect_filenames_auto().select {|n| /\.rb\z/ =~ n }
746
+ end
747
+
748
+ # picked up many entries from cvs-1.11.1/src/ignore.c
749
+ reject_patterns = %w(
750
+ core RCSLOG tags TAGS .make.state
751
+ .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb
752
+ *~ *.old *.bak *.BAK *.orig *.rej _$* *$
753
+
754
+ *.org *.in .*
755
+ )
756
+ mapping = {
757
+ '.' => '\.',
758
+ '$' => '\$',
759
+ '#' => '\#',
760
+ '*' => '.*'
761
+ }
762
+ REJECT_PATTERNS = Regexp.new('\A(?:' +
763
+ reject_patterns.map {|pat|
764
+ pat.gsub(/[\.\$\#\*]/) {|ch| mapping[ch] }
765
+ }.join('|') +
766
+ ')\z')
767
+
768
+ def collect_filenames_auto
769
+ mapdir((existfiles() - hookfiles()).reject {|fname|
770
+ REJECT_PATTERNS =~ fname
771
+ })
772
+ end
773
+
774
+ def existfiles
775
+ all_files_in(curr_srcdir()) | all_files_in('.')
776
+ end
777
+
778
+ def hookfiles
779
+ %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt|
780
+ %w( config setup install clean ).map {|t| sprintf(fmt, t) }
781
+ }.flatten
782
+ end
783
+
784
+ def mapdir(filelist)
785
+ filelist.map {|fname|
786
+ if File.exist?(fname) # objdir
787
+ fname
788
+ else # srcdir
789
+ File.join(curr_srcdir(), fname)
790
+ end
791
+ }
792
+ end
793
+
794
+ def ruby_extentions(dir)
795
+ _ruby_extentions(dir) or
796
+ raise InstallError, "no ruby extention exists: 'ruby #{$0} setup' first"
797
+ end
798
+
799
+ DLEXT = /\.#{ ::Config::CONFIG['DLEXT'] }\z/
800
+
801
+ def _ruby_extentions(dir)
802
+ Dir.open(dir) {|d|
803
+ return d.select {|fname| DLEXT =~ fname }
804
+ }
805
+ end
806
+
807
+ #
808
+ # TASK clean
809
+ #
810
+
811
+ def exec_clean
812
+ exec_task_traverse 'clean'
813
+ rm_f 'config.save'
814
+ rm_f 'InstalledFiles'
815
+ end
816
+
817
+ def clean_dir_bin(rel)
818
+ end
819
+
820
+ def clean_dir_lib(rel)
821
+ end
822
+
823
+ def clean_dir_ext(rel)
824
+ return unless extdir?(curr_srcdir())
825
+ make 'clean' if File.file?('Makefile')
826
+ end
827
+
828
+ def clean_dir_data(rel)
829
+ end
830
+
831
+ #
832
+ # TASK distclean
833
+ #
834
+
835
+ def exec_distclean
836
+ exec_task_traverse 'distclean'
837
+ rm_f 'config.save'
838
+ rm_f 'InstalledFiles'
839
+ end
840
+
841
+ def distclean_dir_bin(rel)
842
+ end
843
+
844
+ def distclean_dir_lib(rel)
845
+ end
846
+
847
+ def distclean_dir_ext(rel)
848
+ return unless extdir?(curr_srcdir())
849
+ make 'distclean' if File.file?('Makefile')
850
+ end
851
+
852
+ #
853
+ # lib
854
+ #
855
+
856
+ def exec_task_traverse(task)
857
+ run_hook "pre-#{task}"
858
+ FILETYPES.each do |type|
859
+ if config('without-ext') == 'yes' and type == 'ext'
860
+ $stderr.puts 'skipping ext/* by user option' if verbose?
861
+ next
862
+ end
863
+ traverse task, type, "#{task}_dir_#{type}"
864
+ end
865
+ run_hook "post-#{task}"
866
+ end
867
+
868
+ def traverse(task, rel, mid)
869
+ dive_into(rel) {
870
+ run_hook "pre-#{task}"
871
+ __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '')
872
+ all_dirs_in(curr_srcdir()).each do |d|
873
+ traverse task, "#{rel}/#{d}", mid
874
+ end
875
+ run_hook "post-#{task}"
876
+ }
877
+ end
878
+
879
+ def dive_into(rel)
880
+ return unless File.dir?("#{@srcdir}/#{rel}")
881
+
882
+ dir = File.basename(rel)
883
+ Dir.mkdir dir unless File.dir?(dir)
884
+ prevdir = Dir.pwd
885
+ Dir.chdir dir
886
+ $stderr.puts '---> ' + rel if verbose?
887
+ @currdir = rel
888
+ yield
889
+ Dir.chdir prevdir
890
+ $stderr.puts '<--- ' + rel if verbose?
891
+ @currdir = File.dirname(rel)
892
+ end
893
+
894
+ end
895
+
896
+ #
897
+ # toplevel.rb
898
+ #
899
+
900
+ class ToplevelInstaller
901
+
902
+ Version = '3.2.2'
903
+ Copyright = 'Copyright (c) 2000-2003 Minero Aoki'
904
+
905
+ TASKS = [
906
+ [ 'config', 'saves your configurations' ],
907
+ [ 'show', 'shows current configuration' ],
908
+ [ 'setup', 'compiles ruby extentions and others' ],
909
+ [ 'install', 'installs files' ],
910
+ [ 'clean', "does `make clean' for each extention" ],
911
+ [ 'distclean',"does `make distclean' for each extention" ]
912
+ ]
913
+
914
+ def ToplevelInstaller.invoke
915
+ instance().invoke
916
+ end
917
+
918
+ @singleton = nil
919
+
920
+ def ToplevelInstaller.instance
921
+ @singleton ||= new(File.dirname($0))
922
+ @singleton
923
+ end
924
+
925
+ include MetaConfigAPI
926
+
927
+ def initialize(ardir_root)
928
+ @config = nil
929
+ @options = { 'verbose' => true }
930
+ @ardir = File.expand_path(ardir_root)
931
+ end
932
+
933
+ def inspect
934
+ "#<#{self.class} #{__id__()}>"
935
+ end
936
+
937
+ def invoke
938
+ run_metaconfigs
939
+ task = parsearg_global()
940
+ @config = load_config(task)
941
+ __send__ "parsearg_#{task}"
942
+ init_installers
943
+ __send__ "exec_#{task}"
944
+ end
945
+
946
+ def run_metaconfigs
947
+ eval_file_ifexist "#{@ardir}/metaconfig"
948
+ end
949
+
950
+ def load_config(task)
951
+ case task
952
+ when 'config'
953
+ ConfigTable.new
954
+ when 'clean', 'distclean'
955
+ if File.exist?('config.save')
956
+ then ConfigTable.load
957
+ else ConfigTable.new
958
+ end
959
+ else
960
+ ConfigTable.load
961
+ end
962
+ end
963
+
964
+ def init_installers
965
+ @installer = Installer.new(@config, @options, @ardir, File.expand_path('.'))
966
+ end
967
+
968
+ #
969
+ # Hook Script API bases
970
+ #
971
+
972
+ def srcdir_root
973
+ @ardir
974
+ end
975
+
976
+ def objdir_root
977
+ '.'
978
+ end
979
+
980
+ def relpath
981
+ '.'
982
+ end
983
+
984
+ #
985
+ # Option Parsing
986
+ #
987
+
988
+ def parsearg_global
989
+ valid_task = /\A(?:#{TASKS.map {|task,desc| task }.join '|'})\z/
990
+
991
+ while arg = ARGV.shift
992
+ case arg
993
+ when /\A\w+\z/
994
+ raise InstallError, "invalid task: #{arg}" unless valid_task =~ arg
995
+ return arg
996
+
997
+ when '-q', '--quiet'
998
+ @options['verbose'] = false
999
+
1000
+ when '--verbose'
1001
+ @options['verbose'] = true
1002
+
1003
+ when '-h', '--help'
1004
+ print_usage $stdout
1005
+ exit 0
1006
+
1007
+ when '-v', '--version'
1008
+ puts "#{File.basename($0)} version #{Version}"
1009
+ exit 0
1010
+
1011
+ when '--copyright'
1012
+ puts Copyright
1013
+ exit 0
1014
+
1015
+ else
1016
+ raise InstallError, "unknown global option '#{arg}'"
1017
+ end
1018
+ end
1019
+
1020
+ raise InstallError, <<EOS
1021
+ No task or global option given.
1022
+ Typical installation procedure is:
1023
+ $ ruby #{File.basename($0)} config
1024
+ $ ruby #{File.basename($0)} setup
1025
+ # ruby #{File.basename($0)} install (may require root privilege)
1026
+ EOS
1027
+ end
1028
+
1029
+ def parsearg_no_options
1030
+ unless ARGV.empty? then
1031
+ raise InstallError, "#{task}: unknown options: #{ARGV.join ' '}"
1032
+ end
1033
+ end
1034
+
1035
+ alias parsearg_show parsearg_no_options
1036
+ alias parsearg_setup parsearg_no_options
1037
+ alias parsearg_clean parsearg_no_options
1038
+ alias parsearg_distclean parsearg_no_options
1039
+
1040
+ def parsearg_config
1041
+ re = /\A--(#{ConfigTable.keys.join '|'})(?:=(.*))?\z/
1042
+ @options['config-opt'] = []
1043
+
1044
+ while i = ARGV.shift
1045
+ if /\A--?\z/ =~ i
1046
+ @options['config-opt'] = ARGV.dup
1047
+ break
1048
+ end
1049
+ m = re.match(i) or raise InstallError, "config: unknown option #{i}"
1050
+ name, value = m.to_a[1,2]
1051
+ if value then
1052
+ if ConfigTable.bool_config?(name) then
1053
+ unless /\A(y(es)?|n(o)?|t(rue)?|f(alse))\z/i =~ value
1054
+ raise InstallError, "config: --#{name} allows only yes/no for argument"
1055
+ end
1056
+ value = (/\Ay(es)?|\At(rue)/i =~ value) ? 'yes' : 'no'
1057
+ end
1058
+ else
1059
+ unless ConfigTable.bool_config?(name)
1060
+ raise InstallError, "config: --#{name} requires argument"
1061
+ end
1062
+ value = 'yes'
1063
+ end
1064
+ @config[name] = value
1065
+ end
1066
+ end
1067
+
1068
+ def parsearg_install
1069
+ @options['no-harm'] = false
1070
+ @options['install-prefix'] = ''
1071
+ while a = ARGV.shift
1072
+ case a
1073
+ when /\A--no-harm\z/
1074
+ @options['no-harm'] = true
1075
+ when /\A--prefix=(.*)\z/
1076
+ path = $1
1077
+ path = File.expand_path(path) unless path[0,1] == '/'
1078
+ @options['install-prefix'] = path
1079
+ else
1080
+ raise InstallError, "install: unknown option #{a}"
1081
+ end
1082
+ end
1083
+ end
1084
+
1085
+ def print_usage(out)
1086
+ out.puts 'Typical Installation Procedure:'
1087
+ out.puts " $ ruby #{File.basename $0} config"
1088
+ out.puts " $ ruby #{File.basename $0} setup"
1089
+ out.puts " # ruby #{File.basename $0} install (may require root privilege)"
1090
+ out.puts
1091
+ out.puts 'Detailed Usage:'
1092
+ out.puts " ruby #{File.basename $0} <global option>"
1093
+ out.puts " ruby #{File.basename $0} [<global options>] <task> [<task options>]"
1094
+
1095
+ fmt = " %-20s %s\n"
1096
+ out.puts
1097
+ out.puts 'Global options:'
1098
+ out.printf fmt, '-q,--quiet', 'suppress message outputs'
1099
+ out.printf fmt, ' --verbose', 'output messages verbosely'
1100
+ out.printf fmt, '-h,--help', 'print this message'
1101
+ out.printf fmt, '-v,--version', 'print version and quit'
1102
+ out.printf fmt, ' --copyright', 'print copyright and quit'
1103
+
1104
+ out.puts
1105
+ out.puts 'Tasks:'
1106
+ TASKS.each do |name, desc|
1107
+ out.printf " %-10s %s\n", name, desc
1108
+ end
1109
+
1110
+ out.puts
1111
+ out.puts 'Options for config:'
1112
+ ConfigTable.each_definition do |name, (default, arg, desc, default2)|
1113
+ out.printf " %-20s %s [%s]\n",
1114
+ '--'+ name + (ConfigTable.bool_config?(name) ? '' : '='+arg),
1115
+ desc,
1116
+ default2 || default
1117
+ end
1118
+ out.printf " %-20s %s [%s]\n",
1119
+ '--rbconfig=path', 'your rbconfig.rb to load', "running ruby's"
1120
+
1121
+ out.puts
1122
+ out.puts 'Options for install:'
1123
+ out.printf " %-20s %s [%s]\n",
1124
+ '--no-harm', 'only display what to do if given', 'off'
1125
+ out.printf " %-20s %s [%s]\n",
1126
+ '--prefix', 'install path prefix', '$prefix'
1127
+
1128
+ out.puts
1129
+ end
1130
+
1131
+ #
1132
+ # Task Handlers
1133
+ #
1134
+
1135
+ def exec_config
1136
+ @installer.exec_config
1137
+ @config.save # must be final
1138
+ end
1139
+
1140
+ def exec_setup
1141
+ @installer.exec_setup
1142
+ end
1143
+
1144
+ def exec_install
1145
+ @installer.exec_install
1146
+ end
1147
+
1148
+ def exec_show
1149
+ ConfigTable.each_name do |k|
1150
+ v = @config.get_raw(k)
1151
+ if not v or v.empty?
1152
+ v = '(not specified)'
1153
+ end
1154
+ printf "%-10s %s\n", k, v
1155
+ end
1156
+ end
1157
+
1158
+ def exec_clean
1159
+ @installer.exec_clean
1160
+ end
1161
+
1162
+ def exec_distclean
1163
+ @installer.exec_distclean
1164
+ end
1165
+
1166
+ end
1167
+
1168
+ class ToplevelInstallerMulti < ToplevelInstaller
1169
+
1170
+ include HookUtils
1171
+ include HookScriptAPI
1172
+ include FileOperations
1173
+
1174
+ def initialize(ardir)
1175
+ super
1176
+ @packages = all_dirs_in("#{@ardir}/packages")
1177
+ raise 'no package exists' if @packages.empty?
1178
+ end
1179
+
1180
+ def run_metaconfigs
1181
+ eval_file_ifexist "#{@ardir}/metaconfig"
1182
+ @packages.each do |name|
1183
+ eval_file_ifexist "#{@ardir}/packages/#{name}/metaconfig"
1184
+ end
1185
+ end
1186
+
1187
+ def init_installers
1188
+ @installers = {}
1189
+ @packages.each do |pack|
1190
+ @installers[pack] = Installer.new(@config, @options,
1191
+ "#{@ardir}/packages/#{pack}",
1192
+ "packages/#{pack}")
1193
+ end
1194
+
1195
+ with = extract_selection(config('with'))
1196
+ without = extract_selection(config('without'))
1197
+ @selected = @installers.keys.select {|name|
1198
+ (with.empty? or with.include?(name)) \
1199
+ and not without.include?(name)
1200
+ }
1201
+ end
1202
+
1203
+ def extract_selection(list)
1204
+ a = list.split(/,/)
1205
+ a.each do |name|
1206
+ unless @installers.key?(name)
1207
+ raise InstallError, "no such package: #{name}"
1208
+ end
1209
+ end
1210
+ a
1211
+ end
1212
+
1213
+ def print_usage(f)
1214
+ super
1215
+ f.puts 'Inluded packages:'
1216
+ f.puts ' ' + @packages.sort.join(' ')
1217
+ f.puts
1218
+ end
1219
+
1220
+ #
1221
+ # multi-package metaconfig API
1222
+ #
1223
+
1224
+ attr_reader :packages
1225
+
1226
+ def declare_packages(list)
1227
+ raise 'package list is empty' if list.empty?
1228
+ list.each do |name|
1229
+ unless File.dir?("#{@ardir}/packages/#{name}")
1230
+ raise "directory packages/#{name} does not exist"
1231
+ end
1232
+ end
1233
+ @packages = list
1234
+ end
1235
+
1236
+ #
1237
+ # Task Handlers
1238
+ #
1239
+
1240
+ def exec_config
1241
+ run_hook 'pre-config'
1242
+ each_selected_installers {|inst| inst.exec_config }
1243
+ run_hook 'post-config'
1244
+ @config.save # must be final
1245
+ end
1246
+
1247
+ def exec_setup
1248
+ run_hook 'pre-setup'
1249
+ each_selected_installers {|inst| inst.exec_setup }
1250
+ run_hook 'post-setup'
1251
+ end
1252
+
1253
+ def exec_install
1254
+ run_hook 'pre-install'
1255
+ each_selected_installers {|inst| inst.exec_install }
1256
+ run_hook 'post-install'
1257
+ end
1258
+
1259
+ def exec_clean
1260
+ rm_f 'config.save'
1261
+ run_hook 'pre-clean'
1262
+ each_selected_installers {|inst| inst.exec_clean }
1263
+ run_hook 'post-clean'
1264
+ end
1265
+
1266
+ def exec_distclean
1267
+ rm_f 'config.save'
1268
+ run_hook 'pre-distclean'
1269
+ each_selected_installers {|inst| inst.exec_distclean }
1270
+ run_hook 'post-distclean'
1271
+ end
1272
+
1273
+ #
1274
+ # lib
1275
+ #
1276
+
1277
+ def each_selected_installers
1278
+ Dir.mkdir 'packages' unless File.dir?('packages')
1279
+ @selected.each do |pack|
1280
+ $stderr.puts "Processing the package `#{pack}' ..." if @options['verbose']
1281
+ Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}")
1282
+ Dir.chdir "packages/#{pack}"
1283
+ yield @installers[pack]
1284
+ Dir.chdir '../..'
1285
+ end
1286
+ end
1287
+
1288
+ def verbose?
1289
+ @options['verbose']
1290
+ end
1291
+
1292
+ def no_harm?
1293
+ @options['no-harm']
1294
+ end
1295
+
1296
+ end
1297
+
1298
+ if $0 == __FILE__
1299
+ begin
1300
+ if multipackage_install?
1301
+ ToplevelInstallerMulti.invoke
1302
+ else
1303
+ ToplevelInstaller.invoke
1304
+ end
1305
+ rescue
1306
+ raise if $DEBUG
1307
+ $stderr.puts $!.message
1308
+ $stderr.puts "Try 'ruby #{$0} --help' for detailed usage."
1309
+ exit 1
1310
+ end
1311
+ end
1312
+