Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
376 changes: 376 additions & 0 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
build/
build-*/
_deps/
.deps/

# CMake
CMakeCache.txt
Expand Down
62 changes: 59 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,53 @@ if(SENDSPIN_CLI_WITH_MDNS)
endif()
endif()

# Two of the error codes describe_error() names are Bonjour's alone: libavahi-compat-libdnssd
# declares neither kDNSServiceErr_ServiceNotRunning nor kDNSServiceErr_Timeout, so naming them
# unconditionally is what stops the Linux build compiling at all.
#
# They have to be probed rather than tested with the preprocessor. Both are enumerators of an
# anonymous enum rather than macros, so `#ifdef kDNSServiceErr_Timeout` is false even on macOS
# -- it would silently drop the two cases on every platform, which is worse than the break it
# was meant to fix. check_cxx_symbol_exists() cannot answer it either: it probes by taking the
# symbol's address, and an enumerator has none. Compiling a use of each is the only test left.
if(SENDSPIN_CLI_MDNS_ENABLED)
include(CheckCXXSourceCompiles)

# Empty on Apple, where dns_sd.h is in the SDK's default search path. Elsewhere it is the
# same directory the mDNS sources are compiled against, so the probe reads the header the
# build will really use rather than whichever one happens to be first on the system path.
set(CMAKE_REQUIRED_INCLUDES "${SENDSPIN_CLI_DNS_SD_INCLUDE_DIR}")

check_cxx_source_compiles(
"#include <dns_sd.h>
int main() { return kDNSServiceErr_ServiceNotRunning; }"
SENDSPIN_CLI_HAVE_ERR_SERVICE_NOT_RUNNING)
check_cxx_source_compiles(
"#include <dns_sd.h>
int main() { return kDNSServiceErr_Timeout; }"
SENDSPIN_CLI_HAVE_ERR_TIMEOUT)

unset(CMAKE_REQUIRED_INCLUDES)
endif()

# ==============================================================================
# Warnings
# ==============================================================================

# Off by default so a contributor on a newer compiler -- which invents diagnostics every
# release -- is not blocked by a warning that has nothing to do with the change they are
# making. CI passes -DSENDSPIN_CLI_WERROR=ON, which is what actually holds the line.
option(SENDSPIN_CLI_WERROR "Turn warnings from sendspin-cli's own sources into errors" OFF)

# Carried in a variable and applied per-target rather than set as a directory or global
# flag, because only our own three targets are ours to keep clean: the FetchContent'd
# sendspin and googletest trees build with these appended to whatever they already use, and
# ixwebsocket alone emits enough -Wunused-parameter to fail every build under -Werror.
set(SENDSPIN_CLI_WARNING_FLAGS -Wall -Wextra -Wpedantic)
if(SENDSPIN_CLI_WERROR)
list(APPEND SENDSPIN_CLI_WARNING_FLAGS -Werror)
endif()

