license_finder 6.14.1 → 7.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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +9 -0
- data/.rubocop.yml +12 -2
- data/CHANGELOG.md +45 -0
- data/Dockerfile +26 -5
- data/README.md +6 -6
- data/Rakefile +2 -2
- data/VERSION +1 -1
- data/bin/license_finder_pip.py +9 -7
- data/ci/pipelines/release.yml.erb +1 -1
- data/ci/scripts/pushscript.sh +2 -3
- data/ci/scripts/run-tests.sh +4 -3
- data/ci/tasks/rubocop.yml +1 -1
- data/dlf +9 -5
- data/lib/license_finder/cli/approvals.rb +2 -2
- data/lib/license_finder/cli/base.rb +9 -5
- data/lib/license_finder/cli/dependencies.rb +4 -4
- data/lib/license_finder/cli/ignored_dependencies.rb +3 -3
- data/lib/license_finder/cli/ignored_groups.rb +3 -3
- data/lib/license_finder/cli/inherited_decisions.rb +5 -5
- data/lib/license_finder/cli/licenses.rb +2 -2
- data/lib/license_finder/cli/main.rb +17 -11
- data/lib/license_finder/cli/permitted_licenses.rb +3 -3
- data/lib/license_finder/cli/project_name.rb +4 -4
- data/lib/license_finder/cli/restricted_licenses.rb +3 -3
- data/lib/license_finder/configuration.rb +6 -3
- data/lib/license_finder/core.rb +2 -1
- data/lib/license_finder/decisions.rb +9 -5
- data/lib/license_finder/license/definitions.rb +35 -1
- data/lib/license_finder/license/templates/Zlib.txt +17 -0
- data/lib/license_finder/license/text.rb +5 -3
- data/lib/license_finder/license.rb +8 -2
- data/lib/license_finder/logger.rb +1 -1
- data/lib/license_finder/package.rb +3 -11
- data/lib/license_finder/package_delta.rb +1 -1
- data/lib/license_finder/package_manager.rb +1 -0
- data/lib/license_finder/package_managers/cocoa_pods.rb +14 -7
- data/lib/license_finder/package_managers/conan.rb +2 -2
- data/lib/license_finder/package_managers/conda.rb +1 -1
- data/lib/license_finder/package_managers/glide.rb +7 -1
- data/lib/license_finder/package_managers/go_15vendorexperiment.rb +2 -2
- data/lib/license_finder/package_managers/go_dep.rb +1 -1
- data/lib/license_finder/package_managers/go_workspace.rb +1 -1
- data/lib/license_finder/package_managers/nuget.rb +1 -1
- data/lib/license_finder/package_managers/pub.rb +86 -0
- data/lib/license_finder/package_managers/sbt.rb +1 -5
- data/lib/license_finder/package_managers/yarn.rb +34 -6
- data/lib/license_finder/package_utils/license_files.rb +2 -2
- data/lib/license_finder/packages/bower_package.rb +5 -2
- data/lib/license_finder/packages/erlangmk_package.rb +2 -4
- data/lib/license_finder/packages/npm_package.rb +1 -0
- data/lib/license_finder/packages/pubspec_package.rb +18 -0
- data/lib/license_finder/printer.rb +39 -0
- data/lib/license_finder/report.rb +2 -1
- data/lib/license_finder/reports/csv_report.rb +1 -1
- data/lib/license_finder/reports/erb_report.rb +7 -3
- data/lib/license_finder/reports/json_report.rb +2 -1
- data/lib/license_finder/reports/junit_report.rb +5 -1
- data/lib/license_finder/reports/xml_report.rb +5 -1
- data/lib/license_finder/scanner.rb +1 -1
- data/license_finder.gemspec +16 -14
- metadata +61 -28
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module LicenseFinder
|
7
|
+
class Pub < PackageManager
|
8
|
+
class PubError < RuntimeError; end
|
9
|
+
|
10
|
+
def current_packages
|
11
|
+
unless File.exist?('pubspec.lock')
|
12
|
+
raise PubError, "No checked-out Pub packages found.
|
13
|
+
Please install your dependencies first."
|
14
|
+
end
|
15
|
+
|
16
|
+
if ENV['PUB_CACHE'].nil? || ENV['PUB_CACHE'].eql?('')
|
17
|
+
raise PubError, 'While PUB_CACHE environment variable is empty, retrieving package licenses is impossible. Please set the PUB_CACHE env variable (default: ~/.pub)'
|
18
|
+
end
|
19
|
+
|
20
|
+
stdout, _stderr, _status = Cmd.run('flutter pub deps --json')
|
21
|
+
yaml_deps = JSON.parse(stdout)
|
22
|
+
yaml_deps['packages'].map do |dependency|
|
23
|
+
package_name = dependency['name']
|
24
|
+
subpath = "#{dependency['name']}-#{dependency['version']}"
|
25
|
+
package_version = dependency['version']
|
26
|
+
|
27
|
+
project_repo = dependency['source'] == 'git' ? Pathname("#{ENV['PUB_CACHE']}/git/#{dependency['name']}-*/") : Pathname("#{ENV['PUB_CACHE']}/hosted/pub.dartlang.org/#{subpath}")
|
28
|
+
|
29
|
+
homepage = read_repository_home(project_repo)
|
30
|
+
homepage = "https://pub.dev/packages/#{package_name}" if homepage.nil? || homepage.empty?
|
31
|
+
PubPackage.new(
|
32
|
+
package_name,
|
33
|
+
package_version,
|
34
|
+
license_text(project_repo),
|
35
|
+
logger: logger,
|
36
|
+
install_path: project_repo,
|
37
|
+
homepage: homepage
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def possible_package_paths
|
43
|
+
[project_path.join('pubspec.lock')]
|
44
|
+
end
|
45
|
+
|
46
|
+
def package_management_command
|
47
|
+
'flutter'
|
48
|
+
end
|
49
|
+
|
50
|
+
def prepare_command
|
51
|
+
'flutter pub get'
|
52
|
+
end
|
53
|
+
|
54
|
+
def prepare
|
55
|
+
prep_cmd = "#{prepare_command} #{production_flag}"
|
56
|
+
_stdout, stderr, status = Dir.chdir(project_path) { Cmd.run(prep_cmd) }
|
57
|
+
|
58
|
+
return if status.success?
|
59
|
+
|
60
|
+
log_errors stderr
|
61
|
+
raise "Prepare command '#{prep_cmd}' failed" unless @prepare_no_fail
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def license_text(subpath)
|
67
|
+
license_path = license_pattern(subpath).find { |f| File.exist?(f) }
|
68
|
+
license_path.nil? ? nil : IO.read(license_path)
|
69
|
+
end
|
70
|
+
|
71
|
+
def license_pattern(subpath)
|
72
|
+
Dir.glob(subpath.join('LICENSE*'), File::FNM_CASEFOLD)
|
73
|
+
end
|
74
|
+
|
75
|
+
def production_flag
|
76
|
+
return '' if @ignored_groups.nil?
|
77
|
+
|
78
|
+
@ignored_groups.include?('devDependencies') ? '' : 'no-'
|
79
|
+
end
|
80
|
+
|
81
|
+
def read_repository_home(project_repo)
|
82
|
+
package_yaml = project_repo.join('pubspec.yaml')
|
83
|
+
YAML.load(IO.read(package_yaml))['repository'] if Dir.exist?(project_repo) && File.exist?(package_yaml)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -17,11 +17,7 @@ module LicenseFinder
|
|
17
17
|
|
18
18
|
dependencies = SbtDependencyFinder.new(project_path).dependencies
|
19
19
|
packages = dependencies.flat_map do |text|
|
20
|
-
|
21
|
-
headers: true
|
22
|
-
}
|
23
|
-
|
24
|
-
contents = CSV.parse(text, options)
|
20
|
+
contents = CSV.parse(text, headers: true)
|
25
21
|
contents.map do |row|
|
26
22
|
group_id, name, version = row['Dependency'].split('#').map(&:strip)
|
27
23
|
spec = {
|
@@ -9,12 +9,12 @@ module LicenseFinder
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def current_packages
|
12
|
-
cmd = "#{Yarn::SHELL_COMMAND}#{
|
12
|
+
cmd = "#{Yarn::SHELL_COMMAND}#{yarn1_production_flag}"
|
13
13
|
suffix = " --cwd #{project_path}" unless project_path.nil?
|
14
14
|
cmd += suffix unless suffix.nil?
|
15
15
|
|
16
|
-
stdout,
|
17
|
-
|
16
|
+
stdout, stderr, status = Cmd.run(cmd)
|
17
|
+
raise "Command '#{cmd}' failed to execute: #{stderr}" unless status.success?
|
18
18
|
|
19
19
|
packages = []
|
20
20
|
incompatible_packages = []
|
@@ -39,7 +39,7 @@ module LicenseFinder
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def prepare
|
42
|
-
prep_cmd =
|
42
|
+
prep_cmd = prepare_command.to_s
|
43
43
|
_stdout, stderr, status = Dir.chdir(project_path) { Cmd.run(prep_cmd) }
|
44
44
|
return if status.success?
|
45
45
|
|
@@ -56,11 +56,33 @@ module LicenseFinder
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def prepare_command
|
59
|
-
|
59
|
+
if yarn2_project?
|
60
|
+
yarn2_prepare_command
|
61
|
+
else
|
62
|
+
yarn1_prepare_command
|
63
|
+
end
|
60
64
|
end
|
61
65
|
|
62
66
|
private
|
63
67
|
|
68
|
+
def yarn2_prepare_command
|
69
|
+
"#{yarn2_production_flag}yarn install"
|
70
|
+
end
|
71
|
+
|
72
|
+
def yarn1_prepare_command
|
73
|
+
"yarn install --ignore-engines --ignore-scripts#{yarn1_production_flag}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def yarn2_project?
|
77
|
+
Dir.chdir(project_path) do
|
78
|
+
version_string, stderr_str, status = Cmd.run('yarn -v')
|
79
|
+
raise "Command 'yarn -v' failed to execute: #{stderr_str}" unless status.success?
|
80
|
+
|
81
|
+
version = version_string.split('.').map(&:to_i)
|
82
|
+
return version[0] >= 2
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
64
86
|
def packages_from_json(json_data)
|
65
87
|
body = json_data['body']
|
66
88
|
head = json_data['head']
|
@@ -98,10 +120,16 @@ module LicenseFinder
|
|
98
120
|
all_packages - [yarn_internal_package]
|
99
121
|
end
|
100
122
|
|
101
|
-
def
|
123
|
+
def yarn1_production_flag
|
102
124
|
return '' if @ignored_groups.nil?
|
103
125
|
|
104
126
|
@ignored_groups.include?('devDependencies') ? ' --production' : ''
|
105
127
|
end
|
128
|
+
|
129
|
+
def yarn2_production_flag
|
130
|
+
return '' if @ignored_groups.nil?
|
131
|
+
|
132
|
+
@ignored_groups.include?('devDependencies') ? 'yarn plugin import workspace-tools && yarn workspaces focus --all --production && ' : ''
|
133
|
+
end
|
106
134
|
end
|
107
135
|
end
|
@@ -4,7 +4,7 @@ require 'license_finder/package_utils/possible_license_file'
|
|
4
4
|
|
5
5
|
module LicenseFinder
|
6
6
|
class LicenseFiles
|
7
|
-
CANDIDATE_FILE_NAMES = %w[License Licence COPYING README].freeze
|
7
|
+
CANDIDATE_FILE_NAMES = %w[LICENSE License license LICENCE Licence licence COPYING copying README readme].freeze
|
8
8
|
CANDIDATE_PATH_WILDCARD = "*{#{CANDIDATE_FILE_NAMES.join(',')}}*"
|
9
9
|
|
10
10
|
def self.find(install_path, options = {})
|
@@ -35,7 +35,7 @@ module LicenseFinder
|
|
35
35
|
def candidate_files_and_dirs
|
36
36
|
return [] if install_path.nil?
|
37
37
|
|
38
|
-
Pathname.glob(install_path.join('**', CANDIDATE_PATH_WILDCARD)
|
38
|
+
Pathname.glob(install_path.join('**', CANDIDATE_PATH_WILDCARD))
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'open-uri'
|
4
|
-
|
5
4
|
module LicenseFinder
|
6
5
|
class BowerPackage < Package
|
7
6
|
def initialize(bower_module, options = {})
|
@@ -35,7 +34,11 @@ module LicenseFinder
|
|
35
34
|
end
|
36
35
|
|
37
36
|
def package_url
|
38
|
-
meta =
|
37
|
+
meta = if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.5.0')
|
38
|
+
JSON.parse(open("https://registry.bower.io/packages/#{CGI.escape(name)}").read)
|
39
|
+
else
|
40
|
+
JSON.parse(URI.open("https://registry.bower.io/packages/#{CGI.escape(name)}").read)
|
41
|
+
end
|
39
42
|
meta['url']
|
40
43
|
end
|
41
44
|
end
|
@@ -37,9 +37,7 @@ module LicenseFinder
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def dep_version
|
40
|
-
@dep_version ||=
|
41
|
-
dep_version_unformatted.sub(version_prefix_re, '')
|
42
|
-
end
|
40
|
+
@dep_version ||= dep_version_unformatted.sub(version_prefix_re, '')
|
43
41
|
end
|
44
42
|
|
45
43
|
def dep_repo
|
@@ -88,7 +86,7 @@ module LicenseFinder
|
|
88
86
|
def dep_version_valid?
|
89
87
|
return false unless set?(dep_version_unformatted)
|
90
88
|
|
91
|
-
if dep_version
|
89
|
+
if dep_version&.match?(version_re)
|
92
90
|
Gem::Version.correct?(dep_version)
|
93
91
|
else
|
94
92
|
dep_version =~ word_dot_re
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LicenseFinder
|
4
|
+
class PubPackage < Package
|
5
|
+
def initialize(name, version, license_text, options = {})
|
6
|
+
super(name, version, options)
|
7
|
+
@license = License.find_by_text(license_text.to_s)
|
8
|
+
end
|
9
|
+
|
10
|
+
def licenses_from_spec
|
11
|
+
[@license].compact
|
12
|
+
end
|
13
|
+
|
14
|
+
def package_manager
|
15
|
+
'Pub'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LicenseFinder
|
4
|
+
class Printer
|
5
|
+
attr_reader :padding
|
6
|
+
|
7
|
+
def initialize #:nodoc:
|
8
|
+
@base = nil
|
9
|
+
@mute = false
|
10
|
+
@padding = 0
|
11
|
+
@always_force = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def say(message = '', color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/))
|
15
|
+
buffer = prepare_message(message, *color)
|
16
|
+
buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
|
17
|
+
|
18
|
+
stdout.print(buffer)
|
19
|
+
stdout.flush
|
20
|
+
end
|
21
|
+
|
22
|
+
def prepare_message(message, *color)
|
23
|
+
spaces = ' ' * padding
|
24
|
+
spaces + set_color(message.to_s, *color)
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_color(string, *) #:nodoc:
|
28
|
+
string
|
29
|
+
end
|
30
|
+
|
31
|
+
def padding=(value)
|
32
|
+
@padding = [0, value].max
|
33
|
+
end
|
34
|
+
|
35
|
+
def stdout
|
36
|
+
$stdout
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -9,11 +9,12 @@ module LicenseFinder
|
|
9
9
|
def initialize(dependencies, options)
|
10
10
|
@dependencies = dependencies
|
11
11
|
@project_name = options[:project_name]
|
12
|
+
@use_spdx_id = options[:use_spdx_id]
|
12
13
|
end
|
13
14
|
|
14
15
|
private
|
15
16
|
|
16
|
-
attr_reader :dependencies, :project_name
|
17
|
+
attr_reader :dependencies, :project_name, :use_spdx_id
|
17
18
|
|
18
19
|
def sorted_dependencies
|
19
20
|
dependencies.sort
|
@@ -5,7 +5,11 @@ module LicenseFinder
|
|
5
5
|
TEMPLATE_PATH = ROOT_PATH.join('reports', 'templates')
|
6
6
|
|
7
7
|
def to_s(filename = TEMPLATE_PATH.join("#{template_name}.erb"))
|
8
|
-
|
8
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.6.0')
|
9
|
+
template = ERB.new(filename.read, nil, '-')
|
10
|
+
else
|
11
|
+
template = ERB.new(filename.read, trim_mode: '-')
|
12
|
+
end
|
9
13
|
template.result(binding)
|
10
14
|
end
|
11
15
|
|
@@ -22,7 +26,7 @@ module LicenseFinder
|
|
22
26
|
end
|
23
27
|
|
24
28
|
def link_to_license(license)
|
25
|
-
link_to_maybe license.name, license.url
|
29
|
+
link_to_maybe (@use_spdx_id ? license.standard_id : license.name), license.url
|
26
30
|
end
|
27
31
|
|
28
32
|
def link_to_dependency(dependency)
|
@@ -42,7 +46,7 @@ module LicenseFinder
|
|
42
46
|
end
|
43
47
|
|
44
48
|
def license_names(dependency)
|
45
|
-
dependency.licenses.map(
|
49
|
+
dependency.licenses.map(&@use_spdx_id? :standard_id : :name).sort.join ', '
|
46
50
|
end
|
47
51
|
|
48
52
|
def license_links(dependency)
|
@@ -6,7 +6,11 @@ module LicenseFinder
|
|
6
6
|
TEMPLATE_PATH = ROOT_PATH.join('templates')
|
7
7
|
|
8
8
|
def to_s(filename = TEMPLATE_PATH.join("#{template_name}.erb"))
|
9
|
-
|
9
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.6.0')
|
10
|
+
template = ERB.new(filename.read, nil, '-')
|
11
|
+
else
|
12
|
+
template = ERB.new(filename.read, trim_mode: '-')
|
13
|
+
end
|
10
14
|
template.result(binding)
|
11
15
|
end
|
12
16
|
|
@@ -6,7 +6,11 @@ module LicenseFinder
|
|
6
6
|
TEMPLATE_PATH = ROOT_PATH.join('templates')
|
7
7
|
|
8
8
|
def to_s(filename = TEMPLATE_PATH.join("#{template_name}.erb"))
|
9
|
-
|
9
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.6.0')
|
10
|
+
template = ERB.new(filename.read, nil, '-')
|
11
|
+
else
|
12
|
+
template = ERB.new(filename.read, trim_mode: '-')
|
13
|
+
end
|
10
14
|
template.result(binding)
|
11
15
|
end
|
12
16
|
|
@@ -5,7 +5,7 @@ module LicenseFinder
|
|
5
5
|
PACKAGE_MANAGERS = [
|
6
6
|
GoModules, GoDep, GoWorkspace, Go15VendorExperiment, Glide, Gvt, Govendor, Trash, Dep, Bundler, NPM, Pip,
|
7
7
|
Yarn, Bower, Maven, Gradle, CocoaPods, Rebar, Erlangmk, Nuget, Carthage, Mix, Conan, Sbt, Cargo, Dotnet, Composer, Pipenv,
|
8
|
-
Conda, Spm
|
8
|
+
Conda, Spm, Pub
|
9
9
|
].freeze
|
10
10
|
|
11
11
|
class << self
|
data/license_finder.gemspec
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
version = File.read(File.expand_path('VERSION', __dir__)).strip
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
|
-
s.required_ruby_version = '>= 2.
|
6
|
+
s.required_ruby_version = '>= 2.4.0'
|
7
7
|
s.name = 'license_finder'
|
8
8
|
s.version = version
|
9
9
|
|
@@ -45,26 +45,28 @@ Gem::Specification.new do |s|
|
|
45
45
|
|
46
46
|
s.add_dependency 'bundler'
|
47
47
|
s.add_dependency 'rubyzip', '>=1', '<3'
|
48
|
-
s.add_dependency 'thor', '~> 1.
|
48
|
+
s.add_dependency 'thor', '~> 1.2'
|
49
49
|
s.add_dependency 'tomlrb', '>= 1.3', '< 2.1'
|
50
50
|
s.add_dependency 'with_env', '1.1.0'
|
51
|
-
s.add_dependency 'xml-simple', '~> 1.1.
|
51
|
+
s.add_dependency 'xml-simple', '~> 1.1.9'
|
52
52
|
|
53
|
-
s.add_development_dependency 'addressable', '2.
|
54
|
-
s.add_development_dependency 'capybara', '~> 3.
|
55
|
-
s.add_development_dependency 'cocoapods', '>= 1.0.0' if RUBY_PLATFORM
|
56
|
-
s.add_development_dependency '
|
57
|
-
s.add_development_dependency '
|
58
|
-
s.add_development_dependency '
|
59
|
-
s.add_development_dependency '
|
53
|
+
s.add_development_dependency 'addressable', '2.8.0'
|
54
|
+
s.add_development_dependency 'capybara', '~> 3.32.2'
|
55
|
+
s.add_development_dependency 'cocoapods', '>= 1.0.0' if RUBY_PLATFORM.match?(/darwin/)
|
56
|
+
s.add_development_dependency 'e2mmap', '~> 0.1.0'
|
57
|
+
s.add_development_dependency 'fakefs', '~> 1.4.1'
|
58
|
+
s.add_development_dependency 'matrix', '~> 0.1.0'
|
59
|
+
s.add_development_dependency 'mime-types', '3.4.1'
|
60
|
+
s.add_development_dependency 'pry', '~> 0.14.1'
|
61
|
+
s.add_development_dependency 'rake', '~> 13.0.6'
|
60
62
|
s.add_development_dependency 'rspec', '~> 3'
|
61
63
|
s.add_development_dependency 'rspec-its', '~> 1.3.0'
|
62
|
-
s.add_development_dependency 'rubocop', '~>
|
63
|
-
s.add_development_dependency 'rubocop-performance', '~> 1.
|
64
|
-
s.add_development_dependency 'webmock', '~> 3.
|
64
|
+
s.add_development_dependency 'rubocop', '~> 1.12.1'
|
65
|
+
s.add_development_dependency 'rubocop-performance', '~> 1.10.2'
|
66
|
+
s.add_development_dependency 'webmock', '~> 3.14'
|
65
67
|
|
66
68
|
s.add_development_dependency 'nokogiri', '~>1.10'
|
67
|
-
s.add_development_dependency 'rack', '~> 2.2.
|
69
|
+
s.add_development_dependency 'rack', '~> 2.2.3'
|
68
70
|
s.add_development_dependency 'rack-test', '~> 1.1.0', '> 0.7'
|
69
71
|
|
70
72
|
s.files = `git ls-files`.split("\n").reject { |f| f.start_with?('spec', 'features') }
|