From 527a813ca1ee70f7df7fb22cf319a9d0cfae7cbd Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 17 Jul 2023 16:11:57 -0700 Subject: [PATCH 1/2] [cmake] Automatically add required cross-compilation variables For the standard `make`, mini_portile provides the `configure` script with the `--host` parameter, and the script figures out the target compilers to use. With CMake, we don't have that benefit so we need to set the C and C++ compilers ourselves. This commit uses a simple heuristic to find the target compilers: 1. For macOS, use `clang` and `clang++`. Otherwise use `gcc` and `gcc++`. 2. We search the PATH for `-compiler`. Use that if we find it. Otherwise default to the base compilers. To make CMake work with cross-compilation, a number of variables have to be set: - CMAKE_SYSTEM_PROCESSOR - CMAKE_SYSTEM_NAME - CMAKE_C_COMPILER - CMAKE_CXX_COMPILER Closes #128 --- lib/mini_portile2/mini_portile.rb | 34 +++++- lib/mini_portile2/mini_portile_cmake.rb | 84 +++++++++++-- test/test_cmake.rb | 155 +++++++++++++++++++++--- 3 files changed, 244 insertions(+), 29 deletions(-) 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..c7bfb35 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,103 @@ def test_make_command_configuration end end + def test_configure_defaults_with_macos + recipe = init_recipe + recipe.host = 'some-host' + + with_env({ "CC" => nil, "CXX" => nil }) do + 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 + 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 +196,47 @@ 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 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) From b8a42de36fe156670e18459a06b3eaa96b15f85a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 18 Jul 2023 00:06:59 -0700 Subject: [PATCH 2/2] Remap x64 processor type to x86_64 Ruby compiled for x64-mingw-ucrt appears to return a processor type of x64, but CMake only understands x86_64. --- lib/mini_portile2/mini_portile_cmake.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/mini_portile2/mini_portile_cmake.rb b/lib/mini_portile2/mini_portile_cmake.rb index 81c4158..8543645 100644 --- a/lib/mini_portile2/mini_portile_cmake.rb +++ b/lib/mini_portile2/mini_portile_cmake.rb @@ -67,7 +67,7 @@ def cmake_compile_flags # 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_SYSTEM_PROCESSOR=#{cpu_type}", "-DCMAKE_C_COMPILER=#{c_compiler}", "-DCMAKE_CXX_COMPILER=#{cxx_compiler}" ] @@ -129,4 +129,10 @@ def generator_available?(generator_type) stdout_str.include?("#{generator_type} Makefiles") end + + def cpu_type + return 'x86_64' if MiniPortile.target_cpu == 'x64' + + MiniPortile.target_cpu + end end