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