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
86 changes: 81 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ jobs:
# Every leg's result, not just the first failure.
fail-fast: false
# Spell every key on every leg, false included: a missing key reads as false and silently disables that leg's work.
# A publish: true leg also needs entries in both asset sets release.yml diffs (macOS: tarball and .pkg).
# A publish: true leg also needs entries in both asset sets release.yml diffs (macOS: tarball and .pkg;
# Windows: zip).
matrix:
include:
- name: linux-x86_64
Expand Down Expand Up @@ -106,6 +107,23 @@ jobs:
avahi: false
systemd: true

# clang-cl from the image's Visual Studio; PortAudio is built from source and DNS-SD is
# part of Windows, so nothing is installed. No probes: there are no Bonjour enumerators.
- name: windows-x86_64
runner: windows-2025
arm32: ''
with_mdns: 'ON'
with_portaudio: 'ON'
with_pulse: 'OFF'
with_pipewire: 'OFF'
expect_probe: ''
expect_backends: '^-- sendspin-cli audio backends: null, stdout, portaudio$'
expect_mdns: '^-- sendspin-cli mDNS: windns \(built in\)$'
runtime_packages: 'none. PortAudio is linked in, and DNS-SD is part of Windows 10 1809 and later.'
publish: true
avahi: false
systemd: false

steps:
# Actions pinned to commits; the trailing comment is the version.
- name: Check out
Expand All @@ -124,6 +142,37 @@ jobs:
libpipewire-0.3-dev \
libavahi-compat-libdnssd-dev

# What vcvars64.bat changes, exported for the later steps; inline rather than a third-party action.
- name: Set up clang-cl and the MSVC environment (Windows)
if: runner.os == 'Windows'
run: |
vswhere='/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe'
vs=$("$vswhere" -latest -products '*' \
-requires Microsoft.VisualStudio.Component.VC.Llvm.Clang -property installationPath)
[ -n "$vs" ] || {
echo '::error::this image has no Visual Studio with the clang-cl component'
exit 1
}
echo "Visual Studio: $vs"

printf '@call "%s\\VC\\Auxiliary\\Build\\vcvars64.bat" >nul\r\n@set\r\n' "$vs" >vcvars.bat
cmd //c set | tr -d '\r' | sort >before.txt
# A full path: cmd may not look in the current directory for it.
cmd //c "$(cygpath -w "$PWD/vcvars.bat")" | tr -d '\r' | sort >after.txt
grep -qi '^VCToolsInstallDir=' after.txt || {
echo '::error::vcvars64.bat did not set up the developer environment'
exit 1
}

# New or changed variables; PATH goes through GITHUB_PATH instead.
comm -13 before.txt after.txt | grep -vi '^path=' >>"$GITHUB_ENV"
before_path=$(grep -i '^path=' before.txt | cut -d= -f2- | tr ';' '\n')
grep -i '^path=' after.txt | cut -d= -f2- | tr ';' '\n' |
grep -vxF -f <(printf '%s\n' "$before_path") >>"$GITHUB_PATH" || true
# The developer environment does not put clang-cl on PATH.
printf '%s\n' "$vs\\VC\\Tools\\Llvm\\x64\\bin" >>"$GITHUB_PATH"
rm -f vcvars.bat before.txt after.txt

# Instead of the step above, so no host-arch -dev library can answer find_library.
- name: Install the cross toolchain and armhf dependencies (cross-compiled 32-bit ARM)
if: matrix.arm32
Expand Down Expand Up @@ -233,6 +282,9 @@ jobs:
-DSENDSPIN_CLI_WITH_PIPEWIRE="$WITH_PIPEWIRE"
-DFETCHCONTENT_BASE_DIR="$DEPS_DIR"
)
if [ "$RUNNER_OS" = 'Windows' ]; then
options+=(-G Ninja -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl)
fi

if [ -n "$ARM32" ]; then
scripts/build_arm32.sh "$ARM32" build "${options[@]}" 2>&1 | tee configure.log
Expand Down Expand Up @@ -331,9 +383,15 @@ jobs:

# Before avahi-daemon is installed, to exercise the non-fatal advertise-failure path.
- name: Smoke test
if: matrix.publish
if: matrix.publish && runner.os != 'Windows'
run: scripts/smoke_test.sh build/sendspin-cli

# smoke_test.sh's kill/wait handling does not apply to a Windows binary; the same ground in PowerShell.
- name: Smoke test (Windows)
if: matrix.publish && runner.os == 'Windows'
shell: pwsh
run: scripts/smoke_test_windows.ps1 -Binary build/sendspin-cli.exe

# Proves libavahi-compat-libdnssd implements our dns_sd calls: register, browse, resolve, QueryRecord.
- name: Advertise and discover through a real Avahi daemon
if: matrix.avahi
Expand Down Expand Up @@ -413,7 +471,7 @@ jobs:

# DESTDIR (not --prefix) keeps ExecStart valid; --component keeps it ours; tarred to keep +x.
- name: Package
if: matrix.publish
if: matrix.publish && runner.os != 'Windows'
id: package
env:
RUNTIME_PACKAGES: ${{ matrix.runtime_packages }}
Expand Down Expand Up @@ -519,7 +577,7 @@ jobs:

# Assert the exact payload; keep in step with build-armv6.yml's list of the same.
- name: Assert the payload holds exactly what it should
if: matrix.publish
if: matrix.publish && runner.os != 'Windows'
env:
NAME: ${{ steps.package.outputs.name }}
run: |
Expand Down Expand Up @@ -873,8 +931,17 @@ jobs:
/usr/local/bin/sendspin-cli --version
scripts/smoke_test.sh /usr/local/bin/sendspin-cli

# A zip of the binary and its docs, asserted to hold exactly that.
- name: Package (Windows)
if: matrix.publish && runner.os == 'Windows'
id: package_windows
shell: pwsh
env:
LEG: ${{ matrix.name }}
run: scripts/package_windows.ps1 -Binary build/sendspin-cli.exe -Leg $env:LEG

- name: Upload
if: matrix.publish
if: matrix.publish && runner.os != 'Windows'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ steps.package.outputs.name }}
Expand All @@ -883,6 +950,15 @@ jobs:
retention-days: 14
if-no-files-found: error

- name: Upload (Windows)
if: matrix.publish && runner.os == 'Windows'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ steps.package_windows.outputs.name }}
path: ${{ steps.package_windows.outputs.name }}.zip
retention-days: 14
if-no-files-found: error

# A separate step: if-no-files-found: error would fail Linux legs, and artifact names must differ.
- name: Upload the macOS installer
if: matrix.publish && runner.os == 'macOS'
Expand Down
14 changes: 10 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,14 @@ jobs:
# Diff the downloaded set against the expected assets: a green build may have uploaded nothing.
- name: Assert the release is complete before creating anything
run: |
# File names, spelled out; macOS contributes a tarball and a .pkg.
# File names, spelled out; macOS contributes a tarball and a .pkg, Windows a zip.
expected=$(mktemp)
{
for leg in linux-x86_64 linux-arm64 linux-armv7 linux-armv6 macos-arm64; do
echo "sendspin-cli-$VERSION-$leg.tar.gz"
done
echo "sendspin-cli-$VERSION-macos-arm64.pkg"
echo "sendspin-cli-$VERSION-windows-x86_64.zip"
} | sort >"$expected"

actual=$(mktemp)
Expand All @@ -177,8 +178,8 @@ jobs:
- name: Checksum what is being published
run: |
cd dist
# The .pkg is checksummed too; an unmatched glob errors under -e, and the set is already asserted.
sha256sum -- *.tar.gz *.pkg >SHA256SUMS
# The .pkg and .zip are checksummed too; an unmatched glob errors under -e, and the set is already asserted.
sha256sum -- *.tar.gz *.pkg *.zip >SHA256SUMS
cat SHA256SUMS

