licensed 0.11.1 → 1.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 +13 -4
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +13 -0
- data/CODE_OF_CONDUCT.md +14 -12
- data/CONTRIBUTING.md +51 -0
- data/Gemfile +2 -1
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +55 -76
- data/Rakefile +3 -2
- data/docs/configuration.md +131 -0
- data/docs/sources/bower.md +5 -0
- data/docs/sources/bundler.md +7 -0
- data/docs/sources/cabal.md +39 -0
- data/docs/sources/go.md +12 -0
- data/docs/sources/manifests.md +26 -0
- data/docs/sources/npm.md +3 -0
- data/docs/sources/stack.md +3 -0
- data/exe/licensed +1 -0
- data/lib/licensed.rb +9 -5
- data/lib/licensed/cli.rb +22 -14
- data/lib/licensed/command/cache.rb +46 -29
- data/lib/licensed/command/list.rb +17 -9
- data/lib/licensed/command/status.rb +78 -0
- data/lib/licensed/configuration.rb +127 -25
- data/lib/licensed/dependency.rb +8 -2
- data/lib/licensed/git.rb +39 -0
- data/lib/licensed/license.rb +1 -0
- data/lib/licensed/shell.rb +28 -0
- data/lib/licensed/source/bower.rb +4 -0
- data/lib/licensed/source/bundler.rb +4 -0
- data/lib/licensed/source/cabal.rb +72 -24
- data/lib/licensed/source/go.rb +23 -36
- data/lib/licensed/source/manifest.rb +26 -23
- data/lib/licensed/source/npm.rb +19 -8
- data/lib/licensed/ui/shell.rb +2 -1
- data/lib/licensed/version.rb +2 -1
- data/licensed.gemspec +9 -5
- data/{bin/setup → script/bootstrap} +13 -8
- data/script/cibuild +7 -0
- data/{bin → script}/console +1 -0
- metadata +53 -158
- data/.bowerrc +0 -3
- data/exe/licensor +0 -5
- data/lib/licensed/command/verify.rb +0 -73
- data/lib/licensed/source/stack.rb +0 -66
data/lib/licensed/dependency.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "licensee"
|
2
3
|
|
3
4
|
module Licensed
|
@@ -11,9 +12,9 @@ module Licensed
|
|
11
12
|
@path = path
|
12
13
|
@search_root = metadata.delete("search_root")
|
13
14
|
|
14
|
-
# with licensee
|
15
|
+
# with licensee providing license_file[:dir],
|
15
16
|
# enforcing absolute paths makes life much easier when determining
|
16
|
-
# an absolute file path in notices
|
17
|
+
# an absolute file path in notices
|
17
18
|
unless Pathname.new(path).absolute?
|
18
19
|
raise "Dependency path #{path} must be absolute"
|
19
20
|
end
|
@@ -21,10 +22,14 @@ module Licensed
|
|
21
22
|
super metadata
|
22
23
|
end
|
23
24
|
|
25
|
+
# Returns a Licensee::Projects::FSProject for the dependency path
|
24
26
|
def project
|
25
27
|
@project ||= Licensee::Projects::FSProject.new(path, search_root: search_root, detect_packages: true, detect_readme: true)
|
26
28
|
end
|
27
29
|
|
30
|
+
# Detects license information and sets it on this dependency object.
|
31
|
+
# After calling `detect_license!``, the license is set at
|
32
|
+
# `dependency["license"]` and legal text is set to `dependency.text`
|
28
33
|
def detect_license!
|
29
34
|
self["license"] = license_key
|
30
35
|
self.text = ([license_text] + self.notices).compact.join("\n" + "-" * 80 + "\n")
|
@@ -35,6 +40,7 @@ module Licensed
|
|
35
40
|
local_files.uniq.map { |f| File.read(f) }
|
36
41
|
end
|
37
42
|
|
43
|
+
# Returns an array of file paths used to locate legal notices
|
38
44
|
def local_files
|
39
45
|
return [] unless Dir.exist?(path)
|
40
46
|
|
data/lib/licensed/git.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Licensed
|
3
|
+
module Git
|
4
|
+
class << self
|
5
|
+
# Returns whether git commands are available
|
6
|
+
def available?
|
7
|
+
@git ||= Licensed::Shell.tool_available?("git")
|
8
|
+
end
|
9
|
+
|
10
|
+
def repository_root
|
11
|
+
return unless available?
|
12
|
+
@root ||= Pathname.new(Licensed::Shell.execute("git", "rev-parse", "--show-toplevel"))
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the most recent git SHA for a file or directory
|
16
|
+
# or nil if SHA is not available
|
17
|
+
#
|
18
|
+
# descriptor - file or directory to retrieve latest SHA for
|
19
|
+
def version(descriptor)
|
20
|
+
return unless available? && descriptor
|
21
|
+
|
22
|
+
dir = File.directory?(descriptor) ? descriptor : File.dirname(descriptor)
|
23
|
+
file = File.directory?(descriptor) ? "." : File.basename(descriptor)
|
24
|
+
|
25
|
+
Dir.chdir dir do
|
26
|
+
Licensed::Shell.execute("git", "rev-list", "-1", "HEAD", "--", file)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the commit date for the provided SHA as a timestamp
|
31
|
+
#
|
32
|
+
# sha - commit sha to retrieve date
|
33
|
+
def commit_date(sha)
|
34
|
+
return unless available? && sha
|
35
|
+
Licensed::Shell.execute("git", "show", "-s", "-1", "--format=%ct", sha)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/licensed/license.rb
CHANGED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "open3"
|
3
|
+
|
4
|
+
module Licensed
|
5
|
+
module Shell
|
6
|
+
# Executes a command, returning it's STDOUT on success. Returns an empty
|
7
|
+
# string on failure
|
8
|
+
def self.execute(cmd, *args)
|
9
|
+
output, _, status = Open3.capture3(cmd, *args)
|
10
|
+
return "" unless status.success?
|
11
|
+
output.strip
|
12
|
+
end
|
13
|
+
|
14
|
+
# Executes a command and returns a boolean value indicating if the command
|
15
|
+
# was succesful
|
16
|
+
def self.success?(cmd, *args)
|
17
|
+
_, _, status = Open3.capture3(cmd, *args)
|
18
|
+
status.success?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns a boolean indicating whether a CLI tool is available in the
|
22
|
+
# current environment
|
23
|
+
def self.tool_available?(tool)
|
24
|
+
output, err, status = Open3.capture3("which", tool)
|
25
|
+
status.success? && !output.strip.empty? && err.strip.empty?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "json"
|
2
3
|
|
3
4
|
module Licensed
|
@@ -33,6 +34,7 @@ module Licensed
|
|
33
34
|
end
|
34
35
|
end
|
35
36
|
|
37
|
+
# Returns a parsed ".bowerrc" configuration, or an empty hash if not found
|
36
38
|
def bower_config
|
37
39
|
@bower_config ||= begin
|
38
40
|
path = @config.pwd.join(".bowerrc")
|
@@ -40,6 +42,8 @@ module Licensed
|
|
40
42
|
end
|
41
43
|
end
|
42
44
|
|
45
|
+
# Returns the expected path to bower components.
|
46
|
+
# Note this does not validate that the returned path is valid
|
43
47
|
def bower_path
|
44
48
|
pwd = bower_config["cwd"] ? Pathname.new(bower_config["cwd"]).expand_path : @config.pwd
|
45
49
|
pwd.join bower_config["directory"] || "bower_components"
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "bundler"
|
2
3
|
|
3
4
|
module Licensed
|
@@ -32,14 +33,17 @@ module Licensed
|
|
32
33
|
@definition ||= ::Bundler::Definition.build(gemfile_path, lockfile_path, nil)
|
33
34
|
end
|
34
35
|
|
36
|
+
# Returns the bundle definition groups, excluding test and development
|
35
37
|
def groups
|
36
38
|
definition.groups - [:test, :development]
|
37
39
|
end
|
38
40
|
|
41
|
+
# Returns the expected path to the Bundler Gemfile
|
39
42
|
def gemfile_path
|
40
43
|
@config.pwd.join ::Bundler.default_gemfile.basename.to_s
|
41
44
|
end
|
42
45
|
|
46
|
+
# Returns the expected path to the Bundler Gemfile.lock
|
43
47
|
def lockfile_path
|
44
48
|
@config.pwd.join ::Bundler.default_lockfile.basename.to_s
|
45
49
|
end
|
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "English"
|
2
3
|
|
3
4
|
module Licensed
|
4
5
|
module Source
|
@@ -8,7 +9,7 @@ module Licensed
|
|
8
9
|
end
|
9
10
|
|
10
11
|
def type
|
11
|
-
|
12
|
+
"cabal"
|
12
13
|
end
|
13
14
|
|
14
15
|
def enabled?
|
@@ -21,47 +22,53 @@ module Licensed
|
|
21
22
|
|
22
23
|
path, search_root = package_docs_dirs(package)
|
23
24
|
Dependency.new(path, {
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
25
|
+
"type" => type,
|
26
|
+
"name" => package["name"],
|
27
|
+
"version" => package["version"],
|
28
|
+
"summary" => package["synopsis"],
|
29
|
+
"homepage" => safe_homepage(package["homepage"]),
|
30
|
+
"search_root" => search_root
|
30
31
|
})
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
35
|
+
# Returns the packages document directory and search root directory
|
36
|
+
# as an array
|
34
37
|
def package_docs_dirs(package)
|
35
|
-
unless package[
|
38
|
+
unless package["haddock-html"]
|
36
39
|
# default to a local vendor directory if haddock-html property
|
37
40
|
# isn't available
|
38
|
-
return [File.join(@config.pwd,
|
41
|
+
return [File.join(@config.pwd, "vendor", package["name"]), nil]
|
39
42
|
end
|
40
43
|
|
41
|
-
html_dir = package[
|
42
|
-
data_dir = package[
|
44
|
+
html_dir = package["haddock-html"]
|
45
|
+
data_dir = package["data-dir"]
|
43
46
|
return [html_dir, nil] unless data_dir
|
44
47
|
|
45
|
-
#
|
46
|
-
unless Pathname.new(html_dir).fnmatch?(File.join(data_dir,
|
48
|
+
# only allow data directories that are ancestors of the html directory
|
49
|
+
unless Pathname.new(html_dir).fnmatch?(File.join(data_dir, "**"))
|
47
50
|
data_dir = nil
|
48
51
|
end
|
49
52
|
|
50
53
|
[html_dir, data_dir]
|
51
54
|
end
|
52
55
|
|
56
|
+
# Returns a homepage url that enforces https and removes url fragments
|
53
57
|
def safe_homepage(homepage)
|
54
58
|
return unless homepage
|
55
59
|
# use https and remove url fragment
|
56
|
-
homepage.gsub(/http:/,
|
57
|
-
.gsub(/#[^?]*\z/,
|
60
|
+
homepage.gsub(/http:/, "https:")
|
61
|
+
.gsub(/#[^?]*\z/, "")
|
58
62
|
end
|
59
63
|
|
64
|
+
# Returns a `Set` of the package ids for all cabal dependencies
|
60
65
|
def package_ids
|
61
66
|
deps = cabal_packages.flat_map { |n| package_dependencies(n, false) }
|
62
67
|
recursive_dependencies(deps)
|
63
68
|
end
|
64
69
|
|
70
|
+
# Recursively finds the dependencies for each cabal package.
|
71
|
+
# Returns a `Set` containing the package names for all dependencies
|
65
72
|
def recursive_dependencies(package_names, results = Set.new)
|
66
73
|
return [] if package_names.nil? || package_names.empty?
|
67
74
|
|
@@ -78,37 +85,70 @@ module Licensed
|
|
78
85
|
results.merge recursive_dependencies(dependencies, results)
|
79
86
|
end
|
80
87
|
|
88
|
+
# Returns an array of dependency package names for the cabal package
|
89
|
+
# given by `id`
|
81
90
|
def package_dependencies(id, full_id = true)
|
82
|
-
package_dependencies_command(id, full_id).gsub(
|
91
|
+
package_dependencies_command(id, full_id).gsub("depends:", "")
|
83
92
|
.split
|
84
93
|
.map(&:strip)
|
85
94
|
end
|
86
95
|
|
96
|
+
# Returns the output of running `ghc-pkg field depends` for a package id
|
97
|
+
# Optionally allows for interpreting the given id as an
|
98
|
+
# installed package id (`--ipid`)
|
87
99
|
def package_dependencies_command(id, full_id)
|
88
|
-
args = full_id ? '--ipid' : ''
|
89
100
|
fields = %w(depends)
|
90
101
|
|
91
|
-
|
102
|
+
if full_id
|
103
|
+
ghc_pkg_field_command(id, fields, "--ipid")
|
104
|
+
else
|
105
|
+
ghc_pkg_field_command(id, fields)
|
106
|
+
end
|
92
107
|
end
|
93
108
|
|
109
|
+
# Returns package information as a hash for the given id
|
94
110
|
def package_info(id)
|
95
111
|
package_info_command(id).lines.each_with_object({}) do |line, info|
|
96
|
-
key, value = line.split(
|
112
|
+
key, value = line.split(":", 2).map(&:strip)
|
97
113
|
next unless key && value
|
98
114
|
|
99
115
|
info[key] = value
|
100
116
|
end
|
101
117
|
end
|
102
118
|
|
119
|
+
# Returns the output of running `ghc-pkg field` to obtain package information
|
103
120
|
def package_info_command(id)
|
104
121
|
fields = %w(name version synopsis homepage haddock-html data-dir)
|
105
|
-
ghc_pkg_field_command(id, fields,
|
122
|
+
ghc_pkg_field_command(id, fields, "--ipid")
|
106
123
|
end
|
107
124
|
|
125
|
+
# Runs a `ghc-pkg field` command for a given set of fields and arguments
|
126
|
+
# Automatically includes ghc package DB locations in the command
|
108
127
|
def ghc_pkg_field_command(id, fields, *args)
|
109
|
-
|
128
|
+
Licensed::Shell.execute("ghc-pkg", "field", id, fields.join(","), *args, *package_db_args)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns an array of ghc package DB locations as specified in the app
|
132
|
+
# configuration
|
133
|
+
def package_db_args
|
134
|
+
return [] unless @config["cabal"]
|
135
|
+
Array(@config["cabal"]["ghc_package_db"]).map do |path|
|
136
|
+
next "--#{path}" if %w(global user).include?(path)
|
137
|
+
path = realized_ghc_package_path(path)
|
138
|
+
path = File.expand_path(path, @config.pwd)
|
139
|
+
|
140
|
+
next unless File.exist?(path)
|
141
|
+
"--package-db=#{path}"
|
142
|
+
end.compact
|
110
143
|
end
|
111
144
|
|
145
|
+
# Returns a ghc package path with template markers replaced by live
|
146
|
+
# data
|
147
|
+
def realized_ghc_package_path(path)
|
148
|
+
path.gsub("<ghc_version>", ghc_version)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Return an array of the top-level cabal packages for the current app
|
112
152
|
def cabal_packages
|
113
153
|
cabal_files.map do |f|
|
114
154
|
name_match = File.read(f).match(/^name:\s*(.*)$/)
|
@@ -116,12 +156,20 @@ module Licensed
|
|
116
156
|
end.compact
|
117
157
|
end
|
118
158
|
|
159
|
+
# Returns an array of the local directory cabal package files
|
119
160
|
def cabal_files
|
120
|
-
@cabal_files ||= Dir.glob(File.join(@config.pwd,
|
161
|
+
@cabal_files ||= Dir.glob(File.join(@config.pwd, "*.cabal"))
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns the ghc cli tool version
|
165
|
+
def ghc_version
|
166
|
+
return unless ghc?
|
167
|
+
@version ||= Licensed::Shell.execute("ghc", "--numeric-version")
|
121
168
|
end
|
122
169
|
|
170
|
+
# Returns whether the ghc cli tool is available
|
123
171
|
def ghc?
|
124
|
-
@ghc
|
172
|
+
@ghc ||= Licensed::Shell.tool_available?("ghc")
|
125
173
|
end
|
126
174
|
end
|
127
175
|
end
|
data/lib/licensed/source/go.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
|
2
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "json"
|
3
|
+
require "English"
|
3
4
|
|
4
5
|
module Licensed
|
5
6
|
module Source
|
@@ -9,7 +10,7 @@ module Licensed
|
|
9
10
|
end
|
10
11
|
|
11
12
|
def type
|
12
|
-
|
13
|
+
"go"
|
13
14
|
end
|
14
15
|
|
15
16
|
def enabled?
|
@@ -22,18 +23,18 @@ module Licensed
|
|
22
23
|
import_path = non_vendored_import_path(package_name)
|
23
24
|
|
24
25
|
if package.empty?
|
25
|
-
next if @config.ignored?(
|
26
|
+
next if @config.ignored?("type" => type, "name" => package_name)
|
26
27
|
raise "couldn't find package for #{import_path}"
|
27
28
|
end
|
28
29
|
|
29
|
-
package_dir = package[
|
30
|
+
package_dir = package["Dir"]
|
30
31
|
Dependency.new(package_dir, {
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
"type" => type,
|
33
|
+
"name" => import_path,
|
34
|
+
"summary" => package["Doc"],
|
35
|
+
"homepage" => homepage(import_path),
|
36
|
+
"search_root" => search_root(package_dir),
|
37
|
+
"version" => Licensed::Git.version(package_dir)
|
37
38
|
})
|
38
39
|
end.compact
|
39
40
|
end
|
@@ -50,14 +51,14 @@ module Licensed
|
|
50
51
|
|
51
52
|
# Returns an array of dependency package import paths
|
52
53
|
def packages
|
53
|
-
return [] unless root_package[
|
54
|
+
return [] unless root_package["Deps"]
|
54
55
|
|
55
56
|
# don't include go std packages
|
56
57
|
# don't include packages under the root project that aren't vendored
|
57
|
-
root_package[
|
58
|
+
root_package["Deps"]
|
58
59
|
.uniq
|
59
60
|
.select { |d| !go_std_packages.include?(d) }
|
60
|
-
.select { |d| !d.start_with?(root_package[
|
61
|
+
.select { |d| !d.start_with?(root_package["ImportPath"]) || vendored_path?(d) }
|
61
62
|
end
|
62
63
|
|
63
64
|
# Returns the root directory to search for a package license
|
@@ -68,18 +69,8 @@ module Licensed
|
|
68
69
|
# 1. vendor folder if package is vendored
|
69
70
|
# 2. GOPATH
|
70
71
|
# 3. nil (no search up directory hierarchy)
|
71
|
-
return package_dir.match(
|
72
|
-
ENV.fetch(
|
73
|
-
end
|
74
|
-
|
75
|
-
# Returns the most recent git SHA for a package, or nil if SHA is
|
76
|
-
# not available
|
77
|
-
#
|
78
|
-
# package_directory - package location
|
79
|
-
def package_version(package_directory)
|
80
|
-
return unless git? && package_directory
|
81
|
-
|
82
|
-
`cd #{package_directory} && git rev-list -1 HEAD -- .`.strip
|
72
|
+
return package_dir.match("^(.*/vendor)/.*$")[1] if vendored_path?(package_dir)
|
73
|
+
ENV.fetch("GOPATH", nil)
|
83
74
|
end
|
84
75
|
|
85
76
|
# Returns whether a package is vendored or not based on the package
|
@@ -87,7 +78,7 @@ module Licensed
|
|
87
78
|
#
|
88
79
|
# path - Package path to test
|
89
80
|
def vendored_path?(path)
|
90
|
-
path && path.include?(
|
81
|
+
path && path.include?("vendor/")
|
91
82
|
end
|
92
83
|
|
93
84
|
# Returns the import path parameter without the vendor component
|
@@ -96,7 +87,7 @@ module Licensed
|
|
96
87
|
def non_vendored_import_path(import_path)
|
97
88
|
return unless import_path
|
98
89
|
return import_path unless vendored_path?(import_path)
|
99
|
-
import_path.split(
|
90
|
+
import_path.split("vendor/")[1]
|
100
91
|
end
|
101
92
|
|
102
93
|
# Returns package information, or {} if package isn't found
|
@@ -112,7 +103,8 @@ module Licensed
|
|
112
103
|
#
|
113
104
|
# package - Go package import path
|
114
105
|
def package_info_command(package)
|
115
|
-
|
106
|
+
package ||= ""
|
107
|
+
Licensed::Shell.execute("go", "list", "-json", package)
|
116
108
|
end
|
117
109
|
|
118
110
|
# Returns the info for the package under test
|
@@ -122,17 +114,12 @@ module Licensed
|
|
122
114
|
|
123
115
|
# Returns whether go source is found
|
124
116
|
def go_source?
|
125
|
-
@go_source ||=
|
117
|
+
@go_source ||= Licensed::Shell.success?("go", "doc")
|
126
118
|
end
|
127
119
|
|
128
120
|
# Returns a list of go standard packages
|
129
121
|
def go_std_packages
|
130
|
-
@std_packages ||=
|
131
|
-
end
|
132
|
-
|
133
|
-
# Returns whether git commands are available
|
134
|
-
def git?
|
135
|
-
@git ||= `which git 2>/dev/null` && $CHILD_STATUS.success?
|
122
|
+
@std_packages ||= Licensed::Shell.execute("go", "list", "std").lines.map(&:strip)
|
136
123
|
end
|
137
124
|
end
|
138
125
|
end
|