utilrb 1.3.3 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,4 @@
1
+ require 'set'
1
2
  require 'shellwords'
2
3
 
3
4
  module Utilrb
@@ -36,36 +37,258 @@ module Utilrb
36
37
  # pkg.libdir
37
38
  #
38
39
  class PkgConfig
40
+ PACKAGE_NAME_RX = /[\w\-\.]+/
41
+ VAR_NAME_RX = /\w+/
42
+ FIELD_NAME_RX = /[\w\.\-]+/
43
+
44
+ class << self
45
+ attr_reader :loaded_packages
46
+
47
+ def clear_cache
48
+ cache.clear
49
+ end
50
+ end
51
+ @loaded_packages = Hash.new
52
+
53
+ def self.load(path)
54
+ pkg_name = File.basename(path, ".pc")
55
+ pkg = Class.instance_method(:new).bind(PkgConfig).call(pkg_name)
56
+ pkg.load(path)
57
+ pkg
58
+ end
59
+
60
+ # Returns the pkg-config object that matches the given name, and
61
+ # optionally a version string
62
+ def self.get(name, version_spec = nil)
63
+ if !(candidates = loaded_packages[name])
64
+ paths = find_all_package_files(name)
65
+ if paths.empty?
66
+ raise NotFound.new(name), "cannot find the pkg-config specification for #{name}"
67
+ end
68
+
69
+ candidates = loaded_packages[name] = Array.new
70
+ paths.each do |p|
71
+ candidates << PkgConfig.load(p)
72
+ end
73
+ end
74
+
75
+ # Now try to find a matching spec
76
+ find_matching_version(candidates, version_spec)
77
+ end
78
+
79
+ def self.new(name, version_spec = nil)
80
+ get(name, version_spec)
81
+ end
82
+
83
+ # Returns the first package in +candidates+ that match the given version
84
+ # spec
85
+ def self.find_matching_version(candidates, version_spec)
86
+ if version_spec
87
+ version_spec =~ /([<>=]+)\s*([\d\.]+)/
88
+ op, requested_version = $1, $2
89
+
90
+ requested_op =
91
+ if op == "=" then [0]
92
+ elsif op == ">" then [1]
93
+ elsif op == "<" then [-1]
94
+ elsif op == "<=" then [-1, 0]
95
+ elsif op == ">=" then [1, 0]
96
+ end
97
+
98
+ requested_version = requested_version.split('.').map { |v| Integer(v) }
99
+
100
+ result = candidates.find do |pkg|
101
+ requested_op.include?(pkg.version <=> requested_version)
102
+ end
103
+ if !result
104
+ raise NotFound.new(name), "no version of #{name} match #{version_spect}. Available versions are: #{candidates.map(&:raw_version).join(", ")}"
105
+ end
106
+ result
107
+ else
108
+ candidates.first
109
+ end
110
+ end
111
+
39
112
  class NotFound < RuntimeError
40
113
  attr_reader :name
41
114
  def initialize(name); @name = name end
42
115
  end
116
+
117
+ attr_reader :file
118
+ attr_reader :path
43
119
 
44
120
  # The module name
45
121
  attr_reader :name
46
- # The module version
47
- attr_reader :version
122
+ attr_reader :description
123
+ # The module version as a string
124
+ attr_reader :raw_version
125
+ # The module version, as an array of integers
126
+ attr_reader :version
127
+
128
+ # Information extracted from the file
129
+ attr_reader :variables
130
+ attr_reader :fields
48
131
 
49
132
  # Create a PkgConfig object for the package +name+
50
133
  # Raises PkgConfig::NotFound if the module does not exist
51
134
  def initialize(name)
52
- if !system("pkg-config --exists #{name}")
53
- raise NotFound.new(name), "#{name} is not available to pkg-config"
54
- end
55
-
56
135
  @name = name
57
- @version = `pkg-config --modversion #{name}`.chomp.strip
58
- @actions = Hash.new
136
+ @fields = Hash.new
59
137
  @variables = Hash.new
60
138
  end
61
139
 
