dependabot-nuget 0.237.0 → 0.239.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/lib/dependabot/nuget/cache_manager.rb +22 -0
- data/lib/dependabot/nuget/file_fetcher/import_paths_finder.rb +15 -0
- data/lib/dependabot/nuget/file_fetcher.rb +61 -64
- data/lib/dependabot/nuget/file_parser/dotnet_tools_json_parser.rb +2 -1
- data/lib/dependabot/nuget/file_parser/global_json_parser.rb +2 -1
- data/lib/dependabot/nuget/file_parser/packages_config_parser.rb +22 -4
- data/lib/dependabot/nuget/file_parser/project_file_parser.rb +287 -15
- data/lib/dependabot/nuget/file_parser/property_value_finder.rb +24 -52
- data/lib/dependabot/nuget/file_parser.rb +4 -1
- data/lib/dependabot/nuget/file_updater.rb +123 -117
- data/lib/dependabot/nuget/native_helpers.rb +94 -0
- data/lib/dependabot/nuget/requirement.rb +5 -1
- data/lib/dependabot/nuget/update_checker/compatibility_checker.rb +85 -0
- data/lib/dependabot/nuget/update_checker/dependency_finder.rb +228 -0
- data/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb +119 -0
- data/lib/dependabot/nuget/update_checker/nuspec_fetcher.rb +83 -0
- data/lib/dependabot/nuget/update_checker/property_updater.rb +30 -3
- data/lib/dependabot/nuget/update_checker/repository_finder.rb +36 -10
- data/lib/dependabot/nuget/update_checker/tfm_comparer.rb +31 -0
- data/lib/dependabot/nuget/update_checker/tfm_finder.rb +127 -0
- data/lib/dependabot/nuget/update_checker/version_finder.rb +47 -6
- data/lib/dependabot/nuget/update_checker.rb +42 -8
- data/lib/dependabot/nuget.rb +2 -0
- metadata +35 -9
- data/lib/dependabot/nuget/file_updater/packages_config_declaration_finder.rb +0 -70
- data/lib/dependabot/nuget/file_updater/project_file_declaration_finder.rb +0 -183
@@ -1,19 +1,22 @@
|
|
1
1
|
# typed: true
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require "dependabot/dependency_file"
|
4
5
|
require "dependabot/file_updaters"
|
5
6
|
require "dependabot/file_updaters/base"
|
7
|
+
require "dependabot/nuget/native_helpers"
|
6
8
|
|
7
9
|
module Dependabot
|
8
10
|
module Nuget
|
9
11
|
class FileUpdater < Dependabot::FileUpdaters::Base
|
10
|
-
require_relative "file_updater/packages_config_declaration_finder"
|
11
|
-
require_relative "file_updater/project_file_declaration_finder"
|
12
12
|
require_relative "file_updater/property_value_updater"
|
13
|
+
require_relative "file_parser/project_file_parser"
|
14
|
+
require_relative "file_parser/dotnet_tools_json_parser"
|
15
|
+
require_relative "file_parser/packages_config_parser"
|
13
16
|
|
14
17
|
def self.updated_files_regex
|
15
18
|
[
|
16
|
-
%r{^[^/]*\.[a-z]{2}proj$},
|
19
|
+
%r{^[^/]*\.([a-z]{2})?proj$},
|
17
20
|
/^packages\.config$/i,
|
18
21
|
/^global\.json$/i,
|
19
22
|
/^dotnet-tools\.json$/i,
|
@@ -24,156 +27,159 @@ module Dependabot
|
|
24
27
|
end
|
25
28
|
|
26
29
|
def updated_dependency_files
|
27
|
-
updated_files = T.let(dependency_files.dup, T.untyped)
|
28
|
-
|
29
|
-
# Loop through each of the changed requirements, applying changes to
|
30
|
-
# all files for that change. Note that the logic is different here
|
31
|
-
# to other languages because donet has property inheritance across
|
32
|
-
# files
|
33
30
|
dependencies.each do |dependency|
|
34
|
-
|
35
|
-
files: updated_files,
|
36
|
-
dependency: dependency
|
37
|
-
)
|
31
|
+
try_update_projects(dependency) || try_update_json(dependency)
|
38
32
|
end
|
39
33
|
|
40
|
-
|
34
|
+
# update all with content from disk
|
35
|
+
updated_files = dependency_files.filter_map do |f|
|
36
|
+
updated_content = File.read(dependency_file_path(f))
|
37
|
+
next if updated_content == f.content
|
41
38
|
|
42
|
-
|
39
|
+
normalized_content = normalize_content(f, updated_content)
|
40
|
+
next if normalized_content == f.content
|
41
|
+
|
42
|
+
puts "The contents of file [#{f.name}] were updated."
|
43
|
+
|
44
|
+
updated_file(file: f, content: normalized_content)
|
45
|
+
end
|
46
|
+
|
47
|
+
# reset repo files
|
48
|
+
SharedHelpers.reset_git_repo(T.cast(repo_contents_path, String)) if repo_contents_path
|
43
49
|
|
44
50
|
updated_files
|
45
51
|
end
|
46
52
|
|
47
53
|
private
|
48
54
|
|
49
|
-
def
|
50
|
-
|
55
|
+
def try_update_projects(dependency)
|
56
|
+
update_ran = T.let(false, T::Boolean)
|
57
|
+
|
58
|
+
# run update for each project file
|
59
|
+
project_files.each do |project_file|
|
60
|
+
project_dependencies = project_dependencies(project_file)
|
61
|
+
proj_path = dependency_file_path(project_file)
|
62
|
+
|
63
|
+
next unless project_dependencies.any? { |dep| dep.name.casecmp(dependency.name).zero? }
|
64
|
+
|
65
|
+
NativeHelpers.run_nuget_updater_tool(repo_contents_path, proj_path, dependency, !dependency.top_level?)
|
66
|
+
update_ran = true
|
67
|
+
end
|
68
|
+
|
69
|
+
update_ran
|
51
70
|
end
|
52
71
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
72
|
+
def try_update_json(dependency)
|
73
|
+
if dotnet_tools_json_dependencies.any? { |dep| dep.name.casecmp(dependency.name).zero? } ||
|
74
|
+
global_json_dependencies.any? { |dep| dep.name.casecmp(dependency.name).zero? }
|
75
|
+
|
76
|
+
# We just need to feed the updater a project file, grab the first
|
77
|
+
project_file = project_files.first
|
78
|
+
proj_path = dependency_file_path(project_file)
|
79
|
+
|
80
|
+
NativeHelpers.run_nuget_updater_tool(repo_contents_path, proj_path, dependency, !dependency.top_level?)
|
81
|
+
return true
|
56
82
|
end
|
83
|
+
|
84
|
+
false
|
57
85
|
end
|
58
86
|
|
59
|
-
def
|
60
|
-
|
87
|
+
def project_dependencies(project_file)
|
88
|
+
# Collect all dependencies from the project file and associated packages.config
|
89
|
+
dependencies = project_file_parser.dependency_set(project_file: project_file).dependencies
|
90
|
+
packages_config = find_packages_config(project_file)
|
91
|
+
return dependencies unless packages_config
|
92
|
+
|
93
|
+
dependencies + FileParser::PackagesConfigParser.new(packages_config: packages_config)
|
94
|
+
.dependency_set.dependencies
|
61
95
|
end
|
62
96
|
|
63
|
-
def
|
64
|
-
|
97
|
+
def find_packages_config(project_file)
|
98
|
+
project_file_name = File.basename(project_file.name)
|
99
|
+
packages_config_path = project_file.name.gsub(project_file_name, "packages.config")
|
100
|
+
packages_config_files.find { |f| f.name == packages_config_path }
|
65
101
|
end
|
66
102
|
|
67
|
-
def
|
68
|
-
|
103
|
+
def project_file_parser
|
104
|
+
@project_file_parser ||=
|
105
|
+
FileParser::ProjectFileParser.new(
|
106
|
+
dependency_files: dependency_files,
|
107
|
+
credentials: credentials
|
108
|
+
)
|
109
|
+
end
|
69
110
|
|
70
|
-
|
111
|
+
def global_json_dependencies
|
112
|
+
return [] unless global_json
|
113
|
+
|
114
|
+
@global_json_dependencies ||=
|
115
|
+
FileParser::GlobalJsonParser.new(global_json: global_json).dependency_set.dependencies
|
71
116
|
end
|
72
117
|
|
73
|
-
def
|
74
|
-
|
75
|
-
# when updating, so we can zip them together in new/old pairs.
|
76
|
-
reqs = dependency.requirements.zip(dependency.previous_requirements)
|
77
|
-
.reject { |new_req, old_req| new_req == old_req }
|
118
|
+
def dotnet_tools_json_dependencies
|
119
|
+
return [] unless dotnet_tools_json
|
78
120
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
next if new_req[:requirement] == old_req[:requirement]
|
121
|
+
@dotnet_tools_json_dependencies ||=
|
122
|
+
FileParser::DotNetToolsJsonParser.new(dotnet_tools_json: dotnet_tools_json).dependency_set.dependencies
|
123
|
+
end
|
83
124
|
|
84
|
-
|
125
|
+
def normalize_content(dependency_file, updated_content)
|
126
|
+
# Fix up line endings
|
127
|
+
if dependency_file.content.include?("\r\n") && updated_content.match?(/(?<!\r)\n/)
|
128
|
+
# The original content contain windows style newlines.
|
129
|
+
# Ensure the updated content also uses windows style newlines.
|
130
|
+
updated_content = updated_content.gsub(/(?<!\r)\n/, "\r\n")
|
131
|
+
puts "Fixing mismatched Windows line endings for [#{dependency_file.name}]."
|
132
|
+
elsif updated_content.include?("\r\n")
|
133
|
+
# The original content does not contain windows style newlines.
|
134
|
+
# Ensure the updated content uses unix style newlines.
|
135
|
+
updated_content = updated_content.gsub("\r\n", "\n")
|
136
|
+
puts "Fixing mismatched Unix line endings for [#{dependency_file.name}]."
|
137
|
+
end
|
85
138
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
139
|
+
# Fix up BOM
|
140
|
+
if !dependency_file.content.start_with?("\uFEFF") && updated_content.start_with?("\uFEFF")
|
141
|
+
updated_content = updated_content.delete_prefix("\uFEFF")
|
142
|
+
puts "Removing BOM from [#{dependency_file.name}]."
|
143
|
+
elsif dependency_file.content.start_with?("\uFEFF") && !updated_content.start_with?("\uFEFF")
|
144
|
+
updated_content = "\uFEFF" + updated_content
|
145
|
+
puts "Adding BOM to [#{dependency_file.name}]."
|
92
146
|
end
|
93
147
|
|
94
|
-
|
148
|
+
updated_content
|
95
149
|
end
|
96
150
|
|
97
|
-
def
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
.
|
103
|
-
.
|
104
|
-
|
105
|
-
updated_value: req.fetch(:requirement),
|
106
|
-
callsite_file: file
|
107
|
-
)
|
151
|
+
def dependency_file_path(dependency_file)
|
152
|
+
if dependency_file.directory.start_with?(repo_contents_path)
|
153
|
+
File.join(dependency_file.directory, dependency_file.name)
|
154
|
+
else
|
155
|
+
file_directory = dependency_file.directory
|
156
|
+
file_directory = file_directory[1..-1] if file_directory.start_with?("/")
|
157
|
+
File.join(repo_contents_path || "", file_directory, dependency_file.name)
|
158
|
+
end
|
108
159
|
end
|
109
160
|
|
110
|
-
def
|
111
|
-
|
112
|
-
|
113
|
-
updated_content = file.content
|
161
|
+
def project_files
|
162
|
+
dependency_files.select { |df| df.name.match?(/\.([a-z]{2})?proj$/) }
|
163
|
+
end
|
114
164
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
updated_declaration(old_dec, old_req, new_req)
|
119
|
-
)
|
165
|
+
def packages_config_files
|
166
|
+
dependency_files.select do |f|
|
167
|
+
T.must(T.must(f.name.split("/").last).casecmp("packages.config")).zero?
|
120
168
|
end
|
169
|
+
end
|
121
170
|
|
122
|
-
|
123
|
-
|
124
|
-
files[files.index(file)] =
|
125
|
-
updated_file(file: file, content: updated_content)
|
126
|
-
files
|
127
|
-
end
|
128
|
-
|
129
|
-
def original_declarations(dependency, requirement)
|
130
|
-
if requirement.fetch(:file).casecmp("global.json").zero?
|
131
|
-
[
|
132
|
-
global_json.content.match(
|
133
|
-
/"#{Regexp.escape(dependency.name)}"\s*:\s*
|
134
|
-
"#{Regexp.escape(dependency.previous_version)}"/x
|
135
|
-
).to_s
|
136
|
-
]
|
137
|
-
elsif requirement.fetch(:file).casecmp(".config/dotnet-tools.json").zero?
|
138
|
-
[
|
139
|
-
dotnet_tools_json.content.match(
|
140
|
-
/"#{Regexp.escape(dependency.name)}"\s*:\s*{\s*"version"\s*:\s*
|
141
|
-
"#{Regexp.escape(dependency.previous_version)}"/xm
|
142
|
-
).to_s
|
143
|
-
]
|
144
|
-
else
|
145
|
-
declaration_finder(dependency, requirement).declaration_strings
|
146
|
-
end
|
171
|
+
def global_json
|
172
|
+
dependency_files.find { |f| T.must(f.name.casecmp("global.json")).zero? }
|
147
173
|
end
|
148
174
|
|
149
|
-
def
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
declaring_requirement: requirement,
|
158
|
-
packages_config:
|
159
|
-
packages_config_files.find { |f| f.name == requirement_fn }
|
160
|
-
)
|
161
|
-
else
|
162
|
-
ProjectFileDeclarationFinder.new(
|
163
|
-
dependency_name: dependency.name,
|
164
|
-
declaring_requirement: requirement,
|
165
|
-
dependency_files: dependency_files
|
166
|
-
)
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
def updated_declaration(old_declaration, previous_req, requirement)
|
171
|
-
original_req_string = previous_req.fetch(:requirement)
|
172
|
-
|
173
|
-
old_declaration.gsub(
|
174
|
-
original_req_string,
|
175
|
-
requirement.fetch(:requirement)
|
176
|
-
)
|
175
|
+
def dotnet_tools_json
|
176
|
+
dependency_files.find { |f| T.must(f.name.casecmp(".config/dotnet-tools.json")).zero? }
|
177
|
+
end
|
178
|
+
|
179
|
+
def check_required_files
|
180
|
+
return if project_files.any? || packages_config_files.any?
|
181
|
+
|
182
|
+
raise "No project file or packages.config!"
|
177
183
|
end
|
178
184
|
end
|
179
185
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Dependabot
|
5
|
+
module Nuget
|
6
|
+
module NativeHelpers
|
7
|
+
def self.native_helpers_root
|
8
|
+
helpers_root = ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", nil)
|
9
|
+
return File.join(helpers_root, "nuget") unless helpers_root.nil?
|
10
|
+
|
11
|
+
File.expand_path("../../../helpers", __dir__)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.run_nuget_framework_check(project_tfms, package_tfms)
|
15
|
+
exe_path = File.join(native_helpers_root, "NuGetUpdater", "NuGetUpdater.Cli")
|
16
|
+
command = [
|
17
|
+
exe_path,
|
18
|
+
"framework-check",
|
19
|
+
"--project-tfms",
|
20
|
+
*project_tfms,
|
21
|
+
"--package-tfms",
|
22
|
+
*package_tfms,
|
23
|
+
"--verbose"
|
24
|
+
].join(" ")
|
25
|
+
|
26
|
+
fingerprint = [
|
27
|
+
exe_path,
|
28
|
+
"framework-check",
|
29
|
+
"--project-tfms",
|
30
|
+
"<project-tfms>",
|
31
|
+
"--package-tfms",
|
32
|
+
"<package-tfms>",
|
33
|
+
"--verbose"
|
34
|
+
].join(" ")
|
35
|
+
|
36
|
+
puts "running NuGet updater:\n" + command
|
37
|
+
|
38
|
+
output = SharedHelpers.run_shell_command(command, fingerprint: fingerprint)
|
39
|
+
puts output
|
40
|
+
|
41
|
+
# Exit code == 0 means that all project frameworks are compatible
|
42
|
+
true
|
43
|
+
rescue Dependabot::SharedHelpers::HelperSubprocessFailed
|
44
|
+
# Exit code != 0 means that not all project frameworks are compatible
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
# rubocop:disable Metrics/MethodLength
|
49
|
+
def self.run_nuget_updater_tool(repo_root, proj_path, dependency, is_transitive)
|
50
|
+
exe_path = File.join(native_helpers_root, "NuGetUpdater", "NuGetUpdater.Cli")
|
51
|
+
command = [
|
52
|
+
exe_path,
|
53
|
+
"update",
|
54
|
+
"--repo-root",
|
55
|
+
repo_root,
|
56
|
+
"--solution-or-project",
|
57
|
+
proj_path,
|
58
|
+
"--dependency",
|
59
|
+
dependency.name,
|
60
|
+
"--new-version",
|
61
|
+
dependency.version,
|
62
|
+
"--previous-version",
|
63
|
+
dependency.previous_version,
|
64
|
+
is_transitive ? "--transitive" : "",
|
65
|
+
"--verbose"
|
66
|
+
].join(" ")
|
67
|
+
|
68
|
+
fingerprint = [
|
69
|
+
exe_path,
|
70
|
+
"update",
|
71
|
+
"--repo-root",
|
72
|
+
"<repo-root>",
|
73
|
+
"--solution-or-project",
|
74
|
+
"<path-to-solution-or-project>",
|
75
|
+
"--dependency",
|
76
|
+
"<dependency-name>",
|
77
|
+
"--new-version",
|
78
|
+
"<new-version>",
|
79
|
+
"--previous-version",
|
80
|
+
"<previous-version>",
|
81
|
+
is_transitive ? "--transitive" : "",
|
82
|
+
"--verbose"
|
83
|
+
].join(" ")
|
84
|
+
|
85
|
+
puts "running NuGet updater:\n" + command
|
86
|
+
|
87
|
+
output = SharedHelpers.run_shell_command(command, fingerprint: fingerprint)
|
88
|
+
|
89
|
+
puts output
|
90
|
+
end
|
91
|
+
# rubocop:enable Metrics/MethodLength
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -1,6 +1,9 @@
|
|
1
1
|
# typed: true
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
require "dependabot/requirement"
|
4
7
|
require "dependabot/utils"
|
5
8
|
require "dependabot/nuget/version"
|
6
9
|
|
@@ -8,7 +11,7 @@ require "dependabot/nuget/version"
|
|
8
11
|
# https://docs.microsoft.com/en-us/nuget/reference/package-versioning
|
9
12
|
module Dependabot
|
10
13
|
module Nuget
|
11
|
-
class Requirement <
|
14
|
+
class Requirement < Dependabot::Requirement
|
12
15
|
def self.parse(obj)
|
13
16
|
return ["=", Nuget::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
|
14
17
|
|
@@ -25,6 +28,7 @@ module Dependabot
|
|
25
28
|
# For consistency with other languages, we define a requirements array.
|
26
29
|
# Dotnet doesn't have an `OR` separator for requirements, so it always
|
27
30
|
# contains a single element.
|
31
|
+
sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Requirement]) }
|
28
32
|
def self.requirements_array(requirement_string)
|
29
33
|
[new(requirement_string)]
|
30
34
|
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "dependabot/nuget/update_checker"
|
5
|
+
|
6
|
+
module Dependabot
|
7
|
+
module Nuget
|
8
|
+
class UpdateChecker
|
9
|
+
class CompatibilityChecker
|
10
|
+
require_relative "nuspec_fetcher"
|
11
|
+
require_relative "nupkg_fetcher"
|
12
|
+
require_relative "tfm_finder"
|
13
|
+
require_relative "tfm_comparer"
|
14
|
+
|
15
|
+
def initialize(dependency_urls:, dependency:, tfm_finder:)
|
16
|
+
@dependency_urls = dependency_urls
|
17
|
+
@dependency = dependency
|
18
|
+
@tfm_finder = tfm_finder
|
19
|
+
end
|
20
|
+
|
21
|
+
def compatible?(version)
|
22
|
+
nuspec_xml = NuspecFetcher.fetch_nuspec(dependency_urls, dependency.name, version)
|
23
|
+
return false unless nuspec_xml
|
24
|
+
|
25
|
+
# development dependencies are packages such as analyzers which need to be
|
26
|
+
# compatible with the compiler not the project itself.
|
27
|
+
return true if development_dependency?(nuspec_xml)
|
28
|
+
|
29
|
+
package_tfms = parse_package_tfms(nuspec_xml)
|
30
|
+
package_tfms = fetch_package_tfms(version) if package_tfms.empty?
|
31
|
+
# nil is a special return value that indicates that the package is likely a development dependency
|
32
|
+
return true if package_tfms.nil?
|
33
|
+
return false if package_tfms.empty?
|
34
|
+
|
35
|
+
return false if project_tfms.nil? || project_tfms.empty?
|
36
|
+
|
37
|
+
TfmComparer.are_frameworks_compatible?(project_tfms, package_tfms)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :dependency_urls, :dependency, :tfm_finder
|
43
|
+
|
44
|
+
def development_dependency?(nuspec_xml)
|
45
|
+
contents = nuspec_xml.at_xpath("package/metadata/developmentDependency")&.content&.strip
|
46
|
+
return false unless contents
|
47
|
+
|
48
|
+
contents.casecmp("true").zero?
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_package_tfms(nuspec_xml)
|
52
|
+
nuspec_xml.xpath("//dependencies/group").map do |group|
|
53
|
+
group.attribute("targetFramework")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def project_tfms
|
58
|
+
return @project_tfms if defined?(@project_tfms)
|
59
|
+
|
60
|
+
@project_tfms = tfm_finder.frameworks(dependency)
|
61
|
+
end
|
62
|
+
|
63
|
+
def fetch_package_tfms(dependency_version)
|
64
|
+
nupkg_buffer = NupkgFetcher.fetch_nupkg_buffer(dependency_urls, dependency.name, dependency_version)
|
65
|
+
return [] unless nupkg_buffer
|
66
|
+
|
67
|
+
# Parse tfms from the folders beneath the lib folder
|
68
|
+
folder_name = "lib/"
|
69
|
+
tfms = Set.new
|
70
|
+
Zip::File.open_buffer(nupkg_buffer) do |zip|
|
71
|
+
lib_file_entries = zip.select { |entry| entry.name.start_with?(folder_name) }
|
72
|
+
# If there is no lib folder in this package, assume it is a development dependency
|
73
|
+
return nil if lib_file_entries.empty?
|
74
|
+
|
75
|
+
lib_file_entries.each do |entry|
|
76
|
+
_, tfm = entry.name.split("/").first(2)
|
77
|
+
tfms << tfm
|
78
|
+
end
|
79
|
+
end
|
80
|
+
tfms.to_a
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|