license_scout 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d48be3c19574905aef576b453b5750796b64e7ed
4
- data.tar.gz: a9424c524ca4e52c478cf45610b229f9f4909268
3
+ metadata.gz: 20de647a9029e60eee95ccfdd6af26ca23f68943
4
+ data.tar.gz: 4d39bfea2c9974e8af4cd400eccfca1bbec4f4bd
5
5
  SHA512:
6
- metadata.gz: b4c68b97c0370efab31ba8a43090ca05762a03275af7f00cd9af0dc4f384a352c701c4c045a4de537af132bb0e7cb1ebe7b5b83122afd3ad6b0d1326fbe6e69d
7
- data.tar.gz: 083fa2b33c02f30de9a39976aa2b9f87f366eb90d3a2f90901c71446eb7c0e25892547555ef04a95bc1ed337517f81a58505e17916484aefee6cb58484695ad8
6
+ metadata.gz: f554b77a3215ce07597d48a64daa48844dc23463372ca5ec5bfda0726418836c25b7b53eb4e123942b5ffdf858a503e14e59d7aa196b6fa27fce0513ed985e8d
7
+ data.tar.gz: 990fd0c28d0f003e72f0cc1840198b0453c8a9f8c52c45f3e7aff9bae145e645b2a46a83901159a97de45d9e8f056bef92fe41ae3da42014350cdeffbdd6f30f
data/README.md CHANGED
@@ -7,9 +7,15 @@ Currently supported project types are:
7
7
 
8
8
  * Ruby - bundler
9
9
  * Erlang - rebar
10
+ * CPAN - perl
11
+ * Berkshelf - chef
10
12
 
11
13
  ## Usage
12
14
 
15
+ ## Thanks
16
+
17
+ Thanks to https://github.com/basho for `config_to_json` binary which helps with parsing Erlang config files. From: https://github.com/basho/erlang_template_helper
18
+
13
19
  ## Contributing
14
20
 
15
21
  This project is maintained by the contribution guidelines identified for
Binary file
data/bin/license_scout CHANGED
@@ -22,10 +22,12 @@ require "license_scout/collector"
22
22
  require "license_scout/overrides"
23
23
  require "license_scout/options"
24
24
 
25
- project_dir = File.expand_path(Dir.pwd)
25
+ project_dir = ARGV[0] || File.expand_path(Dir.pwd)
26
26
  project_name = File.basename(project_dir)
27
27
 
28
- output_dir = project_dir
28
+ # Create the output files under a specific directory in order not to pollute the
29
+ # project_dir too much.
30
+ output_dir = File.join(project_dir, "license-cache")
29
31
 
30
32
  overrides = LicenseScout::Overrides.new
31
33
 
@@ -17,6 +17,7 @@
17
17
 
18
18
  require "license_scout/exceptions"
19
19
  require "license_scout/dependency_manager"
20
+ require "license_scout/reporter"
20
21
 
21
22
  require "ffi_yajl"
22
23
 
@@ -59,30 +60,7 @@ module LicenseScout
59
60
  end
60
61
 
61
62
  def issue_report
62
- report = []
63
- license_report = FFI_Yajl::Parser.parse(File.read(license_manifest_path))
64
-
65
- license_report["dependency_managers"].each do |dependency_manager, dependencies|
66
- dependencies.each do |dependency|
67
- if dependency["name"].nil? || dependency["name"].empty?
68
- report << "There is a dependency with a missing name in '#{dependency_manager}'."
69
- end
70
-
71
- if dependency["version"].nil? || dependency["version"].empty?
72
- report << "Dependency '#{dependency["name"]}' under '#{dependency_manager}' is missing version information."
73
- end
74
-
75
- if dependency["license"].nil? || dependency["license"].empty?
76
- report << "Dependency '#{dependency["name"]}' version '#{dependency["version"]}' under '#{dependency_manager}' is missing license information."
77
- end
78
-
79
- if dependency["license_files"].empty?
80
- report << "Dependency '#{dependency["name"]}' version '#{dependency["version"]}' under '#{dependency_manager}' is missing license files information."
81
- end
82
- end
83
- end
84
-
85
- report
63
+ Reporter.new(output_dir).report
86
64
  end
87
65
 
88
66
  private
