Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for umbrella projects via CLI #2089

Merged
merged 3 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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