Compare commits

...
21 Commits
Author SHA1 Message Date
José Valim 47abe2d107 Release v1.17.2 2024-07-06 23:10:25 +02:00
José Valim e254e7e68e Fix regression when fetching repo with a ref
Also note that depth and ref cannot be used in combination.
2024-07-02 20:09:54 +02:00
Wojtek Mach d3c0b38c5d Improvements to generated rel/bin/app.bat (#13679)
* Validate value of `!RELEASE_MODE!`

  * Exit with `ERRORLEVEL` set to `1` on errors.

    Previously we did not and so apps that use releases and rely on exit
    codes to report errors were not doing so (e.g. Livebook).

  * Rename rpc node name from `rem-(...)` to `rpc-(...)`

  * Update generated `rel/env.bat.exs`
2024-07-02 15:21:38 +02:00
José Valim 662dd6b8d9 Add CHANGELOG entry on jaro_distance, closes #13696 2024-06-27 09:14:53 +02:00
José Valim cadd501cbd Clarify cookie generation, closes #13694 2024-06-26 13:28:57 +02:00
Alexey Nikitin ddf84aba7e Fix vm.args.eex generated file when remote? is false (#13688)
before
```
-start_epmd false -erl_epmd_port 6789false
```

after
```
-start_epmd false -erl_epmd_port 6789
```
2024-06-24 09:47:21 +02:00
Jean Klingler 2d808ec652 Fix logger crash when :gen_statem format_status/2 returns non-tuple (#13684) 2024-06-22 20:14:56 +09:00
José Valim 6bfb95ab88 Release v1.17.1 2024-06-18 13:11:00 +02:00
José Valim d53977002d Print type violation slogan 2024-06-18 12:46:59 +02:00
José Valim 776e5a34ea Do not flip async/sync modules in --repeat-until-failure 2024-06-15 18:00:29 +02:00
Jean Klingler ae5707f90a Fix bug when using pinned variable in with else block (#13667) 2024-06-15 18:49:37 +09:00
José Valim 67617e5d8c Do not warn for assignment with blocks in EEx 2024-06-15 11:05:45 +02:00
José Valim 6f5fc94276 Clarify docs, closes #13666 2024-06-15 10:20:40 +02:00
José Valim 6c935ca6ec Clarify charlists intro, closes #13663 2024-06-14 17:34:27 +02:00
José Valim d1076e487c Do not run fixpoint computations on runtime deps
If A depends on B and B depends on C, all as runtime
dependencies, and C changes, there is no change that
can happen in C that will change the result of running
the checker on A.

This could change in the future in two ways:

1. If we introduce inference of return types, then we
   need to infer and check the types for A again. This
   may be an argument for not introducing inference of
   return types (only of patterns and guards)

2. We will need to track "type exports" once we introduce
   type aliases
2024-06-14 12:07:02 +02:00
Jonatan Kłosko b9e447757d Update RELEASE_DISTRIBUTION docs (#13658) 2024-06-13 16:15:45 +02:00
Jean Klingler b7943e113d Fix dialyzer error when with else clause is calling a no_return function (#13659)
Close #13656
2024-06-13 16:15:32 +02:00
Ben Murden 1a320bb60d Minor grammatical fixes on warning message (#13654) 2024-06-13 16:15:25 +02:00
Ian Harris 64e63b1b9d Update compatibility-and-deprecations.md (#13657)
Fixed link for changelog to v1.17.
2024-06-12 19:12:55 +02:00
José Valim 5a8b28427c Update CHANGELOG 2024-06-12 15:20:57 +02:00
José Valim a1eb59d035 Remove /main link from CHANGELOG 2024-06-12 13:54:10 +02:00
27 changed files with 282 additions and 161 deletions
+39 -4
View File
@@ -22,8 +22,6 @@ We focused on atoms and maps on this initial release as they are respectively th
* Accessing a key on a map or a struct that does not have the given key, such as `user.adress`
* Updating a struct or a map that does not define the given key, such as `%{user | adress: ...}`
* Invoking a function on non-modules, such as `user.address()`
* Capturing a function on non-modules, such as `&user.address/0`
@@ -40,7 +38,7 @@ We focused on atoms and maps on this initial release as they are respectively th
* Accessing a field that is not defined in a rescued exception
These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind, however, that the Elixir typechecker only infers types from patterns within the same function at the moment. Analysis from guards and across function boundaries will be added in future relases. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/main/gradual-set-theoretic-types.html).
These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind, however, that the Elixir typechecker only infers types from patterns within the same function at the moment. Analysis from guards and across function boundaries will be added in future relases. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/gradual-set-theoretic-types.html).
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
@@ -72,6 +70,43 @@ Finally, a new `Kernel.to_timeout/1` function has been added, which helps develo
Process.send_after(pid, :wake_up, to_timeout(hour: 1))
```
## v1.17.2 (2024-07-06)
### 1. Bug fixes
#### Logger
* [Logger.Translator] Fix logger crash when `:gen_statem`'s `format_status/2` returns non-tuple
#### Mix
* [mix deps.get] Fix regression when fetching a git repository with a `:ref`
* [mix release] Validate `RELEASE_MODE` and set ERRORLEVEL on `.bat` scripts
* [mix release] Fix invalid example in code comment inside the generated vm.args.eex
## v1.17.1 (2024-06-18)
### 1. Enhancements
#### Mix
* [mix compile.elixir] Do not run fixpoint computation on runtime dependencies. This should considerably improve compilation times for large projects when changing only one or two files
### 2. Bug fixes
#### EEx
* [EEx] Do not warn for assignment with blocks in EEx
#### Elixir
* [Kernel] Fix bug when using pinned variables inside `with`'s `else` patterns
* [Kernel] Fix Dialyzer error when with else clause is calling a `no_return` function
#### ExUnit
* [ExUnit] Do not alternative sync/async suites on `--repeat-until-failure`
## v1.17.0 (2024-06-12)
### 1. Enhancements
@@ -126,7 +161,6 @@ Process.send_after(pid, :wake_up, to_timeout(hour: 1))
* [mix test] Add `mix test --slowest-modules` to print slowest modules based on all of the tests they hold
* [mix test] Generate cover HTML files in parallel
### 2. Bug fixes
#### Elixir
@@ -141,6 +175,7 @@ Process.send_after(pid, :wake_up, to_timeout(hour: 1))
* [Kernel] Avoid double tracing events when capturing a function
* [Kernel] Fix a bug where captured arguments would conflict when a capture included a macro that also used captures
* [Module] Return default value in `Module.get_attribute/3` for persisted attributes which have not yet been written to
* [String] Properly handle transpositions in `jaro_distance`. This will correct the distance result in certain cases
#### IEx
+1 -1
View File
@@ -1 +1 @@
1.17.0
1.17.2
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/sh
set -e
ELIXIR_VERSION=1.17.0
ELIXIR_VERSION=1.17.2
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
+1 -1
View File
@@ -1,6 +1,6 @@
@echo off
set ELIXIR_VERSION=1.17.0
set ELIXIR_VERSION=1.17.2
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+7 -7
View File
@@ -340,13 +340,6 @@ defmodule EEx.Compiler do
scope,
state
) do
if mark == ~c"" do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end
{rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars}
start_line = meta.line
start_column = column(meta.column, mark)
@@ -359,6 +352,13 @@ defmodule EEx.Compiler do
%{state | quoted: [], line: line}
)
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
generate_buffer(rest, buffer, scope, state)
end
-9
View File
@@ -43,15 +43,6 @@ defmodule EEx.SmartEngineTest do
assert_received :found
end
test "error with unused \"do\" block without \"<%=\" modifier" do
stderr =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
assert_eval("", "<% if true do %>I'm invisible!<% end %>", assigns: %{})
end)
assert stderr =~ "the contents of this expression won't be output"
end
defp assert_eval(expected, actual, binding \\ []) do
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
assert result == expected
+10
View File
@@ -543,6 +543,16 @@ defmodule EExTest do
~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"]
end
test "unused \"do\" block without \"<%=\" modifier" do
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
EEx.compile_string("<% if true do %>I'm invisible!<% end %>")
end) =~ "the contents of this expression won't be output"
# These are fine though
EEx.compile_string("<% foo = fn -> %>Hello<% end %>")
EEx.compile_string("<% foo = if true do %>Hello<% end %>")
end
test "from tokenizer" do
warning =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
+2 -2
View File
@@ -678,8 +678,8 @@ defmodule Module.Types.Of do
Comparison operators (>, <, >=, <=, min, and max) perform structural \
and not semantic comparison. Comparing with a struct won't give meaningful \
results. Struct that can be compared typically define a compare/2 function \
within their modules that can be used for semantic comparison
results. Structs that can be compared typically define a compare/2 function \
within their modules that can be used for semantic comparison.
"""
])
}
@@ -9,7 +9,9 @@ iex> is_binary(string)
true
```
In this chapter, we will gain clarity on what exactly binaries are, how they relate to strings, and what single-quoted values, `'like this'`, mean in Elixir. Although strings are one of the most common data types in computer languages, they are subtly complex and are often misunderstood. To understand strings in Elixir, we have to educate ourselves about [Unicode](https://en.wikipedia.org/wiki/Unicode) and character encodings, specifically the [UTF-8](https://en.wikipedia.org/wiki/UTF-8) encoding.
In this chapter, we will gain clarity on what exactly binaries are and how they relate to strings. We will also learn about charlists, `~c"like this"`, which are often used for interoperability with Erlang.
Although strings are one of the most common data types in computer languages, they are subtly complex and are often misunderstood. To understand strings in Elixir, let's first discuss [Unicode](https://en.wikipedia.org/wiki/Unicode) and character encodings, specifically the [UTF-8](https://en.wikipedia.org/wiki/UTF-8) encoding.
## Unicode and Code Points
@@ -213,4 +213,4 @@ Version | Deprecated feature | Replaced by (ava
[v1.14]: https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md#4-hard-deprecations
[v1.15]: https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md#4-hard-deprecations
[v1.16]: https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md#4-hard-deprecations
[v1.17]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations
[v1.17]: https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md#4-hard-deprecations
+4 -4
View File
@@ -425,16 +425,16 @@ translate_with_else(Meta, [{'else', Else}], S) ->
RaiseClause = {'->', Generated, [[RaiseVar], RaiseExpr]},
Clauses = elixir_erl_clauses:get_clauses('else', [{'else', Else ++ [RaiseClause]}], match),
{TranslatedClauses, SC} = elixir_erl_clauses:clauses(Clauses, SV),
with_else_closure(Meta, TranslatedClauses, SC).
{TranslatedClauses, SC} = elixir_erl_clauses:clauses(Clauses, SV#elixir_erl{extra=pin_guard}),
with_else_closure(Meta, TranslatedClauses, SC#elixir_erl{extra=SV#elixir_erl.extra}).
with_else_closure(Meta, TranslatedClauses, S) ->
Ann = ?ann(Meta),
{_, FunErlVar, SC} = elixir_erl_var:assign(Meta, S),
{_, ArgErlVar, SA} = elixir_erl_var:assign(Meta, SC),
FunAssign = {match, Ann, FunErlVar, {'fun', Ann, {clauses, TranslatedClauses}}},
FunCall = {call, Ann, FunErlVar, [ArgErlVar]},
Generated = erl_anno:set_generated(true, Ann),
FunAssign = {match, Ann, FunErlVar, {'fun', Generated, {clauses, TranslatedClauses}}},
FunCall = {call, Ann, FunErlVar, [ArgErlVar]},
{{clause, Generated, [ArgErlVar], [], [FunCall]}, FunAssign, SA}.
translate_with_do([{'<-', Meta, [{Var, _, Ctx} = Left, Expr]} | Rest], Ann, Do, Else, S) when is_atom(Var), is_atom(Ctx) ->
+1 -1
View File
@@ -177,7 +177,7 @@ format_snippet(Severity, Position, File, Message, Snippet, Diagnostic) ->
unicode:characters_to_binary(Formatted).
format_detail(#{detail := #{typing_traces := _}}, Message) -> [Message | "\ntyping violation found at:"];
format_detail(#{details := #{typing_traces := _}}, Message) -> [Message | "\ntyping violation found at:"];
format_detail(_, Message) -> Message.
calculate_span_length({StartLine, StartCol}, #{span := {StartLine, EndCol}}) -> EndCol - StartCol;
@@ -0,0 +1,13 @@
defmodule Dialyzer.WithNoReturn do
def with_no_return(list) do
no_return = fn -> throw(:no_return) end
with [] <- list do
:ok
else
# note: throwing here directly wouldn't be caught in the first place,
# calling a no_return function is what could cause an issue.
_ -> no_return.()
end
end
end
@@ -159,6 +159,11 @@ defmodule Kernel.DialyzerTest do
assert_dialyze_no_warnings!(context)
end
test "no warnings on with when else has a no_return type", context do
copy_beam!(context, Dialyzer.WithNoReturn)
assert_dialyze_no_warnings!(context)
end
test "no warnings on defmacrop", context do
copy_beam!(context, Dialyzer.Defmacrop)
assert_dialyze_no_warnings!(context)
@@ -25,6 +25,30 @@ defmodule Kernel.WithTest do
assert with({^key, res} <- ok(42), do: res) == 42
end
test "pin matching with multiple else" do
key = :error
first_else =
with nil <- error() do
:ok
else
^key -> :pinned
_other -> :other
end
assert first_else == :pinned
second_else =
with nil <- ok(42) do
:ok
else
^key -> :pinned
_other -> :other
end
assert second_else == :other
end
test "two levels with" do
result =
with {:ok, n1} <- ok(11),
@@ -382,7 +382,7 @@ defmodule Module.Types.ExprTest do
# from: types_test.ex:LINE-2
y = %Point{}
Comparison operators (>, <, >=, <=, min, and max) perform structural and not semantic comparison. Comparing with a struct won't give meaningful results. Struct that can be compared typically define a compare/2 function within their modules that can be used for semantic comparison
Comparison operators (>, <, >=, <=, min, and max) perform structural and not semantic comparison. Comparing with a struct won't give meaningful results. Structs that can be compared typically define a compare/2 function within their modules that can be used for semantic comparison.
"""}
end
end
+1 -1
View File
@@ -500,7 +500,7 @@ defmodule ExUnit do
defp maybe_repeated_run(options, seed, load_us, repeat) do
case ExUnit.Runner.run(options, load_us) do
{%{failures: 0}, {sync_modules, async_modules}}
{%{failures: 0}, {async_modules, sync_modules}}
when repeat > 0 and (sync_modules != [] or async_modules != []) ->
ExUnit.Server.restore_modules(async_modules, sync_modules)
+7 -9
View File
@@ -535,15 +535,13 @@ defmodule ExUnit.Callbacks do
test, as simply shutting down the process would cause it to be restarted
according to its `:restart` value.
Another advantage is that the test process will act as both an ancestor
as well as a caller to the supervised processes. When a process is started
under a supervision tree, it typically populates the `$ancestors` key in
its process dictionary with all of its ancestors, which will include the test
process. Additionally, `start_supervised/2` will also store the test process
in the `$callers` key of the started process, allowing tools that perform
either ancestor or caller tracking to reach the test process. You can learn
more about these keys in
[the `Task` module](`Task#module-ancestor-and-caller-tracking`).
Finally, since Elixir v1.17.0, the test supervisor has both `$ancestors`
and `$callers` key in its process dictionary pointing to the test process.
This means developers can invoke `Process.get(:"$callers", [])` in their
`start_link` function and forward it to the spawned process, which may set
`Process.put(:"$callers", callers)` during its initialization. This may be
useful in projects who track process ownership during tests. You can learn
more about these keys in [the `Task` module](`Task#module-ancestor-and-caller-tracking`).
"""
@doc since: "1.5.0"
@spec start_supervised(Supervisor.child_spec() | module | {module, term}, keyword) ::
+1 -3
View File
@@ -298,7 +298,7 @@ defmodule Logger.Translator do
client_info: client,
name: name,
reason: {kind, reason, stack},
state: {state, data},
state: state,
queue: queue,
postponed: postponed,
callback_mode: callback_mode,
@@ -328,8 +328,6 @@ defmodule Logger.Translator do
msg,
"\nState: ",
inspect(state, inspect_opts),
"\nData: ",
inspect(data, inspect_opts),
"\nCallback mode: ",
"#{inspect(callback_mode, inspect_opts)}, state_enter: #{state_enter?}"
| format_client_info(client)
+56 -10
View File
@@ -100,6 +100,28 @@ defmodule Logger.TranslatorTest do
end
end
defmodule MyGenStatemHandleEvent do
@behaviour :gen_statem
@impl true
def callback_mode, do: :handle_event_function
@impl true
def init(state) do
{:ok, :no_state, state}
end
@impl true
def handle_event({:call, _}, :error, :no_state, _data) do
raise "oops"
end
@impl :gen_statem
def format_status(_opts, [_pdict, _, state]) do
state
end
end
defmodule MyBridge do
@behaviour :supervisor_bridge
@@ -393,7 +415,7 @@ defmodule Logger.TranslatorTest do
assert {%RuntimeError{message: "oops"}, [_ | _]} = process_metadata[:crash_reason]
refute Map.has_key?(gen_statem_metadata, :initial_call)
assert process_metadata[:initial_call] == {Logger.TranslatorTest.MyGenStatem, :init, 1}
assert process_metadata[:initial_call] == {MyGenStatem, :init, 1}
refute Map.has_key?(gen_statem_metadata, :registered_name)
refute Map.has_key?(process_metadata, :registered_name)
@@ -445,7 +467,7 @@ defmodule Logger.TranslatorTest do
assert capture_log(:debug, fn ->
catch_exit(:gen_statem.call(pid, :error))
end) =~ """
[:ok, :ok, :ok, ...]
State: {:started, [:ok, ...]}
"""
after
Application.put_env(:logger, :translator_inspect_opts, [])
@@ -462,8 +484,7 @@ defmodule Logger.TranslatorTest do
.*
Queue: .*
Postponed: \[\]
State: :started
Data: :ok
State: {:started, :ok}
Callback mode: :state_functions, state_enter: false
Client #PID<\d+\.\d+\.\d+> is alive
.*
@@ -488,8 +509,7 @@ defmodule Logger.TranslatorTest do
.*
Queue: .*
Postponed: \[\]
State: :started
Data: :ok
State: {:started, :ok}
Callback mode: :state_functions, state_enter: false
Client :named_client is alive
.*
@@ -513,8 +533,7 @@ defmodule Logger.TranslatorTest do
.*
Queue: .*
Postponed: \[\]
State: :started
Data: :ok
State: {:started, :ok}
Callback mode: :state_functions, state_enter: false
Client #PID<\d+\.\d+\.\d+> is dead
"""s
@@ -533,8 +552,7 @@ defmodule Logger.TranslatorTest do
.*
Queue: .*
Postponed: \[\]
State: :started
Data: :ok
State: {:started, :ok}
Callback mode: :state_functions, state_enter: false
"""s
end
@@ -552,6 +570,34 @@ defmodule Logger.TranslatorTest do
assert_receive {:event, {:string, ["Process " | _]}, _process_metadata}
end
test "translates :gen_statem crashes when callback_mode is :handle_event_function" do
{:ok, pid} = :gen_statem.start(MyGenStatemHandleEvent, :ok, [])
assert capture_log(:debug, fn ->
catch_exit(:gen_statem.call(pid, :error))
end) =~ ~r"""
\[error\] :gen_statem #PID<\d+\.\d+\.\d+> terminating
\*\* \(RuntimeError\) oops
.*
Queue: .*
Postponed: \[\]
State: :ok
Callback mode: .*, state_enter: false
"""s
assert_receive {:event, {:string, [[":gen_statem " <> _ | _] | _]}, gen_statem_metadata}
assert_receive {:event, {:string, ["Process " | _]}, process_metadata}
assert {%RuntimeError{message: "oops"}, [_ | _]} = gen_statem_metadata[:crash_reason]
assert {%RuntimeError{message: "oops"}, [_ | _]} = process_metadata[:crash_reason]
refute Map.has_key?(gen_statem_metadata, :initial_call)
assert process_metadata[:initial_call] == {MyGenStatemHandleEvent, :init, 1}
refute Map.has_key?(gen_statem_metadata, :registered_name)
refute Map.has_key?(process_metadata, :registered_name)
end
test "translates Task crashes" do
{:ok, pid} = Task.start_link(__MODULE__, :task, [self()])
parent = self()
+43 -28
View File
@@ -609,8 +609,8 @@ defmodule Mix.Compilers.Elixir do
# within the dependency, they will be recompiled. However, export
# and runtime dependencies won't have recompiled so we need to
# propagate them to the parent app.
{dep_modules, _, _} =
fixpoint_runtime_modules(manifest_sources, Map.from_keys(dep_modules, true))
dep_modules =
fixpoint_non_compile_modules(manifest_sources, Map.from_keys(dep_modules, true))
old_exports = Map.get(deps_exports, app, %{})
@@ -654,21 +654,19 @@ defmodule Mix.Compilers.Elixir do
end
end
defp fixpoint_runtime_modules(sources, modules) when modules != %{} do
fixpoint_runtime_modules(Map.to_list(sources), modules, false, [], [], sources)
defp fixpoint_non_compile_modules(sources, modules) when modules != %{} do
fixpoint_non_compile_modules(Map.to_list(sources), modules, false, [])
end
defp fixpoint_runtime_modules(sources, modules) do
{modules, [], sources}
defp fixpoint_non_compile_modules(_sources, modules) do
modules
end
defp fixpoint_runtime_modules(
[{source_path, source_entry} = pair | sources],
defp fixpoint_non_compile_modules(
[{_source_path, source_entry} = pair | sources],
modules,
new?,
pending_sources,
acc_modules,
acc_sources
pending_sources
) do
source(export_references: export_refs, runtime_references: runtime_refs) = source_entry
@@ -676,24 +674,19 @@ defmodule Mix.Compilers.Elixir do
new_modules = Enum.reject(source(source_entry, :modules), &Map.has_key?(modules, &1))
modules = Enum.reduce(new_modules, modules, &Map.put(&2, &1, true))
new? = new? or new_modules != []
acc_modules = new_modules ++ acc_modules
acc_sources =
Map.replace!(acc_sources, source_path, source(source_entry, runtime_warnings: []))
fixpoint_runtime_modules(sources, modules, new?, pending_sources, acc_modules, acc_sources)
fixpoint_non_compile_modules(sources, modules, new?, pending_sources)
else
pending_sources = [pair | pending_sources]
fixpoint_runtime_modules(sources, modules, new?, pending_sources, acc_modules, acc_sources)
fixpoint_non_compile_modules(sources, modules, new?, pending_sources)
end
end
defp fixpoint_runtime_modules([], modules, new?, pending_sources, acc_modules, acc_sources)
defp fixpoint_non_compile_modules([], modules, new?, pending_sources)
when new? == false or pending_sources == [],
do: {modules, acc_modules, acc_sources}
do: modules
defp fixpoint_runtime_modules([], modules, true, pending_sources, acc_modules, acc_sources),
do: fixpoint_runtime_modules(pending_sources, modules, false, [], acc_modules, acc_sources)
defp fixpoint_non_compile_modules([], modules, true, pending_sources),
do: fixpoint_non_compile_modules(pending_sources, modules, false, [])
defp exports_md5(module, use_attributes?) do
cond do
@@ -1068,7 +1061,7 @@ defmodule Mix.Compilers.Elixir do
end
end
defp each_cycle(runtime_modules, compile_path, timestamp, state) do
defp each_cycle(stale_modules, compile_path, timestamp, state) do
{modules, _exports, sources, changed, pending_modules, stale_exports} = state
{pending_modules, exports, changed} =
@@ -1081,11 +1074,33 @@ defmodule Mix.Compilers.Elixir do
end
if changed == [] do
# We merge runtime_modules (which is a map of %{module => true}) into
# a map of modules (which is a map of %{module => record}). This is fine
# since fixpoint_runtime_modules only cares about map keys.
{_, runtime_modules, sources} =
fixpoint_runtime_modules(sources, Map.merge(modules, runtime_modules))
# We merge stale_modules (which is a map of %{module => true} that the user changed)
# into a map of modules we compiled (which is a map of %{module => record}). This is
# fine because we only care about the keys.
runtime_modules = Map.merge(modules, stale_modules)
# Now we do a simple pass finding anything that directly depends on the modules that
# changed. We don't need to compute a fixpoint, because now only the directly affected
# matter.
{sources, runtime_modules} =
Enum.reduce(sources, {sources, Map.keys(runtime_modules)}, fn
{source_path, source_entry}, {acc_sources, acc_modules} ->
source(export_references: export_refs, runtime_references: runtime_refs) =
source_entry
if has_any_key?(runtime_modules, export_refs) or
has_any_key?(runtime_modules, runtime_refs) do
acc_sources =
Map.replace!(acc_sources, source_path, source(source_entry, runtime_warnings: []))
new_modules =
Enum.reject(source(source_entry, :modules), &Map.has_key?(runtime_modules, &1))
{acc_sources, new_modules ++ acc_modules}
else
{acc_sources, acc_modules}
end
end)
runtime_paths =
Enum.map(runtime_modules, &{&1, Path.join(compile_path, Atom.to_string(&1) <> ".beam")})
+18 -16
View File
@@ -9,7 +9,7 @@ defmodule Mix.SCM.Git do
@impl true
def format(opts) do
if rev = get_opts_rev(opts) do
if rev = opts[:ref] || opts[:branch] || opts[:tag] do
"#{redact_uri(opts[:git])} - #{rev}"
else
redact_uri(opts[:git])
@@ -22,7 +22,7 @@ defmodule Mix.SCM.Git do
{:git, _, lock_rev, lock_opts} ->
lock = String.slice(lock_rev, 0, 7)
case Enum.find_value([:branch, :ref, :tag], &List.keyfind(lock_opts, &1, 0)) do
case Enum.find_value([:ref, :branch, :tag], &List.keyfind(lock_opts, &1, 0)) do
{:ref, _} -> lock <> " (ref)"
{key, val} -> lock <> " (#{key}: #{val})"
nil -> lock
@@ -125,18 +125,18 @@ defmodule Mix.SCM.Git do
sparse_toggle(opts)
update_origin(opts[:git])
rev = get_lock_rev(opts[:lock], opts) || get_opts_rev(opts)
# Fetch external data
branch_or_tag = opts[:branch] || opts[:tag]
["--git-dir=.git", "fetch", "--force", "--quiet"]
|> Kernel.++(progress_switch(git_version()))
|> Kernel.++(tags_switch(opts[:tag]))
|> Kernel.++(depth_switch(opts[:depth]))
|> Kernel.++(if rev, do: ["origin", rev], else: [])
|> Kernel.++(if branch_or_tag, do: ["origin", branch_or_tag], else: [])
|> git!()
# Migrate the Git repo
rev = rev || default_branch()
rev = get_lock_rev(opts[:lock], opts) || opts[:ref] || branch_or_tag || default_branch()
git!(["--git-dir=.git", "checkout", "--quiet", rev])
if opts[:submodules] do
@@ -250,18 +250,24 @@ defmodule Mix.SCM.Git do
end
defp validate_depth(opts) do
case Keyword.take(opts, [:depth]) do
[] ->
case Keyword.take(opts, [:depth, :ref]) do
[_, _] ->
Mix.raise(
"Cannot specify :depth and :ref at the same time. " <>
"Error on Git dependency: #{redact_uri(opts[:git])}"
)
[depth: depth] when is_integer(depth) and depth > 0 ->
opts
[{:depth, depth}] when is_integer(depth) and depth > 0 ->
opts
invalid_depth ->
[depth: invalid_depth] ->
Mix.raise(
"The depth must be a positive integer, and be specified only once, got: #{inspect(invalid_depth)}. " <>
"Error on Git dependency: #{redact_uri(opts[:git])}"
)
_ ->
opts
end
end
@@ -290,10 +296,6 @@ defmodule Mix.SCM.Git do
end
end
defp get_opts_rev(opts) do
opts[:branch] || opts[:ref] || opts[:tag]
end
defp redact_uri(git) do
case URI.parse(git) do
%{userinfo: nil} -> git
+2 -1
View File
@@ -125,7 +125,8 @@ defmodule Mix.Tasks.Deps do
* `:depth` *(since v1.17.0)* - creates a shallow clone of the Git repository,
limiting the history to the specified number of commits. This can significantly
improve clone speed for large repositories when full history is not needed.
The value must be a positive integer, typically `1`.
The value must be a positive integer, typically `1`. Cannot be used with the
`:ref` option.
If your Git repository requires authentication, such as basic username:password
HTTP authentication via URLs, it can be achieved via Git configuration, keeping
+5 -5
View File
@@ -402,8 +402,8 @@ defmodule Mix.Tasks.Release do
If you are setting this option manually, we recommend the cookie option
to be a long and randomly generated string, such as:
`Base.url_encode64(:crypto.strong_rand_bytes(40))`. We also recommend to restrict
the characters in the cookie to the subset returned by `Base.url_encode64/1`.
`Base.encode32(:crypto.strong_rand_bytes(40))`. We also recommend restricting
the characters in the cookie to only alphanumeric characters and underscore.
* `:validate_compile_env` - by default a release will match all runtime
configuration against any configuration that was marked at compile time
@@ -833,9 +833,9 @@ defmodule Mix.Tasks.Release do
* `RELEASE_DISTRIBUTION` - how do we want to run the distribution.
May be `name` (long names), `sname` (short names) or `none`
(distribution is not started automatically). Defaults to
`sname` which allows access only within the current system.
`name` allows external connections
(distribution is not started automatically). Defaults to `sname`.
When connecting nodes across hosts, you typically want to set
this to `name` (required to use IPs as host names)
* `RELEASE_BOOT_SCRIPT` - the name of the boot script to use when starting
the release. This script is used when running commands such as `start` and
+31 -8
View File
@@ -53,7 +53,7 @@ defmodule Mix.Tasks.Release.Init do
## Enable deployment without epmd
## (requires changing both vm.args and remote.vm.args)
##-start_epmd false -erl_epmd_port 6789#{remote? && " -dist_listen false"}
##-start_epmd false -erl_epmd_port 6789#{if(remote?, do: " -dist_listen false")}
"""
@doc false
@@ -136,7 +136,7 @@ defmodule Mix.Tasks.Release.Init do
;;
*)
echo "ERROR: Expected sname, name, or none in RELEASE_DISTRIBUTION, got: $RELEASE_DISTRIBUTION" >&2
echo "ERROR: Expected RELEASE_DISTRIBUTION to be sname, name, or none, got: $RELEASE_DISTRIBUTION" >&2
exit 1
;;
esac
@@ -280,7 +280,7 @@ defmodule Mix.Tasks.Release.Init do
rem set RELEASE_MODE=interactive
rem Set the release to work across nodes.
rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
rem RELEASE_DISTRIBUTION must be sname (local), name (distributed) or none.
rem set RELEASE_DISTRIBUTION=name
rem set RELEASE_NODE=<%= @release.name %>
"""
@@ -314,13 +314,33 @@ defmodule Mix.Tasks.Release.Init do
if not defined RELEASE_BOOT_SCRIPT_CLEAN (set RELEASE_BOOT_SCRIPT_CLEAN=start_clean)
if not defined RELEASE_SYS_CONFIG (set RELEASE_SYS_CONFIG=!REL_VSN_DIR!\sys)
if "!RELEASE_DISTRIBUTION!" == "none" (
rem
) else if "!RELEASE_DISTRIBUTION!" == "name" (
rem
) else if "!RELEASE_DISTRIBUTION!" == "sname" (
rem
) else (
echo ERROR: Expected RELEASE_DISTRIBUTION to be sname, name, or none, got: !RELEASE_DISTRIBUTION!
exit /B 1
)
if "!RELEASE_MODE!" == "embedded" (
rem
) else if "!RELEASE_MODE!" == "interactive" (
rem
) else (
echo ERROR: Expected RELEASE_MODE to be embedded or interactive, got: !RELEASE_MODE!
exit /B 1
)
if "%~1" == "start" (set "REL_EXEC=elixir" && set "REL_EXTRA=--no-halt" && set "REL_GOTO=start")
if "%~1" == "start_iex" (set "REL_EXEC=iex" && set "REL_EXTRA=--werl" && set "REL_GOTO=start")
if "%~1" == "install" (set "REL_GOTO=install")
if "%~1" == "eval" (
if "%~2" == "" (
echo ERROR: EVAL expects an expression as argument
goto end
exit /B 1
)
set "REL_GOTO=eval"
)
@@ -348,7 +368,7 @@ defmodule Mix.Tasks.Release.Init do
if "%~1" == "rpc" (
if "%~2" == "" (
echo ERROR: RPC expects an expression as argument
goto end
exit /B 1
)
set "REL_RPC=%~2"
goto rpc
@@ -369,7 +389,10 @@ defmodule Mix.Tasks.Release.Init do
echo pid Prints the operating system PID of the running system via a remote command
echo version Prints the release name and version to be booted
echo.
if not "%~1" == "" (echo ERROR: Unknown command %~1)
if not "%~1" == "" (
echo ERROR: Unknown command %~1
exit /B 1
)
goto end
:start
@@ -427,7 +450,7 @@ defmodule Mix.Tasks.Release.Init do
if "!RELEASE_DISTRIBUTION!" == "none" (
set RELEASE_DISTRIBUTION_FLAG=
) else (
set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "rem-!RANDOM!-!RELEASE_NODE!"
set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "rpc-!RANDOM!-!RELEASE_NODE!"
)
"!REL_VSN_DIR!\elixir.bat" ^
@@ -452,7 +475,7 @@ defmodule Mix.Tasks.Release.Init do
if "!RELEASE_DISTRIBUTION!" == "none" (
echo ERROR: RELEASE_DISTRIBUTION is required in install command
goto end
exit /B 1
)
"!ERLSRV!" add "!RELEASE_NAME!_!RELEASE_NAME!" ^
+5 -10
View File
@@ -1597,11 +1597,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
def foo(), do: B.foo()
def bar(), do: B.bar()
def __after_verify__(__MODULE__) do
if Code.ensure_loaded?(B) and not function_exported?(B, :foo, 0) do
:ok
else
IO.warn("AFTER_VERIFY", __ENV__)
end
IO.warn("AFTER_VERIFY", __ENV__)
end
end
""")
@@ -1630,10 +1626,9 @@ defmodule Mix.Tasks.Compile.ElixirTest do
end)
# Check B due to direct dependency on A
# Check C due to transient dependency on A
assert output =~ "A.foo/0 is undefined or private"
assert output =~ "B.bar/0 is undefined or private"
assert output =~ "AFTER_VERIFY"
refute output =~ "B.bar/0 is undefined or private"
refute output =~ "AFTER_VERIFY"
# Ensure only A was recompiled
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
@@ -1650,7 +1645,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
assert output =~ "B.bar/0 is undefined or private"
assert output =~ "AFTER_VERIFY"
# Now we change B and it must no longer emit an AFTER_VERIFY warning
# Now we change B and it must emit an AFTER_VERIFY warning
File.write!("lib/b.ex", """
defmodule B do
end
@@ -1663,7 +1658,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
assert output =~ "B.foo/0 is undefined or private"
assert output =~ "B.bar/0 is undefined or private"
refute output =~ "AFTER_VERIFY"
assert output =~ "AFTER_VERIFY"
end)
end
-37
View File
@@ -530,43 +530,6 @@ defmodule Mix.Tasks.DepsGitTest do
end)
end
test "with ref" do
[last, _ | _] = get_git_repo_revs("git_repo")
Process.put(:git_repo_opts, depth: 1, ref: last)
in_fixture("no_mixfile", fn ->
Mix.Project.push(GitApp)
Mix.Tasks.Deps.Get.run([])
message = "* Getting git_repo (#{fixture_path("git_repo")} - #{last})"
assert_received {:mix_shell, :info, [^message]}
assert_shallow("deps/git_repo", 1)
end)
end
test "changing refspec updates retaining depth" do
[last, first | _] = get_git_repo_revs("git_repo")
Process.put(:git_repo_opts, ref: first, depth: 1)
in_fixture("no_mixfile", fn ->
Mix.Project.push(GitApp)
Mix.Tasks.Deps.Get.run([])
message = "* Getting git_repo (#{fixture_path("git_repo")} - #{first})"
assert_received {:mix_shell, :info, [^message]}
assert_shallow("deps/git_repo", 1)
assert File.read!("mix.lock") =~ first
# Change refspec
update_dep(ref: last, depth: 1)
Mix.Tasks.Deps.Get.run([])
assert_shallow("deps/git_repo", 1)
assert File.read!("mix.lock") =~ last
end)
end
test "removing depth retains shallow repository" do
# For compatibility and simplicity, we follow Git's behavior and do not
# attempt to unshallow an existing repository. This should not be a