• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pprzetacznik / IElixir / 0bdb5f6385be8c9bb2816c7f5538e40e622eb1ca-PR-65

21 Mar 2024 12:19AM UTC coverage: 51.436% (-0.3%) from 51.697%
0bdb5f6385be8c9bb2816c7f5538e40e622eb1ca-PR-65

Pull #65

github

pprzetacznik
chore: Bump otp and iex in Dockerfile requirements
Pull Request #65: chore: Bump otp and iex in Dockerfile requirements

197 of 383 relevant lines covered (51.44%)

40.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.93
/lib/ielixir/boyle.ex
1
defmodule Boyle do
2
  @moduledoc """
3
  This module is responsible for runtime package management. Name of the package honours remarkable chemist, Robert Boyle. This package allows you to manage your Elixir virtual enviromnent without need of restarting erlang virtual machine. Boyle installs environment into `./envs/you_new_environment` directory and creates new mix project there with requested dependencies. It keeps takes care of fetching, compiling and loading/unloading modules from dependencies list of that environment.
4

5
  You can also use this environment as a separate mix project and run it interactively with `iex -S mix` from the environment directory.
6
  """
7

8
  @typedoc "Return values of `start*` functions"
9
  @type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}
10

11
  require Logger
12
  use GenServer
13

14
  @spec start_link(map) :: on_start
15
  def start_link(opts) do
16
    GenServer.start_link(__MODULE__, opts, name: Boyle)
1✔
17
  end
18

19
  def init(opts) do
20
    environment_dir_path = Path.join(opts[:starting_path], "envs")
1✔
21
    File.mkdir_p(environment_dir_path)
1✔
22
    {:ok, %{active_environment: nil,
23
            environment_dir_path: environment_dir_path,
24
            environment_path: nil,
25
            starting_path: opts[:starting_path],
26
            initial_paths: :code.get_path(),
27
            initial_modules: :code.all_loaded()}}
28
  end
29

30
  @doc """
31
  List all available environemnts created in the past.
32

33
  ## Examples
34

35
    iex> {:ok, envs_list} = Boyle.list()
36
    iex> "boyle_test_env" in envs_list
37
    true
38

39
  """
40
  def list do
11✔
41
    {:ok, state().environment_dir_path
11✔
42
          |> Path.join("*")
43
          |> Path.wildcard()
44
          |> Enum.map(&List.last(String.split(&1, "/")))}
18✔
45
  end
46

47
  @doc """
48
  Create new virtual environment where your modules will be stored and compiled.
49

50
  ## Examples
51

52
      iex> {:ok, envs_list} = Boyle.mk("test_env")
53
      iex> "test_env" in envs_list
54
      true
55

56
  """
57
  def mk(name) do
58
    env_path = Path.join(state().environment_dir_path, to_string(name))
4✔
59
    File.mkdir_p(env_path)
4✔
60
    create_mix_exs_file(env_path)
4✔
61
    create_deps_lock_file(env_path)
4✔
62
    :ok = activate(name)
4✔
63
    :ok = deactivate()
4✔
64
    list()
4✔
65
  end
66

67
  @doc """
68
  Remove existing environment.
69

70
  ## Examples
71

72
      iex> {:ok, envs_list} = Boyle.rm("test_env_for_removal")
73
      iex> "test_env_for_removal" in envs_list
74
      false
75

76
  """
77
  def rm(name) do
78
    if active_env_name() == name do
4✔
79
      :ok = deactivate()
×
80
    end
81
    result = remove_environment(name)
4✔
82
    {_, list} = list()
4✔
83
    {result, list}
84
  end
85

86
  @doc """
87
  Show detailed dependencies in the current environment stored in `mix.lock` file created for the environment.
88

89
  ## Examples
90

91
      iex> Boyle.activate("boyle_test_env")
92
      :ok
93
      iex> Boyle.freeze()
94
      {%{decimal:
95
        {:hex,
96
          :decimal,
97
          "1.7.0",
98
          "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa",
99
          [:mix],
100
          [],
101
          "hexpm",
102
          "771ea78576e5fa505ad58a834f57915c7f5f9df11c87a598a01fdf6065ccfb5d"
103
        }},
104
        []}
105
      iex> Boyle.deactivate()
106
      :ok
107

108
  """
109
  def freeze do
110
    lockfile_path = Path.join(environment_path(), "mix.lock")
