license_scout 0.1.0 → 0.1.1

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