62
- def self.define_action(action) # :nodoc:
63
- define_method(action.gsub(/-/, '_')) do
64
- @actions[action] ||= `pkg-config --#{action} #{name}`.chomp.strip
65
- end
140
+ # Helper method that expands ${word} in +value+ using the name to value
141
+ # map +variables+
142
+ #
143
+ # +current+ is a string that describes what we are expanding. It is used
144
+ # to detect recursion in expansion of variables, and to give meaningful
145
+ # errors to the user
146
+ def expand_variables(value, variables, current)
147
+ value = value.gsub(/\$\{(\w+)\}/) do |rx|
148
+ expand_name = $1
149
+ if expand_name == current
150
+ raise "error in pkg-config file #{path}: #{current} contains a reference to itself"
151
+ elsif !(expanded = variables[expand_name])
152
+ raise "error in pkg-config file #{path}: #{current} contains a reference to #{expand_name} but there is no such variable"
153
+ end
154
+ expanded
155
+ end
156
+ value
157
+ end
158
+
159
+ def self.parse_dependencies(string)
160
+ if string =~ /,/
161
+ packages = string.split(',')
162
+ else
163
+ packages = []
164
+ words = string.split(' ')
165
+ while !words.empty?
166
+ w = words.shift
167
+ if w =~ /[<>=]/
168
+ packages[-1] += " #{w} #{words.shift}"
169
+ else
170
+ packages << w
171
+ end
172
+ end
173
+ end
174
+
175
+ result = packages.map do |dep|
176
+ dep = dep.strip
177
+ if dep =~ /^(#{PACKAGE_NAME_RX})\s*([=<>]+.*)/
178
+ PkgConfig.get($1, $2.strip)
179
+ else
180
+ PkgConfig.get(dep)
181
+ end
182
+ end
183
+ result
184
+ end
185
+
186
+ SHELL_VARS = %w{Cflags Libs Libs.private}
187
+
188
+ # Loads the information contained in +path+
189
+ def load(path)
190
+ @path = path
191
+ @file = File.readlines(path).map(&:strip)
192
+
193
+ raw_variables = Hash.new
194
+ raw_fields = Hash.new
195
+ file.each do |line|
196
+ line.gsub! /\s*#.*$/, ''
197
+ next if line.empty?
198
+
199
+ case line
200
+ when /^(#{VAR_NAME_RX})\s*=(.*)/
201
+ raw_variables[$1] = $2.strip
202
+ when /^(#{FIELD_NAME_RX}):\s*(.*)/
203
+ raw_fields[$1] = $2.strip
204
+ else
205
+ raise NotImplementedError, "cannot parse pkg-config line #{line.inspect}"
206
+ end
207
+ end
208
+
209
+ # Resolve the variables
210
+ while variables.size != raw_variables.size
211
+ raw_variables.each do |name, value|
212
+ value = expand_variables(value, raw_variables, name)
213
+ raw_variables[name] = value
214
+ if value !~ /\$\{#{VAR_NAME_RX}\}/
215
+ variables[name] = value
216
+ end
217
+ end
218
+ end
219
+
220
+ # Shell-split the fields, and expand the variables in them
221
+ raw_fields.each do |name, value|
222
+ if SHELL_VARS.include?(name)
223
+ value = Shellwords.shellsplit(value)
224
+ value.map! do |v|
225
+ expand_variables(v, variables, name)
226
+ end
227
+ else
228
+ value = expand_variables(value, variables, name)
229
+ end
230
+
231
+ fields[name] = value
232
+ end
233
+
234
+ # Initialize the main flags
235
+ @raw_version = (fields['Version'] || '')
236
+ @version = raw_version.split('.').map { |v| Integer(v) if v =~ /^\d+$/ }.compact
237
+ @description = (fields['Description'] || '')
238
+
239
+ # Get the requires/conflicts
240
+ @requires = PkgConfig.parse_dependencies(fields['Requires'] || '')
241
+ @requires_private = PkgConfig.parse_dependencies(fields['Requires.private'] || '')
242
+ @conflicts = PkgConfig.parse_dependencies(fields['Conflicts'] || '')
243
+
244
+ # And finally resolve the compilation flags
245
+ @cflags = fields['Cflags'] || []
246
+ @requires.each do |pkg|
247
+ @cflags.concat(pkg.raw_cflags)
248
+ end
249
+ @requires_private.each do |pkg|
250
+ @cflags.concat(pkg.raw_cflags)
251
+ end
252
+ @cflags.uniq!
253
+ @cflags.delete('-I/usr/include')
254
+ @ldflags = Hash.new
255
+ @ldflags[false] = fields['Libs'] || []
256
+ @ldflags[false].delete('-L/usr/lib')
257
+ @ldflags[false].uniq!
258
+ @ldflags[true] = @ldflags[false] + (fields['Libs.private'] || [])
259
+ @ldflags[true].delete('-L/usr/lib')
260
+ @ldflags[true].uniq!
261
+
262
+ @ldflags_with_requires = {
263
+ true => @ldflags[true].dup,
264
+ false => @ldflags[false].dup
265
+ }
266
+ @requires.each do |pkg|
267
+ @ldflags_with_requires[true].concat(pkg.raw_ldflags_with_requires[true])
268
+ @ldflags_with_requires[false].concat(pkg.raw_ldflags_with_requires[false])
269
+ end
270
+ @requires_private.each do |pkg|
271
+ @ldflags_with_requires[true].concat(pkg.raw_ldflags_with_requires[true])
272
+ end
273
+ end
274
+
275
+ def self.define_pkgconfig_action(action) # :nodoc:
276
+ class_eval <<-EOD
277
+ def pkgconfig_#{action.gsub(/-/, '_')}(static = false)
278
+ if static
279
+ `pkg-config --#{action} --static \#{name}`.strip
280
+ else
281
+ `pkg-config --#{action} \#{name}`.strip
282
+ end
283
+ end
284
+ EOD
66
285
  nil
67
286
  end
68
287
 
288
+ def pkgconfig_variable(varname)
289
+ `pkg-config --variable=#{varname}`.strip
290
+ end
291
+
69
292
  # Returns the list of include directories listed in the Cflags: section
70
293
  # of the pkgconfig file
71
294
  def include_dirs
@@ -79,37 +302,98 @@ module Utilrb
79
302
  end
80
303
 
81
304
  ACTIONS = %w{cflags cflags-only-I cflags-only-other
82
- libs libs-only-L libs-only-l libs-only-other static}
83
- ACTIONS.each { |action| define_action(action) }
305
+ libs libs-only-L libs-only-l libs-only-other}
306
+ ACTIONS.each { |action| define_pkgconfig_action(action) }
307
+
308
+ def raw_cflags
309
+ @cflags
310
+ end
311
+
312
+ def cflags
313
+ @cflags.join(" ")
314
+ end
315
+
316
+ def cflags_only_I
317
+ @cflags.grep(/^-I/).join(" ")
318
+ end
319
+
320
+ def cflags_only_other
321
+ @cflags.find_all { |s| s !~ /^-I/ }.join(" ")
322
+ end
323
+
324
+ def raw_ldflags
325
+ @ldflags
326
+ end
327
+
328
+ def raw_ldflags_with_requires
329
+ @ldflags_with_requires
330
+ end
331
+
332
+ def libs(static = false)
333
+ @ldflags_with_requires[static].join(" ")
334
+ end
335
+
336
+ def libs_only_L(static = false)
337
+ @ldflags_with_requires[static].grep(/^-L/).join(" ")
338
+ end
339
+
340
+ def libs_only_l(static = false)
341
+ @ldflags_with_requires[static].grep(/^-l/).join(" ")
342
+ end
343
+
344
+ def libs_only_other(static = false)
345
+ @ldflags[static].find_all { |s| s !~ /^-[lL]/ }.join(" ")
346
+ end
84
347
 
85
348
  def method_missing(varname, *args, &proc) # :nodoc:
86
349
  if args.empty?
87
- @variables[varname] ||= `pkg-config --variable=#{varname} #{name}`.chomp.strip
350
+ variables[varname.to_s]
88
351
  else
89
352
  super(varname, *args, &proc)
90
353
  end
91
354
  end
92
355
 
356
+ def self.each_pkgconfig_directory(&block)
357
+ if path = ENV['PKG_CONFIG_PATH']
358
+ path.split(':').each(&block)
359
+ end
360
+ yield('/usr/local/lib/pkgconfig')
361
+ yield('/usr/lib/pkgconfig')
362
+ yield('/usr/share/pkgconfig')
363
+ end
364
+
365
+ # Returns true if there is a package with this name
366
+ def self.find_all_package_files(name)
367
+ result = []
368
+ each_pkgconfig_directory do |dir|
369
+ path = File.join(dir, "#{name}.pc")
370
+ if File.exists?(path)
371
+ result << path
372
+ end
373
+ end
374
+ result
375
+ end
376
+
93
377
  # Returns true if there is a package with this name
94
378
  def self.has_package?(name)
95
- enum_for(:each_package, name).find { true }
379
+ !find_all_package_files(name).empty?
96
380
  end
97
381
 
98
382
  # Yields the package names of available packages. If +regex+ is given,
99
383
  # lists only the names that match the regular expression.
100
384
  def self.each_package(regex = nil)
101
- `pkg-config --list-all`.chomp.split.
102
- each do |line|
103
- line =~ /^([^\s]+)/
104
- name = $1
105
- if regex
106
- if regex === name
107
- yield(name)
108
- end
109
- else
110
- yield(name)
111
- end
385
+ seen = Set.new
386
+ each_pkgconfig_directory do |dir|
387
+ Dir.glob(File.join(dir, '*.pc')) do |file|
388
+ puts file
389
+ pkg_name = File.basename(file, ".pc")
390
+ next if seen.include?(pkg_name)
391
+ next if regex && pkg_name !~ regex
392
+
393
+ seen << pkg_name
394
+ yield(pkg_name)
112
395
  end
396
+ end
113
397
  end
114
398
  end
115
399
  end
@@ -0,0 +1,5 @@
1
+ class Symbol
2
+ def to_str
3
+ to_s
4
+ end
5
+ end
@@ -24,5 +24,17 @@ Utilrb.require_ext("ValueSet") do
24
24
  def self._load(str)
25
25
  Marshal.load(str).to_value_set
26
26
  end
27
+
28
+ def eql?(obj)
29
+ self == obj
30
+ end
31
+
32
+ def hash
33
+ result = ValueSet.hash / size
34
+ for obj in self
35
+ result = result ^ obj.hash
36
+ end
37
+ result
38
+ end
27
39
  end
28
40
  end
@@ -0,0 +1,10 @@
1
+ prefix=a_prefix
2
+
3
+ Name: test_pkgconfig
4
+ Description:
5
+ Version: 4.2
6
+
7
+ Cflags:
8
+ Libs:
9
+
10
+
data/test/test_kernel.rb CHANGED
@@ -1,8 +1,12 @@
1
1
  require 'test_config'
2
+ require 'flexmock'
3
+ require 'tempfile'
2
4
 
3
5
  require 'utilrb/kernel/options'
4
6
  require 'utilrb/kernel/arity'
5
7
  require 'utilrb/kernel/swap'
8
+ require 'utilrb/kernel/with_module'
9
+ require 'utilrb/kernel/load_dsl_file'
6
10
 
7
11
  class TC_Kernel < Test::Unit::TestCase
8
12
  def test_validate_options
@@ -45,6 +49,160 @@ class TC_Kernel < Test::Unit::TestCase
45
49
  assert_nothing_raised { check_arity(object.method(:arity_1_more), 2) }
46
50
  end
47
51
 
52
+ def test_with_module
53
+ obj = Object.new
54
+ c0, c1 = nil
55
+ mod0 = Module.new do
56
+ const_set(:Const, c0 = Object.new)
57
+ end
58
+ mod1 = Module.new do
59
+ const_set(:Const, c1 = Object.new)
60
+ end
61
+
62
+ eval_string = "Const"
63
+ const_val = obj.with_module(mod0, mod1, eval_string)
64
+ assert_equal(c0, const_val)
65
+ const_val = obj.with_module(mod1, mod0, eval_string)
66
+ assert_equal(c1, const_val)
67
+
68
+ const_val = obj.with_module(mod0, mod1) { Const }
69
+ assert_equal(c0, const_val)
70
+ const_val = obj.with_module(mod1, mod0) { Const }
71
+ assert_equal(c1, const_val)
72
+
73
+ assert_raises(NameError) { Const }
74
+ end
75
+
76
+ module Mod
77
+ module Submod
78
+ class Klass
79
+ end
80
+ end
81
+
82
+ const_set(:KnownConstant, 10)
83
+ end
84
+
85
+ def test_eval_dsl_file
86
+ obj = Class.new do
87
+ def real_method_called?; !!@real_method_called end
88
+ def name(value)
89
+ end
90
+ def real_method
91
+ @real_method_called = true
92
+ end
93
+ end.new
94
+
95
+ Tempfile.open('test_eval_dsl_file') do |io|
96
+ io.puts <<-EOD
97
+ real_method
98
+ if KnownConstant != 10
99
+ raise ArgumentError, "invalid constant value"
100
+ end
101
+ class Submod::Klass
102
+ def my_method
103
+ end
104
+ end
105
+ name('test')
106
+ unknown_method
107
+ EOD
108
+ io.flush
109
+
110
+ begin
111
+ eval_dsl_file(io.path, obj, [], false)
112
+ assert(obj.real_method_called?, "the block has not been evaluated")
113
+ flunk("did not raise NameError for KnownConstant")
114
+ rescue NameError => e
115
+ assert e.message =~ /KnownConstant/, e.message
116
+ assert e.backtrace.first =~ /#{io.path}:2/, "wrong backtrace when checking constant resolution: #{e.backtrace.join("\n")}"
117
+ end
118
+
119
+ begin
120
+ eval_dsl_file(io.path, obj, [Mod], false)
121
+ flunk("did not raise NoMethodError for unknown_method")
122
+ rescue NoMethodError => e
123
+ assert e.message =~ /unknown_method/
124
+ assert e.backtrace.first =~ /#{io.path}:10/, "wrong backtrace when checking method resolution: #{e.backtrace.join("\n")}"
125
+ end
126
+
127
+ # instance_methods returns strings on 1.8 and symbols on 1.9. Conver
128
+ # to strings to have the right assertion on both
129
+ methods = Mod::Submod::Klass.instance_methods(false).map(&:to_s)
130
+ assert(methods.include?('my_method'), "the 'class K' statement did not refer to the already defined class")
131
+ end
132
+ end
133
+
134
+ def test_eval_dsl_file_does_not_allow_class_definition
135
+ obj = Class.new do
136
+ def real_method
137
+ @real_method_called = true
138
+ end
139
+ end.new
140
+
141
+ Tempfile.open('test_eval_dsl_file') do |io|
142
+ io.puts <<-EOD
143
+ class NewClass
144
+ end
145
+ EOD
146
+ io.flush
147
+
148
+ begin
149
+ eval_dsl_file(io.path, obj, [], false)
150
+ flunk("NewClass has been defined")
151
+ rescue NameError => e
152
+ assert e.message =~ /NewClass/, e.message
153
+ assert e.backtrace.first =~ /#{io.path}:1/, "wrong backtrace when checking constant definition detection: #{e.backtrace[0]}, expected #{io.path}:1"
154
+ end
155
+ end
156
+ end
157
+
158
+ DSL_EXEC_BLOCK = Proc.new do
159
+ real_method
160
+ if KnownConstant != 10
161
+ raise ArgumentError, "invalid constant value"
162
+ end
163
+ class Submod::Klass
164
+ def my_method
165
+ end
166
+ end
167
+ name('test')
168
+ unknown_method
169
+ end
170
+
171
+ def test_dsl_exec
172
+ obj = Class.new do
173
+ def real_method_called?; !!@real_method_called end
174
+ def name(value)
175
+ end
176
+ def real_method
177
+ @real_method_called = true
178
+ end
179
+ end.new
180
+
181
+ begin
182
+ dsl_exec(obj, [], false, &DSL_EXEC_BLOCK)
183
+ assert(obj.real_method_called?, "the block has not been evaluated")
184
+ flunk("did not raise NameError for KnownConstant")
185
+ rescue NameError => e
186
+ assert e.message =~ /KnownConstant/, e.message
187
+ expected = "test_kernel.rb:160"
188
+ assert e.backtrace.first =~ /#{expected}/, "wrong backtrace when checking constant resolution: #{e.backtrace[0]}, expected #{expected}"
189
+ end
190
+
191
+ begin
192
+ dsl_exec(obj, [Mod], false, &DSL_EXEC_BLOCK)
193
+ flunk("did not raise NoMethodError for unknown_method")
194
+ rescue NoMethodError => e
195
+ assert e.message =~ /unknown_method/
196
+ expected = "test_kernel.rb:168"
197
+ assert e.backtrace.first =~ /#{expected}/, "wrong backtrace when checking method resolution: #{e.backtrace[0]}, expected #{expected}"
198
+ end
199
+
200
+ # instance_methods returns strings on 1.8 and symbols on 1.9. Conver
201
+ # to strings to have the right assertion on both
202
+ methods = Mod::Submod::Klass.instance_methods(false).map(&:to_s)
203
+ assert(methods.include?('my_method'), "the 'class K' statement did not refer to the already defined class")
204
+ end
205
+
48
206
  Utilrb.require_ext('is_singleton?') do
49
207
  def test_is_singleton
50
208
  klass = Class.new
data/test/test_module.rb CHANGED
@@ -23,10 +23,20 @@ class TC_Module < Test::Unit::TestCase
23
23
  assert(k.respond_to?(:tag))
24
24
  end
25
25
 
26
+ Foo = 42
27
+
26
28
  def test_define_or_reuse
27
29
  mod = Module.new
28
- new_mod = mod.define_or_reuse(:Foo) { Module.new }
30
+ klass = Class.new
31
+
32
+ new_mod = mod.define_or_reuse(:Foo) { klass.new }
33
+ assert_kind_of(klass, new_mod)
29
34
  assert_equal(new_mod, mod.define_or_reuse(:Foo) { flunk("block called in #define_under") })
35
+
36
+ # Now try with a constant that is widely available
37
+ new_mod = mod.define_or_reuse('Signal') { klass.new }
38
+ assert_kind_of(klass, new_mod)
39
+ assert_equal(new_mod, mod.define_or_reuse('Signal') { flunk("block called in #define_under") })
30
40
  end
31
41
 
32
42
  def test_define_method_with_block
@@ -93,9 +103,6 @@ class TC_Module < Test::Unit::TestCase
93
103
  assert(object.singleton_class.respond_to?(:signatures))
94
104
  object.singleton_class.signatures << :in_singleton
95
105
  assert_equal([:in_singleton], object.singleton_class.signatures)
96
- Utilrb.if_ext do
97
- assert_equal([:in_singleton, :in_derived, :in_base], object.singleton_class.enum_for(:each_signature).to_a)
98
- end
99
106
  end
100
107
 
101
108
  def check_inherited_enumerable(base, derived)
@@ -138,18 +145,12 @@ class TC_Module < Test::Unit::TestCase
138
145
  include mod
139
146
  end
140
147
  child = Class.new(parent)
141
- singleton = child.new.singleton_class
142
148
 
143
149
  assert(child.has_ancestor?(parent))
144
150
  assert(child.has_ancestor?(mod))
145
151
  assert(parent.has_ancestor?(mod))
146
152
 
147
- assert(singleton.has_ancestor?(parent), singleton.superclass)
148
- assert(singleton.has_ancestor?(mod))
149
- assert(singleton.has_ancestor?(child))
150
-
151
153
  assert(!parent.has_ancestor?(child))
152
- assert(!parent.has_ancestor?(singleton))
153
154
  end
154
155
  end
155
156
 
data/test/test_object.rb CHANGED
@@ -55,34 +55,6 @@ class TC_Object < Test::Unit::TestCase
55
55
  assert_same(obj1.block, obj2.block)
56
56
  end
57
57
 
58
- def test_singleton_class
59
- klass = Class.new do
60
- def bla; 0 end
61
- end
62
-
63
- object = klass.new
64
-
65
- singleton = class << object
66
- class << self
67
- def bla
68
- "a"
69
- end
70
- end
71
- self
72
- end
73
-
74
- assert(! object.has_singleton?)
75
- assert_equal(object, object.singleton_class.singleton_instance)
76
- assert(object.has_singleton?)
77
- assert_equal(klass, object.singleton_class.superclass)
78
-
79
-
80
- Utilrb.if_ext do
81
- assert_equal([object.singleton_class, klass, Object], object.singleton_class.ancestors[0, 3])
82
- assert_equal([klass, Object], klass.ancestors[0, 2])
83
- end
84
- end
85
-
86
58
  def test_attr_predicate
87
59
  klass = Class.new do
88
60
  attr_predicate :working