@@ -0,0 +1,102 @@
1
+ #
2
+ # Copyright:: Copyright 2016, Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "berkshelf"
19
+
20
+ require "license_scout/dependency_manager/base"
21
+
22
+ module LicenseScout
23
+ module DependencyManager
24
+ class Berkshelf < Base
25
+
26
+ def name
27
+ "chef_berkshelf"
28
+ end
29
+
30
+ def detected?
31
+ File.exists?(berksfile_path) && File.exists?(lockfile_path)
32
+ end
33
+
34
+ def dependencies
35
+ dependencies = []
36
+ cookbook_dependencies = nil
37
+
38
+ Dir.chdir(project_dir) do
39
+ berksfile = ::Berkshelf::Berksfile.from_file("./Berksfile")
40
+
41
+ # Berkshelf should not give an error when there are cookbooks in the
42
+ # lockfile that are no longer in the berksfile. It handles this case in
43
+ # the Installer class which we are not using here. So we handle this
44
+ # case in the same way Installer does.
45
+ berksfile.lockfile.reduce!
46
+
47
+ cookbook_dependencies = berksfile.list
48
+ end
49
+
50
+ cookbook_dependencies.each do |dep|
51
+ dependency_name = dep.name
52
+ dependency_version = dep.cached_cookbook.version
53
+
54
+ dependency_license_files = auto_detect_license_files(dep.cached_cookbook.path.to_s)
55
+
56
+ # Check license override and license_files override separately since
57
+ # only one might be set in the overrides.
58
+ dependency_license = options.overrides.license_for(name, dependency_name, dependency_version) || dep.cached_cookbook.license
59
+
60
+ override_license_files = options.overrides.license_files_for(name, dependency_name, dependency_version)
61
+ cookbook_path = dep.cached_cookbook.path.to_s
62
+
63
+ if override_license_files.empty?
64
+ dependency_license_files = auto_detect_license_files(cookbook_path)
65
+ else
66
+ dependency_license_files = override_license_files.resolve_locations(cookbook_path)
67
+ end
68
+
69
+ dependencies << Dependency.new(
70
+ dependency_name,
71
+ dependency_version,
72
+ dependency_license,
73
+ dependency_license_files
74
+ )
75
+ end
76
+
77
+ dependencies
78
+ end
79
+
80
+ private
81
+
82
+ def berksfile_path
83
+ File.join(project_dir, "Berksfile")
84
+ end
85
+
86
+ def lockfile_path
87
+ File.join(project_dir, "Berksfile.lock")
88
+ end
89
+
90
+ def auto_detect_license_files(cookbook_path)
91
+ unless File.exist?(cookbook_path)
92
+ raise LicenseScout::Exceptions::InaccessibleDependency.new "Autodetected cookbook path '#{cookbook_path}' does not exist"
93
+ end
94
+
95
+ Dir.glob("#{cookbook_path}/*").select do |f|
96
+ POSSIBLE_LICENSE_FILES.include?(File.basename(f))
97
+ end
98
+ end
99
+
100
+ end
101
+ end
102
+ end
@@ -32,13 +32,12 @@ require "bundler/setup"
32
32
  # We're only using things that are in the stdlib.
33
33
  require "json"
34
34
 
35
- definition = ::Bundler::Definition.build("./Gemfile", "./Gemfile.lock", nil)
36
35
  dependencies = []
37
36
 
38
- definition.specs_for(definition.groups).each do |gem_spec|
37
+ Bundler.load.specs.each do |gem_spec|
39
38
  dependencies << {
40
39
  name: gem_spec.name,
41
- version: gem_spec.version,
40
+ version: gem_spec.version.to_s,
42
41
  license: gem_spec.license,
43
42
  path: gem_spec.full_gem_path,
44
43
  }
@@ -33,11 +33,13 @@ module LicenseScout
33
33
  end
34
34
 
35
35
  def detected?
36
- # We only check for the existence of Gemfile in order to declare a
37
- # project a Bundler project. If the Gemfile.lock does not exist
38
- # we will raise a specific error to indicate that "bundle install"
39
- # needs to be run before proceeding.
40
- File.exists?(gemfile_path)
36
+ # We check the existence of both Gemfile and Gemfile.lock. We need both
37
+ # of them to be able to get a concrete set of dependencies which we can
38
+ # search. We used to raise an error when Gemfile.lock did not exist but
39
+ # that created issues with projects like oc_bifrost which is a rebar
40
+ # project but have a Gemfile at its root to be able to run some rake
41
+ # commands.
42
+ File.exists?(gemfile_path) && File.exists?(lockfile_path)
41
43
  end
