utilrb 1.3.3 → 1.4.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.
@@ -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