licensed 0.11.1 → 1.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 +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
|