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