Class: Utilrb::PkgConfig

Inherits:
Object show all
Defined in:
lib/utilrb/pkgconfig.rb

Overview

Access to information from pkg-config(1)

This class allows to enumerate the pkg-config packages available, and create a PkgConfig object that allows to get access to the pkgconfig information.

Create a new pkgconfig object with

pkg = PkgConfig.new(name)

It raises PkgConfig::NotFound if the package is not available.

Then, the classical include directory and library directory flags can be listed with

pkg.include_dirs
pkg.library_dirs

Standard fields are available with

pkg.cflags
pkg.cflags_only_I
pkg.cflags_only_other
pkg.libs
pkg.libs_only_L
pkg.libs_only_l
pkg.libs_only_other
pkg.static

Arbitrary variables defined in the .pc file can be accessed with

pkg.prefix
pkg.libdir

Defined Under Namespace

Classes: Invalid, NotFound

Constant Summary collapse

PACKAGE_NAME_RX =
/[\w\-\.]+/
VAR_NAME_RX =
/\w+/
FIELD_NAME_RX =
/[\w\.\-]+/
SHELL_VARS =
%w{Cflags Libs Libs.private}
ACTIONS =
%w{cflags cflags-only-I cflags-only-other 
libs libs-only-L libs-only-l libs-only-other}
FOUND_PATH_RX =
/Scanning directory '(.*\/)((?:lib|lib64|share)\/.*)'$/
NONEXISTENT_PATH_RX =
/Cannot open directory '.*\/((?:lib|lib64|share)\/.*)' in package search path:.*/

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name) ⇒ PkgConfig

Create a PkgConfig object for the package name Raises PkgConfig::NotFound if the module does not exist



155
156
157
158
159
# File 'lib/utilrb/pkgconfig.rb', line 155

def initialize(name)
    @name    = name
    @fields    = Hash.new
    @variables = Hash.new
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(varname, *args, &proc) ⇒ Object

:nodoc:



405
406
407
408
409
410
411
# File 'lib/utilrb/pkgconfig.rb', line 405

def method_missing(varname, *args, &proc) # :nodoc:
    if args.empty?
               variables[varname.to_s]
    else
	super(varname, *args, &proc)
    end
end

Class Attribute Details

.loaded_packagesObject (readonly)

Returns the value of attribute loaded_packages.



45
46
47
# File 'lib/utilrb/pkgconfig.rb', line 45

def loaded_packages
  @loaded_packages
end

Instance Attribute Details

#descriptionObject (readonly)

Returns the value of attribute description.



143
144
145
# File 'lib/utilrb/pkgconfig.rb', line 143

def description
  @description
end

#fieldsObject (readonly)

Returns the value of attribute fields.



151
152
153
# File 'lib/utilrb/pkgconfig.rb', line 151

def fields
  @fields
end

#fileObject (readonly)

Returns the value of attribute file.



138
139
140
# File 'lib/utilrb/pkgconfig.rb', line 138

def file
  @file
end

#nameObject (readonly)

The module name



142
143
144
# File 'lib/utilrb/pkgconfig.rb', line 142

def name
  @name
end

#pathObject (readonly)

Returns the value of attribute path.



139
140
141
# File 'lib/utilrb/pkgconfig.rb', line 139

def path
  @path
end

#raw_versionObject (readonly)

The module version as a string



145
146
147
# File 'lib/utilrb/pkgconfig.rb', line 145

def raw_version
  @raw_version
end

#variablesObject (readonly)

Information extracted from the file



150
151
152
# File 'lib/utilrb/pkgconfig.rb', line 150

def variables
  @variables
end

#versionObject (readonly)

The module version, as an array of integers



147
148
149
# File 'lib/utilrb/pkgconfig.rb', line 147

def version
  @version
end

Class Method Details

.available_package_namesObject



432
433
434
435
436
437
438
439
440
# File 'lib/utilrb/pkgconfig.rb', line 432

def self.available_package_names
    result = []
    each_pkgconfig_directory do |dir|
        Dir.glob(File.join(dir, "*.pc")) do |path|
            result << File.basename(path, ".pc")
        end
    end
    result
end

.clear_cacheObject



47
48
49
# File 'lib/utilrb/pkgconfig.rb', line 47

def clear_cache
    loaded_packages.clear
end

.default_search_pathObject

Returns the system-wide search path that is embedded in pkg-config



468
469
470
471
472
473
474
475
# File 'lib/utilrb/pkgconfig.rb', line 468

