~cosmicrose/hex_licenses

ref: a4b469780dde0cab6811fb78a23195eb60cfc5d6 hex_licenses/lib/mix/tasks/licenses/lint.ex -rw-r--r-- 4.4 KiB
a4b46978Rosa Richter Reduce complexity of lint function 4 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# SPDX-FileCopyrightText: 2021 Rosa Richter
#
# SPDX-License-Identifier: MIT

defmodule Mix.Tasks.Licenses.Lint do
  @moduledoc """
  Check the current project's licenses.

  The Hex administrators recommend setting a package's `:licenses` value to SPDX license identifiers.
  However, this is only a recommendation, and is not enforced in any way.
  This task will enforce the use of SPDX identifiers in your package,
  and will return an error code if the current project is using any unrecognized or non-OSI-approved licenses.

  ## Configuration

    * `:package` - contain a `:licenses` list, which must be a list containing SPDX license identifiers, for example `["MIT"]`

  ## Command line options

    * `--reuse` - additionally check if the licenses declared in `mix.exs` match those in the `LICENSES` directory
      according to the [REUSE specification](https://reuse.software).
    * `--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.
  """
  use Mix.Task

  @shortdoc "Check the current project's licenses."

  def run(args) do
    package = Mix.Project.get!().project()[:package]

    validate_package!(package)

    license_list =
      if "--update" in args do
        HexLicenses.SPDX.fetch_licenses()
        |> HexLicenses.SPDX.parse_licenses()
      else
        HexLicenses.SPDX.licenses()
      end

    {:ok, result} = HexLicenses.lint(package, license_list)

    error? = false

    error? =
      if "--reuse" in args do
        check_reuse_spec() || error?
      else
        error?
      end

    check_osi_approved = "--osi" in args

    allowed_statuses =
      if check_osi_approved do
        [:osi_approved]
      else
        [:osi_approved, :not_approved]
      end

    unsafe_licenses =
      Enum.filter(result, fn {_license, status} -> status not in allowed_statuses end)

    error? =
      if Enum.empty?(unsafe_licenses) do
        if check_osi_approved do
          Mix.shell().info("This project's licenses are all recognized and OSI-approved.")
        else
          Mix.shell().info("This project's licenses are all valid SPDX identifiers.")
        end

        error?
      else
        Mix.shell().info("This project has #{Enum.count(unsafe_licenses)} unsafe licenses:")

        Enum.each(unsafe_licenses, &print_status/1)

        true
      end

    if error? do
      exit({:shutdown, 1})
    end
  end

  defp validate_package!(package) do
    if is_nil(package) do
      Mix.shell().error("This project does not have :package key defined in mix.exs.")
      exit({:shutdown, 1})
    end

    if Enum.empty?(Keyword.get(package, :licenses, [])) do
      Mix.shell().error("This project's :package config has a nil or empty :licenses list.")
      exit({:shutdown, 1})
    end
  end

  defp print_status({license, :not_approved}) do
    Mix.shell().info(" - \"#{license}\" is not OSI-approved.")
  end

  defp print_status({license, :not_recognized}) do
    Mix.shell().info(" - \"#{license}\" is not an SPDX ID")
  end

  defp check_reuse_spec do
    mix_licenses =
      Mix.Project.config()
      |> Access.fetch!(:package)
      |> Access.fetch!(:licenses)
      |> MapSet.new()

    file_licenses =
      Mix.Project.config_files()
      |> Enum.find(fn config_file -> Path.basename(config_file) == "mix.exs" end)
      |> Path.dirname()
      |> Path.join("LICENSES")
      |> File.ls!()
      |> Enum.map(fn license_file -> Path.basename(license_file, ".txt") end)
      |> MapSet.new()

    missing_from_mix = MapSet.difference(file_licenses, mix_licenses)
    missing_from_dir = MapSet.difference(mix_licenses, file_licenses)

    if Enum.any?(missing_from_mix) do
      Mix.shell().info("This project has licenses in LICENSES/ that are not declared in mix.exs:")

      Enum.each(missing_from_mix, fn license ->
        Mix.shell().info(" - #{license}")
      end)
    end

    if Enum.any?(missing_from_dir) do
      Mix.shell().info(
        "This project has licenses declared in mix.exs that are not present in LICENSES/"
      )

      Enum.each(missing_from_dir, fn license ->
        Mix.shell().info(" - #{license}")
      end)
    end

    if Enum.empty?(missing_from_mix) and Enum.empty?(missing_from_dir) do
      Mix.shell().info(
        "This project's declared licenses match the files in the LICENSES/ directory"
      )

      false
    else
      true
    end
  end
end