Skip to content

Commit

Permalink
Support for umbrella projects via CLI (#2089)
Browse files Browse the repository at this point in the history
  • Loading branch information
robertoaloi authored Feb 6, 2025
1 parent 4bc56b9 commit 2986c5d
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 24 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ You can use ExDoc via the command line.
GITHUB_REPO => ecto
```

It is also possible to specify multiple `ebin` directories in the case of _umbrella_ projects:

```bash
$ ex_doc "PROJECT_NAME" "PROJECT_VERSION" _build/dev/lib/app1/ebin _build/dev/lib/app2/ebin -m "PROJECT_MODULE" -u "https://github.com/GITHUB_USER/GITHUB_REPO" -l path/to/logo.png
```

If multiple `ebin` directories are specified, modules are grouped by application by default. It is possible to override this behaviour by providing a custom `groups_per_modules` option.

You can specify a config file via the `--config` option, both Elixir and Erlang formats are supported. Invoke `ex_doc` without arguments to learn more.

<!-- tabs-close -->
Expand Down
16 changes: 5 additions & 11 deletions lib/ex_doc/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ defmodule ExDoc.CLI do
end

defp generate(args, opts, generator) do
[project, version, source_beam] = parse_args(args)
[project, version | source_beams] = parse_args(args)

Code.prepend_path(source_beam)
Code.prepend_paths(source_beams)

for path <- Keyword.get_values(opts, :paths),
path <- Path.wildcard(path) do
Expand All @@ -80,8 +80,8 @@ defmodule ExDoc.CLI do

opts =
opts
|> Keyword.put(:source_beam, source_beam)
|> Keyword.put(:apps, [app(source_beam)])
|> Keyword.put(:source_beam, source_beams)
|> Keyword.put(:apps, Enum.map(source_beams, &app/1))
|> merge_config()
|> normalize_formatters()

Expand Down Expand Up @@ -166,13 +166,7 @@ defmodule ExDoc.CLI do
end
end

defp parse_args([_project, _version, _source_beam] = args), do: args

defp parse_args([_, _, _ | _]) do
IO.puts("Too many arguments.\n")
print_usage()
exit({:shutdown, 1})
end
defp parse_args([_project, _version | _source_beams] = args), do: args

defp parse_args(_) do
IO.puts("Too few arguments.\n")
Expand Down
16 changes: 15 additions & 1 deletion lib/ex_doc/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ defmodule ExDoc.Config do

{groups_for_docs, options} = Keyword.pop(options, :groups_for_docs, [])
{groups_for_extras, options} = Keyword.pop(options, :groups_for_extras, [])
{groups_for_modules, options} = Keyword.pop(options, :groups_for_modules, [])
apps = Keyword.get(options, :apps, [])

{groups_for_modules, options} =
Keyword.pop(options, :groups_for_modules, default_groups_for_modules(apps))

{skip_undefined_reference_warnings_on, options} =
Keyword.pop(
Expand Down Expand Up @@ -278,4 +281,15 @@ defmodule ExDoc.Config do
defp append_slash(url) do
if :binary.last(url) == ?/, do: url, else: url <> "/"
end

defp default_groups_for_modules([_app]) do
[]
end

defp default_groups_for_modules(apps) do
Enum.map(apps, fn app ->
Application.load(app)
{app, Application.spec(app, :modules)}
end)
end
end
29 changes: 17 additions & 12 deletions test/ex_doc/cli_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule ExDoc.CLITest do
import ExUnit.CaptureIO

@ebin "_build/test/lib/ex_doc/ebin"
@ebin2 "_build/test/lib/makeup/ebin"

defp run(args) do
with_io(fn -> ExDoc.CLI.main(args, &{&1, &2, &3}) end)
Expand All @@ -17,7 +18,7 @@ defmodule ExDoc.CLITest do
formatter: "html",
formatters: ["html", "epub"],
apps: [:ex_doc],
source_beam: @ebin
source_beam: [@ebin]
]}

assert epub ==
Expand All @@ -26,7 +27,7 @@ defmodule ExDoc.CLITest do
formatter: "epub",
formatters: ["html", "epub"],
apps: [:ex_doc],
source_beam: @ebin
source_beam: [@ebin]
]}
end

Expand All @@ -39,7 +40,7 @@ defmodule ExDoc.CLITest do
formatter: "epub",
formatters: ["epub", "html"],
apps: [:ex_doc],
source_beam: @ebin
source_beam: [@ebin]
]}

assert html ==
Expand All @@ -48,7 +49,7 @@ defmodule ExDoc.CLITest do
formatter: "html",
formatters: ["epub", "html"],
apps: [:ex_doc],
source_beam: @ebin
source_beam: [@ebin]
]}
end

Expand All @@ -60,14 +61,18 @@ defmodule ExDoc.CLITest do
assert io == "ExDoc v#{ExDoc.version()}\n"
end

test "too many arguments" do
assert catch_exit(run(["ExDoc", "1.2.3", "/", "kaboom"])) == {:shutdown, 1}
end

test "too few arguments" do
assert catch_exit(run(["ExDoc"])) == {:shutdown, 1}
end

test "multiple apps" do
{[{"ExDoc", "1.2.3", html}, {"ExDoc", "1.2.3", epub}], _io} =
run(["ExDoc", "1.2.3", @ebin, @ebin2])

assert [:ex_doc, :makeup] = Enum.sort(Keyword.get(html, :apps))
assert [:ex_doc, :makeup] = Enum.sort(Keyword.get(epub, :apps))
end

test "arguments that are not aliased" do
File.write!("not_aliased.exs", ~s([key: "val"]))

Expand Down Expand Up @@ -98,7 +103,7 @@ defmodule ExDoc.CLITest do
logo: "logo.png",
main: "Main",
output: "html",
source_beam: "#{@ebin}",
source_beam: ["#{@ebin}"],
source_ref: "abcdefg",
source_url: "http://example.com/username/project"
]
Expand Down Expand Up @@ -127,7 +132,7 @@ defmodule ExDoc.CLITest do
extras: ["README.md"],
formatter: "html",
formatters: ["html"],
source_beam: @ebin
source_beam: [@ebin]
]
after
File.rm!("test.exs")
Expand Down Expand Up @@ -155,7 +160,7 @@ defmodule ExDoc.CLITest do
formatter: "html",
formatters: ["html"],
logo: "opts_logo.png",
source_beam: @ebin
source_beam: [@ebin]
]
after
File.rm!("test.exs")
Expand Down Expand Up @@ -192,7 +197,7 @@ defmodule ExDoc.CLITest do
extras: ["README.md"],
formatter: "html",
formatters: ["html"],
source_beam: @ebin
source_beam: [@ebin]
]
after
File.rm!("test.config")
Expand Down
38 changes: 38 additions & 0 deletions test/ex_doc/config_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,43 @@ defmodule ExDoc.ConfigTest do
config = build(source_url_pattern: "a%{line}b%{path}c")
assert config.source_url_pattern.("foo.ex", 123) == "a123bfoo.exc"
end

test "groups_for_modules" do
# Using real applications, since we load them to extract the corresponding list of modules
stdlib = :stdlib
kernel = :kernel
custom_group = :custom_group

groups_for_modules = fn config, key ->
List.keyfind(config.groups_for_modules, to_string(key), 0)
end

# Single app, no custom grouping
config = build(apps: [stdlib])
assert groups_for_modules.(config, stdlib) == nil
assert groups_for_modules.(config, custom_group) == nil

# Single app, custom grouping
config = build(apps: [stdlib], groups_for_modules: [{"custom_group", ["module_1"]}])
assert groups_for_modules.(config, stdlib) == nil
assert groups_for_modules.(config, custom_group) == {"custom_group", ["module_1"]}

# Multiple apps, no custom grouping
config = build(apps: [stdlib, kernel])
stdlib_groups = groups_for_modules.(config, stdlib)
kernel_groups = groups_for_modules.(config, kernel)
assert match?({"stdlib", _}, stdlib_groups)
assert match?({"kernel", _}, kernel_groups)
{"stdlib", stdlib_modules} = stdlib_groups
{"kernel", kernel_modules} = kernel_groups
assert Enum.member?(stdlib_modules, :gen_server)
assert Enum.member?(kernel_modules, :file)

# Multiple apps, custom grouping
config = build(apps: [stdlib, kernel], groups_for_modules: [{"custom_group", ["module_1"]}])
assert groups_for_modules.(config, stdlib) == nil
assert groups_for_modules.(config, kernel) == nil
assert groups_for_modules.(config, custom_group) == {"custom_group", ["module_1"]}
end
end
end

0 comments on commit 2986c5d

Please sign in to comment.