# Copyright 2008-2022 Sutou Kouhei # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA begin require_relative "pkg-config/version" rescue LoadError end require "English" require "pathname" require "rbconfig" require "shellwords" class PackageConfig class Error < StandardError end class NotFoundError < Error end SEPARATOR = File::PATH_SEPARATOR class << self @native_pkg_config = nil def native_pkg_config @native_pkg_config ||= guess_native_pkg_config end @native_pkg_config_prefix = nil def native_pkg_config_prefix @native_pkg_config_prefix ||= compute_native_pkg_config_prefix end @custom_override_variables = nil def custom_override_variables @custom_override_variables ||= with_config("override-variables", "") end def clear_configure_args_cache @native_pkg_config = nil @native_pkg_config_prefix = nil @custom_override_variables = nil end private def with_config(config, default=nil) if defined?(super) super else default end end def guess_native_pkg_config exeext = RbConfig::CONFIG["EXEEXT"] default_pkg_config = ENV["PKG_CONFIG"] || "pkg-config#{exeext}" pkg_config = with_config("pkg-config", default_pkg_config) pkg_config = Pathname.new(pkg_config) unless pkg_config.absolute? found_pkg_config = search_executable_from_path(pkg_config) pkg_config = found_pkg_config if found_pkg_config end unless pkg_config.absolute? found_pkg_config = search_pkg_config_by_dln_find_exe(pkg_config) pkg_config = found_pkg_config if found_pkg_config end pkg_config end def search_executable_from_path(name) (ENV["PATH"] || "").split(SEPARATOR).each do |path| try_name = Pathname(path) + name return try_name if try_name.executable? end nil end def search_pkg_config_by_dln_find_exe(pkg_config) begin require "dl/import" rescue LoadError return nil end dln = Module.new dln.module_eval do if DL.const_defined?(:Importer) extend DL::Importer else extend DL::Importable end begin dlload RbConfig::CONFIG["LIBRUBY"] rescue RuntimeError return nil if $!.message == "unknown error" return nil if /: image not found\z/ =~ $!.message raise rescue DL::DLError return nil end begin extern "const char *dln_find_exe(const char *, const char *)" rescue DL::DLError return nil end end path = dln.dln_find_exe(pkg_config.to_s, nil) if path.nil? or path.size.zero? nil else Pathname(path.to_s) end end def compute_native_pkg_config_prefix pkg_config = native_pkg_config return nil unless pkg_config.absolute? return nil unless pkg_config.exist? pkg_config_prefix = pkg_config.parent.parent if File::ALT_SEPARATOR normalized_pkg_config_prefix = pkg_config_prefix.to_s.split(File::ALT_SEPARATOR).join(File::SEPARATOR) Pathname(normalized_pkg_config_prefix) else pkg_config_prefix end end end attr_reader :name attr_reader :paths attr_accessor :msvc_syntax def initialize(name, options={}) if Pathname(name).absolute? @pc_path = name @path_position = 0 @name = File.basename(@pc_path, ".*") else @pc_path = nil @path_position = nil @name = name end @options = options path = @options[:path] || ENV["PKG_CONFIG_PATH"] @paths = [path, guess_default_path].compact.join(SEPARATOR).split(SEPARATOR) @paths.unshift(*(@options[:paths] || [])) @paths = normalize_paths(@paths) @msvc_syntax = @options[:msvc_syntax] @variables = @declarations = nil override_variables = self.class.custom_override_variables @override_variables = parse_override_variables(override_variables) default_override_variables = @options[:override_variables] || {} @override_variables = default_override_variables.merge(@override_variables) end def exist? not pc_path.nil? end def requires parse_requires(declaration("Requires")) end def requires_private parse_requires(declaration("Requires.private")) end def cflags path_flags, other_flags = collect_cflags (other_flags + path_flags).join(" ") end def cflags_only_I collect_cflags[0].join(" ") end def cflags_only_other collect_cflags[1].join(" ") end def libs path_flags, other_flags = collect_libs (path_flags + other_flags).join(" ") end def libs_only_l collect_libs[1].find_all do |arg| if @msvc_syntax /\.lib\z/ =~ arg else /\A-l/ =~ arg end end.join(" ") end def libs_only_L collect_libs[0].find_all do |arg| if @msvc_syntax /\A\/libpath:/ =~ arg else /\A-L/ =~ arg end end.join(" ") end def version declaration("Version") end def description declaration("Description") end def variable(name) parse_pc if @variables.nil? expand_value(@override_variables[name] || @variables[name]) end def declaration(name) parse_pc if @declarations.nil? expand_value(@declarations[name]) end def pc_path if @pc_path return @pc_path if File.exist?(@pc_path) else @paths.each_with_index do |path, i| _pc_path = File.join(path, "#{@name}.pc") if File.exist?(_pc_path) @path_position = i + 1 return _pc_path end end end nil end protected def path_position @path_position end def collect_requires(processed_packages={}, &block) packages = [] targets = yield(self) targets.each do |name| next if processed_packages.key?(name) package = self.class.new(name, @options) processed_packages[name] = package packages << package packages.concat(package.collect_requires(processed_packages, &block)) end packages_without_self = packages.reject do |package| package.name == @name end packages_without_self.uniq do |package| package.name end end private def sort_packages(packages) packages.sort_by.with_index do |package, i| [package.path_position, i] end end def collect_cflags target_packages = sort_packages([self, *all_required_packages]) cflags_set = [] target_packages.each do |package| cflags_set << package.declaration("Cflags") end all_cflags = normalize_cflags(Shellwords.split(cflags_set.join(" "))) path_flags, other_flags = all_cflags.partition {|flag| /\A-I/ =~ flag} path_flags = normalize_path_flags(path_flags, "-I") path_flags = path_flags.reject do |flag| flag == "-I/usr/include" end path_flags = path_flags.uniq if @msvc_syntax path_flags = path_flags.collect do |flag| flag.gsub(/\A-I/, "/I") end end [path_flags, other_flags] end def normalize_path_flags(path_flags, flag_option) return path_flags unless /-mingw(?:32|-ucrt)\z/ === RUBY_PLATFORM pkg_config_prefix = self.class.native_pkg_config_prefix return path_flags unless pkg_config_prefix mingw_dir = pkg_config_prefix.basename.to_s path_flags.collect do |path_flag| path = path_flag.sub(/\A#{Regexp.escape(flag_option)}/, "") path = path.sub(/\A\/#{Regexp.escape(mingw_dir)}/i) do pkg_config_prefix.to_s end "#{flag_option}#{path}" end end def normalize_cflags(cflags) normalized_cflags = [] enumerator = cflags.to_enum begin loop do cflag = enumerator.next normalized_cflags << cflag case cflag when "-I" normalized_cflags << enumerator.next end end rescue StopIteration end normalized_cflags end def collect_libs target_packages = sort_packages(required_packages + [self]) libs_set = [] target_packages.each do |package| libs_set << package.declaration("Libs") end all_flags = split_lib_flags(libs_set.join(" ")) path_flags, other_flags = all_flags.partition {|flag| /\A-L/ =~ flag} path_flags = normalize_path_flags(path_flags, "-L") path_flags = path_flags.reject do |flag| /\A-L\/usr\/lib(?:64|x32)?\z/ =~ flag end path_flags = path_flags.uniq if @msvc_syntax path_flags = path_flags.collect do |flag| flag.gsub(/\A-L/, "/libpath:") end other_flags = other_flags.collect do |flag| if /\A-l/ =~ flag "#{$POSTMATCH}.lib" else flag end end end [path_flags, other_flags] end def split_lib_flags(libs_command_line) all_flags = {} flags = [] in_option = false libs_command_line.gsub(/-([Ll]) /, "\\1").split.each do |arg| if in_option flags << arg in_option = false else case arg when /-[lL]/ next if all_flags.key?(arg) all_flags[arg] = true flags << arg in_option = true else flags << arg end end end flags end IDENTIFIER_RE = /[a-zA-Z\d_\.]+/ def parse_pc raise NotFoundError, ".pc doesn't exist: <#{@name}>" unless exist? @variables = {} @declarations = {} File.open(pc_path) do |input| input.each_line do |line| line = line.gsub(/#.*/, "").strip next if line.empty? case line when /^(#{IDENTIFIER_RE})=/ @variables[$1] = $POSTMATCH.strip when /^(#{IDENTIFIER_RE}):/ @declarations[$1] = $POSTMATCH.strip end end end end def parse_requires(requires) return [] if requires.nil? requires_without_version = requires.gsub(/[<>]?=\s*[\d.a-zA-Z_-]+\s*/, "") requires_without_version.split(/[,\s]+/) end def parse_override_variables(override_variables) variables = {} override_variables.split(",").each do |variable| name, value = variable.split("=", 2) variables[name] = value end variables end def expand_value(value) return nil if value.nil? value.gsub(/\$\{(#{IDENTIFIER_RE})\}/) do variable($1) end end def guess_default_path arch_depended_path = Dir.glob("/usr/lib/*/pkgconfig") default_paths = [ "/usr/local/lib64/pkgconfig", "/usr/local/libx32/pkgconfig", "/usr/local/lib/pkgconfig", "/usr/local/libdata/pkgconfig", "/usr/local/share/pkgconfig", "/opt/local/lib/pkgconfig", *arch_depended_path, "/usr/lib64/pkgconfig", "/usr/libx32/pkgconfig", "/usr/lib/pkgconfig", "/usr/libdata/pkgconfig", "/usr/X11R6/lib/pkgconfig", "/usr/X11R6/share/pkgconfig", "/usr/X11/lib/pkgconfig", "/opt/X11/lib/pkgconfig", "/usr/share/pkgconfig", ] if Object.const_defined?(:RubyInstaller) mingw_bin_path = RubyInstaller::Runtime.msys2_installation.mingw_bin_path mingw_pkgconfig_path = Pathname.new(mingw_bin_path) + "../lib/pkgconfig" default_paths.unshift(mingw_pkgconfig_path.cleanpath.to_s) end libdir = ENV["PKG_CONFIG_LIBDIR"] default_paths.unshift(libdir) if libdir paths = [] pkg_config_prefix = self.class.native_pkg_config_prefix if pkg_config_prefix pkg_config_arch_depended_paths = Dir.glob((pkg_config_prefix + "lib/*/pkgconfig").to_s) paths.concat(pkg_config_arch_depended_paths) paths << (pkg_config_prefix + "lib64/pkgconfig").to_s paths << (pkg_config_prefix + "libx32/pkgconfig").to_s paths << (pkg_config_prefix + "lib/pkgconfig").to_s paths << (pkg_config_prefix + "libdata/pkgconfig").to_s paths << (pkg_config_prefix + "share/pkgconfig").to_s end if /-darwin\d[\d\.]*\z/ =~ RUBY_PLATFORM and /\A(\d+\.\d+)/ =~ `sw_vers -productVersion` mac_os_version = $1 homebrew_repository_candidates = [] if pkg_config_prefix brew_path = pkg_config_prefix + "bin" + "brew" if brew_path.exist? escaped_brew_path = Shellwords.escape(brew_path.to_s) homebrew_repository = `#{escaped_brew_path} --repository`.chomp homebrew_repository_candidates << Pathname.new(homebrew_repository) else homebrew_repository_candidates << pkg_config_prefix + "Homebrew" homebrew_repository_candidates << pkg_config_prefix end end brew = self.class.__send__(:search_executable_from_path, "brew") if brew homebrew_repository = `brew --repository`.chomp homebrew_repository_candidates << Pathname(homebrew_repository) end homebrew_repository_candidates.uniq.each do |candidate| pkgconfig_base_path = candidate + "Library/Homebrew/os/mac/pkgconfig" path = pkgconfig_base_path + mac_os_version unless path.exist? path = pkgconfig_base_path + mac_os_version.gsub(/\.\d+\z/, "") end paths << path.to_s if path.exist? end end paths.concat(default_paths) paths.join(SEPARATOR) end def required_packages collect_requires do |package| package.requires end end def all_required_packages collect_requires do |package| package.requires_private + package.requires end end def normalize_paths(paths) paths.reject do |path| path.empty? or !File.exist?(path) end end end module PKGConfig @@paths = [] @@override_variables = {} module_function def add_path(path) @@paths << path end def set_override_variable(key, value) @@override_variables[key] = value end def msvc? /mswin/.match(RUBY_PLATFORM) and /^cl\b/.match(RbConfig::CONFIG["CC"]) end def package_config(package) PackageConfig.new(package, :msvc_syntax => msvc?, :override_variables => @@override_variables, :paths => @@paths) end def exist?(pkg) package_config(pkg).exist? end def libs(pkg) package_config(pkg).libs end def libs_only_l(pkg) package_config(pkg).libs_only_l end def libs_only_L(pkg) package_config(pkg).libs_only_L end def cflags(pkg) package_config(pkg).cflags end def cflags_only_I(pkg) package_config(pkg).cflags_only_I end def cflags_only_other(pkg) package_config(pkg).cflags_only_other end def modversion(pkg) package_config(pkg).version end def description(pkg) package_config(pkg).description end def variable(pkg, name) package_config(pkg).variable(name) end def check_version?(pkg, major=0, minor=0, micro=0) return false unless exist?(pkg) ver = modversion(pkg).split(".").collect {|item| item.to_i} (0..2).each {|i| ver[i] = 0 unless ver[i]} (ver[0] > major || (ver[0] == major && ver[1] > minor) || (ver[0] == major && ver[1] == minor && ver[2] >= micro)) end def have_package(pkg, major=nil, minor=0, micro=0) message = "#{pkg}" unless major.nil? message << " version (>= #{major}.#{minor}.#{micro})" end major ||= 0 enough_version = checking_for(checking_message(message), "%s") do if check_version?(pkg, major, minor, micro) "yes (#{modversion(pkg)})" else "no" end end if enough_version libraries = libs_only_l(pkg) dldflags = libs(pkg) dldflags = (Shellwords.shellwords(dldflags) - Shellwords.shellwords(libraries)) dldflags = dldflags.map {|s| /\s/ =~ s ? "\"#{s}\"" : s }.join(" ") $libs += " " + libraries if /mswin/ =~ RUBY_PLATFORM $DLDFLAGS += " " + dldflags else $LDFLAGS += " " + dldflags end $CFLAGS += " " + cflags_only_other(pkg) if defined?($CXXFLAGS) $CXXFLAGS += " " + cflags_only_other(pkg) end $INCFLAGS += " " + cflags_only_I(pkg) end enough_version end end