Skip to content

Commit 11ae3cd

Browse files
committed
Move recompile-free extraction to its own gettext.extract_from_attributes task
Following Jose's review on PR #437, split the attribute-based extraction out of the --from-attributes flag into a dedicated mix gettext.extract_from_attributes task. The two extraction modes are now separate code paths that share only the POT merge/fuse and output steps via Mix.Tasks.Gettext.Extract.process/3. Also drop the incremental_compile/0 reenable dance. The new task does a plain mix compile (a no-op when the project is already compiled), since by the time it reads the persisted attributes the project is already compiled and up to date.
1 parent ee9f341 commit 11ae3cd

4 files changed

Lines changed: 122 additions & 91 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
* Parallelize per-locale merging in `mix gettext.merge`.
66

7-
* Add experimental `--from-attributes` flag to `mix gettext.extract`: messages
8-
are persisted as module attributes during normal compilation (for backends
9-
with `automatic_extraction: true` in the application environment, for example
7+
* Add experimental `mix gettext.extract_from_attributes` task: messages are
8+
persisted as module attributes during normal compilation (for backends with
9+
`automatic_extraction: true` in the application environment, for example
1010
`config :gettext, MyApp.Gettext, automatic_extraction: true` in
1111
`config/dev.exs`) and read back from the compiled BEAM files, so extraction
1212
no longer needs to force-recompile the project.

lib/mix/tasks/gettext.extract.ex

Lines changed: 13 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -57,43 +57,30 @@ defmodule Mix.Tasks.Gettext.Extract do
5757
5858
## Extraction Without Recompiling (Experimental)
5959
60-
By default, this task extracts messages by **force-recompiling** the whole
61-
project, because extraction happens during the expansion of the Gettext
62-
macros. As an experimental alternative, the `--from-attributes` flag reads
63-
messages back from the compiled BEAM files instead:
64-
65-
```bash
66-
mix gettext.extract --from-attributes
67-
```
68-
69-
For this to work, messages must have been persisted as module attributes
70-
during normal compilation. This happens automatically when the backend has
71-
automatic extraction enabled in the application environment, which you
72-
typically set in `config/dev.exs` so it stays off in `:prod`:
73-
74-
# config/dev.exs
75-
config :gettext, MyApp.Gettext, automatic_extraction: true
76-
77-
With this flag, the task only runs a normal **incremental** compilation
78-
(changed files are recompiled and get fresh attributes; unchanged BEAM
79-
files already carry theirs), then scans the compiled BEAM files. Since the
80-
attributes are only persisted when `automatic_extraction` is enabled (so not
81-
in `:prod`), release artifacts are unaffected.
82-
83-
`--from-attributes` can be combined with `--merge` and `--check-up-to-date`.
60+
This task always **force-recompiles** the whole project, because extraction
61+
happens during the expansion of the Gettext macros. If you would rather
62+
extract without a force-recompile, see `mix gettext.extract_from_attributes`,
63+
which reads the messages back from the compiled BEAM files instead.
8464
8565
"""
8666

87-
@switches [merge: :boolean, check_up_to_date: :boolean, from_attributes: :boolean]
67+
@switches [merge: :boolean, check_up_to_date: :boolean]
8868

8969
@impl true
9070
def run(args) do
9171
Application.ensure_all_started(:gettext)
9272
_ = Mix.Project.get!()
9373
mix_config = Mix.Project.config()
9474
{opts, _} = OptionParser.parse!(args, switches: @switches)
95-
pot_files = extract(mix_config[:app], mix_config[:gettext] || [], opts)
75+
pot_files = extract_via_recompilation(mix_config[:app], mix_config[:gettext] || [])
76+
process(pot_files, opts, args)
77+
end
9678

79+
# Shared by `mix gettext.extract` and `mix gettext.extract_from_attributes`:
80+
# both compute `pot_files` (their only difference) and then write, check, or
81+
# merge them in exactly the same way.
82+
@doc false
83+
def process(pot_files, opts, args) do
9784
if opts[:check_up_to_date] do
9885
run_up_to_date_check(pot_files)
9986
else
@@ -130,14 +117,6 @@ defmodule Mix.Tasks.Gettext.Extract do
130117
end
131118
end
132119

133-
defp extract(app, gettext_config, opts) do
134-
if opts[:from_attributes] do
135-
extract_from_attributes(app, gettext_config)
136-
else
137-
extract_via_recompilation(app, gettext_config)
138-
end
139-
end
140-
141120
defp extract_via_recompilation(app, gettext_config) do
142121
Gettext.Extractor.enable()
143122
force_compile()
@@ -146,48 +125,6 @@ defmodule Mix.Tasks.Gettext.Extract do
146125
Gettext.Extractor.disable()
147126
end
148127

149-
defp extract_from_attributes(app, gettext_config) do
150-
incremental_compile()
151-
152-
{backends, messages} =
153-
Gettext.Extractor.fill_from_compiled_beams(Mix.Project.compile_path())
154-
155-
if backends == 0 and messages == 0 do
156-
Mix.raise("""
157-
mix gettext.extract --from-attributes found no persisted Gettext messages \
158-
or backends in #{Path.relative_to_cwd(Mix.Project.compile_path())}.
159-
160-
Messages are persisted to module attributes during normal compilation only \
161-
when the backend has automatic extraction enabled in the application \
162-
environment, for example in config/dev.exs:
163-
164-
config :gettext, MyApp.Gettext, automatic_extraction: true
165-
166-
If you just enabled this or updated Gettext, force a recompile so that \
167-
up-to-date modules get their attributes written:
168-
169-
mix compile --force
170-
""")
171-
end
172-
173-
Gettext.Extractor.pot_files(app, gettext_config)
174-
end
175-
176-
defp incremental_compile do
177-
# A plain incremental compile, with one wrinkle: "compile" and the
178-
# compilers it runs may already have been invoked in this VM (for
179-
# example, through a task alias), in which case running them again would
180-
# be a no-op unless they are reenabled first. The trailing explicit
181-
# "compile.elixir" run is a no-op when "compile" just ran it, and covers
182-
# the case where a custom "compile" alias does not.
183-
Mix.Task.reenable("compile")
184-
Mix.Task.reenable("compile.all")
185-
Mix.Task.reenable("compile.elixir")
186-
Mix.Task.reenable("compile.app")
187-
Mix.Task.run("compile", [])
188-
Mix.Task.run("compile.elixir", [])
189-
end
190-
191128
defp force_compile do
192129
# For old Elixir versions, we have to clean the manifest,
193130
# otherwise we are forced to compile all dependencies.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
defmodule Mix.Tasks.Gettext.ExtractFromAttributes do
2+
use Mix.Task
3+
@recursive true
4+
5+
@shortdoc "Extracts messages from compiled BEAM files without recompiling"
6+
7+
@moduledoc """
8+
Extracts messages without force-recompiling the project (experimental).
9+
10+
```bash
11+
mix gettext.extract_from_attributes [OPTIONS]
12+
```
13+
14+
Unlike `mix gettext.extract`, which force-recompiles the whole project so
15+
that the Gettext macros run again, this task reads the messages back from
16+
the compiled BEAM files. It compiles the project normally (a no-op when it
17+
is already compiled) and then scans the persisted module attributes, so it
18+
avoids the cost of a force-recompile.
19+
20+
For this to work, messages must have been persisted as module attributes
21+
during normal compilation. This happens automatically when the backend has
22+
automatic extraction enabled in the application environment, which you
23+
typically set in `config/dev.exs` so it stays off in `:prod`:
24+
25+
# config/dev.exs
26+
config :gettext, MyApp.Gettext, automatic_extraction: true
27+
28+
Since the attributes are only persisted when `automatic_extraction` is
29+
enabled (so not in `:prod`), release artifacts are unaffected.
30+
31+
This task accepts the same `--merge` and `--check-up-to-date` options as
32+
`mix gettext.extract`, and forwards any other options to
33+
`Mix.Tasks.Gettext.Merge`:
34+
35+
```bash
36+
mix gettext.extract_from_attributes --merge --no-fuzzy
37+
mix gettext.extract_from_attributes --check-up-to-date
38+
```
39+
40+
"""
41+
42+
@switches [merge: :boolean, check_up_to_date: :boolean]
43+
44+
@impl true
45+
def run(args) do
46+
Application.ensure_all_started(:gettext)
47+
_ = Mix.Project.get!()
48+
mix_config = Mix.Project.config()
49+
{opts, _} = OptionParser.parse!(args, switches: @switches)
50+
pot_files = extract(mix_config[:app], mix_config[:gettext] || [])
51+
Mix.Tasks.Gettext.Extract.process(pot_files, opts, args)
52+
end
53+
54+
defp extract(app, gettext_config) do
55+
# The project is compiled (and its attributes persisted) by the normal
56+
# compilation; here we just make sure that has happened. This is a no-op
57+
# when the project is already compiled.
58+
Mix.Task.run("compile", [])
59+
60+
{backends, messages} =
61+
Gettext.Extractor.fill_from_compiled_beams(Mix.Project.compile_path())
62+
63+
if backends == 0 and messages == 0 do
64+
Mix.raise("""
65+
mix gettext.extract_from_attributes found no persisted Gettext messages \
66+
or backends in #{Path.relative_to_cwd(Mix.Project.compile_path())}.
67+
68+
Messages are persisted to module attributes during normal compilation only \
69+
when the backend has automatic extraction enabled in the application \
70+
environment, for example in config/dev.exs:
71+
72+
config :gettext, MyApp.Gettext, automatic_extraction: true
73+
74+
If you just enabled this or updated Gettext, force a recompile so that \
75+
up-to-date modules get their attributes written:
76+
77+
mix compile --force
78+
""")
79+
end
80+
81+
Gettext.Extractor.pot_files(app, gettext_config)
82+
end
83+
end

test/mix/tasks/gettext.extract_from_attributes_test.exs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ defmodule Mix.Tasks.Gettext.ExtractFromAttributesTest do
124124
# POT files written by the recompilation path unchanged.
125125
output =
126126
capture_io(fn ->
127-
in_project(test, tmp_dir, fn _module -> run(["--from-attributes"]) end)
127+
in_project(test, tmp_dir, fn _module -> run_from_attributes() end)
128128
end)
129129

130130
refute output =~ "Extracted"
@@ -137,7 +137,7 @@ defmodule Mix.Tasks.Gettext.ExtractFromAttributesTest do
137137

138138
output =
139139
capture_io(fn ->
140-
in_project(test, tmp_dir, fn _module -> run(["--from-attributes"]) end)
140+
in_project(test, tmp_dir, fn _module -> run_from_attributes() end)
141141
end)
142142

143143
assert output =~ "Extracted priv/gettext/default.pot"
@@ -184,7 +184,7 @@ defmodule Mix.Tasks.Gettext.ExtractFromAttributesTest do
184184
""")
185185

