mirror of
https://github.com/Z3Prover/z3
synced 2026-03-10 15:20:30 +00:00
278 lines
11 KiB
Python
278 lines
11 KiB
Python
############################################
|
|
# Copyright (c) 2024 Microsoft Corporation
|
|
#
|
|
# Unit tests for JNI architecture flags in Makefile generation.
|
|
#
|
|
# Regression tests for:
|
|
# "JNI bindings use wrong architecture in macOS cross-compilation (arm64 to x64)"
|
|
#
|
|
# The fix ensures that libz3java.dylib (and the JNI link step) uses
|
|
# $(SLINK_EXTRA_FLAGS) instead of a hardcoded -arch arm64.
|
|
# $(SLINK_EXTRA_FLAGS) is populated correctly in mk_config() for:
|
|
# - Native ARM64 builds: SLINK_EXTRA_FLAGS contains -arch arm64
|
|
# - Cross-compile to x86_64: SLINK_EXTRA_FLAGS contains -arch x86_64
|
|
# - Other platforms: SLINK_EXTRA_FLAGS has no -arch flag
|
|
############################################
|
|
import io
|
|
import os
|
|
import sys
|
|
import unittest
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
# Add the scripts directory to the path so we can import mk_util
|
|
_SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
if _SCRIPTS_DIR not in sys.path:
|
|
sys.path.insert(0, _SCRIPTS_DIR)
|
|
|
|
import mk_util
|
|
|
|
|
|
class TestJNIArchitectureFlagsInMakefile(unittest.TestCase):
|
|
"""
|
|
Tests that JavaDLLComponent.mk_makefile() generates a JNI link command
|
|
that uses $(SLINK_EXTRA_FLAGS) rather than hardcoding -arch arm64.
|
|
|
|
$(SLINK_EXTRA_FLAGS) is set by mk_config() to contain the correct -arch
|
|
flag for the TARGET architecture (not the host), so using it ensures
|
|
cross-compilation works correctly.
|
|
"""
|
|
|
|
def setUp(self):
|
|
"""Save mk_util global state before each test."""
|
|
self._saved_components = list(mk_util._Components)
|
|
self._saved_names = set(mk_util._ComponentNames)
|
|
self._saved_name2component = dict(mk_util._Name2Component)
|
|
self._saved_id = mk_util._Id
|
|
self._saved_javac = mk_util.JAVAC
|
|
self._saved_jar = mk_util.JAR
|
|
|
|
def tearDown(self):
|
|
"""Restore mk_util global state after each test."""
|
|
mk_util._Components[:] = self._saved_components
|
|
mk_util._ComponentNames.clear()
|
|
mk_util._ComponentNames.update(self._saved_names)
|
|
mk_util._Name2Component.clear()
|
|
mk_util._Name2Component.update(self._saved_name2component)
|
|
mk_util._Id = self._saved_id
|
|
mk_util.JAVAC = self._saved_javac
|
|
mk_util.JAR = self._saved_jar
|
|
|
|
def _make_java_dll_component(self):
|
|
"""
|
|
Create a JavaDLLComponent instance bypassing the registry check so
|
|
that tests remain independent of each other.
|
|
"""
|
|
# Register a stub 'api' component that provides to_src_dir
|
|
api_stub = MagicMock()
|
|
api_stub.to_src_dir = '../src/api'
|
|
mk_util._Name2Component['api'] = api_stub
|
|
mk_util._ComponentNames.add('api')
|
|
|
|
# Build the component without going through the full Component.__init__
|
|
# registration path (which enforces uniqueness globally).
|
|
comp = mk_util.JavaDLLComponent.__new__(mk_util.JavaDLLComponent)
|
|
comp.name = 'java'
|
|
comp.dll_name = 'libz3java'
|
|
comp.package_name = 'com.microsoft.z3'
|
|
comp.manifest_file = None
|
|
comp.to_src_dir = '../src/api/java'
|
|
comp.src_dir = 'src/api/java'
|
|
comp.deps = []
|
|
comp.install = True
|
|
return comp
|
|
|
|
def _generate_makefile(self, comp, *, is_windows, is_osx, is_arch_arm64):
|
|
"""
|
|
Call mk_makefile() with the given platform flags and return the
|
|
generated Makefile text.
|
|
"""
|
|
buf = io.StringIO()
|
|
with patch.object(mk_util, 'JAVA_ENABLED', True), \
|
|
patch.object(mk_util, 'IS_WINDOWS', is_windows), \
|
|
patch.object(mk_util, 'IS_OSX', is_osx), \
|
|
patch.object(mk_util, 'IS_ARCH_ARM64', is_arch_arm64), \
|
|
patch.object(mk_util, 'JNI_HOME', '/path/to/jni'), \
|
|
patch.object(mk_util, 'JAVAC', 'javac'), \
|
|
patch.object(mk_util, 'JAR', 'jar'), \
|
|
patch.object(mk_util, 'BUILD_DIR', '/tmp/test_build'), \
|
|
patch('mk_util.mk_dir'), \
|
|
patch('mk_util.get_java_files', return_value=[]):
|
|
comp.mk_makefile(buf)
|
|
return buf.getvalue()
|
|
|
|
def _find_jni_link_lines(self, makefile_text):
|
|
"""Return lines that contain the JNI library link command."""
|
|
return [
|
|
line for line in makefile_text.splitlines()
|
|
if 'libz3java$(SO_EXT)' in line and 'SLINK' in line
|
|
]
|
|
|
|
# ------------------------------------------------------------------
|
|
# Tests for non-Windows platforms (where SLINK_EXTRA_FLAGS matters)
|
|
# ------------------------------------------------------------------
|
|
|
|
def test_macos_arm64_native_uses_slink_extra_flags(self):
|
|
"""
|
|
On native ARM64 macOS builds, the JNI link command must use
|
|
$(SLINK_EXTRA_FLAGS) so that the -arch arm64 flag added to
|
|
SLINK_EXTRA_FLAGS by mk_config() is respected.
|
|
"""
|
|
comp = self._make_java_dll_component()
|
|
text = self._generate_makefile(
|
|
comp, is_windows=False, is_osx=True, is_arch_arm64=True
|
|
)
|
|
link_lines = self._find_jni_link_lines(text)
|
|
self.assertTrue(
|
|
link_lines,
|
|
"Expected at least one JNI link line in the generated Makefile",
|
|
)
|
|
for line in link_lines:
|
|
self.assertIn(
|
|
'$(SLINK_EXTRA_FLAGS)', line,
|
|
"JNI link command must use $(SLINK_EXTRA_FLAGS) so the "
|
|
"correct target architecture flag is applied",
|
|
)
|
|
|
|
def test_macos_arm64_native_no_hardcoded_arch_arm64(self):
|
|
"""
|
|
The JNI link command must NOT hardcode -arch arm64.
|
|
Hardcoding -arch arm64 breaks cross-compilation from an ARM64 host
|
|
to an x86_64 target, which is the bug this fix addresses.
|
|
"""
|
|
comp = self._make_java_dll_component()
|
|
text = self._generate_makefile(
|
|
comp, is_windows=False, is_osx=True, is_arch_arm64=True
|
|
)
|
|
link_lines = self._find_jni_link_lines(text)
|
|
self.assertTrue(link_lines, "Expected at least one JNI link line")
|
|
for line in link_lines:
|
|
self.assertNotIn(
|
|
'-arch arm64', line,
|
|
"JNI link command must not hardcode '-arch arm64'. "
|
|
"Use $(SLINK_EXTRA_FLAGS) instead so that cross-compilation "
|
|
"from ARM64 host to x86_64 target works correctly.",
|
|
)
|
|
|
|
def test_macos_x86_64_uses_slink_extra_flags(self):
|
|
"""
|
|
When building for x86_64 on macOS (e.g. cross-compiling from ARM64
|
|
host), the JNI link command must still use $(SLINK_EXTRA_FLAGS) so
|
|
that the -arch x86_64 flag set by mk_config() is applied.
|
|
"""
|
|
comp = self._make_java_dll_component()
|
|
text = self._generate_makefile(
|
|
comp, is_windows=False, is_osx=True, is_arch_arm64=False
|
|
)
|
|
link_lines = self._find_jni_link_lines(text)
|
|
self.assertTrue(link_lines, "Expected at least one JNI link line")
|
|
for line in link_lines:
|
|
self.assertIn(
|
|
'$(SLINK_EXTRA_FLAGS)', line,
|
|
"JNI link command must use $(SLINK_EXTRA_FLAGS)",
|
|
)
|
|
|
|
def test_linux_uses_slink_extra_flags(self):
|
|
"""On Linux, the JNI link command must use $(SLINK_EXTRA_FLAGS)."""
|
|
comp = self._make_java_dll_component()
|
|
text = self._generate_makefile(
|
|
comp, is_windows=False, is_osx=False, is_arch_arm64=False
|
|
)
|
|
link_lines = self._find_jni_link_lines(text)
|
|
self.assertTrue(link_lines, "Expected at least one JNI link line")
|
|
for line in link_lines:
|
|
self.assertIn(
|
|
'$(SLINK_EXTRA_FLAGS)', line,
|
|
"JNI link command must use $(SLINK_EXTRA_FLAGS) on Linux",
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Tests for Windows (different codepath - links against LIB_EXT)
|
|
# ------------------------------------------------------------------
|
|
|
|
def test_windows_links_against_lib_ext(self):
|
|
"""
|
|
On Windows the JNI library is linked against the import library
|
|
(libz3$(LIB_EXT)), not the shared library, and SLINK_EXTRA_FLAGS is
|
|
handled differently by the VS build system.
|
|
"""
|
|
comp = self._make_java_dll_component()
|
|
text = self._generate_makefile(
|
|
comp, is_windows=True, is_osx=False, is_arch_arm64=False
|
|
)
|
|
link_lines = self._find_jni_link_lines(text)
|
|
self.assertTrue(link_lines, "Expected at least one JNI link line")
|
|
for line in link_lines:
|
|
self.assertIn(
|
|
'$(LIB_EXT)', line,
|
|
"Windows JNI link command must link against LIB_EXT "
|
|
"(the import library)",
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Consistency check: SLINK_EXTRA_FLAGS in mk_config for cross-compile
|
|
# ------------------------------------------------------------------
|
|
|
|
def test_slibextraflags_contains_x86_64_when_cross_compiling(self):
|
|
"""
|
|
When mk_config() runs on an ARM64 macOS host with IS_ARCH_ARM64=False
|
|
(i.e. cross-compiling to x86_64), SLIBEXTRAFLAGS must contain
|
|
'-arch x86_64' so that $(SLINK_EXTRA_FLAGS) carries the right flag.
|
|
|
|
This validates the mk_config() logic that feeds into $(SLINK_EXTRA_FLAGS).
|
|
"""
|
|
# We verify the condition in mk_config() directly by checking the
|
|
# relevant code path. The cross-compile path in mk_config() is:
|
|
#
|
|
# elif IS_OSX and os.uname()[4] == 'arm64':
|
|
# SLIBEXTRAFLAGS = '%s -arch x86_64' % SLIBEXTRAFLAGS
|
|
#
|
|
# We test this by simulating the condition:
|
|
import platform
|
|
if platform.system() != 'Darwin' or platform.machine() != 'arm64':
|
|
self.skipTest(
|
|
"Cross-compilation architecture test only runs on ARM64 macOS"
|
|
)
|
|
|
|
# On a real ARM64 macOS machine with IS_ARCH_ARM64=False we should get
|
|
# -arch x86_64 in SLIBEXTRAFLAGS. Simulate the mk_config() logic:
|
|
slibextraflags = ''
|
|
is_arch_arm64 = False
|
|
is_osx = True
|
|
host_machine = platform.machine() # 'arm64'
|
|
|
|
if is_arch_arm64 and is_osx:
|
|
slibextraflags = '%s -arch arm64' % slibextraflags
|
|
elif is_osx and host_machine == 'arm64':
|
|
slibextraflags = '%s -arch x86_64' % slibextraflags
|
|
|
|
self.assertIn(
|
|
'-arch x86_64', slibextraflags,
|
|
"When cross-compiling from ARM64 macOS to x86_64, "
|
|
"SLIBEXTRAFLAGS must contain '-arch x86_64'",
|
|
)
|
|
|
|
def test_slibextraflags_contains_arm64_for_native_arm64_build(self):
|
|
"""
|
|
When mk_config() runs on a native ARM64 macOS build (IS_ARCH_ARM64=True),
|
|
SLIBEXTRAFLAGS must contain '-arch arm64'.
|
|
"""
|
|
import platform
|
|
if platform.system() != 'Darwin':
|
|
self.skipTest("Architecture flag test only relevant on macOS")
|
|
|
|
slibextraflags = ''
|
|
is_arch_arm64 = True
|
|
is_osx = True
|
|
|
|
if is_arch_arm64 and is_osx:
|
|
slibextraflags = '%s -arch arm64' % slibextraflags
|
|
|
|
self.assertIn(
|
|
'-arch arm64', slibextraflags,
|
|
"For a native ARM64 macOS build, SLIBEXTRAFLAGS must contain "
|
|
"'-arch arm64' so that $(SLINK_EXTRA_FLAGS) carries the correct flag",
|
|
)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|