-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Add Mix.Tasks.Compile.reenable
#13771
base: main
Are you sure you want to change the base?
Add Mix.Tasks.Compile.reenable
#13771
Conversation
Can you please expand a bit more on the use case? Why are you reenabling the compiler? Also, if this is specific to the compiler, I would rather have a task for reenabling the compiler explicitly. The reason I ask is because I can see someone wanting to reenable some compilers but not all of them, and this could be as well a change of behaviour. Also remember that you can always get the list of compilers and individually enable the ones you care about. |
I personally do reenable compiler because I have a task which generates a regular file from the template and then generates a test file based on the information which can be retrieved from the loaded module only. That said, before generating the test, I need the file to be compiled and loaded.
To my understanding, this is not specific to the compiler. I took it as an example, but the real issue is more generic. AFAICT,
This is already possible with an explicit reenabling specific compilers. This PR is not after allowing a fine-tuning over what compilers to execute. It is fully about making
As I hopefully clarified above, I understand this (also the example in the PR comment shows that,) but this PR is not about that. I am after the ability to make it possible to roll back my own task |
Thank you. If the compiler is not your use case, then I would like to hear a more concrete example. The ultimate issue is that the concept of reenable one and reenable all are both equally flawed. If your goal is to make it behave as if they were separate CLI invocations, there is already |
Well, the compiler is indeed my use-case, but I have a habit to read sources if something goes in any unexpected way, which is unfortunately not what 100% of developers would do in the first place. I must have expressed myself poorly, let me try again with another example below.
That is exactly why I didn’t propose gathering this info from the task source code and/or do it in any other automatical fashion. The particular task author would surely know if any other task is to be unlimately re-enabled within their own task.
Well, it would work, but I am trying to introduce a fine tuning. Please see an example below.
My proposed semantics does not imply any rules. It’s totally up to the task author what to reenable within their task. As you mentioned above, Exampledefmodule Mix.Tasks.My.Config do
use Mix.Task
@impl Mix.Task
def run([file]) do
File.write!(file, …)
end
end
defmodule Mix.Tasks.My.Test do
use Mix.Task
@impl Mix.Task
def run(args) do
{test, config} = args |> OptionParser.parse(…) |> reshape()
Mix.Task.run("my.config", [config])
Mix.Task.run("test", [test])
end
end
defmodule Mix.Tasks.My.Tests do
use Mix.Task
@impl Mix.Task
def run(args) do
[{_test, _config} | _] =
test_configs =
args |> OptionParser.parse(…) |> reshape()
Enum.each(test_configs, fn {test, config} ->
# reenabling only this won’t work as expected
# "my.config" won’t be re-enabled
Mix.Task.reenable("my.test")
Mix.Task.run("my.test", ["--test", test, "--config", config])
end)
end
end The code above might be made more user-friendly by defmodule Mix.Tasks.My.Test do
use Mix.Task
@impl Mix.Task
def run(args) do
{test, config} = args |> OptionParser.parse(…) |> reshape()
Mix.Task.run("my.config", [config])
Mix.Task.run("test", [test])
end
+ @impl Mix.Task
+ def depends_on do
+ ["my.config", "test"]
+ end
end This could be especially helpful if both If the above does not convince you either, feel free to close the PR, because it seems I am the only one who entered into the necessity to dig the source code to figure out what to re-enable besides the task I really need to re-enable. |
Yes, this is the concern in my first comment. It depends on the author and I am not sure if the default implementation of Since your ultimate goal is to reenable the compiler, we can expose something |
OK, so I am to change this PR to:
Right? |
Just the second part. Add |
None is already covered, this task is in |
Sorry, none would not include compile.all. compile.all is always included, but you can choose the compilers. And FWIW, the Phoenix compiler does this: https://github.com/phoenixframework/phoenix/blob/main/lib/phoenix/code_reloader/server.ex#L247 |
BTW, maybe |
lib/mix/lib/mix/task.compiler.ex
Outdated
""" | ||
@spec reenable([{:compilers, compilers}]) :: :ok when compilers: :all | [Mix.Task.task_name()] | ||
def reenable(opts \\ []) do | ||
if not Keyword.keyword?(opts) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to check for the arguments.
lib/mix/lib/mix/task.compiler.ex
Outdated
case Keyword.get(opts, :compilers, []) do | ||
:all -> Mix.Tasks.Compile.compilers() | ||
list when is_list(list) -> list | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's go with all by default, because it is easier to customize.
case Keyword.get(opts, :compilers, []) do | |
:all -> Mix.Tasks.Compile.compilers() | |
list when is_list(list) -> list | |
end | |
Keyword.get_lazy(opts, :compilers, &Mix.Tasks.Compile.compilers/0) |
I am also thinking we should move this function to Mix.Tasks.Compile
, since compilers
is already defined there.
lib/mix/lib/mix/task.compiler.ex
Outdated
list when is_list(list) -> list | ||
end | ||
|
||
Enum.each(["loadpaths", "compile", "compile.all"], &Mix.Task.reenable(&1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am unsure if we should enable loadpaths. Thoughts? Why not deps.loadpaths
as well? That's one of the slippery slopes that worry me. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am unsure if we should enable loadpaths.
We should not. It is a leftover from the code when I thought the enabling should be mirrored by a roll-back, but it should not in the latest approach. I cannot think of any change resulting in a need to reload paths from within same external task execution, even if it calls reenable/1
.
lib/mix/lib/mix/task.ex
Outdated
@@ -613,8 +613,11 @@ defmodule Mix.Task do | |||
child projects. | |||
""" | |||
@spec reenable(task_name) :: :ok | |||
def reenable(task) when is_binary(task) or is_atom(task) do | |||
task = to_string(task) | |||
def reenable(task) when is_atom(task) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove the changes to this function. :)
depends_on/0
callback to Task
behaviourMix.Tasks.Compile.reenable
There is one question remaining, which is about protocols consolidation. It is treated as a separate compiler, but it should most likely be reenabled too. There are a few things to consider:
|
Co-authored-by: José Valim <[email protected]>
I believe, this issue has nothing to do with Phoenix per se.
I am to start with changing tests to fail by changing direct calls to
That would be most likely the best resulting code of this PR, but first I want to make sure re-enabling protocols works as expected within current design (separated task for protocols.) |
Yeah. I mean the Phoenix PR is related to this one because Phoenix reenables the compilers, and I would be nice if Phoenix could use this API as well. If it can, it is a good indicator this is general, if not, it may be too specific. But in order to assess this, we need to fix current bugs. |
The plot thickens.
The cleanest approach I could come with, would be to enforce The phoenix code could with this change to be simplified to defp mix_compile(compilers, compile_args, config, consolidation_path) do
compilers = config[:compilers] || Mix.compilers()
protocols = if config[:consolidate_protocols], do: :consolidate
Mix.Task.Compile.reenable(compilers: compilers, protocols: protocols)
# We call build_structure mostly for Windows so new
# assets in priv are copied to the build directory.
Mix.Project.build_structure(config)
args = ["--purge-consolidation-path-if-stale", consolidation_path | compile_args]
result =
with_logger_app(config, fn ->
Mix.Task.run("compile", args)
end)
with :error <- result, do: exit({:shutdown, 1})
end I receive the following error while running the complete test suite, but I am unsure if it’s related because no changed in this PR code could somehow affect the behaviour due to my best assessment:
|
"elixir", acc -> ["protocols", "elixir" | acc] | ||
"app", acc -> ["app", "protocols", "elixir" | acc] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If :elixir
has been given, append :protocols
right after it, prepend both :elixir
and :protocols
to :app
otherwise, so that :app
is still the latest compiler.
I am not sure if this is actually needed.
|
||
assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") != | ||
@old | ||
|
||
# Delete a local protocol | ||
File.rm!("lib/protocol.ex") | ||
assert compile_elixir_and_protocols() == :noop | ||
assert compile_elixir_and_protocols() == :ok |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:ok
here and below is because :protocols
now implies :elixir
invocation which would return {:ok, []}
not {:noop, []}
. Checks for Compile.Protocol.__protocol__(:impls)
now plays the role of real checker.
And this is going to change soon, because protocols will be bundled into the Elixir compiler. Perhaps we should prioritize that work to be on 1.18 and we can merge this with more confidence. Stay tuned! |
Apologies, this will be postponed to v1.19 since I was not able to get the compile.protocols refactor in time for v1.18. |
No issue. Would you like me to try to help with compile.protocols? I understand at least 20% of the issues with it since I dug into this dependency hell when I first came out with this proposal. I might be missing a ton of internals, though, so I would be fine with “your approach sucks” as a result :) |
No need, thanks, since I still need to figure out how exactly it will look like. |
Sometimes the look from the other bench helps. OK, just shout out when you have run out of ideas. |
Mix.Task.reenable/1
does not currently re-enable tasks which were implicitly invoked fromMix.Task.run/1
.For instance, when some task creates the new file within
lib
directory, the following code works for"compile.elixir"
but results in{:noop, []}
for"compile"
.This PR introduces an optional callback
depends_on/0
for bothMix.Task
andMix.Task.Compiler
which is used to recursively re-enable tasks this one depends on.