~cosmicrose/hex_licenses

ref: f5594ed33b13bd236c1b76f492058e5df08aa35d hex_licenses/lib/mix/tasks/licenses/lint.ex -rw-r--r-- 4.4 KiB
f5594ed3Rosa Richter Add missing status_line case 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
# 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]

    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

    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 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