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

Add links, more licenses, save-to-file, and markdown-compatible output #17

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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)
# 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)
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
24 changes: 20 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,17 +53,20 @@ 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)
else
deps
end
Expand All @@ -70,14 +75,21 @@ defmodule Licensir.Scanner do
defp get_version(%Mix.Dep{status: {:ok, version}}), do: version
defp get_version(_), do: nil

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(_), 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 +119,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 +129,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))
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),
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
|> render_title
|> render_title_separator
|> render_header
Expand Down
Loading