license_finder 6.5.0 → 6.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/CONTRIBUTING.md +5 -4
  4. data/Dockerfile +26 -9
  5. data/README.md +53 -15
  6. data/Rakefile +1 -10
  7. data/VERSION +1 -1
  8. data/ci/pipelines/pull-request.yml.erb +2 -0
  9. data/ci/pipelines/release.yml.erb +16 -4
  10. data/ci/tasks/rubocop.yml +2 -0
  11. data/ci/tasks/update-changelog.yml +2 -0
  12. data/examples/Gemfile +4 -0
  13. data/examples/custom_erb_template.rb +24 -0
  14. data/examples/extract_license_data.rb +63 -0
  15. data/examples/sample_template.erb +7 -0
  16. data/lib/license_finder/cli/base.rb +8 -1
  17. data/lib/license_finder/cli/inherited_decisions.rb +18 -0
  18. data/lib/license_finder/cli/main.rb +5 -1
  19. data/lib/license_finder/configuration.rb +13 -1
  20. data/lib/license_finder/core.rb +5 -2
  21. data/lib/license_finder/decisions.rb +58 -10
  22. data/lib/license_finder/license.rb +45 -1
  23. data/lib/license_finder/license/definitions.rb +49 -2
  24. data/lib/license_finder/license/header_matcher.rb +7 -2
  25. data/lib/license_finder/license/templates/0BSD.txt +10 -0
  26. data/lib/license_finder/license/templates/MPL1_1.txt +469 -0
  27. data/lib/license_finder/license/text.rb +2 -2
  28. data/lib/license_finder/logger.rb +2 -0
  29. data/lib/license_finder/package.rb +2 -0
  30. data/lib/license_finder/package_manager.rb +15 -5
  31. data/lib/license_finder/package_managers/composer.rb +8 -4
  32. data/lib/license_finder/package_managers/conda.rb +131 -0
  33. data/lib/license_finder/package_managers/dep.rb +6 -1
  34. data/lib/license_finder/package_managers/dotnet.rb +2 -1
  35. data/lib/license_finder/package_managers/erlangmk.rb +50 -0
  36. data/lib/license_finder/package_managers/go_15vendorexperiment.rb +6 -1
  37. data/lib/license_finder/package_managers/go_dep.rb +15 -8
  38. data/lib/license_finder/package_managers/go_modules.rb +43 -15
  39. data/lib/license_finder/package_managers/mix.rb +1 -1
  40. data/lib/license_finder/package_managers/npm.rb +1 -1
  41. data/lib/license_finder/package_managers/nuget.rb +36 -1
  42. data/lib/license_finder/package_managers/pipenv.rb +1 -1
  43. data/lib/license_finder/package_managers/rebar.rb +29 -8
  44. data/lib/license_finder/package_managers/trash.rb +6 -1
  45. data/lib/license_finder/package_managers/yarn.rb +1 -1
  46. data/lib/license_finder/packages/conda_package.rb +74 -0
  47. data/lib/license_finder/packages/erlangmk_package.rb +114 -0
  48. data/lib/license_finder/packages/pip_package.rb +9 -2
  49. data/lib/license_finder/report.rb +1 -0
  50. data/lib/license_finder/reports/junit_report.rb +19 -0
  51. data/lib/license_finder/reports/templates/junit_report.erb +41 -0
  52. data/lib/license_finder/scanner.rb +25 -2
  53. data/license_finder.gemspec +3 -2
  54. metadata +41 -9
@@ -10,8 +10,8 @@ module LicenseFinder
10
10
  SPECIAL_DOUBLE_QUOTES = /[“”„«»]/.freeze
11
11
  ALPHABET_ORDERED_LIST = /\\\([a-z]\\\)\\\s/.freeze
12
12
  ALPHABET_ORDERED_LIST_OPTIONAL = '(\([a-z]\)\s)?'
