Fix assertions that NDEBUG deleted, and match OBS's real macOS SDK regex
Build / macOS (macos-latest) (push) Failing after 18s
Build / Linux (ubuntu-24.04) (push) Successful in 54s
Build / Windows (windows-latest) (push) Failing after 8m11s

test_core.cpp used bare assert(). CI builds Release, Release defines NDEBUG,
and NDEBUG compiles assert() out entirely -- so that suite had been passing
unconditionally, checking nothing. It now uses the same always-live ST_ASSERT
harness as the other suites and reports a count (10 checks), and additionally
asserts that core_version() really is the version CMake injected rather than a
stale literal.

macOS SDK, third iteration. The previous fix assumed OBS only wanted a
version-carrying SDK filename. Reading OBS 30.0.2's
cmake/macos/compilerconfig.cmake shows the actual pattern is stricter:

  ".+/MacOSX.platform/Developer/SDKs/MacOSX([0-9]+\.[0-9])+\.sdk$"

which only ever matches a full-Xcode SDK path. A Command-Line-Tools-only
install keeps its SDK at /Library/Developer/CommandLineTools/SDKs/MacOSX<ver>.sdk,
with no MacOSX.platform/Developer/SDKs segment at all, so it can never match
however it is named -- which is why the second attempt got past the
"REGEX needs at least 5 arguments" error and still landed on "Your macOS SDK
version () is too low", with the version still empty.

_resolve_versioned_macos_sdk now builds a symlink tree under .deps/ whose
shape matches that pattern and which points at exactly the same SDK, and uses
the toolchain's own path untouched when it already matches (i.e. when real
Xcode is installed). Nothing about the compilation changes -- only the
spelling of the path, which is all OBS's check reads.

Also refreshes the scaffold-era comments in core.h and core_c.h, which still
described this library as a placeholder that would one day talk to
livekit-ffi.