186186
capture_io(fn ->
187-
in_project(test, tmp_dir, fn _module -> run(["--from-attributes"]) end)
187+
in_project(test, tmp_dir, fn _module -> run_from_attributes() end)
188188
end)
189189

190190
pot = read_file(context, "priv/gettext/default.pot")
@@ -215,7 +215,7 @@ defmodule Mix.Tasks.Gettext.ExtractFromAttributesTest do
215215
""")
216216

217217
capture_io(fn ->
218-
in_project(test, tmp_dir, fn _module -> run(["--from-attributes"]) end)
218+
in_project(test, tmp_dir, fn _module -> run_from_attributes() end)
219219
end)
220220

221221
default_pot = read_file(context, "priv/gettext/default.pot")
@@ -248,7 +248,7 @@ defmodule Mix.Tasks.Gettext.ExtractFromAttributesTest do
248248
""")
249249

250250
capture_io(fn ->
251-
in_project(test, tmp_dir, fn _module -> run(["--from-attributes"]) end)
251+
in_project(test, tmp_dir, fn _module -> run_from_attributes() end)
252252
end)
253253

254254
pot = read_file(context, "priv/gettext/default.pot")
@@ -267,7 +267,7 @@ defmodule Mix.Tasks.Gettext.ExtractFromAttributesTest do
267267
write_file(context, "priv/gettext/it/LC_MESSAGES/default.po", "")
268268

