From 53f1f1b03e58bf72cee756a096be380efa06875b Mon Sep 17 00:00:00 2001 From: Aqua Date: Sat, 31 Oct 2026 09:14:08 +0000 Subject: [PATCH] Faust loader: platform-aware library lookup and explicit path override Find libfaust.so on Linux, libfaust.dylib on macOS, libfaust.dll/faust.dll on Windows; join PATH with Path.PathSeparator; accept FaustLibraryPath or AQUASYNTH_FAUST_LIBRARY as an explicit library file. Platform default homes replace the unconditional C:\Program Files\Faust. Co-Authored-By: Claude Sonnet 5.5 --- docs/faust-toolchain-boundary.md | 8 ++ src/AquaSynth.Faust/NativeFaustRuntime.cs | 74 ++++++++++++++---- .../FaustLoaderPlatformTests.cs | 78 +++++++++++++++++++ 3 files changed, 144 insertions(+), 16 deletions(-) create mode 100644 tests/AquaSynth.Dsl.Tests/FaustLoaderPlatformTests.cs diff --git a/docs/faust-toolchain-boundary.md b/docs/faust-toolchain-boundary.md index 7b047c1..2a717a1 100644 --- a/docs/faust-toolchain-boundary.md +++ b/docs/faust-toolchain-boundary.md @@ -83,6 +83,14 @@ returns `AquaSynthCompiledPatch` products with `AquaSynthNativeManifest`. `AquaSynthRenderSession` is only a render-proof convenience wrapper for tests and offline one-shot buffers. +Library lookup is platform-aware: `libfaust.so` on Linux, `libfaust.dylib` on +macOS, `libfaust.dll` (then `faust.dll`) on Windows, always at `/lib` with +`/share/faust` beside it. `AquaSynthNativeOptions.FaustLibraryPath` or +`AQUASYNTH_FAUST_LIBRARY` names the library file directly and overrides the home +search; `FaustHome` then supplies `share/faust` if the prefix is not the file's +grandparent. The loader prepends `lib` and `bin` to `PATH` using the platform +path separator. + ## Invariants - AquaSynth remains the authority for what a patch means. diff --git a/src/AquaSynth.Faust/NativeFaustRuntime.cs b/src/AquaSynth.Faust/NativeFaustRuntime.cs index 84d7d5a..bf707ef 100644 --- a/src/AquaSynth.Faust/NativeFaustRuntime.cs +++ b/src/AquaSynth.Faust/NativeFaustRuntime.cs @@ -11,7 +11,8 @@ public sealed record AquaSynthNativeOptions( string? FaustHome = null, string? DspSourceDirectory = null, float MinRenderSeconds = 0.05f, - float MaxRenderSeconds = 3.0f); + float MaxRenderSeconds = 3.0f, + string? FaustLibraryPath = null); public sealed record AquaSynthCompileIdentity( string Id, @@ -484,7 +485,7 @@ public sealed class AquaSynthNativeCompiler : IDisposable { options ??= new AquaSynthNativeOptions(); compiler = null; - if (!FaustNativeToolchain.TryLoad(options.FaustHome, out var toolchain, out error)) + if (!FaustNativeToolchain.TryLoad(options.FaustHome, options.FaustLibraryPath, out var toolchain, out error)) { return false; } @@ -648,6 +649,13 @@ public sealed class AquaSynthNativeCompiler : IDisposable public static class AquaSynthFaustToolchain { + public const string LibraryOverrideVariable = "AQUASYNTH_FAUST_LIBRARY"; + + public static string[] LibraryFileNames => + OperatingSystem.IsWindows() ? ["libfaust.dll", "faust.dll"] + : OperatingSystem.IsMacOS() ? ["libfaust.dylib"] + : ["libfaust.so"]; + public static string? Find(string? preferredHome = null) { var candidates = new[] @@ -656,7 +664,8 @@ public static class AquaSynthFaustToolchain Environment.GetEnvironmentVariable("AQUASYNTH_FAUST_HOME"), Environment.GetEnvironmentVariable("AQUARIUM_FAUST_HOME"), Path.Combine(AppContext.BaseDirectory, "Tools", "Faust"), - @"C:\Program Files\Faust" + OperatingSystem.IsWindows() ? @"C:\Program Files\Faust" : "/usr/local", + OperatingSystem.IsWindows() ? null : "/usr" }; foreach (var candidate in candidates) @@ -667,7 +676,7 @@ public static class AquaSynthFaustToolchain } var root = Path.GetFullPath(candidate); - if (File.Exists(Path.Combine(root, "lib", "faust.dll")) && + if (FindLibrary(root) is not null && Directory.Exists(Path.Combine(root, "share", "faust"))) { return root; @@ -676,6 +685,20 @@ public static class AquaSynthFaustToolchain return null; } + + public static string? FindLibrary(string home) => + LibraryFileNames + .Select(name => Path.Combine(home, "lib", name)) + .FirstOrDefault(File.Exists); + + /// Explicit library file: the argument wins over AQUASYNTH_FAUST_LIBRARY. Null when neither is set. + public static string? LibraryOverride(string? explicitPath) + { + var path = !string.IsNullOrWhiteSpace(explicitPath) + ? explicitPath + : Environment.GetEnvironmentVariable(LibraryOverrideVariable); + return string.IsNullOrWhiteSpace(path) ? null : Path.GetFullPath(path); + } } internal sealed class FaustNativeToolchain : IDisposable @@ -723,28 +746,47 @@ internal sealed class FaustNativeToolchain : IDisposable public CreateFactoryFromStringFn CreateFactoryFromString { get; } - public static bool TryLoad(string? preferredHome, out FaustNativeToolchain? toolchain, out string? error) + public static bool TryLoad(string? preferredHome, string? libraryPath, out FaustNativeToolchain? toolchain, out string? error) { toolchain = null; error = null; - var home = AquaSynthFaustToolchain.Find(preferredHome); - if (home is null) + var dllPath = AquaSynthFaustToolchain.LibraryOverride(libraryPath); + string home; + if (dllPath is not null) { - error = "Faust toolchain not found. Expected Tools\\Faust beside the app, AQUASYNTH_FAUST_HOME, AQUARIUM_FAUST_HOME, or C:\\Program Files\\Faust."; - return false; - } + if (!File.Exists(dllPath)) + { + error = $"Faust library override not found at {dllPath}."; + return false; + } - var dllPath = Path.Combine(home, "lib", "faust.dll"); - if (!File.Exists(dllPath)) + // lib/ under a prefix; an explicit home still wins for share/faust. + home = !string.IsNullOrWhiteSpace(preferredHome) + ? Path.GetFullPath(preferredHome) + : Path.GetFullPath(Path.Combine(Path.GetDirectoryName(dllPath)!, "..")); + if (!Directory.Exists(Path.Combine(home, "share", "faust"))) + { + error = $"Faust share directory not found at {Path.Combine(home, "share", "faust")}; set FaustHome to the prefix that holds share/faust."; + return false; + } + } + else { - error = $"Faust DLL not found at {dllPath}."; - return false; + var found = AquaSynthFaustToolchain.Find(preferredHome); + if (found is null) + { + error = $"Faust toolchain not found. Expected {string.Join(" or ", AquaSynthFaustToolchain.LibraryFileNames)} under /lib with /share/faust, where home is Tools{Path.DirectorySeparatorChar}Faust beside the app, AQUASYNTH_FAUST_HOME, AQUARIUM_FAUST_HOME, or the platform default; or set {AquaSynthFaustToolchain.LibraryOverrideVariable}."; + return false; + } + + home = found; + dllPath = AquaSynthFaustToolchain.FindLibrary(home)!; } var binPath = Path.Combine(home, "bin"); - var libPath = Path.Combine(home, "lib"); + var libPath = Path.GetDirectoryName(dllPath)!; var path = Environment.GetEnvironmentVariable("PATH") ?? ""; - Environment.SetEnvironmentVariable("PATH", $"{libPath};{binPath};{path}"); + Environment.SetEnvironmentVariable("PATH", string.Join(Path.PathSeparator, [libPath, binPath, path])); try { diff --git a/tests/AquaSynth.Dsl.Tests/FaustLoaderPlatformTests.cs b/tests/AquaSynth.Dsl.Tests/FaustLoaderPlatformTests.cs new file mode 100644 index 0000000..1721898 --- /dev/null +++ b/tests/AquaSynth.Dsl.Tests/FaustLoaderPlatformTests.cs @@ -0,0 +1,78 @@ +namespace AquaSynth.Dsl.Tests; + +public sealed class FaustLoaderPlatformTests +{ + [Fact] + public void LibraryFileNamesFollowThePlatform() + { + var names = AquaSynthFaustToolchain.LibraryFileNames; + if (OperatingSystem.IsWindows()) + { + Assert.Contains("libfaust.dll", names); + } + else if (OperatingSystem.IsLinux()) + { + Assert.Equal(["libfaust.so"], names); + } + } + + [Fact] + public void FindLibraryLooksUnderLibForThePlatformName() + { + var home = Directory.CreateTempSubdirectory("faust-home").FullName; + try + { + Directory.CreateDirectory(Path.Combine(home, "lib")); + Directory.CreateDirectory(Path.Combine(home, "share", "faust")); + Assert.Null(AquaSynthFaustToolchain.FindLibrary(home)); + Assert.Null(AquaSynthFaustToolchain.Find(home)); + + var lib = Path.Combine(home, "lib", AquaSynthFaustToolchain.LibraryFileNames[0]); + File.WriteAllBytes(lib, []); + Assert.Equal(lib, AquaSynthFaustToolchain.FindLibrary(home)); + Assert.Equal(Path.GetFullPath(home), AquaSynthFaustToolchain.Find(home)); + } + finally + { + Directory.Delete(home, recursive: true); + } + } + + [Fact] + public void ExplicitLibraryPathMustExist() + { + var missing = Path.Combine(Path.GetTempPath(), "no-such-dir", AquaSynthFaustToolchain.LibraryFileNames[0]); + var loaded = AquaSynthNativeCompiler.TryLoad( + out var compiler, out var error, new AquaSynthNativeOptions(FaustLibraryPath: missing)); + Assert.False(loaded); + Assert.Null(compiler); + Assert.Contains("override not found", error); + } + + // Real load. The source uses no Faust libraries, so a prefix without share/faust/*.lib still works. + // Set AQUASYNTH_FAUST_LIBRARY to a libfaust file; unset, this is a no-op. + [Fact] + public void ExplicitLibraryPathLoadsRealFaustAndCompiles() + { + var library = Environment.GetEnvironmentVariable(AquaSynthFaustToolchain.LibraryOverrideVariable); + if (string.IsNullOrWhiteSpace(library)) + { + return; + } + + Assert.True( + AquaSynthNativeCompiler.TryLoad(out var compiler, out var error, new AquaSynthNativeOptions(FaustLibraryPath: library)), + error); + using (compiler) + { + Console.WriteLine($"SMOKE library={Path.GetFullPath(library)} faustVersion={compiler!.FaustVersion} home={compiler.FaustHome}"); + Assert.False(string.IsNullOrWhiteSpace(compiler.FaustVersion)); + + var identity = new AquaSynthCompileIdentity("smoke", "smoke", ""); + using var patch = compiler.CompileSource(identity, "smoke", "process = (+(0.0625) ~ _) : sin : *(0.1);", 0.1f); + var samples = patch.Render(); + Console.WriteLine($"SMOKE compiled outputs={patch.Manifest.OutputCount} frames={samples.Length} peak={samples.Max(Math.Abs):F4}"); + Assert.Contains(samples, s => Math.Abs(s) > 0.01f); + } + } +}