diff --git a/.gitignore b/.gitignore index fc3791f..7158171 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Gemfile.lock pkg ports tmp +mkmf.log diff --git a/examples/Rakefile b/examples/Rakefile index 85f8b90..cc1bd28 100644 --- a/examples/Rakefile +++ b/examples/Rakefile @@ -3,8 +3,6 @@ require 'rbconfig' $: << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) require "mini_portile2" -require "mkmf" - recipes = [] recipe_hooks = {} @@ -132,10 +130,9 @@ yaml.files = [{ }] recipes.push(yaml) recipe_hooks["yaml"] = lambda do |recipe| - conf = pkg_config(File.join(recipe.path, "lib", "pkgconfig", "yaml-0.1.pc")) - puts "pkg_config: #{conf.inspect}" + recipe.activate_mkmf(pkgconf: File.join(recipe.path, "lib", "pkgconfig", "yaml-0.1.pc")) - expected = "-L" + MiniPortile.native_path(File.join(recipe.path, "lib")) + expected = "-L" + File.join(recipe.path, "lib") $LDFLAGS.split.include?(expected) or raise(<<~MSG) assertion failed: LDFLAGS not updated correctly: #{$LDFLAGS} @@ -158,9 +155,10 @@ namespace :ports do desc "Install port #{recipe.name} #{recipe.version}" task recipe.name => ["ports"] do |t| recipe.cook - recipe.activate if hook = recipe_hooks[recipe.name] hook.call(recipe) + else + recipe.activate end end diff --git a/lib/mini_portile2/mini_portile.rb b/lib/mini_portile2/mini_portile.rb index 2538a47..b9db74b 100644 --- a/lib/mini_portile2/mini_portile.rb +++ b/lib/mini_portile2/mini_portile.rb @@ -94,6 +94,44 @@ def self.posix_path(path) end end + # + # this minimal version of pkg_config is based on ruby 29dc9378 (2023-01-09) + # + # specifically with the fix from b90e56e6 to support multiple pkg-config options + # + def self.pkg_config(pkg, *options) + if options.empty? + raise ArgumentError, "no pkg-config options are given" + end + + fmt = "not found" + def fmt.%(x) + x ? x.inspect : self + end + + MakeMakefile.checking_for "mini_portile pkg-config for #{pkg}", fmt do + if ($PKGCONFIG ||= + (pkgconfig = MakeMakefile.with_config("pkg-config") {MakeMakefile.config_string("PKG_CONFIG") || "pkg-config"}) && + MakeMakefile.find_executable0(pkgconfig) && pkgconfig) + # default to pkg-config command + pkgconfig = $PKGCONFIG + args = [pkg] + elsif MakeMakefile.find_executable0(pkgconfig = "#{pkg}-config") + # default to package specific config command, as a last resort. + else + raise RuntimeError, "pkg-config is not found" + end + + opts = Array(options).map { |o| "--#{o}" } + + output = MakeMakefile.xpopen([pkgconfig, *opts, *args], err:[:child, :out], &:read) + MakeMakefile::Logging.open {puts output.each_line.map{|s|"=> #{s.inspect}"}} + raise RuntimeError, output unless $?.success? + + output.strip + end + end + def initialize(name, version, **kwargs) @name = name @version = version @@ -236,7 +274,21 @@ def cook return true end - def activate + def activate(mkmf: false, pkgconf: nil) + if mkmf + activate_mkmf(pkgconf: pkgconf) + else + activate_classic(pkgconf: pkgconf) + end + end + + def activate_classic(pkgconf: nil) + if pkgconf + # because I don't know if anybody has this use case, + # I just haven't bothered to implement this yet. + raise NotImplementedError, "MiniPortile#activate classic does not yet support a pkgconf. If you want this functionality, please open an issue! ♥ ♥ ♥" + end + lib_path = File.join(port_path, "lib") vars = { 'PATH' => File.join(port_path, 'bin'), @@ -268,6 +320,33 @@ def activate end end + def activate_mkmf(pkgconf: nil) + if !pkgconf + raise NotImplementedError, "MiniPortile#activate for mkmf does not yet work without a pkgconf. If you want this functionality, please open an issue! ♥ ♥ ♥" + end + + unless File.exist?(pkgconf) + raise RuntimeError, "pkg-config file '#{pkgconf}' does not exist" + end + + output "Activating #{@name} #{@version} using #{pkgconf} ..." + + require "mkmf" + + # on macos, pkg-config will not return --cflags without this + ENV["PKG_CONFIG_ALLOW_SYSTEM_CFLAGS"] = "t" + + cflags = MiniPortile.pkg_config(pkgconf, "cflags") + ldflags = MiniPortile.pkg_config(pkgconf, "libs", "static") + + $CFLAGS << " " << cflags + $LDFLAGS << " " << ldflags + + # make sure future calls to activate_mkmf are able to find this pkg-config + # e.g., in nokogiri, the libxslt pc file references libxml2 + ENV["PKG_CONFIG_PATH"] = "#{native_path(File.dirname(pkgconf))}#{File::PATH_SEPARATOR}#{ENV["PKG_CONFIG_PATH"]}" + end + def path File.expand_path(port_path) end @@ -656,4 +735,8 @@ def with_tempfile(filename, full_path) FileUtils.mkdir_p File.dirname(full_path) FileUtils.mv temp_file.path, full_path, :force => true end + + def abort_pkg_config(id) + abort("\nCould not configure the build properly (#{id}). Please install either the `pkg-config` utility or the `pkg-config` rubygem.\n\n") + end end diff --git a/test/assets/pkgconf/libxml2/libxml-2.0.pc b/test/assets/pkgconf/libxml2/libxml-2.0.pc new file mode 100644 index 0000000..d6601b3 --- /dev/null +++ b/test/assets/pkgconf/libxml2/libxml-2.0.pc @@ -0,0 +1,13 @@ +prefix=/foo/libxml2/2.11.5 +exec_prefix=${prefix} +libdir=/foo/libxml2/2.11.5/lib +includedir=${prefix}/include +modules=1 + +Name: libXML +Version: 2.11.5 +Description: libXML library version2. +Requires: +Libs: -L${libdir} -lxml2 +Libs.private: -L/foo/zlib/1.3/lib -lz -lm +Cflags: -I${includedir}/libxml2 diff --git a/test/assets/pkgconf/libxslt/libxslt.pc b/test/assets/pkgconf/libxslt/libxslt.pc new file mode 100644 index 0000000..0000042 --- /dev/null +++ b/test/assets/pkgconf/libxslt/libxslt.pc @@ -0,0 +1,13 @@ +prefix=/foo/libxslt/1.1.38 +exec_prefix=${prefix} +libdir=/foo/libxslt/1.1.38/lib +includedir=${prefix}/include + + +Name: libxslt +Version: 1.1.38 +Description: XSLT library version 2. +Requires: libxml-2.0 +Cflags: -I${includedir} +Libs: -L${libdir} -lxslt +Libs.private: -lm diff --git a/test/test_activate.rb b/test/test_activate.rb index f175af0..828c350 100644 --- a/test/test_activate.rb +++ b/test/test_activate.rb @@ -3,14 +3,17 @@ class TestActivate < TestCase attr_reader :recipe + LIBXML_PC = File.join(__dir__, "assets", "pkgconf", "libxml2", "libxml-2.0.pc") + LIBXSLT_PC = File.join(__dir__, "assets", "pkgconf", "libxslt", "libxslt.pc") + def setup super - @save_env = %w[PATH CPATH LIBRARY_PATH LDFLAGS].inject({}) do |env, var| + @save_env = %w[PATH CPATH LIBRARY_PATH LDFLAGS PKG_CONFIG_PATH].inject({}) do |env, var| env.update(var => ENV[var]) end - $LDFLAGS = nil - $CPPFLAGS = nil + $LDFLAGS = "" + $CFLAGS = "" FileUtils.rm_rf(["tmp", "ports"]) # remove any previous test files @@ -22,8 +25,8 @@ def setup def teardown FileUtils.rm_rf(["tmp", "ports"]) # remove any previous test files - $LDFLAGS = nil - $CPPFLAGS = nil + $LDFLAGS = "" + $CFLAGS = "" @save_env.each do |var, val| ENV[var] = val end @@ -119,6 +122,64 @@ def test_LDFLAGS_env_var_when_cross_compiling assert_equal(flag_elements('LDFLAGS').first, "-L#{lib_path}") end + def test_activate_classic_with_pkgconf_raises_an_exception + assert_raises(NotImplementedError) do + recipe.activate(pkgconf: "fake/path/to/foo.pc") + end + + assert_raises(NotImplementedError) do + recipe.activate_classic(pkgconf: "fake/path/to/foo.pc") + end + end + + def test_activate_mkmf_without_pkgconf_raises_an_exception + assert_raises(NotImplementedError) do + recipe.activate(mkmf: true) + end + + assert_raises(NotImplementedError) do + recipe.activate_mkmf + end + end + + def test_activate_mkmf_pkgconf_does_not_exist + assert_raises(RuntimeError) do + recipe.activate(mkmf: true, pkgconf: "nonexistent/foo.pc") + end + end + + def test_activate_mkmf_LDFLAGS_global + # can't get pkgconf to install on windows with ruby 2.3 + skip if MiniPortile.windows? && RUBY_VERSION < "2.4" + + recipe.activate(mkmf: true, pkgconf: LIBXML_PC) + + assert_includes($LDFLAGS.split, "-lxml2") + assert_includes($LDFLAGS.split, "-L/foo/libxml2/2.11.5/lib") + end + + def test_activate_mkmf_CFLAGS_global + # can't get pkgconf to install on windows with ruby 2.3 + skip if MiniPortile.windows? && RUBY_VERSION < "2.4" + + recipe.activate(mkmf: true, pkgconf: LIBXML_PC) + + assert_includes($CFLAGS.split, "-I/foo/libxml2/2.11.5/include/libxml2") + end + + def test_activate_mkmf_chains_pkgconf_path + # can't get pkgconf to install on windows with ruby 2.3 + skip if MiniPortile.windows? && RUBY_VERSION < "2.4" + + recipe.activate(mkmf: true, pkgconf: LIBXML_PC) + recipe.activate(mkmf: true, pkgconf: LIBXSLT_PC) + + xmlflags = $CFLAGS.split.select { |v| v == "-I/foo/libxml2/2.11.5/include/libxml2" } + + # the -I from libxml should be present twice -- once from libxml2 and once from libxslt (transitively) + assert_equal(2, xmlflags.size, $CFLAGS) + end + private def path_elements(varname)