licensed 0.11.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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