Skip to content

Commit 773cacd

Browse files
authored
ci: add GitHub Actions workflow for multi-platform testing (#10)
* ci: add GitHub Actions workflow for multi-platform testing - Add CI workflow with lint job (C++, TypeScript, types, markdown) - Add build jobs for Linux glibc (Rocky 8), Linux musl (Alpine 3.20) - Add build jobs for macOS ARM64 (macos-14), macOS x64 (macos-13) - Test on Node.js 20 and 22 (matching engines field) - Use system FFmpeg via package managers for CI validation - Add @pproenca/webcodecs-ffmpeg-* as optionalDependencies - Update gyp/ffmpeg-paths-lib.ts to resolve FFmpeg from npm package - Resolution order: FFMPEG_ROOT > npm package > ./ffmpeg-install > system The CI uses --omit=optional to validate the system FFmpeg fallback works. Developers get FFmpeg automatically on npm install, or can opt-out to use system FFmpeg with npm install --omit=optional. * fix(ci): use npm install instead of npm ci The project doesn't commit package-lock.json, so npm ci fails. Use npm install with --omit=optional to skip FFmpeg optionalDeps. * fix(ci): use pip cpplint on Linux, fix Rocky FFmpeg install - Install cpplint via pip (npm package doesn't support Linux) - Enable EPEL and PowerTools repos before FFmpeg install - Try RPMFusion as fallback if ffmpeg-devel not in EPEL * fix(ci): don't skip optional deps for lint (biome needs them) * fix(ci): build TS for lint:types, use Python 3.9 on Rocky - Add npm run build:ts step before lint:types (tsd needs dist/index.d.ts) - Use Python 3.9 on Rocky Linux 8 (node-gyp requires Python 3.8+) - Use RPMFusion for FFmpeg on Rocky 8 (EPEL doesn't have ffmpeg-devel) * fix(ci): use GCC Toolset 12 on Rocky, install Alpine FFmpeg deps Linux glibc (Rocky 8): - Install gcc-toolset-12 for C++20 support (Rocky 8 default GCC 8.x lacks -std=c++20) - Source toolset enable script before build Linux musl (Alpine 3.20): - Install all FFmpeg transitive dependencies required by pkg-config - Alpine's FFmpeg is compiled with many optional features, all need their dev packages * fix(ci): use Rocky 9 for FFmpeg 6+, simplify Alpine FFmpeg Rocky Linux 9: - Switch from Rocky 8 to Rocky 9 (glibc 2.34 instead of 2.28) - Rocky 8's RPMFusion only has FFmpeg 4.x, project requires 5.0+ - Rocky 9 RPMFusion has FFmpeg 6+ with C++20 support out of box Alpine 3.20: - Use pkgconf instead of pkgconfig (correct Alpine package name) - Simplify to just ffmpeg-dev (pulls in all core dependencies) - Add pkg-config verification step to catch errors early * fix: add FFmpeg 5.0 compatibility for AVFrame::duration AVFrame::duration was added in FFmpeg 5.1 (libavutil 57.28.100). For FFmpeg 5.0, use the deprecated pkt_duration field instead. Added AV_FRAME_DURATION and AV_FRAME_SET_DURATION macros in common.h that select the correct field based on FFmpeg version. * chore: commit cmake * feat(build): harden native build configuration - Add Linux FFmpeg fallback paths (/usr/include, /usr/local/include, /usr/lib, /usr/local/lib) - Add RPATH for macOS (@loader_path/../lib) and Linux ($ORIGIN/../lib) - Add MacPorts support (/opt/local/include, /opt/local/lib) - Add dynamic linking fallback when static fails on Linux - Add explicit error logging with [node-webcodecs] prefix - Add pkg-config output validation (empty check) - Add isPkgConfigAvailable() helper function - Add -Wpedantic and -Wshadow compiler warnings - Add Debug/Release configurations block - Add parallel compilation (-j max) to build scripts - Add node-gyp caching to CI workflow - Implement rpath mode in ffmpeg-paths.js Closes: harden-native-build-config * fix(build): use dynamic linking for Linux pkg-config fallback Alpine's ffmpeg-dev only provides shared libraries. The --static flag caused pkg-config to output flags for non-existent static libraries, which then failed at link time. Dynamic linking works correctly for CI testing with system FFmpeg. Static linking is only needed for distribution builds with bundled FFmpeg. * Revert "fix(build): use dynamic linking for Linux pkg-config fallback" This reverts commit 7b038e5. * feat(ci): use static FFmpeg musl package for Alpine builds - Add musl libc detection in ffmpeg-paths-lib.ts - On musl systems, try @pproenca/webcodecs-ffmpeg-linux-x64-musl first - Update CI to install musl FFmpeg package instead of system ffmpeg-dev - Remove system pkg-config fallback for Alpine (use npm package only) This enables proper static linking on Alpine Linux using the pre-built FFmpeg package with musl libc support. * feat: add musl FFmpeg package to optionalDependencies npm will auto-install @pproenca/webcodecs-ffmpeg-linux-x64-musl on Alpine/musl systems via the libc field in the package's package.json. Simplified CI to use regular npm install instead of explicit package install. * chore: re-trigger CI after musl FFmpeg package published * feat(build): use npm packages for FFmpeg headers and link flags - Add @pproenca/webcodecs-ffmpeg-dev for cross-platform headers - Add tryResolveIncludeFromDevPackage() for header resolution - Add tryResolveLinkFlagsFromNpmPackage() for static link flags - Update resolution order: npm packages take precedence - Bump FFmpeg packages to ^0.1.4 with ./link-flags export - Update unit test for new resolution priority This enables fully static builds from npm packages without requiring system FFmpeg or pkg-config with hardcoded paths. * fix(ci): add zlib-dev and bzip2-dev for Alpine musl builds FFmpeg linking requires system compression libraries (-lz, -lbz2) which were missing from the Alpine container package installation. * fix(ci): disable LTO for Alpine musl builds Add -fno-lto to Linux cflags_cc and ldflags to prevent LTO conflicts with musl's fortified libc functions. The FFmpeg static libraries are compiled with LTO, which causes link failures on Alpine due to vsnprintf being marked always_inline in fortify headers. * fix(ci): use npm FFmpeg packages for Linux glibc builds - Remove ffmpeg-devel from dnf install (not needed with npm packages) - Remove --omit=optional to install @pproenca/webcodecs-ffmpeg-* packages The platform packages contain static FFmpeg libs and link-flags.js, enabling fully static builds without system FFmpeg. * feat(ci): align build matrix with ffmpeg-prebuilds packages Expand CI from 4 builds to 10 builds matching @pproenca/webcodecs-ffmpeg: - 5 platforms: darwin-arm64, darwin-x64, linux-x64-glibc, linux-arm64, linux-x64-musl - 2 variants: free (LGPL), non-free (GPL) Changes: - Refactor to unified matrix strategy (replaces 4 separate jobs) - Add linux-arm64 support via QEMU emulation - Add variant-aware FFmpeg package installation - macOS free variant uses npm packages, non-free uses Homebrew - Linux builds explicitly install correct FFmpeg package per variant - Update ffmpeg-prebuilds to 0.1.6 - Add linux-arm64 to optionalDependencies - Simplify to Node.js 20 only * fix(ci): update ffmpeg-prebuilds to 0.1.7 and add zlib/bzip2 deps - Update @pproenca/webcodecs-ffmpeg packages to ^0.1.7 (fixes LTO issue) - Add zlib-devel bzip2-devel to Rocky Linux builds * fix(ci): clean npm cache before installing FFmpeg packages Prevents stale cache from causing "Access token expired" warnings. * fix(ci): add libatomic for Linux glibc builds The npm FFmpeg packages include -latomic in link flags, which requires the libatomic library to be installed for linking. This was missing from the Rocky Linux CI containers.
1 parent ca693e7 commit 773cacd

33 files changed

Lines changed: 3820 additions & 42 deletions

.claude/skills/dev-cmake/SKILL.md

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
---
2+
name: dev-cmake
3+
description: Write CMake and Make files like a principal engineer with decades of experience. Use when creating, refactoring, reviewing, or debugging CMakeLists.txt, Makefiles, or build system configurations for C/C++ projects. Produces elegant, minimal, modern build files that reflect deep understanding of build systems.
4+
---
5+
6+
# CMake & Make Expert
7+
8+
Write build files that are elegant because the understanding is deep. Every line should have a reason. Simplicity comes from mastery, not shortcuts.
9+
10+
## Core Philosophy
11+
12+
**Targets are everything.** Modern CMake is about targets and properties, not variables and directories. Think of targets as objects with member functions and properties.
13+
14+
**Explicit over implicit.** Always specify `PRIVATE`, `PUBLIC`, or `INTERFACE`. Never rely on inherited directory-level settings.
15+
16+
**Minimal surface area.** Expose only what consumers need. Default to `PRIVATE`; use `PUBLIC` only when downstream targets genuinely require it.
17+
18+
## CMake: The Principal Engineer Approach
19+
20+
### Project Structure
21+
```cmake
22+
cmake_minimum_required(VERSION 3.16)
23+
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
24+
25+
# Set standards at target level, not globally
26+
# Use compile features, not flags
27+
```
28+
29+
### Target Definition Pattern
30+
```cmake
31+
add_library(mylib)
32+
add_library(MyProject::mylib ALIAS mylib)
33+
34+
target_sources(mylib
35+
PRIVATE
36+
src/impl.cpp
37+
PUBLIC
38+
FILE_SET HEADERS
39+
BASE_DIRS include
40+
FILES include/mylib/api.h
41+
)
42+
43+
target_compile_features(mylib PUBLIC cxx_std_17)
44+
45+
target_include_directories(mylib
46+
PUBLIC
47+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
48+
$<INSTALL_INTERFACE:include>
49+
)
50+
```
51+
52+
### What to Never Do
53+
- `include_directories()` — use `target_include_directories()`
54+
- `link_directories()` — use full paths or targets
55+
- `add_definitions()` — use `target_compile_definitions()`
56+
- `link_libraries()` — use `target_link_libraries()`
57+
- `CMAKE_CXX_FLAGS` manipulation — use `target_compile_options()` or features
58+
- `file(GLOB)` for sources — list files explicitly
59+
- Bare library names in `target_link_libraries()` — use namespaced targets
60+
61+
### Dependency Handling
62+
63+
For find_package dependencies:
64+
```cmake
65+
find_package(Boost 1.70 REQUIRED COMPONENTS filesystem)
66+
target_link_libraries(mylib PRIVATE Boost::filesystem)
67+
```
68+
69+
For FetchContent (prefer over ExternalProject for CMake deps):
70+
```cmake
71+
include(FetchContent)
72+
FetchContent_Declare(fmt
73+
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
74+
GIT_TAG 10.1.0
75+
)
76+
FetchContent_MakeAvailable(fmt)
77+
target_link_libraries(mylib PRIVATE fmt::fmt)
78+
```
79+
80+
### Generator Expressions
81+
82+
Use for build/install path differences:
83+
```cmake
84+
target_include_directories(mylib PUBLIC
85+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
86+
$<INSTALL_INTERFACE:include>
87+
)
88+
```
89+
90+
Use for conditional compilation:
91+
```cmake
92+
target_compile_definitions(mylib PRIVATE
93+
$<$<CONFIG:Debug>:DEBUG_MODE>
94+
)
95+
```
96+
97+
## Make: The Principal Engineer Approach
98+
99+
### Essential Structure
100+
```makefile
101+
# Immediately expanded defaults
102+
CC ?= gcc
103+
CXX ?= g++
104+
CFLAGS ?= -Wall -Wextra -pedantic
105+
LDFLAGS ?=
106+
107+
# Preserve user-provided flags
108+
CFLAGS += -MMD -MP
109+
110+
# Automatic dependency tracking
111+
SRCS := $(wildcard src/*.c)
112+
OBJS := $(SRCS:src/%.c=build/%.o)
113+
DEPS := $(OBJS:.o=.d)
114+
115+
.PHONY: all clean
116+
117+
all: bin/program
118+
119+
bin/program: $(OBJS) | bin
120+
$(CC) $(LDFLAGS) -o $@ $^
121+
122+
build/%.o: src/%.c | build
123+
$(CC) $(CFLAGS) -c -o $@ $<
124+
125+
bin build:
126+
mkdir -p $@
127+
128+
clean:
129+
rm -rf build bin
130+
131+
-include $(DEPS)
132+
```
133+
134+
### Pattern Rules — The Elegant Way
135+
```makefile
136+
# Single pattern rule replaces N explicit rules
137+
%.o: %.c
138+
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
139+
```
140+
141+
### Automatic Variables (memorize these)
142+
- `$@` — target
143+
- `$<` — first prerequisite
144+
- `$^` — all prerequisites (no duplicates)
145+
- `$+` — all prerequisites (with duplicates, for libs)
146+
- `$*` — stem (matched by %)
147+
148+
### What Makes Makefiles Elegant
149+
1. Order-only prerequisites (`| dir`) for directory creation
150+
2. `-include` for optional dependency files
151+
3. `?=` for overridable defaults, `+=` to append
152+
4. `.PHONY` for non-file targets
153+
5. `.DELETE_ON_ERROR` to clean failed builds
154+
6. Consistent variable naming (UPPERCASE for user-facing)
155+
156+
## Reference Files
157+
158+
- **references/cmake-patterns.md** — Complete modern CMake patterns (library export, install, presets, toolchains)
159+
- **references/make-patterns.md** — Advanced Make patterns (multi-directory, cross-compilation, dependencies)
160+
- **references/antipatterns.md** — Common mistakes and their fixes
161+
162+
## Quality Checklist
163+
164+
Before finalizing any build file:
165+
166+
### CMake
167+
- [ ] Every target has a namespaced alias
168+
- [ ] All `target_*` calls specify scope (`PRIVATE`/`PUBLIC`/`INTERFACE`)
169+
- [ ] No directory-level commands (`include_directories`, etc.)
170+
- [ ] Generator expressions for build/install differences
171+
- [ ] Version requirements on `find_package`
172+
- [ ] `cmake_minimum_required` reflects actual features used
173+
174+
### Make
175+
- [ ] All variables use `?=` or `+=` appropriately
176+
- [ ] Automatic dependency generation (`-MMD -MP`)
177+
- [ ] Pattern rules instead of repeated explicit rules
178+
- [ ] `.PHONY` declared for non-file targets
179+
- [ ] Clean target removes all generated files
180+
- [ ] Build artifacts in separate directory from source
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
cmake_minimum_required(VERSION 3.16)
2+
project(MyLibrary
3+
VERSION 1.0.0
4+
DESCRIPTION "A well-structured C++ library"
5+
LANGUAGES CXX
6+
)
7+
8+
# ============================================================================
9+
# Options
10+
# ============================================================================
11+
option(MYLIB_BUILD_TESTS "Build unit tests" OFF)
12+
option(MYLIB_BUILD_EXAMPLES "Build examples" OFF)
13+
14+
# ============================================================================
15+
# Library Target
16+
# ============================================================================
17+
add_library(mylib)
18+
add_library(${PROJECT_NAME}::mylib ALIAS mylib)
19+
20+
target_sources(mylib
21+
PRIVATE
22+
src/mylib.cpp
23+
# PUBLIC headers via FILE_SET (CMake 3.23+) or target_include_directories
24+
)
25+
26+
target_compile_features(mylib PUBLIC cxx_std_17)
27+
28+
target_include_directories(mylib
29+
PUBLIC
30+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
31+
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
32+
PRIVATE
33+
${CMAKE_CURRENT_SOURCE_DIR}/src
34+
)
35+
36+
# Uncomment and modify for dependencies:
37+
# find_package(fmt REQUIRED)
38+
# target_link_libraries(mylib PUBLIC fmt::fmt)
39+
40+
target_compile_options(mylib
41+
PRIVATE
42+
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic>
43+
$<$<CXX_COMPILER_ID:MSVC>:/W4>
44+
)
45+
46+
set_target_properties(mylib PROPERTIES
47+
VERSION ${PROJECT_VERSION}
48+
SOVERSION ${PROJECT_VERSION_MAJOR}
49+
CXX_VISIBILITY_PRESET hidden
50+
VISIBILITY_INLINES_HIDDEN ON
51+
)
52+
53+
# ============================================================================
54+
# Installation
55+
# ============================================================================
56+
include(GNUInstallDirs)
57+
include(CMakePackageConfigHelpers)
58+
59+
install(TARGETS mylib
60+
EXPORT ${PROJECT_NAME}Targets
61+
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
62+
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
63+
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
64+
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
65+
)
66+
67+
install(DIRECTORY include/
68+
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
69+
)
70+
71+
install(EXPORT ${PROJECT_NAME}Targets
72+
FILE ${PROJECT_NAME}Targets.cmake
73+
NAMESPACE ${PROJECT_NAME}::
74+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
75+
)
76+
77+
configure_package_config_file(
78+
${CMAKE_CURRENT_SOURCE_DIR}/cmake/${PROJECT_NAME}Config.cmake.in
79+
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
80+
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
81+
)
82+
83+
write_basic_package_version_file(
84+
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
85+
VERSION ${PROJECT_VERSION}
86+
COMPATIBILITY SameMajorVersion
87+
)
88+
89+
install(FILES
90+
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
91+
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
92+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
93+
)
94+
95+
# ============================================================================
96+
# Testing
97+
# ============================================================================
98+
if(MYLIB_BUILD_TESTS)
99+
enable_testing()
100+
add_subdirectory(tests)
101+
endif()

0 commit comments

Comments
 (0)