import subprocess import pytest from pyodide_build.pywasmcross import ( BuildArgs, calculate_exports, filter_objects, get_cmake_compiler_flags, get_library_output, handle_command_generate_args, replay_f2c, replay_genargs_handle_dashI, ) @pytest.fixture(scope="function") def build_args(): yield BuildArgs( cflags="", cxxflags="", ldflags="", target_install_dir="", host_install_dir="", pythoninclude="python/include", exports="whole_archive", ) def _args_wrapper(func): """Convert function to take as input / return a string instead of a list of arguments Also sets dryrun=True """ def _inner(line, *pargs): args = line.split() res = func(args, *pargs, dryrun=True) if hasattr(res, "__len__"): return " ".join(res) else: return res return _inner f2c_wrap = _args_wrapper(replay_f2c) def generate_args(line: str, args: BuildArgs, is_link_cmd: bool = False) -> str: splitline = line.split() res = handle_command_generate_args(splitline, args, is_link_cmd) if res[0] in ("emcc", "em++"): for arg in [ "-Werror=implicit-function-declaration", "-Werror=mismatched-parameter-types", "-Werror=return-type", ]: assert arg in res res.remove(arg) if "-c" in splitline: if "python/include" in res: include_index = res.index("python/include") del res[include_index] del res[include_index - 1] if is_link_cmd: arg = "-Wl,--fatal-warnings" assert arg in res res.remove(arg) return " ".join(res) def test_handle_command(build_args): args = build_args assert handle_command_generate_args(["gcc", "-print-multiarch"], args, True) == [ "echo", "wasm32-emscripten", ] proxied_commands = { "cc": "emcc", "c++": "em++", "gcc": "emcc", "ld": "emcc", "ar": "emar", "ranlib": "emranlib", "strip": "emstrip", "cmake": "emcmake", } for cmd, proxied_cmd in proxied_commands.items(): assert generate_args(cmd, args).split()[0] == proxied_cmd assert ( generate_args("gcc -c test.o -o test.so", args, True) == "emcc -c test.o -o test.so" ) # check cxxflags injection and cpp detection args = BuildArgs( cflags="-I./lib2", cxxflags="-std=c++11", ldflags="-lm", exports="whole_archive", ) assert ( generate_args("gcc -I./lib1 -c test.cpp -o test.o", args) == "em++ -I./lib1 -c test.cpp -o test.o -I./lib2 -std=c++11" ) # check ldflags injection args = BuildArgs( cflags="", cxxflags="", ldflags="-lm", target_install_dir="", exports="whole_archive", ) assert ( generate_args("gcc -c test.o -o test.so", args, True) == "emcc -c test.o -o test.so -lm" ) # Test that repeated libraries are removed assert ( generate_args("gcc test.o -lbob -ljim -ljim -lbob -o test.so", args) == "emcc test.o -lbob -ljim -o test.so" ) def test_handle_command_ldflags(build_args): # Make sure to remove unsupported link flags for wasm-ld args = build_args assert ( generate_args( "gcc -Wl,--strip-all,--as-needed -Wl,--sort-common,-z,now,-Bsymbolic-functions -c test.o -o test.so", args, True, ) == "emcc -Wl,-z,now -c test.o -o test.so" ) def test_replay_genargs_handle_dashI(monkeypatch): import sys mock_prefix = "/mock_prefix" mock_base_prefix = "/mock_base_prefix" monkeypatch.setattr(sys, "prefix", mock_prefix) monkeypatch.setattr(sys, "base_prefix", mock_base_prefix) target_dir = "/target" target_cpython_include = "/target/include/python3.11" assert replay_genargs_handle_dashI("-I/usr/include", target_dir) is None assert ( replay_genargs_handle_dashI(f"-I{mock_prefix}/include/python3.11", target_dir) == f"-I{target_cpython_include}" ) assert ( replay_genargs_handle_dashI( f"-I{mock_base_prefix}/include/python3.11", target_dir ) == f"-I{target_cpython_include}" ) def test_f2c(): assert f2c_wrap("gfortran test.f") == "gcc test.c" assert f2c_wrap("gcc test.c") is None assert f2c_wrap("gfortran --version") is None assert ( f2c_wrap("gfortran --shared -c test.o -o test.so") == "gcc --shared -c test.o -o test.so" ) def test_conda_unsupported_args(build_args): # Check that compile arguments that are not supported by emcc and are sometimes # used in conda are removed. args = build_args assert generate_args("gcc -c test.o -B /compiler_compat -o test.so", args) == ( "emcc -c test.o -o test.so" ) assert generate_args("gcc -c test.o -Wl,--sysroot=/ -o test.so", args) == ( "emcc -c test.o -o test.so" ) @pytest.mark.parametrize( "line, expected", [ ([], []), ( [ "obj1.o", "obj2.o", "slib1.so", "slib2.so", "lib1.a", "lib2.a", "-o", "test.so", ], ["obj1.o", "obj2.o", "lib1.a", "lib2.a"], ), ( ["@dir/link.txt", "obj1.o", "obj2.o", "test.so"], ["@dir/link.txt", "obj1.o", "obj2.o"], ), ], ) def test_filter_objects(line, expected): assert filter_objects(line) == expected @pytest.mark.xfail(reason="FIXME: emcc is not available during test") def test_exports_node(tmp_path): template = """ int l(); __attribute__((visibility("hidden"))) int f%s() { return l(); } __attribute__ ((visibility ("default"))) int g%s() { return l(); } int h%s(){ return l(); } """ (tmp_path / "f1.c").write_text(template % (1, 1, 1)) (tmp_path / "f2.c").write_text(template % (2, 2, 2)) subprocess.run(["emcc", "-c", tmp_path / "f1.c", "-o", tmp_path / "f1.o", "-fPIC"]) subprocess.run(["emcc", "-c", tmp_path / "f2.c", "-o", tmp_path / "f2.o", "-fPIC"]) assert set(calculate_exports([str(tmp_path / "f1.o")], True)) == {"g1", "h1"} assert set( calculate_exports([str(tmp_path / "f1.o"), str(tmp_path / "f2.o")], True) ) == { "g1", "h1", "g2", "h2", } # Currently if the object file contains bitcode we can't tell what the # symbol visibility is. subprocess.run( ["emcc", "-c", tmp_path / "f1.c", "-o", tmp_path / "f1.o", "-fPIC", "-flto"] ) assert set(calculate_exports([str(tmp_path / "f1.o")], True)) == {"f1", "g1", "h1"} def test_get_cmake_compiler_flags(): cmake_flags = " ".join(get_cmake_compiler_flags()) compiler_flags = ( "CMAKE_C_COMPILER", "CMAKE_CXX_COMPILER", "CMAKE_C_COMPILER_AR", "CMAKE_CXX_COMPILER_AR", ) for compiler_flag in compiler_flags: assert f"-D{compiler_flag}" in cmake_flags emscripten_compilers = ( "emcc", "em++", "emar", ) for emscripten_compiler in emscripten_compilers: assert emscripten_compiler not in cmake_flags def test_handle_command_cmake(build_args): args = build_args assert "--fresh" in handle_command_generate_args(["cmake", "./"], args, False) build_cmd = ["cmake", "--build", "." "--target", "target"] assert handle_command_generate_args(build_cmd, args, False) == build_cmd def test_get_library_output(): assert get_library_output(["test.so"]) == "test.so" assert get_library_output(["test.so.1.2.3"]) == "test.so.1.2.3" assert ( get_library_output(["test", "test.a", "test.o", "test.c", "test.cpp", "test.h"]) is None )