# ==============================================================================
# sendspin-cli
# ==============================================================================
Expand Down Expand Up @@ -179,6 +226,15 @@ add_library(sendspin-cli-core STATIC
if(SENDSPIN_CLI_MDNS_ENABLED)
target_sources(sendspin-cli-core PRIVATE src/mdns_dnssd.cpp)
target_compile_definitions(sendspin-cli-core PUBLIC SENDSPIN_CLI_HAVE_MDNS)
# PRIVATE where SENDSPIN_CLI_HAVE_MDNS is PUBLIC: these two only ever reach a switch inside
# mdns_dnssd.cpp, so nothing that links this archive has to be told about them.
if(SENDSPIN_CLI_HAVE_ERR_SERVICE_NOT_RUNNING)
target_compile_definitions(sendspin-cli-core PRIVATE
SENDSPIN_CLI_HAVE_ERR_SERVICE_NOT_RUNNING)
endif()
if(SENDSPIN_CLI_HAVE_ERR_TIMEOUT)
target_compile_definitions(sendspin-cli-core PRIVATE SENDSPIN_CLI_HAVE_ERR_TIMEOUT)
endif()
if(NOT APPLE)
target_include_directories(sendspin-cli-core PRIVATE ${SENDSPIN_CLI_DNS_SD_INCLUDE_DIR})
target_link_libraries(sendspin-cli-core PUBLIC ${SENDSPIN_CLI_DNS_SD_LIBRARY})
Expand All @@ -201,15 +257,15 @@ endif()

target_include_directories(sendspin-cli-core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(sendspin-cli-core PUBLIC sendspin)
target_compile_options(sendspin-cli-core PRIVATE -Wall -Wextra -Wpedantic)
target_compile_options(sendspin-cli-core PRIVATE ${SENDSPIN_CLI_WARNING_FLAGS})
target_compile_definitions(sendspin-cli-core PUBLIC
SENDSPIN_CLI_VERSION="${PROJECT_VERSION}"
SENDSPIN_CLI_LIB_TAG="${SENDSPIN_GIT_TAG}"
)

add_executable(sendspin-cli src/main.cpp)
target_link_libraries(sendspin-cli PRIVATE sendspin-cli-core)
target_compile_options(sendspin-cli PRIVATE -Wall -Wextra -Wpedantic)
target_compile_options(sendspin-cli PRIVATE ${SENDSPIN_CLI_WARNING_FLAGS})

list(JOIN SENDSPIN_CLI_BACKENDS ", " SENDSPIN_CLI_BACKEND_LIST)
message(STATUS "sendspin-cli ${PROJECT_VERSION} against sendspin-cpp ${SENDSPIN_GIT_TAG}")
Expand Down Expand Up @@ -260,6 +316,6 @@ if(SENDSPIN_CLI_BUILD_TESTS)
tests/supported_formats_test.cpp
)
target_link_libraries(sendspin-cli-tests PRIVATE sendspin-cli-core GTest::gtest_main)
target_compile_options(sendspin-cli-tests PRIVATE -Wall -Wextra -Wpedantic)
target_compile_options(sendspin-cli-tests PRIVATE ${SENDSPIN_CLI_WARNING_FLAGS})
gtest_discover_tests(sendspin-cli-tests)
endif()
64 changes: 62 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ you got:
```

```bash
sudo dnf install alsa-lib-devel portaudio-devel avahi-compat-libdns_sd-devel # Fedora / RHEL
sudo apt install libasound2-dev portaudio19-dev libavahi-compat-libdnssd-dev # Debian / Ubuntu
sudo dnf install pkgconf alsa-lib-devel portaudio-devel avahi-compat-libdns_sd-devel # Fedora / RHEL
sudo apt install pkg-config libasound2-dev portaudio19-dev libavahi-compat-libdnssd-dev # Debian / Ubuntu
brew install portaudio pkgconf # macOS (no ALSA, and Bonjour is built in)
```

Expand All @@ -60,6 +60,12 @@ A host without `pkg-config` gets its own configure message saying so.
Pass `-DSENDSPIN_CLI_WITH_ALSA=OFF`, `-DSENDSPIN_CLI_WITH_PORTAUDIO=OFF` or
`-DSENDSPIN_CLI_WITH_MDNS=OFF` to leave one out even where its library is available.

`-DSENDSPIN_CLI_WERROR=ON` makes warnings fatal, for sendspin-cli's own three
targets and nothing else — the `sendspin` and GoogleTest trees fetched at
configure time are not ours to keep clean. It is off by default so that a fresh
diagnostic from a newer compiler cannot block a contributor who did not cause it;
CI turns it on, which is where the line is actually held.

> C++20 rather than C++17: sendspin-cpp's host build declares
> `target_compile_features(sendspin PUBLIC cxx_std_20)`, so the requirement
> propagates to anything that links it.
Expand Down Expand Up @@ -500,6 +506,60 @@ default only when this is the top-level project, so vendoring `sendspin-cli` int
another build does not pay for it; `-DSENDSPIN_CLI_BUILD_TESTS=OFF` turns it off
outright.

The suite is pure — nothing in `tests/` opens an audio device, a socket or the
mDNS daemon, which is what keeps `ctest` runnable anywhere at all. What that
leaves out is everything needing a real process, and that is a script instead:

```bash
scripts/smoke_test.sh build/sendspin-cli
```

It checks that the binary runs, comes up on its port, forks under `-z`, refuses a
second instance holding the same `-P`, survives an mDNS daemon it cannot reach,
and exits `0` on `SIGTERM`. CI runs it on every platform leg; run it yourself
against any build.

## CI

Every push and pull request builds on `ubuntu-24.04`, `ubuntu-24.04-arm` and
`macos-14`, plus a fourth leg configured `-DSENDSPIN_CLI_WITH_MDNS=OFF` — which
compiles `src/mdns_null.cpp` in place of `src/mdns_dnssd.cpp`, so that
configuration is built rather than assumed. Every leg builds with
`-DSENDSPIN_CLI_WERROR=ON` and runs the unit suite, and each asserts from its own
configure output that it found the backends it expects: a missing `-dev` package
does not fail a configure, so without that check the matrix would happily go green
on a deaf, undiscoverable binary.

To try a commit without building it, open its run under the repository's Actions
tab and take `sendspin-cli-<version>-<os>-<arch>` from the run summary. Inside is
a tarball holding the binary, this README, the licence, and a `BUILD-INFO.txt`
naming the runtime packages it needs. These are build outputs kept for 14 days
rather than an installation — `install()` rules, a systemd unit and distribution
packages are [`docs/ROADMAP.md`](docs/ROADMAP.md) item 10.

### macOS, and Gatekeeper

Unpack the tarball from a terminal rather than in Finder:

```bash
tar -xzf sendspin-cli-0.1.0-macos-arm64.tar.gz
./sendspin-cli-0.1.0-macos-arm64/sendspin-cli --version
```

That is not fussiness. These binaries are **ad-hoc signed** — the minimum an
arm64 Mach-O needs to execute at all, applied by the linker — so they carry no
developer identity and `spctl` rejects them. What decides whether you notice is
the quarantine flag, and `tar` does not propagate it where Finder's Archive
Utility does. If you did unpack in Finder, or macOS refuses it anyway:

```bash
xattr -d com.apple.quarantine ./sendspin-cli
```

A Developer ID signature and notarization are item 10's, together with the
`.pkg` that lets the notarization be *stapled* — `xcrun stapler` refuses a bare
executable, so signing alone would still leave an offline Mac asking Apple.

## Roadmap

See [`docs/ROADMAP.md`](docs/ROADMAP.md) for the epic breakdown and the child
Expand Down
Loading
Loading