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
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Licensed
|
4
|
-
module
|
5
|
-
class GitSubmodule
|
4
|
+
module Sources
|
5
|
+
class GitSubmodule < Source
|
6
6
|
REMOTE_URL_ARGUMENT = "$(git remote get-url origin)".freeze
|
7
7
|
GIT_SUBMODULES_ARGUMENTS = [
|
8
8
|
"$displaypath", # path from repo root to submodule folder to find the name and submodule content
|
@@ -11,21 +11,13 @@ module Licensed
|
|
11
11
|
"$(git config --get remote.origin.url)", # use the configured remote origin url as the homepage
|
12
12
|
].freeze
|
13
13
|
|
14
|
-
def self.type
|
15
|
-
"git_submodule"
|
16
|
-
end
|
17
|
-
|
18
|
-
def initialize(config)
|
19
|
-
@config = config
|
20
|
-
end
|
21
|
-
|
22
14
|
def enabled?
|
23
15
|
return false unless Licensed::Shell.tool_available?("git") && Licensed::Git.git_repo?
|
24
16
|
gitmodules_path.exist?
|
25
17
|
end
|
26
18
|
|
27
|
-
def
|
28
|
-
|
19
|
+
def enumerate_dependencies
|
20
|
+
git_submodules_command.lines.map do |line|
|
29
21
|
displaypath, toplevel, version, homepage = line.strip.split
|
30
22
|
name = File.basename(displaypath)
|
31
23
|
submodule_path = if toplevel == @config.pwd.to_s
|
@@ -36,13 +28,16 @@ module Licensed
|
|
36
28
|
end
|
37
29
|
submodule_paths[name] = submodule_path
|
38
30
|
|
39
|
-
Licensed::Dependency.new(
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
31
|
+
Licensed::Dependency.new(
|
32
|
+
name: submodule_path,
|
33
|
+
version: version,
|
34
|
+
path: @config.pwd.join(displaypath),
|
35
|
+
metadata: {
|
36
|
+
"type" => self.class.type,
|
37
|
+
"name" => name,
|
38
|
+
"homepage" => homepage
|
39
|
+
}
|
40
|
+
)
|
46
41
|
end
|
47
42
|
end
|
48
43
|
|
@@ -3,33 +3,31 @@ require "json"
|
|
3
3
|
require "pathname"
|
4
4
|
|
5
5
|
module Licensed
|
6
|
-
module
|
7
|
-
class Go
|
8
|
-
def self.type
|
9
|
-
"go"
|
10
|
-
end
|
11
|
-
|
12
|
-
def initialize(config)
|
13
|
-
@config = config
|
14
|
-
end
|
15
|
-
|
6
|
+
module Sources
|
7
|
+
class Go < Source
|
16
8
|
def enabled?
|
17
9
|
Licensed::Shell.tool_available?("go") && go_source?
|
18
10
|
end
|
19
11
|
|
20
|
-
def
|
21
|
-
|
12
|
+
def enumerate_dependencies
|
13
|
+
with_configured_gopath do
|
22
14
|
packages.map do |package|
|
23
15
|
import_path = non_vendored_import_path(package["ImportPath"])
|
16
|
+
error = package.dig("Error", "Err") if package["Error"]
|
24
17
|
package_dir = package["Dir"]
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
18
|
+
|
19
|
+
Dependency.new(
|
20
|
+
name: import_path,
|
21
|
+
version: package_version(package),
|
22
|
+
path: package_dir,
|
23
|
+
search_root: search_root(package_dir),
|
24
|
+
errors: Array(error),
|
25
|
+
metadata: {
|
26
|
+
"type" => Go.type,
|
27
|
+
"summary" => package["Doc"],
|
28
|
+
"homepage" => homepage(import_path)
|
29
|
+
}
|
30
|
+
)
|
33
31
|
end
|
34
32
|
end
|
35
33
|
end
|
@@ -53,25 +51,18 @@ module Licensed
|
|
53
51
|
def root_package_deps
|
54
52
|
# check for ignored packages to avoid raising errors calling `go list`
|
55
53
|
# when ignored package is not found
|
56
|
-
Array(root_package["Deps"])
|
57
|
-
.reject { |name| @config.ignored?("type" => Go.type, "name" => name) }
|
58
|
-
.map { |name| package_info(name) }
|
54
|
+
Array(root_package["Deps"]).map { |name| package_info(name) }
|
59
55
|
end
|
60
56
|
|
61
57
|
# Returns the list of dependencies as returned by "go list -json -deps"
|
62
58
|
# available in go 1.11
|
63
59
|
def go_list_deps
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
deps.gsub!(/^}(?!\z)$/m, "},")
|
71
|
-
JSON.parse("[#{deps}]")
|
72
|
-
.reject { |pkg| @config.ignored?("type" => Go.type, "name" => pkg["ImportPath"]) }
|
73
|
-
.each { |pkg| raise pkg.dig("Error", "Err") if pkg["Error"] }
|
74
|
-
end
|
60
|
+
# the CLI command returns packages in a pretty-printed JSON format but
|
61
|
+
# not separated by commas. this gsub adds commas after all non-indented
|
62
|
+
# "}" that close root level objects.
|
63
|
+
# (?!\z) uses negative lookahead to not match the final "}"
|
64
|
+
deps = package_info_command("-deps").gsub(/^}(?!\z)$/m, "},")
|
65
|
+
JSON.parse("[#{deps}]")
|
75
66
|
end
|
76
67
|
|
77
68
|
# Returns whether the given package import path belongs to the
|
@@ -129,6 +120,8 @@ module Licensed
|
|
129
120
|
#
|
130
121
|
# package - package object obtained from package_info
|
131
122
|
def search_root(package_dir)
|
123
|
+
return nil if package_dir.nil? || package_dir.empty?
|
124
|
+
|
132
125
|
# search root choices:
|
133
126
|
# 1. vendor folder if package is vendored
|
134
127
|
# 2. GOPATH
|
@@ -165,7 +158,7 @@ module Licensed
|
|
165
158
|
#
|
166
159
|
# args - additional arguments to `go list`, e.g. Go package import path
|
167
160
|
def package_info_command(*args)
|
168
|
-
Licensed::Shell.execute("go", "list", "-json", *Array(args)).strip
|
161
|
+
Licensed::Shell.execute("go", "list", "-e", "-json", *Array(args)).strip
|
169
162
|
end
|
170
163
|
|
171
164
|
# Returns the info for the package under test
|
@@ -2,29 +2,22 @@
|
|
2
2
|
require "pathname/common_prefix"
|
3
3
|
|
4
4
|
module Licensed
|
5
|
-
module
|
6
|
-
class Manifest
|
7
|
-
def self.type
|
8
|
-
"manifest"
|
9
|
-
end
|
10
|
-
|
11
|
-
def initialize(config)
|
12
|
-
@config = config
|
13
|
-
end
|
14
|
-
|
5
|
+
module Sources
|
6
|
+
class Manifest < Source
|
15
7
|
def enabled?
|
16
8
|
File.exist?(manifest_path) || generate_manifest?
|
17
9
|
end
|
18
10
|
|
19
|
-
def
|
20
|
-
|
21
|
-
Licensed::
|
22
|
-
|
23
|
-
|
24
|
-
|
11
|
+
def enumerate_dependencies
|
12
|
+
packages.map do |package_name, sources|
|
13
|
+
Licensed::Sources::Manifest::Dependency.new(
|
14
|
+
name: package_name,
|
15
|
+
version: package_version(sources),
|
16
|
+
path: configured_license_path(package_name) || sources_license_path(sources),
|
17
|
+
sources: sources,
|
18
|
+
metadata: {
|
25
19
|
"type" => Manifest.type,
|
26
|
-
"name" => package_name
|
27
|
-
"version" => package_version(sources)
|
20
|
+
"name" => package_name
|
28
21
|
}
|
29
22
|
)
|
30
23
|
end
|
@@ -40,7 +33,7 @@ module Licensed
|
|
40
33
|
end
|
41
34
|
|
42
35
|
# Returns the license path for a package specified in the configuration.
|
43
|
-
def
|
36
|
+
def configured_license_path(package_name)
|
44
37
|
license_path = @config.dig("manifest", "licenses", package_name)
|
45
38
|
return unless license_path
|
46
39
|
|
@@ -49,6 +42,25 @@ module Licensed
|
|
49
42
|
license_path
|
50
43
|
end
|
51
44
|
|
45
|
+
# Returns the top-most directory that is common to all paths in `sources`
|
46
|
+
def sources_license_path(sources)
|
47
|
+
# if there is more than one source, try to find a directory common to
|
48
|
+
# all sources
|
49
|
+
if sources.size > 1
|
50
|
+
common_prefix = Pathname.common_prefix(*sources).to_path
|
51
|
+
|
52
|
+
# don't allow the workspace root to be used as common prefix
|
53
|
+
# the project this is run for should be excluded from the manifest,
|
54
|
+
# or ignored in the config. any license in the root should be ignored.
|
55
|
+
return common_prefix if common_prefix != @config.root
|
56
|
+
end
|
57
|
+
|
58
|
+
# use the first (or only) sources directory to find license information
|
59
|
+
source = sources.first
|
60
|
+
return File.dirname(source) if File.file?(source)
|
61
|
+
source
|
62
|
+
end
|
63
|
+
|
52
64
|
# Returns a map of package names -> array of full source paths found
|
53
65
|
# in the app manifest
|
54
66
|
def packages
|
@@ -98,7 +110,7 @@ module Licensed
|
|
98
110
|
def verify_configured_dependencies!
|
99
111
|
# verify that dependencies are configured
|
100
112
|
if configured_dependencies.empty?
|
101
|
-
raise "The manifest \"dependencies\" cannot be empty!"
|
113
|
+
raise Source::Error.new("The manifest \"dependencies\" cannot be empty!")
|
102
114
|
end
|
103
115
|
|
104
116
|
# verify all included files match a single configured dependency
|
@@ -116,7 +128,7 @@ module Licensed
|
|
116
128
|
end
|
117
129
|
|
118
130
|
errors.compact!
|
119
|
-
raise errors.join($/)
|
131
|
+
raise Source::Error.new(errors.join($/)) if errors.any?
|
120
132
|
end
|
121
133
|
|
122
134
|
# Returns the project dependencies specified from the licensed configuration
|
@@ -177,87 +189,53 @@ module Licensed
|
|
177
189
|
)
|
178
190
|
/imx.freeze
|
179
191
|
|
180
|
-
def initialize(
|
192
|
+
def initialize(name:, version:, path:, sources:, metadata: {})
|
181
193
|
@sources = sources
|
182
|
-
|
183
|
-
super license_path, metadata
|
184
|
-
end
|
185
|
-
|
186
|
-
# Detects license information and sets it on this dependency object.
|
187
|
-
# After calling `detect_license!``, the license is set at
|
188
|
-
# `dependency["license"]` and legal text is set to `dependency.text`
|
189
|
-
def detect_license!
|
190
|
-
# if no license key is found for the project, try to create a
|
191
|
-
# temporary LICENSE file from unique source file license headers
|
192
|
-
if license_key == "none"
|
193
|
-
tmp_license_file = write_license_from_source_licenses(self.path, @sources)
|
194
|
-
reset_license!
|
195
|
-
end
|
196
|
-
|
197
|
-
super
|
198
|
-
ensure
|
199
|
-
File.delete(tmp_license_file) if tmp_license_file && File.exist?(tmp_license_file)
|
194
|
+
super(name: name, version: version, path: path, metadata: metadata)
|
200
195
|
end
|
201
196
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
# return the source directory if there is only one source given
|
207
|
-
return source_directory(sources[0]) if sources.size == 1
|
208
|
-
|
209
|
-
common_prefix = Pathname.common_prefix(*sources).to_path
|
210
|
-
|
211
|
-
# don't allow the workspace root to be used as common prefix
|
212
|
-
# the project this is run for should be excluded from the manifest,
|
213
|
-
# or ignored in the config. any license in the root should be ignored.
|
214
|
-
return common_prefix if common_prefix != root
|
215
|
-
|
216
|
-
# use the first source directory as the license path.
|
217
|
-
source_directory(sources.first)
|
197
|
+
def project_files
|
198
|
+
files = super
|
199
|
+
files.concat(source_files) if files.empty?
|
200
|
+
files
|
218
201
|
end
|
219
202
|
|
220
|
-
# Returns
|
221
|
-
#
|
222
|
-
def
|
223
|
-
|
224
|
-
|
203
|
+
# Returns an enumeration of Licensee::ProjectFiles::LicenseFile
|
204
|
+
# representing licenses found in source header comments
|
205
|
+
def source_files
|
206
|
+
@source_files ||= begin
|
207
|
+
@sources
|
208
|
+
.select { |file| File.file?(file) }
|
209
|
+
.flat_map { |file| source_file_comments(file) }
|
210
|
+
.map do |comment, file|
|
211
|
+
Licensee::ProjectFiles::LicenseFile.new(comment, file)
|
212
|
+
end
|
213
|
+
end
|
225
214
|
end
|
226
215
|
|
227
|
-
|
228
|
-
# file at `dir`
|
229
|
-
# Returns the path to the license file
|
230
|
-
def write_license_from_source_licenses(dir, sources)
|
231
|
-
license_path = File.join(dir, "LICENSE")
|
232
|
-
File.open(license_path, "w") do |f|
|
233
|
-
licenses = source_comment_licenses(sources).uniq
|
234
|
-
f.puts(licenses.join("\n#{LICENSE_SEPARATOR}\n"))
|
235
|
-
end
|
216
|
+
private
|
236
217
|
|
237
|
-
|
218
|
+
# Returns all source header comments for a file
|
219
|
+
def source_file_comments(file)
|
220
|
+
file_parts = { dir: File.dirname(file), name: File.basename(file) }
|
221
|
+
content = File.read(file)
|
222
|
+
matches = content.scan(HEADER_LICENSE_REGEX)
|
223
|
+
matches.map { |match| [source_comment_text(match[0]), file_parts] }
|
238
224
|
end
|
239
225
|
|
240
|
-
# Returns
|
241
|
-
def
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
comments.map do |comment|
|
248
|
-
# strip leading "*" and whitespace
|
249
|
-
indent = nil
|
250
|
-
comment.lines.map do |line|
|
251
|
-
# find the length of the indent as the number of characters
|
252
|
-
# until the first word character
|
253
|
-
indent ||= line[/\A([^\w]*)\w/, 1]&.size
|
226
|
+
# Returns the comment text with leading * and whitespace stripped
|
227
|
+
def source_comment_text(comment)
|
228
|
+
indent = nil
|
229
|
+
comment.lines.map do |line|
|
230
|
+
# find the length of the indent as the number of characters
|
231
|
+
# until the first word character
|
232
|
+
indent ||= line[/\A([^\w]*)\w/, 1]&.size
|
254
233
|
|
255
|
-
|
256
|
-
|
234
|
+
# insert newline for each line until a word character is found
|
235
|
+
next "\n" unless indent
|
257
236
|
|
258
|
-
|
259
|
-
|
260
|
-
end
|
237
|
+
line[/([^\w\r\n]{0,#{indent}})(.*)/m, 2]
|
238
|
+
end.join
|
261
239
|
end
|
262
240
|
end
|
263
241
|
end
|
@@ -2,39 +2,36 @@
|
|
2
2
|
require "json"
|
3
3
|
|
4
4
|
module Licensed
|
5
|
-
module
|
6
|
-
class NPM
|
5
|
+
module Sources
|
6
|
+
class NPM < Source
|
7
7
|
def self.type
|
8
8
|
"npm"
|
9
9
|
end
|
10
10
|
|
11
|
-
def initialize(config)
|
12
|
-
@config = config
|
13
|
-
end
|
14
|
-
|
15
11
|
def enabled?
|
16
12
|
Licensed::Shell.tool_available?("npm") && File.exist?(@config.pwd.join("package.json"))
|
17
13
|
end
|
18
14
|
|
19
|
-
def
|
20
|
-
|
15
|
+
def enumerate_dependencies
|
16
|
+
packages.map do |name, package|
|
21
17
|
path = package["path"]
|
22
|
-
|
23
|
-
|
24
|
-
"
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
18
|
+
Dependency.new(
|
19
|
+
name: name,
|
20
|
+
version: package["version"],
|
21
|
+
path: path,
|
22
|
+
metadata: {
|
23
|
+
"type" => NPM.type,
|
24
|
+
"name" => package["name"],
|
25
|
+
"summary" => package["description"],
|
26
|
+
"homepage" => package["homepage"]
|
27
|
+
}
|
28
|
+
)
|
31
29
|
end
|
32
30
|
end
|
33
31
|
|
34
32
|
def packages
|
35
33
|
root_dependencies = JSON.parse(package_metadata_command)["dependencies"]
|
36
34
|
recursive_dependencies(root_dependencies).each_with_object({}) do |(name, results), hsh|
|
37
|
-
next if @config.ignored?("type" => NPM.type, "name" => name)
|
38
35
|
results.uniq! { |package| package["version"] }
|
39
36
|
if results.size == 1
|
40
37
|
hsh[name] = results[0]
|
@@ -59,13 +56,7 @@ module Licensed
|
|
59
56
|
|
60
57
|
# Returns the output from running `npm list` to get package metadata
|
61
58
|
def package_metadata_command
|
62
|
-
|
63
|
-
end
|
64
|
-
|
65
|
-
# Executes an `npm list` command with the provided args and returns the
|
66
|
-
# output from stdout
|
67
|
-
def npm_list_command(*args)
|
68
|
-
Licensed::Shell.execute("npm", "list", *args)
|
59
|
+
Licensed::Shell.execute("npm", "list", "--json", "--production", "--long", allow_failure: true)
|
69
60
|
end
|
70
61
|
end
|
71
62
|
end
|
@@ -3,42 +3,40 @@ require "json"
|
|
3
3
|
require "English"
|
4
4
|
|
5
5
|
module Licensed
|
6
|
-
module
|
7
|
-
class Pip
|
8
|
-
|
9
|
-
|
10
|
-
end
|
11
|
-
|
12
|
-
def initialize(config)
|
13
|
-
@config = config
|
14
|
-
end
|
6
|
+
module Sources
|
7
|
+
class Pip < Source
|
8
|
+
VERSION_OPERATORS = %w(< > <= >= == !=).freeze
|
9
|
+
PACKAGE_REGEX = /^(\w+)(#{VERSION_OPERATORS.join("|")})?/
|
15
10
|
|
16
11
|
def enabled?
|
17
12
|
return unless virtual_env_pip && Licensed::Shell.tool_available?(virtual_env_pip)
|
18
13
|
File.exist?(@config.pwd.join("requirements.txt"))
|
19
14
|
end
|
20
15
|
|
21
|
-
def
|
22
|
-
|
16
|
+
def enumerate_dependencies
|
17
|
+
packages_from_requirements_txt.map do |package_name|
|
23
18
|
package = package_info(package_name)
|
24
19
|
location = File.join(package["Location"], package["Name"] + "-" + package["Version"] + ".dist-info")
|
25
|
-
Dependency.new(
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
20
|
+
Dependency.new(
|
21
|
+
name: package["Name"],
|
22
|
+
version: package["Version"],
|
23
|
+
path: location,
|
24
|
+
metadata: {
|
25
|
+
"type" => Pip.type,
|
26
|
+
"summary" => package["Summary"],
|
27
|
+
"homepage" => package["Home-page"]
|
28
|
+
}
|
29
|
+
)
|
32
30
|
end
|
33
31
|
end
|
34
32
|
|
35
|
-
|
36
|
-
|
37
|
-
def
|
38
|
-
File.
|
39
|
-
|
40
|
-
|
41
|
-
|
33
|
+
private
|
34
|
+
|
35
|
+
def packages_from_requirements_txt
|
36
|
+
File.read(@config.pwd.join("requirements.txt"))
|
37
|
+
.lines
|
38
|
+
.map { |line| line.strip.match(PACKAGE_REGEX) { |match| match.captures.first } }
|
39
|
+
.compact
|
42
40
|
end
|
43
41
|
|
44
42
|
def package_info(package_name)
|