Skip to content

Commit

Permalink
feat: introduce activate(mkmf: true) with support for pkg-config
Browse files Browse the repository at this point in the history
which will set $LDFLAGS and $CFLAGS according to the pkg-config file

This should allow gems with C extensions that use mini_portile to pass
in the pkg-config and everything should just work.
  • Loading branch information
flavorjones committed Sep 10, 2023
1 parent eb88a08 commit 2bc8ae3
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 10 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ Gemfile.lock
pkg
ports
tmp
mkmf.log
8 changes: 3 additions & 5 deletions examples/Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ require 'rbconfig'
$: << File.expand_path(File.join(File.dirname(__FILE__), "../lib"))
require "mini_portile2"

require "mkmf"

recipes = []
recipe_hooks = {}

Expand Down Expand Up @@ -132,8 +130,7 @@ 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"))
$LDFLAGS.split.include?(expected) or raise(<<~MSG)
Expand All @@ -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

Expand Down
47 changes: 46 additions & 1 deletion lib/mini_portile2/mini_portile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,21 @@ def cook
return true
end

def activate
def activate(mkmf: false, pkgconf: nil)
if mkmf
activate_mkmf(pkgconf:)
else
activate_classic(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'),
Expand Down Expand Up @@ -268,6 +282,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_relative "mkmf_patches"

# on macos, pkg-config will not return --cflags without this
ENV["PKG_CONFIG_ALLOW_SYSTEM_CFLAGS"] = "t"

cflags = MakeMakefile.pkg_config(pkgconf, "cflags")
ldflags = MakeMakefile.pkg_config(pkgconf, "libs", "static")

MakeMakefile.append_cflags(cflags)
MakeMakefile.append_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
Expand Down Expand Up @@ -656,4 +697,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
81 changes: 81 additions & 0 deletions lib/mini_portile2/mkmf_patches.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# frozen-string-literal: false

require "mkmf"

module MakeMakefile
#
# this version of pkg_config is taken from ruby 29dc9378 (2023-01-09)
#
# specifically with the fix from b90e56e6 to support multiple pkg-config options
#
def pkg_config(pkg, *options)
fmt = "not found"
def fmt.%(x)
x ? x.inspect : self
end

checking_for "pkg-config for #{pkg}", fmt do
_, ldir = dir_config(pkg)
if ldir
pkg_config_path = "#{ldir}/pkgconfig"
if File.directory?(pkg_config_path)
Logging.message("PKG_CONFIG_PATH = %s\n", pkg_config_path)
envs = ["PKG_CONFIG_PATH"=>[pkg_config_path, ENV["PKG_CONFIG_PATH"]].compact.join(File::PATH_SEPARATOR)]
end
end
if pkgconfig = with_config("#{pkg}-config") and find_executable0(pkgconfig)
# if and only if package specific config command is given
elsif ($PKGCONFIG ||=
(pkgconfig = with_config("pkg-config") {config_string("PKG_CONFIG") || "pkg-config"}) &&
find_executable0(pkgconfig) && pkgconfig) and
xsystem([*envs, $PKGCONFIG, "--exists", pkg])
# default to pkg-config command
pkgconfig = $PKGCONFIG
args = [pkg]
elsif find_executable0(pkgconfig = "#{pkg}-config")
# default to package specific config command, as a last resort.
else
pkgconfig = nil
end
if pkgconfig
get = proc {|opts|
opts = Array(opts).map { |o| "--#{o}" }
opts = xpopen([*envs, pkgconfig, *opts, *args], err:[:child, :out], &:read)
Logging.open {puts opts.each_line.map{|s|"=> #{s.inspect}"}}
opts.strip if $?.success?
}
end
orig_ldflags = $LDFLAGS
if get and !options.empty?
get[options]
elsif get and try_ldflags(ldflags = get['libs'])
if incflags = get['cflags-only-I']
$INCFLAGS << " " << incflags
cflags = get['cflags-only-other']
else
cflags = get['cflags']
end
libs = get['libs-only-l']
if cflags
$CFLAGS += " " << cflags
$CXXFLAGS += " " << cflags
end
if libs
ldflags = (Shellwords.shellwords(ldflags) - Shellwords.shellwords(libs)).quote.join(" ")
else
libs, ldflags = Shellwords.shellwords(ldflags).partition {|s| s =~ /-l([^ ]+)/ }.map {|l|l.quote.join(" ")}
end
$libs += " " << libs

$LDFLAGS = [orig_ldflags, ldflags].join(' ')
Logging::message "package configuration for %s\n", pkg
Logging::message "incflags: %s\ncflags: %s\nldflags: %s\nlibs: %s\n\n",
incflags, cflags, ldflags, libs
[[incflags, cflags].join(' '), ldflags, libs]
else
Logging::message "package configuration for %s is not found\n", pkg
nil
end
end
end
end
13 changes: 13 additions & 0 deletions test/assets/pkgconf/libxml2/libxml-2.0.pc
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions test/assets/pkgconf/libxslt/libxslt.pc
Original file line number Diff line number Diff line change
@@ -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
60 changes: 56 additions & 4 deletions test/test_activate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
env.update(var => ENV[var])
end
$LDFLAGS = nil
$CPPFLAGS = nil
$LDFLAGS = ""
$CFLAGS = ""

FileUtils.rm_rf(["tmp", "ports"]) # remove any previous test files

Expand All @@ -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
Expand Down Expand Up @@ -119,6 +122,55 @@ 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
recipe.activate(mkmf: true, pkgconf: LIBXML_PC)

assert_includes($LDFLAGS.split, "-lxml2")
assert_includes($LDFLAGS.split, "-L#{MiniPortile.native_path("/foo/libxml2/2.11.5/lib")}")
end

def test_activate_mkmf_CFLAGS_global
recipe.activate(mkmf: true, pkgconf: LIBXML_PC)

assert_includes($CFLAGS.split, "-I#{MiniPortile.native_path("/foo/libxml2/2.11.5/include/libxml2")}")
end

def test_activate_mkmf_chains_pkgconf_path
recipe.activate(mkmf: true, pkgconf: LIBXML_PC)
recipe.activate(mkmf: true, pkgconf: LIBXSLT_PC)

xmlflags = $LDFLAGS.split.select { |v| v == "-L#{MiniPortile.native_path("/foo/libxml2/2.11.5/lib")}" }

# the -L from both libxml and libxslt should be present
assert_equal(2, xmlflags.size)
end

private

def path_elements(varname)
Expand Down

0 comments on commit 2bc8ae3

Please sign in to comment.