13
- LIST_BULLETS = /(\d{1,2}\\\.|\\\*)\\\s/.freeze
14
- LIST_BULLETS_OPTIONAL = '(\d{1,2}.|\*)?\s*'
13
+ LIST_BULLETS = /(\d{1,2}\\\.|\\\*|\\\-)\\\s/.freeze
14
+ LIST_BULLETS_OPTIONAL = '(\d{1,2}.|\*|\-)?\s*'
15
15
  NEWLINE_CHARACTER = /\n+/.freeze
16
16
  QUOTE_COMMENT_CHARACTER = /^\s*\>+/.freeze
17
17
  ESCAPED_QUOTES = /\\\"/.freeze
@@ -36,6 +36,8 @@ module LicenseFinder
36
36
  "\e[31m#{string}\e[0m"
37
37
  when :green
38
38
  "\e[32m#{string}\e[0m"
39
+ when :magenta
40
+ "\e[35m#{string}\e[0m"
39
41
  else
40
42
  string
41
43
  end
@@ -189,6 +189,7 @@ require 'license_finder/packages/gradle_package'
189
189
  require 'license_finder/packages/cocoa_pods_package'
190
190
  require 'license_finder/packages/carthage_package'
191
191
  require 'license_finder/packages/rebar_package'
192
+ require 'license_finder/packages/erlangmk_package'
192
193
  require 'license_finder/packages/mix_package'
193
194
  require 'license_finder/packages/merged_package'
194
195
  require 'license_finder/packages/nuget_package'
@@ -197,3 +198,4 @@ require 'license_finder/packages/yarn_package'
197
198
  require 'license_finder/packages/sbt_package'
198
199
  require 'license_finder/packages/cargo_package'
199
200
  require 'license_finder/packages/composer_package'
201
+ require 'license_finder/packages/conda_package'
@@ -22,6 +22,10 @@ module LicenseFinder
22
22
  def takes_priority_over
23
23
  nil
24
24
  end
25
+
26
+ def id
27
+ name.split('::').last.downcase
28
+ end
25
29
  end
26
30
 
27
31
  def installed?(logger = Core.default_logger)
@@ -119,12 +123,16 @@ module LicenseFinder
119
123
  attr_reader :logger, :project_path
120
124
 
121
125
  def log_errors(stderr)
122
- logger.info prepare_command, 'did not succeed.', color: :red
123
- logger.info prepare_command, stderr, color: :red
124
- log_to_file stderr
126
+ log_errors_with_cmd(prepare_command, stderr)
127
+ end
128
+
129
+ def log_errors_with_cmd(prep_cmd, stderr)
130
+ logger.info(prep_cmd, 'did not succeed.', color: :red)
131
+ logger.info(prep_cmd, stderr, color: :red)
132
+ log_to_file(prep_cmd, stderr)
125
133
  end
126
134
 
127
- def log_to_file(contents)
135
+ def log_to_file(prep_cmd, contents)
128
136
  FileUtils.mkdir_p @log_directory
129
137
 
130
138
  # replace whitespace with underscores and remove slashes
@@ -132,7 +140,7 @@ module LicenseFinder
132
140
  log_file = File.join(@log_directory, "prepare_#{log_file_name || 'errors'}.log")
133
141
 
134
142
  File.open(log_file, 'w') do |f|
135
- f.write("Prepare command \"#{prepare_command}\" failed with:\n")
143
+ f.write("Prepare command \"#{prep_cmd}\" failed with:\n")
136
144
  f.write("#{contents}\n\n")
137
145
  end
138
146
  end
@@ -159,6 +167,7 @@ require 'license_finder/package_managers/cocoa_pods'
159
167
  require 'license_finder/package_managers/carthage'
160
168
  require 'license_finder/package_managers/gradle'
161
169
  require 'license_finder/package_managers/rebar'
170
+ require 'license_finder/package_managers/erlangmk'
162
171
  require 'license_finder/package_managers/nuget'
163
172
  require 'license_finder/package_managers/dotnet'
