diff --git a/cmake/common/buildspec_common.cmake b/cmake/common/buildspec_common.cmake index 7110635..f6b4994 100644 --- a/cmake/common/buildspec_common.cmake +++ b/cmake/common/buildspec_common.cmake @@ -68,24 +68,29 @@ function(_check_deps_version version) return(PROPAGATE found CMAKE_PREFIX_PATH) endfunction() -# _resolve_versioned_macos_sdk: return an SDK path whose *filename* carries the -# version number, e.g. .../MacOSX15.5.sdk. +# _resolve_versioned_macos_sdk: return an SDK path that satisfies OBS's own +# macOS SDK version check. # -# Not upstream. OBS's own cmake/macos/compilerconfig.cmake reads the SDK -# version by regex-matching "MacOSX..sdk" out of -# CMAKE_OSX_SYSROOT, and hard-fails if that does not match: +# Not upstream. OBS 30.0.2's cmake/macos/compilerconfig.cmake reads the SDK +# version straight out of CMAKE_OSX_SYSROOT with this regex: # -# string sub-command REGEX, mode MATCH needs at least 5 arguments -# Your macOS SDK version () is too low. +# ".+/MacOSX.platform/Developer/SDKs/MacOSX([0-9]+\\.[0-9])+\\.sdk$" # -# With upstream's Xcode generator CMAKE_OSX_SYSROOT stays the literal string -# "macosx" and Xcode resolves it late, so the regex never runs against a real -# path. With Ninja -- which this project uses because CI has no Xcode -- CMake -# resolves it eagerly to whatever `xcrun --show-sdk-path` returns, and on a -# Command-Line-Tools-only install that is the UNVERSIONED symlink -# /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk. Hence this: prefer a -# versioned sibling if the toolchain ships one, and otherwise synthesise a -# correctly-named symlink to the same SDK. +# and hard-fails if it does not match ("Your macOS SDK version () is too low", +# with an empty version, which is the tell). That pattern only ever matches a +# full-Xcode SDK path; a Command-Line-Tools-only install has its SDK at +# /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk, with no +# MacOSX.platform/Developer/SDKs segment at all, and can never match. +# +# Upstream never hits this because it uses the Xcode generator, where +# 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) set(${out_path} "" PARENT_SCOPE) @@ -97,12 +102,11 @@ function(_resolve_versioned_macos_sdk out_path) ERROR_QUIET ) 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() endif() - # Already versioned: nothing to do. - get_filename_component(_sdk_name "${_sdk}" NAME) - if(_sdk_name MATCHES "^MacOSX[0-9]+\\.[0-9]+\\.sdk$") + if(_sdk MATCHES "/MacOSX\\.platform/Developer/SDKs/MacOSX[0-9]+\\.[0-9]+\\.sdk$") set(${out_path} "${_sdk}" PARENT_SCOPE) return() endif() @@ -115,27 +119,22 @@ function(_resolve_versioned_macos_sdk out_path) ERROR_QUIET ) 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() endif() set(_short "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}") - # A versioned sibling next to the symlink is the common layout. - 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_dir "${dependencies_dir}/sdk/MacOSX.platform/Developer/SDKs") set(_link "${_link_dir}/MacOSX${_short}.sdk") + file(MAKE_DIRECTORY "${_link_dir}") if(NOT EXISTS "${_link}") file(CREATE_LINK "${_sdk}" "${_link}" SYMBOLIC) endif() 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) + else() + message(WARNING "Could not create the versioned macOS SDK symlink at ${_link}.") endif() endfunction() diff --git a/core/include/stplugin/core.h b/core/include/stplugin/core.h index 28de919..bc16935 100644 --- a/core/include/stplugin/core.h +++ b/core/include/stplugin/core.h @@ -20,32 +20,23 @@ with this program. If not, see #include -// C++ API for the core library. Per the design doc -// (docs/superpowers/specs/2026-09-06-obs-camera-plugin-design.md in the -// streamer-tools repo), this library will eventually own: streamer-tools -// API auth, LiveKit FFI session management (connect, subscribe, decode, -// reconnect), and frame callbacks -- all with zero OBS dependency, so it -// 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. +// The small shared pieces of the core library: its version string, and the +// streamer-tools connection settings that both the API client and the OBS +// adapter pass around. Everything substantial lives in its own header -- +// api_client.h, session.h, http.h, json.h -- and none of it depends on OBS, +// so the whole library builds and tests headlessly on all three platforms. namespace stplugin { -// Returns the core library's version string. Placeholder for a real -// version scheme once the library does something. +// The core library's version string, injected by CMake from the top-level +// project() version, so what OBS logs on load is the actual build. const char *core_version(); -// Minimal connection configuration the future core library will use to -// authenticate against the streamer-tools API -// (see apps/server/src/rooms/join.routes.ts and -// apps/server/src/livekit/tokens.ts in the streamer-tools repo for the -// existing read-key-authed token pattern this will follow) and mint a -// 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. +// What an operator types into the source's properties, and what ApiClient +// needs to reach the two read-key-scoped endpoints in +// apps/server/src/obs/plugin.routes.ts (streamer-tools repo). The read key is +// a credential: it is masked in the properties UI and never logged (see +// ApiClient::redactedUrl). struct ConnectionConfig { std::string server_url; std::string room_slug; diff --git a/core/include/stplugin/core_c.h b/core/include/stplugin/core_c.h index eb6f384..1648b44 100644 --- a/core/include/stplugin/core_c.h +++ b/core/include/stplugin/core_c.h @@ -18,15 +18,13 @@ with this program. If not, see #pragma once -// Minimal C ABI surface of the core library, for the OBS adapter (plain -// C, per the obs-plugintemplate convention) to call into the core -// library (C++) without needing a C++ compiler in that translation unit. +// A minimal C ABI over the core library's version string. // -// This mirrors the boundary the real integration will cross in the -// other direction: livekit-ffi is a Rust library exposing a C ABI that -// the C++ core library will link against. Proving a small, deliberate -// C ABI seam works cleanly here is part of what this scaffold is for. - +// The OBS adapter is C++ and calls stplugin::core_version() directly, so +// nothing in this repository needs this header today. It is kept because it +// is the seam a plain-C consumer would use, and because the unit tests assert +// the two entry points agree -- which is a cheap check that the C++ library +// really is linkable from a C translation unit. #ifdef __cplusplus extern "C" { #endif diff --git a/core/tests/CMakeLists.txt b/core/tests/CMakeLists.txt index 313ef78..434e6b3 100644 --- a/core/tests/CMakeLists.txt +++ b/core/tests/CMakeLists.txt @@ -15,6 +15,9 @@ function(stplugin_add_test name) endfunction() 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_api_client) stplugin_add_test(test_session) diff --git a/core/tests/test_core.cpp b/core/tests/test_core.cpp index 521c600..bf40ad8 100644 --- a/core/tests/test_core.cpp +++ b/core/tests/test_core.cpp @@ -16,44 +16,48 @@ You should have received a copy of the GNU General Public License along with this program. If not, see */ -// Deliberately dependency-free (no gtest/catch2 etc.) so this test target -// has no network fetch or package-manager step in CI -- proving the -// "core library builds and tests headlessly, no OBS required" claim -// without adding another moving part to this scaffolding pass. +// NOTE on why this file uses ST_ASSERT and not assert(): CI builds Release, +// which defines NDEBUG, which compiles every bare assert() out entirely. This +// suite previously passed unconditionally for exactly that reason. The +// ST_ASSERT macros in test_util.h are always live and report a pass/fail +// count. -#include -#include #include +#include #include "stplugin/core.h" #include "stplugin/core_c.h" +#include "test_util.h" -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. +int main() +{ + // 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(); - assert(cpp_version != nullptr); - assert(std::strlen(cpp_version) > 0); + ST_ASSERT(cpp_version != nullptr); + ST_ASSERT(cpp_version != nullptr && std::strlen(cpp_version) > 0); const char *c_version = stplugin_core_version(); - assert(c_version != nullptr); - assert(std::strcmp(cpp_version, c_version) == 0); + ST_ASSERT(c_version != nullptr); + ST_ASSERT_EQ(std::string(cpp_version ? cpp_version : ""), std::string(c_version ? c_version : "")); - // ConnectionConfig::is_valid() -- trivial non-empty checks, but - // exercised through all four combinations to prove the core library - // is genuinely testable in isolation. - stplugin::ConnectionConfig valid{"https://streamers.example.com", "main-room", "readkey123"}; - assert(valid.is_valid()); + // It is the version CMake injected, not a hand-maintained literal. + ST_ASSERT_EQ(std::string(cpp_version ? cpp_version : ""), std::string(STPLUGIN_EXPECTED_CORE_VERSION)); - stplugin::ConnectionConfig missing_url{"", "main-room", "readkey123"}; - assert(!missing_url.is_valid()); + // ConnectionConfig::is_valid(): all three fields are required. Named + // 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"}; - assert(!missing_slug.is_valid()); + ST_ASSERT(complete.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", ""}; - assert(!missing_key.is_valid()); - - std::printf("core: all tests passed\n"); - return 0; + return st_test_report("core"); }