licensed 1.5.2 → 2.0.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 +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG.md +22 -1
- data/CONTRIBUTING.md +2 -2
- data/README.md +17 -24
- data/Rakefile +2 -2
- data/docs/adding_a_new_source.md +93 -0
- data/docs/commands.md +81 -0
- data/docs/configuration.md +8 -8
- data/docs/migrating_to_newer_versions.md +3 -0
- data/docs/reporters.md +174 -0
- data/docs/sources/bundler.md +5 -5
- data/lib/licensed.rb +5 -14
- data/lib/licensed/cli.rb +23 -9
- data/lib/licensed/commands.rb +9 -0
- data/lib/licensed/commands/cache.rb +82 -0
- data/lib/licensed/commands/command.rb +112 -0
- data/lib/licensed/commands/list.rb +24 -0
- data/lib/licensed/commands/status.rb +49 -0
- data/lib/licensed/configuration.rb +3 -8
- data/lib/licensed/dependency.rb +116 -58
- data/lib/licensed/dependency_record.rb +76 -0
- data/lib/licensed/migrations.rb +7 -0
- data/lib/licensed/migrations/v2.rb +65 -0
- data/lib/licensed/reporters.rb +9 -0
- data/lib/licensed/reporters/cache_reporter.rb +76 -0
- data/lib/licensed/reporters/list_reporter.rb +69 -0
- data/lib/licensed/reporters/reporter.rb +119 -0
- data/lib/licensed/reporters/status_reporter.rb +67 -0
- data/lib/licensed/shell.rb +8 -10
- data/lib/licensed/sources.rb +15 -0
- data/lib/licensed/{source → sources}/bower.rb +14 -19
- data/lib/licensed/{source → sources}/bundler.rb +73 -48
- data/lib/licensed/{source → sources}/cabal.rb +40 -46
- data/lib/licensed/{source → sources}/dep.rb +15 -27
- data/lib/licensed/{source → sources}/git_submodule.rb +14 -19
- data/lib/licensed/{source → sources}/go.rb +28 -35
- data/lib/licensed/{source → sources}/manifest.rb +68 -90
- data/lib/licensed/{source → sources}/npm.rb +16 -25
- data/lib/licensed/{source → sources}/pip.rb +23 -25
- data/lib/licensed/sources/source.rb +69 -0
- data/lib/licensed/ui/shell.rb +4 -0
- data/lib/licensed/version.rb +6 -1
- data/licensed.gemspec +4 -4
- data/script/source-setup/bundler +1 -1
- metadata +32 -18
- data/lib/licensed/command/cache.rb +0 -82
- data/lib/licensed/command/list.rb +0 -43
- data/lib/licensed/command/status.rb +0 -79
- data/lib/licensed/command/version.rb +0 -18
- data/lib/licensed/license.rb +0 -68
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "fileutils"
|
3
|
+
require "forwardable"
|
4
|
+
require "licensee"
|
5
|
+
|
6
|
+
module Licensed
|
7
|
+
class DependencyRecord
|
8
|
+
include Licensee::ContentHelper
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
EXTENSION = "dep.yml".freeze
|
12
|
+
|
13
|
+
# Read an existing record file
|
14
|
+
#
|
15
|
+
# filename - A String path to the file
|
16
|
+
#
|
17
|
+
# Returns a Licensed::DependencyRecord
|
18
|
+
def self.read(filename)
|
19
|
+
return unless File.exist?(filename)
|
20
|
+
data = YAML.load_file(filename)
|
21
|
+
return if data.nil? || data.empty?
|
22
|
+
new(
|
23
|
+
licenses: data.delete("licenses"),
|
24
|
+
notices: data.delete("notices"),
|
25
|
+
metadata: data
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def_delegators :@metadata, :[], :[]=
|
30
|
+
attr_reader :licenses
|
31
|
+
attr_reader :notices
|
32
|
+
|
33
|
+
# Construct a new record
|
34
|
+
#
|
35
|
+
# licenses - a string, or array of strings, representing the content of each license
|
36
|
+
# notices - a string, or array of strings, representing the content of each legal notice
|
37
|
+
# metadata - a Hash of the metadata for the package
|
38
|
+
def initialize(licenses: [], notices: [], metadata: {})
|
39
|
+
@licenses = [licenses].flatten.compact
|
40
|
+
@notices = [notices].flatten.compact
|
41
|
+
@metadata = metadata
|
42
|
+
end
|
43
|
+
|
44
|
+
# Save the metadata and text to a file
|
45
|
+
#
|
46
|
+
# filename - The destination file to save record contents at
|
47
|
+
def save(filename)
|
48
|
+
data_to_save = @metadata.merge({
|
49
|
+
"licenses" => licenses,
|
50
|
+
"notices" => notices
|
51
|
+
})
|
52
|
+
|
53
|
+
FileUtils.mkdir_p(File.dirname(filename))
|
54
|
+
File.write(filename, data_to_save.to_yaml)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the content used to compare two licenses using normalization from
|
58
|
+
# `Licensee::CotentHelper`
|
59
|
+
def content
|
60
|
+
return if licenses.nil? || licenses.empty?
|
61
|
+
licenses.map do |license|
|
62
|
+
if license.is_a?(String)
|
63
|
+
license
|
64
|
+
elsif license.respond_to?(:[])
|
65
|
+
license["text"]
|
66
|
+
end
|
67
|
+
end.join
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns whether two records match based on their contents
|
71
|
+
def matches?(other)
|
72
|
+
return false unless other.is_a?(DependencyRecord)
|
73
|
+
self.content_normalized == other.content_normalized
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "licensed/shell"
|
3
|
+
|
4
|
+
module Licensed
|
5
|
+
module Migrations
|
6
|
+
class V2
|
7
|
+
YAML_FRONTMATTER_PATTERN = /\A---\s*\n(.*?\n?)^---\s*$\n?(.*)\z/m
|
8
|
+
TEXT_SEPARATOR = ("-" * 80).freeze
|
9
|
+
LICENSE_SEPARATOR = ("*" * 80).freeze
|
10
|
+
|
11
|
+
def self.migrate(config_path, shell = Licensed::UI::Shell.new)
|
12
|
+
shell.info "updating to v2"
|
13
|
+
|
14
|
+
shell.info "updating bundler configuration keys"
|
15
|
+
# replace all "rubygem" and "rubygems" configuration keys with "bundler"
|
16
|
+
# to account for the bundler source's `type` change from `rubygem` to `bundler`
|
17
|
+
File.write(config_path, File.read(config_path).gsub(/("?)rubygems?("?):/, "\\1bundler\\2:"))
|
18
|
+
|
19
|
+
shell.info "updating cached records"
|
20
|
+
# load the configuration to find and update cached contents
|
21
|
+
configuration = Licensed::Configuration.load_from(config_path)
|
22
|
+
configuration.apps.each do |app|
|
23
|
+
|
24
|
+
# move any bundler records from the `rubygem` folder to the `bundler` folder
|
25
|
+
rubygem_cache = app.cache_path.join("rubygem")
|
26
|
+
if rubygem_cache.exist?
|
27
|
+
File.rename rubygem_cache, app.cache_path.join("bundler")
|
28
|
+
end
|
29
|
+
|
30
|
+
app.sources.each do |source|
|
31
|
+
Dir.chdir app.cache_path.join(source.class.type) do
|
32
|
+
# licensed v1 cached records were stored as .txt files with YAML frontmatter
|
33
|
+
Dir["**/*.txt"].each do |file|
|
34
|
+
yaml, licenses, notices = parse_file(file)
|
35
|
+
|
36
|
+
# rename the rubygem type to bundler
|
37
|
+
yaml["type"] = "bundler" if yaml["type"] == "rubygem"
|
38
|
+
|
39
|
+
# set licenses and notices as yaml properties
|
40
|
+
yaml["licenses"] = licenses.map { |text| { "text" => text } }
|
41
|
+
yaml["notices"] = notices.map { |text| { "text" => text } }
|
42
|
+
|
43
|
+
# v2 records are stored in `.dep.yml` files
|
44
|
+
# write the new yaml contents to the new file and delete old file
|
45
|
+
new_file = file.gsub(".txt", ".dep.yml")
|
46
|
+
File.write(new_file, yaml.to_yaml)
|
47
|
+
File.delete(file)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# find the yaml and non-yaml data according to parsing logic from v1
|
55
|
+
def self.parse_file(filename)
|
56
|
+
match = File.read(filename).scrub.match(YAML_FRONTMATTER_PATTERN)
|
57
|
+
yaml = YAML.load(match[1])
|
58
|
+
# in v1, licenses and notices are separated by special text dividers
|
59
|
+
licenses, *notices = match[2].split(TEXT_SEPARATOR).map(&:strip)
|
60
|
+
licenses = licenses.split(LICENSE_SEPARATOR).map(&:strip)
|
61
|
+
[yaml, licenses, notices]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Licensed
|
3
|
+
module Reporters
|
4
|
+
class CacheReporter < Reporter
|
5
|
+
# Reports on an application configuration in a cache command run
|
6
|
+
#
|
7
|
+
# app - An application configuration
|
8
|
+
#
|
9
|
+
# Returns the result of the yielded method
|
10
|
+
# Note - must be called from inside the `report_run` scope
|
11
|
+
def report_app(app)
|
12
|
+
super do |report|
|
13
|
+
shell.info "Caching dependency records for #{app["name"]}"
|
14
|
+
yield report
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Reports on a dependency source enumerator in a cache command run.
|
19
|
+
# Shows the type and count of dependencies found by the source.
|
20
|
+
#
|
21
|
+
# source - A dependency source enumerator
|
22
|
+
#
|
23
|
+
# Returns the result of the yielded method
|
24
|
+
# Note - must be called from inside the `report_run` scope
|
25
|
+
def report_source(source)
|
26
|
+
super do |report|
|
27
|
+
shell.info " #{source.class.type}"
|
28
|
+
result = yield report
|
29
|
+
|
30
|
+
errored_reports = report.all_reports.select { |report| report.errors.any? }.to_a
|
31
|
+
if errored_reports.any?
|
32
|
+
shell.newline
|
33
|
+
shell.error " * Errors:"
|
34
|
+
errored_reports.each do |report|
|
35
|
+
display_metadata = report.map { |k, v| "#{k}: #{v}" }.join(", ")
|
36
|
+
|
37
|
+
shell.error " * #{report.name}"
|
38
|
+
shell.error " #{display_metadata}" unless display_metadata.empty?
|
39
|
+
report.errors.each do |error|
|
40
|
+
shell.error " - #{error}"
|
41
|
+
end
|
42
|
+
shell.newline
|
43
|
+
end
|
44
|
+
else
|
45
|
+
shell.confirm " * #{report.reports.size} #{source.class.type} dependencies"
|
46
|
+
end
|
47
|
+
|
48
|
+
result
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Reports on a dependency in a cache command run.
|
53
|
+
# Shows whether the dependency's record was cached or reused.
|
54
|
+
#
|
55
|
+
# dependency - An application dependency
|
56
|
+
#
|
57
|
+
# Returns the result of the yielded method
|
58
|
+
# Note - must be called from inside the `report_run` scope
|
59
|
+
def report_dependency(dependency)
|
60
|
+
super do |report|
|
61
|
+
result = yield report
|
62
|
+
|
63
|
+
if report.errors.any?
|
64
|
+
shell.error " Error #{dependency.name} (#{dependency.version})"
|
65
|
+
elsif report["cached"]
|
66
|
+
shell.info " Caching #{dependency.name} (#{dependency.version})"
|
67
|
+
else
|
68
|
+
shell.info " Using #{dependency.name} (#{dependency.version})"
|
69
|
+
end
|
70
|
+
|
71
|
+
result
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Licensed
|
4
|
+
module Reporters
|
5
|
+
class ListReporter < Reporter
|
6
|
+
# Reports on an application configuration in a list command run
|
7
|
+
#
|
8
|
+
# app - An application configuration
|
9
|
+
#
|
10
|
+
# Returns the result of the yielded method
|
11
|
+
# Note - must be called from inside the `report_run` scope
|
12
|
+
def report_app(app)
|
13
|
+
super do |report|
|
14
|
+
shell.info "Listing dependencies for #{app["name"]}"
|
15
|
+
yield report
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Reports on a dependency source enumerator in a list command run.
|
20
|
+
# Shows the type and count of dependencies found by the source.
|
21
|
+
#
|
22
|
+
# source - A dependency source enumerator
|
23
|
+
#
|
24
|
+
# Returns the result of the yielded method
|
25
|
+
# Note - must be called from inside the `report_run` scope
|
26
|
+
def report_source(source)
|
27
|
+
super do |report|
|
28
|
+
shell.info " #{source.class.type}"
|
29
|
+
result = yield report
|
30
|
+
|
31
|
+
errored_reports = report.all_reports.select { |report| report.errors.any? }.to_a
|
32
|
+
if errored_reports.any?
|
33
|
+
shell.newline
|
34
|
+
shell.error " * Errors:"
|
35
|
+
errored_reports.each do |report|
|
36
|
+
display_metadata = report.map { |k, v| "#{k}: #{v}" }.join(", ")
|
37
|
+
|
38
|
+
shell.error " * #{report.name}"
|
39
|
+
shell.error " #{display_metadata}" unless display_metadata.empty?
|
40
|
+
report.errors.each do |error|
|
41
|
+
shell.error " - #{error}"
|
42
|
+
end
|
43
|
+
shell.newline
|
44
|
+
end
|
45
|
+
else
|
46
|
+
shell.confirm " * #{report.reports.size} #{source.class.type} dependencies"
|
47
|
+
end
|
48
|
+
|
49
|
+
result
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Reports on a dependency in a list command run.
|
54
|
+
#
|
55
|
+
# dependency - An application dependency
|
56
|
+
#
|
57
|
+
# Returns the result of the yielded method
|
58
|
+
# Note - must be called from inside the `report_run` scope
|
59
|
+
def report_dependency(dependency)
|
60
|
+
super do |report|
|
61
|
+
result = yield report
|
62
|
+
shell.info " #{dependency.name} (#{dependency.version})"
|
63
|
+
|
64
|
+
result
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Licensed
|
3
|
+
module Reporters
|
4
|
+
class Reporter
|
5
|
+
class Report < Hash
|
6
|
+
attr_reader :name
|
7
|
+
attr_reader :target
|
8
|
+
def initialize(name:, target:)
|
9
|
+
super()
|
10
|
+
@name = name
|
11
|
+
@target = target
|
12
|
+
end
|
13
|
+
|
14
|
+
def reports
|
15
|
+
@reports ||= []
|
16
|
+
end
|
17
|
+
|
18
|
+
def errors
|
19
|
+
@errors ||= []
|
20
|
+
end
|
21
|
+
|
22
|
+
def all_reports
|
23
|
+
result = []
|
24
|
+
result << self
|
25
|
+
result.push(*reports.flat_map(&:all_reports))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class ReportingError < StandardError; end;
|
30
|
+
|
31
|
+
def initialize(shell = Licensed::UI::Shell.new)
|
32
|
+
@shell = shell
|
33
|
+
end
|
34
|
+
|
35
|
+
# Generate a report for a licensed command execution
|
36
|
+
# Yields a report object which can be used to view or add
|
37
|
+
# data generated for this run
|
38
|
+
#
|
39
|
+
# Returns the result of the yielded method
|
40
|
+
def report_run(command)
|
41
|
+
result = nil
|
42
|
+
@run_report = Report.new(name: nil, target: command)
|
43
|
+
begin
|
44
|
+
result = yield @run_report
|
45
|
+
ensure
|
46
|
+
@run_report = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
result
|
50
|
+
end
|
51
|
+
|
52
|
+
# Generate a report for a licensed app configuration
|
53
|
+
# Yields a report object which can be used to view or add
|
54
|
+
# data generated for this app
|
55
|
+
#
|
56
|
+
# app - An application configuration
|
57
|
+
#
|
58
|
+
# Returns the result of the yielded method
|
59
|
+
# Note - must be called from inside the `report_run` scope
|
60
|
+
def report_app(app)
|
61
|
+
raise ReportingError.new("Cannot call report_app with active app context") unless @app_report.nil?
|
62
|
+
raise ReportingError.new("Call report_run before report_app") if @run_report.nil?
|
63
|
+
result = nil
|
64
|
+
@app_report = Report.new(name: app["name"], target: app)
|
65
|
+
begin
|
66
|
+
result = yield @app_report
|
67
|
+
ensure
|
68
|
+
@run_report.reports << @app_report
|
69
|
+
@app_report = nil
|
70
|
+
end
|
71
|
+
|
72
|
+
result
|
73
|
+
end
|
74
|
+
|
75
|
+
# Generate a report for a licensed dependency source enumerator
|
76
|
+
# Yields a report object which can be used to view or add
|
77
|
+
# data generated for this dependency source
|
78
|
+
#
|
79
|
+
# source - A dependency source enumerator
|
80
|
+
#
|
81
|
+
# Returns the result of the yielded method
|
82
|
+
# Note - must be called from inside the `report_app` scope
|
83
|
+
def report_source(source)
|
84
|
+
raise ReportingError.new("Cannot call report_source with active source context") unless @source_report.nil?
|
85
|
+
raise ReportingError.new("Call report_app before report_source") if @app_report.nil?
|
86
|
+
result = nil
|
87
|
+
@source_report = Report.new(name: [@app_report.name, source.class.type].join("."), target: source)
|
88
|
+
begin
|
89
|
+
result = yield @source_report
|
90
|
+
ensure
|
91
|
+
@app_report.reports << @source_report
|
92
|
+
@source_report = nil
|
93
|
+
end
|
94
|
+
|
95
|
+
result
|
96
|
+
end
|
97
|
+
|
98
|
+
# Generate a report for a licensed dependency
|
99
|
+
# Yields a report object which can be used to view or add
|
100
|
+
# data generated for this dependency
|
101
|
+
#
|
102
|
+
# dependency - An application dependency
|
103
|
+
#
|
104
|
+
# Returns the result of the yielded method
|
105
|
+
# Note - must be called from inside the `report_source` scope
|
106
|
+
def report_dependency(dependency)
|
107
|
+
raise ReportingError.new("Call report_source before report_dependency") if @source_report.nil?
|
108
|
+
|
109
|
+
dependency_report = Report.new(name: [@source_report.name, dependency.name].join("."), target: dependency)
|
110
|
+
@source_report.reports << dependency_report
|
111
|
+
yield dependency_report
|
112
|
+
end
|
113
|
+
|
114
|
+
protected
|
115
|
+
|
116
|
+
attr_reader :shell
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Licensed
|
4
|
+
module Reporters
|
5
|
+
class StatusReporter < Reporter
|
6
|
+
# Generate a report for a licensed status command run
|
7
|
+
# Shows the errors found when checking status, as well as
|
8
|
+
# overall number of dependencies checked
|
9
|
+
#
|
10
|
+
# Returns the result of the yielded method
|
11
|
+
def report_app(app)
|
12
|
+
super do |report|
|
13
|
+
shell.info "Checking cached dependency records for #{app["name"]}"
|
14
|
+
|
15
|
+
result = yield report
|
16
|
+
|
17
|
+
all_reports = report.all_reports
|
18
|
+
errored_reports = all_reports.select { |report| report.errors.any? }.to_a
|
19
|
+
|
20
|
+
dependency_count = all_reports.select { |report| report.target.is_a?(Licensed::Dependency) }.size
|
21
|
+
error_count = errored_reports.sum { |report| report.errors.size }
|
22
|
+
|
23
|
+
if error_count > 0
|
24
|
+
shell.newline
|
25
|
+
shell.error "Errors:"
|
26
|
+
errored_reports.each do |report|
|
27
|
+
display_metadata = report.map { |k, v| "#{k}: #{v}" }.join(", ")
|
28
|
+
|
29
|
+
shell.error "* #{report.name}"
|
30
|
+
shell.error " #{display_metadata}" unless display_metadata.empty?
|
31
|
+
report.errors.each do |error|
|
32
|
+
shell.error " - #{error}"
|
33
|
+
end
|
34
|
+
shell.newline
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
shell.newline
|
39
|
+
shell.info "#{dependency_count} dependencies checked, #{error_count} errors found."
|
40
|
+
|
41
|
+
result
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Reports on a dependency in a status command run.
|
46
|
+
# Shows whether the dependency's status is valid in dot format
|
47
|
+
#
|
48
|
+
# dependency - An application dependency
|
49
|
+
#
|
50
|
+
# Returns the result of the yielded method
|
51
|
+
# Note - must be called from inside the `report_run` scope
|
52
|
+
def report_dependency(dependency)
|
53
|
+
super do |report|
|
54
|
+
result = yield report
|
55
|
+
|
56
|
+
if report.errors.empty?
|
57
|
+
shell.confirm(".", false)
|
58
|
+
else
|
59
|
+
shell.error("F", false)
|
60
|
+
end
|
61
|
+
|
62
|
+
result
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|