- name: Write the release notes
Expand Down Expand Up @@ -215,6 +216,10 @@ jobs:
a link because it is worth knowing before you download; what to do about it is under
[the macOS installation guide](https://github.com/@REPO@/wiki/Installation#macos).

The Windows zip is **unsigned** as well, so SmartScreen may warn the first time it runs.
[Getting Started on Windows](https://github.com/@REPO@/wiki/Getting-Started-on-Windows)
has a guided installer that checks the zip against `SHA256SUMS` before installing it.

- [README](https://github.com/@REPO@/blob/@TAG@/README.md) — what it is, how to run
it, and how to contribute
- [Contributors](https://github.com/@REPO@/blob/@TAG@/contributors.md) — development,
Expand Down Expand Up @@ -244,7 +249,7 @@ jobs:
--title "sendspin-cli $VERSION" \
--notes-file notes.md \
--draft \
dist/*.tar.gz dist/*.pkg dist/SHA256SUMS | tee created.txt
dist/*.tar.gz dist/*.pkg dist/*.zip dist/SHA256SUMS | tee created.txt

# Resolve the draft's numeric id from its html_url once; later steps use the REST id endpoints.
url="$(tail -n1 created.txt)"
Expand All @@ -271,6 +276,7 @@ jobs:
echo "sendspin-cli-$VERSION-$leg.tar.gz"
done
echo "sendspin-cli-$VERSION-macos-arm64.pkg"
echo "sendspin-cli-$VERSION-windows-x86_64.zip"
} | sort >"$expected"

# Read back by id: releases/tags/<tag> never returns drafts, which once failed every release.
Expand Down
67 changes: 58 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ endif()

# sendspin-cpp

set(SENDSPIN_GIT_TAG "90cbd80864bf763fb6a4100f23b1ad2e128c8a75" CACHE STRING
set(SENDSPIN_GIT_TAG "d961f5fb4d2c95e10162f5939d086860e5aeb501" CACHE STRING
"sendspin-cpp git tag or commit to build against")

# player, metadata, controller (group transport verbs) and source; no FORCE, so -D overrides still win.
Expand Down Expand Up @@ -103,14 +103,32 @@ if(SENDSPIN_CLI_WITH_COREAUDIO AND APPLE)
list(APPEND SENDSPIN_CLI_BACKENDS coreaudio)
endif()

if(SENDSPIN_CLI_WITH_PORTAUDIO)
if(SENDSPIN_CLI_WITH_PORTAUDIO AND WIN32)
# No pkg-config or system PortAudio on Windows, so it is built from source; it is C.
enable_language(C)
FetchContent_Declare(
portaudio
GIT_REPOSITORY https://github.com/PortAudio/portaudio.git
GIT_TAG v19.7.0
GIT_SHALLOW TRUE
)
set(PA_BUILD_SHARED OFF CACHE BOOL "Build PortAudio as a shared library")
# v19.7.0 declares cmake_minimum_required(VERSION 3.1), which CMake 4 rejects.
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
FetchContent_MakeAvailable(portaudio)
unset(CMAKE_POLICY_VERSION_MINIMUM)
set(SENDSPIN_CLI_PORTAUDIO_TARGET portaudio_static)
set(SENDSPIN_CLI_PORTAUDIO_ENABLED ON)
list(APPEND SENDSPIN_CLI_BACKENDS portaudio)
elseif(SENDSPIN_CLI_WITH_PORTAUDIO)
# No CMake module; IMPORTED_TARGET carries macOS framework links through.
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(PORTAUDIO QUIET IMPORTED_TARGET portaudio-2.0)
endif()

if(TARGET PkgConfig::PORTAUDIO)
set(SENDSPIN_CLI_PORTAUDIO_TARGET PkgConfig::PORTAUDIO)
set(SENDSPIN_CLI_PORTAUDIO_ENABLED ON)
list(APPEND SENDSPIN_CLI_BACKENDS portaudio)
elseif(NOT PkgConfig_FOUND)
Expand Down Expand Up @@ -186,14 +204,19 @@ endif()

# mDNS

# Auto-detected: Bonjour on macOS, libavahi-compat-libdnssd on Linux. OFF forces it out.
# Auto-detected: Bonjour on macOS, libavahi-compat-libdnssd on Linux, windns on Windows. OFF
# forces it out.
option(SENDSPIN_CLI_WITH_MDNS "Build mDNS advertisement/discovery if dns_sd.h is available" ON)

set(SENDSPIN_CLI_MDNS_ENABLED OFF)
set(SENDSPIN_CLI_MDNS_DESCRIPTION "none")

if(SENDSPIN_CLI_WITH_MDNS)
if(APPLE)
if(WIN32)
# Windows 10 1809+ has DNS-SD in dnsapi, so there is nothing to find.
set(SENDSPIN_CLI_MDNS_ENABLED ON)
set(SENDSPIN_CLI_MDNS_DESCRIPTION "windns (built in)")
elseif(APPLE)
# dns_sd lives in libSystem here, so there is nothing to find and nothing to link.
set(SENDSPIN_CLI_MDNS_ENABLED ON)
set(SENDSPIN_CLI_MDNS_DESCRIPTION "dns_sd (Bonjour, built in)")
Expand All @@ -214,7 +237,7 @@ if(SENDSPIN_CLI_WITH_MDNS)
endif()

# Bonjour-only error enumerators, probed by compiling a use: #ifdef cannot see enumerators.
if(SENDSPIN_CLI_MDNS_ENABLED)
if(SENDSPIN_CLI_MDNS_ENABLED AND NOT WIN32)
include(CheckCXXSourceCompiles)

# The same dns_sd.h include directory the build uses.
Expand All @@ -238,7 +261,12 @@ endif()
option(SENDSPIN_CLI_WERROR "Turn warnings from sendspin-cli's own sources into errors" OFF)

# Applied per target: FetchContent'd trees (ixwebsocket especially) would fail under -Werror.
set(SENDSPIN_CLI_WARNING_FLAGS -Wall -Wextra -Wpedantic)
if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
# clang-cl reads -Wall as -Weverything; /W4 is its -Wall -Wextra.
set(SENDSPIN_CLI_WARNING_FLAGS /W4 -Wpedantic)
else()
set(SENDSPIN_CLI_WARNING_FLAGS -Wall -Wextra -Wpedantic)
endif()
if(SENDSPIN_CLI_WERROR)
list(APPEND SENDSPIN_CLI_WARNING_FLAGS -Werror)
endif()
Expand All @@ -254,7 +282,6 @@ add_library(sendspin-cli-core STATIC
src/control_common.cpp
src/control_socket.cpp
src/config_file.cpp
src/daemon.cpp
src/hooks.cpp
src/key_value_file.cpp
src/log.cpp
Expand All @@ -270,8 +297,23 @@ add_library(sendspin-cli-core STATIC
src/supported_formats.cpp
)

# Windows has no fork(), flock() or /bin/sh, so -z, locks and hooks have their own implementation.
if(WIN32)
target_sources(sendspin-cli-core PRIVATE src/daemon_win.cpp src/hooks_win.cpp src/win_security.cpp)
# POSIX names (open, close, unlink, ...) without the CRT's deprecation warnings.
target_compile_definitions(sendspin-cli-core PUBLIC
NOMINMAX WIN32_LEAN_AND_MEAN NOGDI _CRT_NONSTDC_NO_WARNINGS _CRT_SECURE_NO_WARNINGS)
target_link_libraries(sendspin-cli-core PUBLIC ws2_32 advapi32 shell32 ole32)
else()
target_sources(sendspin-cli-core PRIVATE src/daemon.cpp src/hooks_posix.cpp)
endif()

# One MdnsService implementation per build.
if(SENDSPIN_CLI_MDNS_ENABLED)
if(SENDSPIN_CLI_MDNS_ENABLED AND WIN32)
target_sources(sendspin-cli-core PRIVATE src/mdns_windns.cpp)
target_compile_definitions(sendspin-cli-core PUBLIC SENDSPIN_CLI_HAVE_MDNS)
target_link_libraries(sendspin-cli-core PUBLIC dnsapi)
elseif(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: only mdns_dnssd.cpp reads these.
Expand Down Expand Up @@ -319,7 +361,7 @@ if(SENDSPIN_CLI_PORTAUDIO_ENABLED)
src/portaudio_sink.cpp
src/portaudio_source.cpp
)
target_link_libraries(sendspin-cli-core PUBLIC PkgConfig::PORTAUDIO)
target_link_libraries(sendspin-cli-core PUBLIC ${SENDSPIN_CLI_PORTAUDIO_TARGET})
target_compile_definitions(sendspin-cli-core PUBLIC SENDSPIN_CLI_HAVE_PORTAUDIO)
endif()

Expand Down Expand Up @@ -433,7 +475,14 @@ if(SENDSPIN_CLI_BUILD_TESTS)
)
set(INSTALL_GTEST OFF CACHE BOOL "Install GoogleTest alongside our own targets")
set(BUILD_GMOCK OFF CACHE BOOL "Build GoogleMock")
# The CRT the rest of the build links (/MD), not GoogleTest's own /MT default.
set(gtest_force_shared_crt ON CACHE BOOL "Use the shared CRT for GoogleTest")
FetchContent_MakeAvailable(googletest)
if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
# GoogleTest builds itself with /WX, which newer clang-cl warnings break.
target_compile_options(gtest PRIVATE -Wno-error)
target_compile_options(gtest_main PRIVATE -Wno-error)
endif()

enable_testing()
include(GoogleTest)
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[Sendspin](https://github.com/Sendspin/spec) protocol. It appears on your network, a
Sendspin server finds it, and it plays synchronized audio with the rest of your group.

It runs on Linux and Apple-silicon macOS.
It runs on Linux, Apple-silicon macOS and Windows 10 1809 or later.

## Quick start

Expand Down Expand Up @@ -39,6 +39,20 @@ Download the Apple-silicon `.pkg` installer or tarball from
[installation guide](https://github.com/Sendspin/sendspin-cpp-cli/wiki/Installation#macos)
and start a player from a terminal with `sendspin-cli -n living-room`.

### Windows

The guided Windows installer needs no administrator rights:

```powershell
Invoke-WebRequest https://raw.githubusercontent.com/Sendspin/sendspin-cpp-cli/main/scripts/get_started_windows.ps1 -OutFile get_started_windows.ps1
notepad get_started_windows.ps1
powershell -ExecutionPolicy Bypass -File .\get_started_windows.ps1
```

It verifies the release, installs it for your user and starts it whenever you log on. See
[Getting Started on Windows](https://github.com/Sendspin/sendspin-cpp-cli/wiki/Getting-Started-on-Windows)
for the walkthrough, including the firewall rule a server needs to reach it.

### Pair it with your server

The player advertises itself on the local network and a Sendspin server finds it.
Expand All @@ -49,7 +63,7 @@ Print the token to paste into the server:
# Linux service, as installed above
sudo sendspin-cli pair-token --control-socket /run/sendspin-cli/control.sock

# --user-service, macOS, or a player started from your own shell
# --user-service, macOS, Windows, or a player started from your own shell
sendspin-cli pair-token
```

Expand Down
Loading
Loading