1✔
111
    {deps, bindings} = Code.eval_file(lockfile_path)
1✔
112
    {deps, bindings}
113
  end
114

115
  @doc """
116
  Activate environment and load all modules that are installed within this module.
117

118
  ## Examples
119

120
      iex> Boyle.activate("boyle_test_env")
121
      :ok
122
      iex> Boyle.deactivate()
123
      :ok
124

125
  """
126
  def activate(name) do
127
    GenServer.call(Boyle, {:activate, name})
14✔
128
    reinstall()
14✔
129
  end
130

131
  @doc """
132
  Activate environment and unload all modules that are installed within this module.
133

134
  ## Examples
135

136
      iex> Boyle.activate("boyle_test_env")
137
      :ok
138
      iex> Boyle.deactivate()
139
      :ok
140

141
  """
142
  def deactivate do
143
    File.cd!(state()[:starting_path], fn ->
13✔
144
      if active_env_name() do
13✔
145
        Mix.Project.pop()
13✔
146
        Mix.Task.clear()
13✔
147
        # Mix.Shell.Process.flush()
148
        Mix.State.clear_cache()
13✔
149
        # Mix.ProjectStack.clear_stack()
150
        environment_path = environment_path()
13✔
151
        state = state()
13✔
152

153
        :code.get_path |> Enum.map(fn path ->
13✔
154
          if path not in state.initial_paths and
819✔
155
            String.contains?(to_string(path), environment_path) do
13✔
156

157
            Code.delete_path(path)
9✔
158
            Logger.debug("Removed path #{to_string(path)}")
9✔
159
          end
160
        end)
161
        :code.all_loaded |> Enum.map(fn {module, path} ->
13✔
162
          if {module, path} not in state.initial_modules and
11,491✔
163
            (String.contains?(to_string(path), environment_path) or "" == to_string(path)) and
1,313✔
164
            not String.contains?(to_string(module), ["Elixir.Boyle", "Elixir.IElixir"]) do
126✔
165

166
            purge([module])
48✔
167
            Logger.debug("Purged module #{to_string(module)} : #{to_string(path)}")
48✔
168
          end
169
        end)
170
        Code.load_file("mix.exs")
13✔
171
        GenServer.call(Boyle, {:activate, nil})
13✔
172
      else
173
        :ok
174
      end
175
    end)
176
  end
177

178
  @doc """
179
  Activate environment and unload all modules that are installed within this module.
180

181
  ## Examples
182

183
      iex> Boyle.activate("boyle_test_env")
184
      :ok
185
      iex> Boyle.install({:decimal, "~> 1.5.0"})
186
      :ok
187
      iex> Boyle.deactivate()
188
      :ok
189

190
  """
191
  def install(new_dep) do
192
    deps_list = read()
3✔
193
    app_names = for dep <- deps_list, do: elem(dep, 0)
3✔
194
    new_dep_app_name = elem(new_dep, 0)
3✔
195
    if new_dep_app_name not in app_names do
3✔
196
      new_deps_list = deps_list ++ [new_dep]
2✔
197
      File.cd!(environment_path(), fn ->
2✔
198
        write(new_deps_list)
2✔
199
      end)
200

201
      env_name = active_env_name()
2✔
202
      :ok = deactivate()
2✔
203
      :ok = activate(env_name)
2✔
204
    else
205
      :ok
206
    end
207
  end
208

209
  @doc """
210
  Get name of active environment.
211

212
  ## Examples
213

214
      iex> Boyle.activate("boyle_test_env")
215
      :ok
216
      iex> Boyle.active_env_name()
217
      "boyle_test_env"
218

219
  """
220
  def active_env_name do
221
    GenServer.call(Boyle, :get_active_env_name)
27✔
222
  end
223

224
  @doc """
225
  Get absolute path of active environment.
226
  """
227
  def environment_path do
228
    GenServer.call(Boyle, :get_environment_path)
33✔
229
  end
230

231
  @doc """
232
  Get state of Boyle module, some internal paths useful for loaded modules management.
233
  """
234
  def state do
235
    GenServer.call(Boyle, :get_state)
59✔
236
  end
237

238
  @doc """
239
  Make sure all dependencies are fetched, compiled and loaded.
240
  """
241
  def reinstall() do
242
    reinstall(environment_path())
14✔
243
  end
244
  defp reinstall(env_path) do
