Skip to content
This repository has been archived by the owner on Aug 1, 2021. It is now read-only.

add links for depedencies, extra licenses, and output as file #20

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,24 @@ Run `mix licenses` to get the list of packages and their licenses:

```shell
$ mix licenses
+---------------------+---------+--------------------------------------------------------+
| Package | Version | License |
+---------------------+---------+--------------------------------------------------------+
| certifi | | BSD |
| earmark | 1.3.2 | Apache 2.0 |
| ex_doc | 0.20.2 | Apache 2.0 |
| excoveralls | | Unsure (found: MIT, Unrecognized license file content) |
| hackney | | Apache 2.0 |
| idna | | Unsure (found: BSD, MIT) |
| jason | | Apache 2.0 |
| makeup | 0.8.0 | Unsure (found: BSD, Unrecognized license file content) |
| makeup_elixir | 0.13.0 | BSD |
| metrics | | BSD |
| mimerl | | MIT |
| nimble_parsec | 0.5.0 | Apache 2.0 |
| ssl_verify_fun | | MIT |
| table_rex | 2.0.0 | MIT |
| unicode_util_compat | | Unsure (found: Apache 2.0, BSD) |
+---------------------+---------+--------------------------------------------------------+
Notice: This is not a legal advice. Use the information below at your own risk.
| Package | License | Version | Link |
|---------------------|---------------------------|---------|---------------------------------------------|
| certifi | BSD | | https://hex.pm/packages/certifi |
| earmark | Apache 2.0 | 1.3.2 | https://hex.pm/packages/earmark |
| ex_doc | Apache 2.0 | 0.20.2 | https://hex.pm/packages/ex_doc |
| excoveralls | MIT | | https://hex.pm/packages/excoveralls |
| hackney | Apache 2.0 | | https://hex.pm/packages/hackney |
| idna | BSD; MIT | | https://hex.pm/packages/idna |
| jason | Apache 2.0 | | https://hex.pm/packages/jason |
| makeup | BSD; Unrecognized license | 0.8.0 | https://hex.pm/packages/makeup |
| makeup_elixir | BSD | 0.13.0 | https://hex.pm/packages/makeup_elixir |
| metrics | BSD | | https://hex.pm/packages/metrics |
| mimerl | MIT | | https://hex.pm/packages/mimerl |
| nimble_parsec | Apache 2.0 | 0.5.0 | https://hex.pm/packages/nimble_parsec |
| ssl_verify_fun | MIT | | https://hex.pm/packages/ssl_verify_fun |
| unicode_util_compat | Apache 2.0; BSD | | https://hex.pm/packages/unicode_util_compat |
|---------------------|---------------------------|---------|---------------------------------------------|
```

