packetfu 1.1.2 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/.gitignore +3 -0
  2. data/INSTALL.rdoc +40 -0
  3. data/LICENSE.txt +25 -0
  4. data/examples/100kpackets.rb +41 -0
  5. data/examples/ackscan.rb +38 -0
  6. data/examples/arp.rb +60 -0
  7. data/examples/arphood.rb +59 -0
  8. data/examples/dissect_thinger.rb +22 -0
  9. data/examples/ethernet.rb +10 -0
  10. data/examples/examples.rb +3 -0
  11. data/examples/ids.rb +4 -0
  12. data/examples/idsv2.rb +6 -0
  13. data/examples/new-simple-stats.rb +52 -0
  14. data/examples/oui.txt +84177 -0
  15. data/examples/packetfu-shell.rb +113 -0
  16. data/examples/simple-sniffer.rb +40 -0
  17. data/examples/simple-stats.rb +50 -0
  18. data/examples/slammer.rb +33 -0
  19. data/examples/uniqpcap.rb +15 -0
  20. data/lib/packetfu.rb +147 -0
  21. data/lib/packetfu/capture.rb +169 -0
  22. data/lib/packetfu/config.rb +58 -0
  23. data/lib/packetfu/inject.rb +65 -0
  24. data/lib/packetfu/packet.rb +533 -0
  25. data/lib/packetfu/pcap.rb +594 -0
  26. data/lib/packetfu/protos/arp.rb +268 -0
  27. data/lib/packetfu/protos/eth.rb +296 -0
  28. data/lib/packetfu/protos/hsrp.rb +206 -0
  29. data/lib/packetfu/protos/icmp.rb +179 -0
  30. data/lib/packetfu/protos/invalid.rb +55 -0
  31. data/lib/packetfu/protos/ip.rb +378 -0
  32. data/lib/packetfu/protos/ipv6.rb +250 -0
  33. data/lib/packetfu/protos/tcp.rb +1127 -0
  34. data/lib/packetfu/protos/udp.rb +240 -0
  35. data/lib/packetfu/structfu.rb +294 -0
  36. data/lib/packetfu/utils.rb +194 -0
  37. data/lib/packetfu/version.rb +50 -0
  38. data/packetfu.gemspec +21 -0
  39. data/setup.rb +1586 -0
  40. data/test/all_tests.rb +41 -0
  41. data/test/ethpacket_spec.rb +74 -0
  42. data/test/packet_spec.rb +73 -0
  43. data/test/packet_subclasses_spec.rb +13 -0
  44. data/test/packetfu_spec.rb +90 -0
  45. data/test/ptest.rb +16 -0
  46. data/test/sample-ipv6.pcap +0 -0
  47. data/test/sample.pcap +0 -0
  48. data/test/sample2.pcap +0 -0
  49. data/test/sample_hsrp_pcapr.cap +0 -0
  50. data/test/structfu_spec.rb +335 -0
  51. data/test/tcp_spec.rb +101 -0
  52. data/test/test_arp.rb +135 -0
  53. data/test/test_eth.rb +91 -0
  54. data/test/test_hsrp.rb +20 -0
  55. data/test/test_icmp.rb +54 -0
  56. data/test/test_inject.rb +31 -0
  57. data/test/test_invalid.rb +28 -0
  58. data/test/test_ip.rb +69 -0
  59. data/test/test_ip6.rb +68 -0
  60. data/test/test_octets.rb +37 -0
  61. data/test/test_packet.rb +174 -0
  62. data/test/test_pcap.rb +209 -0
  63. data/test/test_structfu.rb +112 -0
  64. data/test/test_tcp.rb +327 -0
  65. data/test/test_udp.rb +73 -0
  66. data/test/vlan-pcapr.cap +0 -0
  67. metadata +85 -6
