defmodule Mix.Tasks.Firebird.New do @moduledoc """ Creates a new Elixir project with Firebird WASM integration pre-configured. Similar to `mix new` but with Firebird already set up. ## Usage mix firebird.new my_app mix firebird.new my_app --rust # Include Rust WASM scaffold mix firebird.new my_app --sup # With supervision tree ## Options - `--rust` - Include a Rust WASM scaffold in `native/` - `--sup` - Generate with supervision tree (OTP app) - `--module` - Module name (default: inferred from app name) """ use Mix.Task @shortdoc "Create a new project with Firebird WASM integration" @impl Mix.Task def run(args) do {opts, positional, _} = OptionParser.parse(args, switches: [rust: :boolean, sup: :boolean, module: :string], aliases: [r: :rust, s: :sup, m: :module] ) app_name = case positional do [name | _] -> name [] -> Mix.raise("Usage: mix firebird.new APP_NAME [--rust] [--sup]") end module_name = opts[:module] || Macro.camelize(app_name) app_path = app_name if File.exists?(app_path) do Mix.raise("Directory #{app_path} already exists!") end Mix.shell().info("🔥 Creating Firebird project #{app_name}...") File.mkdir_p!(app_path) File.mkdir_p!(Path.join(app_path, "lib/#{app_name}")) File.mkdir_p!(Path.join(app_path, "test")) File.mkdir_p!(Path.join(app_path, "wasm")) write_mix_exs(app_path, app_name, module_name, opts) write_lib(app_path, app_name, module_name, opts) write_test(app_path, app_name, module_name) write_config(app_path) write_gitignore(app_path) write_readme(app_path, app_name, module_name) if opts[:rust] do write_rust_scaffold(app_path) end Mix.shell().info(""" 🔥 Your Firebird project is ready! cd #{app_name} mix deps.get #{if opts[:rust], do: " # Build the Rust WASM module:\n cd native && ./build.sh && cd ..\n", else: ""} Then try it in iex: iex -S mix iex> Firebird.check!() # Verify setup iex> Firebird.demo() # See it in action iex> wasm = Firebird.playground() # Get a pre-loaded instance Or call WASM functions directly: iex> {:ok, [8]} = Firebird.run(Firebird.sample_wasm_path(), :add, [5, 3]) Happy WASM coding! 🚀 """) end defp write_mix_exs(path, app_name, module_name, opts) do sup_line = if opts[:sup], do: ",\n mod: {#{module_name}.Application, []}", else: "" content = """ defmodule #{module_name}.MixProject do use Mix.Project def project do [ app: :#{app_name}, version: "0.1.0", elixir: "~> 1.14", start_permanent: Mix.env() == :prod, deps: deps() ] end def application do [ extra_applications: [:logger]#{sup_line} ] end defp deps do [ {:firebird, "~> 0.1.0"} ] end end """ File.write!(Path.join(path, "mix.exs"), content) Mix.shell().info(" Created #{path}/mix.exs") end defp write_lib(path, app_name, module_name, opts) do # Main module main = """ defmodule #{module_name} do @moduledoc \"\"\" #{module_name} - powered by Firebird WASM. \"\"\" end """ File.write!(Path.join(path, "lib/#{app_name}.ex"), main) # Application module (if --sup) if opts[:sup] do app = """ defmodule #{module_name}.Application do use Application @impl true def start(_type, _args) do children = [ # Add your WASM modules here: # #{module_name}.Math, # {Firebird.Pool, wasm: "wasm/math.wasm", size: 4, name: :math_pool} ] opts = [strategy: :one_for_one, name: #{module_name}.Supervisor] Supervisor.start_link(children, opts) end end """ File.write!(Path.join(path, "lib/#{app_name}/application.ex"), app) end # Example WASM module wrapper example = """ defmodule #{module_name}.Math do @moduledoc \"\"\" Example WASM math module. Replace "wasm/math.wasm" with your actual WASM file. ## Usage {:ok, [8]} = #{module_name}.Math.add(5, 3) [55] = #{module_name}.Math.fibonacci!(10) \"\"\" use Firebird, wasm: "wasm/math.wasm" wasm_fn :add, args: 2, doc: "Add two integers" wasm_fn :multiply, args: 2, doc: "Multiply two integers" wasm_fn :fibonacci, args: 1, doc: "Calculate fibonacci number" end """ File.write!(Path.join(path, "lib/#{app_name}/math.ex"), example) Mix.shell().info(" Created lib/ files") end defp write_test(path, app_name, module_name) do helper = """ ExUnit.start() """ File.write!(Path.join(path, "test/test_helper.exs"), helper) test = """ defmodule #{module_name}.MathTest do use ExUnit.Case # To run these tests, place a math.wasm in wasm/ # with add/2, multiply/2, and fibonacci/1 exports. @tag :skip test "add" do {:ok, pid} = #{module_name}.Math.start_link() assert {:ok, [8]} = #{module_name}.Math.add(5, 3) #{module_name}.Math.stop() end end """ File.write!(Path.join(path, "test/#{app_name}_test.exs"), test) Mix.shell().info(" Created test/ files") end defp write_config(path) do File.mkdir_p!(Path.join(path, "config")) config = """ import Config # Firebird configuration (optional - sensible defaults are used) # config :firebird, # wasm_path: "wasm", # memory_limit: 67_108_864, # timeout: 5_000 """ File.write!(Path.join(path, "config/config.exs"), config) end defp write_gitignore(path) do content = """ /_build/ /cover/ /deps/ /doc/ /.fetch erl_crash.dump *.ez #{Path.basename(path)}-*.tar /tmp/ /native/target/ """ File.write!(Path.join(path, ".gitignore"), content) end defp write_readme(path, _app_name, module_name) do content = """ # #{module_name} A WASM-powered Elixir project using [Firebird](https://github.com/hdresearch/firebird). ## Setup ```bash mix deps.get ``` ## Usage Place your `.wasm` files in the `wasm/` directory, then: ```elixir # One-shot {:ok, [8]} = Firebird.run("wasm/math.wasm", :add, [5, 3]) # Or use the module wrapper {:ok, [8]} = #{module_name}.Math.add(5, 3) ``` ## Building WASM Modules ### Rust ```bash cd native ./build.sh ``` ### Any language Any language that compiles to WebAssembly will work. Just place the `.wasm` file in `wasm/` and call it with Firebird. """ File.write!(Path.join(path, "README.md"), content) end defp write_rust_scaffold(path) do native_path = Path.join(path, "native") File.mkdir_p!(Path.join(native_path, "src")) cargo = """ [package] name = "wasm_native" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [profile.release] opt-level = "z" lto = true strip = true """ File.write!(Path.join(native_path, "Cargo.toml"), cargo) lib_rs = ~S""" #[no_mangle] pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b } #[no_mangle] pub extern "C" fn multiply(a: i32, b: i32) -> i32 { a * b } #[no_mangle] pub extern "C" fn fibonacci(n: i32) -> i32 { if n <= 1 { return n; } let (mut a, mut b) = (0i32, 1i32); for _ in 2..=n { let tmp = b; b = a + b; a = tmp; } b } """ File.write!(Path.join(native_path, "src/lib.rs"), lib_rs) build_sh = """ #!/bin/bash set -e cd "$(dirname "$0")" cargo build --target wasm32-unknown-unknown --release mkdir -p ../wasm cp target/wasm32-unknown-unknown/release/wasm_native.wasm ../wasm/ echo "✅ Built wasm/wasm_native.wasm" """ build_path = Path.join(native_path, "build.sh") File.write!(build_path, build_sh) File.chmod!(build_path, 0o755) Mix.shell().info(" Created native/ Rust scaffold") end end