refinement 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/VERSION +1 -1
- data/exe/refine +1 -1
- data/lib/cocoapods_plugin.rb +2 -2
- data/lib/refinement.rb +6 -27
- data/lib/sq/refinement/analyzer.rb +396 -0
- data/lib/sq/refinement/annotated_target.rb +82 -0
- data/lib/sq/refinement/changeset/file_modification.rb +144 -0
- data/lib/sq/refinement/changeset.rb +225 -0
- data/lib/sq/refinement/cli.rb +121 -0
- data/lib/sq/refinement/cocoapods_post_install_writer.rb +129 -0
- data/lib/sq/refinement/setup.rb +22 -0
- data/lib/sq/refinement/used_path.rb +156 -0
- data/lib/sq/refinement/version.rb +8 -0
- metadata +15 -27
- data/lib/refinement/analyzer.rb +0 -395
- data/lib/refinement/annotated_target.rb +0 -78
- data/lib/refinement/changeset/file_modification.rb +0 -138
- data/lib/refinement/changeset.rb +0 -223
- data/lib/refinement/cli.rb +0 -119
- data/lib/refinement/cocoapods_post_install_writer.rb +0 -126
- data/lib/refinement/used_path.rb +0 -154
- data/lib/refinement/version.rb +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 80840c17572abec1f76bc0b66fadf26f82794ece72f8b8240d32adfae917dfeb
|
4
|
+
data.tar.gz: e947035d3a2ce3490aac22e3458cd83dcefd1af652ae5416e8a2b6cfca397be6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a5b71c0a0b027fd695cc400e5278dd1fa5fc0f3af17392678c0aa68752de45a6a2f5529c5a5c0ba39d8379d3436eee915c0c6a3a14802af7d6e80358d54be00
|
7
|
+
data.tar.gz: e79b94e4c7dc6fb58bdebaa272377ff4244255ea5335cd39a634c287c7d051f44f24f1536ac5cfbf7fd879619b5b884448f4bb08eab1cc7cb188f500e92054de
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,21 @@
|
|
1
1
|
# Refinement Changes
|
2
2
|
|
3
|
+
## 0.7.0 (2023-08-11)
|
4
|
+
|
5
|
+
##### Breaking Changes
|
6
|
+
|
7
|
+
Since the introduction of the class '**Refinement**'
|
8
|
+
[in Ruby 3.1](https://ruby-doc.org/core-3.1.0/Refinement.html), there is a naming
|
9
|
+
collision that requires this gem to be updated.
|
10
|
+
|
11
|
+
We have prefixed the `Refinement` module with `Sq::` to minimize the amount of
|
12
|
+
downstream changes required, in addition to maintaining the meaning and purpose
|
13
|
+
of the project.
|
14
|
+
|
15
|
+
##### Enhancements
|
16
|
+
|
17
|
+
* Update dependencies to modern versions
|
18
|
+
|
3
19
|
## 0.3.1 (2019-08-09)
|
4
20
|
|
5
21
|
##### Bug Fixes
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.7.0
|
data/exe/refine
CHANGED
data/lib/cocoapods_plugin.rb
CHANGED
@@ -11,9 +11,9 @@ Pod::Installer
|
|
11
11
|
|
12
12
|
raise Pod::Informative, 'Refinement requires a CocoaPods version >= 1.6.0' unless Gem::Version.create(Pod::VERSION) >= Gem::Version.create('1.6.0')
|
13
13
|
|
14
|
-
require 'refinement/cocoapods_post_install_writer'
|
14
|
+
require 'sq/refinement/cocoapods_post_install_writer'
|
15
15
|
Pod::UI.message 'Writing refinement file' do
|
16
|
-
Refinement::CocoaPodsPostInstallWriter.new(aggregate_targets, pod_targets, config, plugins['refinement']).write!
|
16
|
+
Sq::Refinement::CocoaPodsPostInstallWriter.new(aggregate_targets, pod_targets, config, plugins['refinement']).write!
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end)
|
data/lib/refinement.rb
CHANGED
@@ -1,29 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
# @visibility private
|
10
|
-
# @param enum [Enumerable]
|
11
|
-
# Enumerates through `enum`, and applied the given block to each element.
|
12
|
-
# If the result of calling the block is truthy, the first such result is returned.
|
13
|
-
# If no such result is found, `nil` is returned.
|
14
|
-
def self.map_find(enum)
|
15
|
-
enum.each do |elem|
|
16
|
-
transformed = yield elem
|
17
|
-
return transformed if transformed
|
18
|
-
end
|
19
|
-
|
20
|
-
nil
|
21
|
-
end
|
22
|
-
|
23
|
-
require 'refinement/version'
|
24
|
-
|
25
|
-
require 'refinement/analyzer'
|
26
|
-
require 'refinement/annotated_target'
|
27
|
-
require 'refinement/changeset'
|
28
|
-
require 'refinement/used_path'
|
29
|
-
end
|
3
|
+
require 'sq/refinement/version'
|
4
|
+
require 'sq/refinement/setup'
|
5
|
+
require 'sq/refinement/analyzer'
|
6
|
+
require 'sq/refinement/annotated_target'
|
7
|
+
require 'sq/refinement/changeset'
|
8
|
+
require 'sq/refinement/used_path'
|
@@ -0,0 +1,396 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'xcodeproj'
|
4
|
+
|
5
|
+
module Sq
|
6
|
+
module Refinement
|
7
|
+
# Analyzes changes in a repository
|
8
|
+
# and determines how those changes impact the targets in Xcode projects in the workspace.
|
9
|
+
class Analyzer
|
10
|
+
attr_reader :changesets, :workspace_path, :augmenting_paths_yaml_files
|
11
|
+
private :changesets, :workspace_path, :augmenting_paths_yaml_files
|
12
|
+
|
13
|
+
# Initializes an analyzer with changesets, projects, and augmenting paths.
|
14
|
+
# @param changesets [Array<Changeset>]
|
15
|
+
# @param workspace_path [Pathname] path to a root workspace or project,
|
16
|
+
# must be `nil` if `projects` are specified explicitly
|
17
|
+
# @param projects [Array<Xcodeproj::Project>] projects to find targets in,
|
18
|
+
# must not be specified if `workspace_path` is not `nil`
|
19
|
+
# @param augmenting_paths_yaml_files [Array<Pathname>] paths to YAML files that provide augmenting paths by target,
|
20
|
+
# must be `nil` if `augmenting_paths_by_target` are specified explicitly
|
21
|
+
# @param augmenting_paths_by_target [Hash<String, Array>] arrays of hashes keyed by target name
|
22
|
+
# (or '*' for all targets)
|
23
|
+
# describing paths or globs that each target should be considered to be using,
|
24
|
+
# must not be specified if `augmenting_paths_yaml_files` is not `nil`
|
25
|
+
#
|
26
|
+
# @raise [ArgumentError] when conflicting arguments are given
|
27
|
+
#
|
28
|
+
def initialize(changesets:, workspace_path:, augmenting_paths_yaml_files:, projects: nil, augmenting_paths_by_target: nil)
|
29
|
+
@changesets = changesets
|
30
|
+
|
31
|
+
raise ArgumentError, 'Can only specify one of workspace_path and projects' if workspace_path && projects
|
32
|
+
|
33
|
+
@workspace_path = workspace_path
|
34
|
+
@projects = projects
|
35
|
+
|
36
|
+
raise ArgumentError, 'Can only specify one of augmenting_paths_yaml_files and augmenting_paths_by_target' if augmenting_paths_yaml_files && augmenting_paths_by_target
|
37
|
+
|
38
|
+
@augmenting_paths_yaml_files = augmenting_paths_yaml_files
|
39
|
+
@augmenting_paths_by_target = augmenting_paths_by_target
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Array<AnnotatedTarget>] targets from the projects annotated with their changes, based upon
|
43
|
+
# the changeset
|
44
|
+
def annotate_targets!
|
45
|
+
@annotate_targets ||= annotated_targets
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param scheme_path [Pathname] the absolute path to the scheme to be filtered
|
49
|
+
# @param change_level [Symbol] the change level at which a target must have changed in order
|
50
|
+
# to remain in the scheme. defaults to `:full_transitive`
|
51
|
+
# @param filter_when_scheme_has_changed [Boolean] whether the scheme should be filtered
|
52
|
+
# even when the changeset includes the scheme's path as changed.
|
53
|
+
# Defaults to `false`
|
54
|
+
# @param log_changes [Boolean] whether modifications to the scheme are logged.
|
55
|
+
# Defaults to `false`
|
56
|
+
# @param filter_scheme_for_build_action [:building, :testing]
|
57
|
+
# The xcodebuild action the scheme is being filtered for. The currently supported values are
|
58
|
+
# `:building` and `:testing`, with the only difference being `BuildActionEntry` are not
|
59
|
+
# filtered out when building for testing, since test action macro expansion could
|
60
|
+
# depend on a build entry being present.
|
61
|
+
# @param each_target [Proc] A proc called each time a target was determined to have changed or not.
|
62
|
+
# @return [Xcodeproj::XCScheme] a scheme whose unchanged targets have been removed.
|
63
|
+
def filtered_scheme(scheme_path:, filter_scheme_for_build_action:, change_level: :full_transitive, filter_when_scheme_has_changed: false, log_changes: false, each_target: nil)
|
64
|
+
scheme = Xcodeproj::XCScheme.new(scheme_path)
|
65
|
+
|
66
|
+
sections_to_filter =
|
67
|
+
case filter_scheme_for_build_action
|
68
|
+
when :building
|
69
|
+
%w[BuildActionEntry TestableReference]
|
70
|
+
when :testing
|
71
|
+
# don't want to filter out build action entries running
|
72
|
+
# xcodebuild build-for-testing / test, since the test action could have a macro expansion
|
73
|
+
# that depends upon one of the build targets.
|
74
|
+
%w[TestableReference]
|
75
|
+
else
|
76
|
+
raise ArgumentError,
|
77
|
+
'The supported values for the `filter_scheme_for_build_action` parameter are: [:building, :testing]. ' \
|
78
|
+
"Given: #{filter_scheme_for_build_action.inspect}."
|
79
|
+
end
|
80
|
+
|
81
|
+
if !filter_when_scheme_has_changed &&
|
82
|
+
UsedPath.new(path: Pathname(scheme_path), inclusion_reason: 'scheme').find_in_changesets(changesets)
|
83
|
+
return scheme
|
84
|
+
end
|
85
|
+
|
86
|
+
changes_by_suite_name = annotate_targets!
|
87
|
+
.to_h { |at| [at.xcode_target.name, at.change_reason(level: change_level)] }
|
88
|
+
|
89
|
+
doc = scheme.doc
|
90
|
+
|
91
|
+
xpaths = sections_to_filter.map { |section| "//*/#{section}/BuildableReference" }
|
92
|
+
xpaths.each do |xpath|
|
93
|
+
doc.get_elements(xpath).to_a.each do |buildable_reference|
|
94
|
+
suite_name = buildable_reference.attributes['BlueprintName']
|
95
|
+
if (change_reason = changes_by_suite_name[suite_name])
|
96
|
+
puts "#{suite_name} changed because #{change_reason}" if log_changes
|
97
|
+
each_target&.call(type: :changed, target_name: suite_name, change_reason:)
|
98
|
+
next
|
99
|
+
end
|
100
|
+
puts "#{suite_name} did not change, removing from scheme" if log_changes
|
101
|
+
each_target&.call(type: :unchanged, target_name: suite_name, change_reason: nil)
|
102
|
+
buildable_reference.parent.remove
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
if filter_scheme_for_build_action == :testing
|
107
|
+
doc.get_elements('//*/BuildActionEntry/BuildableReference').to_a.each do |buildable_reference|
|
108
|
+
suite_name = buildable_reference.attributes['BlueprintName']
|
109
|
+
if (change_reason = changes_by_suite_name[suite_name])
|
110
|
+
puts "#{suite_name} changed because #{change_reason}" if log_changes
|
111
|
+
each_target&.call(type: :changed, target_name: suite_name, change_reason:)
|
112
|
+
next
|
113
|
+
end
|
114
|
+
puts "#{suite_name} did not change, setting to not build for testing" if log_changes
|
115
|
+
each_target&.call(type: :unchanged, target_name: suite_name, change_reason: nil)
|
116
|
+
buildable_reference.parent.attributes['buildForTesting'] = 'NO'
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
scheme
|
121
|
+
end
|
122
|
+
|
123
|
+
# @return [String] a string suitable for user display that explains target changes
|
124
|
+
# @param include_unchanged_targets [Boolean] whether targets that have not changed should also be displayed
|
125
|
+
# @param change_level [Symbol] the change level used for computing whether a target has changed
|
126
|
+
def format_changes(include_unchanged_targets: false, change_level: :full_transitive)
|
127
|
+
annotate_targets!.group_by { |target| target.xcode_target.project.path.to_s }.sort_by(&:first)
|
128
|
+
.map do |project, annotated_targets|
|
129
|
+
changes = annotated_targets.sort_by { |annotated_target| annotated_target.xcode_target.name }
|
130
|
+
.map do |annotated_target|
|
131
|
+
change_reason = annotated_target.change_reason(level: change_level)
|
132
|
+
next if !include_unchanged_targets && !change_reason
|
133
|
+
|
134
|
+
change_reason ||= 'did not change'
|
135
|
+
"\t#{annotated_target.xcode_target}: #{change_reason}"
|
136
|
+
end.compact
|
137
|
+
"#{project}:\n#{changes.join("\n")}" unless changes.empty?
|
138
|
+
end.compact.join("\n")
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
# @return [Array<Xcodeproj::Project>]
|
144
|
+
def projects
|
145
|
+
@projects ||= find_projects(workspace_path)
|
146
|
+
end
|
147
|
+
|
148
|
+
# @return [Hash<String,Array<Hash>>]
|
149
|
+
def augmenting_paths_by_target
|
150
|
+
@augmenting_paths_by_target ||= begin
|
151
|
+
require 'yaml'
|
152
|
+
augmenting_paths_yaml_files.reduce({}) do |augmenting_paths_by_target, yaml_file|
|
153
|
+
yaml_file = Pathname(yaml_file).expand_path(changesets.first.repository)
|
154
|
+
yaml = YAML.safe_load(yaml_file.read)
|
155
|
+
augmenting_paths_by_target.merge(yaml) do |_target_name, prior_paths, new_paths|
|
156
|
+
prior_paths + new_paths
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# @return [Array<AnnotatedTarget>] targets in the given list of Xcode projects,
|
163
|
+
# annotated according to the given changeset
|
164
|
+
def annotated_targets
|
165
|
+
workspace_modification = find_workspace_modification_in_changesets
|
166
|
+
project_changes = projects.to_h do |project|
|
167
|
+
[project, find_project_modification_in_changesets(project:) || workspace_modification]
|
168
|
+
end
|
169
|
+
|
170
|
+
require 'tsort'
|
171
|
+
targets = projects.flat_map(&:targets)
|
172
|
+
targets_by_uuid = targets.to_h { |t| [t.uuid, t] }
|
173
|
+
targets_by_name = targets.to_h { |t| [t.name, t] }
|
174
|
+
targets_by_product_name = targets.each_with_object({}) do |t, h|
|
175
|
+
next unless t.respond_to?(:product_reference)
|
176
|
+
h[File.basename(t.product_reference.path)] = t
|
177
|
+
h[File.basename(t.product_reference.name)] = t if t.product_reference.name
|
178
|
+
end
|
179
|
+
|
180
|
+
find_dep = ->(td) { targets_by_uuid[td.native_target_uuid] || targets_by_name[td.name] }
|
181
|
+
target_deps = lambda do |target|
|
182
|
+
target_dependencies = []
|
183
|
+
target.dependencies.each do |td|
|
184
|
+
target_dependencies << find_dep[td]
|
185
|
+
end
|
186
|
+
|
187
|
+
# TODO: also resolve OTHER_LDFLAGS?
|
188
|
+
# yay auto-linking
|
189
|
+
if (phase = target.frameworks_build_phases)
|
190
|
+
phase.files_references.each do |fr|
|
191
|
+
if (dt = fr&.path && targets_by_product_name[File.basename(fr.path)])
|
192
|
+
target_dependencies << dt
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
target_dependencies
|
198
|
+
end
|
199
|
+
|
200
|
+
targets = TSort.tsort(
|
201
|
+
->(&b) { targets.each(&b) },
|
202
|
+
->(target, &b) { target_deps[target].each(&b) }
|
203
|
+
)
|
204
|
+
|
205
|
+
targets.each_with_object({}) do |target, h|
|
206
|
+
change_reason = project_changes[target.project] || find_target_modification_in_changesets(target:)
|
207
|
+
|
208
|
+
h[target] = AnnotatedTarget.new(
|
209
|
+
target:,
|
210
|
+
dependencies: target_deps[target].map { |td| h.fetch(td) },
|
211
|
+
change_reason:
|
212
|
+
)
|
213
|
+
end.values
|
214
|
+
end
|
215
|
+
|
216
|
+
# @return [Array<Xcodeproj::Project>] the projects found by walking the
|
217
|
+
# project/workspace at the given path
|
218
|
+
# @param path [Pathname] path to a `.xcodeproj` or `.xcworkspace` on disk
|
219
|
+
def find_projects(path)
|
220
|
+
seen = {}
|
221
|
+
find_projects_cached = lambda do |project_path|
|
222
|
+
return if seen.key?(project_path)
|
223
|
+
|
224
|
+
case File.extname(project_path)
|
225
|
+
when '.xcodeproj'
|
226
|
+
project = Xcodeproj::Project.open(project_path)
|
227
|
+
seen[project_path] = project
|
228
|
+
project.files.each do |file_reference|
|
229
|
+
next unless File.extname(file_reference.path) == '.xcodeproj'
|
230
|
+
|
231
|
+
find_projects_cached[file_reference.real_path]
|
232
|
+
end
|
233
|
+
when '.xcworkspace'
|
234
|
+
workspace = Xcodeproj::Workspace.new_from_xcworkspace(project_path)
|
235
|
+
workspace.file_references.each do |file_reference|
|
236
|
+
next unless File.extname(file_reference.path) == '.xcodeproj'
|
237
|
+
|
238
|
+
find_projects_cached[file_reference.absolute_path(File.dirname(project_path))]
|
239
|
+
end
|
240
|
+
else
|
241
|
+
raise ArgumentError, "Unknown path #{project_path.inspect}"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
find_projects_cached[path]
|
245
|
+
|
246
|
+
seen.values
|
247
|
+
end
|
248
|
+
|
249
|
+
# @yieldparam used_path [UsedPath] an absolute path that belongs to the given target
|
250
|
+
# @return [Void]
|
251
|
+
# @param target [Xcodeproj::Project::AbstractTarget]
|
252
|
+
def target_each_file_path(target:)
|
253
|
+
return enum_for(__method__, target:) unless block_given?
|
254
|
+
|
255
|
+
expand_build_settings = lambda do |s|
|
256
|
+
return [s] unless s =~ /\$(?:\{([_a-zA-Z0-0]+?)\}|\(([_a-zA-Z0-0]+?)\))/
|
257
|
+
|
258
|
+
match, key = Regexp.last_match.values_at(0, 1, 2).compact
|
259
|
+
substitutions = target.resolved_build_setting(key, true).values.compact.uniq
|
260
|
+
substitutions.flat_map do |sub|
|
261
|
+
expand_build_settings[s.gsub(match, sub)]
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
target.build_configuration_list.build_configurations.each do |build_configuration|
|
266
|
+
ref = build_configuration.base_configuration_reference
|
267
|
+
next unless ref
|
268
|
+
|
269
|
+
yield UsedPath.new(path: ref.real_path,
|
270
|
+
inclusion_reason: "base configuration reference for #{build_configuration}")
|
271
|
+
end
|
272
|
+
|
273
|
+
target.build_phases.each do |build_phase|
|
274
|
+
build_phase.files_references.each do |fr|
|
275
|
+
next unless fr
|
276
|
+
|
277
|
+
yield UsedPath.new(path: fr.real_path,
|
278
|
+
inclusion_reason: "#{build_phase.display_name.downcase.chomp('s')} file")
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
target.shell_script_build_phases.each do |shell_script_build_phase|
|
283
|
+
%w[input_file_list_paths output_file_list_paths input_paths output_paths].each do |method|
|
284
|
+
next unless (paths = shell_script_build_phase.public_send(method))
|
285
|
+
|
286
|
+
file_type = method.tr('_', ' ').chomp('s')
|
287
|
+
paths.each do |config_path|
|
288
|
+
next unless config_path
|
289
|
+
|
290
|
+
expand_build_settings[config_path].each do |path|
|
291
|
+
path = Pathname(path).expand_path(target.project.project_dir)
|
292
|
+
yield UsedPath.new(path:,
|
293
|
+
inclusion_reason: "#{shell_script_build_phase.name} build phase #{file_type}")
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
%w[INFOPLIST_FILE HEADER_SEARCH_PATHS FRAMEWORK_SEARCH_PATHS USER_HEADER_SEARCH_PATHS].each do |build_setting|
|
300
|
+
target.resolved_build_setting(build_setting, true).each_value do |paths|
|
301
|
+
Array(paths).each do |path|
|
302
|
+
next unless path
|
303
|
+
|
304
|
+
path = Pathname(path).expand_path(target.project.project_dir)
|
305
|
+
yield UsedPath.new(path:, inclusion_reason: "#{build_setting} value")
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# @return [FileModification,Nil] a modification to a file that is used by the given target, or `nil`
|
312
|
+
# if none if found
|
313
|
+
# @param target [Xcodeproj::Project::AbstractTarget]
|
314
|
+
def find_target_modification_in_changesets(target:)
|
315
|
+
augmenting_paths = used_paths_from_augmenting_paths_by_target[target.name]
|
316
|
+
find_in_changesets = ->(path) { path.find_in_changesets(changesets) }
|
317
|
+
Sq::Refinement.map_find(augmenting_paths, &find_in_changesets) ||
|
318
|
+
Sq::Refinement.map_find(target_each_file_path(target:), &find_in_changesets)
|
319
|
+
end
|
320
|
+
|
321
|
+
# @yieldparam used_path [UsedPath] an absolute path that belongs to the given project
|
322
|
+
# @return [Void]
|
323
|
+
# @param project [Xcodeproj::Project]
|
324
|
+
def project_each_file_path(project:)
|
325
|
+
return enum_for(__method__, project:) unless block_given?
|
326
|
+
|
327
|
+
yield UsedPath.new(path: project.path, inclusion_reason: 'project directory')
|
328
|
+
|
329
|
+
project.root_object.build_configuration_list.build_configurations.each do |build_configuration|
|
330
|
+
ref = build_configuration.base_configuration_reference
|
331
|
+
next unless ref
|
332
|
+
|
333
|
+
yield UsedPath.new(path: ref.real_path,
|
334
|
+
inclusion_reason: "base configuration reference for #{build_configuration}")
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
# # @return [FileModification,Nil] a modification to a file that is directly used by the given project, or `nil`
|
339
|
+
# if none if found
|
340
|
+
# @note This method does not take into account whatever file paths targets in the project may reference
|
341
|
+
# @param project [Xcodeproj::Project]
|
342
|
+
def find_project_modification_in_changesets(project:)
|
343
|
+
Sq::Refinement.map_find(project_each_file_path(project:)) do |path|
|
344
|
+
path.find_in_changesets(changesets)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# @return [FileModification,Nil] a modification to the workspace itself, or `nil`
|
349
|
+
# if none if found
|
350
|
+
# @note This method does not take into account whatever file paths projects or
|
351
|
+
# targets in the workspace path may reference
|
352
|
+
def find_workspace_modification_in_changesets
|
353
|
+
return unless workspace_path
|
354
|
+
|
355
|
+
UsedPath.new(path: workspace_path, inclusion_reason: 'workspace directory')
|
356
|
+
.find_in_changesets(changesets)
|
357
|
+
end
|
358
|
+
|
359
|
+
# @return [Hash<String,UsedPath>]
|
360
|
+
def used_paths_from_augmenting_paths_by_target
|
361
|
+
@used_paths_from_augmenting_paths_by_target ||= begin
|
362
|
+
repo = changesets.first.repository
|
363
|
+
used_paths_from_augmenting_paths_by_target =
|
364
|
+
augmenting_paths_by_target.transform_values do |augmenting_paths|
|
365
|
+
augmenting_paths.map do |augmenting_path|
|
366
|
+
case augmenting_path.keys.sort
|
367
|
+
when %w[inclusion_reason path], %w[inclusion_reason path yaml_keypath]
|
368
|
+
kwargs = {
|
369
|
+
path: Pathname(augmenting_path['path']).expand_path(repo),
|
370
|
+
inclusion_reason: augmenting_path['inclusion_reason']
|
371
|
+
}
|
372
|
+
if augmenting_path.key?('yaml_keypath')
|
373
|
+
kwargs[:yaml_keypath] = augmenting_path['yaml_keypath']
|
374
|
+
UsedPath::YAML.new(**kwargs)
|
375
|
+
else
|
376
|
+
UsedPath.new(**kwargs)
|
377
|
+
end
|
378
|
+
when %w[glob inclusion_reason]
|
379
|
+
UsedGlob.new(glob: File.expand_path(augmenting_path['glob'], repo),
|
380
|
+
inclusion_reason: augmenting_path['inclusion_reason'])
|
381
|
+
else
|
382
|
+
raise ArgumentError,
|
383
|
+
"unhandled set of keys in augmenting paths dictionary entry: #{augmenting_path.keys.inspect}"
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
wildcard_paths = used_paths_from_augmenting_paths_by_target.fetch('*', [])
|
388
|
+
|
389
|
+
Hash.new do |h, k|
|
390
|
+
h[k] = wildcard_paths + used_paths_from_augmenting_paths_by_target.fetch(k, [])
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'xcodeproj'
|
4
|
+
|
5
|
+
module Sq
|
6
|
+
module Refinement
|
7
|
+
# A target, annotated with any changes
|
8
|
+
class AnnotatedTarget
|
9
|
+
# @return [Xcodeproj::Project::AbstactTarget] the target in an Xcode project
|
10
|
+
attr_reader :xcode_target
|
11
|
+
|
12
|
+
# @return [String,Nil] the reason why the target has changed, or `nil` if it has not changed
|
13
|
+
attr_reader :direct_change_reason
|
14
|
+
private :direct_change_reason
|
15
|
+
|
16
|
+
def initialize(target:, change_reason:, dependencies: [])
|
17
|
+
@xcode_target = target
|
18
|
+
@direct_change_reason = change_reason
|
19
|
+
@dependencies = dependencies
|
20
|
+
dependencies.each do |dependency|
|
21
|
+
dependency.depended_upon_by << self
|
22
|
+
end
|
23
|
+
@depended_upon_by = []
|
24
|
+
end
|
25
|
+
|
26
|
+
# @visibility private
|
27
|
+
def to_s
|
28
|
+
xcode_target.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
CHANGE_LEVELS = %i[
|
32
|
+
itself
|
33
|
+
at_most_n_away
|
34
|
+
full_transitive
|
35
|
+
].freeze
|
36
|
+
private_constant :CHANGE_LEVELS
|
37
|
+
|
38
|
+
# @return [Boolean] whether the target has changed, at the given change level
|
39
|
+
# @param level [Symbol,(:at_most_n_away,Integer)] change level, e.g. :itself, :at_most_n_away, :full_transitive
|
40
|
+
def change_reason(level:)
|
41
|
+
@change_reason ||= {}
|
42
|
+
# need to use this form for memoization, as opposed to ||=,
|
43
|
+
# since this will (often) be nil and it makes a significant performance difference
|
44
|
+
return @change_reason[level] if @change_reason.key?(level)
|
45
|
+
|
46
|
+
@change_reason[level] =
|
47
|
+
case level
|
48
|
+
when :itself
|
49
|
+
direct_change_reason
|
50
|
+
when :full_transitive
|
51
|
+
direct_change_reason || Sq::Refinement.map_find(dependencies) do |dependency|
|
52
|
+
next unless (dependency_change_reason = dependency.change_reason(level:))
|
53
|
+
|
54
|
+
"dependency #{dependency} changed because #{dependency_change_reason}"
|
55
|
+
end
|
56
|
+
when proc { |symbol, int| (symbol == :at_most_n_away) && int.is_a?(Integer) }
|
57
|
+
distance_from_target = level.last
|
58
|
+
raise ArgumentError, "level must be positive, not #{distance_from_target}" if distance_from_target.negative?
|
59
|
+
|
60
|
+
change_reason = direct_change_reason
|
61
|
+
if distance_from_target.positive?
|
62
|
+
change_reason ||= Sq::Refinement.map_find(dependencies) do |dependency|
|
63
|
+
unless (dependency_change_reason = dependency.change_reason(level: [:at_most_n_away, level.last.pred]))
|
64
|
+
next
|
65
|
+
end
|
66
|
+
|
67
|
+
"dependency #{dependency} changed because #{dependency_change_reason}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
change_reason
|
71
|
+
else
|
72
|
+
raise Error, "No known change level #{level.inspect}, only #{CHANGE_LEVELS.inspect} are known"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [Array<AnnotatedTarget>] the list of annotated targets this target depends upon
|
77
|
+
attr_reader :dependencies
|
78
|
+
# @return [Array<AnnotatedTarget>] the list of annotated targets that depend upon this target
|
79
|
+
attr_reader :depended_upon_by
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|