@@ -0,0 +1,194 @@
1
+ require 'singleton'
2
+ module PacketFu
3
+
4
+ # Utils is a collection of various and sundry network utilities that are useful for packet
5
+ # manipulation.
6
+ class Utils
7
+
8
+ # Returns the MAC address of an IP address, or nil if it's not responsive to arp. Takes
9
+ # a dotted-octect notation of the target IP address, as well as a number of parameters:
10
+ #
11
+ # === Parameters
12
+ # :eth_saddr
13
+ # Source MAC address. Defaults to "00:00:00:00:00:00".
14
+ # :ip_saddr
15
+ # Source IP address. Defaults to "0.0.0.0"
16
+ # :flavor
17
+ # The flavor of the ARP request. Defaults to :none.
18
+ # :timeout
19
+ # Timeout in seconds. Defaults to 3.
20
+ #
21
+ # === Example
22
+ # PacketFu::Utils::arp("192.168.1.1") #=> "00:18:39:01:33:70"
23
+ # PacketFu::Utils::arp("192.168.1.1", :timeout => 5, :flavor => :hp_deskjet)
24
+ #
25
+ # === Warning
26
+ #
27
+ # It goes without saying, spewing forged ARP packets on your network is a great way to really
28
+ # irritate your co-workers.
29
+ def self.arp(target_ip,args={})
30
+ iface = args[:iface] || :eth0
31
+ args[:config] ||= whoami?(:iface => iface)
32
+ arp_pkt = PacketFu::ARPPacket.new(:flavor => (args[:flavor] || :none), :config => args[:config])
33
+ arp_pkt.eth_daddr = "ff:ff:ff:ff:ff:ff"
34
+ arp_pkt.arp_daddr_mac = "00:00:00:00:00:00"
35
+ arp_pkt.arp_daddr_ip = target_ip
36
+ # Stick the Capture object in its own thread.
37
+ cap_thread = Thread.new do
38
+ target_mac = nil
39
+ cap = PacketFu::Capture.new(:iface => iface, :start => true,
40
+ :filter => "arp src #{target_ip} and ether dst #{arp_pkt.eth_saddr}")
41
+ arp_pkt.to_w(iface) # Shorthand for sending single packets to the default interface.
42
+ timeout = 0
43
+ while target_mac.nil? && timeout <= (args[:timeout] || 3)
44
+ if cap.save > 0
45
+ arp_response = PacketFu::Packet.parse(cap.array[0])
46
+ target_mac = arp_response.arp_saddr_mac if arp_response.arp_saddr_ip = target_ip
47
+ end
48
+ timeout += 0.1
49
+ sleep 0.1 # Check for a response ten times per second.
50
+ end
51
+ target_mac
52
+ end # cap_thread
53
+ cap_thread.value
54
+ end
55
+
56
+ # Discovers the local IP and Ethernet address, which is useful for writing
57
+ # packets you expect to get a response to. Note, this is a noisy
58
+ # operation; a UDP packet is generated and dropped on to the default (or named)
59
+ # interface, and then captured (which means you need to be root to do this).
60
+ #
61
+ # whoami? returns a hash of :eth_saddr, :eth_src, :ip_saddr, :ip_src,
62
+ # :ip_src_bin, :eth_dst, and :eth_daddr (the last two are usually suitable
63
+ # for a gateway mac address). It's most useful as an argument to
64
+ # PacketFu::Config.new, or as an argument to the many Packet constructors.
65
+ #
66
+ # Note that if you have multiple interfaces with the same route (such as when
67
+ # wlan0 and eth0 are associated to the same network), the "first" one
68
+ # according to Pcap.lookupdev will be used, regardless of which :iface you
69
+ # pick.
70
+ #
71
+ # === Parameters
72
+ # :iface => "eth0"
73
+ # An interface to listen for packets on. Note that since we rely on the OS to send the probe packet,
74
+ # you will need to specify a target which will use this interface.
75
+ # :target => "1.2.3.4"
76
+ # A target IP address. By default, a packet will be sent to a random address in the 177/8 network.
77
+ # Since this network is IANA reserved (for now), this network should be handled by your default gateway
78
+ # and default interface.
79
+ def self.whoami?(args={})
80
+ unless args.kind_of? Hash
81
+ raise ArgumentError, "Argument to `whoami?' must be a Hash"
82
+ end
83
+ if args[:iface].to_s =~ /^lo/ # Linux loopback more or less. Need a switch for windows loopback, too.
84
+ dst_host = "127.0.0.1"
85
+ else
86
+ dst_host = (args[:target] || IPAddr.new((rand(16777216) + 2969567232), Socket::AF_INET).to_s)
87
+ end
88
+
89
+ dst_port = rand(0xffff-1024)+1024
90
+ msg = "PacketFu whoami? packet #{(Time.now.to_i + rand(0xffffff)+1)}"
91
+ iface = (args[:iface] || ENV['IFACE'] || Pcap.lookupdev || :lo ).to_s
92
+ cap = PacketFu::Capture.new(:iface => iface, :promisc => false, :start => true, :filter => "udp and dst host #{dst_host} and dst port #{dst_port}")
93
+ udp_sock = UDPSocket.new
94
+ udp_sock.send(msg,0,dst_host,dst_port)
95
+ udp_sock = nil
96
+ cap.save
97
+ pkt = Packet.parse(cap.array[0]) unless cap.save.zero?
98
+ timeout = 0
99
+ while timeout < 1 # Sometimes packet generation can be a little pokey.
100
+ if pkt
101
+ timeout = 1.1 # Cancel the timeout
102
+ if pkt.payload == msg
103
+ my_data = {
104
+ :iface => (args[:iface] || ENV['IFACE'] || Pcap.lookupdev || "lo").to_s,
105
+ :pcapfile => args[:pcapfile] || "/tmp/out.pcap",
106
+ :eth_saddr => pkt.eth_saddr,
107
+ :eth_src => pkt.eth_src.to_s,
108
+ :ip_saddr => pkt.ip_saddr,
109
+ :ip_src => pkt.ip_src,
110
+ :ip_src_bin => [pkt.ip_src].pack("N"),
111
+ :eth_dst => pkt.eth_dst.to_s,
112
+ :eth_daddr => pkt.eth_daddr
113
+ }
114
+ else raise SecurityError,
115
+ "whoami() packet doesn't match sent data. Something fishy's going on."
116
+ end
117
+ else
118
+ sleep 0.1; timeout += 0.1
119
+ cap.save
120
+ pkt = Packet.parse(cap.array[0]) unless cap.save.zero?
121
+ end
122
+ raise SocketError, "Didn't receive the whomi() packet, can't automatically configure." if !pkt
123
+ cap = nil
124
+ end
125
+ my_data
126
+ end
127
+
128
+ # This is a brute-force approach at trying to find a suitable interface with an IP address.
129
+ def self.lookupdev
130
+ # XXX cycle through eth0-9 and wlan0-9, and if a cap start throws a RuntimeErorr (and we're
131
+ # root), it's not a good interface. Boy, really ought to fix lookupdev directly with another
132
+ # method that returns an array rather than just the first candidate.
133
+ end
134
+
135
+ # Handles ifconfig for various (okay, one) platforms. Mac guys, fix this and submit a patch!
136
+ # Will have Windows done shortly.
137
+ #
138
+ # Takes an argument (either string or symbol) of the interface to look up, and
139
+ # returns a hash which contains at least the :iface element, and if configured,
140
+ # these additional elements:
141
+ #
142
+ # :eth_saddr # A human readable MAC address
143
+ # :eth_src # A packed MAC address
144
+ # :ip_saddr # A dotted-quad string IPv4 address
145
+ # :ip_src # A packed IPv4 address
146
+ # :ip4_obj # An IPAddr object with bitmask
147
+ # :ip6_saddr # A colon-delimited hex IPv6 address, with bitmask
148
+ # :ip6_obj # An IPAddr object with bitmask
149
+ #
150
+ # === Example
151
+ # PacketFu::Utils.ifconfig :wlan0 # Not associated yet
152
+ # #=> {:eth_saddr=>"00:1d:e0:73:9d:ff", :eth_src=>"\000\035\340s\235\377", :iface=>"wlan0"}
153
+ # PacketFu::Utils.ifconfig("eth0") # Takes 'eth0' as default
154
+ # #=> {:eth_saddr=>"00:1c:23:35:70:3b", :eth_src=>"\000\034#5p;", :ip_saddr=>"10.10.10.9", :ip4_obj=>#<IPAddr: IPv4:10.10.10.0/255.255.254.0>, :ip_src=>"\n\n\n\t", :iface=>"eth0", :ip6_saddr=>"fe80::21c:23ff:fe35:703b/64", :ip6_obj=>#<IPAddr: IPv6:fe80:0000:0000:0000:0000:0000:0000:0000/ffff:ffff:ffff:ffff:0000:0000:0000:0000>}
155
+ # PacketFu::Utils.ifconfig :lo
156
+ # #=> {:ip_saddr=>"127.0.0.1", :ip4_obj=>#<IPAddr: IPv4:127.0.0.0/255.0.0.0>, :ip_src=>"\177\000\000\001", :iface=>"lo", :ip6_saddr=>"::1/128", :ip6_obj=>#<IPAddr: IPv6:0000:0000:0000:0000:0000:0000:0000:0001/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>}
157
+ def self.ifconfig(iface='eth0')
158
+ ret = {}
159
+ iface = iface.to_s.scan(/[0-9A-Za-z]/).join # Sanitizing input, no spaces, semicolons, etc.
160
+ case RUBY_PLATFORM
161
+ when /linux/i
162
+ ifconfig_data = %x[ifconfig #{iface}]
163
+ if ifconfig_data =~ /#{iface}/i
164
+ ifconfig_data = ifconfig_data.split(/[\s]*\n[\s]*/)
165
+ else
166
+ raise ArgumentError, "Cannot ifconfig #{iface}"
167
+ end
168
+ real_iface = ifconfig_data.first
169
+ ret[:iface] = real_iface.split.first.downcase
170
+ if real_iface =~ /[\s]HWaddr[\s]+([0-9a-fA-F:]{17})/i
171
+ ret[:eth_saddr] = $1.downcase
172
+ ret[:eth_src] = EthHeader.mac2str(ret[:eth_saddr])
173
+ end
174
+ ifconfig_data.each do |s|
175
+ case s
176
+ when /inet addr:[\s]*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(.*Mask:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+))?/i
177
+ ret[:ip_saddr] = $1
178
+ ret[:ip_src] = [IPAddr.new($1).to_i].pack("N")
179
+ ret[:ip4_obj] = IPAddr.new($1)
180
+ ret[:ip4_obj] = ret[:ip4_obj].mask($3) if $3
181
+ when /inet6 addr:[\s]*([0-9a-fA-F:\x2f]+)/
182
+ ret[:ip6_saddr] = $1
183
+ ret[:ip6_obj] = IPAddr.new($1)
184
+ end
185
+ end
186
+ end # linux
187
+ ret
188
+ end
189
+
190
+ end
191
+
192
+ end
193
+
194
+ # vim: nowrap sw=2 sts=0 ts=2 ff=unix ft=ruby
@@ -0,0 +1,50 @@
1
+ module PacketFu
2
+
3
+ # Check the repo's for version release histories
4
+ VERSION = "1.1.3" # Replacing the empty and useless packetfu-1.1.2
5
+
6
+ # Returns PacketFu::VERSION
7
+ def self.version
8
+ VERSION
9
+ end
10
+
11
+ # Returns a version string in a binary format for easy comparisons.
12
+ def self.binarize_version(str)
13
+ if(str.respond_to?(:split) && str =~ /^[0-9]+(\.([0-9]+)(\.[0-9]+)?)?\..+$/)
14
+ bin_major,bin_minor,bin_teeny = str.split(/\x2e/).map {|x| x.to_i}
15
+ bin_version = (bin_major.to_i << 16) + (bin_minor.to_i << 8) + bin_teeny.to_i
16
+ else
17
+ raise ArgumentError, "Compare version malformed. Should be \x22x.y.z\x22"
18
+ end
19
+ end
20
+
21
+ # Returns true if the version is equal to or greater than the compare version.
22
+ # If the current version of PacketFu is "0.3.1" for example:
23
+ #
24
+ # PacketFu.at_least? "0" # => true
25
+ # PacketFu.at_least? "0.2.9" # => true
26
+ # PacketFu.at_least? "0.3" # => true
27
+ # PacketFu.at_least? "1" # => true after 1.0's release
28
+ # PacketFu.at_least? "1.12" # => false
29
+ # PacketFu.at_least? "2" # => false
30
+ def self.at_least?(str)
31
+ this_version = binarize_version(self.version)
32
+ ask_version = binarize_version(str)
33
+ this_version >= ask_version
34
+ end
35
+
36
+ # Returns true if the current version is older than the compare version.
37
+ def self.older_than?(str)
38
+ return false if str == self.version
39
+ this_version = binarize_version(self.version)
40
+ ask_version = binarize_version(str)
41
+ this_version < ask_version
42
+ end
43
+
44
+ # Returns true if the current version is newer than the compare version.
45
+ def self.newer_than?(str)
46
+ return false if str == self.version
47
+ !self.older_than?(str)
48
+ end
49
+
50
+ end
data/packetfu.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ require 'rake'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'packetfu'
5
+ s.version = '1.1.3'
6
+ s.authors = ['Tod Beardsley']
7
+ s.email = 'todb@planb-security.net'
8
+ s.summary = 'PacketFu is a mid-level packet manipulation library.'
9
+ s.homepage = 'https://github.com/todb/packetfu'
10
+ s.description = %q{PacketFu is a mid-level packet manipulation library for Ruby. With it, users can read, parse, and write network packets with the level of ease and fun they expect from Ruby. Note that this gem does not automatically require pcaprub, since users may install pcaprub through non-gem means.}
11
+ s.files = `git ls-files`.split($/)
12
+ s.license = 'BSD'
13
+
14
+ s.add_development_dependency('pcaprub', '>= 0.9.2')
15
+ s.add_development_dependency('rspec', '>= 2.6.2')
16
+ s.add_development_dependency('sdoc', '>= 0.2.0')
17
+
18
+ s.extra_rdoc_files = %w[.document README.rdoc]
19
+ s.test_files = (s.files & Dir['test/test_*.rb'])
20
+ s.rubyforge_project = 'packetfu'
21
+ end
data/setup.rb ADDED
@@ -0,0 +1,1586 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # setup.rb
4
+ #
5
+ # Copyright (c) 2000-2005 Minero Aoki
6
+ #
7
+ # This program is free software.
8
+ # You can distribute/modify this program under the terms of
9
+ # the GNU LGPL, Lesser General Public License version 2.1.
10
+ #
11
+
12
+ unless Enumerable.method_defined?(:map) # Ruby 1.4.6
13
+ module Enumerable
14
+ alias map collect
15
+ end
16
+ end
17
+
18
+ unless File.respond_to?(:read) # Ruby 1.6
19
+ def File.read(fname)
20
+ open(fname) {|f|
21
+ return f.read
22
+ }
23
+ end
24
+ end
25
+
26
+ unless Errno.const_defined?(:ENOTEMPTY) # Windows?
27
+ module Errno
28
+ class ENOTEMPTY
29
+ # We do not raise this exception, implementation is not needed.
30
+ end
31
+ end
32
+ end
33
+
34
+ def File.binread(fname)
35
+ open(fname, 'rb') {|f|
36
+ return f.read
37
+ }
38
+ end
39
+
40
+ # for corrupted Windows' stat(2)
41
+ def File.dir?(path)
42
+ File.directory?((path[-1,1] == '/') ? path : path + '/')
43
+ end
44
+
45
+
46
+ class ConfigTable
47
+
48
+ include Enumerable
49
+
50
+ def initialize(rbconfig)
51
+ @rbconfig = rbconfig
52
+ @items = []
53
+ @table = {}
54
+ # options
55
+ @install_prefix = nil
56
+ @config_opt = nil
57
+ @verbose = true
58
+ @no_harm = false
59
+ end
60
+
61
+ attr_accessor :install_prefix
62
+ attr_accessor :config_opt
63
+
64
+ attr_writer :verbose
65
+
66
+ def verbose?
67
+ @verbose
68
+ end
69
+
70
+ attr_writer :no_harm
71
+
72
+ def no_harm?
73
+ @no_harm
74
+ end
75
+
76
+ def [](key)
77
+ lookup(key).resolve(self)
78
+ end
79
+
80
+ def []=(key, val)
81
+ lookup(key).set val
82
+ end
83
+
84
+ def names
85
+ @items.map {|i| i.name }
86
+ end
87
+
88
+ def each(&block)
89
+ @items.each(&block)
90
+ end
91
+
92
+ def key?(name)
93
+ @table.key?(name)
94
+ end
95
+
96
+ def lookup(name)
97
+ @table[name] or setup_rb_error "no such config item: #{name}"
98
+ end
99
+
100
+ def add(item)
101
+ @items.push item
102
+ @table[item.name] = item
103
+ end
104
+
105
+ def remove(name)
106
+ item = lookup(name)
107
+ @items.delete_if {|i| i.name == name }
108
+ @table.delete_if {|name, i| i.name == name }
109
+ item
110
+ end
111
+
112
+ def load_script(path, inst = nil)
113
+ if File.file?(path)
114
+ MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path
115
+ end
116
+ end
117
+
118
+ def savefile
119
+ '.config'
120
+ end
121
+
122
+ def load_savefile
123
+ begin
124
+ File.foreach(savefile()) do |line|
125
+ k, v = *line.split(/=/, 2)
126
+ self[k] = v.strip
127
+ end
128
+ rescue Errno::ENOENT
129
+ setup_rb_error $!.message + "\n#{File.basename($0)} config first"
130
+ end
131
+ end
132
+
133
+ def save
134
+ @items.each {|i| i.value }
135
+ File.open(savefile(), 'w') {|f|
136
+ @items.each do |i|
137
+ f.printf "%s=%s\n", i.name, i.value if i.value? and i.value
138
+ end
139
+ }
140
+ end
141
+
142
+ def load_standard_entries
143
+ standard_entries(@rbconfig).each do |ent|
144
+ add ent
145
+ end
146
+ end
147
+
148
+ def standard_entries(rbconfig)
149
+ c = rbconfig
150
+
151
+ rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT'])
152
+
153
+ major = c['MAJOR'].to_i
154
+ minor = c['MINOR'].to_i
155
+ teeny = c['TEENY'].to_i
156
+ version = "#{major}.#{minor}"
157
+
158
+ # ruby ver. >= 1.4.4?
159
+ newpath_p = ((major >= 2) or
160
+ ((major == 1) and
161
+ ((minor >= 5) or
162
+ ((minor == 4) and (teeny >= 4)))))
163
+
164
+ if c['rubylibdir']
165
+ # V > 1.6.3
166
+ libruby = "#{c['prefix']}/lib/ruby"
167
+ librubyver = c['rubylibdir']
168
+ librubyverarch = c['archdir']
169
+ siteruby = c['sitedir']
170
+ siterubyver = c['sitelibdir']
171
+ siterubyverarch = c['sitearchdir']
172
+ elsif newpath_p
173
+ # 1.4.4 <= V <= 1.6.3
174
+ libruby = "#{c['prefix']}/lib/ruby"
175
+ librubyver = "#{c['prefix']}/lib/ruby/#{version}"
176
+ librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
177
+ siteruby = c['sitedir']
178
+ siterubyver = "$siteruby/#{version}"
179
+ siterubyverarch = "$siterubyver/#{c['arch']}"
180
+ else
181
+ # V < 1.4.4
182
+ libruby = "#{c['prefix']}/lib/ruby"
183
+ librubyver = "#{c['prefix']}/lib/ruby/#{version}"
184
+ librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
185
+ siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby"
186
+ siterubyver = siteruby
187
+ siterubyverarch = "$siterubyver/#{c['arch']}"
188
+ end
189
+ parameterize = lambda {|path|
190
+ path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix')
191
+ }
192
+
193
+ if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg }
194
+ makeprog = arg.sub(/'/, '').split(/=/, 2)[1]
195
+ else
196
+ makeprog = 'make'
197
+ end
198
+
199
+ [
200
+ ExecItem.new('installdirs', 'std/site/home',
201
+ 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\
202
+ {|val, table|
203
+ case val
204
+ when 'std'
205
+ table['rbdir'] = '$librubyver'
206
+ table['sodir'] = '$librubyverarch'
207
+ when 'site'
208
+ table['rbdir'] = '$siterubyver'
209
+ table['sodir'] = '$siterubyverarch'
210
+ when 'home'
211
+ setup_rb_error '$HOME was not set' unless ENV['HOME']
212
+ table['prefix'] = ENV['HOME']
213
+ table['rbdir'] = '$libdir/ruby'
214
+ table['sodir'] = '$libdir/ruby'
215
+ end
216
+ },
217
+ PathItem.new('prefix', 'path', c['prefix'],
218
+ 'path prefix of target environment'),
219
+ PathItem.new('bindir', 'path', parameterize.call(c['bindir']),
220
+ 'the directory for commands'),
221
+ PathItem.new('libdir', 'path', parameterize.call(c['libdir']),
222
+ 'the directory for libraries'),
223
+ PathItem.new('datadir', 'path', parameterize.call(c['datadir']),
224
+ 'the directory for shared data'),
225
+ PathItem.new('mandir', 'path', parameterize.call(c['mandir']),
226
+ 'the directory for man pages'),
227
+ PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']),
228
+ 'the directory for system configuration files'),
229
+ PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']),
230
+ 'the directory for local state data'),
231
+ PathItem.new('libruby', 'path', libruby,
232
+ 'the directory for ruby libraries'),
233
+ PathItem.new('librubyver', 'path', librubyver,
234
+ 'the directory for standard ruby libraries'),
235
+ PathItem.new('librubyverarch', 'path', librubyverarch,
236
+ 'the directory for standard ruby extensions'),
237
+ PathItem.new('siteruby', 'path', siteruby,
238
+ 'the directory for version-independent aux ruby libraries'),
239
+ PathItem.new('siterubyver', 'path', siterubyver,
240
+ 'the directory for aux ruby libraries'),
241
+ PathItem.new('siterubyverarch', 'path', siterubyverarch,
242
+ 'the directory for aux ruby binaries'),
243
+ PathItem.new('rbdir', 'path', '$siterubyver',
244
+ 'the directory for ruby scripts'),
245
+ PathItem.new('sodir', 'path', '$siterubyverarch',
246
+ 'the directory for ruby extentions'),
247
+ PathItem.new('rubypath', 'path', rubypath,
248
+ 'the path to set to #! line'),
249
+ ProgramItem.new('rubyprog', 'name', rubypath,
250
+ 'the ruby program using for installation'),
251
+ ProgramItem.new('makeprog', 'name', makeprog,
252
+ 'the make program to compile ruby extentions'),
253
+ SelectItem.new('shebang', 'all/ruby/never', 'ruby',
254
+ 'shebang line (#!) editing mode'),
255
+ BoolItem.new('without-ext', 'yes/no', 'no',
256
+ 'does not compile/install ruby extentions')
257
+ ]
258
+ end
259
+ private :standard_entries
260
+
261
+ def load_multipackage_entries
262
+ multipackage_entries().each do |ent|
263
+ add ent
264
+ end
265
+ end
266
+
267
+ def multipackage_entries
268
+ [
269
+ PackageSelectionItem.new('with', 'name,name...', '', 'ALL',
270
+ 'package names that you want to install'),
271
+ PackageSelectionItem.new('without', 'name,name...', '', 'NONE',
272
+ 'package names that you do not want to install')
273
+ ]
274
+ end
275
+ private :multipackage_entries
276
+
277
+ ALIASES = {
278
+ 'std-ruby' => 'librubyver',
279
+ 'stdruby' => 'librubyver',
280
+ 'rubylibdir' => 'librubyver',
281
+ 'archdir' => 'librubyverarch',
282
+ 'site-ruby-common' => 'siteruby', # For backward compatibility
283
+ 'site-ruby' => 'siterubyver', # For backward compatibility
284
+ 'bin-dir' => 'bindir',
285
+ 'bin-dir' => 'bindir',
286
+ 'rb-dir' => 'rbdir',
287
+ 'so-dir' => 'sodir',
288
+ 'data-dir' => 'datadir',
289
+ 'ruby-path' => 'rubypath',
290
+ 'ruby-prog' => 'rubyprog',
291
+ 'ruby' => 'rubyprog',
292
+ 'make-prog' => 'makeprog',
293
+ 'make' => 'makeprog'
294
+ }
295
+
296
+ def fixup
297
+ ALIASES.each do |ali, name|
298
+ @table[ali] = @table[name]
299
+ end
300
+ @items.freeze
301
+ @table.freeze
302
+ @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/
303
+ end
304
+
305
+ def parse_opt(opt)
306
+ m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}"
307
+ m.to_a[1,2]
308
+ end
309
+
310
+ def dllext
311
+ @rbconfig['DLEXT']
312
+ end
313
+
314
+ def value_config?(name)
315
+ lookup(name).value?
316
+ end
317
+
318
+ class Item
319
+ def initialize(name, template, default, desc)
320
+ @name = name.freeze
321
+ @template = template
322
+ @value = default
323
+ @default = default
324
+ @description = desc
325
+ end
326
+
327
+ attr_reader :name
328
+ attr_reader :description
329
+
330
+ attr_accessor :default
331
+ alias help_default default
332
+
333
+ def help_opt
334
+ "--#{@name}=#{@template}"
335
+ end
336
+
337
+ def value?
338
+ true
339
+ end
340
+
341
+ def value
342
+ @value
343
+ end
344
+
345
+ def resolve(table)
346
+ @value.gsub(%r<\$([^/]+)>) { table[$1] }
347
+ end
348
+
349
+ def set(val)
350
+ @value = check(val)
351
+ end
352
+
353
+ private
354
+
355
+ def check(val)
356
+ setup_rb_error "config: --#{name} requires argument" unless val
357
+ val
358
+ end
359
+ end
360
+
361
+ class BoolItem < Item
362
+ def config_type
363
+ 'bool'
364
+ end
365
+
366
+ def help_opt
367
+ "--#{@name}"
368
+ end
369
+
370
+ private
371
+
372
+ def check(val)
373
+ return 'yes' unless val
374
+ case val
375
+ when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes'
376
+ when /\An(o)?\z/i, /\Af(alse)\z/i then 'no'
377
+ else
378
+ setup_rb_error "config: --#{@name} accepts only yes/no for argument"
379
+ end
380
+ end
381
+ end
382
+
383
+ class PathItem < Item
384
+ def config_type
385
+ 'path'
386
+ end
387
+
388
+ private
389
+
390
+ def check(path)
391
+ setup_rb_error "config: --#{@name} requires argument" unless path
392
+ path[0,1] == '$' ? path : File.expand_path(path)
393
+ end
394
+ end
395
+
396
+ class ProgramItem < Item
397
+ def config_type
398
+ 'program'
399
+ end
400
+ end
401
+
402
+ class SelectItem < Item
403
+ def initialize(name, selection, default, desc)
404
+ super
405
+ @ok = selection.split('/')
406
+ end
407
+
408
+ def config_type
409
+ 'select'
410
+ end
411
+
412
+ private
413
+
414
+ def check(val)
415
+ unless @ok.include?(val.strip)
416
+ setup_rb_error "config: use --#{@name}=#{@template} (#{val})"
417
+ end
418
+ val.strip
419
+ end
420
+ end
421
+
422
+ class ExecItem < Item
423
+ def initialize(name, selection, desc, &block)
424
+ super name, selection, nil, desc
425
+ @ok = selection.split('/')
426
+ @action = block
427
+ end
428
+
429
+ def config_type
430
+ 'exec'
431
+ end
432
+
433
+ def value?
434
+ false
435
+ end
436
+
437
+ def resolve(table)
438
+ setup_rb_error "$#{name()} wrongly used as option value"
439
+ end
440
+
441
+ undef set
442
+
443
+ def evaluate(val, table)
444
+ v = val.strip.downcase
445
+ unless @ok.include?(v)
446
+ setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})"
447
+ end
448
+ @action.call v, table
449
+ end
450
+ end
451
+
452
+ class PackageSelectionItem < Item
453
+ def initialize(name, template, default, help_default, desc)
454
+ super name, template, default, desc
455
+ @help_default = help_default
456
+ end
457
+
458
+ attr_reader :help_default
459
+
460
+ def config_type
461
+ 'package'
462
+ end
463
+
464
+ private
465
+
466
+ def check(val)
467
+ unless File.dir?("packages/#{val}")
468
+ setup_rb_error "config: no such package: #{val}"
469
+ end
470
+ val
471
+ end
472
+ end
473
+
474
+ class MetaConfigEnvironment
475
+ def initialize(config, installer)
476
+ @config = config
477
+ @installer = installer
478
+ end
479
+
480
+ def config_names
481
+ @config.names
482
+ end
483
+
484
+ def config?(name)
485
+ @config.key?(name)
486
+ end
487
+
488
+ def bool_config?(name)
489
+ @config.lookup(name).config_type == 'bool'
490
+ end
491
+
492
+ def path_config?(name)
493
+ @config.lookup(name).config_type == 'path'
494
+ end
495
+
496
+ def value_config?(name)
497
+ @config.lookup(name).config_type != 'exec'
498
+ end
499
+
500
+ def add_config(item)
501
+ @config.add item
502
+ end
503
+
504
+ def add_bool_config(name, default, desc)
505
+ @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc)
506
+ end
507
+
508
+ def add_path_config(name, default, desc)
509
+ @config.add PathItem.new(name, 'path', default, desc)
510
+ end
511
+
512
+ def set_config_default(name, default)
513
+ @config.lookup(name).default = default
514
+ end
515
+
516
+ def remove_config(name)
517
+ @config.remove(name)
518
+ end
519
+
520
+ # For only multipackage
521
+ def packages
522
+ raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer
523
+ @installer.packages
524
+ end
525
+
526
+ # For only multipackage
527
+ def declare_packages(list)
528
+ raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer
529
+ @installer.packages = list
530
+ end
531
+ end
532
+
533
+ end # class ConfigTable
534
+
535
+
536
+ # This module requires: #verbose?, #no_harm?
537
+ module FileOperations
538
+
539
+ def mkdir_p(dirname, prefix = nil)
540
+ dirname = prefix + File.expand_path(dirname) if prefix
541
+ $stderr.puts "mkdir -p #{dirname}" if verbose?
542
+ return if no_harm?
543
+
544
+ # Does not check '/', it's too abnormal.
545
+ dirs = File.expand_path(dirname).split(%r<(?=/)>)
546
+ if /\A[a-z]:\z/i =~ dirs[0]
547
+ disk = dirs.shift
548
+ dirs[0] = disk + dirs[0]
549
+ end
550
+ dirs.each_index do |idx|
551
+ path = dirs[0..idx].join('')
552
+ Dir.mkdir path unless File.dir?(path)
553
+ end
554
+ end
555
+
556
+ def rm_f(path)
557
+ $stderr.puts "rm -f #{path}" if verbose?
558
+ return if no_harm?
559
+ force_remove_file path
560
+ end
561
+
562
+ def rm_rf(path)
563
+ $stderr.puts "rm -rf #{path}" if verbose?
564
+ return if no_harm?
565
+ remove_tree path
566
+ end
567
+
568
+ def remove_tree(path)
569
+ if File.symlink?(path)
570
+ remove_file path
571
+ elsif File.dir?(path)
572
+ remove_tree0 path
573
+ else
574
+ force_remove_file path
575
+ end
576
+ end
577
+
578
+ def remove_tree0(path)
579
+ Dir.foreach(path) do |ent|
580
+ next if ent == '.'
581
+ next if ent == '..'
582
+ entpath = "#{path}/#{ent}"
583
+ if File.symlink?(entpath)
584
+ remove_file entpath
585
+ elsif File.dir?(entpath)
586
+ remove_tree0 entpath
587
+ else
588
+ force_remove_file entpath
589
+ end
590
+ end
591
+ begin
592
+ Dir.rmdir path
593
+ rescue Errno::ENOTEMPTY
594
+ # directory may not be empty
595
+ end
596
+ end
597
+
598
+ def move_file(src, dest)
599
+ force_remove_file dest
600
+ begin
601
+ File.rename src, dest
602
+ rescue
603
+ File.open(dest, 'wb') {|f|
604
+ f.write File.binread(src)
605
+ }
606
+ File.chmod File.stat(src).mode, dest
607
+ File.unlink src
608
+ end
609
+ end
610
+
611
+ def force_remove_file(path)
612
+ begin
613
+ remove_file path
614
+ rescue
615
+ end
616
+ end
617
+
618
+ def remove_file(path)
619
+ File.chmod 0777, path
620
+ File.unlink path
621
+ end
622
+
623
+ def install(from, dest, mode, prefix = nil)
624
+ $stderr.puts "install #{from} #{dest}" if verbose?
625
+ return if no_harm?
626
+
627
+ realdest = prefix ? prefix + File.expand_path(dest) : dest
628
+ realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest)
629
+ str = File.binread(from)
630
+ if diff?(str, realdest)
631
+ verbose_off {
632
+ rm_f realdest if File.exist?(realdest)
633
+ }
634
+ File.open(realdest, 'wb') {|f|
635
+ f.write str
636
+ }
637
+ File.chmod mode, realdest
638
+
639
+ File.open("#{objdir_root()}/InstalledFiles", 'a') {|f|
640
+ if prefix
641
+ f.puts realdest.sub(prefix, '')
642
+ else
643
+ f.puts realdest
644
+ end
645
+ }
646
+ end
647
+ end
648
+
649
+ def diff?(new_content, path)
650
+ return true unless File.exist?(path)
651
+ new_content != File.binread(path)
652
+ end
653
+
654
+ def command(*args)
655
+ $stderr.puts args.join(' ') if verbose?
656
+ system(*args) or raise RuntimeError,
657
+ "system(#{args.map{|a| a.inspect }.join(' ')}) failed"
658
+ end
659
+
660
+ def ruby(*args)
661
+ command config('rubyprog'), *args
662
+ end
663
+
664
+ def make(task = nil)
665
+ command(*[config('makeprog'), task].compact)
666
+ end
667
+
668
+ def extdir?(dir)
669
+ File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb")
670
+ end
671
+
672
+ def files_of(dir)
673
+ Dir.open(dir) {|d|
674
+ return d.select {|ent| File.file?("#{dir}/#{ent}") }
675
+ }
676
+ end
677
+
678
+ DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn )
679
+
680
+ def directories_of(dir)
681
+ Dir.open(dir) {|d|
682
+ return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT
683
+ }
684
+ end
685
+
686
+ end
687
+
688
+
689
+ # This module requires: #srcdir_root, #objdir_root, #relpath
690
+ module HookScriptAPI
691
+
692
+ def get_config(key)
693
+ @config[key]
694
+ end
695
+
696
+ alias config get_config
697
+
698
+ # obsolete: use metaconfig to change configuration
699
+ def set_config(key, val)
700
+ @config[key] = val
701
+ end
702
+
703
+ #
704
+ # srcdir/objdir (works only in the package directory)
705
+ #
706
+
707
+ def curr_srcdir
708
+ "#{srcdir_root()}/#{relpath()}"
709
+ end
710
+
711
+ def curr_objdir
712
+ "#{objdir_root()}/#{relpath()}"
713
+ end
714
+
715
+ def srcfile(path)
716
+ "#{curr_srcdir()}/#{path}"
717
+ end
718
+
719
+ def srcexist?(path)
720
+ File.exist?(srcfile(path))
721
+ end
722
+
723
+ def srcdirectory?(path)
724
+ File.dir?(srcfile(path))
725
+ end
726
+
727
+ def srcfile?(path)
728
+ File.file?(srcfile(path))
729
+ end
730
+
731
+ def srcentries(path = '.')
732
+ Dir.open("#{curr_srcdir()}/#{path}") {|d|
733
+ return d.to_a - %w(. ..)
734
+ }
735
+ end
736
+
737
+ def srcfiles(path = '.')
738
+ srcentries(path).select {|fname|
739
+ File.file?(File.join(curr_srcdir(), path, fname))
740
+ }
741
+ end
742
+
743
+ def srcdirectories(path = '.')
744
+ srcentries(path).select {|fname|
745
+ File.dir?(File.join(curr_srcdir(), path, fname))
746
+ }
747
+ end
748
+
749
+ end
750
+
751
+
752
+ class ToplevelInstaller
753
+
754
+ Version = '3.4.1'
755
+ Copyright = 'Copyright (c) 2000-2005 Minero Aoki'
756
+
757
+ TASKS = [
758
+ [ 'all', 'do config, setup, then install' ],
759
+ [ 'config', 'saves your configurations' ],
760
+ [ 'show', 'shows current configuration' ],
761
+ [ 'setup', 'compiles ruby extentions and others' ],
762
+ [ 'install', 'installs files' ],
763
+ [ 'test', 'run all tests in test/' ],
764
+ [ 'clean', "does `make clean' for each extention" ],
765
+ [ 'distclean',"does `make distclean' for each extention" ]
766
+ ]
767
+
768
+ def ToplevelInstaller.invoke
769
+ config = ConfigTable.new(load_rbconfig())
770
+ config.load_standard_entries
771
+ config.load_multipackage_entries if multipackage?
772
+ config.fixup
773
+ klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller)
774
+ klass.new(File.dirname($0), config).invoke
775
+ end
776
+
777
+ def ToplevelInstaller.multipackage?
778
+ File.dir?(File.dirname($0) + '/packages')
779
+ end
780
+
781
+ def ToplevelInstaller.load_rbconfig
782
+ if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg }
783
+ ARGV.delete(arg)
784
+ load File.expand_path(arg.split(/=/, 2)[1])
785
+ $".push 'rbconfig.rb'
786
+ else
787
+ require 'rbconfig'
788
+ end
789
+ ::Config::CONFIG
790
+ end
791
+
792
+ def initialize(ardir_root, config)
793
+ @ardir = File.expand_path(ardir_root)
794
+ @config = config
795
+ # cache
796
+ @valid_task_re = nil
797
+ end
798
+
799
+ def config(key)
800
+ @config[key]
801
+ end
802
+
803
+ def inspect
804
+ "#<#{self.class} #{__id__()}>"
805
+ end
806
+
807
+ def invoke
808
+ run_metaconfigs
809
+ case task = parsearg_global()
810
+ when nil, 'all'
811
+ parsearg_config
812
+ init_installers
813
+ exec_config
814
+ exec_setup
815
+ exec_install
816
+ else
817
+ case task
818
+ when 'config', 'test'
819
+ ;
820
+ when 'clean', 'distclean'
821
+ @config.load_savefile if File.exist?(@config.savefile)
822
+ else
823
+ @config.load_savefile
824
+ end
825
+ __send__ "parsearg_#{task}"
826
+ init_installers
827
+ __send__ "exec_#{task}"
828
+ end
829
+ end
830
+
831
+ def run_metaconfigs
832
+ @config.load_script "#{@ardir}/metaconfig"
833
+ end
834
+
835
+ def init_installers
836
+ @installer = Installer.new(@config, @ardir, File.expand_path('.'))
837
+ end
838
+
839
+ #
840
+ # Hook Script API bases
841
+ #
842
+
843
+ def srcdir_root
844
+ @ardir
845
+ end
846
+
847
+ def objdir_root
848
+ '.'
849
+ end
850
+
851
+ def relpath
852
+ '.'
853
+ end
854
+
855
+ #
856
+ # Option Parsing
857
+ #
858
+
859
+ def parsearg_global
860
+ while arg = ARGV.shift
861
+ case arg
862
+ when /\A\w+\z/
863
+ setup_rb_error "invalid task: #{arg}" unless valid_task?(arg)
864
+ return arg
865
+ when '-q', '--quiet'
866
+ @config.verbose = false
867
+ when '--verbose'
868
+ @config.verbose = true
869
+ when '--help'
870
+ print_usage $stdout
871
+ exit 0
872
+ when '--version'
873
+ puts "#{File.basename($0)} version #{Version}"
874
+ exit 0
875
+ when '--copyright'
876
+ puts Copyright
877
+ exit 0
878
+ else
879
+ setup_rb_error "unknown global option '#{arg}'"
880
+ end
881
+ end
882
+ nil
883
+ end
884
+
885
+ def valid_task?(t)
886
+ valid_task_re() =~ t
887
+ end
888
+
889
+ def valid_task_re
890
+ @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/
891
+ end
892
+
893
+ def parsearg_no_options
894
+ unless ARGV.empty?
895
+ task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1)
896
+ setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}"
897
+ end
898
+ end
899
+
900
+ alias parsearg_show parsearg_no_options
901
+ alias parsearg_setup parsearg_no_options
902
+ alias parsearg_test parsearg_no_options
903
+ alias parsearg_clean parsearg_no_options
904
+ alias parsearg_distclean parsearg_no_options
905
+
906
+ def parsearg_config
907
+ evalopt = []
908
+ set = []
909
+ @config.config_opt = []
910
+ while i = ARGV.shift
911
+ if /\A--?\z/ =~ i
912
+ @config.config_opt = ARGV.dup
913
+ break
914
+ end
915
+ name, value = *@config.parse_opt(i)
916
+ if @config.value_config?(name)
917
+ @config[name] = value
918
+ else
919
+ evalopt.push [name, value]
920
+ end
921
+ set.push name
922
+ end
923
+ evalopt.each do |name, value|
924
+ @config.lookup(name).evaluate value, @config
925
+ end
926
+ # Check if configuration is valid
927
+ set.each do |n|
928
+ @config[n] if @config.value_config?(n)
929
+ end
930
+ end
931
+
932
+ def parsearg_install
933
+ @config.no_harm = false
934
+ @config.install_prefix = ''
935
+ while a = ARGV.shift
936
+ case a
937
+ when '--no-harm'
938
+ @config.no_harm = true
939
+ when /\A--prefix=/
940
+ path = a.split(/=/, 2)[1]
941
+ path = File.expand_path(path) unless path[0,1] == '/'
942
+ @config.install_prefix = path
943
+ else
944
+ setup_rb_error "install: unknown option #{a}"
945
+ end
946
+ end
947
+ end
948
+
949
+ def print_usage(out)
950
+ out.puts 'Typical Installation Procedure:'
951
+ out.puts " $ ruby #{File.basename $0} config"
952
+ out.puts " $ ruby #{File.basename $0} setup"
953
+ out.puts " # ruby #{File.basename $0} install (may require root privilege)"
954
+ out.puts
955
+ out.puts 'Detailed Usage:'
956
+ out.puts " ruby #{File.basename $0} <global option>"
957
+ out.puts " ruby #{File.basename $0} [<global options>] <task> [<task options>]"
958
+
959
+ fmt = " %-24s %s\n"
960
+ out.puts
961
+ out.puts 'Global options:'
962
+ out.printf fmt, '-q,--quiet', 'suppress message outputs'
963
+ out.printf fmt, ' --verbose', 'output messages verbosely'
964
+ out.printf fmt, ' --help', 'print this message'
965
+ out.printf fmt, ' --version', 'print version and quit'
966
+ out.printf fmt, ' --copyright', 'print copyright and quit'
967
+ out.puts
968
+ out.puts 'Tasks:'
969
+ TASKS.each do |name, desc|
970
+ out.printf fmt, name, desc
971
+ end
972
+
973
+ fmt = " %-24s %s [%s]\n"
974
+ out.puts
975
+ out.puts 'Options for CONFIG or ALL:'
976
+ @config.each do |item|
977
+ out.printf fmt, item.help_opt, item.description, item.help_default
978
+ end
979
+ out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's"
980
+ out.puts
981
+ out.puts 'Options for INSTALL:'
982
+ out.printf fmt, '--no-harm', 'only display what to do if given', 'off'
983
+ out.printf fmt, '--prefix=path', 'install path prefix', ''
984
+ out.puts
985
+ end
986
+
987
+ #
988
+ # Task Handlers
989
+ #
990
+
991
+ def exec_config
992
+ @installer.exec_config
993
+ @config.save # must be final
994
+ end
995
+
996
+ def exec_setup
997
+ @installer.exec_setup
998
+ end
999
+
1000
+ def exec_install
1001
+ @installer.exec_install
1002
+ end
1003
+
1004
+ def exec_test
1005
+ @installer.exec_test
1006
+ end
1007
+
1008
+ def exec_show
1009
+ @config.each do |i|
1010
+ printf "%-20s %s\n", i.name, i.value if i.value?
1011
+ end
1012
+ end
1013
+
1014
+ def exec_clean
1015
+ @installer.exec_clean
1016
+ end
1017
+
1018
+ def exec_distclean
1019
+ @installer.exec_distclean
1020
+ end
1021
+
1022
+ end # class ToplevelInstaller
1023
+
1024
+
1025
+ class ToplevelInstallerMulti < ToplevelInstaller
1026
+
1027
+ include FileOperations
1028
+
1029
+ def initialize(ardir_root, config)
1030
+ super
1031
+ @packages = directories_of("#{@ardir}/packages")
1032
+ raise 'no package exists' if @packages.empty?
1033
+ @root_installer = Installer.new(@config, @ardir, File.expand_path('.'))
1034
+ end
1035
+
1036
+ def run_metaconfigs
1037
+ @config.load_script "#{@ardir}/metaconfig", self
1038
+ @packages.each do |name|
1039
+ @config.load_script "#{@ardir}/packages/#{name}/metaconfig"
1040
+ end
1041
+ end
1042
+
1043
+ attr_reader :packages
1044
+
1045
+ def packages=(list)
1046
+ raise 'package list is empty' if list.empty?
1047
+ list.each do |name|
1048
+ raise "directory packages/#{name} does not exist"\
1049
+ unless File.dir?("#{@ardir}/packages/#{name}")
1050
+ end
1051
+ @packages = list
1052
+ end
1053
+
1054
+ def init_installers
1055
+ @installers = {}
1056
+ @packages.each do |pack|
1057
+ @installers[pack] = Installer.new(@config,
1058
+ "#{@ardir}/packages/#{pack}",
1059
+ "packages/#{pack}")
1060
+ end
1061
+ with = extract_selection(config('with'))
1062
+ without = extract_selection(config('without'))
1063
+ @selected = @installers.keys.select {|name|
1064
+ (with.empty? or with.include?(name)) \
1065
+ and not without.include?(name)
1066
+ }
1067
+ end
1068
+
1069
+ def extract_selection(list)
1070
+ a = list.split(/,/)
1071
+ a.each do |name|
1072
+ setup_rb_error "no such package: #{name}" unless @installers.key?(name)
1073
+ end
1074
+ a
1075
+ end
1076
+
1077
+ def print_usage(f)
1078
+ super
1079
+ f.puts 'Inluded packages:'
1080
+ f.puts ' ' + @packages.sort.join(' ')
1081
+ f.puts
1082
+ end
1083
+
1084
+ #
1085
+ # Task Handlers
1086
+ #
1087
+
1088
+ def exec_config
1089
+ run_hook 'pre-config'
1090
+ each_selected_installers {|inst| inst.exec_config }
1091
+ run_hook 'post-config'
1092
+ @config.save # must be final
1093
+ end
1094
+
1095
+ def exec_setup
1096
+ run_hook 'pre-setup'
1097
+ each_selected_installers {|inst| inst.exec_setup }
1098
+ run_hook 'post-setup'
1099
+ end
1100
+
1101
+ def exec_install
1102
+ run_hook 'pre-install'
1103
+ each_selected_installers {|inst| inst.exec_install }
1104
+ run_hook 'post-install'
1105
+ end
1106
+
1107
+ def exec_test
1108
+ run_hook 'pre-test'
1109
+ each_selected_installers {|inst| inst.exec_test }
1110
+ run_hook 'post-test'
1111
+ end
1112
+
1113
+ def exec_clean
1114
+ rm_f @config.savefile
1115
+ run_hook 'pre-clean'
1116
+ each_selected_installers {|inst| inst.exec_clean }
1117
+ run_hook 'post-clean'
1118
+ end
1119
+
1120
+ def exec_distclean
1121
+ rm_f @config.savefile
1122
+ run_hook 'pre-distclean'
1123
+ each_selected_installers {|inst| inst.exec_distclean }
1124
+ run_hook 'post-distclean'
1125
+ end
1126
+
1127
+ #
1128
+ # lib
1129
+ #
1130
+
1131
+ def each_selected_installers
1132
+ Dir.mkdir 'packages' unless File.dir?('packages')
1133
+ @selected.each do |pack|
1134
+ $stderr.puts "Processing the package `#{pack}' ..." if verbose?
1135
+ Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}")
1136
+ Dir.chdir "packages/#{pack}"
1137
+ yield @installers[pack]
1138
+ Dir.chdir '../..'
1139
+ end
1140
+ end
1141
+
1142
+ def run_hook(id)
1143
+ @root_installer.run_hook id
1144
+ end
1145
+
1146
+ # module FileOperations requires this
1147
+ def verbose?
1148
+ @config.verbose?
1149
+ end
1150
+
1151
+ # module FileOperations requires this
1152
+ def no_harm?
1153
+ @config.no_harm?
1154
+ end
1155
+
1156
+ end # class ToplevelInstallerMulti
1157
+
1158
+
1159
+ class Installer
1160
+
1161
+ FILETYPES = %w( bin lib ext data conf man )
1162
+
1163
+ include FileOperations
1164
+ include HookScriptAPI
1165
+
1166
+ def initialize(config, srcroot, objroot)
1167
+ @config = config
1168
+ @srcdir = File.expand_path(srcroot)
1169
+ @objdir = File.expand_path(objroot)
1170
+ @currdir = '.'
1171
+ end
1172
+
1173
+ def inspect
1174
+ "#<#{self.class} #{File.basename(@srcdir)}>"
1175
+ end
1176
+
1177
+ def noop(rel)
1178
+ end
1179
+
1180
+ #
1181
+ # Hook Script API base methods
1182
+ #
1183
+
1184
+ def srcdir_root
1185
+ @srcdir
1186
+ end
1187
+
1188
+ def objdir_root
1189
+ @objdir
1190
+ end
1191
+
1192
+ def relpath
1193
+ @currdir
1194
+ end
1195
+
1196
+ #
1197
+ # Config Access
1198
+ #
1199
+
1200
+ # module FileOperations requires this
1201
+ def verbose?
1202
+ @config.verbose?
1203
+ end
1204
+
1205
+ # module FileOperations requires this
1206
+ def no_harm?
1207
+ @config.no_harm?
1208
+ end
1209
+
1210
+ def verbose_off
1211
+ begin
1212
+ save, @config.verbose = @config.verbose?, false
1213
+ yield
1214
+ ensure
1215
+ @config.verbose = save
1216
+ end
1217
+ end
1218
+
1219
+ #
1220
+ # TASK config
1221
+ #
1222
+
1223
+ def exec_config
1224
+ exec_task_traverse 'config'
1225
+ end
1226
+
1227
+ alias config_dir_bin noop
1228
+ alias config_dir_lib noop
1229
+
1230
+ def config_dir_ext(rel)
1231
+ extconf if extdir?(curr_srcdir())
1232
+ end
1233
+
1234
+ alias config_dir_data noop
1235
+ alias config_dir_conf noop
1236
+ alias config_dir_man noop
1237
+
1238
+ def extconf
1239
+ ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt
1240
+ end
1241
+
1242
+ #
1243
+ # TASK setup
1244
+ #
1245
+
1246
+ def exec_setup
1247
+ exec_task_traverse 'setup'
1248
+ end
1249
+
1250
+ def setup_dir_bin(rel)
1251
+ files_of(curr_srcdir()).each do |fname|
1252
+ update_shebang_line "#{curr_srcdir()}/#{fname}"
1253
+ end
1254
+ end
1255
+
1256
+ alias setup_dir_lib noop
1257
+
1258
+ def setup_dir_ext(rel)
1259
+ make if extdir?(curr_srcdir())
1260
+ end
1261
+
1262
+ alias setup_dir_data noop
1263
+ alias setup_dir_conf noop
1264
+ alias setup_dir_man noop
1265
+
1266
+ def update_shebang_line(path)
1267
+ return if no_harm?
1268
+ return if config('shebang') == 'never'
1269
+ old = Shebang.load(path)
1270
+ if old
1271
+ $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1
1272
+ new = new_shebang(old)
1273
+ return if new.to_s == old.to_s
1274
+ else
1275
+ return unless config('shebang') == 'all'
1276
+ new = Shebang.new(config('rubypath'))
1277
+ end
1278
+ $stderr.puts "updating shebang: #{File.basename(path)}" if verbose?
1279
+ open_atomic_writer(path) {|output|
1280
+ File.open(path, 'rb') {|f|
1281
+ f.gets if old # discard
1282
+ output.puts new.to_s
1283
+ output.print f.read
1284
+ }
1285
+ }
1286
+ end
1287
+
1288
+ def new_shebang(old)
1289
+ if /\Aruby/ =~ File.basename(old.cmd)
1290
+ Shebang.new(config('rubypath'), old.args)
1291
+ elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby'
1292
+ Shebang.new(config('rubypath'), old.args[1..-1])
1293
+ else
1294
+ return old unless config('shebang') == 'all'
1295
+ Shebang.new(config('rubypath'))
1296
+ end
1297
+ end
1298
+
1299
+ def open_atomic_writer(path, &block)
1300
+ tmpfile = File.basename(path) + '.tmp'
1301
+ begin
1302
+ File.open(tmpfile, 'wb', &block)
1303
+ File.rename tmpfile, File.basename(path)
1304
+ ensure
1305
+ File.unlink tmpfile if File.exist?(tmpfile)
1306
+ end
1307
+ end
1308
+
1309
+ class Shebang
1310
+ def Shebang.load(path)
1311
+ line = nil
1312
+ File.open(path) {|f|
1313
+ line = f.gets
1314
+ }
1315
+ return nil unless /\A#!/ =~ line
1316
+ parse(line)
1317
+ end
1318
+
1319
+ def Shebang.parse(line)
1320
+ cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ')
1321
+ new(cmd, args)
1322
+ end
1323
+
1324
+ def initialize(cmd, args = [])
1325
+ @cmd = cmd
1326
+ @args = args
1327
+ end
1328
+
1329
+ attr_reader :cmd
1330
+ attr_reader :args
1331
+
1332
+ def to_s
1333
+ "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}")
1334
+ end
1335
+ end
1336
+
1337
+ #
1338
+ # TASK install
1339
+ #
1340
+
1341
+ def exec_install
1342
+ rm_f 'InstalledFiles'
1343
+ exec_task_traverse 'install'
1344
+ end
1345
+
1346
+ def install_dir_bin(rel)
1347
+ install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755
1348
+ end
1349
+
1350
+ def install_dir_lib(rel)
1351
+ install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644
1352
+ end
1353
+
1354
+ def install_dir_ext(rel)
1355
+ return unless extdir?(curr_srcdir())
1356
+ install_files rubyextentions('.'),
1357
+ "#{config('sodir')}/#{File.dirname(rel)}",
1358
+ 0555
1359
+ end
1360
+
1361
+ def install_dir_data(rel)
1362
+ install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644
1363
+ end
1364
+
1365
+ def install_dir_conf(rel)
1366
+ # FIXME: should not remove current config files
1367
+ # (rename previous file to .old/.org)
1368
+ install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644
1369
+ end
1370
+
1371
+ def install_dir_man(rel)
1372
+ install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644
1373
+ end
1374
+
1375
+ def install_files(list, dest, mode)
1376
+ mkdir_p dest, @config.install_prefix
1377
+ list.each do |fname|
1378
+ install fname, dest, mode, @config.install_prefix
1379
+ end
1380
+ end
1381
+
1382
+ def libfiles
1383
+ glob_reject(%w(*.y *.output), targetfiles())
1384
+ end
1385
+
1386
+ def rubyextentions(dir)
1387
+ ents = glob_select("*.#{@config.dllext}", targetfiles())
1388
+ if ents.empty?
1389
+ setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first"
1390
+ end
1391
+ ents
1392
+ end
1393
+
1394
+ def targetfiles
1395
+ mapdir(existfiles() - hookfiles())
1396
+ end
1397
+
1398
+ def mapdir(ents)
1399
+ ents.map {|ent|
1400
+ if File.exist?(ent)
1401
+ then ent # objdir
1402
+ else "#{curr_srcdir()}/#{ent}" # srcdir
1403
+ end
1404
+ }
1405
+ end
1406
+
1407
+ # picked up many entries from cvs-1.11.1/src/ignore.c
1408
+ JUNK_FILES = %w(
1409
+ core RCSLOG tags TAGS .make.state
1410
+ .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb
1411
+ *~ *.old *.bak *.BAK *.orig *.rej _$* *$
1412
+
1413
+ *.org *.in .*
1414
+ )
1415
+
1416
+ def existfiles
1417
+ glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.')))
1418
+ end
1419
+
1420
+ def hookfiles
1421
+ %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt|
1422
+ %w( config setup install clean ).map {|t| sprintf(fmt, t) }
1423
+ }.flatten
1424
+ end
1425
+
1426
+ def glob_select(pat, ents)
1427
+ re = globs2re([pat])
1428
+ ents.select {|ent| re =~ ent }
1429
+ end
1430
+
1431
+ def glob_reject(pats, ents)
1432
+ re = globs2re(pats)
1433
+ ents.reject {|ent| re =~ ent }
1434
+ end
1435
+
1436
+ GLOB2REGEX = {
1437
+ '.' => '\.',
1438
+ '$' => '\$',
1439
+ '#' => '\#',
1440
+ '*' => '.*'
1441
+ }
1442
+
1443
+ def globs2re(pats)
1444
+ /\A(?:#{
1445
+ pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|')
1446
+ })\z/
1447
+ end
1448
+
1449
+ #
1450
+ # TASK test
1451
+ #
1452
+
1453
+ TESTDIR = 'test'
1454
+
1455
+ def exec_test
1456
+ unless File.directory?('test')
1457
+ $stderr.puts 'no test in this package' if verbose?
1458
+ return
1459
+ end
1460
+ $stderr.puts 'Running tests...' if verbose?
1461
+ begin
1462
+ require 'test/unit'
1463
+ rescue LoadError
1464
+ setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.'
1465
+ end
1466
+ runner = Test::Unit::AutoRunner.new(true)
1467
+ runner.to_run << TESTDIR
1468
+ runner.run
1469
+ end
1470
+
1471
+ #
1472
+ # TASK clean
1473
+ #
1474
+
1475
+ def exec_clean
1476
+ exec_task_traverse 'clean'
1477
+ rm_f @config.savefile
1478
+ rm_f 'InstalledFiles'
1479
+ end
1480
+
1481
+ alias clean_dir_bin noop
1482
+ alias clean_dir_lib noop
1483
+ alias clean_dir_data noop
1484
+ alias clean_dir_conf noop
1485
+ alias clean_dir_man noop
1486
+
1487
+ def clean_dir_ext(rel)
1488
+ return unless extdir?(curr_srcdir())
1489
+ make 'clean' if File.file?('Makefile')
1490
+ end
1491
+
1492
+ #
1493
+ # TASK distclean
1494
+ #
1495
+
1496
+ def exec_distclean
1497
+ exec_task_traverse 'distclean'
1498
+ rm_f @config.savefile
1499
+ rm_f 'InstalledFiles'
1500
+ end
1501
+
1502
+ alias distclean_dir_bin noop
1503
+ alias distclean_dir_lib noop
1504
+
1505
+ def distclean_dir_ext(rel)
1506
+ return unless extdir?(curr_srcdir())
1507
+ make 'distclean' if File.file?('Makefile')
1508
+ end
1509
+
1510
+ alias distclean_dir_data noop
1511
+ alias distclean_dir_conf noop
1512
+ alias distclean_dir_man noop
1513
+
1514
+ #
1515
+ # Traversing
1516
+ #
1517
+
1518
+ def exec_task_traverse(task)
1519
+ run_hook "pre-#{task}"
1520
+ FILETYPES.each do |type|
1521
+ if type == 'ext' and config('without-ext') == 'yes'
1522
+ $stderr.puts 'skipping ext/* by user option' if verbose?
1523
+ next
1524
+ end
1525
+ traverse task, type, "#{task}_dir_#{type}"
1526
+ end
1527
+ run_hook "post-#{task}"
1528
+ end
1529
+
1530
+ def traverse(task, rel, mid)
1531
+ dive_into(rel) {
1532
+ run_hook "pre-#{task}"
1533
+ __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '')
1534
+ directories_of(curr_srcdir()).each do |d|
1535
+ traverse task, "#{rel}/#{d}", mid
1536
+ end
1537
+ run_hook "post-#{task}"
1538
+ }
1539
+ end
1540
+
1541
+ def dive_into(rel)
1542
+ return unless File.dir?("#{@srcdir}/#{rel}")
1543
+
1544
+ dir = File.basename(rel)
1545
+ Dir.mkdir dir unless File.dir?(dir)
1546
+ prevdir = Dir.pwd
1547
+ Dir.chdir dir
1548
+ $stderr.puts '---> ' + rel if verbose?
1549
+ @currdir = rel
1550
+ yield
1551
+ Dir.chdir prevdir
1552
+ $stderr.puts '<--- ' + rel if verbose?
1553
+ @currdir = File.dirname(rel)
1554
+ end
1555
+
1556
+ def run_hook(id)
1557
+ path = [ "#{curr_srcdir()}/#{id}",
1558
+ "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) }
1559
+ return unless path
1560
+ begin
1561
+ instance_eval File.read(path), path, 1
1562
+ rescue
1563
+ raise if $DEBUG
1564
+ setup_rb_error "hook #{path} failed:\n" + $!.message
1565
+ end
1566
+ end
1567
+
1568
+ end # class Installer
1569
+
1570
+
1571
+ class SetupError < StandardError; end
1572
+
1573
+ def setup_rb_error(msg)
1574
+ raise SetupError, msg
1575
+ end
1576
+
1577
+ if $0 == __FILE__
1578
+ begin
1579
+ ToplevelInstaller.invoke
1580
+ rescue SetupError
1581
+ raise if $DEBUG
1582
+ $stderr.puts $!.message
1583
+ $stderr.puts "Try 'ruby #{$0} --help' for detailed usage."
1584
+ exit 1
1585
+ end
1586
+ end