Skip to content

Commit 313cfc9

Browse files
committed
Merge remote-tracking branch 'origin/master' into time-on-demands
2 parents c943b49 + 9c81d5a commit 313cfc9

19 files changed

Lines changed: 1194 additions & 15 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
tmp/
2+
13
.elixir_ls
24
/priv/plts/*
35
!/priv/plts/.gitkeep

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* Deprecate `:links` option for `:unsafely_name_processes_for_observer` in favour of `:report_links_to_observer` configuration entry
88
* [Set label](https://hexdocs.pm/elixir/Process.html#set_label/1) for all the Membrane processes (i.e. for both Membrane components like pipelines, elements and bins and for utility components like subprocess supervisors)
99
* Update required Elixir version to `~> 1.17`
10+
* Add a Mix task for generating demos [#1067](https://github.com/membraneframework/membrane_core/pull/1067)
11+
* Add Mix tasks for easier initialization of components [#1059](https://github.com/membraneframework/membrane_core/pull/1059)
1012
* Support input demand unit expressed in timestamps [#1079](https://github.com/membraneframework/membrane_core/pull/1079)
1113

1214
## 1.2.6

lib/membrane/bin.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ defmodule Membrane.Bin do
9999
100100
The callback won't be invoked, when you have initiated the pad removal,
101101
eg. when you have returned `t:Membrane.Bin.Action.remove_link()` action
102-
which made one of your children's pads be removed.
103-
By default, it does nothing.
102+
which made one of your children's pads be removed. Not having the
103+
callback implemented when a child removes its pad will result in the
104+
bin crashing.
104105
"""
105106
@callback handle_child_pad_removed(
106107
child :: Child.name(),

lib/membrane/core/options_specs.ex

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@ defmodule Membrane.Core.OptionsSpecs do
77
alias Membrane.Pad
88

99
@spec options_doc() :: String.t()
10-
def options_doc do
10+
def options_doc() do
1111
"""
1212
Options are defined by a keyword list, where each key is an option name and
1313
is described by another keyword list with following fields:
1414
15-
* `spec:` typespec for value in struct
16-
* `default:` default value for option. If not present, value for this option
17-
will have to be provided each time options struct is created
18-
* `inspector:` function converting fields' value to a string. Used when
19-
creating documentation instead of `inspect/1`, eg. `inspector: &Membrane.Time.inspect/1`
20-
* `description:` string describing an option. It will be used for generating the docs
15+
* `spec:` Typespec of the values this option can assume. Will be used in t() type.
16+
* `default:` Default value for option. If not present, value for this option
17+
will have to be provided each time options struct is created.
18+
* `inspector:` Function converting fields' value to a string. Used when
19+
creating documentation instead of `inspect/1`, eg. `inspector: &Membrane.Time.inspect/1`.
20+
* `description:` String describing the option. It will be used for generating the docs.
2121
"""
2222
end
2323

@@ -55,6 +55,21 @@ defmodule Membrane.Core.OptionsSpecs do
5555
end
5656
end
5757

58+
@spec pad_options_doc() :: String.t()
59+
def pad_options_doc() do
60+
"""
61+
Pad options are defined by a keyword list, where each key is an option name and
62+
is described by another keyword list with following fields:
63+
64+
* `spec:` Typespec of the values this option can assume. Will be used in the type definition of pad options for a given pad.
65+
* `default:` Default value for option. If not present, value for this option
66+
will have to be provided each time options the pad is created.
67+
* `inspector:` Function converting fields' value to a string. Used when
68+
creating documentation instead of `inspect/1`, eg. `inspector: &Membrane.Time.inspect/1`.
69+
* `description:` String describing the option. It will be used for generating the docs.
70+
"""
71+
end
72+
5873
@spec def_pad_options(Pad.name(), nil | Keyword.t()) :: {Macro.t(), Macro.t()}
5974
def def_pad_options(_pad_name, []) do
6075
no_code =

lib/membrane/pad.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defmodule Membrane.Pad do
1313
use Bunch
1414

1515
alias Membrane.Buffer
16+
alias Membrane.Core.OptionsSpecs
1617

1718
@availability_values [:always, :on_request]
1819

@@ -120,6 +121,8 @@ defmodule Membrane.Pad do
120121
121122
Demand unit is derived from the first element inside the bin linked to the
122123
given input.
124+
125+
#{OptionsSpecs.pad_options_doc()}
123126
"""
124127
@type bin_spec ::
125128
{name(),
@@ -131,8 +134,7 @@ defmodule Membrane.Pad do
131134
@typedoc """
132135
Describes how a pad should be declared inside an element.
133136
134-
For details on `demand_unit` and how the different units affect demand
135-
behaviour, see the [Manual demands](manual_demands.md) guide.
137+
#{OptionsSpecs.pad_options_doc()}
136138
"""
137139
@type element_spec ::
138140
{name(),

lib/membrane/pipeline.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,9 @@ defmodule Membrane.Pipeline do
143143
144144
The callback won't be invoked, when you have initiated the pad removal,
145145
eg. when you have returned `t:Membrane.Pipeline.Action.remove_link()`
146-
action which made one of your children's pads be removed.
147-
By default, it does nothing.
146+
action which made one of your children's pads be removed. Not having the
147+
callback implemented when a child removes its pad will result in the
148+
pipeline crashing.
148149
"""
149150
@callback handle_child_pad_removed(
150151
child :: Child.name(),

lib/mix/tasks/membrane.demo.ex

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
defmodule Mix.Tasks.Membrane.Demo do
2+
@shortdoc "Download Membrane demos and examples"
3+
@moduledoc """
4+
Download Membrane demos and examples. Requires `git` installed.
5+
6+
$ mix membrane.demo [-a] [-l] [-d <repo_dir>] [<demos> ...]
7+
8+
## Options
9+
* `-l, --list` - List all demos available and their brief descriptions.
10+
* `-a, --all` - Pull the repository with all demos.
11+
* `-d, --directory` - Specify a directory where the demos should be placed in. By default they're placed in the current working directory
12+
"""
13+
use Mix.Task
14+
15+
@switches [
16+
list: :boolean,
17+
all: :boolean,
18+
directory: :string
19+
]
20+
21+
@aliases [
22+
l: :list,
23+
a: :all,
24+
d: :directory
25+
]
26+
27+
@demos_search_list [".", "livebooks"]
28+
29+
@demos_readme_url "https://raw.githubusercontent.com/membraneframework/membrane_demo/refs/heads/master/README.md"
30+
@demos_clone_url "https://github.com/membraneframework/membrane_demo.git"
31+
32+
@impl true
33+
def run([]) do
34+
Mix.Tasks.Help.run(["membrane.demo"])
35+
end
36+
37+
def run(argv) do
38+
{opts, demos_names} = OptionParser.parse!(argv, aliases: @aliases, switches: @switches)
39+
40+
if opts[:list] do
41+
list_available_demos()
42+
end
43+
44+
target_dir = Path.expand(opts[:directory] || ".")
45+
46+
cond do
47+
opts[:all] -> copy_all_demos(target_dir)
48+
demos_names != [] -> copy_specific_demos(target_dir, demos_names)
49+
true -> :ok
50+
end
51+
end
52+
53+
defp list_available_demos() do
54+
{:ok, _apps} = Application.ensure_all_started(:req)
55+
response = Req.get!(@demos_readme_url)
56+
57+
demos_info =
58+
response.body
59+
|> String.split("\n")
60+
|> Enum.map(&Regex.named_captures(~r/^- \[(?<name>.*?)\]\(.*?\) - (?<description>.*)/, &1))
61+
|> Enum.filter(&(&1 != nil))
62+
|> Enum.map(
63+
&Map.update!(
64+
&1,
65+
"description",
66+
fn description -> Regex.replace(~r/\[(.*?)\]\(.*?\)/, description, "\\1") end
67+
)
68+
)
69+
70+
max_name_length =
71+
demos_info
72+
|> Enum.map(&String.length(&1["name"]))
73+
|> Enum.max()
74+
75+
demos_table =
76+
demos_info
77+
|> Enum.map_join("\n", fn %{"name" => name, "description" => description} ->
78+
dots_line = String.duplicate(".", max_name_length - String.length(name) + 3)
79+
" | #{name} #{dots_line} | #{description}"
80+
end)
81+
82+
Mix.shell().info(demos_table)
83+
end
84+
85+
defp copy_all_demos(target_dir) do
86+
repo_dir = Path.join(target_dir, "membrane_demo")
87+
execute_git_command(["clone", "-q", "--depth", "1", @demos_clone_url, repo_dir])
88+
end
89+
90+
defp copy_specific_demos(target_dir, demos_names) do
91+
repo_dir = Path.join(target_dir, "membrane_demo")
92+
execute_git_command(["clone", "-q", "--depth", "1", "-n", @demos_clone_url, repo_dir])
93+
94+
Enum.each(demos_names, fn demo_name ->
95+
case copy_demo(target_dir, demo_name) do
96+
:error ->
97+
Mix.shell().error(
98+
"No demo named #{demo_name}, run this task with -l to list all available demos"
99+
)
100+
101+
:ok ->
102+
Mix.shell().info("`#{demo_name}` demo created at #{Path.join(target_dir, demo_name)}")
103+
end
104+
end)
105+
106+
File.rm_rf!(repo_dir)
107+
end
108+
109+
defp copy_demo(target_dir, demo_name) do
110+
repo_dir = Path.join(target_dir, "membrane_demo")
111+
112+
Enum.reduce_while(@demos_search_list, :error, fn demo_dir, _status ->
113+
demo_path = Path.join([repo_dir, demo_dir, demo_name])
114+
115+
execute_git_command(["sparse-checkout", "set", Path.join(demo_dir, demo_name)], repo_dir)
116+
execute_git_command(["checkout"], repo_dir)
117+
118+
case File.cp_r(demo_path, Path.join(target_dir, Path.basename(demo_path))) do
119+
{:ok, _files} -> {:halt, :ok}
120+
{:error, :enoent, _dir} -> {:cont, :error}
121+
end
122+
end)
123+
end
124+
125+
defp execute_git_command(argv, dir \\ ".") do
126+
case System.cmd("git", argv, stderr_to_stdout: true, cd: dir) do
127+
{_output, 0} ->
128+
:ok
129+
130+
{output, exit_code} ->
131+
Mix.shell().error("""
132+
Git command `git #{Enum.join(argv, " ")} exited with code #{exit_code}. Output:
133+
#{output}
134+
""")
135+
end
136+
end
137+
end
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
[Filter, Endpoint, Sink, Source, Bin, Pipeline]
2+
|> Enum.map(fn component_type ->
3+
component_name = inspect(component_type)
4+
5+
defmodule Module.concat(Mix.Tasks.Membrane.Gen, component_type) do
6+
@shortdoc "Generates a template for a Membrane #{component_name}"
7+
@moduledoc """
8+
Generates a template for a Membrane #{component_name} with the provided module name.
9+
10+
$ mix membrane.gen.#{String.downcase(component_name)} module_name [-l target_location]
11+
12+
## Options
13+
* `-l, --location` - If a target location is provided, the #{component_name} will be created there, relative to the `lib` directory.
14+
The filename must also be present and have an `.ex` extension. If location is not provided, then it will be
15+
inferred from the provided module name - it will be converted to lowercase and `.` separators will be interpreted
16+
as directory separators. For example, a #{component_name} with module name `Foo.Bar` will be created at `lib/foo/bar.ex`.
17+
"""
18+
use Mix.Task
19+
20+
@switches [location: :string]
21+
@aliases [l: :location]
22+
23+
@impl true
24+
def run(argv) do
25+
do_run("lib", argv)
26+
end
27+
28+
@spec do_run(binary(), [binary()]) :: any()
29+
def do_run(base_dir, argv) do
30+
{option, argv} = OptionParser.parse!(argv, aliases: @aliases, strict: @switches)
31+
32+
module_name =
33+
case argv do
34+
[] ->
35+
Mix.raise("""
36+
Module name not provided.
37+
38+
This task expects a module name for the newly created #{unquote(component_type)}:
39+
40+
$ mix membrane.gen.#{String.downcase(unquote(component_name))} My#{unquote(component_name)}
41+
42+
""")
43+
44+
[module_name | _rest] ->
45+
module_name
46+
end
47+
48+
if not valid_module_name?(module_name) do
49+
Mix.raise("""
50+
Invalid module name, please provide a valid one.
51+
(no other special characters than dots are allowed and the module name as well as dot-separated segments in it must start with uppercase letters).
52+
""")
53+
end
54+
55+
component_path =
56+
case option do
57+
[] -> Macro.underscore(module_name) <> ".ex"
58+
[{:location, path} | _rest] -> path
59+
end
60+
|> then(&Path.join(base_dir, &1))
61+
62+
component_path |> Path.dirname() |> File.mkdir_p!()
63+
64+
File.write!(component_path, get_component(module_name))
65+
end
66+
67+
defp get_component(module_name) do
68+
template =
69+
"../../../templates"
70+
|> Path.expand(__DIR__)
71+
|> Path.join(Macro.underscore(unquote(component_type)) <> ".ex")
72+
|> File.read!()
73+
74+
template
75+
|> String.split("\n")
76+
|> List.replace_at(0, "defmodule #{module_name} do")
77+
|> Enum.join("\n")
78+
end
79+
80+
@spec valid_module_name?(String.t()) :: boolean()
81+
defp valid_module_name?(name) do
82+
case Code.string_to_quoted(name) do
83+
{:ok, {:__aliases__, _metadata, _name}} -> true
84+
_other -> false
85+
end
86+
end
87+
end
88+
end)

lib/mix/tasks/membrane.gen.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
defmodule Mix.Tasks.Membrane.Gen do
2+
@shortdoc "Lists all available Membrane component generators"
3+
@moduledoc @shortdoc
4+
use Mix.Task
5+
6+
@impl true
7+
def run(_argv) do
8+
Mix.Tasks.Help.run(["--search", "membrane.gen."])
9+
end
10+
end

mix.exs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ defmodule Membrane.Mixfile do
4545
defp dialyzer() do
4646
opts = [
4747
plt_local_path: "priv/plts",
48-
flags: [:error_handling, :unmatched_returns]
48+
flags: [:error_handling, :unmatched_returns],
49+
plt_add_apps: [:mix, :req]
4950
]
5051

5152
if System.get_env("CI") == "true" do
@@ -238,7 +239,8 @@ defmodule Membrane.Mixfile do
238239
"GitHub" => link(),
239240
"Membrane Framework Homepage" => "https://membrane.stream"
240241
},
241-
files: ~w"lib .formatter.exs mix.exs README* LICENSE* CHANGELOG* #{@hex_packages_path}"
242+
files:
243+
~w"lib .formatter.exs mix.exs README* LICENSE* CHANGELOG* #{@hex_packages_path} templates"
242244
]
243245
end
244246

@@ -253,6 +255,7 @@ defmodule Membrane.Mixfile do
253255
{:makeup_diff, "~> 0.1", only: :dev, runtime: false},
254256
{:dialyxir, "~> 1.1", only: :dev, runtime: false},
255257
{:credo, "~> 1.7", only: :dev, runtime: false},
258+
{:req, "~> 0.5.17", only: [:dev, :test], runtime: false},
256259
# Testing
257260
{:mox, "~> 1.0", only: :test},
258261
{:mock, "~> 0.3.8", only: :test},

0 commit comments

Comments
 (0)