Linux CI is green on the previous commit: real libobs adapter linked
(ldd shows libobs.so.0 plus liblivekit/liblivekit_ffi resolving from the
staged package directory), obs_module_load exported, ctest 6/6, artifact
uploaded.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
This commit is contained in:
2026-09-06 22:13:28 -07:00
co-authored by Claude Sonnet 5
parent 0fbcb7c2f4
commit 6b12859887
5 changed files with 80 additions and 85 deletions
+28 -29
View File
@@ -68,24 +68,29 @@ function(_check_deps_version version)
return(PROPAGATE found CMAKE_PREFIX_PATH) return(PROPAGATE found CMAKE_PREFIX_PATH)
endfunction() endfunction()
# _resolve_versioned_macos_sdk: return an SDK path whose *filename* carries the # _resolve_versioned_macos_sdk: return an SDK path that satisfies OBS's own
# version number, e.g. .../MacOSX15.5.sdk. # macOS SDK version check.
# #
# Not upstream. OBS's own cmake/macos/compilerconfig.cmake reads the SDK # Not upstream. OBS 30.0.2's cmake/macos/compilerconfig.cmake reads the SDK
# version by regex-matching "MacOSX<major>.<minor>.sdk" out of # version straight out of CMAKE_OSX_SYSROOT with this regex:
# CMAKE_OSX_SYSROOT, and hard-fails if that does not match:
# #
# string sub-command REGEX, mode MATCH needs at least 5 arguments # ".+/MacOSX.platform/Developer/SDKs/MacOSX([0-9]+\\.[0-9])+\\.sdk$"
# Your macOS SDK version () is too low.
# #
# With upstream's Xcode generator CMAKE_OSX_SYSROOT stays the literal string # and hard-fails if it does not match ("Your macOS SDK version () is too low",
# "macosx" and Xcode resolves it late, so the regex never runs against a real # with an empty version, which is the tell). That pattern only ever matches a
# path. With Ninja -- which this project uses because CI has no Xcode -- CMake # full-Xcode SDK path; a Command-Line-Tools-only install has its SDK at
# resolves it eagerly to whatever `xcrun --show-sdk-path` returns, and on a # /Library/Developer/CommandLineTools/SDKs/MacOSX<ver>.sdk, with no
# Command-Line-Tools-only install that is the UNVERSIONED symlink # MacOSX.platform/Developer/SDKs segment at all, and can never match.
# /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk. Hence this: prefer a #
# versioned sibling if the toolchain ships one, and otherwise synthesise a # Upstream never hits this because it uses the Xcode generator, where
# correctly-named symlink to the same SDK. # CMAKE_OSX_SYSROOT stays the literal string "macosx" and Xcode resolves it
# late. This project uses Ninja (the CI runner has no xcodebuild), so CMake
# resolves the sysroot eagerly and the regex runs against a real path.
#
# So: if the toolchain's own SDK path already matches, use it untouched.
# Otherwise build a symlink tree under .deps/ whose shape matches the regex
# and which points at exactly the same SDK. Nothing about the compilation
# changes -- only the spelling of the path, which is all the check reads.
function(_resolve_versioned_macos_sdk out_path) function(_resolve_versioned_macos_sdk out_path)
set(${out_path} "" PARENT_SCOPE) set(${out_path} "" PARENT_SCOPE)
@@ -97,12 +102,11 @@ function(_resolve_versioned_macos_sdk out_path)
ERROR_QUIET ERROR_QUIET
) )
if(NOT _rc EQUAL 0 OR NOT _sdk) if(NOT _rc EQUAL 0 OR NOT _sdk)
message(WARNING "Could not determine the macOS SDK path via xcrun; leaving CMAKE_OSX_SYSROOT alone.")
return() return()
endif() endif()
# Already versioned: nothing to do. if(_sdk MATCHES "/MacOSX\\.platform/Developer/SDKs/MacOSX[0-9]+\\.[0-9]+\\.sdk$")
get_filename_component(_sdk_name "${_sdk}" NAME)
if(_sdk_name MATCHES "^MacOSX[0-9]+\\.[0-9]+\\.sdk$")
set(${out_path} "${_sdk}" PARENT_SCOPE) set(${out_path} "${_sdk}" PARENT_SCOPE)
return() return()
endif() endif()
@@ -115,27 +119,22 @@ function(_resolve_versioned_macos_sdk out_path)
ERROR_QUIET ERROR_QUIET
) )
if(NOT _rc EQUAL 0 OR NOT _sdk_version MATCHES "^([0-9]+)\\.([0-9]+)") if(NOT _rc EQUAL 0 OR NOT _sdk_version MATCHES "^([0-9]+)\\.([0-9]+)")
message(WARNING "Could not determine the macOS SDK version via xcrun (got '${_sdk_version}').")
return() return()
endif() endif()
set(_short "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}") set(_short "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}")
# A versioned sibling next to the symlink is the common layout. set(_link_dir "${dependencies_dir}/sdk/MacOSX.platform/Developer/SDKs")
get_filename_component(_sdk_dir "${_sdk}" DIRECTORY)
if(EXISTS "${_sdk_dir}/MacOSX${_short}.sdk")
set(${out_path} "${_sdk_dir}/MacOSX${_short}.sdk" PARENT_SCOPE)
return()
endif()
# Otherwise make one, inside our own dependency directory.
set(_link_dir "${dependencies_dir}/sdk")
file(MAKE_DIRECTORY "${_link_dir}")
set(_link "${_link_dir}/MacOSX${_short}.sdk") set(_link "${_link_dir}/MacOSX${_short}.sdk")
file(MAKE_DIRECTORY "${_link_dir}")
if(NOT EXISTS "${_link}") if(NOT EXISTS "${_link}")
file(CREATE_LINK "${_sdk}" "${_link}" SYMBOLIC) file(CREATE_LINK "${_sdk}" "${_link}" SYMBOLIC)
endif() endif()
if(EXISTS "${_link}") if(EXISTS "${_link}")
message(STATUS "Using synthesised versioned macOS SDK path: ${_link}") message(STATUS "macOS SDK ${_short} at ${_sdk}; presenting it to OBS as ${_link}")
set(${out_path} "${_link}" PARENT_SCOPE) set(${out_path} "${_link}" PARENT_SCOPE)
else()
message(WARNING "Could not create the versioned macOS SDK symlink at ${_link}.")
endif() endif()
endfunction() endfunction()
+12 -21
View File
@@ -20,32 +20,23 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include <string> #include <string>
// C++ API for the core library. Per the design doc // The small shared pieces of the core library: its version string, and the
// (docs/superpowers/specs/2026-09-06-obs-camera-plugin-design.md in the // streamer-tools connection settings that both the API client and the OBS
// streamer-tools repo), this library will eventually own: streamer-tools // adapter pass around. Everything substantial lives in its own header --
// API auth, LiveKit FFI session management (connect, subscribe, decode, // api_client.h, session.h, http.h, json.h -- and none of it depends on OBS,
// reconnect), and frame callbacks -- all with zero OBS dependency, so it // so the whole library builds and tests headlessly on all three platforms.
// can be built and tested headlessly.
//
// THIS IS SCAFFOLDING. Nothing below talks to a real server or to
// livekit-ffi yet. It exists to prove the core-library/OBS-adapter split
// builds, links, and is unit-testable, ahead of a later phase that
// implements the real logic.
namespace stplugin { namespace stplugin {
// Returns the core library's version string. Placeholder for a real // The core library's version string, injected by CMake from the top-level
// version scheme once the library does something. // project() version, so what OBS logs on load is the actual build.
const char *core_version(); const char *core_version();
// Minimal connection configuration the future core library will use to // What an operator types into the source's properties, and what ApiClient
// authenticate against the streamer-tools API // needs to reach the two read-key-scoped endpoints in
// (see apps/server/src/rooms/join.routes.ts and // apps/server/src/obs/plugin.routes.ts (streamer-tools repo). The read key is
// apps/server/src/livekit/tokens.ts in the streamer-tools repo for the // a credential: it is masked in the properties UI and never logged (see
// existing read-key-authed token pattern this will follow) and mint a // ApiClient::redactedUrl).
// scoped LiveKit subscriber token. Validation here is intentionally
// trivial -- it exists to prove the core library is unit-testable
// headlessly, not to implement the real API client.
struct ConnectionConfig { struct ConnectionConfig {
std::string server_url; std::string server_url;
std::string room_slug; std::string room_slug;
+6 -8
View File
@@ -18,15 +18,13 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#pragma once #pragma once
// Minimal C ABI surface of the core library, for the OBS adapter (plain // A minimal C ABI over the core library's version string.
// C, per the obs-plugintemplate convention) to call into the core
// library (C++) without needing a C++ compiler in that translation unit.
// //
// This mirrors the boundary the real integration will cross in the // The OBS adapter is C++ and calls stplugin::core_version() directly, so
// other direction: livekit-ffi is a Rust library exposing a C ABI that // nothing in this repository needs this header today. It is kept because it
// the C++ core library will link against. Proving a small, deliberate // is the seam a plain-C consumer would use, and because the unit tests assert
// C ABI seam works cleanly here is part of what this scaffold is for. // the two entry points agree -- which is a cheap check that the C++ library
// really is linkable from a C translation unit.
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
+3
View File
@@ -15,6 +15,9 @@ function(stplugin_add_test name)
endfunction() endfunction()
stplugin_add_test(test_core) stplugin_add_test(test_core)
target_compile_definitions(test_core PRIVATE
STPLUGIN_EXPECTED_CORE_VERSION="${PROJECT_VERSION}"
)
stplugin_add_test(test_json) stplugin_add_test(test_json)
stplugin_add_test(test_api_client) stplugin_add_test(test_api_client)
stplugin_add_test(test_session) stplugin_add_test(test_session)
+31 -27
View File
@@ -16,44 +16,48 @@ You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/> with this program. If not, see <https://www.gnu.org/licenses/>
*/ */
// Deliberately dependency-free (no gtest/catch2 etc.) so this test target // NOTE on why this file uses ST_ASSERT and not assert(): CI builds Release,
// has no network fetch or package-manager step in CI -- proving the // which defines NDEBUG, which compiles every bare assert() out entirely. This
// "core library builds and tests headlessly, no OBS required" claim // suite previously passed unconditionally for exactly that reason. The
// without adding another moving part to this scaffolding pass. // ST_ASSERT macros in test_util.h are always live and report a pass/fail
// count.
#include <cassert>
#include <cstdio>
#include <cstring> #include <cstring>
#include <string>
#include "stplugin/core.h" #include "stplugin/core.h"
#include "stplugin/core_c.h" #include "stplugin/core_c.h"
#include "test_util.h"
int main() { int main()
// core_version() should return a non-empty string, via both the {
// C++ API and the C ABI wrapper the OBS adapter will actually call. // The version string comes through both the C++ API and the C ABI
// wrapper, and the two must agree.
const char *cpp_version = stplugin::core_version(); const char *cpp_version = stplugin::core_version();
assert(cpp_version != nullptr); ST_ASSERT(cpp_version != nullptr);
assert(std::strlen(cpp_version) > 0); ST_ASSERT(cpp_version != nullptr && std::strlen(cpp_version) > 0);
const char *c_version = stplugin_core_version(); const char *c_version = stplugin_core_version();
assert(c_version != nullptr); ST_ASSERT(c_version != nullptr);
assert(std::strcmp(cpp_version, c_version) == 0); ST_ASSERT_EQ(std::string(cpp_version ? cpp_version : ""), std::string(c_version ? c_version : ""));
// ConnectionConfig::is_valid() -- trivial non-empty checks, but // It is the version CMake injected, not a hand-maintained literal.
// exercised through all four combinations to prove the core library ST_ASSERT_EQ(std::string(cpp_version ? cpp_version : ""), std::string(STPLUGIN_EXPECTED_CORE_VERSION));
// is genuinely testable in isolation.
stplugin::ConnectionConfig valid{"https://streamers.example.com", "main-room", "readkey123"};
assert(valid.is_valid());
stplugin::ConnectionConfig missing_url{"", "main-room", "readkey123"}; // ConnectionConfig::is_valid(): all three fields are required. Named
assert(!missing_url.is_valid()); // locals rather than braced temporaries inline, because the commas inside
// a braced initialiser would be read as macro argument separators.
const stplugin::ConnectionConfig complete{"https://streamers.example.com", "main-room", "readkey123"};
const stplugin::ConnectionConfig no_url{"", "main-room", "readkey123"};
const stplugin::ConnectionConfig no_slug{"https://streamers.example.com", "", "readkey123"};
const stplugin::ConnectionConfig no_key{"https://streamers.example.com", "main-room", ""};
const stplugin::ConnectionConfig empty;
stplugin::ConnectionConfig missing_slug{"https://streamers.example.com", "", "readkey123"}; ST_ASSERT(complete.is_valid());
assert(!missing_slug.is_valid()); ST_ASSERT(!no_url.is_valid());
ST_ASSERT(!no_slug.is_valid());
ST_ASSERT(!no_key.is_valid());
ST_ASSERT(!empty.is_valid());
stplugin::ConnectionConfig missing_key{"https://streamers.example.com", "main-room", ""}; return st_test_report("core");
assert(!missing_key.is_valid());
std::printf("core: all tests passed\n");
return 0;
} }