diff --git a/lib/mini_portile2/mini_portile.rb b/lib/mini_portile2/mini_portile.rb index 8110c04..4a30181 100644 --- a/lib/mini_portile2/mini_portile.rb +++ b/lib/mini_portile2/mini_portile.rb @@ -35,17 +35,45 @@ class MiniPortile attr_accessor :host, :files, :patch_files, :target, :logger, :source_directory def self.windows? - RbConfig::CONFIG['target_os'] =~ /mswin|mingw/ + target_os =~ /mswin|mingw/ end # GNU MinGW compiled Ruby? def self.mingw? - RbConfig::CONFIG['target_os'] =~ /mingw/ + target_os =~ /mingw/ end # MS Visual-C compiled Ruby? def self.mswin? - RbConfig::CONFIG['target_os'] =~ /mswin/ + target_os =~ /mswin/ + end + + def self.darwin? + target_os =~ /darwin/ + end + + def self.freebsd? + target_os =~ /freebsd/ + end + + def self.openbsd? + target_os =~ /openbsd/ + end + + def self.linux? + target_os =~ /linux/ + end + + def self.solaris? + target_os =~ /solaris/ + end + + def self.target_os + RbConfig::CONFIG['target_os'] + end + + def self.target_cpu + RbConfig::CONFIG['target_cpu'] end def initialize(name, version, **kwargs) diff --git a/lib/mini_portile2/mini_portile_cmake.rb b/lib/mini_portile2/mini_portile_cmake.rb index bb7f3e4..81c4158 100644 --- a/lib/mini_portile2/mini_portile_cmake.rb +++ b/lib/mini_portile2/mini_portile_cmake.rb @@ -2,6 +2,8 @@ require 'open3' class MiniPortileCMake < MiniPortile + attr_accessor :system_name + def configure_prefix "-DCMAKE_INSTALL_PREFIX=#{File.expand_path(port_path)}" end @@ -12,13 +14,10 @@ def initialize(name, version, **kwargs) end def configure_defaults - if MiniPortile.mswin? && generator_available?('NMake') - ['-G', 'NMake Makefiles'] - elsif MiniPortile.mingw? && generator_available?('MSYS') - ['-G', 'MSYS Makefiles'] - else - [] - end + [ + generator_defaults, + cmake_compile_flags, + ].flatten end def configure @@ -52,6 +51,77 @@ def cmake_cmd private + def generator_defaults + if MiniPortile.mswin? && generator_available?('NMake') + ['-G', 'NMake Makefiles'] + elsif MiniPortile.mingw? && generator_available?('MSYS') + ['-G', 'MSYS Makefiles'] + else + [] + end + end + + def cmake_compile_flags + c_compiler, cxx_compiler = find_c_and_cxx_compilers(host) + + # needed to ensure cross-compilation with CMake targets the right CPU and compilers + [ + "-DCMAKE_SYSTEM_NAME=#{cmake_system_name}", + "-DCMAKE_SYSTEM_PROCESSOR=#{MiniPortile.target_cpu}", + "-DCMAKE_C_COMPILER=#{c_compiler}", + "-DCMAKE_CXX_COMPILER=#{cxx_compiler}" + ] + end + + def find_compiler(compilers) + compilers.find { |binary| which(binary) } + end + + # configure automatically searches for the right compiler based on the + # `--host` parameter. However, CMake doesn't have an equivalent feature. + # Search for the right compiler for the target architecture using + # some basic heruistics. + def find_c_and_cxx_compilers(host) + c_compiler = ENV["CC"] + cxx_compiler = ENV["CXX"] + + if MiniPortile.darwin? + c_compiler ||= 'clang' + cxx_compiler ||='clang++' + else + c_compiler ||= 'gcc' + cxx_compiler ||= 'g++' + end + + c_platform_compiler = "#{host}-#{c_compiler}" + cxx_platform_compiler = "#{host}-#{cxx_compiler}" + c_compiler = find_compiler([c_platform_compiler, c_compiler]) + cxx_compiler = find_compiler([cxx_platform_compiler, cxx_compiler]) + + [c_compiler, cxx_compiler] + end + + # Full list: https://gitlab.kitware.com/cmake/cmake/-/blob/v3.26.4/Modules/CMakeDetermineSystem.cmake?ref_type=tags#L12-31 + def cmake_system_name + return system_name if system_name + + if MiniPortile.linux? + 'Linux' + elsif MiniPortile.darwin? + 'Darwin' + elsif MiniPortile.windows? + 'Windows' + elsif MiniPortile.freebsd? + 'FreeBSD' + elsif MiniPortile.openbsd? + 'OpenBSD' + elsif MiniPortile.solaris? + 'SunOS' + else + raise "Unable to set CMAKE_SYSTEM_NAME for #{MiniPortile.target_os}" + end + end + def generator_available?(generator_type) stdout_str, status = Open3.capture2("#{cmake_cmd} --help") diff --git a/test/test_cmake.rb b/test/test_cmake.rb index c64f3f3..5711284 100644 --- a/test/test_cmake.rb +++ b/test/test_cmake.rb @@ -14,13 +14,11 @@ def before_all create_tar(@tar_path, @assets_path, "test-cmake-1.0") start_webrick(File.dirname(@tar_path)) - @recipe = MiniPortileCMake.new("test-cmake", "1.0").tap do |recipe| - recipe.files << "http://localhost:#{HTTP_PORT}/#{ERB::Util.url_encode(File.basename(@tar_path))}" - recipe.patch_files << File.join(@assets_path, "patch 1.diff") - git_dir = File.join(@assets_path, "git") - with_custom_git_dir(git_dir) do - recipe.cook - end + @recipe = init_recipe + + git_dir = File.join(@assets_path, "git") + with_custom_git_dir(git_dir) do + recipe.cook end end @@ -57,6 +55,13 @@ def test_install binary = File.join(recipe.path, "bin", exe_name) assert File.exist?(binary), binary end + + def init_recipe + MiniPortileCMake.new("test-cmake", "1.0").tap do |recipe| + recipe.files << "http://localhost:#{HTTP_PORT}/#{ERB::Util.url_encode(File.basename(@tar_path))}" + recipe.patch_files << File.join(@assets_path, "patch 1.diff") + end + end end class TestCMakeConfig < TestCMake @@ -77,26 +82,101 @@ def test_make_command_configuration end end + def test_configure_defaults_with_macos + recipe = init_recipe + recipe.host = 'some-host' + + MiniPortile.stub(:darwin?, true) do + with_stubbed_target(os: 'darwin22', cpu: 'arm64') do + with_compilers(recipe, host_prefix: true, c_compiler: 'clang', cxx_compiler: 'clang++') do + Open3.stub(:capture2, cmake_help_mock('Unix')) do + assert_equal( + [ + "-DCMAKE_SYSTEM_NAME=Darwin", + "-DCMAKE_SYSTEM_PROCESSOR=arm64", + "-DCMAKE_C_COMPILER=some-host-clang", + "-DCMAKE_CXX_COMPILER=some-host-clang++" + ], + recipe.configure_defaults) + end + end + end + end + end + + def test_configure_defaults_with_manual_system_name + recipe = init_recipe + recipe.system_name = 'Custom' + + MiniPortile.stub(:darwin?, false) do + with_stubbed_target do + with_compilers(recipe) do + Open3.stub(:capture2, cmake_help_mock('Unix')) do + assert_equal( + [ + "-DCMAKE_SYSTEM_NAME=Custom", + "-DCMAKE_SYSTEM_PROCESSOR=x86_64", + "-DCMAKE_C_COMPILER=gcc", + "-DCMAKE_CXX_COMPILER=g++" + ], + recipe.configure_defaults) + end + end + end + end + end + def test_configure_defaults_with_unix_makefiles - Open3.stub(:capture2, cmake_help_mock('Unix')) do - MiniPortile.stub(:mingw?, true) do - assert_equal([], @recipe.configure_defaults) + recipe = init_recipe + + MiniPortile.stub(:linux?, true) do + MiniPortile.stub(:darwin?, false) do + with_stubbed_target do + with_compilers(recipe) do + Open3.stub(:capture2, cmake_help_mock('Unix')) do + MiniPortile.stub(:mingw?, true) do + assert_equal(default_x86_compile_flags, + recipe.configure_defaults) + end + end + end + end end end end def test_configure_defaults_with_msys_makefiles - Open3.stub(:capture2, cmake_help_mock('MSYS')) do - MiniPortile.stub(:mingw?, true) do - assert_equal(['-G', 'MSYS Makefiles'], @recipe.configure_defaults) + recipe = init_recipe + + MiniPortile.stub(:linux?, true) do + MiniPortile.stub(:darwin?, false) do + with_stubbed_target do + with_compilers(recipe) do + Open3.stub(:capture2, cmake_help_mock('MSYS')) do + MiniPortile.stub(:mingw?, true) do + assert_equal(['-G', 'MSYS Makefiles'] + default_x86_compile_flags, recipe.configure_defaults) + end + end + end + end end end end def test_configure_defaults_with_nmake_makefiles - Open3.stub(:capture2, cmake_help_mock('NMake')) do - MiniPortile.stub(:mswin?, true) do - assert_equal(['-G', 'NMake Makefiles'], @recipe.configure_defaults) + recipe = init_recipe + + MiniPortile.stub(:linux?, true) do + MiniPortile.stub(:darwin?, false) do + with_stubbed_target do + with_compilers(recipe) do + Open3.stub(:capture2, cmake_help_mock('NMake')) do + MiniPortile.stub(:mswin?, true) do + assert_equal(['-G', 'NMake Makefiles'] + default_x86_compile_flags, recipe.configure_defaults) + end + end + end + end end end end @@ -114,12 +194,56 @@ def test_cmake_command_configuration private + def with_stubbed_target(os: 'linux', cpu: 'x86_64') + MiniPortile.stub(:target_os, os) do + MiniPortile.stub(:target_cpu, cpu) do + yield + end + end + end + + def with_compilers(recipe, host_prefix: false, c_compiler: 'gcc', cxx_compiler: 'g++') + mock = MiniTest::Mock.new + + if host_prefix + mock.expect(:call, true, ["#{recipe.host}-#{c_compiler}"]) + mock.expect(:call, true, ["#{recipe.host}-#{cxx_compiler}"]) + else + mock.expect(:call, false, ["#{recipe.host}-#{c_compiler}"]) + mock.expect(:call, true, [c_compiler]) + mock.expect(:call, false, ["#{recipe.host}-#{cxx_compiler}"]) + mock.expect(:call, true, [cxx_compiler]) + end + + recipe.stub(:which, mock) do + yield + end + end + + def default_x86_compile_flags + [ + "-DCMAKE_SYSTEM_NAME=Linux", + "-DCMAKE_SYSTEM_PROCESSOR=x86_64", + "-DCMAKE_C_COMPILER=gcc", + "-DCMAKE_CXX_COMPILER=g++" + ] + end + + def default_macos_compile_flags + [ + "-DCMAKE_SYSTEM_NAME=Darwin", + "-DCMAKE_SYSTEM_PROCESSOR=arm64", + "-DCMAKE_C_COMPILER=some-host-clang", + "-DCMAKE_CXX_COMPILER=some-host-clang++" + ] + end + def cmake_help_mock(generator_type) open3_mock = MiniTest::Mock.new cmake_script = <<~SCRIPT - echo "The following generators are available on this platform (* marks default):" - echo "* #{generator_type} Makefiles = Generates standard #{generator_type.upcase} makefiles." - SCRIPT + echo "The following generators are available on this platform (* marks default):" + echo "* #{generator_type} Makefiles = Generates standard #{generator_type.upcase} makefiles." + SCRIPT exit_status = MiniTest::Mock.new exit_status.expect(:success?, true)