diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 502092e..245d6b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,5 +67,22 @@ jobs: - uses: actions/cache@v3 with: path: examples/ports/archives - key: ${{ matrix.platform }}-examples-${{ hashFiles('examples/Rakefile') }} + key: examples-${{ hashFiles('examples/Rakefile') }} + - run: bundle exec rake test:examples + + fedora: # see https://github.com/flavorjones/mini_portile/issues/118 + runs-on: ubuntu-latest + container: + image: fedora:35 + steps: + - run: | + dnf group install -y "C Development Tools and Libraries" + dnf install -y ruby ruby-devel libyaml-devel git-all patch cmake xz + - uses: actions/checkout@v3 + - uses: actions/cache@v3 + with: + path: examples/ports/archives + key: examples-${{ hashFiles('examples/Rakefile') }} + - run: bundle install + - run: bundle exec rake test:unit - run: bundle exec rake test:examples 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 7e60c55..340b305 100644 --- a/examples/Rakefile +++ b/examples/Rakefile @@ -4,6 +4,7 @@ $: << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) require "mini_portile2" recipes = [] +recipe_hooks = {} def windows? RbConfig::CONFIG['target_os'] =~ /mswin|mingw32/ @@ -119,6 +120,29 @@ zlib.files << { recipes.push zlib +# +# libyaml, using pkgconf for configuration +# +yaml = MiniPortile.new("yaml", "0.2.5") +yaml.files = [{ + url: "https://github.com/yaml/libyaml/releases/download/0.2.5/yaml-0.2.5.tar.gz", + sha256: "c642ae9b75fee120b2d96c712538bd2cf283228d2337df2cf2988e3c02678ef4", + }] +recipes.unshift(yaml) +recipe_hooks["yaml"] = lambda do |recipe| + recipe.mkmf_config(pkg: "yaml-0.1") + + expected = "-L" + File.join(recipe.path, "lib") + $LDFLAGS.split.include?(expected) or raise(<<~MSG) + assertion failed: LDFLAGS not updated correctly: + #{$LDFLAGS} + should have included '#{expected}' + MSG + + unless have_library("yaml", "yaml_get_version", "yaml.h") + raise("could not find libyaml development environment") + end +end namespace :ports do directory "ports" @@ -135,7 +159,11 @@ 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 task :all => recipe.name @@ -146,7 +174,10 @@ namespace :ports do recipes.each do |recipe| puts "Artifacts of '#{recipe.name}' in '#{recipe.path}'" end - puts "LDFLAGS: " + ENV['LDFLAGS'].inspect + puts "LIBRARY_PATH: #{ENV['LIBRARY_PATH'].inspect}" + puts "LDFLAGS: #{ENV['LDFLAGS'].inspect}" + puts "$LDFLAGS: #{$LDFLAGS.inspect}" + puts "$CFLAGS: #{$CFLAGS.inspect}" end end diff --git a/lib/mini_portile2/mini_portile.rb b/lib/mini_portile2/mini_portile.rb index e23733e..d701f21 100644 --- a/lib/mini_portile2/mini_portile.rb +++ b/lib/mini_portile2/mini_portile.rb @@ -76,6 +76,24 @@ def self.target_cpu RbConfig::CONFIG['target_cpu'] end + def self.native_path(path) + path = File.expand_path(path) + if File::ALT_SEPARATOR + path.tr(File::SEPARATOR, File::ALT_SEPARATOR) + else + path + end + end + + def self.posix_path(path) + path = File.expand_path(path) + if File::ALT_SEPARATOR + "/" + path.tr(File::ALT_SEPARATOR, File::SEPARATOR).tr(":", File::SEPARATOR) + else + path + end + end + def initialize(name, version, **kwargs) @name = name @version = version @@ -240,7 +258,7 @@ def activate # rely on LDFLAGS when cross-compiling if File.exist?(lib_path) && (@host != @original_host) - full_path = File.expand_path(lib_path) + full_path = native_path(lib_path) old_value = ENV.fetch("LDFLAGS", "") @@ -250,6 +268,43 @@ def activate end end + def mkmf_config(pkg: nil, dir: nil) + require "mkmf" + + if pkg + dir ||= File.join(path, "lib", "pkgconfig") + pcfile = File.join(dir, "#{pkg}.pc") + unless File.exist?(pcfile) + raise ArgumentError, "pkg-config file '#{pcfile}' does not exist" + end + + output "Configuring MakeMakefile for #{File.basename(pcfile)} (in #{File.dirname(pcfile)})\n" + + # on macos, pkg-config will not return --cflags without this + ENV["PKG_CONFIG_ALLOW_SYSTEM_CFLAGS"] = "t" + + # append to PKG_CONFIG_PATH as we go, so later pkg-config files can depend on earlier ones + ENV["PKG_CONFIG_PATH"] = [ENV["PKG_CONFIG_PATH"], dir].compact.join(File::PATH_SEPARATOR) + + cflags = minimal_pkg_config(pcfile, "cflags") + ldflags = minimal_pkg_config(pcfile, "libs", "static") + else + output "Configuring MakeMakefile for #{@name} #{@version} (from #{path})\n" + + include_path = File.join(path, "include") + lib_path = File.join(path, "lib") + + lib_name = name.sub(/\Alib/, "") # TODO: use delete_prefix when we no longer support ruby 2.4 + + cflags = "-I#{include_path}" if Dir.exist?(include_path) + ldflags = "-L#{lib_path} -l#{lib_name}" if Dir.exist?(lib_path) + end + + $CFLAGS << " " << cflags if cflags + $CXXFLAGS << " " << cflags if cflags + $LDFLAGS << " " << ldflags if ldflags + end + def path File.expand_path(port_path) end @@ -265,21 +320,11 @@ def make_cmd private def native_path(path) - path = File.expand_path(path) - if File::ALT_SEPARATOR - path.tr(File::SEPARATOR, File::ALT_SEPARATOR) - else - path - end + MiniPortile.native_path(path) end def posix_path(path) - path = File.expand_path(path) - if File::ALT_SEPARATOR - "/" + path.tr(File::ALT_SEPARATOR, File::SEPARATOR).tr(":", File::SEPARATOR) - else - path - end + MiniPortile.posix_path(path) end def tmp_path @@ -648,4 +693,29 @@ def with_tempfile(filename, full_path) FileUtils.mkdir_p File.dirname(full_path) FileUtils.mv temp_file.path, full_path, :force => true 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, and removing + # code paths that aren't helpful for mini-portile's use case of parsing pc files. + # + def minimal_pkg_config(pkg, *pcoptions) + if pcoptions.empty? + raise ArgumentError, "no pkg-config options are given" + end + + if ($PKGCONFIG ||= + (pkgconfig = MakeMakefile.with_config("pkg-config") {MakeMakefile.config_string("PKG_CONFIG") || "pkg-config"}) && + MakeMakefile.find_executable0(pkgconfig) && pkgconfig) + pkgconfig = $PKGCONFIG + else + raise RuntimeError, "pkg-config is not found" + end + + pcoptions = Array(pcoptions).map { |o| "--#{o}" } + response = IO.popen([pkgconfig, *pcoptions, pkg], err:[:child, :out], &:read) + raise RuntimeError, response unless $?.success? + response.strip + 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/libexslt.pc b/test/assets/pkgconf/libxslt/libexslt.pc new file mode 100644 index 0000000..7deb33e --- /dev/null +++ b/test/assets/pkgconf/libxslt/libexslt.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: libexslt +Version: 0.8.21 +Description: EXSLT Extension library +Requires: libxml-2.0, libxslt +Cflags: -I${includedir} +Libs: -L${libdir} -lexslt +Libs.private: -lm 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 new file mode 100644 index 0000000..ef0d059 --- /dev/null +++ b/test/test_activate.rb @@ -0,0 +1,139 @@ +require File.expand_path('../helper', __FILE__) + +class TestActivate < TestCase + attr_reader :recipe + + def setup + super + + @save_env = %w[PATH CPATH LIBRARY_PATH LDFLAGS].inject({}) do |env, var| + env.update(var => ENV[var]) + end + + FileUtils.rm_rf(["tmp", "ports"]) # remove any previous test files + + @recipe = MiniPortile.new("foo", "1.0.0").tap do |recipe| + recipe.logger = StringIO.new + end + end + + def teardown + FileUtils.rm_rf(["tmp", "ports"]) # remove any previous test files + + @save_env.each do |var, val| + ENV[var] = val + end + + super + end + + def test_PATH_env_var_when_bin_does_not_exist + ENV["PATH"] = "foo" + refute(Dir.exist?(bin_path)) + refute_includes(path_elements('PATH'), bin_path) + + recipe.activate + + refute_includes(path_elements('PATH'), bin_path) + end + + def test_PATH_env_var_when_bin_exists + ENV["PATH"] = "foo" + FileUtils.mkdir_p(bin_path) + refute_includes(path_elements('PATH'), bin_path) + + recipe.activate + + assert_includes(path_elements('PATH'), bin_path) + assert_equal(path_elements('PATH').first, bin_path) + end + + def test_CPATH_env_var_when_include_does_not_exist + ENV["CPATH"] = "foo" + refute(Dir.exist?(include_path)) + refute_includes(path_elements('CPATH'), include_path) + + recipe.activate + + refute_includes(path_elements('CPATH'), include_path) + end + + def test_CPATH_env_var_when_include_exists + ENV["CPATH"] = "foo" + FileUtils.mkdir_p(include_path) + refute_includes(path_elements('CPATH'), include_path) + + recipe.activate + + assert_includes(path_elements('CPATH'), include_path) + assert_equal(path_elements('CPATH').first, include_path) + end + + def test_LIBRARY_PATH_env_var_when_lib_does_not_exist + ENV["LIBRARY_PATH"] = "foo" + refute(Dir.exist?(lib_path)) + refute_includes(path_elements('LIBRARY_PATH'), lib_path) + + recipe.activate + + refute_includes(path_elements('LIBRARY_PATH'), lib_path) + end + + def test_LIBRARY_PATH_env_var_when_lib_exists + ENV["LIBRARY_PATH"] = "foo" + FileUtils.mkdir_p(lib_path) + refute_includes(path_elements('LIBRARY_PATH'), lib_path) + + recipe.activate + + assert_includes(path_elements('LIBRARY_PATH'), lib_path) + assert_equal(path_elements('LIBRARY_PATH').first, lib_path) + end + + def test_LDFLAGS_env_var_when_not_cross_compiling + ENV["LDFLAGS"] = "-lfoo" + FileUtils.mkdir_p(lib_path) + assert_equal(recipe.host, recipe.original_host) # assert on setup) + + refute_includes(flag_elements('LDFLAGS'), "-L#{lib_path}") + + recipe.activate + + refute_includes(flag_elements('LDFLAGS'), "-L#{lib_path}") + end + + def test_LDFLAGS_env_var_when_cross_compiling + ENV["LDFLAGS"] = "-lfoo" + recipe.host = recipe.original_host + "-x" # make them not-equal + FileUtils.mkdir_p(lib_path) + + refute_includes(flag_elements('LDFLAGS'), "-L#{lib_path}") + + recipe.activate + + assert_includes(flag_elements('LDFLAGS'), "-L#{lib_path}") + assert_equal(flag_elements('LDFLAGS').first, "-L#{lib_path}") + end + + private + + def path_elements(varname) + ENV.fetch(varname, "").split(File::PATH_SEPARATOR) + end + + def flag_elements(varname) + ENV.fetch(varname, "").split + end + + def bin_path + MiniPortile.native_path(File.join(recipe.path, "bin")) + end + + def include_path + MiniPortile.native_path(File.join(recipe.path, "include")) + end + + def lib_path + MiniPortile.native_path(File.join(recipe.path, "lib")) + end +end diff --git a/test/test_mkmf_config.rb b/test/test_mkmf_config.rb new file mode 100644 index 0000000..8bef90b --- /dev/null +++ b/test/test_mkmf_config.rb @@ -0,0 +1,133 @@ +require File.expand_path('../helper', __FILE__) + +class TestMkmfConfig < TestCase + attr_reader :recipe, :include_path, :lib_path + + LIBXML_PCP = File.join(__dir__, "assets", "pkgconf", "libxml2") + LIBXSLT_PCP = File.join(__dir__, "assets", "pkgconf", "libxslt") + + def setup + super + + @save_env = %w[PATH CPATH LIBRARY_PATH LDFLAGS PKG_CONFIG_PATH].inject({}) do |env, var| + env.update(var => ENV[var]) + end + $LDFLAGS = "" + $CFLAGS = "" + + FileUtils.rm_rf(["tmp", "ports"]) # remove any previous test files + + @recipe = MiniPortile.new("libfoo", "1.0.0").tap do |recipe| + recipe.logger = StringIO.new + end + @include_path = File.join(@recipe.path, "include") + @lib_path = File.join(@recipe.path, "lib") + end + + def teardown + FileUtils.rm_rf(["tmp", "ports"]) # remove any previous test files + + $LDFLAGS = "" + $CFLAGS = "" + @save_env.each do |var, val| + ENV[var] = val + end + + super + end + + def test_mkmf_config_recipe_LDFLAGS_global_lib_dir_does_not_exist + recipe.mkmf_config + + refute_includes($LDFLAGS.split, "-L#{lib_path}") + refute_includes($LDFLAGS.split, "-lfoo") + end + + def test_mkmf_config_recipe_LDFLAGS_global + FileUtils.mkdir_p(lib_path) + + recipe.mkmf_config + + assert_includes($LDFLAGS.split, "-L#{lib_path}") + assert_includes($LDFLAGS.split, "-lfoo") # note the recipe name is "libfoo" + end + + def test_mkmf_config_recipe_CFLAGS_global_include_dir_does_not_exist + recipe.mkmf_config + + refute_includes($CFLAGS.split, "-I#{include_path}") + end + + def test_mkmf_config_recipe_CFLAGS_global + FileUtils.mkdir_p(include_path) + + recipe.mkmf_config + + assert_includes($CFLAGS.split, "-I#{include_path}") + end + + def test_mkmf_config_pkgconf_does_not_exist + assert_raises(ArgumentError) do + recipe.mkmf_config(pkg: "foo") + end + end + + def test_mkmf_config_pkgconf_LDFLAGS_global + # can't get the pkgconf utility to install on windows with ruby 2.3 in CI + skip if MiniPortile.windows? && RUBY_VERSION < "2.4" + + recipe.mkmf_config(pkg: "libxml-2.0", dir: LIBXML_PCP) + + assert_includes($LDFLAGS.split, "-L/foo/libxml2/2.11.5/lib") + assert_includes($LDFLAGS.split, "-lxml2") + end + + def test_mkmf_config_pkgconf_CFLAGS_global + # can't get the pkgconf utility to install on windows with ruby 2.3 in CI + skip if MiniPortile.windows? && RUBY_VERSION < "2.4" + + recipe.mkmf_config(pkg: "libxml-2.0", dir: LIBXML_PCP) + + assert_includes($CFLAGS.split, "-I/foo/libxml2/2.11.5/include/libxml2") + end + + def test_mkmf_config_pkgconf_path_accumulation + # can't get the pkgconf utility to install on windows with ruby 2.3 in CI + skip if MiniPortile.windows? && RUBY_VERSION < "2.4" + + (ENV["PKG_CONFIG_PATH"] || "").split(File::PATH_SEPARATOR).tap do |pcpaths| + refute_includes(pcpaths, LIBXML_PCP) + refute_includes(pcpaths, LIBXSLT_PCP) + end + + recipe.mkmf_config(pkg: "libxml-2.0", dir: LIBXML_PCP) + + ENV["PKG_CONFIG_PATH"].split(File::PATH_SEPARATOR).tap do |pcpaths| + assert_includes(pcpaths, LIBXML_PCP) + refute_includes(pcpaths, LIBXSLT_PCP) + end + + recipe.mkmf_config(pkg: "libxslt", dir: LIBXSLT_PCP) + + ENV["PKG_CONFIG_PATH"].split(File::PATH_SEPARATOR).tap do |pcpaths| + assert_includes(pcpaths, LIBXML_PCP) + assert_includes(pcpaths, LIBXSLT_PCP) + end + + recipe.mkmf_config(pkg: "libexslt", dir: LIBXSLT_PCP) + + $CFLAGS.split.tap do |cflags| + assert_includes(cflags, "-I/foo/libxml2/2.11.5/include/libxml2") + assert_includes(cflags, "-I/foo/libxslt/1.1.38/include") + end + $LDFLAGS.split.tap do |ldflags| + assert_includes(ldflags, "-L/foo/libxml2/2.11.5/lib") + assert_includes(ldflags, "-lxml2") + assert_includes(ldflags, "-L/foo/libxslt/1.1.38/lib") + assert_includes(ldflags, "-lxslt") + assert_includes(ldflags, "-lexslt") + assert_includes(ldflags, "-L/foo/zlib/1.3/lib") # from `--static` + assert_includes(ldflags, "-lz") # from `--static` + end + end +end