[Build] (P13 W7): the library links from module OBJECT targets along the ratchet's partition, and a server-only link check target must link without the frontend modules.

This commit is contained in:
2026-10-06 19:50:31 -04:00
parent 3fb0054e79
commit 9fa61b84a5
4 changed files with 302 additions and 54 deletions
+125 -42
View File
@@ -794,8 +794,80 @@ endif()
# find <glvnd/libeglabi.h>; 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}
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>: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 $<TARGET_OBJECTS:mg_${MGL_MODULE}>)
if (NOT MGL_MODULE STREQUAL "frontend" AND NOT MGL_MODULE STREQUAL "remote_client")
list(APPEND MGL_SERVER_MODULE_OBJECTS $<TARGET_OBJECTS:mg_${MGL_MODULE}>)
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}
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>: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 $<TARGET_OBJECTS:mg_${MGL_MODULE}>)
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 ()
+120
View File
@@ -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=<mangled>=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 --<mode>-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())
+38 -12
View File
@@ -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 <build-dir>/CMakeFiles. Since P13 W7 (D12) the library is
# linked from module OBJECT libraries, so its objects live under CMakeFiles/mg_<module>.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 <build-dir>/CMakeFiles/<target>.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 <target> (and, for the library, of its mg_* module objects), keyed by path
relative to its object directory. Returns (<build-dir>/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 --<mode>-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)
+19
View File
@@ -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=<symbol>=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 <cstdio>
#include <cstdlib>
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();
}