Skip to content

Commit f66ae89

Browse files
authored
Report otp purls for System applications (#137)
1 parent e20d828 commit f66ae89

File tree

9 files changed

+305
-10
lines changed

9 files changed

+305
-10
lines changed

lib/mix_dependency_submission/application.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ defmodule MixDependencySubmission.Application do
1212
def start(_start_type, _start_args) do
1313
Mix.Hex.start()
1414

15+
Mix.SCM.delete(Hex.SCM)
16+
Mix.SCM.append(MixDependencySubmission.SCM.System)
17+
Mix.SCM.append(Hex.SCM)
18+
1519
if Burrito.Util.running_standalone?() do
1620
exit_code = Submit.run(Args.argv())
1721

lib/mix_dependency_submission/fetcher.ex

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,27 @@ defmodule MixDependencySubmission.Fetcher do
5555

5656
@manifest_fetchers [__MODULE__.MixFile, __MODULE__.MixLock, __MODULE__.MixRuntime]
5757

58+
@static_deps %{
59+
elixir: %{
60+
scm: MixDependencySubmission.SCM.System,
61+
mix_dep: {:elixir, nil, []},
62+
relationship: :direct,
63+
scope: :runtime
64+
},
65+
stdlib: %{
66+
scm: MixDependencySubmission.SCM.System,
67+
mix_dep: {:stdlib, nil, []},
68+
relationship: :direct,
69+
scope: :runtime
70+
},
71+
kernel: %{
72+
scm: MixDependencySubmission.SCM.System,
73+
mix_dep: {:kernel, nil, []},
74+
relationship: :direct,
75+
scope: :runtime
76+
}
77+
}
78+
5879
@doc """
5980
Fetches and merges dependencies from all registered fetchers.
6081
@@ -87,13 +108,26 @@ defmodule MixDependencySubmission.Fetcher do
87108
Map.merge(acc || %{}, dependencies, &merge/3)
88109
end)
89110
|> case do
90-
nil -> nil
91-
%{} = deps -> transform_all(deps)
111+
nil ->
112+
nil
113+
114+
%{} = deps ->
115+
deps = Map.merge(deps, @static_deps, &merge/3)
116+
117+
transform_all(deps)
92118
end
93119
end
94120

95121
@spec merge(app_name(), left :: dependency(), right :: dependency()) :: dependency()
96-
defp merge(_app, left, right), do: Map.merge(left, right)
122+
defp merge(_app, left, right), do: Map.merge(left, right, &merge_property/3)
123+
124+
@spec merge_property(key :: atom(), left :: value, right :: value) :: value when value: term()
125+
defp merge_property(key, left, right)
126+
defp merge_property(_key, value, value), do: value
127+
defp merge_property(:relationship, :direct, _right), do: :direct
128+
defp merge_property(:relationship, _left, :direct), do: :direct
129+
defp merge_property(:dependencies, left, right), do: Enum.uniq(left ++ right)
130+
defp merge_property(_key, _left, right), do: right
97131

98132
@spec transform_all(dependencies :: %{app_name() => dependency()}) :: %{
99133
String.t() => Dependency.t()

lib/mix_dependency_submission/fetcher/mix_file.ex

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,25 @@ defmodule MixDependencySubmission.Fetcher.MixFile do
3535
"""
3636
@impl Fetcher
3737
def fetch do
38-
Mix.Project.config()[:deps] |> List.wrap() |> Map.new(&normalize_dep/1)
38+
app_config = get_application_config()
39+
40+
[
41+
Mix.Project.config()[:deps] || [],
42+
[{:elixir, Mix.Project.config()[:elixir], []}],
43+
Enum.map(app_config[:applications] || [], &{&1, []}),
44+
Enum.map(app_config[:extra_applications] || [], &{&1, []}),
45+
Enum.map(app_config[:included_applications] || [], &{&1, included: true})
46+
]
47+
|> Enum.concat()
48+
|> Enum.uniq_by(&elem(&1, 0))
49+
|> Map.new(&normalize_dep/1)
50+
end
51+
52+
@spec get_application_config() :: Keyword.t()
53+
defp get_application_config do
54+
Mix.Project.get!().application()
55+
rescue
56+
UndefinedFunctionError -> []
3957
end
4058

4159
@spec normalize_dep(

lib/mix_dependency_submission/fetcher/mix_runtime.ex

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ defmodule MixDependencySubmission.Fetcher.MixRuntime do
2222
iex> %{
2323
...> burrito: %{
2424
...> scm: Hex.SCM,
25-
...> dependencies: [:jason, :req, :typed_struct],
25+
...> dependencies: [:jason, :req, :typed_struct, :kernel, :stdlib, :elixir, :logger, :eex],
2626
...> mix_config: _config,
2727
...> relationship: :direct,
2828
...> scope: :runtime,
@@ -35,11 +35,83 @@ defmodule MixDependencySubmission.Fetcher.MixRuntime do
3535
"""
3636
@impl Fetcher
3737
def fetch do
38-
root_deps = [depth: 1] |> Mix.Project.deps_tree() |> Map.keys()
39-
deps_paths = Mix.Project.deps_paths()
40-
deps_scms = Mix.Project.deps_scms()
38+
app = Mix.Project.config()[:app]
4139

42-
Map.new(Mix.Project.deps_tree(), &resolve_dep(&1, root_deps, deps_paths, deps_scms))
40+
root_deps =
41+
[depth: 1]
42+
|> Mix.Project.deps_tree()
43+
|> Map.keys()
44+
|> Enum.concat(get_app_dependencies(app, true))
45+
|> Enum.uniq()
46+
47+
deps_tree = full_runtime_tree(app)
48+
49+
deps_paths =
50+
deps_tree
51+
|> Map.keys()
52+
|> Enum.reduce(Mix.Project.deps_paths(), fn dep, deps_paths ->
53+
try do
54+
app_dir = Application.app_dir(dep)
55+
Map.put_new(deps_paths, dep, app_dir)
56+
rescue
57+
ArgumentError ->
58+
deps_paths
59+
end
60+
end)
61+
62+
deps_scms =
63+
deps_tree
64+
|> Map.keys()
65+
|> Enum.reduce(Mix.Project.deps_scms(), fn dep, deps_scms ->
66+
Map.put_new(deps_scms, dep, MixDependencySubmission.SCM.System)
67+
end)
68+
69+
Map.new(deps_tree, &resolve_dep(&1, root_deps, deps_paths, deps_scms))
70+
end
71+
72+
@spec full_runtime_tree(app :: Fetcher.app_name()) :: %{
73+
Fetcher.app_name() => [Fetcher.app_name()]
74+
}
75+
defp full_runtime_tree(app) do
76+
app_dependencies = app |> get_app_dependencies(true) |> Enum.map(&{&1, []})
77+
78+
Mix.Project.deps_tree()
79+
|> Enum.concat(app_dependencies)
80+
|> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
81+
|> Enum.flat_map(fn {app, dependencies} ->
82+
dependencies =
83+
[dependencies | get_app_dependencies(app, false)] |> List.flatten() |> Enum.uniq()
84+
85+
[{app, dependencies} | Enum.map(dependencies, &{&1, []})]
86+
end)
87+
|> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
88+
|> Map.new(fn {app, dependencies} ->
89+
{app, dependencies |> List.flatten() |> Enum.uniq()}
90+
end)
91+
end
92+
93+
@spec get_app_dependencies(app :: Fetcher.app_name(), root? :: boolean()) :: [
94+
Fetcher.app_name()
95+
]
96+
defp get_app_dependencies(app, root?)
97+
defp get_app_dependencies(nil, _root?), do: []
98+
99+
defp get_app_dependencies(app, root?) do
100+
case Application.spec(app) do
101+
nil ->
102+
[]
103+
104+
spec ->
105+
included = spec[:included_applications] || []
106+
applications = spec[:applications] || []
107+
optional = spec[:optional_applications] || []
108+
109+
if root? do
110+
Enum.uniq(included ++ applications ++ optional)
111+
else
112+
Enum.uniq(included ++ (applications -- optional))
113+
end
114+
end
43115
end
44116

45117
@spec resolve_dep(
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
defmodule MixDependencySubmission.SCM.System do
2+
@moduledoc """
3+
A `Mix.SCM` implementation that looks up the system dependencies.
4+
(Erlang, Elixir, Hex, etc.)
5+
"""
6+
7+
@behaviour Mix.SCM
8+
9+
@elixir_applications ~w[eex elixir ex_unit iex logger mix]a
10+
defguard is_elixir_app(app) when app in @elixir_applications
11+
12+
{:ok, dirs} = :file.list_dir_all(:code.lib_dir())
13+
14+
@erlang_applications Enum.map(dirs, fn dir ->
15+
[name, _version] = dir |> List.to_string() |> String.split("-", parts: 2)
16+
String.to_atom(name)
17+
end)
18+
19+
defguard is_erlang_app(app) when app in @erlang_applications
20+
21+
defguard is_hex_app(app) when app == :hex
22+
defguard is_system_app(app) when is_elixir_app(app) or is_erlang_app(app) or is_hex_app(app)
23+
24+
@impl Mix.SCM
25+
def accepts_options(app, opts)
26+
27+
def accepts_options(app, opts) when is_system_app(app),
28+
do: Keyword.merge(opts, app: app, build: Application.app_dir(app), dest: Application.app_dir(app))
29+
30+
def accepts_options(_app, _opts), do: nil
31+
32+
@impl Mix.SCM
33+
def fetchable?, do: false
34+
35+
@impl Mix.SCM
36+
def format(opts), do: opts[:app]
37+
38+
@impl Mix.SCM
39+
def format_lock(_opts), do: nil
40+
41+
@impl Mix.SCM
42+
def checked_out?(_opts), do: true
43+
44+
@impl Mix.SCM
45+
def lock_status(_opts), do: :ok
46+
47+
@impl Mix.SCM
48+
def equal?(opts1, opts2), do: opts1[:app] == opts2[:app]
49+
50+
@impl Mix.SCM
51+
def managers(_opts), do: []
52+
53+
@impl Mix.SCM
54+
@dialyzer {:no_return, checkout: 1}
55+
def checkout(_opts), do: Mix.raise("System SCM does not support checkout.")
56+
57+
@impl Mix.SCM
58+
@dialyzer {:no_return, update: 1}
59+
def update(_opts), do: Mix.raise("System SCM does not support update.")
60+
end
61+
62+
defmodule MixDependencySubmission.SCM.MixDependencySubmission.SCM.System do
63+
@moduledoc """
64+
`MixDependencySubmission.SCM` implementation for system dependencies.
65+
"""
66+
67+
@behaviour MixDependencySubmission.SCM
68+
69+
import MixDependencySubmission.SCM.System,
70+
only: [is_elixir_app: 1, is_erlang_app: 1, is_hex_app: 1]
71+
72+
@impl MixDependencySubmission.SCM
73+
def mix_dep_to_purl(app, version)
74+
75+
def mix_dep_to_purl({app, _version_requirement, _opts}, _version) when is_elixir_app(app) do
76+
Purl.new!(%Purl{
77+
type: "generic",
78+
# TODO: Use once the spec is merged
79+
# type: "otp",
80+
name: to_string(app),
81+
subpath: ["lib", to_string(app)],
82+
qualifiers: %{"vcs_url" => "git+https://github.com/elixir-lang/elixir.git"}
83+
})
84+
end
85+
86+
def mix_dep_to_purl({app, _version_requirement, _opts}, _version) when is_erlang_app(app) do
87+
Purl.new!(%Purl{
88+
type: "generic",
89+
# TODO: Use once the spec is merged
90+
# type: "otp",
91+
name: to_string(app),
92+
subpath: ["lib", to_string(app)],
93+
qualifiers: %{"vcs_url" => "git+https://github.com/erlang/otp.git"}
94+
})
95+
end
96+
97+
def mix_dep_to_purl({app, _version_requirement, _opts}, _version) when is_hex_app(app) do
98+
Purl.new!(%Purl{
99+
type: "generic",
100+
# TODO: Use once the spec is merged
101+
# type: "otp",
102+
name: to_string(app),
103+
qualifiers: %{"vcs_url" => "git+https://github.com/hexpm/hex.git"}
104+
})
105+
end
106+
end

test/fixtures/app_locked/mix.exs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule AppNameToReplace.MixProject do
55
[
66
app: :app_name_to_replace,
77
version: "0.0.0-dev",
8+
elixir: "1.18.4",
89
deps: [
910
{:credo, "~> 1.7"},
1011
{:mime, "~> 2.0"},
@@ -14,4 +15,10 @@ defmodule AppNameToReplace.MixProject do
1415
]
1516
]
1617
end
18+
19+
def application do
20+
[
21+
extra_applications: [:logger, :public_key]
22+
]
23+
end
1724
end

test/mix_dependency_submission/fetcher/mix_file_test.exs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,24 @@ defmodule MixDependencySubmission.Fetcher.MixFileTest do
5555
depth: 1
5656
]},
5757
relationship: :direct
58+
},
59+
elixir: %{
60+
scope: :runtime,
61+
scm: MixDependencySubmission.SCM.System,
62+
mix_dep: {:elixir, "1.18.4", [app: :elixir, build: _elixir_build, dest: _elixir_dest]},
63+
relationship: :direct
64+
},
65+
logger: %{
66+
scope: :runtime,
67+
scm: MixDependencySubmission.SCM.System,
68+
mix_dep: {:logger, nil, [app: :logger, build: _logger_build, dest: _logger_dest]},
69+
relationship: :direct
70+
},
71+
public_key: %{
72+
scope: :runtime,
73+
scm: MixDependencySubmission.SCM.System,
74+
mix_dep: {:public_key, nil, [app: :public_key, build: _public_key_build, dest: _public_key_dest]},
75+
relationship: :direct
5876
}
5977
} = MixFile.fetch()
6078
end)

test/mix_dependency_submission/fetcher/mix_runtime_test.exs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,16 @@ defmodule MixDependencySubmission.Fetcher.MixRuntimeTest do
5858
version: nil,
5959
mix_config: [],
6060
scm: Hex.SCM,
61-
dependencies: [],
61+
dependencies: [:kernel, :stdlib, :elixir, :logger],
6262
relationship: :direct
63+
},
64+
logger: %{
65+
scope: :runtime,
66+
version: nil,
67+
mix_config: [{:app, :logger} | _logger_rest],
68+
scm: MixDependencySubmission.SCM.System,
69+
dependencies: [],
70+
relationship: :indirect
6371
}
6472
} = MixRuntime.fetch()
6573
end)

test/mix_dependency_submission_test.exs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,34 @@ defmodule MixDependencySubmissionTest do
199199
%Purl{type: "hex", name: "file_system", version: "0.2.10"},
200200
%Purl{type: "hex", name: "jason", version: "1.4.0"}
201201
]
202+
},
203+
"elixir" => %Dependency{
204+
scope: :runtime,
205+
metadata: %{},
206+
dependencies: [],
207+
relationship: :direct,
208+
package_url: %Purl{
209+
type: "generic",
210+
# TODO: Use once the spec is merged
211+
# type: "otp",
212+
name: "elixir",
213+
qualifiers: %{"vcs_url" => "git+https://github.com/elixir-lang/elixir.git"},
214+
subpath: ["lib", "elixir"]
215+
}
216+
},
217+
"stdlib" => %Dependency{
218+
scope: :runtime,
219+
metadata: %{},
220+
dependencies: [],
221+
relationship: :direct,
222+
package_url: %Purl{
223+
type: "generic",
224+
# TODO: Use once the spec is merged
225+
# type: "otp",
226+
name: "stdlib",
227+
qualifiers: %{"vcs_url" => "git+https://github.com/erlang/otp.git"},
228+
subpath: ["lib", "stdlib"]
229+
}
202230
}
203231
} = resolved
204232
end

0 commit comments

Comments
 (0)