Merge pull request 'Faust loader: platform-aware library lookup and AQUASYNTH_FAUST_LIBRARY override' (#1) from faust-loader-platform-aware into main

This commit is contained in:
Aqua 2026-10-31 09:40:59 +00:00
commit 3b43b7bd72
3 changed files with 144 additions and 16 deletions

View file

@ -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 `<home>/lib` with
`<home>/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.

View file

@ -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);
/// <summary>Explicit library file: the argument wins over AQUASYNTH_FAUST_LIBRARY. Null when neither is set.</summary>
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.";
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/<file> 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 DLL not found at {dllPath}.";
error = $"Faust share directory not found at {Path.Combine(home, "share", "faust")}; set FaustHome to the prefix that holds share/faust.";
return false;
}
}
else
{
var found = AquaSynthFaustToolchain.Find(preferredHome);
if (found is null)
{
error = $"Faust toolchain not found. Expected {string.Join(" or ", AquaSynthFaustToolchain.LibraryFileNames)} under <home>/lib with <home>/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
{

View file

@ -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);
}
}
}