defmodule Mix.Tasks.Membrane.Demo do @shortdoc "Download Membrane demos and examples" @moduledoc """ Download Membrane demos and examples. Requires `git` installed. $ mix membrane.demo [-a] [-l] [-d ] [ ...] ## Options * `-l, --list` - List all demos available and their brief descriptions. * `-a, --all` - Pull the repository with all demos. * `-d, --directory` - Specify a directory where the demos should be placed in. By default they're placed in the current working directory """ use Mix.Task @switches [ list: :boolean, all: :boolean, directory: :string ] @aliases [ l: :list, a: :all, d: :directory ] @demos_readme_url "https://raw.githubusercontent.com/membraneframework/membrane_demo/refs/heads/master/README.md" @demos_clone_url "https://github.com/membraneframework/membrane_demo.git" @impl true def run([]) do Mix.Tasks.Help.run(["membrane.demo"]) end def run(argv) do {opts, demos_names} = OptionParser.parse!(argv, aliases: @aliases, switches: @switches) if opts[:list] do list_available_demos() end target_dir = Path.expand(opts[:directory] || ".") cond do opts[:all] -> copy_all_demos(target_dir) demos_names != [] -> copy_specific_demos(target_dir, demos_names) true -> :ok end end defp list_available_demos() do {:ok, _apps} = Application.ensure_all_started([:inets, :ssl]) case :httpc.request(:get, {~c"#{@demos_readme_url}", []}, [{:autoredirect, true}], []) do {:ok, {{_version, 200, _reason}, _headers, body}} -> fetch_demos_info(body) {:ok, {{_version, status, reason}, _headers, _body}} -> Mix.raise("Failed to fetch demos list: HTTP #{status} #{reason}") {:error, reason} -> Mix.raise("Failed to fetch demos list: #{inspect(reason)}") end end defp fetch_demos_info(body) do demos_info = List.to_string(body) |> String.split("\n") |> Enum.map(&Regex.named_captures(~r/^- \[(?.*?)\]\(.*?\) - (?.*)/, &1)) |> Enum.filter(&(&1 != nil)) |> Enum.map( &Map.update!( &1, "description", fn description -> Regex.replace(~r/\[(.*?)\]\(.*?\)/, description, "\\1") end ) ) max_name_length = demos_info |> Enum.map(&String.length(&1["name"])) |> Enum.max() demos_table = demos_info |> Enum.map_join("\n", fn %{"name" => name, "description" => description} -> dots_line = String.duplicate(".", max_name_length - String.length(name) + 3) " | #{name} #{dots_line} | #{description}" end) Mix.shell().info(demos_table) end defp copy_all_demos(target_dir) do repo_path = Path.join(target_dir, "membrane_demo") execute_git_command(["clone", "-q", "--depth", "1", @demos_clone_url, repo_path]) end defp copy_specific_demos(target_dir, demos_names) do repo_path = Path.join(target_dir, "membrane_demo") execute_git_command(["clone", "-q", "--depth", "1", "-n", @demos_clone_url, repo_path]) Enum.each(demos_names, fn demo_name -> case copy_demo(target_dir, demo_name) do :error -> Mix.shell().error( "No demo named #{demo_name}, run this task with -l to list all available demos" ) :ok -> Mix.shell().info("`#{demo_name}` demo created at #{Path.join(target_dir, demo_name)}") end end) File.rm_rf!(repo_path) end defp copy_demo(target_dir, demo_name) do repo_path = Path.join(target_dir, "membrane_demo") demo_path = Path.join(repo_path, demo_name) execute_git_command(["sparse-checkout", "set", demo_name], repo_path) execute_git_command(["checkout"], repo_path) case File.cp_r(demo_path, Path.join(target_dir, demo_name)) do {:ok, _files} -> :ok {:error, :enoent, _dir} -> :error end end defp execute_git_command(argv, dir \\ ".") do case System.cmd("git", argv, stderr_to_stdout: true, cd: dir) do {_output, 0} -> :ok {output, exit_code} -> Mix.shell().error(""" Git command `git #{Enum.join(argv, " ")} exited with code #{exit_code}. Output: #{output} """) end end end