42
44
 
43
45
  def dependency_data
@@ -56,10 +58,6 @@ module LicenseScout
56
58
  end
57
59
 
58
60
  def dependencies
59
- if !File.exists?(lockfile_path)
60
- raise LicenseScout::Exceptions::DependencyManagerNotRun.new(project_dir, name)
61
- end
62
-
63
61
  dependencies = []
64
62
  dependency_data.each do |gem_data|
65
63
  dependency_name = gem_data["name"]
@@ -74,6 +72,14 @@ module LicenseScout
74
72
  # bundler's lib/ dir, so we have to munge it.
75
73
  dependency_license = "MIT"
76
74
  dependency_license_files = [File.join(File.dirname(__FILE__), "bundler/LICENSE.md")]
75
+ elsif dependency_name == "json"
76
+ # json is different weird. When project is using the json that is prepackaged with
77
+ # Ruby, its included not as a full fledged gem but an *.rb file at:
78
+ # /opt/opscode/embedded/lib/ruby/2.2.0/json.rb
79
+ # Because of this its license is reported as nil and its license files can not be
80
+ # found. That is why we need to provide them manually here.
81
+ dependency_license = "Ruby"
82
+ dependency_license_files = [File.join(File.dirname(__FILE__), "json/README.md")]
77
83
  else
78
84
  # Check license override and license_files override separately since
79
85
  # only one might be set in the overrides.
