dependabot-hex 0.88.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/helpers/build +19 -0
  3. data/helpers/deps/jason/.fetch +0 -0
  4. data/helpers/deps/jason/.hex +2 -0
  5. data/helpers/deps/jason/CHANGELOG.md +60 -0
  6. data/helpers/deps/jason/LICENSE +13 -0
  7. data/helpers/deps/jason/README.md +179 -0
  8. data/helpers/deps/jason/hex_metadata.config +20 -0
  9. data/helpers/deps/jason/lib/codegen.ex +158 -0
  10. data/helpers/deps/jason/lib/decoder.ex +657 -0
  11. data/helpers/deps/jason/lib/encode.ex +630 -0
  12. data/helpers/deps/jason/lib/encoder.ex +216 -0
  13. data/helpers/deps/jason/lib/formatter.ex +253 -0
  14. data/helpers/deps/jason/lib/fragment.ex +11 -0
  15. data/helpers/deps/jason/lib/helpers.ex +90 -0
  16. data/helpers/deps/jason/lib/jason.ex +228 -0
  17. data/helpers/deps/jason/mix.exs +92 -0
  18. data/helpers/lib/check_update.exs +92 -0
  19. data/helpers/lib/do_update.exs +39 -0
  20. data/helpers/lib/parse_deps.exs +103 -0
  21. data/helpers/lib/run.exs +76 -0
  22. data/helpers/mix.exs +21 -0
  23. data/helpers/mix.lock +3 -0
  24. data/lib/dependabot/hex.rb +11 -0
  25. data/lib/dependabot/hex/file_fetcher.rb +79 -0
  26. data/lib/dependabot/hex/file_parser.rb +125 -0
  27. data/lib/dependabot/hex/file_updater.rb +71 -0
  28. data/lib/dependabot/hex/file_updater/lockfile_updater.rb +142 -0
  29. data/lib/dependabot/hex/file_updater/mixfile_git_pin_updater.rb +51 -0
  30. data/lib/dependabot/hex/file_updater/mixfile_requirement_updater.rb +72 -0
  31. data/lib/dependabot/hex/file_updater/mixfile_sanitizer.rb +26 -0
  32. data/lib/dependabot/hex/file_updater/mixfile_updater.rb +94 -0
  33. data/lib/dependabot/hex/metadata_finder.rb +70 -0
  34. data/lib/dependabot/hex/native_helpers.rb +20 -0
  35. data/lib/dependabot/hex/requirement.rb +53 -0
  36. data/lib/dependabot/hex/update_checker.rb +275 -0
  37. data/lib/dependabot/hex/update_checker/file_preparer.rb +191 -0
  38. data/lib/dependabot/hex/update_checker/requirements_updater.rb +173 -0
  39. data/lib/dependabot/hex/update_checker/version_resolver.rb +170 -0
  40. data/lib/dependabot/hex/version.rb +67 -0
  41. metadata +208 -0
