licensure 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.licensure.yml.example +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +116 -0
- data/Rakefile +8 -0
- data/exe/licensure +9 -0
- data/lib/licensure/cli.rb +258 -0
- data/lib/licensure/configuration.rb +96 -0
- data/lib/licensure/dependency_resolver.rb +41 -0
- data/lib/licensure/errors.rb +15 -0
- data/lib/licensure/formatters/base.rb +54 -0
- data/lib/licensure/formatters/csv.rb +45 -0
- data/lib/licensure/formatters/json.rb +61 -0
- data/lib/licensure/formatters/markdown.rb +57 -0
- data/lib/licensure/formatters/table.rb +56 -0
- data/lib/licensure/license_checker.rb +57 -0
- data/lib/licensure/license_fetcher.rb +104 -0
- data/lib/licensure/types.rb +28 -0
- data/lib/licensure/version.rb +5 -0
- data/lib/licensure.rb +19 -0
- metadata +92 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6759a51d14e8f88b4d718fb1368cf9e2593660e43863949d73bb929062be7c12
|
|
4
|
+
data.tar.gz: 396ad8b96f81968fffab0dce383b368632ae6c12f71c8055589c3b361b6a0fb8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 69a98b94fe4fe29946a48d8281ce993632db4be662e6cefb70cb5826d38807546a2dc8d4d1bf8598d63265ef4613b19b1797490b83efd84754d7a1a2a59f9a49
|
|
7
|
+
data.tar.gz: df348e79ddac7e9f8bb783a6743a0706b1fac62de1100268fceaa51b895c80813ebd730d0edb734be0905a95d4b88fdc0b666670baefbb6225664bccd0f831ee
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Licensure
|
|
2
|
+
|
|
3
|
+
Licensure is a RubyGem CLI tool that inspects dependency licenses from `Gemfile.lock` and checks them against a configurable allow list.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install as a gem:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
gem install licensure
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or add it to your `Gemfile`:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem "licensure"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
Initialize config:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
licensure init
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
List dependency licenses:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
licensure list
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Check licenses against `.licensure.yml`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
licensure check
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
Licensure uses `.licensure.yml`:
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
allowed_licenses:
|
|
45
|
+
- MIT
|
|
46
|
+
- Apache-2.0
|
|
47
|
+
- BSD-2-Clause
|
|
48
|
+
- BSD-3-Clause
|
|
49
|
+
- ISC
|
|
50
|
+
- Ruby
|
|
51
|
+
|
|
52
|
+
ignored_gems:
|
|
53
|
+
- bundler
|
|
54
|
+
- rake
|
|
55
|
+
|
|
56
|
+
deny_unknown: true
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `allowed_licenses`: Allowed license identifiers. Empty means allow all.
|
|
60
|
+
- `ignored_gems`: Gem names excluded from checks.
|
|
61
|
+
- `deny_unknown`: Treat gems without license metadata as warnings.
|
|
62
|
+
|
|
63
|
+
## Commands
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
licensure list [--format table|csv|json|markdown] [--recursive] [--output FILE] [--gemfile-lock PATH]
|
|
67
|
+
licensure check [--config FILE] [--recursive] [--format table|csv|json|markdown] [--gemfile-lock PATH]
|
|
68
|
+
licensure init
|
|
69
|
+
licensure version
|
|
70
|
+
licensure help [command]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Output Formats
|
|
74
|
+
|
|
75
|
+
`list` and `check` support:
|
|
76
|
+
|
|
77
|
+
- `table`
|
|
78
|
+
- `csv`
|
|
79
|
+
- `json`
|
|
80
|
+
- `markdown`
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
licensure list --format json
|
|
86
|
+
licensure check --format markdown
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## CI Example (GitHub Actions)
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
name: License Check
|
|
93
|
+
on: [push, pull_request]
|
|
94
|
+
jobs:
|
|
95
|
+
check:
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
steps:
|
|
98
|
+
- uses: actions/checkout@v4
|
|
99
|
+
- uses: ruby/setup-ruby@v1
|
|
100
|
+
with:
|
|
101
|
+
ruby-version: "3.3"
|
|
102
|
+
bundler-cache: true
|
|
103
|
+
- run: gem install licensure
|
|
104
|
+
- run: licensure check
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
bundle install
|
|
111
|
+
bundle exec rake spec
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
Released under the MIT License. See `LICENSE.txt`.
|
data/Rakefile
ADDED
data/exe/licensure
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optionparser"
|
|
4
|
+
|
|
5
|
+
module Licensure
|
|
6
|
+
# CLI entrypoint and subcommand orchestrator.
|
|
7
|
+
class CLI
|
|
8
|
+
FORMATS = %w[table csv json markdown].freeze
|
|
9
|
+
|
|
10
|
+
# @param argv [Array<String>]
|
|
11
|
+
# @param output [IO]
|
|
12
|
+
# @param error_output [IO]
|
|
13
|
+
# @param input [IO]
|
|
14
|
+
def initialize(argv = ARGV, output: $stdout, error_output: $stderr, input: $stdin)
|
|
15
|
+
@argv = argv.dup
|
|
16
|
+
@output = output
|
|
17
|
+
@error_output = error_output
|
|
18
|
+
@input = input
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Integer]
|
|
22
|
+
def run
|
|
23
|
+
command = @argv.shift
|
|
24
|
+
|
|
25
|
+
case command
|
|
26
|
+
when nil, "help", "-h", "--help"
|
|
27
|
+
run_help(@argv.shift)
|
|
28
|
+
when "list"
|
|
29
|
+
run_list
|
|
30
|
+
when "check"
|
|
31
|
+
run_check
|
|
32
|
+
when "init"
|
|
33
|
+
run_init
|
|
34
|
+
when "version"
|
|
35
|
+
run_version
|
|
36
|
+
else
|
|
37
|
+
@error_output.puts("Unknown command: #{command}")
|
|
38
|
+
2
|
|
39
|
+
end
|
|
40
|
+
rescue OptionParser::ParseError, Licensure::Error => e
|
|
41
|
+
@error_output.puts(e.message)
|
|
42
|
+
2
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
@error_output.puts("Unexpected error: #{e.message}")
|
|
45
|
+
@error_output.puts(e.backtrace.join("\n"))
|
|
46
|
+
2
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# @return [Integer]
|
|
52
|
+
def run_list
|
|
53
|
+
options = {
|
|
54
|
+
format: "table",
|
|
55
|
+
recursive: false,
|
|
56
|
+
output: nil,
|
|
57
|
+
lockfile_path: "Gemfile.lock",
|
|
58
|
+
help: false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
parser = OptionParser.new do |opts|
|
|
62
|
+
opts.banner = "Usage: licensure list [options]"
|
|
63
|
+
opts.on("-f", "--format FORMAT", FORMATS, "Output format: #{FORMATS.join(', ')}") { |value| options[:format] = value }
|
|
64
|
+
opts.on("-r", "--recursive", "Include transitive dependencies") { options[:recursive] = true }
|
|
65
|
+
opts.on("-o", "--output FILE", "Write output to file") { |value| options[:output] = value }
|
|
66
|
+
opts.on("--gemfile-lock PATH", "Path to Gemfile.lock") { |value| options[:lockfile_path] = value }
|
|
67
|
+
opts.on("-h", "--help", "Show help") { options[:help] = true }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
parser.parse!(@argv)
|
|
71
|
+
return print_help(parser) if options[:help]
|
|
72
|
+
|
|
73
|
+
assert_no_extra_arguments!
|
|
74
|
+
|
|
75
|
+
dependencies = DependencyResolver.new(
|
|
76
|
+
lockfile_path: options[:lockfile_path],
|
|
77
|
+
recursive: options[:recursive]
|
|
78
|
+
).resolve
|
|
79
|
+
|
|
80
|
+
infos = LicenseFetcher.new.fetch_all(dependencies)
|
|
81
|
+
render_to(options[:format], infos: infos, output_path: options[:output])
|
|
82
|
+
0
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [Integer]
|
|
86
|
+
def run_check
|
|
87
|
+
options = {
|
|
88
|
+
config_path: ".licensure.yml",
|
|
89
|
+
format: "table",
|
|
90
|
+
recursive: false,
|
|
91
|
+
lockfile_path: "Gemfile.lock",
|
|
92
|
+
help: false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
parser = OptionParser.new do |opts|
|
|
96
|
+
opts.banner = "Usage: licensure check [options]"
|
|
97
|
+
opts.on("-c", "--config FILE", "Path to .licensure.yml") { |value| options[:config_path] = value }
|
|
98
|
+
opts.on("-r", "--recursive", "Include transitive dependencies") { options[:recursive] = true }
|
|
99
|
+
opts.on("-f", "--format FORMAT", FORMATS, "Output format: #{FORMATS.join(', ')}") { |value| options[:format] = value }
|
|
100
|
+
opts.on("--gemfile-lock PATH", "Path to Gemfile.lock") { |value| options[:lockfile_path] = value }
|
|
101
|
+
opts.on("-h", "--help", "Show help") { options[:help] = true }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
parser.parse!(@argv)
|
|
105
|
+
return print_help(parser) if options[:help]
|
|
106
|
+
|
|
107
|
+
assert_no_extra_arguments!
|
|
108
|
+
|
|
109
|
+
configuration = Configuration.load(options[:config_path])
|
|
110
|
+
dependencies = DependencyResolver.new(
|
|
111
|
+
lockfile_path: options[:lockfile_path],
|
|
112
|
+
recursive: options[:recursive]
|
|
113
|
+
).resolve
|
|
114
|
+
infos = LicenseFetcher.new.fetch_all(dependencies)
|
|
115
|
+
result = LicenseChecker.new(configuration: configuration).check(infos)
|
|
116
|
+
|
|
117
|
+
formatter_for(options[:format], output: @output).render_check_result(result)
|
|
118
|
+
result.violations.empty? ? 0 : 1
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @return [Integer]
|
|
122
|
+
def run_init
|
|
123
|
+
parser = OptionParser.new do |opts|
|
|
124
|
+
opts.banner = "Usage: licensure init"
|
|
125
|
+
opts.on("-h", "--help", "Show help") { @output.puts(opts); return 0 }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
parser.parse!(@argv)
|
|
129
|
+
assert_no_extra_arguments!
|
|
130
|
+
|
|
131
|
+
config_path = ".licensure.yml"
|
|
132
|
+
if File.exist?(config_path)
|
|
133
|
+
@output.print("#{config_path} already exists. Overwrite? [y/N]: ")
|
|
134
|
+
answer = @input.gets&.strip&.downcase
|
|
135
|
+
unless %w[y yes].include?(answer)
|
|
136
|
+
@output.puts("Aborted")
|
|
137
|
+
return 0
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
File.write(config_path, Configuration::SAMPLE_CONFIG)
|
|
142
|
+
@output.puts("Created #{config_path}")
|
|
143
|
+
0
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# @return [Integer]
|
|
147
|
+
def run_version
|
|
148
|
+
@output.puts("Licensure #{Licensure::VERSION}")
|
|
149
|
+
0
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @param subcommand [String, nil]
|
|
153
|
+
# @return [Integer]
|
|
154
|
+
def run_help(subcommand)
|
|
155
|
+
return print_main_help if subcommand.nil?
|
|
156
|
+
|
|
157
|
+
case subcommand
|
|
158
|
+
when "list"
|
|
159
|
+
print_help(build_list_help_parser)
|
|
160
|
+
when "check"
|
|
161
|
+
print_help(build_check_help_parser)
|
|
162
|
+
when "init"
|
|
163
|
+
@output.puts("Usage: licensure init")
|
|
164
|
+
0
|
|
165
|
+
when "version"
|
|
166
|
+
@output.puts("Usage: licensure version")
|
|
167
|
+
0
|
|
168
|
+
else
|
|
169
|
+
@error_output.puts("Unknown help topic: #{subcommand}")
|
|
170
|
+
2
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# @return [Integer]
|
|
175
|
+
def print_main_help
|
|
176
|
+
@output.puts <<~TEXT
|
|
177
|
+
Usage: licensure <command> [options]
|
|
178
|
+
|
|
179
|
+
Commands:
|
|
180
|
+
list Show dependency license information
|
|
181
|
+
check Validate licenses against .licensure.yml
|
|
182
|
+
init Create .licensure.yml
|
|
183
|
+
version Show current version
|
|
184
|
+
help Show help
|
|
185
|
+
TEXT
|
|
186
|
+
0
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# @param parser [OptionParser]
|
|
190
|
+
# @return [Integer]
|
|
191
|
+
def print_help(parser)
|
|
192
|
+
@output.puts(parser)
|
|
193
|
+
0
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# @return [OptionParser]
|
|
197
|
+
def build_list_help_parser
|
|
198
|
+
OptionParser.new do |opts|
|
|
199
|
+
opts.banner = "Usage: licensure list [options]"
|
|
200
|
+
opts.on("-f", "--format FORMAT", FORMATS)
|
|
201
|
+
opts.on("-r", "--recursive")
|
|
202
|
+
opts.on("-o", "--output FILE")
|
|
203
|
+
opts.on("--gemfile-lock PATH")
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# @return [OptionParser]
|
|
208
|
+
def build_check_help_parser
|
|
209
|
+
OptionParser.new do |opts|
|
|
210
|
+
opts.banner = "Usage: licensure check [options]"
|
|
211
|
+
opts.on("-c", "--config FILE")
|
|
212
|
+
opts.on("-r", "--recursive")
|
|
213
|
+
opts.on("-f", "--format FORMAT", FORMATS)
|
|
214
|
+
opts.on("--gemfile-lock PATH")
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# @param format [String]
|
|
219
|
+
# @param output [IO]
|
|
220
|
+
# @return [Licensure::Formatters::Base]
|
|
221
|
+
def formatter_for(format, output:)
|
|
222
|
+
case format
|
|
223
|
+
when "table"
|
|
224
|
+
Formatters::Table.new(output: output)
|
|
225
|
+
when "csv"
|
|
226
|
+
Formatters::Csv.new(output: output)
|
|
227
|
+
when "json"
|
|
228
|
+
Formatters::Json.new(output: output)
|
|
229
|
+
when "markdown"
|
|
230
|
+
Formatters::Markdown.new(output: output)
|
|
231
|
+
else
|
|
232
|
+
raise CLIError, "Unsupported format: #{format}"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# @param format [String]
|
|
237
|
+
# @param infos [Array<Licensure::GemLicenseInfo>]
|
|
238
|
+
# @param output_path [String, nil]
|
|
239
|
+
# @return [void]
|
|
240
|
+
def render_to(format, infos:, output_path:)
|
|
241
|
+
if output_path
|
|
242
|
+
File.open(output_path, "w") do |file|
|
|
243
|
+
formatter_for(format, output: file).render(infos)
|
|
244
|
+
end
|
|
245
|
+
return
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
formatter_for(format, output: @output).render(infos)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# @return [void]
|
|
252
|
+
def assert_no_extra_arguments!
|
|
253
|
+
return if @argv.empty?
|
|
254
|
+
|
|
255
|
+
raise CLIError, "Unknown arguments: #{@argv.join(' ')}"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Licensure
|
|
6
|
+
# Loads and validates .licensure.yml configuration.
|
|
7
|
+
class Configuration
|
|
8
|
+
VALID_KEYS = %w[allowed_licenses ignored_gems deny_unknown].freeze
|
|
9
|
+
|
|
10
|
+
SAMPLE_CONFIG = <<~YAML.freeze
|
|
11
|
+
# .licensure.yml
|
|
12
|
+
allowed_licenses:
|
|
13
|
+
- MIT
|
|
14
|
+
- Apache-2.0
|
|
15
|
+
- BSD-2-Clause
|
|
16
|
+
- BSD-3-Clause
|
|
17
|
+
- ISC
|
|
18
|
+
- Ruby
|
|
19
|
+
|
|
20
|
+
ignored_gems:
|
|
21
|
+
- bundler
|
|
22
|
+
- rake
|
|
23
|
+
|
|
24
|
+
# Treat gems with unspecified licenses as warnings
|
|
25
|
+
deny_unknown: true
|
|
26
|
+
YAML
|
|
27
|
+
|
|
28
|
+
# @param path [String]
|
|
29
|
+
# @return [Licensure::Configuration]
|
|
30
|
+
def self.load(path = ".licensure.yml")
|
|
31
|
+
raise ConfigurationError, "Configuration file not found: #{path}" unless File.exist?(path)
|
|
32
|
+
|
|
33
|
+
payload = YAML.safe_load(File.read(path), aliases: false) || {}
|
|
34
|
+
unless payload.is_a?(Hash)
|
|
35
|
+
raise ConfigurationError, "Configuration must be a mapping"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
new(payload.transform_keys(&:to_s))
|
|
39
|
+
rescue Psych::SyntaxError => e
|
|
40
|
+
raise ConfigurationError, "Failed to parse configuration: #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Licensure::Configuration]
|
|
44
|
+
def self.default
|
|
45
|
+
new({})
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
attr_reader :allowed_licenses, :ignored_gems
|
|
49
|
+
|
|
50
|
+
# @param data [Hash]
|
|
51
|
+
def initialize(data)
|
|
52
|
+
warn_unknown_keys(data)
|
|
53
|
+
|
|
54
|
+
@allowed_licenses = validate_array!(data.fetch("allowed_licenses", []), "allowed_licenses")
|
|
55
|
+
@ignored_gems = validate_array!(data.fetch("ignored_gems", []), "ignored_gems")
|
|
56
|
+
@deny_unknown = validate_boolean!(data.fetch("deny_unknown", true), "deny_unknown")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
def deny_unknown?
|
|
61
|
+
@deny_unknown
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# @param data [Hash]
|
|
67
|
+
# @return [void]
|
|
68
|
+
def warn_unknown_keys(data)
|
|
69
|
+
(data.keys - VALID_KEYS).each do |key|
|
|
70
|
+
$stderr.puts("Warning: Unknown configuration key '#{key}'")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param value [Object]
|
|
75
|
+
# @param key [String]
|
|
76
|
+
# @return [Array<String>]
|
|
77
|
+
def validate_array!(value, key)
|
|
78
|
+
unless value.is_a?(Array)
|
|
79
|
+
raise ConfigurationError, "#{key} must be an array"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
value.map(&:to_s)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @param value [Object]
|
|
86
|
+
# @param key [String]
|
|
87
|
+
# @return [Boolean]
|
|
88
|
+
def validate_boolean!(value, key)
|
|
89
|
+
unless value == true || value == false
|
|
90
|
+
raise ConfigurationError, "#{key} must be a boolean"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
value
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler"
|
|
4
|
+
|
|
5
|
+
module Licensure
|
|
6
|
+
# Parses Gemfile.lock and resolves dependency names/versions.
|
|
7
|
+
class DependencyResolver
|
|
8
|
+
# @param lockfile_path [String]
|
|
9
|
+
# @param recursive [Boolean]
|
|
10
|
+
def initialize(lockfile_path: "Gemfile.lock", recursive: false)
|
|
11
|
+
@lockfile_path = lockfile_path
|
|
12
|
+
@recursive = recursive
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [Array<Hash{Symbol => String}>]
|
|
16
|
+
def resolve
|
|
17
|
+
raise DependencyResolutionError, "Gemfile.lock not found: #{@lockfile_path}" unless File.exist?(@lockfile_path)
|
|
18
|
+
|
|
19
|
+
parser = Bundler::LockfileParser.new(File.read(@lockfile_path))
|
|
20
|
+
specs_by_name = parser.specs.to_h { |spec| [spec.name, spec] }
|
|
21
|
+
|
|
22
|
+
names = if @recursive
|
|
23
|
+
parser.specs.map(&:name)
|
|
24
|
+
else
|
|
25
|
+
parser.dependencies.keys
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
names.uniq
|
|
29
|
+
.reject { |name| name == "bundler" }
|
|
30
|
+
.filter_map do |name|
|
|
31
|
+
spec = specs_by_name[name]
|
|
32
|
+
next unless spec
|
|
33
|
+
|
|
34
|
+
{ name: name, version: spec.version.to_s }
|
|
35
|
+
end
|
|
36
|
+
.sort_by { |dependency| dependency[:name] }
|
|
37
|
+
rescue Bundler::LockfileError => e
|
|
38
|
+
raise DependencyResolutionError, "Failed to parse #{@lockfile_path}: #{e.message}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Licensure
|
|
4
|
+
# Base error class for Licensure.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when configuration loading or validation fails.
|
|
8
|
+
class ConfigurationError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when Gemfile.lock parsing fails.
|
|
11
|
+
class DependencyResolutionError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised for CLI argument and execution failures.
|
|
14
|
+
class CLIError < Error; end
|
|
15
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Licensure
|
|
4
|
+
module Formatters
|
|
5
|
+
# Base formatter abstraction.
|
|
6
|
+
class Base
|
|
7
|
+
# @param output [IO]
|
|
8
|
+
def initialize(output: $stdout)
|
|
9
|
+
@output = output
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @param _gem_license_infos [Array<Licensure::GemLicenseInfo>]
|
|
13
|
+
# @return [String]
|
|
14
|
+
def render(_gem_license_infos)
|
|
15
|
+
raise NotImplementedError, "Implement #render in formatter subclasses"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param _check_result [Licensure::CheckResult]
|
|
19
|
+
# @return [String]
|
|
20
|
+
def render_check_result(_check_result)
|
|
21
|
+
raise NotImplementedError, "Implement #render_check_result in formatter subclasses"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
protected
|
|
25
|
+
|
|
26
|
+
# @param content [String]
|
|
27
|
+
# @return [String]
|
|
28
|
+
def write_output(content)
|
|
29
|
+
@output.write(content)
|
|
30
|
+
@output.write("\n") unless content.end_with?("\n")
|
|
31
|
+
content
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param info [Licensure::GemLicenseInfo]
|
|
35
|
+
# @return [Array<String>]
|
|
36
|
+
def gem_row(info)
|
|
37
|
+
[
|
|
38
|
+
info.name.to_s,
|
|
39
|
+
info.version.to_s,
|
|
40
|
+
info.licenses.join(" | "),
|
|
41
|
+
info.source.to_s,
|
|
42
|
+
info.homepage.to_s
|
|
43
|
+
]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param violation [Licensure::Violation]
|
|
47
|
+
# @return [Array<String>]
|
|
48
|
+
def violation_row(violation)
|
|
49
|
+
row = gem_row(violation.gem_info).take(4)
|
|
50
|
+
row << violation.reason.to_s
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
|
|
5
|
+
module Licensure
|
|
6
|
+
module Formatters
|
|
7
|
+
# Renders CSV output for list/check results.
|
|
8
|
+
class Csv < Base
|
|
9
|
+
# @param gem_license_infos [Array<Licensure::GemLicenseInfo>]
|
|
10
|
+
# @return [String]
|
|
11
|
+
def render(gem_license_infos)
|
|
12
|
+
content = ::CSV.generate do |csv|
|
|
13
|
+
csv << %w[Gem Version License Source Homepage]
|
|
14
|
+
gem_license_infos.each do |info|
|
|
15
|
+
csv << gem_row(info)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
write_output(content)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param check_result [Licensure::CheckResult]
|
|
23
|
+
# @return [String]
|
|
24
|
+
def render_check_result(check_result)
|
|
25
|
+
content = ::CSV.generate do |csv|
|
|
26
|
+
csv << %w[Gem Version License Source Status Reason]
|
|
27
|
+
|
|
28
|
+
check_result.passed.each do |info|
|
|
29
|
+
csv << gem_row(info).take(4) + ["PASSED", ""]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
check_result.violations.each do |violation|
|
|
33
|
+
csv << violation_row(violation).take(4) + ["VIOLATION", violation.reason]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
check_result.warnings.each do |warning|
|
|
37
|
+
csv << violation_row(warning).take(4) + ["WARNING", warning.reason]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
write_output(content)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Licensure
|
|
7
|
+
module Formatters
|
|
8
|
+
# Renders JSON output for list/check results.
|
|
9
|
+
class Json < Base
|
|
10
|
+
# @param gem_license_infos [Array<Licensure::GemLicenseInfo>]
|
|
11
|
+
# @return [String]
|
|
12
|
+
def render(gem_license_infos)
|
|
13
|
+
payload = {
|
|
14
|
+
generated_at: Time.now.iso8601,
|
|
15
|
+
gems: gem_license_infos.map { |info| gem_payload(info) }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
write_output(JSON.pretty_generate(payload))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param check_result [Licensure::CheckResult]
|
|
22
|
+
# @return [String]
|
|
23
|
+
def render_check_result(check_result)
|
|
24
|
+
payload = {
|
|
25
|
+
generated_at: Time.now.iso8601,
|
|
26
|
+
summary: {
|
|
27
|
+
total: check_result.passed.size + check_result.violations.size + check_result.warnings.size,
|
|
28
|
+
passed: check_result.passed.size,
|
|
29
|
+
violations: check_result.violations.size,
|
|
30
|
+
warnings: check_result.warnings.size
|
|
31
|
+
},
|
|
32
|
+
violations: check_result.violations.map { |violation| violation_payload(violation) },
|
|
33
|
+
warnings: check_result.warnings.map { |warning| violation_payload(warning) },
|
|
34
|
+
passed: check_result.passed.map { |info| gem_payload(info) }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
write_output(JSON.pretty_generate(payload))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# @param info [Licensure::GemLicenseInfo]
|
|
43
|
+
# @return [Hash]
|
|
44
|
+
def gem_payload(info)
|
|
45
|
+
{
|
|
46
|
+
name: info.name,
|
|
47
|
+
version: info.version,
|
|
48
|
+
licenses: info.licenses,
|
|
49
|
+
source: info.source.to_s,
|
|
50
|
+
homepage: info.homepage
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param violation [Licensure::Violation]
|
|
55
|
+
# @return [Hash]
|
|
56
|
+
def violation_payload(violation)
|
|
57
|
+
gem_payload(violation.gem_info).merge(reason: violation.reason)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Licensure
|
|
4
|
+
module Formatters
|
|
5
|
+
# Renders Markdown tables for list/check results.
|
|
6
|
+
class Markdown < Base
|
|
7
|
+
LIST_HEADERS = ["Gem", "Version", "License", "Source", "Homepage"].freeze
|
|
8
|
+
CHECK_HEADERS = ["Gem", "Version", "License", "Source", "Reason"].freeze
|
|
9
|
+
|
|
10
|
+
# @param gem_license_infos [Array<Licensure::GemLicenseInfo>]
|
|
11
|
+
# @return [String]
|
|
12
|
+
def render(gem_license_infos)
|
|
13
|
+
rows = gem_license_infos.map { |info| gem_row(info) }
|
|
14
|
+
write_output(markdown_table(LIST_HEADERS, rows))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param check_result [Licensure::CheckResult]
|
|
18
|
+
# @return [String]
|
|
19
|
+
def render_check_result(check_result)
|
|
20
|
+
sections = [
|
|
21
|
+
markdown_section("PASSED", LIST_HEADERS, check_result.passed.map { |info| gem_row(info) }),
|
|
22
|
+
markdown_section("VIOLATIONS", CHECK_HEADERS, check_result.violations.map { |item| violation_row(item) }),
|
|
23
|
+
markdown_section("WARNINGS", CHECK_HEADERS, check_result.warnings.map { |item| violation_row(item) })
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
write_output(sections.join("\n\n"))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# @param title [String]
|
|
32
|
+
# @param headers [Array<String>]
|
|
33
|
+
# @param rows [Array<Array<String>>]
|
|
34
|
+
# @return [String]
|
|
35
|
+
def markdown_section(title, headers, rows)
|
|
36
|
+
"## #{title}\n\n#{markdown_table(headers, rows)}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param headers [Array<String>]
|
|
40
|
+
# @param rows [Array<Array<String>>]
|
|
41
|
+
# @return [String]
|
|
42
|
+
def markdown_table(headers, rows)
|
|
43
|
+
header_line = "| #{headers.join(' | ')} |"
|
|
44
|
+
separator = "| #{Array.new(headers.size, '---').join(' | ')} |"
|
|
45
|
+
row_lines = rows.map { |row| "| #{row.map { |value| escape_markdown(value.to_s) }.join(' | ')} |" }
|
|
46
|
+
|
|
47
|
+
[header_line, separator, *row_lines].join("\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param value [String]
|
|
51
|
+
# @return [String]
|
|
52
|
+
def escape_markdown(value)
|
|
53
|
+
value.gsub("|", "\\|")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Licensure
|
|
4
|
+
module Formatters
|
|
5
|
+
# Renders terminal-friendly ASCII tables.
|
|
6
|
+
class Table < Base
|
|
7
|
+
LIST_HEADERS = ["Gem", "Version", "License", "Source", "Homepage"].freeze
|
|
8
|
+
CHECK_HEADERS = ["Gem", "Version", "License", "Source", "Reason"].freeze
|
|
9
|
+
|
|
10
|
+
# @param gem_license_infos [Array<Licensure::GemLicenseInfo>]
|
|
11
|
+
# @return [String]
|
|
12
|
+
def render(gem_license_infos)
|
|
13
|
+
rows = gem_license_infos.map { |info| gem_row(info) }
|
|
14
|
+
write_output(build_table(LIST_HEADERS, rows))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param check_result [Licensure::CheckResult]
|
|
18
|
+
# @return [String]
|
|
19
|
+
def render_check_result(check_result)
|
|
20
|
+
sections = []
|
|
21
|
+
sections << section("PASSED", build_table(LIST_HEADERS, check_result.passed.map { |info| gem_row(info) }))
|
|
22
|
+
sections << section("VIOLATIONS", build_table(CHECK_HEADERS, check_result.violations.map { |item| violation_row(item) }))
|
|
23
|
+
sections << section("WARNINGS", build_table(CHECK_HEADERS, check_result.warnings.map { |item| violation_row(item) }))
|
|
24
|
+
|
|
25
|
+
write_output(sections.join("\n\n"))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# @param title [String]
|
|
31
|
+
# @param table [String]
|
|
32
|
+
# @return [String]
|
|
33
|
+
def section(title, table)
|
|
34
|
+
"#{title}\n#{table}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param headers [Array<String>]
|
|
38
|
+
# @param rows [Array<Array<String>>]
|
|
39
|
+
# @return [String]
|
|
40
|
+
def build_table(headers, rows)
|
|
41
|
+
widths = headers.each_with_index.map do |header, index|
|
|
42
|
+
[header.length, *rows.map { |row| row[index].to_s.length }].max
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
border = "+-#{widths.map { |width| "-" * width }.join("-+-")}-+"
|
|
46
|
+
header_line = "| #{headers.each_with_index.map { |header, index| header.ljust(widths[index]) }.join(" | ")} |"
|
|
47
|
+
|
|
48
|
+
row_lines = rows.map do |row|
|
|
49
|
+
"| #{row.each_with_index.map { |value, index| value.to_s.ljust(widths[index]) }.join(" | ")} |"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
[border, header_line, border, *row_lines, border].join("\n")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Licensure
|
|
4
|
+
# Checks dependency licenses against configuration rules.
|
|
5
|
+
class LicenseChecker
|
|
6
|
+
# @param configuration [Licensure::Configuration]
|
|
7
|
+
def initialize(configuration:)
|
|
8
|
+
@configuration = configuration
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @param gem_license_infos [Array<Licensure::GemLicenseInfo>]
|
|
12
|
+
# @return [Licensure::CheckResult]
|
|
13
|
+
def check(gem_license_infos)
|
|
14
|
+
filtered_infos = gem_license_infos.reject do |info|
|
|
15
|
+
@configuration.ignored_gems.include?(info.name)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
violations = []
|
|
19
|
+
warnings = []
|
|
20
|
+
passed = []
|
|
21
|
+
|
|
22
|
+
filtered_infos.each do |info|
|
|
23
|
+
if info.licenses.empty?
|
|
24
|
+
if @configuration.deny_unknown?
|
|
25
|
+
warnings << Violation.new(gem_info: info, reason: "License not specified")
|
|
26
|
+
else
|
|
27
|
+
passed << info
|
|
28
|
+
end
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if @configuration.allowed_licenses.empty?
|
|
33
|
+
passed << info
|
|
34
|
+
next
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if allowed_license?(info.licenses)
|
|
38
|
+
passed << info
|
|
39
|
+
next
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
reason = "License '#{info.licenses.join(", ")}' is not in the allowed list"
|
|
43
|
+
violations << Violation.new(gem_info: info, reason: reason)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
CheckResult.new(violations: violations, warnings: warnings, passed: passed)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# @param licenses [Array<String>]
|
|
52
|
+
# @return [Boolean]
|
|
53
|
+
def allowed_license?(licenses)
|
|
54
|
+
licenses.any? { |license| @configuration.allowed_licenses.include?(license) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
|
|
6
|
+
module Licensure
|
|
7
|
+
# Fetches gem license data from local gemspecs and RubyGems API.
|
|
8
|
+
class LicenseFetcher
|
|
9
|
+
RUBYGEMS_ENDPOINT = "https://rubygems.org/api/v1/gems".freeze
|
|
10
|
+
REQUEST_TIMEOUT = 5
|
|
11
|
+
|
|
12
|
+
# @param dependencies [Array<Hash{Symbol => String}>]
|
|
13
|
+
# @return [Array<Licensure::GemLicenseInfo>]
|
|
14
|
+
def fetch_all(dependencies)
|
|
15
|
+
dependencies.map { |dependency| fetch(dependency[:name], dependency[:version]) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param name [String]
|
|
19
|
+
# @param version [String]
|
|
20
|
+
# @return [Licensure::GemLicenseInfo]
|
|
21
|
+
def fetch(name, version)
|
|
22
|
+
local = fetch_from_gemspec(name)
|
|
23
|
+
return build_info(name, version, local[:licenses], :gemspec, local[:homepage]) if local
|
|
24
|
+
|
|
25
|
+
remote = fetch_from_api(name)
|
|
26
|
+
return build_info(name, version, remote[:licenses], :api, remote[:homepage]) if remote
|
|
27
|
+
|
|
28
|
+
build_info(name, version, [], :unknown, nil)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# @param name [String]
|
|
34
|
+
# @return [Hash, nil]
|
|
35
|
+
def fetch_from_gemspec(name)
|
|
36
|
+
spec = Gem::Specification.find_by_name(name)
|
|
37
|
+
licenses = normalize_licenses(spec.licenses, spec.license)
|
|
38
|
+
return nil if licenses.empty?
|
|
39
|
+
|
|
40
|
+
{ licenses: licenses, homepage: spec.homepage }
|
|
41
|
+
rescue Gem::LoadError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param name [String]
|
|
46
|
+
# @return [Hash, nil]
|
|
47
|
+
def fetch_from_api(name)
|
|
48
|
+
uri = URI("#{RUBYGEMS_ENDPOINT}/#{name}.json")
|
|
49
|
+
request = Net::HTTP::Get.new(uri)
|
|
50
|
+
request["User-Agent"] = "Licensure/#{Licensure::VERSION}"
|
|
51
|
+
|
|
52
|
+
response = http_client_for(uri).request(request)
|
|
53
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
54
|
+
|
|
55
|
+
payload = JSON.parse(response.body)
|
|
56
|
+
licenses = normalize_licenses(payload["licenses"], payload["license"])
|
|
57
|
+
return nil if licenses.empty?
|
|
58
|
+
|
|
59
|
+
{ licenses: licenses, homepage: payload["homepage_uri"] || payload["homepage"] }
|
|
60
|
+
rescue JSON::ParserError, StandardError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param uri [URI::HTTPS]
|
|
65
|
+
# @return [Net::HTTP]
|
|
66
|
+
def http_client_for(uri)
|
|
67
|
+
client = Net::HTTP.new(uri.host, uri.port)
|
|
68
|
+
client.use_ssl = true
|
|
69
|
+
client.open_timeout = REQUEST_TIMEOUT
|
|
70
|
+
client.read_timeout = REQUEST_TIMEOUT
|
|
71
|
+
client
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param licenses [Array<String>, nil]
|
|
75
|
+
# @param license [String, nil]
|
|
76
|
+
# @return [Array<String>]
|
|
77
|
+
def normalize_licenses(licenses, license)
|
|
78
|
+
items = []
|
|
79
|
+
items.concat(Array(licenses))
|
|
80
|
+
items << license if license
|
|
81
|
+
|
|
82
|
+
items.compact
|
|
83
|
+
.map { |item| item.to_s.strip }
|
|
84
|
+
.reject(&:empty?)
|
|
85
|
+
.uniq
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param name [String]
|
|
89
|
+
# @param version [String]
|
|
90
|
+
# @param licenses [Array<String>]
|
|
91
|
+
# @param source [Symbol]
|
|
92
|
+
# @param homepage [String, nil]
|
|
93
|
+
# @return [Licensure::GemLicenseInfo]
|
|
94
|
+
def build_info(name, version, licenses, source, homepage)
|
|
95
|
+
GemLicenseInfo.new(
|
|
96
|
+
name: name,
|
|
97
|
+
version: version,
|
|
98
|
+
licenses: licenses,
|
|
99
|
+
source: source,
|
|
100
|
+
homepage: homepage
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Licensure
|
|
4
|
+
# License information for a single gem dependency.
|
|
5
|
+
GemLicenseInfo = Struct.new(
|
|
6
|
+
:name,
|
|
7
|
+
:version,
|
|
8
|
+
:licenses,
|
|
9
|
+
:source,
|
|
10
|
+
:homepage,
|
|
11
|
+
keyword_init: true
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# License check result aggregation.
|
|
15
|
+
CheckResult = Struct.new(
|
|
16
|
+
:violations,
|
|
17
|
+
:warnings,
|
|
18
|
+
:passed,
|
|
19
|
+
keyword_init: true
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Violation or warning entry with reason.
|
|
23
|
+
Violation = Struct.new(
|
|
24
|
+
:gem_info,
|
|
25
|
+
:reason,
|
|
26
|
+
keyword_init: true
|
|
27
|
+
)
|
|
28
|
+
end
|
data/lib/licensure.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "licensure/version"
|
|
4
|
+
require_relative "licensure/errors"
|
|
5
|
+
require_relative "licensure/types"
|
|
6
|
+
require_relative "licensure/configuration"
|
|
7
|
+
require_relative "licensure/dependency_resolver"
|
|
8
|
+
require_relative "licensure/license_fetcher"
|
|
9
|
+
require_relative "licensure/license_checker"
|
|
10
|
+
require_relative "licensure/formatters/base"
|
|
11
|
+
require_relative "licensure/formatters/table"
|
|
12
|
+
require_relative "licensure/formatters/csv"
|
|
13
|
+
require_relative "licensure/formatters/json"
|
|
14
|
+
require_relative "licensure/formatters/markdown"
|
|
15
|
+
require_relative "licensure/cli"
|
|
16
|
+
|
|
17
|
+
# Top-level namespace for Licensure.
|
|
18
|
+
module Licensure
|
|
19
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: licensure
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: bundler
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: csv
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.0'
|
|
40
|
+
description: Licensure collects dependency license metadata and validates it against
|
|
41
|
+
a configurable allow list.
|
|
42
|
+
email:
|
|
43
|
+
- t.yudai92@gmail.com
|
|
44
|
+
executables:
|
|
45
|
+
- licensure
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- ".licensure.yml.example"
|
|
50
|
+
- LICENSE.txt
|
|
51
|
+
- README.md
|
|
52
|
+
- Rakefile
|
|
53
|
+
- exe/licensure
|
|
54
|
+
- lib/licensure.rb
|
|
55
|
+
- lib/licensure/cli.rb
|
|
56
|
+
- lib/licensure/configuration.rb
|
|
57
|
+
- lib/licensure/dependency_resolver.rb
|
|
58
|
+
- lib/licensure/errors.rb
|
|
59
|
+
- lib/licensure/formatters/base.rb
|
|
60
|
+
- lib/licensure/formatters/csv.rb
|
|
61
|
+
- lib/licensure/formatters/json.rb
|
|
62
|
+
- lib/licensure/formatters/markdown.rb
|
|
63
|
+
- lib/licensure/formatters/table.rb
|
|
64
|
+
- lib/licensure/license_checker.rb
|
|
65
|
+
- lib/licensure/license_fetcher.rb
|
|
66
|
+
- lib/licensure/types.rb
|
|
67
|
+
- lib/licensure/version.rb
|
|
68
|
+
homepage: https://github.com/ydah/licensure
|
|
69
|
+
licenses:
|
|
70
|
+
- MIT
|
|
71
|
+
metadata:
|
|
72
|
+
homepage_uri: https://github.com/ydah/licensure
|
|
73
|
+
source_code_uri: https://github.com/ydah/licensure
|
|
74
|
+
changelog_uri: https://github.com/ydah/licensure/releases
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.1'
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 4.0.6
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: License compliance checker for Ruby dependencies
|
|
92
|
+
test_files: []
|