Skip to content
This repository was archived by the owner on Aug 1, 2021. It is now read-only.
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
37 changes: 29 additions & 8 deletions lib/licensir/file_analyzer.ex
Original file line number Diff line number Diff line change
@@ -1,52 +1,73 @@
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

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} ->
analyze_content(content)
{:error, _} -> nil
end
end)
end

# Returns the first license that matches
defp analyze_content(content) do
content = clean(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()

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

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

defp unrecognised(_content) do
: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
5 changes: 3 additions & 2 deletions lib/licensir/guesser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ 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 <> ")"
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: 20 additions & 7 deletions lib/licensir/scanner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ defmodule Licensir.Scanner do
alias Licensir.{License, FileAnalyzer, Guesser}

@human_names %{
apache2: "Apache 2",
agpl_v3: "AGPL v3",
apache2: "Apache 2.0",
bsd: "BSD",
cc0: "CC0-1.0",
gpl_v2: "GPLv2",
gpl_v3: "GPLv3",
cc0: "CC0 1.0",
gpl_v2: "GPL v2",
gpl_v3: "GPL v3",
isc: "ISC",
lgpl: "LGPL",
mit: "MIT",
mpl2: "MPL2",
mpl2: "MPL 2.0",
licensir_mock_license: "Licensir Mock License",
unrecognized_license_file: "Unrecognized license file content"
unrecognized_license_file: "Unrecognized license"
}

@doc """
Expand Down Expand Up @@ -51,10 +52,12 @@ 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

%License{
app: dep.app,
name: Atom.to_string(dep.app),
version: get_version(dep),
link: get_link(dep.opts),
dep: dep
}
end
Expand All @@ -68,13 +71,22 @@ defmodule Licensir.Scanner do
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))
Expand Down Expand Up @@ -107,6 +119,7 @@ defmodule Licensir.Scanner do
end

defp search_file(%Mix.Dep{} = dep) do

license_atom =
Mix.Dep.in_dependency(dep, fn _ ->
case File.cwd() do
Expand Down
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 generated licensing information 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
3 changes: 1 addition & 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,6 @@ defmodule TableRex.Renderer.Text do

rendered =
{table, meta, opts, []}
|> render_top_frame
|> render_title
|> render_title_separator
|> render_header
Expand Down
Loading