1defmodule Mix.Tasks.Deps.Clean do
2  use Mix.Task
3
4  @shortdoc "Deletes the given dependencies' files"
5
6  @moduledoc """
7  Deletes the given dependencies' files, including build artifacts and fetched
8  sources.
9
10  Since this is a destructive action, cleaning of dependencies
11  only occurs when passing arguments/options:
12
13    * `dep1 dep2` - the names of dependencies to be deleted separated by a space
14    * `--unlock` - also unlocks the deleted dependencies
15    * `--build` - deletes only compiled files (keeps source files)
16    * `--all` - deletes all dependencies
17    * `--unused` - deletes only unused dependencies
18      (i.e. dependencies no longer mentioned in `mix.exs`)
19
20  By default this task works across all environments,
21  unless `--only` is given which will clean all dependencies
22  for the chosen environment.
23  """
24
25  @switches [unlock: :boolean, all: :boolean, only: :string, unused: :boolean, build: :boolean]
26
27  @impl true
28  def run(args) do
29    Mix.Project.get!()
30    {opts, apps, _} = OptionParser.parse(args, switches: @switches)
31
32    build_path =
33      Mix.Project.build_path()
34      |> Path.dirname()
35      |> Path.join("*#{opts[:only]}/lib")
36
37    deps_path = Mix.Project.deps_path()
38
39    loaded_opts =
40      for {switch, key} <- [only: :env, target: :target],
41          value = opts[switch],
42          do: {key, :"#{value}"}
43
44    loaded_deps = Mix.Dep.load_on_environment(loaded_opts)
45
46    apps_to_clean =
47      cond do
48        opts[:all] ->
49          checked_deps(build_path, deps_path)
50
51        opts[:unused] ->
52          checked_deps(build_path, deps_path) |> filter_loaded(loaded_deps)
53
54        apps != [] ->
55          apps
56
57        true ->
58          Mix.raise(
59            "\"mix deps.clean\" expects dependencies as arguments or " <>
60              "an option indicating which dependencies to clean. " <>
61              "The --all option will clean all dependencies while " <>
62              "the --unused option cleans unused dependencies"
63          )
64      end
65
66    do_clean(apps_to_clean, loaded_deps, build_path, deps_path, opts[:build])
67
68    if opts[:unlock] do
69      Mix.Task.run("deps.unlock", args)
70    else
71      :ok
72    end
73  end
74
75  defp checked_deps(build_path, deps_path) do
76    deps_names =
77      for root <- [deps_path, build_path],
78          path <- Path.wildcard(Path.join(root, "*")),
79          File.dir?(path),
80          uniq: true,
81          do: Path.basename(path)
82
83    List.delete(deps_names, to_string(Mix.Project.config()[:app]))
84  end
85
86  defp filter_loaded(apps, deps) do
87    apps -- Enum.map(deps, &Atom.to_string(&1.app))
88  end
89
90  defp maybe_warn_for_invalid_path([], dependency) do
91    Mix.shell().error(
92      "warning: the dependency #{dependency} is not present in the build directory"
93    )
94
95    []
96  end
97
98  defp maybe_warn_for_invalid_path(paths, _dependency) do
99    paths
100  end
101
102  defp do_clean(apps, deps, build_path, deps_path, build_only?) do
103    shell = Mix.shell()
104
105    local = for %{scm: scm, app: app} <- deps, not scm.fetchable?, do: Atom.to_string(app)
106
107    Enum.each(apps, fn app ->
108      shell.info("* Cleaning #{app}")
109
110      # Remove everything from the build directory of dependencies
111      build_path
112      |> Path.join(to_string(app))
113      |> Path.wildcard()
114      |> maybe_warn_for_invalid_path(app)
115      |> Enum.each(&File.rm_rf!/1)
116
117      # Remove everything from the source directory of dependencies.
118      # Skip this step if --build option is specified or if
119      # the dependency is local, i.e., referenced using :path.
120      if build_only? || app in local do
121        :do_not_delete_source
122      else
123        deps_path
124        |> Path.join(to_string(app))
125        |> File.rm_rf!()
126      end
127    end)
128  end
129end
130