def self.default_search_path
    if !@default_search_path
        output = `LANG=C PKG_CONFIG_PATH= pkg-config --debug 2>&1`.split("\n")
        @default_search_path = output.grep(FOUND_PATH_RX).
            map { |l| l.gsub(FOUND_PATH_RX, '\1\2') }
    end
    return @default_search_path
end

.default_search_suffixesObject

Returns the system-wide standard suffixes that should be appended to new prefixes to find pkg-config files



480
481
482
483
484
485
486
487
488
489
490
491
492
# File 'lib/utilrb/pkgconfig.rb', line 480

def self.default_search_suffixes
    if !@default_search_suffixes
        output = `LANG=C PKG_CONFIG_PATH= pkg-config --debug 2>&1`.split("\n")
        found_paths = output.grep(FOUND_PATH_RX).
            map { |l| l.gsub(FOUND_PATH_RX, '\2') }.
            to_set
        not_found = output.grep(NONEXISTENT_PATH_RX).
            map { |l| l.gsub(NONEXISTENT_PATH_RX, '\1') }.
            to_set
        @default_search_suffixes = found_paths | not_found
    end
    return @default_search_suffixes
end

.define_pkgconfig_action(action) ⇒ Object

:nodoc:



324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/utilrb/pkgconfig.rb', line 324

def self.define_pkgconfig_action(action) # :nodoc:
           class_eval <<-EOD, __FILE__, __LINE__+1
           def pkgconfig_#{action.gsub(/-/, '_')}(static = false)
               if static
                   `pkg-config --#{action} --static \#{name}`.strip
               else
                   `pkg-config --#{action} \#{name}`.strip
               end
           end
           EOD
    nil
end

.each_package(regex = nil) ⇒ Object

Yields the package names of available packages. If regex is given, lists only the names that match the regular expression.



449
450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/utilrb/pkgconfig.rb', line 449

def self.each_package(regex = nil)
    seen = Set.new
    each_pkgconfig_directory do |dir|
        Dir.glob(File.join(dir, '*.pc')) do |file|
            pkg_name = File.basename(file, ".pc")
            next if seen.include?(pkg_name)
            next if regex && pkg_name !~ regex

            seen << pkg_name
            yield(pkg_name)
        end
    end
end

.each_pkgconfig_directory(&block) ⇒ Object



413
414
415
416
417
418
# File 'lib/utilrb/pkgconfig.rb', line 413

def self.each_pkgconfig_directory(&block)
    if path = ENV['PKG_CONFIG_PATH']
        path.split(':').each(&block)
    end
    default_search_path.each(&block)
end

.find_all_package_files(name) ⇒ Object

Returns true if there is a package with this name



421
422
423
424
425
426
427
428
429
430
# File 'lib/utilrb/pkgconfig.rb', line 421

def self.find_all_package_files(name)
    result = []
    each_pkgconfig_directory do |dir|
        path = File.join(dir, "#{name}.pc")
        if File.exist?(path)
            result << path
        end
    end
    result
end

.find_matching_version(candidates, version_spec) ⇒ Object

Returns the first package in candidates that match the given version spec



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/utilrb/pkgconfig.rb', line 93

def self.find_matching_version(candidates, version_spec)
    if version_spec
        version_spec =~ /([<>=]+)\s*([\d\.]+)/
        op, requested_version = $1, $2

        requested_op =
            if op == "=" then [0]
            elsif op == ">" then [1]
            elsif op == "<" then [-1]
            elsif op == "<=" then [-1, 0]
            elsif op == ">=" then [1, 0]
            end

        requested_version = requested_version.split('.').map { |v| Integer(v) }

        result = candidates.find do |pkg|
            requested_op.include?(pkg.version <=> requested_version)
        end
        if !result
            name = candidates.first.name
            raise NotFound.new(name), "no version of #{name} match #{version_spec}. Available versions are: #{candidates.map(&:raw_version).join(", ")}"
        end
        result
    else
        candidates.first
    end
end

.get(name, version_spec = nil, preset_variables = Hash.new) ⇒ Object

Returns the pkg-config object that matches the given name, and optionally a version string



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/utilrb/pkgconfig.rb', line 62

def self.get(name, version_spec = nil, preset_variables = Hash.new)
    if !(candidates = loaded_packages[name])
        paths = find_all_package_files(name)
        if paths.empty?
            raise NotFound.new(name), "cannot find the pkg-config specification for #{name}"
        end

        candidates = loaded_packages[name] = Array.new
        paths.each do |p|
            candidates << PkgConfig.load(p, preset_variables)
        end
    end

    # Now try to find a matching spec
    find_matching_version(candidates, version_spec)
end

.has_package?(name) ⇒ Boolean

Returns true if there is a package with this name

Returns:

  • (Boolean)