269269
capture_io(fn ->
270-
in_project(test, tmp_dir, fn _module -> run(["--from-attributes", "--merge"]) end)
270+
in_project(test, tmp_dir, fn _module -> run_from_attributes(["--merge"]) end)
271271
end)
272272

273273
po = read_file(context, "priv/gettext/it/LC_MESSAGES/default.po")
@@ -291,7 +291,7 @@ defmodule Mix.Tasks.Gettext.ExtractFromAttributesTest do
291291
""")
292292

293293
capture_io(fn ->
294-
in_project(test, tmp_dir, fn _module -> run(["--from-attributes"]) end)
294+
in_project(test, tmp_dir, fn _module -> run_from_attributes() end)
295295
end)
296296

297297
assert read_file(context, "priv/custom_gettext/default.pot") =~
@@ -307,11 +307,11 @@ defmodule Mix.Tasks.Gettext.ExtractFromAttributesTest do
307307

308308
capture_io(fn ->
309309
in_project(test, tmp_dir, fn _module ->
310-
run(["--from-attributes"])
310+
run_from_attributes()
311311
end)
312312

313313
in_project(test, tmp_dir, fn _module ->
314-
run(["--from-attributes", "--check-up-to-date"])
314+
run_from_attributes(["--check-up-to-date"])
315315
end)
316316
end)
317317

@@ -325,7 +325,7 @@ defmodule Mix.Tasks.Gettext.ExtractFromAttributesTest do
325325
capture_io(fn ->
326326
assert_raise Mix.Error, expected_error, fn ->
327327
in_project(test, tmp_dir, fn _module ->
328-
run(["--from-attributes", "--check-up-to-date"])
328+
run_from_attributes(["--check-up-to-date"])
329329
end)
330330
end
331331
end)
@@ -344,7 +344,7 @@ defmodule Mix.Tasks.Gettext.ExtractFromAttributesTest do
344344
capture_io(fn ->
345345
assert_raise Mix.Error, ~r/found no persisted Gettext messages/, fn ->
346346
in_project(test, tmp_dir, fn _module ->
347-
run(["--from-attributes"])
347+
run_from_attributes()
348348
end)
349349
end
350350
end)
@@ -415,7 +415,7 @@ defmodule Mix.Tasks.Gettext.ExtractFromAttributesTest do
415415
in_project(test, tmp_dir, fn _module ->
416416
# Mix.Task.run (not a direct module call) so that @recursive true
417417
# recurses into each umbrella app.
418-
Mix.Task.run("gettext.extract", ["--from-attributes"])
418+
Mix.Task.run("gettext.extract_from_attributes", [])
419419
end)
420420
end)
421421

@@ -440,4 +440,15 @@ defmodule Mix.Tasks.Gettext.ExtractFromAttributesTest do
440440
defp run(args) do
441441
Mix.Tasks.Gettext.Extract.run(args)
442442
end
443+
444+
defp run_from_attributes(args \\ []) do
445+
# Reenable compile so each invocation picks up source changes, mirroring a
446+
# fresh `mix` invocation (the task itself does a plain compile, not a
447+
# force-recompile).
448+
for task <- ~w(compile compile.all compile.elixir compile.app) do
449+
Mix.Task.reenable(task)
450+
end
451+
452+
Mix.Tasks.Gettext.ExtractFromAttributes.run(args)
453+
end
443454
end

0 commit comments

Comments
 (0)