# SPDX-FileCopyrightText: 2021 Rosa Richter # # SPDX-License-Identifier: MIT defmodule Mix.Tasks.Licenses do @moduledoc """ Lists all dependencies along with a summary of their licenses. This task checks each entry in dependency package's `:licenses` list against the SPDX License List. To see details about licenses that are not found in the SPDX list, use `mix licenses.explain`. ## Command line options * `--osi` - additionally check if all licenses are approved by the [Open Source Initiative](https://opensource.org/licenses) * `--update` - pull down a fresh copy of the SPDX license list instead of using the version checked in with this tool. """ @shortdoc "Lists all dependencies along with a summary of their licenses." use Mix.Task @impl Mix.Task def run(args) do check_osi_approved = "--osi" in args license_list = if "--update" in args do HexLicenses.SPDX.fetch_licenses() |> HexLicenses.SPDX.parse_licenses() else HexLicenses.SPDX.licenses() end check = HexLicenses.license_check(license_list) |> Map.new(fn {dep, licenses} -> {dep, summary(licenses, check_osi_approved)} end) first_column_width = Map.keys(check) |> Enum.map(&to_string/1) |> Enum.map(&String.length/1) |> Enum.max(fn -> 0 end) |> max(String.length("Dependency")) |> Kernel.+(2) rows = Enum.sort_by(check, fn {dep, _summary} -> to_string(dep) end) |> Enum.map(fn {dep, summary} -> dep = String.pad_trailing(to_string(dep), first_column_width) IO.ANSI.format([dep, summary]) end) header = IO.ANSI.format([:faint, String.pad_trailing("Dependency", first_column_width), "Status"]) shell = Mix.shell() shell.info(header) Enum.each(rows, &shell.info/1) end defp summary(:not_in_hex, _check_osi_approved) do IO.ANSI.format([:red, "not in Hex"]) end defp summary(licenses, check_osi_approved) when is_map(licenses) do values = Map.values(licenses) all_approved = Enum.all?(values, &(&1 == :osi_approved)) count_not_approved = Enum.count(values, &(&1 == :not_approved)) count_not_recognized = Enum.count(values, &(&1 == :not_recognized)) check_passed? = (check_osi_approved && all_approved) or count_not_recognized == 0 if check_passed? do pass_message(check_osi_approved) else fail_message(count_not_approved, count_not_recognized, check_osi_approved) end end defp pass_message(true), do: IO.ANSI.format([:green, "all OSI approved"]) defp pass_message(false), do: IO.ANSI.format([:green, "all valid"]) defp fail_message(count_not_approved, count_not_recognized, check_osi_approved) do not_approved_message = "#{count_not_approved} not OSI approved" not_recognized_message = "#{count_not_recognized} not recognized" message = cond do check_osi_approved && count_not_approved > 0 && count_not_recognized > 0 -> not_approved_message <> ", " <> not_recognized_message check_osi_approved && count_not_approved > 0 -> not_approved_message count_not_recognized > 0 -> not_recognized_message end IO.ANSI.format([:red, message]) end end