@@ -0,0 +1,321 @@
1
+ #
2
+ # Copyright:: Copyright 2016, Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "rexml/document"
19
+
20
+ require "ffi_yajl"
21
+ require "psych"
22
+ require "mixlib/shellout"
23
+
24
+ require "license_scout/dependency_manager/base"
25
+ require "license_scout/net_fetcher"
26
+ require "license_scout/exceptions"
27
+ require "license_scout/dependency"
28
+
29
+ module LicenseScout
30
+ module DependencyManager
31
+ class CPAN < Base
32
+
33
+ class CPANDependency
34
+
35
+ LICENSE_TYPE_MAP = {
36
+ "perl_5" => "Perl-5",
37
+ "perl" => "Perl-5",
38
+ "apache_2_0" => "Apache-2.0",
39
+ "artistic_2" => "Artistic-2.0",
40
+ "gpl_3" => "GPL-3.0",
41
+ }.freeze
42
+
43
+ attr_reader :module_name
44
+ attr_reader :dist
45
+ attr_reader :version
46
+ attr_reader :cpanfile
47
+
48
+ attr_reader :license_files
49
+ attr_reader :license
50
+
51
+ attr_reader :cache_root
52
+
53
+ attr_reader :overrides
54
+
55
+ def initialize(module_name:, dist:, version:, cpanfile:, cache_root:, overrides:)
56
+ @module_name = module_name
57
+ @dist = dist
58
+ @version = version
59
+ @cpanfile = cpanfile
60
+ @cache_root = cache_root
61
+ @overrides = overrides
62
+
63
+ @deps_list = nil
64
+
65
+ @license = nil
66
+ @license_files = []
67
+ end
68
+
69
+ def desc
70
+ "#{module_name} in #{dist} (#{version}) [#{license}]"
71
+ end
72
+
73
+ def to_dep
74
+ Dependency.new(
75
+ # we use dist for the name because there can be multiple modules in
76
+ # a dist, but the dist is the unit of packaging and licensing
77
+ dist,
78
+ version,
79
+ license,
80
+ license_files
81
+ )
82
+ end
83
+
84
+ def collect_licenses
85
+ ensure_cached
86
+ Dir.mktmpdir do |tmpdir|
87
+ FileUtils.cp(distribution_fullpath, tmpdir)
88
+ Dir.chdir(tmpdir) do
89
+ untar!
90
+ distribution_unpack_fullpath = File.join(tmpdir, distribution_unpack_relpath)
91
+ collect_licenses_in(distribution_unpack_fullpath)
92
+ end
93
+ end
94
+ end
95
+
96
+ def ensure_cached
97
+ cache_path = File.join(dist_cache_root, cpanfile)
98
+
99
+ # CPAN download URL is like:
100
+ # http://www.cpan.org/authors/id/R/RJ/RJBS/Sub-Install-0.928.tar.gz
101
+ # cpanfile is like:
102
+ # R/RJ/RJBS/Sub-Install-0.928.tar.gz
103
+ unless File.exist?(cache_path)
104
+
105
+ url = "http://www.cpan.org/authors/id/#{cpanfile}"
106
+ tmp_path = NetFetcher.cache(url)
107
+
108
+ FileUtils.mkdir_p(File.dirname(cache_path))
109
+ FileUtils.cp(tmp_path, cache_path)
110
+
111
+ end
112
+ end
113
+
114
+ def distribution_filename
115
+ File.basename(cpanfile)
116
+ end
117
+
118
+ def distribution_unpack_relpath
119
+ # Most packages have tar.gz extension but some have .tgz like
120
+ # IO-Pager-0.36.tgz
121
+ [".tar.gz", ".tgz"].each do |ext|
122
+ if distribution_filename.end_with?(ext)
123
+ return File.basename(distribution_filename, ext)
124
+ end
125
+ end
126
+ end
127
+
128
+ def distribution_fullpath
129
+ File.join(dist_cache_root, cpanfile)
130
+ end
131
+
132
+ # Untar the distribution.
133
+ #
134
+ # NOTE: On some platforms, you only get a usable version of tar as
135
+ # `gtar`, and on windows, symlinks break a lot of stuff. We (Chef
136
+ # Software) currently only use perl in server products, which we only
137
+ # build for a handful of Linux distros, so this is sufficient.
138
+ def untar!
139
+ s = Mixlib::ShellOut.new("tar zxf #{distribution_filename}")
140
+ s.run_command
141
+ s.error!
142
+ s.stdout
143
+ end
144
+
145
+ def collect_licenses_in(unpack_path)
146
+ collect_license_info_in(unpack_path)
147
+ collect_license_files_info_in(unpack_path)
148
+ end
149
+
150
+ def collect_license_info_in(unpack_path)
151
+ # Notice that we use "dist" as the dependency name
152
+ # See #to_dep for details.
153
+ @license = overrides.license_for("perl_cpan", dist, version) || begin
154
+ metadata = if File.exist?(meta_json_in(unpack_path))
155
+ slurp_meta_json_in(unpack_path)
156
+ elsif File.exist?(meta_yaml_in(unpack_path))
157
+ slurp_meta_yaml_in(unpack_path)
158
+ end
159
+
160
+ if metadata && metadata.key?("license")
161
+ given_type = Array(metadata["license"]).reject { |l| l == "unknown" }.first
162
+ normalize_license_type(given_type)
163
+ end
164
+ end
165
+ end
166
+
167
+ def collect_license_files_info_in(unpack_path)
168
+ override_license_files = overrides.license_files_for("perl_cpan", dist, version)
169
+
170
+ license_files = if override_license_files.empty?
171
+ find_license_files_in(unpack_path)
172
+ else
173
+ override_license_files.resolve_locations(unpack_path)
174
+ end
175
+
176
+ license_files.each do |f|
177
+ @license_files << cache_license_file(f)
178
+ end
179
+ end
180
+
181
+ # Copy license file to the cache. We unpack the CPAN dists in a tempdir
182
+ # and throw it away after we've inspected the contents, so we need to
183
+ # put the license file somewhere it can be copied from later.
184
+ def cache_license_file(unpacked_file)
185
+ basename = File.basename(unpacked_file)
186
+ license_cache_path = File.join(license_cache_root, "#{dist}-#{basename}")
187
+ FileUtils.mkdir_p(license_cache_root)
188
+ FileUtils.cp(unpacked_file, license_cache_path)
189
+ # In some cases, the license files get unpacked with 0444
190
+ # permissions which could make a re-run fail on the `cp` step.
191
+ FileUtils.chmod(0644, license_cache_path)
192
+ license_cache_path
193
+ end
194
+
195
+ def slurp_meta_yaml_in(unpack_path)
196
+ Psych.safe_load(File.read(meta_yaml_in(unpack_path)))
197
+ end
198
+
199
+ def slurp_meta_json_in(unpack_path)
200
+ FFI_Yajl::Parser.parse(File.read(meta_json_in(unpack_path)))
201
+ end
202
+
203
+ def license_cache_root
204
+ File.join(cache_root, "cpan-licenses")
205
+ end
206
+
207
+ def dist_cache_root
208
+ File.join(cache_root, "cpan-dists")
209
+ end
210
+
211
+ def normalize_license_type(given_type)
212
+ LICENSE_TYPE_MAP[given_type] || given_type
213
+ end
214
+
215
+ def meta_json_in(unpack_path)
216
+ File.join(unpack_path, "META.json")
217
+ end
218
+
219
+ def mymeta_json_in(unpack_path)
220
+ File.join(unpack_path, "MYMETA.json")
221
+ end
222
+
223
+ def meta_yaml_in(unpack_path)
224
+ File.join(unpack_path, "META.yml")
225
+ end
226
+
227
+ def find_license_files_in(unpack_path)
228
+ Dir["#{unpack_path}/*"].select do |f|
229
+ CPAN::POSSIBLE_LICENSE_FILES.include?(File.basename(f))
230
+ end
231
+ end
232
+
233
+ end
234
+
235
+ def initialize(*args, &block)
236
+ super
237
+ @dependencies = nil
238
+ end
239
+
240
+ def name
241
+ "perl_cpan"
242
+ end
243
+
244
+ def dependencies
245
+ return @dependencies if @dependencies
246
+ @dependencies = deps_list.map do |d|
247
+ d.collect_licenses
248
+ d.to_dep
249
+ end
250
+ end
251
+
252
+ def deps_list
253
+ return @deps_list if @deps_list
254
+
255
+ xml_doc = REXML::Document.new(dependency_graph_xml)
256
+
257
+ root = xml_doc.root
258
+
259
+ deps = root.get_elements("//dependency")
260
+
261
+ @deps_list = []
262
+
263
+ deps.each do |dep|
264
+ dep_module_name = dep.get_text("module").to_s
265
+ next if dep_module_name == module_name
266
+ @deps_list << CPANDependency.new(
267
+ module_name: dep_module_name,
268
+ dist: dep.get_text("dist").to_s,
269
+ version: dep.get_text("distversion").to_s,
270
+ cpanfile: dep.get_text("cpanfile").to_s,
271
+ cache_root: options.cpan_cache,
272
+ overrides: options.overrides
273
+ )
274
+ end
275
+
276
+ @deps_list
277
+ end
278
+
279
+ def dependency_graph_xml
280
+ @dependency_graph_xml ||=
281
+ begin
282
+ dependency_graph_xml_file = NetFetcher.cache(dependency_graph_url)
283
+ raw_xml = File.read(dependency_graph_xml_file)
284
+ FileUtils.rm_f(dependency_graph_xml_file)
285
+ raw_xml
286
+ end
287
+ end
288
+
289
+ # NOTE: there's no SSL version available. Take care handling any
290
+ # data/code referenced in responses from this site.
291
+ def dependency_graph_url
292
+ "http://deps.cpantesters.org/?xml=1;module=#{module_name};perl=5.24.0;os=any%20OS;pureperl=0"
293
+ end
294
+
295
+ # Infers the module name from the directory name. For Chef Server, the
296
+ # two perl packages we use are:
297
+ # * "App-Sqitch-VERSION" => "App::Sqitch"
298
+ # * "DBD-Pg-VERSION" => "DBD::Pg"
299
+ #
300
+ # NOTE: Distributions may contain multiple modules that would each have
301
+ # their own dependency graphs and it's possible to get a perl project
302
+ # that doesn't obey this convention (e.g., if you git clone it). But this
303
+ # meets our immediate needs.
304
+ def module_name
305
+ File.basename(project_dir).split("-")[0...-1].join("::")
306
+ end
307
+
308
+ # NOTE: it's possible that projects won't have a META.yml, but the two
309
+ # that we care about for Chef Server do have one. As of 2015, 84% of perl
310
+ # distribution packages have one: http://neilb.org/2015/10/18/spotters-guide.html
311
+ def detected?
312
+ File.exist?(meta_yml_path)
313
+ end
314
+
315
+ def meta_yml_path
316
+ File.join(project_dir, "META.yml")
317
+ end
318
+
319
+ end
320
+ end
321
+ end