Packages
igniter
0.3.75
0.8.2
0.8.1
0.8.0
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.30
0.6.29
0.6.28
0.6.27
0.6.26
0.6.25
0.6.24
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.52
0.5.51
0.5.50
0.5.49
0.5.48
0.5.47
0.5.46
0.5.45
0.5.44
0.5.43
0.5.42
0.5.41
0.5.40
0.5.39
0.5.38
0.5.37
0.5.36
0.5.35
0.5.34
0.5.33
0.5.32
0.5.31
0.5.30
0.5.29
0.5.28
0.5.27
0.5.26
0.5.25
0.5.24
0.5.23
0.5.22
0.5.21
0.5.20
0.5.19
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.78
0.3.77
0.3.76
0.3.75
0.3.74
0.3.73
0.3.72
0.3.71
0.3.70
0.3.69
0.3.68
0.3.67
0.3.66
0.3.65
0.3.64
0.3.63
0.3.62
0.3.61
0.3.60
0.3.59
0.3.58
0.3.57
0.3.56
0.3.55
0.3.54
0.3.53
0.3.52
0.3.51
0.3.50
0.3.49
0.3.48
0.3.47
0.3.46
0.3.45
0.3.44
0.3.43
0.3.42
0.3.41
0.3.40
0.3.39
0.3.38
0.3.37
0.3.36
0.3.35
0.3.34
0.3.33
0.3.32
0.3.31
0.3.30
0.3.29
0.3.28
0.3.27
0.3.26
0.3.25
0.3.24
0.3.23
0.3.22
0.3.21
0.3.20
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
A code generation and project patching framework
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/igniter.upgrade.ex
defmodule Mix.Tasks.Igniter.Upgrade do
use Igniter.Mix.Task
@example "mix igniter.upgrade package1 package2@1.2.1"
@shortdoc "Fetch and upgrade dependencies. A drop in replacement for `mix deps.update` that also runs upgrade tasks."
@moduledoc """
#{@shortdoc}
Updates dependencies via `mix deps.update` and then runs any upgrade tasks for any changed dependencies.
By default, this task updates to the latest versions allowed by the `mix.exs` file, just like `mix deps.update`.
To upgrade a package to a specific version, you can specify the version after the package name,
separated by an `@` symbol. This allows upgrading beyond what your mix.exs file currently specifies,
i.e if you have `~> 1.0` in your mix.exs file, you can use `mix igniter.upgrade package@2.0` to
upgrade to version 2.0, which will update your `mix.exs` and run any equivalent upgraders.
## Limitations
The new version of the package must be "compile compatible" with your existing code. See the upgrades guide for more.
## Example
```bash
#{@example}
```
## Options
* `--yes` or `-y` - Accept all changes automatically
* `--all` or `-a` - Upgrades all dependencies
* `--only` or `-o` - only fetches dependencies for given environment
* `--target` or `-t` - only fetches dependencies for given target
* `--no-archives-check` or `-n` - does not check archives before fetching deps
* `--git-ci` or `-g` - Uses git history (HEAD~1) to check the previous versions in the lock file.
See the upgrade guides for more. Sets --yes automatically.
"""
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
group: :igniter,
example: @example,
positional: [
packages: [rest: true, optional: true]
],
schema: [
yes: :boolean,
all: :boolean,
only: :string,
target: :string,
no_archives_check: :boolean,
git_ci: :boolean
],
aliases: [y: :yes, a: :all, o: :only, t: :target, n: :no_archives_check, g: :git_ci],
defaults: [yes: false]
}
end
def igniter(igniter, argv) do
{%{packages: packages}, argv} = positional_args!(argv)
options = options!(argv)
options =
if options[:git_ci] do
Keyword.put(options, :yes, true)
else
options
end
packages =
packages
|> Enum.join(",")
|> String.split(",")
if Enum.empty?(packages) && !options[:all] do
Mix.shell().error("""
Must specify at least one package to upgrade or use --all to upgrade all packages.
""")
exit({:shutdown, 1})
end
if options[:only] && !Enum.empty?(packages) do
Mix.shell().error("""
Cannot specify both --only and package names.
""")
exit({:shutdown, 1})
end
if options[:target] && !Enum.empty?(packages) do
Mix.shell().error("""
Cannot specify both --target and package names.
""")
exit({:shutdown, 1})
end
original_deps_info =
if options[:git_ci] do
System.cmd("git", ["show", "HEAD~1:mix.lock"])
|> elem(0)
|> Code.format_string!()
|> IO.iodata_to_binary()
|> Code.eval_string()
|> elem(0)
|> Enum.flat_map(fn {key, config} ->
with {_, _, version, _, _, _, _, _} <- config,
{:ok, _version} <- Version.parse(version) do
[%{app: key, status: {:ok, version}}]
else
_ ->
[]
end
end)
else
Mix.Dep.cached()
|> expand_deps()
|> Enum.filter(&match?({:ok, v} when is_binary(v), &1.status))
end
Mix.Task.run("compile")
igniter =
igniter
|> Igniter.include_existing_file("mix.exs")
|> Igniter.include_existing_file("mix.lock")
original_mix_exs = Rewrite.Source.get(Rewrite.source!(igniter.rewrite, "mix.exs"), :content)
original_mix_lock = Rewrite.Source.get(Rewrite.source!(igniter.rewrite, "mix.lock"), :content)
validate_packages!(packages)
package_names =
packages
|> Enum.map(&(String.split(&1, "@") |> List.first()))
|> Enum.map(&String.to_atom/1)
update_deps_args = update_deps_args(options)
igniter =
if options[:git_ci] do
igniter
else
packages
|> Enum.reduce(igniter, &replace_dep(&2, &1))
|> Igniter.apply_and_fetch_dependencies(
error_on_abort?: true,
yes: options[:yes],
update_deps: Enum.map(package_names, &to_string/1),
update_deps_args: update_deps_args,
force?: true
)
end
try do
new_deps_info =
Mix.Dep.load_and_cache()
|> expand_deps()
|> then(fn deps ->
if options[:git_ci] do
deps
else
Enum.map(deps, fn dep ->
status =
Mix.Dep.in_dependency(dep, fn _ ->
if File.exists?("mix.exs") do
Mix.Project.pop()
Installer.Lib.Private.SharedUtils.reevaluate_mix_exs()
{:ok, Mix.Project.get!().project()[:version]}
else
dep.status
end
end)
%{dep | status: status}
end)
end
end)
|> Enum.filter(&match?({:ok, v} when is_binary(v), &1.status))
Mix.Task.reenable("compile")
Mix.Task.reenable("loadpaths")
Mix.Task.run("compile")
Mix.Task.reenable("compile")
original_deps_info
|> dep_changes_in_order(new_deps_info)
|> Enum.reduce({igniter, []}, fn update, {igniter, missing} ->
case apply_updates(igniter, update, argv) do
{:ok, igniter} ->
{igniter, missing}
{:missing, missing_package} ->
{igniter, [missing_package | missing]}
end
end)
|> case do
{igniter, []} ->
igniter
{igniter, missing} ->
Igniter.add_notice(
igniter,
"The packages `#{Enum.join(missing, ", ")}` did not have upgrade tasks."
)
end
rescue
e ->
if !options[:git_ci] do
recover_mix_exs_and_lock(
igniter,
original_mix_exs,
original_mix_lock,
Exception.format(:error, e, __STACKTRACE__),
options
)
end
reraise e, __STACKTRACE__
catch
:exit, reason ->
if !options[:git_ci] do
recover_mix_exs_and_lock(
igniter,
original_mix_exs,
original_mix_lock,
"exit: " <> inspect(reason),
options
)
end
exit(reason)
end
end
defp update_deps_args(options) do
update_deps_args =
if only = options[:only] do
["--only", only]
else
[]
end
update_deps_args =
if target = options[:target] do
["--target", target] ++ update_deps_args
else
update_deps_args
end
update_deps_args =
if options[:no_archives_check] do
["--no-archives-check"] ++ update_deps_args
else
update_deps_args
end
if options[:all] do
["--all"] ++ update_deps_args
else
update_deps_args
end
end
defp apply_updates(igniter, {package, from, to}, argv) do
task =
if package == :igniter do
"igniter.upgrade_igniter"
else
"#{package}.upgrade"
end
with task when not is_nil(task) <- Mix.Task.get(task),
true <- function_exported?(task, :info, 2) do
{:ok, task.igniter(igniter, [from, to] ++ argv)}
else
_ ->
{:missing, package}
end
end
defp recover_mix_exs_and_lock(igniter, mix_exs, mix_lock, reason, options) do
if !igniter.assigns[:test_mode?] do
if options[:yes] ||
Igniter.Util.IO.yes?("""
Something went wrong during the upgrade process.
#{reason}
Restore mix.exs and mix.lock to their original contents?
If you don't do this, you will need to reset them to upgrade again,
or perform any upgrade steps manually.
""") do
File.write!("mix.exs", mix_exs)
File.write!("mix.lock", mix_lock)
end
end
end
defp dep_changes_in_order(old_deps_info, new_deps_info) do
new_deps_info
|> sort_deps()
|> Enum.flat_map(fn dep ->
case Enum.find(old_deps_info, &(&1.app == dep.app)) do
nil ->
[{dep.app, nil, Version.parse!(elem(dep.status, 1))}]
%{status: {:ok, old_version}} ->
[{dep.app, Version.parse!(old_version), Version.parse!(elem(dep.status, 1))}]
_other ->
[]
end
end)
|> Enum.reject(fn {_app, old, new} ->
old == new
end)
end
defp sort_deps([]), do: []
defp sort_deps(deps) do
free_dep_name =
Enum.find_value(deps, fn %{app: app, deps: children} ->
if !Enum.any?(children, fn child ->
Enum.any?(deps, &(&1.app == child))
end) do
app
end
end)
next_dep_name = free_dep_name || elem(Enum.min_by(deps, &length(&1.deps)), 0)
{[next_dep], others} = Enum.split_with(deps, &(&1.app == next_dep_name))
[
next_dep
| sort_deps(
Enum.map(others, fn dep ->
%{dep | deps: Enum.reject(dep.deps, &(&1.app == next_dep_name))}
end)
)
]
end
defp replace_dep(igniter, package) do
if String.contains?(package, "@") do
requirement =
case Igniter.Project.Deps.determine_dep_type_and_version(package) do
{package, requirement} ->
{package, requirement}
:error ->
Mix.shell().error("Invalid package identifier: #{package}")
exit({:shutdown, 1})
end
Igniter.Project.Deps.add_dep(igniter, requirement, yes?: true)
else
igniter
end
end
defp expand_deps(deps) do
if Enum.any?(deps, &(&1.deps != [])) do
expand_deps(Enum.flat_map(deps, &[%{&1 | deps: []} | &1.deps]))
else
Enum.uniq_by(deps, & &1.app)
end
end
defp validate_packages!(packages) do
Enum.each(packages, fn package ->
package_name = String.split(package, "@") |> Enum.at(0) |> String.to_atom()
dependency_declaration =
Mix.Project.get!().project()[:deps] |> Enum.find(&(elem(&1, 0) == package_name))
if String.contains?(package, "@") do
is_non_version_updatable_package =
case dependency_declaration do
{_dep, opts} when is_list(opts) ->
!!(opts[:path] || opts[:git] || opts[:github])
{_dep, _, opts} ->
!!(opts[:path] || opts[:git] || opts[:github])
_ ->
false
end
if is_non_version_updatable_package do
Mix.shell().error("""
The update specification `#{package}` is invalid because the package `#{package_name}`
is pointing at a path, git, or github. These do not currently accept versions while upgrading.
""")
end
end
allowed_envs =
case dependency_declaration do
{_dep, opts} when is_list(opts) ->
opts[:only]
{_dep, _, opts} ->
opts[:only]
_ ->
nil
end
allowed_envs =
if allowed_envs == [] do
nil
else
allowed_envs
end
if allowed_envs && !(Mix.env() in allowed_envs) do
package_name = String.split(package, "@") |> Enum.at(0)
Mix.shell().error("""
Cannot update apply upgrade `#{package}` because the package `#{package_name}` is only included
in the following environments: `#{inspect(allowed_envs)}`, but the current environment is `#{Mix.env()}`.
Rerun this command with `MIX_ENV=#{Enum.at(allowed_envs, 0)} mix igniter.upgrade ...`
""")
end
end)
end
end