Current section
Files
Jump to
Current section
Files
lib/mix/tasks/firebird.target.link.ex
defmodule Mix.Tasks.Firebird.Target.Link do
@moduledoc """
Link multiple WASM modules into a single combined binary.
## Usage
# Link all @wasm modules in lib/
mix firebird.target.link
# Link specific files
mix firebird.target.link --files lib/math.ex,lib/utils.ex
# Link with module name
mix firebird.target.link --name MyApp
# Prefix exports with module name to avoid collisions
mix firebird.target.link --prefix
# Output to custom directory
mix firebird.target.link --output _build/wasm
## Export Naming
By default, all public functions are exported with their original names.
If two modules have functions with the same name, use `--prefix` to
namespace them:
# Without prefix: add, multiply
# With prefix: Math_add, Utils_multiply
"""
use Mix.Task
@shortdoc "Link multiple Elixir modules into a single WASM binary"
@switches [
output: :string,
files: :string,
dirs: :string,
name: :string,
prefix: :boolean,
wat_only: :boolean,
verbose: :boolean,
optimize: :boolean,
tco: :boolean,
inline: :boolean
]
@aliases [
o: :output,
f: :files,
d: :dirs,
n: :name,
v: :verbose
]
@impl Mix.Task
def run(args) do
{opts, _rest, _invalid} = OptionParser.parse(args, switches: @switches, aliases: @aliases)
link(opts)
end
@doc """
Link multiple modules into a single WASM binary.
"""
@spec link(keyword()) :: :ok | {:error, term()}
def link(opts) do
output_dir = Keyword.get(opts, :output, "_build/wasm")
name = Keyword.get(opts, :name, "Combined")
prefix = Keyword.get(opts, :prefix, false)
wat_only = Keyword.get(opts, :wat_only, false)
verbose = Keyword.get(opts, :verbose, false)
# Check prerequisites
unless wat_only do
unless Firebird.Compiler.wat2wasm_available?() do
Mix.raise("wat2wasm not found! Install wabt or use --wat-only")
end
end
# Discover sources
sources = discover_sources(opts)
if Enum.empty?(sources) do
Mix.shell().info("π No WASM sources found to link")
:ok
else
Mix.shell().info("π Firebird WASM Linker")
Mix.shell().info(" Sources: #{length(sources)} file(s)")
Mix.shell().info(" Output: #{output_dir}/#{name}.wasm")
Mix.shell().info(" Prefix: #{prefix}")
Mix.shell().info("")
combiner_opts = [
name: name,
prefix: prefix,
output_dir: output_dir,
wat_only: wat_only,
optimize: Keyword.get(opts, :optimize, false),
tco: Keyword.get(opts, :tco, false),
inline: Keyword.get(opts, :inline, false)
]
case Firebird.Compiler.Combiner.combine(sources, combiner_opts) do
{:ok, result} ->
exports = result[:exports] || []
if verbose do
Mix.shell().info(" Exports:")
Enum.each(exports, fn e -> Mix.shell().info(" β’ #{e}") end)
Mix.shell().info("")
end
Mix.shell().info(" β
Linked #{length(exports)} export(s) into #{name}")
if result.wat do
Mix.shell().info(" π WAT: #{byte_size(result.wat)} bytes")
end
if result.wasm do
Mix.shell().info(" π¦ WASM: #{byte_size(result.wasm)} bytes")
end
:ok
{:error, reason} ->
Mix.shell().error(" β Link failed: #{inspect(reason)}")
{:error, reason}
end
end
end
defp discover_sources(opts) do
files =
case Keyword.get(opts, :files) do
nil -> []
files_str -> String.split(files_str, ",", trim: true)
end
dirs =
case Keyword.get(opts, :dirs) do
nil ->
if Enum.empty?(files) do
["lib"]
else
[]
end
dirs_str ->
String.split(dirs_str, ",", trim: true)
end
explicit_files = Enum.filter(files, &File.regular?/1)
discovered =
dirs
|> Enum.flat_map(fn dir ->
if File.dir?(dir) do
Path.wildcard(Path.join(dir, "**/*.{ex,exs}"))
else
[]
end
end)
|> Enum.filter(&has_wasm_content?/1)
(explicit_files ++ discovered) |> Enum.sort() |> Enum.uniq()
end
defp has_wasm_content?(path) do
case File.read(path) do
{:ok, content} ->
String.contains?(content, "@wasm true") or String.contains?(path, "wasm_modules")
_ ->
false
end
end
end