diff --git a/CMakeLists.txt b/CMakeLists.txt index b1a13a1a..246e34fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -794,8 +794,80 @@ endif() # find ; a Linux host only built because its system has the header). list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/glvnd/include) +# ---- P13 W7 (D12): MODULE TARGETS ------------------------------------------------------------ +# +# One flat SOURCE_FILES list used to feed MobileGL and MobileGL_s separately (every file compiled +# twice). It is split here into OBJECT libraries along the link ratchet's partition +# (scripts/link_ratchet.py PARTITION, which is the written form of these boundaries): +# +# mg_util MG_Util, Init / GlobalObjects / ConfigLoader, MG_Remote/FatalFunnel SHARED +# mg_pipe MG_Pipe SERVER +# mg_backend MG_Backend (both backends, the record arm's staged stores) SERVER +# mg_frontend MG_Impl + MG_State FRONTEND +# mg_remote_transport MG_Remote/{Transport,Protocol,Wire}, CapsCodec SERVER +# mg_remote_client MG_Remote/Client FRONTEND +# mg_remote_server MG_Remote/Server SERVER +# +# Both MobileGL and MobileGL_s link the same objects. mgl_build_iface carries every compile-side +# usage requirement (include directories, definitions, the link libraries whose headers the +# sources include), so a property set on it reaches every module and both libraries; a property +# that only matters at link time may still be set on the libraries themselves. +add_library(mgl_build_iface INTERFACE) +target_include_directories(mgl_build_iface INTERFACE ${MOBILEGL_INCLUDE_DIR}) +target_link_libraries(mgl_build_iface INTERFACE ${MOBILEGL_LINK_LIBRARIES}) +target_compile_definitions(mgl_build_iface INTERFACE + ${MOBILEGL_COMPILE_DEF} + MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL} + $<$:MOBILEGL_TRACE_ANGLE_VARIANTS=1> +) + +set(MGL_MODULES util pipe backend frontend remote_transport remote_client remote_server) +foreach(MGL_MODULE IN LISTS MGL_MODULES) + set(MGL_MODULE_SOURCES_${MGL_MODULE}) +endforeach() +foreach(MGL_SOURCE IN LISTS SOURCE_FILES) + if (MGL_SOURCE MATCHES "^MobileGL/MG_Remote/Client/") + list(APPEND MGL_MODULE_SOURCES_remote_client ${MGL_SOURCE}) + elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Remote/Server/") + list(APPEND MGL_MODULE_SOURCES_remote_server ${MGL_SOURCE}) + elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Remote/(Transport|Protocol|Wire)/" OR + MGL_SOURCE STREQUAL "MobileGL/MG_Remote/CapsCodec.cpp") + list(APPEND MGL_MODULE_SOURCES_remote_transport ${MGL_SOURCE}) + elseif (MGL_SOURCE MATCHES "^MobileGL/MG_(Impl|State)/") + list(APPEND MGL_MODULE_SOURCES_frontend ${MGL_SOURCE}) + elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Backend/") + list(APPEND MGL_MODULE_SOURCES_backend ${MGL_SOURCE}) + elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Pipe/") + list(APPEND MGL_MODULE_SOURCES_pipe ${MGL_SOURCE}) + else() + list(APPEND MGL_MODULE_SOURCES_util ${MGL_SOURCE}) + endif() +endforeach() + +set(MGL_MODULE_OBJECTS) +set(MGL_SERVER_MODULE_OBJECTS) +foreach(MGL_MODULE IN LISTS MGL_MODULES) + if (NOT MGL_MODULE_SOURCES_${MGL_MODULE}) + continue() # MG_Remote's modules are empty without MOBILEGL_BUILD_DISAGGREGATED + endif() + add_library(mg_${MGL_MODULE} OBJECT ${MGL_MODULE_SOURCES_${MGL_MODULE}}) + target_link_libraries(mg_${MGL_MODULE} PUBLIC mgl_build_iface) + set_target_properties(mg_${MGL_MODULE} PROPERTIES POSITION_INDEPENDENT_CODE ON) + if (CMAKE_BUILD_TYPE STREQUAL "Debug") + set_target_properties(mg_${MGL_MODULE} PROPERTIES + C_VISIBILITY_PRESET default CXX_VISIBILITY_PRESET default VISIBILITY_INLINES_HIDDEN OFF) + else() + set_target_properties(mg_${MGL_MODULE} PROPERTIES + C_VISIBILITY_PRESET hidden CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON) + endif() + list(APPEND MGL_MODULE_OBJECTS $) + if (NOT MGL_MODULE STREQUAL "frontend" AND NOT MGL_MODULE STREQUAL "remote_client") + list(APPEND MGL_SERVER_MODULE_OBJECTS $) + endif() +endforeach() + add_library(${CMAKE_PROJECT_NAME} SHARED - ${SOURCE_FILES} + ${MGL_MODULE_OBJECTS} ) if (WIN32) @@ -820,21 +892,7 @@ else() ) endif() -target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC - ${MOBILEGL_INCLUDE_DIR} -) - -target_link_libraries(${CMAKE_PROJECT_NAME} - PUBLIC - ${MOBILEGL_LINK_LIBRARIES} -) - -target_compile_definitions(${CMAKE_PROJECT_NAME} - PUBLIC - ${MOBILEGL_COMPILE_DEF} - MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL} - $<$:MOBILEGL_TRACE_ANGLE_VARIANTS=1> -) +target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC mgl_build_iface) if(UNIX AND NOT APPLE AND NOT ANDROID) foreach(MOBILEGL_LOADER_ALIAS @@ -861,8 +919,8 @@ if(WIN32) endif() if(NOT ANDROID) - add_library(${CMAKE_PROJECT_NAME}_s STATIC - ${SOURCE_FILES} + add_library(${CMAKE_PROJECT_NAME}_s STATIC + ${MGL_MODULE_OBJECTS} ) if (CMAKE_BUILD_TYPE STREQUAL "Debug") @@ -879,27 +937,12 @@ if(NOT ANDROID) ) endif() - target_include_directories(${CMAKE_PROJECT_NAME}_s PUBLIC - ${MOBILEGL_INCLUDE_DIR} - ) - - target_link_libraries(${CMAKE_PROJECT_NAME}_s - PUBLIC - ${MOBILEGL_LINK_LIBRARIES} - ) - - target_compile_definitions(${CMAKE_PROJECT_NAME}_s - PUBLIC - ${MOBILEGL_COMPILE_DEF} - MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL} - ) + target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC mgl_build_iface) endif() if (TRACY_ENABLE) - target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Tracy::TracyClient) - target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC Tracy::TracyClient) - target_compile_definitions(${CMAKE_PROJECT_NAME} PUBLIC -DTRACY_ENABLE) - target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC -DTRACY_ENABLE) + target_link_libraries(mgl_build_iface INTERFACE Tracy::TracyClient) + target_compile_definitions(mgl_build_iface INTERFACE -DTRACY_ENABLE) endif () # GLVND, AND WHY THIS IS A LINKER FLAG RATHER THAN A VISIBILITY ONE. @@ -924,6 +967,49 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND UNIX AND NOT APPLE AND NOT ANDROID) add_subdirectory(MobileGL/MG_Gbm) endif() +# ---- P13 W7 (D12): THE SERVER-ONLY LINK CHECK --------------------------------------------------- +# The server and shared modules linked WITHOUT mg_frontend / mg_remote_client, with --no-undefined: +# the server half has to link from its own half of the tree. The frontend references it still makes +# are the link ratchet's baseline, each a named exemption that resolves to an aborting stub through +# a generated linker response file (scripts/ci/server_link_check.py refuses, before the link, any +# server reference the baseline does not name). Built with the split shape on Linux hosts; never +# installed or loaded. +if (MOBILEGL_BUILD_DISAGGREGATED AND UNIX AND NOT APPLE AND NOT ANDROID) + find_package(Python3 COMPONENTS Interpreter QUIET) + if (Python3_Interpreter_FOUND) + set(MGL_SERVER_LINK_RSP "${CMAKE_BINARY_DIR}/mgl_server_link_exemptions.rsp") + set(MGL_ALL_MODULE_OBJECT_DEPS) + foreach(MGL_MODULE IN LISTS MGL_MODULES) + if (TARGET mg_${MGL_MODULE}) + list(APPEND MGL_ALL_MODULE_OBJECT_DEPS $) + endif() + endforeach() + add_custom_command( + OUTPUT "${MGL_SERVER_LINK_RSP}" + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/scripts/ci/server_link_check.py" + --build-dir "${CMAKE_BINARY_DIR}" + --baseline "${CMAKE_SOURCE_DIR}/scripts/data/link_ratchet_baseline.txt" + --out "${MGL_SERVER_LINK_RSP}" + DEPENDS ${MGL_ALL_MODULE_OBJECT_DEPS} + "${CMAKE_SOURCE_DIR}/scripts/ci/server_link_check.py" + "${CMAKE_SOURCE_DIR}/scripts/link_ratchet.py" + "${CMAKE_SOURCE_DIR}/scripts/data/link_ratchet_baseline.txt" + COMMENT "Server-only link check: naming the frontend references the baseline exempts" + VERBATIM) + add_custom_target(mgl_server_link_exemptions DEPENDS "${MGL_SERVER_LINK_RSP}") + add_library(MobileGL_server_linkcheck SHARED + ${MGL_SERVER_MODULE_OBJECTS} + tools/server_link_check/Exempt.cpp) + target_link_libraries(MobileGL_server_linkcheck PRIVATE mgl_build_iface) + target_link_options(MobileGL_server_linkcheck PRIVATE + "LINKER:--no-undefined" "@${MGL_SERVER_LINK_RSP}") + set_property(TARGET MobileGL_server_linkcheck APPEND PROPERTY LINK_DEPENDS "${MGL_SERVER_LINK_RSP}") + add_dependencies(MobileGL_server_linkcheck mgl_server_link_exemptions) + else() + message(STATUS "MobileGL: no Python3 interpreter - the server-only link check target is not configured") + endif() +endif() + if (ANDROID) target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC android @@ -964,7 +1050,7 @@ if (APPLE AND NOT MOBILEGL_IOS) endif() if (APPLE AND MOBILEGL_IOS) - target_compile_definitions(${CMAKE_PROJECT_NAME} PUBLIC MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY) + target_compile_definitions(mgl_build_iface INTERFACE MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY) target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC "-framework CoreGraphics" "-framework Foundation" @@ -975,7 +1061,6 @@ if (APPLE AND MOBILEGL_IOS) endif() if(TARGET ${CMAKE_PROJECT_NAME}_s) - target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY) target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC "-framework CoreGraphics" "-framework Foundation" @@ -990,10 +1075,8 @@ endif() if (NOT ANDROID AND NOT MOBILEGL_IOS) find_package(Vulkan) if (Vulkan_FOUND) - target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Vulkan::Vulkan Vulkan::Headers) - target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC Vulkan::Vulkan Vulkan::Headers) - target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ${Vulkan_INCLUDE_DIR}) - target_include_directories(${CMAKE_PROJECT_NAME}_s PUBLIC ${Vulkan_INCLUDE_DIR}) + target_link_libraries(mgl_build_iface INTERFACE Vulkan::Vulkan Vulkan::Headers) + target_include_directories(mgl_build_iface INTERFACE ${Vulkan_INCLUDE_DIR}) endif () endif () diff --git a/scripts/ci/server_link_check.py b/scripts/ci/server_link_check.py new file mode 100644 index 00000000..7d6fb660 --- /dev/null +++ b/scripts/ci/server_link_check.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""P13 W7 (D12): the exemption list for the server-only link check target. + +THE TARGET. `MobileGL_server_linkcheck` (CMakeLists.txt) links the SERVER and SHARED module objects +(mg_util, mg_pipe, mg_backend, mg_remote_transport, mg_remote_server) WITHOUT mg_frontend and +mg_remote_client, with `--no-undefined`: a server image must link from its own half of the tree. +The references that still need the frontend are the link ratchet's baseline +(scripts/data/link_ratchet_baseline.txt) - each one a named debt, not an accident - and this script +turns exactly those into named exemptions: one `--defsym==mgl_server_link_check_exempt` +per symbol, written to a compiler-driver response file the target links with. The stub aborts; the target +is a link proof, never run. + +WHAT IS REFUSED. A SERVER object's reference that only a FRONTEND object satisfies and that the +baseline does not list fails here, before the link, naming the symbol and the referrer - the same +verdict the ratchet gives, raised by the build. References from SHARED objects (Init, the global +objects, the config loader) into the frontend are exempted and only counted: those files ARE the +library's entry glue, and a real server image replaces them rather than sheds them. + +Usage: server_link_check.py --build-dir B --baseline F --out RSP [--nm nm] [--cxxfilt c++filt] +""" +import argparse +import os +import subprocess +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +import link_ratchet as lr # noqa: E402 + +STUB = "mgl_server_link_check_exempt" + + +def nm_mangled(nm_tool, mode, keys, cwd): + """{key: {mangled: type}} via `nm -o ---only` (no demangling).""" + table = {k: {} for k in keys} + back = {lr.OBJECT_PATHS.get(k, k): k for k in keys} + for i in range(0, len(keys), 150): + batch = [lr.OBJECT_PATHS.get(k, k) for k in keys[i:i + 150]] + done = subprocess.run([nm_tool, "-o", "--" + mode + "-only"] + batch, cwd=cwd, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if done.returncode != 0 and done.stderr.strip(): + raise SystemExit("server-link-check: nm failed: " + done.stderr.strip()) + for line in done.stdout.splitlines(): + head, _, rest = line.partition(":") + key = back.get(head) + parts = rest.split() + if key is None or len(parts) < 2: + continue + table[key][parts[-1]] = parts[-2] + return table + + +def demangle(names, tool): + names = sorted(names) + done = subprocess.run([tool], input="\n".join(names) + "\n", stdout=subprocess.PIPE, text=True, + check=True) + return dict(zip(names, done.stdout.splitlines())) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--build-dir", required=True) + ap.add_argument("--baseline", required=True) + ap.add_argument("--out", required=True) + ap.add_argument("--nm", default="nm") + ap.add_argument("--cxxfilt", default="c++filt") + args = ap.parse_args() + + root, objects = lr.find_objects(args.build_dir, "MobileGL") + roles = {o: lr.classify(o) for o in objects} + unclassified = [o for o, r in roles.items() if r is None] + if unclassified: + raise SystemExit("server-link-check: object(s) match no PARTITION row: " + ", ".join(unclassified)) + server = sorted(o for o, r in roles.items() if r == lr.SERVER) + shared = sorted(o for o, r in roles.items() if r == lr.SHARED) + frontend = sorted(o for o, r in roles.items() if r == lr.FRONTEND) + + undefined = nm_mangled(args.nm, "undefined", server + shared, root) + defined = nm_mangled(args.nm, "defined", objects, root) + image_defined = set() + for obj in server + shared: + image_defined.update(defined[obj]) + frontend_defined = set() + for obj in frontend: + frontend_defined.update(defined[obj]) + + wanted = {} # mangled -> set of referrer keys + for obj in server + shared: + for name in undefined[obj]: + if name in image_defined or name not in frontend_defined: + continue + wanted.setdefault(name, set()).add(obj) + + readable = demangle(wanted, args.cxxfilt) + baseline, _notes = lr.read_baseline(args.baseline) + baseline = set(baseline) + refused, from_shared = [], 0 + for name, referrers in sorted(wanted.items()): + from_server = [r for r in referrers if roles[r] == lr.SERVER] + if not from_server: + from_shared += 1 + continue + if readable[name] not in baseline: + refused.append((readable[name], sorted(from_server))) + if refused: + for symbol, referrers in refused: + print("server-link-check: NEW frontend reference from the server half: {} <- {}" + .format(symbol, ", ".join(referrers))) + print("server-link-check: {} reference(s) the baseline does not name - fix the reference or, " + "if it is a debt, add it to {} with its reason".format(len(refused), args.baseline)) + return 1 + with open(args.out, "w", encoding="utf-8") as out: + for name in sorted(wanted): + out.write("-Wl,--defsym={}={}\n".format(name, STUB)) + print("server-link-check: {} named exemption(s) ({} from server objects, all in the baseline; {} " + "from the shared entry glue)".format(len(wanted), len(wanted) - from_shared, from_shared)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/link_ratchet.py b/scripts/link_ratchet.py index 88f6abd4..099c2ca4 100644 --- a/scripts/link_ratchet.py +++ b/scripts/link_ratchet.py @@ -211,21 +211,39 @@ def family_of(obj_path, table=FAMILIES): return best[1] +# Object key (path relative to its target's object directory, e.g. MobileGL/MG_Backend/X.cpp.o) +# -> the path nm is run on, relative to /CMakeFiles. Since P13 W7 (D12) the library is +# linked from module OBJECT libraries, so its objects live under CMakeFiles/mg_.dir as well +# as CMakeFiles/MobileGL.dir (the few sources added to the library target itself); the key keeps the +# source-relative spelling every PARTITION row is written against. +OBJECT_PATHS = {} + + def find_objects(build_dir, target): - """Every .o under /CMakeFiles/.dir, keyed by path relative to that dir.""" - root = os.path.join(build_dir, "CMakeFiles", target + ".dir") - if not os.path.isdir(root): + """Every .o of (and, for the library, of its mg_* module objects), keyed by path + relative to its object directory. Returns (/CMakeFiles, sorted keys).""" + cmake_files = os.path.join(build_dir, "CMakeFiles") + dirs = [target + ".dir"] + if target in ("MobileGL", "MobileGL_s") and os.path.isdir(cmake_files): + dirs += sorted(d for d in os.listdir(cmake_files) if d.startswith("mg_") and d.endswith(".dir")) + if not os.path.isdir(os.path.join(cmake_files, dirs[0])) and len(dirs) == 1: raise SystemExit(PREFIX + "no such object directory: {} (is --build-dir a configured " "CMake build, and is --target right? the disaggregated build " "has MobileGL.dir, MobileGL_s.dir and MobileGLServer.dir)" - .format(root)) + .format(os.path.join(cmake_files, dirs[0]))) objects = [] - for dirpath, _dirnames, filenames in os.walk(root): - for name in filenames: - if name.endswith(".o"): - full = os.path.join(dirpath, name) - objects.append(os.path.relpath(full, root).replace(os.sep, "/")) - return root, sorted(objects) + for sub in dirs: + root = os.path.join(cmake_files, sub) + if not os.path.isdir(root): + continue + for dirpath, _dirnames, filenames in os.walk(root): + for name in filenames: + if name.endswith(".o"): + full = os.path.join(dirpath, name) + key = os.path.relpath(full, root).replace(os.sep, "/") + OBJECT_PATHS[key] = os.path.relpath(full, cmake_files).replace(os.sep, "/") + objects.append(key) + return cmake_files, sorted(objects) def check_flags(build_dir): @@ -255,8 +273,9 @@ def check_flags(build_dir): def run_nm(nm_tool, mode, paths, cwd, chunk=150): """`nm -C -o ---only` over paths, returned as raw text. Chunked for argv limits.""" out = [] + back = {OBJECT_PATHS.get(p, p): p for p in paths} for i in range(0, len(paths), chunk): - batch = paths[i:i + chunk] + batch = [OBJECT_PATHS.get(p, p) for p in paths[i:i + chunk]] completed = subprocess.run([nm_tool, "-C", "-o", "--" + mode + "-only"] + batch, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=cwd) @@ -265,7 +284,14 @@ def run_nm(nm_tool, mode, paths, cwd, chunk=150): # silently shrunk SERVER set. if completed.returncode != 0 and completed.stderr.strip(): raise SystemExit(PREFIX + "nm failed: " + completed.stderr.strip()) - out.append(completed.stdout) + text = completed.stdout + if any(k != v for k, v in back.items()): + lines = [] + for line in text.splitlines(True): + head, sep, rest = line.partition(":") + lines.append(back.get(head, head) + sep + rest) + text = "".join(lines) + out.append(text) return "".join(out) diff --git a/tools/server_link_check/Exempt.cpp b/tools/server_link_check/Exempt.cpp new file mode 100644 index 00000000..a31d9b45 --- /dev/null +++ b/tools/server_link_check/Exempt.cpp @@ -0,0 +1,19 @@ +// MobileGL - tools/server_link_check/Exempt.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// P13 W7 (D12): the one definition every named exemption of the server-only link check resolves to +// (scripts/ci/server_link_check.py writes `--defsym==mgl_server_link_check_exempt` for each +// frontend reference the link ratchet's baseline names). The check target is a link proof and is +// never loaded; reaching this would mean a server image called into the frontend half. +#include +#include + +extern "C" [[noreturn]] void mgl_server_link_check_exempt() { + std::fputs("MobileGL server link check: an exempted frontend symbol was called\n", stderr); + std::abort(); +}