licensed 1.5.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|