164
173
  require 'license_finder/package_managers/dep'
@@ -166,5 +175,6 @@ require 'license_finder/package_managers/conan'
166
175
  require 'license_finder/package_managers/sbt'
167
176
  require 'license_finder/package_managers/cargo'
168
177
  require 'license_finder/package_managers/composer'
178
+ require 'license_finder/package_managers/conda'
169
179
 
170
180
  require 'license_finder/package'
@@ -4,7 +4,10 @@ require 'json'
4
4
 
5
5
  module LicenseFinder
6
6
  class Composer < PackageManager
7
- SHELL_COMMAND = 'composer licenses --format=json'
7
+ def initialize(options = {})
8
+ super
9
+ @check_require_only = !!options[:composer_check_require_only]
10
+ end
8
11
 
9
12
  def possible_package_paths
10
13
  [project_path.join('composer.lock'), project_path.join('composer.json')]
@@ -33,7 +36,7 @@ module LicenseFinder
33
36
  end
34
37
 
35
38
  def prepare_command
36
- 'composer install --no-plugins --ignore-platform-reqs --no-interaction'
39
+ 'composer install --no-plugins --no-scripts --ignore-platform-reqs --no-interaction'
37
40
  end
38
41
 
39
42
  def package_path
@@ -50,8 +53,9 @@ module LicenseFinder
50
53
  end
51
54
 
52
55
  def composer_json
53
- stdout, stderr, status = Dir.chdir(project_path) { Cmd.run(Composer::SHELL_COMMAND) }
54
- raise "Command '#{Composer::SHELL_COMMAND}' failed to execute: #{stderr}" unless status.success?
56
+ command = "composer licenses --format=json#{@check_require_only ? ' --no-dev' : ''}"
57
+ stdout, stderr, status = Dir.chdir(project_path) { Cmd.run(command) }
58
+ raise "Command '#{command}' failed to execute: #{stderr}" unless status.success?
55
59
 
56
60
  JSON(stdout)
57
61
  end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module LicenseFinder
