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|share)\/.*)'$/
NONEXISTENT_PATH_RX =
/Cannot open directory '.*\/((?:lib|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



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

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:



395
396
397
398
399
400
401
# File 'lib/utilrb/pkgconfig.rb', line 395

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.



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

def description
  @description
end

#fieldsObject (readonly)

Returns the value of attribute fields.



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

def fields
  @fields
end

#fileObject (readonly)

Returns the value of attribute file.



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

def file
  @file
end

#nameObject (readonly)

The module name



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

def name
  @name
end

#pathObject (readonly)

Returns the value of attribute path.



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

def path
  @path
end

#raw_versionObject (readonly)

The module version as a string



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

def raw_version
  @raw_version
end

#variablesObject (readonly)

Information extracted from the file



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

def variables
  @variables
end

#versionObject (readonly)

The module version, as an array of integers



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

def version
  @version
end

Class Method Details

.available_package_namesObject



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

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



458
459
460
461
462
463
464
465
# File 'lib/utilrb/pkgconfig.rb', line 458

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



469
470
471
472
473
474
475
476
477
478
479
480
481
# File 'lib/utilrb/pkgconfig.rb', line 469

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:



314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/utilrb/pkgconfig.rb', line 314

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.



439
440
441
442
443
444
445
446
447
448
449
450
451
# File 'lib/utilrb/pkgconfig.rb', line 439

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



403
404
405
406
407
408
# File 'lib/utilrb/pkgconfig.rb', line 403

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



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

def self.find_all_package_files(name)
    result = []
    each_pkgconfig_directory do |dir|
        path = File.join(dir, "#{name}.pc")
        if File.exists?(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
# 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
            raise NotFound.new(name), "no version of #{name} match #{version_spect}. Available versions are: #{candidates.map(&:raw_version).join(", ")}"
        end
        result
    else
        candidates.first
    end
end

.get(name, version_spec = nil) ⇒ 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)
    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)
        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)


433
434
435
# File 'lib/utilrb/pkgconfig.rb', line 433

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

.load(path) ⇒ Object



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

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

.new(name, version_spec = nil) ⇒ 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)
    get(name, version_spec)
end

.parse_dependencies(string) ⇒ Object



179
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
# File 'lib/utilrb/pkgconfig.rb', line 179

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



359
360
361
# File 'lib/utilrb/pkgconfig.rb', line 359

def cflags
    @cflags.join(" ")
end

#cflags_only_IObject



363
364
365
# File 'lib/utilrb/pkgconfig.rb', line 363

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

#cflags_only_otherObject



367
368
369
# File 'lib/utilrb/pkgconfig.rb', line 367

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



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

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



333
334
335
336
337
338
339
# File 'lib/utilrb/pkgconfig.rb', line 333

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



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

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



379
380
381
# File 'lib/utilrb/pkgconfig.rb', line 379

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

#libs_only_l(static = false) ⇒ Object



387
388
389
# File 'lib/utilrb/pkgconfig.rb', line 387

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

#libs_only_L(static = false) ⇒ Object



383
384
385
# File 'lib/utilrb/pkgconfig.rb', line 383

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

#libs_only_other(static = false) ⇒ Object



391
392
393
# File 'lib/utilrb/pkgconfig.rb', line 391

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

#load(path) ⇒ Object

Loads the information contained in path



209
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
# File 'lib/utilrb/pkgconfig.rb', line 209

def load(path)
    @path = path
    @file = File.readlines(path).map(&:strip)

    raw_variables = Hash.new
    raw_fields    = Hash.new

    running_line = nil
    @file = file.map do |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)
            value.map! do |v|
                expand_variables(v, variables, name)
            end
        else
            value = expand_variables(value, variables, name)
        end

        fields[name] = value
    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



327
328
329
# File 'lib/utilrb/pkgconfig.rb', line 327

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

#raw_cflagsObject



355
356
357
# File 'lib/utilrb/pkgconfig.rb', line 355

def raw_cflags
    @cflags
end

#raw_ldflagsObject



371
372
373
# File 'lib/utilrb/pkgconfig.rb', line 371

def raw_ldflags
    @ldflags
end

#raw_ldflags_with_requiresObject



375
376
377
# File 'lib/utilrb/pkgconfig.rb', line 375

def raw_ldflags_with_requires
    @ldflags_with_requires
end