Skip to content

Commit

Permalink
[cmake] Automatically add required cross-compilation variables
Browse files Browse the repository at this point in the history
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 `<host>-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 flavorjones#128
  • Loading branch information
stanhu committed Jul 18, 2023
1 parent 9ac9ee8 commit c76c45d
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 29 deletions.
34 changes: 31 additions & 3 deletions lib/mini_portile2/mini_portile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
84 changes: 77 additions & 7 deletions lib/mini_portile2/mini_portile_cmake.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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")

Expand Down
153 changes: 134 additions & 19 deletions test/test_cmake.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -114,12 +194,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)
Expand Down

0 comments on commit c76c45d

Please sign in to comment.