Current section
Files
Jump to
Current section
Files
lib/mix/tasks/waveform.doctor.ex
defmodule Mix.Tasks.Waveform.Doctor do
@moduledoc """
Verifies that your system is properly configured to use Waveform.
This task checks for:
- SuperCollider installation (sclang executable)
- SuperCollider server executable (scsynth)
- SuperDirt Quark installation (for pattern-based live coding)
- Environment variable configuration
## Usage
mix waveform.doctor
"""
@shortdoc "Verifies system requirements for Waveform"
use Mix.Task
@impl Mix.Task
def run(_args) do
Mix.shell().info("Checking Waveform system requirements...\n")
checks = [
check_sclang(),
check_sclang_executable(),
check_scsynth(),
check_superdirt(),
check_env_vars()
]
failed = Enum.count(checks, &(&1 == :error))
passed = Enum.count(checks, &(&1 == :ok))
Mix.shell().info("\n" <> String.duplicate("=", 60))
if failed == 0 do
Mix.shell().info([:green, "✓ All checks passed (#{passed}/#{passed})", :reset])
Mix.shell().info("\nYour system is ready to use Waveform!")
else
Mix.shell().error("✗ #{failed} check(s) failed, #{passed} passed")
Mix.shell().info("\nPlease address the issues above before using Waveform.")
Mix.shell().info("See: https://github.com/rpmessner/waveform#prerequisites")
end
end
defp check_sclang do
Mix.shell().info("Checking for sclang...")
sclang_path = get_sclang_path()
cond do
File.exists?(sclang_path) ->
Mix.shell().info([:green, " ✓ Found sclang at: #{sclang_path}", :reset])
:ok
sclang_in_path?() ->
path = System.find_executable("sclang")
Mix.shell().info([:green, " ✓ Found sclang in PATH at: #{path}", :reset])
:ok
true ->
Mix.shell().error(" ✗ sclang not found at: #{sclang_path}")
print_installation_instructions()
:error
end
end
defp check_sclang_executable do
Mix.shell().info("Checking if sclang is executable...")
sclang_path = get_sclang_path()
path = if File.exists?(sclang_path), do: sclang_path, else: System.find_executable("sclang")
cond do
is_nil(path) or not File.exists?(path) ->
Mix.shell().info([:yellow, " ⊘ Skipping (sclang not found)", :reset])
:ok
not readable?(path) ->
Mix.shell().error(" ✗ sclang is not executable")
Mix.shell().info(" Run: chmod +x #{path}")
:error
true ->
check_sclang_version(path)
end
end
defp readable?(path) do
stat = File.stat!(path)
stat.access in [:read_write, :read]
end
defp check_sclang_version(path) do
case System.cmd(path, ["-v"], stderr_to_stdout: true) do
{output, 0} ->
report_version(output)
{output, _} ->
# Some versions of sclang return non-zero with -v
if String.contains?(output, "SuperCollider") do
report_version(output)
else
Mix.shell().error(" ✗ sclang exists but failed to execute")
Mix.shell().info(" Output: #{String.trim(output)}")
:error
end
end
end
defp report_version(output) do
version = extract_version(output)
Mix.shell().info([
:green,
" ✓ sclang is executable (version: #{version})",
:reset
])
:ok
end
defp check_scsynth do
Mix.shell().info("Checking for scsynth (SuperCollider server)...")
# Try to find scsynth in common locations
scsynth_paths = default_scsynth_paths() ++ [System.find_executable("scsynth")]
scsynth_path = Enum.find(scsynth_paths, &(&1 && File.exists?(&1)))
if scsynth_path do
Mix.shell().info([:green, " ✓ Found scsynth at: #{scsynth_path}", :reset])
:ok
else
Mix.shell().error(" ✗ scsynth not found")
Mix.shell().info(" This is usually installed with SuperCollider")
:error
end
end
defp check_superdirt do
Mix.shell().info("Checking for SuperDirt Quark...")
sclang_path = get_sclang_path()
path = if File.exists?(sclang_path), do: sclang_path, else: System.find_executable("sclang")
if is_nil(path) or not File.exists?(path) do
Mix.shell().info([:yellow, " ⊘ Skipping (sclang not found)", :reset])
:ok
else
check_superdirt_installed(path)
end
end
defp check_superdirt_installed(sclang_path) do
# Run sclang with a command to check if SuperDirt class exists
# We use -e to evaluate code and quit immediately
command =
"SuperDirt.class.notNil.if({ \"SUPERDIRT_INSTALLED\".postln }, { \"SUPERDIRT_NOT_INSTALLED\".postln }); 0.exit;"
case System.cmd(sclang_path, ["-e", command], stderr_to_stdout: true) do
{output, _} ->
cond do
String.contains?(output, "SUPERDIRT_INSTALLED") ->
Mix.shell().info([:green, " ✓ SuperDirt Quark is installed", :reset])
:ok
String.contains?(output, "SUPERDIRT_NOT_INSTALLED") ->
Mix.shell().error(" ✗ SuperDirt Quark is not installed")
print_superdirt_installation_instructions()
:error
true ->
# Couldn't determine - maybe old SC version or other issue
Mix.shell().info([
:yellow,
" ⊘ Could not verify SuperDirt (SC may be too old or not responding)",
:reset
])
:ok
end
end
rescue
_ ->
Mix.shell().info([:yellow, " ⊘ Could not verify SuperDirt installation", :reset])
:ok
end
defp check_env_vars do
Mix.shell().info("Checking environment variables...")
if System.get_env("SCLANG_PATH") do
custom_path = System.get_env("SCLANG_PATH")
Mix.shell().info([:green, " ✓ SCLANG_PATH is set to: #{custom_path}", :reset])
:ok
else
Mix.shell().info([:yellow, " ⊘ SCLANG_PATH not set (using default location)", :reset])
:ok
end
end
defp get_sclang_path do
System.get_env("SCLANG_PATH") || default_sclang_path()
end
defp default_sclang_path do
case :os.type() do
{:unix, :darwin} ->
"/Applications/SuperCollider.app/Contents/MacOS/sclang"
{:unix, _} ->
# Linux: try common installation paths
Enum.find(
["/usr/bin/sclang", "/usr/local/bin/sclang"],
&File.exists?/1
) || "/usr/bin/sclang"
{:win32, _} ->
# Windows: try common installation paths
Enum.find(
[
"C:\\Program Files\\SuperCollider\\sclang.exe",
"C:\\Program Files (x86)\\SuperCollider\\sclang.exe"
],
&File.exists?/1
) || "C:\\Program Files\\SuperCollider\\sclang.exe"
end
end
defp default_scsynth_paths do
case :os.type() do
{:unix, :darwin} ->
["/Applications/SuperCollider.app/Contents/Resources/scsynth"]
{:unix, _} ->
# Linux: try common installation paths
["/usr/bin/scsynth", "/usr/local/bin/scsynth"]
{:win32, _} ->
# Windows: try common installation paths
[
"C:\\Program Files\\SuperCollider\\scsynth.exe",
"C:\\Program Files (x86)\\SuperCollider\\scsynth.exe"
]
end
end
defp sclang_in_path? do
System.find_executable("sclang") != nil
end
defp extract_version(output) do
case Regex.run(~r/Version (\d+\.\d+\.\d+[^\s]*)/, output) do
[_, version] -> version
_ -> "unknown"
end
end
defp print_installation_instructions do
Mix.shell().info("\n Installation instructions:\n")
Mix.shell().info(" macOS:")
Mix.shell().info(" brew install supercollider\n")
Mix.shell().info(" Linux (Debian/Ubuntu):")
Mix.shell().info(" sudo apt-get install supercollider\n")
Mix.shell().info(" Linux (Arch):")
Mix.shell().info(" sudo pacman -S supercollider\n")
Mix.shell().info(" Windows:")
Mix.shell().info(" Download from https://supercollider.github.io/\n")
Mix.shell().info(" Custom location:")
Mix.shell().info(" export SCLANG_PATH=/path/to/sclang")
end
defp print_superdirt_installation_instructions do
Mix.shell().info("\n SuperDirt installation instructions:\n")
Mix.shell().info(" 1. Open SuperCollider IDE (or run sclang)")
Mix.shell().info(" 2. Install SuperDirt Quark:")
Mix.shell().info(" Quarks.install(\"SuperDirt\");\n")
Mix.shell().info(" 3. Recompile the class library:")
Mix.shell().info(" thisProcess.recompile;\n")
Mix.shell().info(" 4. (Optional) Add to startup file for automatic loading:")
Mix.shell().info(" SuperDirt.start;\n")
Mix.shell().info(" For more info:")
Mix.shell().info(" https://github.com/musikinformatik/SuperDirt")
end
end