cocoapods-core 0.17.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +36 -0
- data/lib/cocoapods-core/core_ui.rb +19 -0
- data/lib/cocoapods-core/dependency.rb +295 -0
- data/lib/cocoapods-core/gem_version.rb +6 -0
- data/lib/cocoapods-core/lockfile.rb +440 -0
- data/lib/cocoapods-core/platform.rb +171 -0
- data/lib/cocoapods-core/podfile/dsl.rb +459 -0
- data/lib/cocoapods-core/podfile/target_definition.rb +503 -0
- data/lib/cocoapods-core/podfile.rb +345 -0
- data/lib/cocoapods-core/requirement.rb +15 -0
- data/lib/cocoapods-core/source/validator.rb +183 -0
- data/lib/cocoapods-core/source.rb +284 -0
- data/lib/cocoapods-core/specification/consumer.rb +356 -0
- data/lib/cocoapods-core/specification/dsl/attribute.rb +245 -0
- data/lib/cocoapods-core/specification/dsl/attribute_support.rb +76 -0
- data/lib/cocoapods-core/specification/dsl/deprecations.rb +47 -0
- data/lib/cocoapods-core/specification/dsl/platform_proxy.rb +67 -0
- data/lib/cocoapods-core/specification/dsl.rb +1110 -0
- data/lib/cocoapods-core/specification/linter.rb +436 -0
- data/lib/cocoapods-core/specification/root_attribute_accessors.rb +152 -0
- data/lib/cocoapods-core/specification/set/presenter.rb +229 -0
- data/lib/cocoapods-core/specification/set/statistics.rb +277 -0
- data/lib/cocoapods-core/specification/set.rb +171 -0
- data/lib/cocoapods-core/specification/yaml.rb +60 -0
- data/lib/cocoapods-core/specification.rb +592 -0
- data/lib/cocoapods-core/standard_error.rb +84 -0
- data/lib/cocoapods-core/vendor/dependency.rb +264 -0
- data/lib/cocoapods-core/vendor/requirement.rb +208 -0
- data/lib/cocoapods-core/vendor/version.rb +333 -0
- data/lib/cocoapods-core/vendor.rb +56 -0
- data/lib/cocoapods-core/version.rb +99 -0
- data/lib/cocoapods-core/yaml_converter.rb +202 -0
- data/lib/cocoapods-core.rb +23 -0
- metadata +154 -0
@@ -0,0 +1,345 @@
|
|
1
|
+
require 'cocoapods-core/podfile/dsl'
|
2
|
+
require 'cocoapods-core/podfile/target_definition'
|
3
|
+
|
4
|
+
module Pod
|
5
|
+
|
6
|
+
# The Podfile is a specification that describes the dependencies of the
|
7
|
+
# targets of an Xcode project.
|
8
|
+
#
|
9
|
+
# It supports its own DSL and generally is stored in files named
|
10
|
+
# `CocoaPods.podfile` or `Podfile`.
|
11
|
+
#
|
12
|
+
# The Podfile creates a hierarchy of target definitions that that store the
|
13
|
+
# information of necessary to generate the CocoaPods libraries.
|
14
|
+
#
|
15
|
+
class Podfile
|
16
|
+
|
17
|
+
# @!group DSL support
|
18
|
+
|
19
|
+
include Pod::Podfile::DSL
|
20
|
+
|
21
|
+
#-------------------------------------------------------------------------#
|
22
|
+
|
23
|
+
class Pod::Podfile::StandardError < StandardError; end
|
24
|
+
|
25
|
+
#-------------------------------------------------------------------------#
|
26
|
+
|
27
|
+
# @return [Pathname] the path where the podfile was loaded from. It is nil
|
28
|
+
# if the podfile was generated programmatically.
|
29
|
+
#
|
30
|
+
attr_accessor :defined_in_file
|
31
|
+
|
32
|
+
# @param [Pathname] defined_in_file
|
33
|
+
# the path of the podfile.
|
34
|
+
#
|
35
|
+
# @param [Proc] block
|
36
|
+
# an optional block that configures the podfile through the DSL.
|
37
|
+
#
|
38
|
+
# @example Creating a Podfile.
|
39
|
+
#
|
40
|
+
# platform :ios, "6.0"
|
41
|
+
# target :my_app do
|
42
|
+
# pod "AFNetworking", "~> 1.0"
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
def initialize(defined_in_file = nil, internal_hash = {}, &block)
|
46
|
+
self.defined_in_file = defined_in_file
|
47
|
+
@internal_hash = internal_hash
|
48
|
+
if block
|
49
|
+
default_target_def = TargetDefinition.new(:default, self)
|
50
|
+
@root_target_definitions = [default_target_def]
|
51
|
+
@current_target_definition = default_target_def
|
52
|
+
instance_eval(&block)
|
53
|
+
else
|
54
|
+
@root_target_definitions = []
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return [String] a string useful to represent the Podfile in a message
|
59
|
+
# presented to the user.
|
60
|
+
#
|
61
|
+
def to_s
|
62
|
+
"Podfile"
|
63
|
+
end
|
64
|
+
|
65
|
+
#-------------------------------------------------------------------------#
|
66
|
+
|
67
|
+
public
|
68
|
+
|
69
|
+
# @!group Working with a podfile
|
70
|
+
|
71
|
+
# @return [Hash{Symbol,String => TargetDefinition}] the target definitions
|
72
|
+
# of the podfile stored by their name.
|
73
|
+
#
|
74
|
+
def target_definitions
|
75
|
+
Hash[target_definition_list.map { |td| [td.name, td] }]
|
76
|
+
end
|
77
|
+
|
78
|
+
def target_definition_list
|
79
|
+
root_target_definitions.map { |td| [td, td.recursive_children] }.flatten
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Array<TargetDefinition>] The root target definition.
|
83
|
+
#
|
84
|
+
attr_accessor :root_target_definitions
|
85
|
+
|
86
|
+
# @return [Array<Dependency>] the dependencies of the all the target
|
87
|
+
# definitions.
|
88
|
+
#
|
89
|
+
def dependencies
|
90
|
+
target_definition_list.map(&:dependencies).flatten.uniq
|
91
|
+
end
|
92
|
+
|
93
|
+
#-------------------------------------------------------------------------#
|
94
|
+
|
95
|
+
public
|
96
|
+
|
97
|
+
# @!group Attributes
|
98
|
+
|
99
|
+
# @return [String] the path of the workspace if specified by the user.
|
100
|
+
#
|
101
|
+
def workspace_path
|
102
|
+
path = get_hash_value('workspace')
|
103
|
+
if path
|
104
|
+
if File.extname(path) == '.xcworkspace'
|
105
|
+
path
|
106
|
+
else
|
107
|
+
"#{path}.xcworkspace"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# @return [Bool] whether the podfile should generate a BridgeSupport
|
113
|
+
# metadata document.
|
114
|
+
#
|
115
|
+
def generate_bridge_support?
|
116
|
+
get_hash_value('generate_bridge_support')
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [Bool] whether the -fobjc-arc flag should be added to the
|
120
|
+
# OTHER_LD_FLAGS.
|
121
|
+
#
|
122
|
+
def set_arc_compatibility_flag?
|
123
|
+
get_hash_value('set_arc_compatibility_flag')
|
124
|
+
end
|
125
|
+
|
126
|
+
#-------------------------------------------------------------------------#
|
127
|
+
|
128
|
+
public
|
129
|
+
|
130
|
+
# @!group Hooks
|
131
|
+
|
132
|
+
# Calls the pre install callback if defined.
|
133
|
+
#
|
134
|
+
# @param [Pod::Installer] installer
|
135
|
+
# the installer that is performing the installation.
|
136
|
+
#
|
137
|
+
# @return [Bool] whether a pre install callback was specified and it was
|
138
|
+
# called.
|
139
|
+
#
|
140
|
+
def pre_install!(installer)
|
141
|
+
if @pre_install_callback
|
142
|
+
@pre_install_callback.call(installer)
|
143
|
+
true
|
144
|
+
else
|
145
|
+
false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Calls the post install callback if defined.
|
150
|
+
#
|
151
|
+
# @param [Pod::Installer] installer
|
152
|
+
# the installer that is performing the installation.
|
153
|
+
#
|
154
|
+
# @return [Bool] whether a post install callback was specified and it was
|
155
|
+
# called.
|
156
|
+
#
|
157
|
+
def post_install!(installer)
|
158
|
+
if @post_install_callback
|
159
|
+
@post_install_callback.call(installer)
|
160
|
+
true
|
161
|
+
else
|
162
|
+
false
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
#-------------------------------------------------------------------------#
|
167
|
+
|
168
|
+
public
|
169
|
+
|
170
|
+
# @!group Representations
|
171
|
+
|
172
|
+
# @return [Array] The keys used by the hash representation of the Podfile.
|
173
|
+
#
|
174
|
+
HASH_KEYS = [
|
175
|
+
'target_definitions',
|
176
|
+
'workspace',
|
177
|
+
'generate_bridge_support',
|
178
|
+
'set_arc_compatibility_flag',
|
179
|
+
].freeze
|
180
|
+
|
181
|
+
# @return [Hash] The hash representation of the Podfile.
|
182
|
+
#
|
183
|
+
def to_hash
|
184
|
+
hash = {}
|
185
|
+
hash['target_definitions'] = Hash[root_target_definitions.map { |child| [child.name, child.to_hash] }]
|
186
|
+
hash.merge!(internal_hash)
|
187
|
+
hash
|
188
|
+
end
|
189
|
+
|
190
|
+
# @return [String] The YAML representation of the Podfile.
|
191
|
+
#
|
192
|
+
def to_yaml
|
193
|
+
to_hash.to_yaml
|
194
|
+
end
|
195
|
+
|
196
|
+
#-------------------------------------------------------------------------#
|
197
|
+
|
198
|
+
public
|
199
|
+
|
200
|
+
# @!group Class methods
|
201
|
+
|
202
|
+
# Initializes a podfile from the file with the given path.
|
203
|
+
#
|
204
|
+
# @param [Pathname] path
|
205
|
+
# the path from where the podfile should be loaded.
|
206
|
+
#
|
207
|
+
# @return [Podfile] the generated podfile.
|
208
|
+
#
|
209
|
+
def self.from_file(path)
|
210
|
+
path = Pathname.new(path)
|
211
|
+
unless path.exist?
|
212
|
+
raise StandardError, "No Podfile exists at path `#{path}`."
|
213
|
+
end
|
214
|
+
string = File.open(path, 'r:utf-8') { |f| f.read }
|
215
|
+
# Work around for Rubinius incomplete encoding in 1.9 mode
|
216
|
+
if string.respond_to?(:encoding) && string.encoding.name != "UTF-8"
|
217
|
+
string.encode!('UTF-8')
|
218
|
+
end
|
219
|
+
|
220
|
+
case path.extname
|
221
|
+
when ''
|
222
|
+
Podfile.from_ruby(string, path)
|
223
|
+
when '.yaml', '.cocoapods'
|
224
|
+
Podfile.from_yaml(string, path)
|
225
|
+
else
|
226
|
+
raise StandardError, "Unsupported Podfile format `#{path}`."
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Configures a new Podfile from the given ruby string.
|
231
|
+
#
|
232
|
+
# @param [String] string
|
233
|
+
# The ruby string which will configure the podfile with the DSL.
|
234
|
+
#
|
235
|
+
# @param [Pathname] path
|
236
|
+
# The path from which the Podfile is loaded.
|
237
|
+
#
|
238
|
+
# @return [Podfile] the new Podfile
|
239
|
+
#
|
240
|
+
def self.from_ruby(string, path = nil)
|
241
|
+
podfile = Podfile.new(path) do
|
242
|
+
begin
|
243
|
+
eval(string, nil, path.to_s)
|
244
|
+
rescue Exception => e
|
245
|
+
raise DSLError.new("Invalid `#{path.basename}` file: #{e.message}", path, e.backtrace)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
podfile
|
249
|
+
end
|
250
|
+
|
251
|
+
# Configures a new Podfile from the given hash.
|
252
|
+
#
|
253
|
+
# @param [Hash] hash
|
254
|
+
# The hash which contains the information of the Podfile.
|
255
|
+
#
|
256
|
+
# @param [Pathname] path
|
257
|
+
# The path from which the Podfile is loaded.
|
258
|
+
#
|
259
|
+
# @return [Podfile] the new Podfile
|
260
|
+
#
|
261
|
+
def self.from_hash(hash, path = nil)
|
262
|
+
internal_hash = hash.dup
|
263
|
+
target_definitions = internal_hash.delete('target_definitions') || []
|
264
|
+
podfile = Podfile.new(path,internal_hash)
|
265
|
+
target_definitions.each do |name, definition_hash|
|
266
|
+
definition = TargetDefinition.from_hash(name, definition_hash, podfile)
|
267
|
+
podfile.root_target_definitions << definition
|
268
|
+
end
|
269
|
+
podfile
|
270
|
+
end
|
271
|
+
|
272
|
+
# Configures a new Podfile from the given YAML representation.
|
273
|
+
#
|
274
|
+
# @param [String] yaml
|
275
|
+
# The YAML encoded hash which contains the information of the
|
276
|
+
# Podfile.
|
277
|
+
#
|
278
|
+
# @param [Pathname] path
|
279
|
+
# The path from which the Podfile is loaded.
|
280
|
+
#
|
281
|
+
# @return [Podfile] the new Podfile
|
282
|
+
#
|
283
|
+
def self.from_yaml(yaml, path = nil)
|
284
|
+
hash = YAML.load(yaml)
|
285
|
+
from_hash(hash)
|
286
|
+
end
|
287
|
+
|
288
|
+
#-------------------------------------------------------------------------#
|
289
|
+
|
290
|
+
private
|
291
|
+
|
292
|
+
# @!group Private helpers
|
293
|
+
|
294
|
+
# @return [Hash] The hash which store the attributes of the Podfile.
|
295
|
+
#
|
296
|
+
attr_accessor :internal_hash
|
297
|
+
|
298
|
+
# Set a value in the internal hash of the Podfile for the given key.
|
299
|
+
#
|
300
|
+
# @param [String] key
|
301
|
+
# The key for which to store the value.
|
302
|
+
#
|
303
|
+
# @param [Object] value
|
304
|
+
# The value to store.
|
305
|
+
#
|
306
|
+
# @raise If the key is not recognized.
|
307
|
+
#
|
308
|
+
# @return [void]
|
309
|
+
#
|
310
|
+
def set_hash_value(key, value)
|
311
|
+
raise StandardError, "Unsupported hash key `#{key}`" unless HASH_KEYS.include?(key)
|
312
|
+
internal_hash[key] = value
|
313
|
+
end
|
314
|
+
|
315
|
+
# Returns the value for the given key in the internal hash of the Podfile.
|
316
|
+
#
|
317
|
+
# @param [String] key
|
318
|
+
# The key for which the value is needed.
|
319
|
+
#
|
320
|
+
# @raise If the key is not recognized.
|
321
|
+
#
|
322
|
+
# @return [Object] The value for the key.
|
323
|
+
#
|
324
|
+
def get_hash_value(key)
|
325
|
+
raise StandardError, "Unsupported hash key `#{key}`" unless HASH_KEYS.include?(key)
|
326
|
+
internal_hash[key]
|
327
|
+
end
|
328
|
+
|
329
|
+
# @return [TargetDefinition] The current target definition to which the DSL
|
330
|
+
# commands apply.
|
331
|
+
#
|
332
|
+
attr_accessor :current_target_definition
|
333
|
+
|
334
|
+
#-------------------------------------------------------------------------#
|
335
|
+
|
336
|
+
# @deprecated Deprecated in favour of the more succinct {#pod}. Remove for
|
337
|
+
# CocoaPods 1.0.
|
338
|
+
#
|
339
|
+
def dependency(name = nil, *requirements, &block)
|
340
|
+
warn "[DEPRECATED] `dependency' is deprecated (use `pod')"
|
341
|
+
pod(name, *requirements, &block)
|
342
|
+
end
|
343
|
+
|
344
|
+
end
|
345
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Pod
|
2
|
+
|
3
|
+
# A Requirement is a set of one or more version restrictions of a
|
4
|
+
# {Dependency}.
|
5
|
+
#
|
6
|
+
# It is based on the RubyGems class adapted to support CocoaPods specific
|
7
|
+
# information.
|
8
|
+
#
|
9
|
+
# @todo Move support about external sources and head information here from
|
10
|
+
# the Dependency class.
|
11
|
+
#
|
12
|
+
class Requirement < Pod::Vendor::Gem::Requirement
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
module Pod
|
2
|
+
class Source
|
3
|
+
|
4
|
+
# TODO: THIS CLASS IS A STUB
|
5
|
+
|
6
|
+
# Checks whether a podspec can be accepted by a source.
|
7
|
+
#
|
8
|
+
# This class can work on Travis but some checks are lost as the repo is
|
9
|
+
# already merged during the test.
|
10
|
+
#
|
11
|
+
class Validator
|
12
|
+
|
13
|
+
# @return [Source] the source where the podspec should be added.
|
14
|
+
#
|
15
|
+
attr_reader :source
|
16
|
+
|
17
|
+
# @param [Pathname] repo @see Source#repo.
|
18
|
+
#
|
19
|
+
def initialize(repo)
|
20
|
+
@source = Source.new(repo)
|
21
|
+
@errors = {}
|
22
|
+
@linter_results = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param [Array<Pathname>] spec_paths
|
26
|
+
# a list of path that should be checked for being accepted in
|
27
|
+
# specs repo.
|
28
|
+
#
|
29
|
+
# @return [Bool] whether to podspec can be accepted by the source.
|
30
|
+
#
|
31
|
+
def check(spec_paths)
|
32
|
+
spec_paths = [ spec_paths ] unless spec_paths.is_a?(Array)
|
33
|
+
@errors = {}
|
34
|
+
spec_paths.each do |path|
|
35
|
+
@spec_path = Pathname.new(path)
|
36
|
+
lint
|
37
|
+
next unless spec
|
38
|
+
# check_spec_path
|
39
|
+
check_spec_source_change
|
40
|
+
check_if_untagged_version_is_acceptable
|
41
|
+
check_commit_change_for_untagged_version
|
42
|
+
check_dependencies
|
43
|
+
end
|
44
|
+
errors.values.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
#-----------------------------------------------------------------------#
|
48
|
+
|
49
|
+
# @!group Validation results
|
50
|
+
|
51
|
+
public
|
52
|
+
|
53
|
+
|
54
|
+
# @return [Hash{Pathname=>Array<String>}] the errors generated by the
|
55
|
+
# validation. If there is any error the podspec cannot be
|
56
|
+
# accepted.
|
57
|
+
#
|
58
|
+
attr_reader :errors
|
59
|
+
|
60
|
+
# @return [Hash{Pathname=>Array<Specification::Linter::Result>}] the
|
61
|
+
# result generated by the linter for not approved specifications.
|
62
|
+
#
|
63
|
+
attr_reader :linter_results
|
64
|
+
|
65
|
+
#-----------------------------------------------------------------------#
|
66
|
+
|
67
|
+
# @!group Validation helpers
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# @return [Pathname] The path of the current specification that should be
|
72
|
+
# checked.
|
73
|
+
#
|
74
|
+
attr_reader :spec_path
|
75
|
+
|
76
|
+
# @return [Specification] The current specification that should be
|
77
|
+
# checked.
|
78
|
+
#
|
79
|
+
# @note The specification is generated by the linter that catches any
|
80
|
+
# exception.
|
81
|
+
#
|
82
|
+
attr_reader :spec
|
83
|
+
|
84
|
+
|
85
|
+
#
|
86
|
+
#
|
87
|
+
def error(message)
|
88
|
+
@errors[spec_path] ||= []
|
89
|
+
@errors[spec_path] << message
|
90
|
+
end
|
91
|
+
|
92
|
+
#-----------------------------------------------------------------------#
|
93
|
+
|
94
|
+
# @!group Check steps
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def lint
|
99
|
+
linter = Specification::Linter.new(spec_path)
|
100
|
+
if linter.lint
|
101
|
+
@spec = linter.spec
|
102
|
+
else
|
103
|
+
error 'Linter failed validation.'
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# TODO: this check cannot be performed before a merge.
|
108
|
+
def check_spec_path
|
109
|
+
expected = "#{spec.name}/#{spec.version}/#{spec.name}.podspec"
|
110
|
+
relative_path = spec_path.relative_path_from(source.repo).to_s
|
111
|
+
unless relative_path == expected
|
112
|
+
error "Incorrect path, the path is `#{relative_path}` and should be `#{expected}`."
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def check_spec_source_change
|
117
|
+
return unless spec
|
118
|
+
return unless reference_spec
|
119
|
+
keys = Spec::DSL::SOURCE_KEYS.keys
|
120
|
+
source = spec.source.values_at(*keys).compact.first
|
121
|
+
old_source = reference_spec.source.values_at(*keys).compact.first
|
122
|
+
unless source == old_source
|
123
|
+
error "Attempt to change the source of the specification. " \
|
124
|
+
"Source: `#{source}`. Previous: `#{old_source}`.\n " \
|
125
|
+
"Contact specs repos maintainers if the library changed location."
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def check_if_untagged_version_is_acceptable
|
130
|
+
return unless spec
|
131
|
+
return if !spec.source[:git] || spec.source[:tag]
|
132
|
+
|
133
|
+
# Allow to fix a 0.0.1 podspec
|
134
|
+
if !related_specifications.any? { |s| s.version == '0.0.1' }
|
135
|
+
error "There is already versioned specifications so " \
|
136
|
+
"untagged versions cannot be added."
|
137
|
+
elsif spec.version != Version.new('0.0.1')
|
138
|
+
error "Untagged Git repositories should be versioned as 0.0.1"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# TODO: this cannot be tested on Travis with the current setup
|
143
|
+
def check_commit_change_for_untagged_version
|
144
|
+
return unless spec
|
145
|
+
return unless spec.version == Version.new('0.0.1')
|
146
|
+
ref_spec = related_specifications.find { |s| s.version != '0.0.1' }
|
147
|
+
return unless ref_spec
|
148
|
+
unless ref_spec.source[:commit] == spec.source[:commit]
|
149
|
+
error "Attempt to rewrite the commit of 0.0.1 version."
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def check_dependencies
|
154
|
+
return unless spec
|
155
|
+
spec.external_dependencies(true).each do |dep|
|
156
|
+
set = source.search(dep)
|
157
|
+
unless set && set.specification
|
158
|
+
error "Unable to find a specification for the `#{dep}` dependency."
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
#-----------------------------------------------------------------------#
|
164
|
+
|
165
|
+
# @!group Source helpers
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
def related_specifications
|
170
|
+
versions = source.versions(spec.name)
|
171
|
+
return unless versions
|
172
|
+
specs = versions.sort.map { |v| source.specification(spec.name, v) }
|
173
|
+
specs.reject { |s| s.defined_in_file == spec_path }
|
174
|
+
end
|
175
|
+
|
176
|
+
def reference_spec
|
177
|
+
specs = related_specifications
|
178
|
+
specs.last if specs
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|