245
    File.cd!(state()[:starting_path], fn ->
14✔
246
      :code.purge(IElixir.Mixfile)
14✔
247
      :code.delete(IElixir.Mixfile)
14✔
248
      Mix.start()
14✔
249
      Mix.Project.pop()
14✔
250

251
      File.cd!(env_path, fn ->
14✔
252
        Code.load_file("mix.exs")
14✔
253
        # Mix.Project.push(CustomEnvironment)
254
        # IO.inspect(CustomEnvironment.project())
255
        Mix.Task.run("deps.get")
14✔
256
        Mix.Tasks.Deps.Compile.run([])
14✔
257
      end)
258
      :ok
259
    end)
260
  end
261

262
  @doc """
263
  Print list of modules paths and loaded modules.
264
  """
265
  def paths do
266
    :code.get_path()
267
    |> Enum.map(&IO.puts(&1))
×
268

269
    :code.all_loaded()
270
    |> Enum.sort_by(fn {name, _} ->
271
      to_string(name)
×
272
    end)
273
    |> Enum.map(fn {name, path} ->
×
274
      IO.puts(to_string(name) <> " : " <> to_string(path))
×
275
    end)
276
  end
277

278
  def handle_call({:activate, nil}, _from, state) do
279
    new_state = Map.put(state, :active_environment, nil)
13✔
280
    new_state = Map.put(new_state, :environment_path, nil)
13✔
281
    {:reply, :ok, new_state}
13✔
282
  end
283
  def handle_call({:activate, new_name}, _from, state = %{environment_dir_path: environment_dir_path}) do
284
    new_state = Map.put(state, :active_environment, new_name)
14✔
285
    new_state = Map.put(new_state, :environment_path, Path.join(environment_dir_path, new_name))
14✔
286
    {:reply, new_name, new_state}
14✔
287
  end
288

289
  def handle_call(:get_active_env_name, _from, state = %{active_environment: active_environment}) do
290
    {:reply, active_environment, state}
27✔
291
  end
292

293
  def handle_call(:get_environment_path, _from, state = %{environment_path: environment_path}) do
294
    {:reply, environment_path, state}
33✔
295
  end
296

297
  def handle_call(:get_state, _from, state) do
298
    {:reply, state, state}
59✔
299
  end
300

301
  defp remove_environment(name) do
302
    envs_path = state().environment_dir_path
4✔
303
    final_path = envs_path
4✔
304
                 |> Path.join(name)
305
                 |> Path.wildcard()
306
    if not String.contains?(name, ["..", "\\", "/"]) and
4✔
307
       name != "" and
4✔
308
       final_path != [] and
4✔
309
       String.starts_with?(hd(final_path), envs_path) do
4✔
310

311
      case File.rm_rf(hd(final_path)) do
4✔
312
        {:ok, _} -> :ok
4✔
313
        _ -> :error
×
314
      end
315
    else
316
      :error
317
    end
318
  end
319

320
  defp write(map) do
321
    deps = Enum.sort(map)
2✔
322
           |> Enum.map(fn a -> inspect a end)
2✔
323
           |> Enum.join(",\n")
324
    File.write!("deps.lock", "[" <> deps <> "]\n")
2✔
325
    :ok
326
  end
327

328
  defp read() do
329
    case active_env_name() do
3✔
330
      nil -> nil
×
331
      _env_name ->
332
        File.cd!(environment_path(), fn ->
3✔
333
          {deps, _bindings} = Code.eval_file("deps.lock")
3✔
334
          deps
3✔
335
        end)
336
    end
337
  end
338

339
  defp create_mix_exs_file(env_path) do
340
    File.write!(Path.join(env_path, "mix.exs"), """
4✔
341
                defmodule CustomEnvironment do
342
                        use Mix.Project
343

344
                        def project do
345
        [app: :customenv,
346
         version: "1.0.0",
347
         build_per_environment: false,
348
         deps: deps()]
349
                        end
350

351
                        def deps do
352
                                {deps, _bindings} = Code.eval_file("deps.lock")
353
                                deps
354
                        end
355
                end
356
                """)
357
  end
358

359
  defp create_deps_lock_file(env_path) do
360
    File.write!(Path.join(env_path, "deps.lock"), "[]")
4✔
361
  end
362

363
  defp purge(modules) do
364
    Enum.each(modules, fn module ->
48✔
365
      :code.purge(module)
48✔
366
      :code.delete(module)
48✔
367
    end)
368
  end
369
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc