summaryrefslogtreecommitdiff
path: root/lib/mix/lib/mix/tasks/archive.install.ex
blob: c4df108899dc31f9269cd9cefc107b61d5994ff2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
defmodule Mix.Tasks.Archive.Install do
  use Mix.Task

  @shortdoc "Installs an archive locally"

  @moduledoc """
  Installs an archive locally.

  If no argument is supplied but there is an archive in the project's
  root directory (created with `mix archive.build`), then the archive
  will be installed locally. For example:

      mix do archive.build, archive.install

  If an argument is provided, it should be a local path or a URL to a
  prebuilt archive, a Git repository, a GitHub repository, or a Hex
  package.

      mix archive.install archive.ez
      mix archive.install path/to/archive.ez
      mix archive.install https://example.com/my_archive.ez
      mix archive.install git https://path/to/git/repo
      mix archive.install git https://path/to/git/repo branch git_branch
      mix archive.install git https://path/to/git/repo tag git_tag
      mix archive.install git https://path/to/git/repo ref git_ref
      mix archive.install github user/project
      mix archive.install github user/project branch git_branch
      mix archive.install github user/project tag git_tag
      mix archive.install github user/project ref git_ref
      mix archive.install hex hex_package
      mix archive.install hex hex_package 1.2.3

  After installation, the tasks in the archive are available locally:

      mix some_task

  Note that installing via Git/GitHub/Hex fetches the source of the archive
  and builds it, while using URL/local path fetches a pre-built archive.

  ## Command line options

    * `--sha512` - checks the archive matches the given SHA-512 checksum. Only
      applies to installations via URL or local path

    * `--force` - forces installation without a shell prompt; primarily
      intended for automation in build systems like Make

    * `--submodules` - fetches repository submodules before building archive from
      Git or GitHub

    * `--app` - specifies a custom app name to be used for building the archive
      from Git, GitHub, or Hex

    * `--organization` - specifies an organization to use if fetching the package
      from a private Hex repository

  """

  @behaviour Mix.Local.Installer

  @switches [
    force: :boolean,
    sha512: :string,
    submodules: :boolean,
    app: :string,
    organization: :string
  ]

  def run(argv) do
    Mix.Local.Installer.install(__MODULE__, argv, @switches)
  end

  # Callbacks
  def check_install_spec({local_or_url, path_or_url} = _install_spec, _opts)
      when local_or_url in [:local, :url] do
    if Path.extname(path_or_url) == ".ez" do
      :ok
    else
      {:error, "Expected a local file path or a file URL ending in \".ez\"."}
    end
  end

  def check_install_spec(_, _), do: :ok

  def find_previous_versions(src) do
    app =
      src
      |> Mix.Local.archive_name()
      |> String.split("-")
      |> List.first()

    if app do
      archives(app) ++ archives(app <> "-*")
    else
      []
    end
  end

  def install(basename, contents, previous) do
    ez_path = Path.join(Mix.Local.path_for(:archive), basename)
    dir_dest = resolve_destination(ez_path, contents)

    remove_previous_versions(previous)

    File.mkdir_p!(dir_dest)
    {:ok, _} = :zip.extract(contents, cwd: dir_dest)
    Mix.shell().info([:green, "* creating ", :reset, Path.relative_to_cwd(dir_dest)])

    ebin = Mix.Local.archive_ebin(dir_dest)
    Mix.Local.check_elixir_version_in_ebin(ebin)
    true = Code.append_path(ebin)
    :ok
  end

  def build(_install_spec, _opts) do
    src = Mix.Local.name_for(:archive, Mix.Project.config())
    previous = find_previous_versions(src)

    Enum.each(previous, fn path ->
      Code.delete_path(Mix.Local.archive_ebin(path))
    end)

    Mix.Task.run("archive.build", [])
    src
  end

  ### Private helpers

  defp resolve_destination(ez_path, contents) do
    with {:ok, [_comment, zip_first_file | _]} <- :zip.list_dir(contents),
         {:zip_file, zip_first_path, _, _, _, _} = zip_first_file,
         [zip_root_dir | _] = Path.split(zip_first_path) do
      Path.join(Path.dirname(ez_path), zip_root_dir)
    else
      _ ->
        Mix.raise("Installation failed: invalid archive file")
    end
  end

  defp archives(name) do
    Mix.Local.path_for(:archive)
    |> Path.join(name)
    |> Path.wildcard()
  end

  defp remove_previous_versions([]), do: :ok
  defp remove_previous_versions(previous), do: Enum.each(previous, &File.rm_rf!/1)
end