6
+ class Conda < PackageManager
7
+ attr_reader :conda_bash_setup_script
8
+
9
+ def initialize(options = {})
10
+ @conda_bash_setup_script = options[:conda_bash_setup_script] || Pathname("#{ENV['HOME']}/miniconda3/etc/profile.d/conda.sh")
11
+ super
12
+ end
13
+
14
+ # This command is *not* directly executable. See .conda() below.
15
+ def prepare_command
16
+ "conda env create -f #{detected_package_path}"
17
+ end
18
+
19
+ def prepare
20
+ return if environment_exists?
21
+
22
+ prep_cmd = prepare_command
23
+ _stdout, stderr, status = Dir.chdir(project_path) { conda(prep_cmd) }
24
+ return if status.success?
25
+
26
+ log_errors stderr
27
+ raise "Prepare command '#{prep_cmd}' failed" unless @prepare_no_fail
28
+ end
29
+
30
+ def current_packages
31
+ conda_list.map do |entry|
32
+ case entry['channel']
33
+ when 'pypi'
34
+ # PyPI is much faster than `conda search`, use it when we can.
35
+ PipPackage.new(entry['name'], entry['version'], PyPI.definition(entry['name'], entry['version']))
36
+ else
37
+ CondaPackage.new(conda_search_info(entry))
38
+ end
39
+ end.compact
40
+ end
41
+
42
+ def possible_package_paths
43
+ [project_path.join('environment.yaml'), project_path.join('environment.yml')]
44
+ end
45
+
46
+ private
47
+
48
+ def environment_exists?
49
+ environments.grep(environment_name).any?
50
+ end
51
+
52
+ def environments
53
+ command = 'conda env list'
54
+ stdout, stderr, status = conda command
55
+
56
+ environments = []
57
+ if status.success?
58
+ environments = stdout.split("\n").grep_v(/^#/).map { |line| line.split.first }
59
+ else
60
+ log_errors_with_cmd command, stderr
61
+ end
62
+ environments
63
+ end
64
+
65
+ def environment_file
66
+ detected_package_path
67
+ end
68
+
69
+ def environment_name
70
+ @environment_name ||= YAML.load_file(environment_file).fetch('name')
71
+ end
72
+
73
+ def conda(command)
74
+ Open3.capture3('bash', '-c', "source #{conda_bash_setup_script} && #{command}")
75
+ end
76
+
77
+ def activated_conda(command)
78
+ Open3.capture3('bash', '-c', "source #{conda_bash_setup_script} && conda activate #{environment_name} && #{command}")
79
+ end
80
+
81
+ # Algorithm is based on
82
+ # https://bioinformatics.stackexchange.com/a/11226
83
+ # but completely recoded in Ruby. Like the poster, if the package is
84
+ # actually managed by conda, we assume that all the potential infos (for
85
+ # various architectures, versions of python, etc) have the same license.
86
+ def conda_list
87
+ command = 'conda list'
88
+ stdout, stderr, status = activated_conda(command)
89
+
90
+ if status.success?
91
+ conda_list = []
92
+ stdout.each_line do |line|
93
+ next if line =~ /^\s*#/
94
+
95
+ name, version, build, channel = line.split
96
+ conda_list << {
97
+ 'name' => name,
98
+ 'version' => version,
99
+ 'build' => build,
100
+ 'channel' => channel
101
+ }
102
+ end
103
+ conda_list
104
+ else
105
+ log_errors_with_cmd command, stderr
106
+ []
107
+ end
108
+ end
109
+
110
+ def conda_search_info(list_entry)
111
+ command = 'conda search --info --json '
112
+ command += "--channel #{list_entry['channel']} " if list_entry['channel'] && !list_entry['channel'].empty?
113
+ command += "'#{list_entry['name']} #{list_entry['version']}'"
114
+
115
+ # Errors from conda (in --json mode, at least) show up in stdout, not stderr
116
+ stdout, _stderr, status = activated_conda(command)
117
+
118
+ name = list_entry['name']
119
+
120
+ if status.success?
121
+ JSON(stdout).fetch(name).first
122
+ else
123
+ log_errors_with_cmd command, stdout
124
+ list_entry
125
+ end
126
+ rescue KeyError
127
+ logger.info('Conda', "Key error trying to find #{name} in\n#{JSON(stdout)}")
128
+ list_entry
129
+ end
130
+ end
131
+ end
@@ -18,11 +18,16 @@ module LicenseFinder
18
18
  GoPackage.from_dependency({
19
19
  'ImportPath' => project['name'],
20
20
  'InstallPath' => project_path.join('vendor', project['name']),
21
- 'Rev' => project['revision']
21
+ 'Rev' => project['revision'],
22
+ 'Homepage' => repo_name(project['name'])
22
23
  }, nil, true)
23
24
  end
24
25
  end
25
26
 
27
+ def repo_name(name)
28
+ name.split('/')[0..2].join('/')
29
+ end
30
+
26
31
  def self.takes_priority_over
27
32
  Go15VendorExperiment
28
33
  end
@@ -63,7 +63,8 @@ module LicenseFinder
63
63
  .uniq { |d| [d.name, d.version] }
64
64
 
65
65
  package_metadatas.map do |d|
66
- NugetPackage.new(d.name, d.version, spec_licenses: d.read_license_urls)
66
+ path = Dir.glob("#{Dir.home}/.nuget/packages/#{d.name.downcase}/#{d.version}").first
67
+ NugetPackage.new(d.name, d.version, spec_licenses: d.read_license_urls, install_path: path)
67
68
  end
68
69
  end
69
70
 
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LicenseFinder
4
+ class Erlangmk < PackageManager
5
+ def package_management_command
6
+ 'make'
7
+ end
8
+
9
+ def package_management_command_with_path
10
+ "#{package_management_command} --directory=#{project_path} --no-print-directory"
11
+ end
12
+
13
+ # The IS_DEP=1 is added because not all erlang.mk-based projects are
14
+ # updated to a version that is compatible with LicenseFinder
15
+ def prepare_command
16
+ "#{package_management_command_with_path} IS_DEP=1 fetch-deps"
17
+ end
18
+
19
+ def possible_package_paths
20
+ [
21
+ project_path.join('Erlang.mk'),
22
+ project_path.join('erlang.mk')
23
+ ]
24
+ end
25
+
26
+ def current_packages
27
+ deps.map do |dep|
28
+ ErlangmkPackage.new(dep)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def deps
35
+ command = "#{package_management_command_with_path} QUERY='name fetch_method repo version absolute_path' query-deps"
36
+ stdout, stderr, status = Cmd.run(command)
37
+ if status.success?
38
+ dep_re = Regexp.new('^\s*DEP')
39
+ line_re = Regexp.new('^[_a-z0-9]+:')
40
+ stdout.each_line.map(&:strip).select { |line| !(line.start_with?('make') || line =~ dep_re) && line =~ line_re }
41
+ elsif stderr.include? "No rule to make target 'query-deps'"
42
+ # The stderr check happens because not all erlang.mk-based projects are
43
+ # updated to a version that is compatible with LicenseFinder
44
+ []
45
+ else
46
+ raise "Command '#{command}' failed to execute: #{stderr}"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -37,11 +37,16 @@ module LicenseFinder
37
37
  GoPackage.from_dependency({
38
38
  'ImportPath' => dep,
39
39
  'InstallPath' => detected_package_path.join(dep),
40
- 'Rev' => 'vendored-' + project_sha(detected_package_path.join(dep))
40
+ 'Rev' => 'vendored-' + project_sha(detected_package_path.join(dep)),
41
+ 'Homepage' => repo_name(dep)
41
42
  }, nil, true)
42
43
  end
43
44
  end
44
45
 
46
+ def repo_name(name)
47
+ name.split('/')[0..2].join('/')
48
+ end
49
+
45
50
  def package_management_command
46
51
  'go'
47
52
  end
@@ -4,6 +4,9 @@ require 'json'
4
4
 
5
5
  module LicenseFinder
6
6
  class GoDep < PackageManager
7
+ OLD_GODEP_VENDOR_PATH = 'Godeps/_workspace/src'
8
+ GODEP_VENDOR_PATH = 'vendor'
9
+
7
10
  def initialize(options = {})
8
11
  super
9
12
  @full_version = options[:go_full_version]
@@ -29,16 +32,20 @@ module LicenseFinder
29
32
  private
30
33
 
31
34
  def install_prefix
32
- go_path = if workspace_dir.directory?
33
- workspace_dir
34
- else
35
- Pathname(ENV['GOPATH'] || ENV['HOME'] + '/go')
36
- end
37
- go_path.join('src')
35
+ @install_prefix ||= if project_path.join(OLD_GODEP_VENDOR_PATH).directory?
36
+ project_path.join(OLD_GODEP_VENDOR_PATH)
37
+ elsif project_path.join(GODEP_VENDOR_PATH).directory?
38
+ project_path.join(GODEP_VENDOR_PATH)
39
+ else
40
+ download_dependencies
41
+ Pathname(ENV['GOPATH'] ? ENV['GOPATH'] + '/src' : ENV['HOME'] + '/go/src')
42
+ end
38
43
  end
39
44
 
40
- def workspace_dir
41
- project_path.join('Godeps/_workspace')
45
+ def download_dependencies
46
+ command = "#{package_management_command} restore"
47
+ _, stderr, status = Dir.chdir(project_path) { Cmd.run(command) }
48
+ raise "Command '#{command}' failed to execute: #{stderr}" if !status.success? && status.exitstatus != 1
42
49
  end
43
50
 
44
51
  def packages_from_json(json_string)
@@ -4,7 +4,7 @@ require 'license_finder/packages/go_package'
4
4
 
5
5
  module LicenseFinder
6
6
  class GoModules < PackageManager
7
- PACKAGES_FILE = 'go.sum'
7
+ PACKAGES_FILE = 'go.mod'
8
8
 
9
9
  class << self
10
10
  def takes_priority_over
@@ -12,12 +12,8 @@ module LicenseFinder
12
12
  end
13
13
  end
14
14
 
15
- def prepare_command
16
- 'GO111MODULE=on go mod tidy && GO111MODULE=on go mod vendor'
17
- end
18
-
19
15
  def active?
20
- sum_files?
16
+ mod_files?
21
17
  end
22
18
 
23
19
  def current_packages
@@ -33,19 +29,46 @@ module LicenseFinder
33
29
  private
34
30
 
35
31
  def packages_info
36
- info_output, stderr, _status = Cmd.run("GO111MODULE=on go list -m -f '{{.Path}},{{.Version}},{{.Dir}}' all")
37
- if stderr =~ Regexp.compile("can't compute 'all' using the vendor directory")
38
- info_output, _stderr, _status = Cmd.run("GO111MODULE=on go list -m -mod=mod -f '{{.Path}},{{.Version}},{{.Dir}}' all")
39
- end
32
+ Dir.chdir(project_path) do
33
+ # Explanations:
34
+ # * Only list dependencies (packages not listed in the project directory)
35
+ # (.DepOnly)
36
+ # * Ignore standard library packages
37
+ # (not .Standard)
38
+ # * Replacement modules are respected
39
+ # (or .Module.Replace .Module)
40
+ # * Module cache directory or (vendored) package directory
41
+ # (or $mod.Dir .Dir)
42
+ format_str = \
43
+ '{{ if and (.DepOnly) (not .Standard) }}'\
44
+ '{{ $mod := (or .Module.Replace .Module) }}'\
45
+ '{{ $mod.Path }},{{ $mod.Version }},{{ or $mod.Dir .Dir }}'\
46
+ '{{ end }}'
47
+
48
+ # The module list flag (`-m`) is intentionally not used here. If the module
49
+ # dependency tree were followed, transitive dependencies that are never imported
50
+ # may be included.
51
+ #
52
+ # Instead, the owning module is listed for each imported package. This better
53
+ # matches the implementation of other Go package managers.
54
+ #
55
+ # TODO: Figure out a way to make the vendor directory work (i.e. remove the
56
+ # -mod=readonly flag). Each of the imported packages gets listed separatly,
57
+ # confusing the issue as to which package is the root of the module.
58
+ go_list_cmd = "GO111MODULE=on go list -mod=readonly -deps -f '#{format_str}' ./..."
59
+ info_output, stderr, status = Cmd.run(go_list_cmd)
60
+ log_errors_with_cmd(go_list_cmd, "Getting the dependencies from go list failed \n\t#{stderr}") unless status.success?
40
61
 
41
- info_output.split("\n")
62
+ # Since many packages may belong to a single module, #uniq is used to deduplicate
63
+ info_output.split("\n").uniq
64
+ end
42
65
  end
43
66
 
44
- def sum_files?
45
- sum_file_paths.any?
67
+ def mod_files?
68
+ mod_file_paths.any?
46
69
  end
47
70
 
48
- def sum_file_paths
71
+ def mod_file_paths
49
72
  Dir[project_path.join(PACKAGES_FILE)]
50
73
  end
51
74
 
@@ -53,10 +76,15 @@ module LicenseFinder
53
76
  info = {
54
77
  'ImportPath' => name,
55
78
  'InstallPath' => install_path,
56
- 'Rev' => version
79
+ 'Rev' => version,
80
+ 'Homepage' => repo_name(name)
57
81
  }
58
82
 
59
83
  GoPackage.from_dependency(info, nil, true)
60
84
  end
85
+
86
+ def repo_name(name)
87
+ name.split('/')[0..2].join('/')
88
+ end
61
89
  end
62
90
  end