@@ -0,0 +1,39 @@
1
+ [dependency_name | credentials] = System.argv()
2
+
3
+ grouped_creds = Enum.reduce credentials, [], fn cred, acc ->
4
+ if List.last(acc) == nil || List.last(acc)[:token] do
5
+ List.insert_at(acc, -1, %{ organization: cred })
6
+ else
7
+ { item, acc } = List.pop_at(acc, -1)
8
+ item = Map.put(item, :token, cred)
9
+ List.insert_at(acc, -1, item)
10
+ end
11
+ end
12
+
13
+ Enum.each grouped_creds, fn cred ->
14
+ hexpm = Hex.Repo.get_repo("hexpm")
15
+ repo = %{
16
+ url: hexpm.url <> "/repos/#{cred.organization}",
17
+ public_key: nil,
18
+ auth_key: cred.token
19
+ }
20
+
21
+ Hex.Config.read()
22
+ |> Hex.Config.read_repos()
23
+ |> Map.put("hexpm:#{cred.organization}", repo)
24
+ |> Hex.Config.update_repos()
25
+ end
26
+
27
+ # dependency atom
28
+ dependency = String.to_atom(dependency_name)
29
+
30
+ # Fetch dependencies that needs updating
31
+ {dependency_lock, rest_lock} = Map.split(Mix.Dep.Lock.read(), [dependency])
32
+ Mix.Dep.Fetcher.by_name([dependency_name], dependency_lock, rest_lock, [])
33
+
34
+ lockfile_content =
35
+ "mix.lock"
36
+ |> File.read()
37
+ |> :erlang.term_to_binary()
38
+
39
+ IO.write(:stdio, lockfile_content)
@@ -0,0 +1,103 @@
1
+ defmodule Parser do
2
+ def run do
3
+ Mix.Dep.load_on_environment([])
4
+ |> Enum.flat_map(&parse_dep/1)
5
+ |> Enum.map(&build_dependency(&1.opts[:lock], &1))
6
+ end
7
+
8
+ defp build_dependency(nil, dep) do
9
+ %{
10
+ name: dep.app,
11
+ from: Path.relative_to_cwd(dep.from),
12
+ groups: [],
13
+ requirement: normalise_requirement(dep.requirement),
14
+ top_level: dep.top_level || umbrella_top_level_dep?(dep)
15
+ }
16
+ end
17
+
18
+ defp build_dependency(lock, dep) do
19
+ {version, checksum, source} = parse_lock(lock)
20
+ groups = parse_groups(dep.opts[:only])
21
+
22
+ %{
23
+ name: dep.app,
24
+ from: Path.relative_to_cwd(dep.from),
25
+ version: version,
26
+ groups: groups,
27
+ checksum: checksum,
28
+ requirement: normalise_requirement(dep.requirement),
29
+ source: source,
30
+ top_level: dep.top_level || umbrella_top_level_dep?(dep)
31
+ }
32
+ end
33
+
34
+ defp parse_groups(nil), do: []
35
+ defp parse_groups(only) when is_list(only), do: only
36
+ defp parse_groups(only), do: [only]
37
+
38
+ # path dependency
39
+ defp parse_dep(%{scm: Mix.SCM.Path, opts: opts} = dep) do
40
+ cond do
41
+ # umbrella dependency - ignore
42
+ opts[:in_umbrella] ->
43
+ []
44
+
45
+ # umbrella application
46
+ opts[:from_umbrella] ->
47
+ Enum.reject(dep.deps, fn dep -> dep.opts[:in_umbrella] end)
48
+
49
+ true ->
50
+ []
51
+ end
52
+ end
53
+
54
+ # hex, git dependency
55
+ defp parse_dep(%{scm: scm} = dep) when scm in [Hex.SCM, Mix.SCM.Git], do: [dep]
56
+
57
+ # unsupported
58
+ defp parse_dep(_dep), do: []
59
+
60
+ defp umbrella_top_level_dep?(dep) do
61
+ if Mix.Project.umbrella?() do
62
+ apps_paths = Path.expand(Mix.Project.config()[:apps_path], File.cwd!())
63
+ String.contains?(Path.dirname(Path.dirname(dep.from)), apps_paths)
64
+ else
65
+ false
66
+ end
67
+ end
68
+
69
+ defp parse_lock({:git, repo_url, checksum, opts}),
70
+ do: {nil, checksum, git_source(repo_url, opts)}
71
+
72
+ defp parse_lock({:hex, _app, version, checksum, _managers, _dependencies, _source}),
73
+ do: {version, checksum, nil}
74
+
75
+ defp parse_lock({:hex, _app, version, checksum, _managers, _dependencies}),
76
+ do: {version, checksum, nil}
77
+
78
+ defp normalise_requirement(req) do
79
+ req
80
+ |> maybe_regex_to_str()
81
+ |> empty_str_to_nil()
82
+ end
83
+
84
+ defp maybe_regex_to_str(s), do: if Regex.regex?(s), do: Regex.source(s), else: s
85
+ defp empty_str_to_nil(""), do: nil
86
+ defp empty_str_to_nil(s), do: s
87
+
88
+ def git_source(repo_url, opts) do
89
+ ref = opts[:ref] || opts[:tag]
90
+ ref = if is_list(ref), do: to_string(ref), else: ref
91
+
92
+ %{
93
+ type: "git",
94
+ url: repo_url,
95
+ branch: opts[:branch] || "master",
96
+ ref: ref
97
+ }
98
+ end
99
+ end
100
+
101
+ dependencies = :erlang.term_to_binary({:ok, Parser.run()})
102
+
103
+ IO.write(:stdio, dependencies)
@@ -0,0 +1,76 @@
1
+ defmodule DependencyHelper do
2
+ def main() do
3
+ IO.read(:stdio, :all)
4
+ |> Jason.decode!()
5
+ |> run()
6
+ |> case do
7
+ {output, 0} ->
8
+ if output =~ "No authenticated organization found" do
9
+ {:error, output}
10
+ else
11
+ {:ok, :erlang.binary_to_term(output)}
12
+ end
13
+
14
+ {error, 1} -> {:error, error}
15
+ end
16
+ |> handle_result()
17
+ end
18
+
19
+ defp handle_result({:ok, {:ok, result}}) do
20
+ encode_and_write(%{"result" => result})
21
+ end
22
+
23
+ defp handle_result({:ok, {:error, reason}}) do
24
+ encode_and_write(%{"error" => reason})
25
+ System.halt(1)
26
+ end
27
+
28
+ defp handle_result({:error, reason}) do
29
+ encode_and_write(%{"error" => reason})
30
+ System.halt(1)
31
+ end
32
+
33
+ defp encode_and_write(content) do
34
+ content
35
+ |> Jason.encode!()
36
+ |> IO.write()
37
+ end
38
+
39
+ defp run(%{"function" => "parse", "args" => [dir]}) do
40
+ run_script("parse_deps.exs", dir)
41
+ end
42
+
43
+ defp run(%{"function" => "get_latest_resolvable_version", "args" => [dir, dependency_name, credentials]}) do
44
+ run_script("check_update.exs", dir, [dependency_name] ++ credentials)
45
+ end
46
+
47
+ defp run(%{"function" => "get_updated_lockfile", "args" => [dir, dependency_name, credentials]}) do
48
+ run_script("do_update.exs", dir, [dependency_name] ++ credentials)
49
+ end
50
+
51
+ defp run_script(script, dir, args \\ []) do
52
+ args = [
53
+ "run",
54
+ "--no-deps-check",
55
+ "--no-start",
56
+ "--no-compile",
57
+ "--no-elixir-version-check",
58
+ script
59
+ ] ++ args
60
+
61
+ System.cmd(
62
+ "mix",
63
+ args,
64
+ [
65
+ cd: dir,
66
+ env: %{
67
+ "MIX_EXS" => nil,
68
+ "MIX_LOCK" => nil,
69
+ "MIX_DEPS" => nil
70
+ }
71
+ ]
72
+ )
73
+ end
74
+ end
75
+
76
+ DependencyHelper.main()
@@ -0,0 +1,21 @@
1
+ defmodule DependabotCore.Mixfile do
2
+ use Mix.Project
3
+
4
+ def project do
5
+ [app: :dependabot_core,
6
+ version: "0.1.0",
7
+ elixir: "~> 1.5",
8
+ start_permanent: Mix.env == :prod,
9
+ lockfile: System.get_env("MIX_LOCK") || "mix.lock",
10
+ deps_path: System.get_env("MIX_DEPS") || "deps",
11
+ deps: deps()]
12
+ end
13
+
14
+ def application do
15
+ [extra_applications: [:logger]]
16
+ end
17
+
18
+ defp deps() do
19
+ [{:jason, "~> 1.0-rc"}]
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ %{
2
+ "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
3
+ }
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # These all need to be required so the various classes can be registered in a
4
+ # lookup table of package manager names to concrete classes.
5
+ require "dependabot/hex/file_fetcher"
6
+ require "dependabot/hex/file_parser"
7
+ require "dependabot/hex/update_checker"
8
+ require "dependabot/hex/file_updater"
9
+ require "dependabot/hex/metadata_finder"
10
+ require "dependabot/hex/requirement"
11
+ require "dependabot/hex/version"
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/file_fetchers"
4
+ require "dependabot/file_fetchers/base"
5
+
6
+ module Dependabot
7
+ module Hex
8
+ class FileFetcher < Dependabot::FileFetchers::Base
9
+ APPS_PATH_REGEX = /apps_path:\s*"(?<path>.*?)"/m.freeze
10
+ STRING_ARG = %{(?:["'](.*?)["'])}
11
+ EVAL_FILE = /Code\.eval_file\(#{STRING_ARG}(?:\s*,\s*#{STRING_ARG})?\)/.
12
+ freeze
13
+
14
+ def self.required_files_in?(filenames)
15
+ filenames.include?("mix.exs")
16
+ end
17
+
18
+ def self.required_files_message
19
+ "Repo must contain a mix.exs."
20
+ end
21
+
22
+ private
23
+
24
+ def fetch_files
25
+ fetched_files = []
26
+ fetched_files << mixfile
27
+ fetched_files << lockfile if lockfile
28
+ fetched_files += subapp_mixfiles
29
+ fetched_files += evaled_files
30
+ fetched_files
31
+ end
32
+
33
+ def mixfile
34
+ @mixfile ||= fetch_file_from_host("mix.exs")
35
+ end
36
+
37
+ def lockfile
38
+ return @lockfile if @lockfile_lookup_attempted
39
+
40
+ @lockfile_lookup_attempted = true
41
+ @lockfile ||= fetch_file_from_host("mix.lock")
42
+ rescue Dependabot::DependencyFileNotFound
43
+ nil
44
+ end
45
+
46
+ def subapp_mixfiles
47
+ apps_path = mixfile.content.match(APPS_PATH_REGEX)&.
48
+ named_captures&.fetch("path")
49
+ return [] unless apps_path
50
+
51
+ app_directories = repo_contents(dir: apps_path).
52
+ select { |f| f.type == "dir" }
53
+
54
+ app_directories.map do |dir|
55
+ fetch_file_from_host("#{dir.path}/mix.exs")
56
+ rescue Dependabot::DependencyFileNotFound
57
+ # If the folder doesn't have a mix.exs it *might* be because it's
58
+ # not an app. Ignore the fact we couldn't fetch one and proceed with
59
+ # updating (it will blow up later if there are problems)
60
+ nil
61
+ end.compact
62
+ rescue Octokit::NotFound, Gitlab::Error::NotFound
63
+ # If the path specified in apps_path doesn't exist then it's not being
64
+ # used. We can just return an empty array of subapp files.
65
+ []
66
+ end
67
+
68
+ def evaled_files
69
+ mixfile.content.scan(EVAL_FILE).map do |eval_file_args|
70
+ path = Pathname.new(File.join(*eval_file_args.reverse)).
71
+ cleanpath.to_path
72
+ fetch_file_from_host(path).tap { |f| f.support_file = true }
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ Dependabot::FileFetchers.register("hex", Dependabot::Hex::FileFetcher)
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/dependency"
4
+ require "dependabot/file_parsers"
5
+ require "dependabot/file_parsers/base"
6
+ require "dependabot/hex/native_helpers"
7
+ require "dependabot/shared_helpers"
8
+ require "dependabot/errors"
9
+
10
+ # For docs, see https://hexdocs.pm/mix/Mix.Tasks.Deps.html
11
+ module Dependabot
12
+ module Hex
13
+ class FileParser < Dependabot::FileParsers::Base
14
+ require "dependabot/file_parsers/base/dependency_set"
15
+
16
+ def parse
17
+ dependency_set = DependencySet.new
18
+
19
+ dependency_details.each do |dep|
20
+ git_dependency = dep["source"]&.fetch("type") == "git"
21
+
22
+ dependency_set <<
23
+ Dependency.new(
24
+ name: dep["name"],
25
+ version: git_dependency ? dep["checksum"] : dep["version"],
26
+ requirements: [{
27
+ requirement: dep["requirement"],
28
+ groups: dep["groups"],
29
+ source: dep["source"] && symbolize_keys(dep["source"]),
30
+ file: dep["from"]
31
+ }],
32
+ package_manager: "hex"
33
+ )
34
+ end
35
+
36
+ dependency_set.dependencies.sort_by(&:name)
37
+ end
38
+
39
+ private
40
+
41
+ def dependency_details
42
+ SharedHelpers.in_a_temporary_directory do
43
+ write_sanitized_mixfiles
44
+ write_supporting_files
45
+ File.write("mix.lock", lockfile.content) if lockfile
46
+ FileUtils.cp(elixir_helper_parse_deps_path, "parse_deps.exs")
47
+
48
+ SharedHelpers.run_helper_subprocess(
49
+ env: mix_env,
50
+ command: "mix run #{elixir_helper_path}",
51
+ function: "parse",
52
+ args: [Dir.pwd],
53
+ popen_opts: { err: %i(child out) }
54
+ )
55
+ end
56
+ rescue Dependabot::SharedHelpers::HelperSubprocessFailed => error
57
+ result_json =
58
+ error.message.lines.
59
+ drop_while { |l| !l.start_with?('{"result":') }.
60
+ join
61
+
62
+ raise DependencyFileNotEvaluatable, error.message if result_json.empty?
63
+
64
+ JSON.parse(result_json).fetch("result")
65
+ end
66
+
67
+ def write_sanitized_mixfiles
68
+ mixfiles.each do |file|
69
+ path = file.name
70
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
71
+ File.write(path, sanitize_mixfile(file.content))
72
+ end
73
+ end
74
+
75
+ def write_supporting_files
76
+ dependency_files.select(&:support_file).each do |file|
77
+ path = file.name
78
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
79
+ File.write(path, file.content)
80
+ end
81
+ end
82
+
83
+ def sanitize_mixfile(content)
84
+ content.
85
+ gsub(/File\.read!\(.*?\)/, '"0.0.1"').
86
+ gsub(/File\.read\(.*?\)/, '{:ok, "0.0.1"}')
87
+ end
88
+
89
+ def mix_env
90
+ {
91
+ "MIX_EXS" => File.join(NativeHelpers.hex_helpers_dir, "mix.exs"),
92
+ "MIX_LOCK" => File.join(NativeHelpers.hex_helpers_dir, "mix.lock"),
93
+ "MIX_DEPS" => File.join(NativeHelpers.hex_helpers_dir, "deps"),
94
+ "MIX_QUIET" => "1"
95
+ }
96
+ end
97
+
98
+ def elixir_helper_path
99
+ File.join(NativeHelpers.hex_helpers_dir, "lib/run.exs")
100
+ end
101
+
102
+ def elixir_helper_parse_deps_path
103
+ File.join(NativeHelpers.hex_helpers_dir, "lib/parse_deps.exs")
104
+ end
105
+
106
+ def check_required_files
107
+ raise "No mixfile!" if mixfiles.none?
108
+ end
109
+
110
+ def symbolize_keys(hash)
111
+ Hash[hash.keys.map { |k| [k.to_sym, hash[k]] }]
112
+ end
113
+
114
+ def mixfiles
115
+ dependency_files.select { |f| f.name.end_with?("mix.exs") }
116
+ end
117
+
118
+ def lockfile
119
+ @lockfile ||= get_original_file("mix.lock")
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ Dependabot::FileParsers.register("hex", Dependabot::Hex::FileParser)