Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
cmake_minimum_required(VERSION 3.15)
project(miniaudio LANGUAGES C)

include(GNUInstallDirs)

set(SOURCE_SUBFOLDER ${MINIAUDIO_SRC_DIR}/extras/miniaudio_split/)
add_library(miniaudio ${SOURCE_SUBFOLDER}/miniaudio.c)
target_include_directories(miniaudio PRIVATE ${SOURCE_SUBFOLDER})

if (BUILD_SHARED_LIBS)
target_compile_definitions(miniaudio PRIVATE MA_DLL MINIAUDIO_IMPLEMENTATION)
endif()

set_target_properties(miniaudio
PROPERTIES
VERSION ${MINIAUDIO_VERSION_STRING}
PUBLIC_HEADER ${SOURCE_SUBFOLDER}/miniaudio.h)

install(TARGETS miniaudio
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import (
"os"
"path/filepath"
"runtime"
"slices"
"strings"
)

id "mackron/miniaudio"

// The CCI recipe serves every listed release with the same split-source
// interface. The oldest ref is a commit because upstream did not tag 0.10.39.
Comment on lines +11 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Comment overstates split-source as the universal interface

"serves every listed release with the same split-source interface" is inaccurate for the default: with header_only=ON (the default) the served header is the single-file top-level miniaudio.h copied on line 63, not the split source. Split source is only used in the compiled (!headerOnly) path. Similarly, the miniaudio_cmp.gox header comment claims it maps "every current upstream tag", but the string branch is a fixed list capped at 0.11.25 (see the P1 note). Recommend clarifying both comments.

fromVer "8bf157f10e278302f8a6c1c9cd1065f2bea26dd2"

defaults {
"header_only": "ON",
"shared": "OFF",
"fPIC": "ON",
}

filter => {
for name, values in target.options {
if name != "header_only" && name != "shared" && name != "fPIC" {
return false
}
for value in values {
if value != "ON" && value != "OFF" {
return false
}
}
}
return true
}

onBuild ctx => {
installDir := ctx.outputDir
headerOnly := slices.contains(target.options["header_only"], "ON")
shared := slices.contains(target.options["shared"], "ON") && !headerOnly

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] header_only=ON + shared=ON collapses to header-only silently

shared is forced false whenever headerOnly is true, but the filter (lines 21-33) accepts any ON/OFF combination. So a target requesting header_only=ON, shared=ON passes the filter as a distinct, cacheable configuration yet produces the exact same header-only artifact as header_only=ON, shared=OFF — a consumer that asked for a shared library gets a header-only interface with no diagnostic. Either reject the contradictory combination in filter, or normalize the option set so the two do not present as separate variants.

fPIC := slices.contains(target.options["fPIC"], "ON")

header := string(os.readFile(filepath.join(ctx.SourceDir, "miniaudio.h"))!)
versionParts := map[string]string{}
for line in strings.split(header, "\n") {
fields := strings.fields(line)
if fields.len == 3 && fields[0] == "#define" {
versionParts[fields[1]] = fields[2]
}
}
version := strings.join([]string{
versionParts["MA_VERSION_MAJOR"],
versionParts["MA_VERSION_MINOR"],
versionParts["MA_VERSION_REVISION"],
}, ".")
Comment on lines +49 to +53

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Version parts are not validated before use

version is assembled from MA_VERSION_MAJOR/MINOR/REVISION with no guard that all three were found. If any #define is absent (e.g. a future header layout change), the corresponding map lookup returns "" and version becomes something like 0..39 or ... That malformed string is then written into the CMake VERSION property (line 71 -> set_target_properties, which will error) and into the .pc Version: field (line 116). Since the header is untrusted build input, validate that each part is non-empty and numeric before assembling, and fail fast otherwise. (Minor: the parse loop also scans the entire multi-MB header; a break once all three are found avoids needless work.)

osName := runtime.GOOS
osValues := target.require["os"]
if osValues.len > 0 {
osName = osValues[0]
}

includeDir := filepath.join(installDir, "include")
os.mkdirAll(includeDir, 0o755)!
if headerOnly {
cp filepath.join(ctx.SourceDir, "miniaudio.h"), includeDir
lastErr!
} else {
cmakeLists := ctx.Proj.readFile("8bf157f10e278302f8a6c1c9cd1065f2bea26dd2/CMakeLists.txt")!
os.writeFile(filepath.join(ctx.SourceDir, "CMakeLists.txt"), cmakeLists, 0o644)!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] onBuild writes CMakeLists.txt into ctx.SourceDir

The compiled path writes a generated CMakeLists.txt directly into the (potentially shared/cached) source tree. The cglm/json-c recipes keep all generated build inputs in a dedicated scratch dir and never write project files back into SourceDir. Since one fromVer source tree serves many option/os variants, writing into it makes builds order-dependent and can contaminate reuse across variants. Consider writing the generated CMakeLists into the _build scratch dir instead.


c := cmake.new(ctx.SourceDir, filepath.join(ctx.SourceDir, "_build"), installDir)
c.define "MINIAUDIO_SRC_DIR", ctx.SourceDir
c.define "MINIAUDIO_VERSION_STRING", version
c.defineBool "BUILD_SHARED_LIBS", shared
c.defineBool "CMAKE_POSITION_INDEPENDENT_CODE", fPIC
c.configure
c.build
c.install
}

cp "-R", filepath.join(ctx.SourceDir, "extras"), includeDir
lastErr!

licenseDir := filepath.join(installDir, "licenses")
os.mkdirAll(licenseDir, 0o755)!
license := os.readFile(filepath.join(ctx.SourceDir, "LICENSE"))!
os.writeFile(filepath.join(licenseDir, "LICENSE"), license, 0o644)!

libs := ""
if !headerOnly {
libs = "-L$${libdir} -lminiaudio"
}
if osName == "linux" {
libs += " -lm -lpthread -ldl"
} else if osName == "freebsd" {
libs += " -lm -lpthread"
} else if osName == "darwin" {
libs += " -framework CoreFoundation -framework CoreAudio -framework AudioUnit"
}

cflags := "-I$${includedir}"
if shared {
cflags += " -DMA_DLL"
}
if osName == "darwin" {
cflags += " -DMA_NO_RUNTIME_LINKING=1"
}

pcDir := filepath.join(installDir, "lib", "pkgconfig")
os.mkdirAll(pcDir, 0o755)!
pc := `prefix=$${pcfiledir}/../..
exec_prefix=$${prefix}
libdir=$${prefix}/lib
includedir=$${prefix}/include

Name: miniaudio
Description: A single file audio playback and capture library.
Version: ` + version + `
Libs: ` + libs + `
Cflags: ` + cflags + `
`
os.writeFile(filepath.join(pcDir, "miniaudio.pc"), []byte(pc), 0o644)!

pkgconfig.use installDir
ctx.setMetadata pkgconfig.lookup("miniaudio")!
}

onTest ctx => {
installDir := ctx.outputDir
testDir := filepath.join(ctx.SourceDir, "_llar_consumer")
os.mkdirAll(testDir, 0o755)!

consumer := filepath.join(testDir, "consumer.c")
os.writeFile(consumer, []byte(`#define MINIAUDIO_IMPLEMENTATION
#include <miniaudio.h>

#include <stdio.h>

int main(void) {
ma_result result;
ma_context context;
ma_device_info *pPlaybackDeviceInfos;
ma_uint32 playbackDeviceCount;
ma_device_info *pCaptureDeviceInfos;
ma_uint32 captureDeviceCount;

if (ma_context_init(NULL, 0, NULL, &context) != MA_SUCCESS) {
printf("Failed to initialize context.\n");
return -2;
}

result = ma_context_get_devices(&context, &pPlaybackDeviceInfos,
&playbackDeviceCount, &pCaptureDeviceInfos,
&captureDeviceCount);
if (result != MA_SUCCESS) {
printf("Failed to retrieve device information.\n");
return -3;
}

printf("Playback Devices\n");
printf("Capture Devices\n");
ma_context_uninit(&context);
return 0;
}
`), 0o644)!

pkgconfig.use installDir
flags := pkgconfig.lookup("miniaudio")!
flagsFile := filepath.join(testDir, "miniaudio.flags")
os.writeFile(flagsFile, []byte(flags), 0o644)!

binary := filepath.join(testDir, "consumer")
exec "cc", "-std=c99", "@"+flagsFile, consumer, "-o", binary
lastErr!
os.setenv("LD_LIBRARY_PATH", filepath.join(installDir, "lib"))!
os.setenv("DYLD_LIBRARY_PATH", filepath.join(installDir, "lib"))!
Comment on lines +173 to +174

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] LD_LIBRARY_PATH/DYLD_LIBRARY_PATH overwritten instead of prepended

Both setenv calls replace any inherited value rather than prepending the install lib dir to it. If the test binary or toolchain relies on paths already in LD_LIBRARY_PATH (e.g. sibling formula outputs or sandbox runtime libs), they are dropped for the test and it may fail or resolve the wrong library. Prepend to the existing value, e.g. libDir + os.pathListSeparator + os.getenv("LD_LIBRARY_PATH"). These are also only needed for the shared build; note DYLD_LIBRARY_PATH is frequently stripped on macOS under SIP, so the shared-lib test may be fragile there regardless.

exec binary
lastErr!
}
30 changes: 30 additions & 0 deletions mackron/miniaudio/miniaudio_cmp.gox
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// CCI carries older miniaudio releases as commit refs because those releases
// do not have upstream tags. Map every CCI ref and every current upstream tag
// to its release version before comparing the complete visible set.
func normalize(version string) string {
switch version {
case "8bf157f10e278302f8a6c1c9cd1065f2bea26dd2":
return "v0.10.39"
case "37fe1343f04f6fd9bd82229ca50a48b77ecce564":
return "v0.10.40"
case "42abbbea4602af80d1ccb4a22cdc35813aceee7a":
return "v0.11.2"
case "c3a9ab9b900b1ac316f7e2cb5e05e5cc27179f19":
return "v0.11.6"
case "073b7bbbba3a27adcf44fd62bd055ccee67e1973":
return "v0.11.7"
case "82e70f4cbe6e613c8edc0ac7b97ff3dd00f2ca27":
return "v0.11.8"
case "4dfe7c4c31df46e78d9a1cc0d2d6f1aef5a5d58c":
return "v0.11.9"
case "a0dc1037f99a643ff5fad7272cd3d6461f2d63fa":
return "v0.11.11"
case "0.11.15", "0.11.16", "0.11.17", "0.11.18", "0.11.19", "0.11.20", "0.11.21", "0.11.22", "0.11.23", "0.11.24", "0.11.25":
return "v" + version
}
return version
Comment on lines +22 to +25

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] normalize() mis-orders any version not in the hard-coded list

Versions not matched by the switch fall through to return version unchanged (line 25). For a bare numeric tag outside the enumerated 0.11.15..0.11.25 set (e.g. a future 0.11.26) the value has no v prefix, so semver.Compare treats it as invalid and orders it below every normalized version — silently mis-selecting "latest". The recp/cglm/Cglm_cmp.gox comparator avoids this by normalizing via a prefix rule instead of an exhaustive enumeration. Consider a rule-based normalize (add v to bare numeric tags, map only the known untagged commit refs) so new upstream releases sort correctly without editing this file. At minimum, document that new versions must be appended here or they will sort incorrectly.

}

compareVer (a, b) => {
return semver.Compare(normalize(a.Version), normalize(b.Version))
}
4 changes: 4 additions & 0 deletions mackron/miniaudio/versions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"path": "mackron/miniaudio",
"deps": {}
}
Loading