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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +13 -4
  3. data/.rubocop.yml +3 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +13 -0
  6. data/CODE_OF_CONDUCT.md +14 -12
  7. data/CONTRIBUTING.md +51 -0
  8. data/Gemfile +2 -1
  9. data/{LICENSE.txt → LICENSE} +1 -1
  10. data/README.md +55 -76
  11. data/Rakefile +3 -2
  12. data/docs/configuration.md +131 -0
  13. data/docs/sources/bower.md +5 -0
  14. data/docs/sources/bundler.md +7 -0
  15. data/docs/sources/cabal.md +39 -0
  16. data/docs/sources/go.md +12 -0
  17. data/docs/sources/manifests.md +26 -0
  18. data/docs/sources/npm.md +3 -0
  19. data/docs/sources/stack.md +3 -0
  20. data/exe/licensed +1 -0
  21. data/lib/licensed.rb +9 -5
  22. data/lib/licensed/cli.rb +22 -14
  23. data/lib/licensed/command/cache.rb +46 -29
  24. data/lib/licensed/command/list.rb +17 -9
  25. data/lib/licensed/command/status.rb +78 -0
  26. data/lib/licensed/configuration.rb +127 -25
  27. data/lib/licensed/dependency.rb +8 -2
  28. data/lib/licensed/git.rb +39 -0
  29. data/lib/licensed/license.rb +1 -0
  30. data/lib/licensed/shell.rb +28 -0
  31. data/lib/licensed/source/bower.rb +4 -0
  32. data/lib/licensed/source/bundler.rb +4 -0
  33. data/lib/licensed/source/cabal.rb +72 -24
  34. data/lib/licensed/source/go.rb +23 -36
  35. data/lib/licensed/source/manifest.rb +26 -23
  36. data/lib/licensed/source/npm.rb +19 -8
  37. data/lib/licensed/ui/shell.rb +2 -1
  38. data/lib/licensed/version.rb +2 -1
  39. data/licensed.gemspec +9 -5
  40. data/{bin/setup → script/bootstrap} +13 -8
  41. data/script/cibuild +7 -0
  42. data/{bin → script}/console +1 -0
  43. metadata +53 -158
  44. data/.bowerrc +0 -3
  45. data/exe/licensor +0 -5
  46. data/lib/licensed/command/verify.rb +0 -73
  47. data/lib/licensed/source/stack.rb +0 -66
@@ -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 now providing license_file[:dir],
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
 
@@ -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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "yaml"
2
3
  require "fileutils"
3
4
  require "forwardable"
@@ -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
- require 'English'
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
- 'cabal'
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
- 'type' => type,
25
- 'name' => package['name'],
26
- 'version' => package['version'],
27
- 'summary' => package['synopsis'],
28
- 'homepage' => safe_homepage(package['homepage']),
29
- 'search_root' => search_root
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['haddock-html']
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, 'vendor', package['name']), nil]
41
+ return [File.join(@config.pwd, "vendor", package["name"]), nil]
39
42
  end
40
43
 
41
- html_dir = package['haddock-html']
42
- data_dir = package['data-dir']
44
+ html_dir = package["haddock-html"]
45
+ data_dir = package["data-dir"]
43
46
  return [html_dir, nil] unless data_dir
44
47
 
45
- # don't use a search root that isn't an ancestor of the haddock-html dir
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:/, 'https:')
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('depends:', '')
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
- ghc_pkg_field_command(id, fields, args)
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(':', 2).map(&:strip)
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, '--ipid')
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
- `ghc-pkg field #{id} #{fields.join(',')} #{args.join(' ')}`
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, '*.cabal'))
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 = `which ghc 2>/dev/null` && $CHILD_STATUS.success?
172
+ @ghc ||= Licensed::Shell.tool_available?("ghc")
125
173
  end
126
174
  end
127
175
  end
@@ -1,5 +1,6 @@
1
- require 'json'
2
- require 'English'
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
- 'go'
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?('type' => type, 'name' => package_name)
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['Dir']
30
+ package_dir = package["Dir"]
30
31
  Dependency.new(package_dir, {
31
- 'type' => type,
32
- 'name' => import_path,
33
- 'summary' => package['Doc'],
34
- 'homepage' => homepage(import_path),
35
- 'search_root' => search_root(package_dir),
36
- 'version' => package_version(package_dir)
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['Deps']
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['Deps']
58
+ root_package["Deps"]
58
59
  .uniq
59
60
  .select { |d| !go_std_packages.include?(d) }
60
- .select { |d| !d.start_with?(root_package['ImportPath']) || vendored_path?(d) }
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('^(.*/vendor)/.*$')[1] if vendored_path?(package_dir)
72
- ENV.fetch('GOPATH', nil)
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?('vendor/')
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('vendor/')[1]
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
- `go list -json #{package}`
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 ||= `go doc 2>/dev/null` && $CHILD_STATUS.success?
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 ||= `go list std`.lines.map(&:strip)
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