Run `mix licenses --csv` to output in csv format:
Expand All @@ -74,7 +73,8 @@ unicode_util_compat,,"Unsure (found: Apache 2.0, BSD)"
```

### Flags
* `--top-level-only` - Only fetch license information from top level dependencies (e.g. packages that are directly listed in your application's `mix.exs`). Excludes transitive dependencies.

- `--top-level-only` - Only fetch license information from top level dependencies (e.g. packages that are directly listed in your application's `mix.exs`). Excludes transitive dependencies.

## Usage as a library

Expand Down
46 changes: 38 additions & 8 deletions lib/licensir/file_analyzer.ex
Original file line number Diff line number Diff line change
@@ -1,52 +1,82 @@
defmodule Licensir.FileAnalyzer do
# The file names to check for licenses
@license_files ["LICENSE", "LICENSE.md", "LICENSE.txt"]
@license_files ["LICENSE", "LICENSE.md", "LICENSE.txt", "LICENCE"]

# The files that contain the actual text for each license
@files [
apache2: ["Apache2_text.txt", "Apache2_text.variant-2.txt", "Apache2_url.txt"],
agpl_v3: ["AGPLv3.txt"],
apache2: [
"Apache2_text.txt",
"Apache2_text.variant-2.txt",
"Apache2_text.variant-3.txt",
"Apache2_url.txt"
],
bsd: ["BSD-3.txt", "BSD-3.variant-2.txt"],
cc0: ["CC0-1.0.txt"],
gpl_v2: ["GPLv2.txt"],
gpl_v3: ["GPLv3.txt"],
isc: ["ISC.txt", "ISC.variant-2.txt"],
lgpl: ["LGPL.txt"],
mit: ["MIT.txt", "MIT.variant-2.txt", "MIT.variant-3.txt"],
mpl2: ["MPL2.txt"],
mpl2: ["MPL2.txt", "MPL2b.txt"],
licensir_mock_license: ["LicensirMockLicense.txt"]
]

def analyze(dir_path) do
# IO.inspect(analyze_dir: dir_path)
Enum.find_value(@license_files, fn file_name ->
dir_path
|> Path.join(file_name)
|> File.read()
|> case do
{:ok, content} -> analyze_content(content)
{:ok, content} ->
# IO.inspect(analyze: file_name)
analyze_content(content)
#|> IO.inspect
{:error, _} -> nil
end
end)
end

# Returns the first license that matches
defp analyze_content(content) do
content = clean(content)
# IO.inspect(content: content)

Enum.find_value(@files, fn {license, license_files} ->
found =
Enum.find(license_files, fn license_file ->
license =
license_text =
:licensir
|> :code.priv_dir()
|> Path.join("licenses")
|> Path.join(license_file)
|> File.read!()
|> clean()

# IO.inspect(license: license)
mayel marked this conversation as resolved.
Show resolved Hide resolved
# IO.inspect(license_file: license_file)

# Returns true only if the content is a superset of the license text
clean(content) =~ clean(license)
content =~ license_text
end)

# IO.inspect(found: found)

if found, do: license, else: nil
end) || :unrecognized_license_file
end) || unrecognised(content)
end

defp unrecognised(_content) do
# IO.inspect(unrecognised_license: content)
:unrecognized_license_file
end

defp clean(content), do: String.replace(content, ~r/\v/, "")
def clean(content),
do:
content
|> String.replace("\n", " ")
|> String.replace(~r/\s\s+/, " ")
|> String.trim()
|> String.downcase()
end
6 changes: 4 additions & 2 deletions lib/licensir/guesser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ defmodule Licensir.Guesser do
Map.put(license, :license, conclusion)
end

defp guess(file, ""), do: guess(file, nil)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remember correctly, there shouldn't be any case where the 2nd argument is an empty string. So I think if it falls into this case we should fix the root cause than handling it here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a case of a dependency with no license which results in an empty license column in the final rather than "Undefined" if this line is present.

defp guess(nil, nil), do: "Undefined"
defp guess(nil, file), do: file
defp guess(hex, nil) when length(hex) > 0, do: Enum.join(hex, ", ")
defp guess(hex, nil) when length(hex) > 0, do: Enum.join(hex, "; ")
defp guess(hex, file) when length(hex) == 1 and hd(hex) == file, do: file

defp guess(hex, file) do
"Unsure (found: " <> Enum.join(hex, ", ") <> ", " <> file <> ")"
# IO.inspect(file: file)
Enum.join(hex, "; ") <> "; " <> file
end
end
2 changes: 2 additions & 0 deletions lib/licensir/license.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule Licensir.License do
name: "",
version: nil,
dep: nil,
link: nil,
license: nil,
certainty: 0.0,
mix: nil,
Expand All @@ -30,6 +31,7 @@ defmodule Licensir.License do
version: String.t() | nil,
dep: Mix.Dep.t(),
license: String.t() | nil,
link: String.t() | nil,
certainty: float(),
mix: list(String.t()) | nil,
hex_metadata: list(String.t()) | nil,
Expand Down
27 changes: 23 additions & 4 deletions lib/licensir/scanner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Licensir.Scanner do
alias Licensir.{License, FileAnalyzer, Guesser}

@human_names %{
agpl_v3: "AGPL v3",
apache2: "Apache 2",
bsd: "BSD",
cc0: "CC0-1.0",
Expand All @@ -15,7 +16,7 @@ defmodule Licensir.Scanner do
mit: "MIT",
mpl2: "MPL2",
licensir_mock_license: "Licensir Mock License",
unrecognized_license_file: "Unrecognized license file content"
unrecognized_license_file: "Unrecognized license"
}

@doc """
Expand All @@ -31,6 +32,7 @@ defmodule Licensir.Scanner do
|> filter_top_level(opts)
|> search_hex_metadata()
|> search_file()
# |> IO.inspect()
|> Guesser.guess()
end

Expand All @@ -51,33 +53,46 @@ defmodule Licensir.Scanner do
defp to_struct(deps) when is_list(deps), do: Enum.map(deps, &to_struct/1)

defp to_struct(%Mix.Dep{} = dep) do
# IO.inspect(dep)

%License{
app: dep.app,
name: Atom.to_string(dep.app),
version: get_version(dep),
link: get_link(dep.opts),
dep: dep
}
end

defp filter_top_level(deps, opts) do
if Keyword.get(opts, :top_level_only) do
Enum.filter(deps, &(&1.dep.top_level))
Enum.filter(deps, & &1.dep.top_level)
mayel marked this conversation as resolved.
Show resolved Hide resolved
else
deps
end
end

defp get_version(%Mix.Dep{status: {:ok, version}}), do: version
defp get_version(%Mix.Dep{requirement: version}), do: version
defp get_version(_), do: nil

defp get_link(%{opts: opts}) when is_list(opts), do: get_link(Enum.into(opts, %{}))
defp get_link(opts) when is_list(opts), do: get_link(Enum.into(opts, %{}))
defp get_link(%{git: url}), do: url
defp get_link(%{hex: hex}), do: "https://hex.pm/packages/#{hex}"
defp get_link(%{lock: {:git, url, _, _}}), do: url
defp get_link(_), do: nil

#
# Search in hex_metadata.config
#

defp search_hex_metadata(licenses) when is_list(licenses), do: Enum.map(licenses, &search_hex_metadata/1)
defp search_hex_metadata(licenses) when is_list(licenses),
do: Enum.map(licenses, &search_hex_metadata/1)

defp search_hex_metadata(%License{} = license) do
Map.put(license, :hex_metadata, search_hex_metadata(license.dep))
# IO.inspect(license)
end

defp search_hex_metadata(%Mix.Dep{} = dep) do
Expand Down Expand Up @@ -107,6 +122,8 @@ defmodule Licensir.Scanner do
end

defp search_file(%Mix.Dep{} = dep) do
# IO.inspect(search_file: dep)

license_atom =
Mix.Dep.in_dependency(dep, fn _ ->
case File.cwd() do
Expand All @@ -115,6 +132,8 @@ defmodule Licensir.Scanner do
end
end)

Map.get(@human_names, license_atom)
# IO.inspect(license_atom: license_atom)

Map.get(@human_names, license_atom, to_string(license_atom))
mayel marked this conversation as resolved.
Show resolved Hide resolved
end
end
76 changes: 62 additions & 14 deletions lib/mix/tasks/licenses.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,93 @@ defmodule Mix.Tasks.Licenses do
"""
use Mix.Task

@output_file "DEPENDENCIES.md"
@shortdoc "Lists each dependency's licenses"
@recursive true
@switches [
top_level_only: :boolean,
csv: :boolean
csv: :boolean,
only_license: :boolean
]

def run(argv) do
{opts, _argv} = OptionParser.parse!(argv, switches: @switches)

Licensir.Scanner.scan(opts)
|> Enum.sort_by(fn license -> license.name end)
|> Enum.map(&to_row/1)
|> Enum.sort_by(fn lib -> lib.name end)
|> Enum.map(&to_row(&1, opts))
|> render(opts)
end

defp to_row(map) do
[map.name, map.version, map.license]
defp to_row(map, opts) do
if Keyword.get(opts, :only_license),
do: [map.name, map.license],
else: [map.name, map.license, map.version, map.link]
end

defp render(rows, opts) do
if Keyword.get(opts, :only_license),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the short hand if: ..., do: ..., else: ... are discouraged for multi-line conditionals per the popular code style.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are single lines though

do: render(rows, opts, ["Package", "License"]),
else: render(rows, opts, ["Package", "License", "Version", "Link"])
end

defp render(rows, opts, headers) do
cond do
Keyword.get(opts, :csv) -> render_csv(rows)
true -> render_ascii_table(rows)
Keyword.get(opts, :csv) ->
render_csv(rows, headers)

true ->
render_ascii_table(rows, headers)
end
end

defp render_ascii_table(rows) do
_ = Mix.Shell.IO.info([:yellow, "Notice: This is not a legal advice. Use the information below at your own risk."])
defp render_ascii_table(rows, headers) do
_ =
Mix.Shell.IO.info([
:yellow,
"Notice: This is not a legal advice. Use the information below at your own risk."
])

rows
|> TableRex.quick_render!(["Package", "Version", "License"])
|> IO.puts()
|> TableRex.quick_render!(headers)
|> file_touch()
|> output()
end

defp render_csv(rows) do
defp render_csv(rows, headers) do
rows
|> List.insert_at(0, ["Package", "Version", "License"])
|> List.insert_at(0, headers)
|> Licensir.CSV.encode()
|> Enum.each(&IO.write/1)
|> file_touch()
|> Enum.each(&output/1)
end

defp file_touch(text) do
if @output_file do
with {:ok, file} <- File.open(@output_file, [:write]) do
# IO.puts("\n\nSaving the output to " <> @output_file)
IO.binwrite(file, "\n")
text
else
e ->
_ =
Mix.Shell.IO.info([
:yellow,
"WARNING: Could not write to " <> @output_file
])

nil
end
end
end

defp output(text) do
filewriter = fn filename, data ->
File.open(filename, [:append])
|> elem(1)
|> IO.binwrite(data)
end

if @output_file, do: filewriter.(@output_file, text)
end
end
4 changes: 2 additions & 2 deletions lib/table_rex/renderer/text.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ defmodule TableRex.Renderer.Text do
vertical_style: :all,
horizontal_symbol: "-",
vertical_symbol: "|",
intersection_symbol: "+",
intersection_symbol: "|",
top_frame_symbol: "-",
title_separator_symbol: "-",
header_separator_symbol: "-",
Expand Down Expand Up @@ -77,7 +77,7 @@ defmodule TableRex.Renderer.Text do

rendered =
{table, meta, opts, []}
|> render_top_frame
# |> render_top_frame
mayel marked this conversation as resolved.
Show resolved Hide resolved
|> render_title
|> render_title_separator
|> render_header
Expand Down
Loading