443
444
445
# File 'lib/utilrb/pkgconfig.rb', line 443

def self.has_package?(name)
    !find_all_package_files(name).empty?
end

.load(path, preset_variables) ⇒ Object



53
54
55
56
57
58
# File 'lib/utilrb/pkgconfig.rb', line 53

def self.load(path, preset_variables)
    pkg_name = File.basename(path, ".pc")
    pkg = Class.instance_method(:new).bind(PkgConfig).call(pkg_name)
    pkg.load(path, preset_variables)
    pkg
end

.new(name, version_spec = nil, options = Hash.new) ⇒ PkgConfig

Finds the provided package and optional version and returns its PkgConfig description

number“, where op is < <= >= > or == and the version number X, X.y, …

Parameters:

  • version_spec (String) (defaults to: nil)

    version specification, of the form “op

Returns:

Raises:

  • (NotFound)

    if the package is not found



87
88
89
# File 'lib/utilrb/pkgconfig.rb', line 87

def self.new(name, version_spec = nil, options = Hash.new)
    get(name, version_spec, options)
end

.parse_dependencies(string) ⇒ Object



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/utilrb/pkgconfig.rb', line 180

def self.parse_dependencies(string)
    if string =~ /,/
        packages = string.split(',')
    else
        packages = []
        words = string.split(' ')
        while !words.empty?
            w = words.shift
            if w =~ /[<>=]/
                packages[-1] += " #{w} #{words.shift}"
            else
                packages << w
            end
        end
    end

    result = packages.map do |dep|
        dep = dep.strip
        if dep =~ /^(#{PACKAGE_NAME_RX})\s*([=<>]+.*)/
            PkgConfig.get($1, $2.strip)
        else
            PkgConfig.get(dep)
        end
    end
    result
end

Instance Method Details

#cflagsObject



369
370
371
# File 'lib/utilrb/pkgconfig.rb', line 369

def cflags
    @cflags.join(" ")
end

#cflags_only_IObject



373
374
375
# File 'lib/utilrb/pkgconfig.rb', line 373

def cflags_only_I
    @cflags.grep(/^-I/).join(" ")
end

#cflags_only_otherObject



377
378
379
# File 'lib/utilrb/pkgconfig.rb', line 377

def cflags_only_other
    @cflags.find_all { |s| s !~ /^-I/ }.join(" ")
end

#expand_variables(value, variables, current) ⇒ Object

Helper method that expands $word in value using the name to value map variables

current is a string that describes what we are expanding. It is used to detect recursion in expansion of variables, and to give meaningful errors to the user



167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/utilrb/pkgconfig.rb', line 167

def expand_variables(value, variables, current)
    value = value.gsub(/\$\{(\w+)\}/) do |rx|
        expand_name = $1
        if expand_name == current
            raise "error in pkg-config file #{path}: #{current} contains a reference to itself"
        elsif !(expanded = variables[expand_name])
            raise "error in pkg-config file #{path}: #{current} contains a reference to #{expand_name} but there is no such variable"
        end
        expanded
    end
    value
end

#include_dirsObject

Returns the list of include directories listed in the Cflags: section of the pkgconfig file



343
344
345
346
347
348
349
# File 'lib/utilrb/pkgconfig.rb', line 343

def include_dirs
    result = Shellwords.shellsplit(cflags_only_I).map { |v| v[2..-1] }
    if result.any?(&:empty?)
        raise Invalid.new(name), "empty include directory (-I without argument) found in pkg-config package #{name}"
    end
    result
end

#library_dirsObject

Returns the list of library directories listed in the Libs: section of the pkgconfig file



353
354
355
356
357
358
359
# File 'lib/utilrb/pkgconfig.rb', line 353

def library_dirs
    result = Shellwords.shellsplit(libs_only_L).map { |v| v[2..-1] }
    if result.any?(&:empty?)
        raise Invalid.new(name), "empty link directory (-L without argument) found in pkg-config package #{name}"
    end
    result
end

#libs(static = false) ⇒ Object



389
390
391
# File 'lib/utilrb/pkgconfig.rb', line 389

def libs(static = false)
    @ldflags_with_requires[static].join(" ")
end

#libs_only_l(static = false) ⇒ Object



397
398
399
# File 'lib/utilrb/pkgconfig.rb', line 397

def libs_only_l(static = false)
    @ldflags_with_requires[static].grep(/^-l/).join(" ")
end

#libs_only_L(static = false) ⇒ Object



393
394
395
# File 'lib/utilrb/pkgconfig.rb', line 393

def libs_only_L(static = false)
    @ldflags_with_requires[static].grep(/^-L/).join(" ")
end

#libs_only_other(static = false) ⇒ Object



401
402
403
# File 'lib/utilrb/pkgconfig.rb', line 401

def libs_only_other(static = false)
    @ldflags_with_requires[static].find_all { |s| s !~ /^-[lL]/ }.join(" ")
end

#load(path, preset_variables = Hash.new) ⇒ Object

Loads the information contained in path



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/utilrb/pkgconfig.rb', line 210

def load(path, preset_variables = Hash.new)
    @path = path
    @file = File.readlines(path).map(&:strip)

    raw_variables = preset_variables.dup
    raw_fields    = Hash.new

    running_line = nil
    @file = file.map do |line|
        line = line.gsub(/\s*#.*$/, '')
        line = line.strip
        next if line.empty?

        value = line.gsub(/\\$/, '')
        if running_line
            running_line << " " << value
        end

        if line =~ /\\$/
            running_line ||= value
        elsif running_line
            running_line = nil
        else
            value
        end
    end.compact


    file.each do |line|
        case line
        when /^(#{VAR_NAME_RX})\s*=(.*)/
            raw_variables[$1] = $2.strip
        when /^(#{FIELD_NAME_RX}):\s*(.*)/
            raw_fields[$1] = $2.strip
        else
            raise NotImplementedError, "#{path}: cannot parse pkg-config line #{line.inspect}"
        end
    end

    # Resolve the variables
    while variables.size != raw_variables.size
        raw_variables.each do |name, value|
            value = expand_variables(value, raw_variables, name)
            raw_variables[name] = value
            if value !~ /\$\{#{VAR_NAME_RX}\}/
                variables[name] = value
            end
        end
    end

    # Shell-split the fields, and expand the variables in them
    raw_fields.each do |name, value|
        if SHELL_VARS.include?(name) 
            value = Shellwords.shellsplit(value)
            resolved = Array.new
            while !value.empty?
                value = value.flat_map do |v|
                    expanded = expand_variables(v, variables, name)
                    if expanded == v
                        resolved << v
                        nil
                    else
                        Shellwords.shellsplit(expanded)
                    end
                end.compact
            end
            fields[name] = resolved
        else
            fields[name] = expand_variables(value, variables, name)
        end

    end

    # Initialize the main flags
    @raw_version = (fields['Version'] || '')
    @version = raw_version.split('.').map { |v| Integer(v) if v =~ /^\d+$/ }.compact
    @description = (fields['Description'] || '')

    # Get the requires/conflicts
    @requires  = PkgConfig.parse_dependencies(fields['Requires'] || '')
    @requires_private  = PkgConfig.parse_dependencies(fields['Requires.private'] || '')
    @conflicts = PkgConfig.parse_dependencies(fields['Conflicts'] || '')

    # And finally resolve the compilation flags
    @cflags = fields['Cflags'] || []
    @requires.each do |pkg|
        @cflags.concat(pkg.raw_cflags)
    end
    @requires_private.each do |pkg|
        @cflags.concat(pkg.raw_cflags)
    end
    @cflags.uniq!
    @cflags.delete('-I/usr/include')
    @ldflags = Hash.new
    @ldflags[false] = fields['Libs'] || []
    @ldflags[false].delete('-L/usr/lib')
    @ldflags[false].uniq!
    @ldflags[true] = @ldflags[false] + (fields['Libs.private'] || [])
    @ldflags[true].delete('-L/usr/lib')
    @ldflags[true].uniq!

    @ldflags_with_requires = {
        true => @ldflags[true].dup,
        false => @ldflags[false].dup
    }
    @requires.each do |pkg|
        @ldflags_with_requires[true].concat(pkg.raw_ldflags_with_requires[true])
        @ldflags_with_requires[false].concat(pkg.raw_ldflags_with_requires[false])
    end
    @requires_private.each do |pkg|
        @ldflags_with_requires[true].concat(pkg.raw_ldflags_with_requires[true])
    end
end

#pkgconfig_variable(varname) ⇒ Object



337
338
339
# File 'lib/utilrb/pkgconfig.rb', line 337

def pkgconfig_variable(varname)
    `pkg-config --variable=#{varname}`.strip
end

#raw_cflagsObject



365
366
367
# File 'lib/utilrb/pkgconfig.rb', line 365

def raw_cflags
    @cflags
end

#raw_ldflagsObject



381
382
383
# File 'lib/utilrb/pkgconfig.rb', line 381

def raw_ldflags
    @ldflags
end

#raw_ldflags_with_requiresObject



385
386
387
# File 'lib/utilrb/pkgconfig.rb', line 385

def raw_ldflags_with_requires
    @ldflags_with_requires
end