refinement 0.6.0 → 0.7.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/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
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sq
|
4
|
+
module Refinement
|
5
|
+
# Represents a path that some target depends upon.
|
6
|
+
class UsedPath
|
7
|
+
# @return [Pathname] the absolute path to the file
|
8
|
+
attr_reader :path
|
9
|
+
private :path
|
10
|
+
|
11
|
+
# @return [String] the reason why this path is being used by a target
|
12
|
+
attr_reader :inclusion_reason
|
13
|
+
private :inclusion_reason
|
14
|
+
|
15
|
+
def initialize(path:, inclusion_reason:)
|
16
|
+
@path = path
|
17
|
+
@inclusion_reason = inclusion_reason
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Nil, String] If the path has been modified, a string explaining the modification
|
21
|
+
# @param changeset [Changeset] the changeset to search for a modification to this path
|
22
|
+
def find_in_changeset(changeset)
|
23
|
+
add_reason changeset.find_modification_for_path(absolute_path: path), changeset:
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Nil, String] If the path has been modified, a string explaining the modification
|
27
|
+
# @param changesets [Array<Changeset>] the changesets to search for a modification to this path
|
28
|
+
def find_in_changesets(changesets)
|
29
|
+
raise ArgumentError, 'Must provide at least one changeset' if changesets.empty?
|
30
|
+
|
31
|
+
changesets.reduce(true) do |explanation, changeset|
|
32
|
+
explanation && find_in_changeset(changeset)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [String]
|
37
|
+
# @visibility private
|
38
|
+
def to_s
|
39
|
+
"#{path.to_s.inspect} (#{inclusion_reason})"
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# @return [Nil, String] A string suitable for user display that explains
|
45
|
+
# why the given modification means a target is modified
|
46
|
+
# @param modification [Nil, FileModification]
|
47
|
+
# @param changeset [Changeset]
|
48
|
+
def add_reason(modification, changeset:)
|
49
|
+
return unless modification
|
50
|
+
|
51
|
+
add_changeset_description "#{modification.path} (#{inclusion_reason}) #{modification.type}", changeset:
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [String] A string suitable for user display that explains
|
55
|
+
# why the given modification means a target is modified, including the description
|
56
|
+
# of the changeset that contains the modification
|
57
|
+
# @param description [String]
|
58
|
+
# @param changeset [Nil, Changeset]
|
59
|
+
def add_changeset_description(description, changeset:)
|
60
|
+
return description unless changeset&.description
|
61
|
+
|
62
|
+
description + " (#{changeset.description})"
|
63
|
+
end
|
64
|
+
|
65
|
+
# Represents a path to a YAML file that some target depends upon,
|
66
|
+
# but where only a subset of the YAML is needed to determine a change.
|
67
|
+
class YAML < UsedPath
|
68
|
+
# @return [Array] the keypath to search for modifications in a YAML document
|
69
|
+
attr_reader :yaml_keypath
|
70
|
+
private :yaml_keypath
|
71
|
+
|
72
|
+
def initialize(yaml_keypath:, **kwargs)
|
73
|
+
super(**kwargs)
|
74
|
+
@yaml_keypath = yaml_keypath
|
75
|
+
end
|
76
|
+
|
77
|
+
# (see UsedPath#find_in_changeset)
|
78
|
+
def find_in_changeset(changeset)
|
79
|
+
modification, _yaml_diff = changeset.find_modification_for_yaml_keypath(absolute_path: path, keypath: yaml_keypath)
|
80
|
+
add_reason modification, changeset:
|
81
|
+
end
|
82
|
+
|
83
|
+
# (see UsedPath#to_s)
|
84
|
+
def to_s
|
85
|
+
"#{path.to_s.inspect} @ #{yaml_keypath.join('.')} (#{inclusion_reason})"
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# (see UsedPath#add_reason)
|
91
|
+
def add_reason(modification, changeset:)
|
92
|
+
return unless modification
|
93
|
+
|
94
|
+
keypath_string =
|
95
|
+
if yaml_keypath.empty?
|
96
|
+
''
|
97
|
+
else
|
98
|
+
" @ #{yaml_keypath.map { |path| path.to_s =~ /\A[a-zA-Z0-9_]+\z/ ? path : path.inspect }.join('.')}"
|
99
|
+
end
|
100
|
+
add_changeset_description "#{modification.path}#{keypath_string} (#{inclusion_reason}) #{modification.type}", changeset:
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Represents a glob that some target depends upon.
|
106
|
+
class UsedGlob
|
107
|
+
# @return [String] a relative path glob
|
108
|
+
attr_reader :glob
|
109
|
+
private :glob
|
110
|
+
|
111
|
+
# (see UsedPath#inclusion_reason)
|
112
|
+
attr_reader :inclusion_reason
|
113
|
+
private :inclusion_reason
|
114
|
+
|
115
|
+
def initialize(glob:, inclusion_reason:)
|
116
|
+
@glob = glob
|
117
|
+
@inclusion_reason = inclusion_reason
|
118
|
+
end
|
119
|
+
|
120
|
+
# (see UsedPath#find_in_changeset)
|
121
|
+
def find_in_changeset(changeset)
|
122
|
+
add_reason changeset.find_modification_for_glob(absolute_glob: glob), changeset:
|
123
|
+
end
|
124
|
+
|
125
|
+
# (see UsedPath#find_in_changesets)
|
126
|
+
def find_in_changesets(changesets)
|
127
|
+
raise ArgumentError, 'Must provide at least one changeset' if changesets.empty?
|
128
|
+
|
129
|
+
changesets.reduce(true) do |explanation, changeset|
|
130
|
+
explanation && find_in_changeset(changeset)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# (see UsedPath#to_s)
|
135
|
+
def to_s
|
136
|
+
"#{glob.to_s.inspect} (#{inclusion_reason})"
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
# (see UsedPath#add_reason)
|
142
|
+
def add_reason(modification, changeset:)
|
143
|
+
return unless modification
|
144
|
+
|
145
|
+
add_changeset_description "#{modification.path} (#{inclusion_reason}) #{modification.type}", changeset:
|
146
|
+
end
|
147
|
+
|
148
|
+
# (see UsedPath#add_changeset_description)
|
149
|
+
def add_changeset_description(description, changeset:)
|
150
|
+
return description unless changeset&.description
|
151
|
+
|
152
|
+
description + " (#{changeset.description})"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: refinement
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Giddins
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-08-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: xcodeproj
|
@@ -30,20 +30,6 @@ dependencies:
|
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: '2'
|
33
|
-
- !ruby/object:Gem::Dependency
|
34
|
-
name: rake
|
35
|
-
requirement: !ruby/object:Gem::Requirement
|
36
|
-
requirements:
|
37
|
-
- - "~>"
|
38
|
-
- !ruby/object:Gem::Version
|
39
|
-
version: '10.0'
|
40
|
-
type: :development
|
41
|
-
prerelease: false
|
42
|
-
version_requirements: !ruby/object:Gem::Requirement
|
43
|
-
requirements:
|
44
|
-
- - "~>"
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: '10.0'
|
47
33
|
description:
|
48
34
|
email:
|
49
35
|
- segiddins@squareup.com
|
@@ -59,17 +45,19 @@ files:
|
|
59
45
|
- exe/refine
|
60
46
|
- lib/cocoapods_plugin.rb
|
61
47
|
- lib/refinement.rb
|
62
|
-
- lib/refinement/analyzer.rb
|
63
|
-
- lib/refinement/annotated_target.rb
|
64
|
-
- lib/refinement/changeset.rb
|
65
|
-
- lib/refinement/changeset/file_modification.rb
|
66
|
-
- lib/refinement/cli.rb
|
67
|
-
- lib/refinement/cocoapods_post_install_writer.rb
|
68
|
-
- lib/refinement/
|
69
|
-
- lib/refinement/
|
48
|
+
- lib/sq/refinement/analyzer.rb
|
49
|
+
- lib/sq/refinement/annotated_target.rb
|
50
|
+
- lib/sq/refinement/changeset.rb
|
51
|
+
- lib/sq/refinement/changeset/file_modification.rb
|
52
|
+
- lib/sq/refinement/cli.rb
|
53
|
+
- lib/sq/refinement/cocoapods_post_install_writer.rb
|
54
|
+
- lib/sq/refinement/setup.rb
|
55
|
+
- lib/sq/refinement/used_path.rb
|
56
|
+
- lib/sq/refinement/version.rb
|
70
57
|
homepage: https://github.com/square/refinement
|
71
58
|
licenses: []
|
72
|
-
metadata:
|
59
|
+
metadata:
|
60
|
+
rubygems_mfa_required: 'true'
|
73
61
|
post_install_message:
|
74
62
|
rdoc_options: []
|
75
63
|
require_paths:
|
@@ -78,14 +66,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
78
66
|
requirements:
|
79
67
|
- - ">="
|
80
68
|
- !ruby/object:Gem::Version
|
81
|
-
version: '
|
69
|
+
version: '3.1'
|
82
70
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
71
|
requirements:
|
84
72
|
- - ">="
|
85
73
|
- !ruby/object:Gem::Version
|
86
74
|
version: '0'
|
87
75
|
requirements: []
|
88
|
-
rubygems_version: 3.
|
76
|
+
rubygems_version: 3.3.26
|
89
77
|
signing_key:
|
90
78
|
specification_version: 4
|
91
79
|
summary: Generates a list of Xcode targets to build & test as a result of a git diff.
|
data/lib/refinement/analyzer.rb
DELETED
@@ -1,395 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Refinement
|
4
|
-
# Analyzes changes in a repository
|
5
|
-
# and determines how those changes impact the targets in Xcode projects in the workspace.
|
6
|
-
class Analyzer
|
7
|
-
attr_reader :changesets, :workspace_path, :augmenting_paths_yaml_files
|
8
|
-
private :changesets, :workspace_path, :augmenting_paths_yaml_files
|
9
|
-
|
10
|
-
# Initializes an analyzer with changesets, projects, and augmenting paths.
|
11
|
-
# @param changesets [Array<Changeset>]
|
12
|
-
# @param workspace_path [Pathname] path to a root workspace or project,
|
13
|
-
# must be `nil` if `projects` are specified explicitly
|
14
|
-
# @param projects [Array<Xcodeproj::Project>] projects to find targets in,
|
15
|
-
# must not be specified if `workspace_path` is not `nil`
|
16
|
-
# @param augmenting_paths_yaml_files [Array<Pathname>] paths to YAML files that provide augmenting paths by target,
|
17
|
-
# must be `nil` if `augmenting_paths_by_target` are specified explicitly
|
18
|
-
# @param augmenting_paths_by_target [Hash<String, Array>] arrays of hashes keyed by target name
|
19
|
-
# (or '*' for all targets)
|
20
|
-
# describing paths or globs that each target should be considered to be using,
|
21
|
-
# must not be specified if `augmenting_paths_yaml_files` is not `nil`
|
22
|
-
#
|
23
|
-
# @raise [ArgumentError] when conflicting arguments are given
|
24
|
-
#
|
25
|
-
def initialize(changesets:, workspace_path:, projects: nil,
|
26
|
-
augmenting_paths_yaml_files:, augmenting_paths_by_target: nil)
|
27
|
-
|
28
|
-
@changesets = changesets
|
29
|
-
|
30
|
-
raise ArgumentError, 'Can only specify one of workspace_path and projects' if workspace_path && projects
|
31
|
-
|
32
|
-
@workspace_path = workspace_path
|
33
|
-
@projects = projects
|
34
|
-
|
35
|
-
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
|
36
|
-
|
37
|
-
@augmenting_paths_yaml_files = augmenting_paths_yaml_files
|
38
|
-
@augmenting_paths_by_target = augmenting_paths_by_target
|
39
|
-
end
|
40
|
-
|
41
|
-
# @return [Array<AnnotatedTarget>] targets from the projects annotated with their changes, based upon
|
42
|
-
# the changeset
|
43
|
-
def annotate_targets!
|
44
|
-
@annotate_targets ||= annotated_targets
|
45
|
-
end
|
46
|
-
|
47
|
-
# @param scheme_path [Pathname] the absolute path to the scheme to be filtered
|
48
|
-
# @param change_level [Symbol] the change level at which a target must have changed in order
|
49
|
-
# to remain in the scheme. defaults to `:full_transitive`
|
50
|
-
# @param filter_when_scheme_has_changed [Boolean] whether the scheme should be filtered
|
51
|
-
# even when the changeset includes the scheme's path as changed.
|
52
|
-
# Defaults to `false`
|
53
|
-
# @param log_changes [Boolean] whether modifications to the scheme are logged.
|
54
|
-
# Defaults to `false`
|
55
|
-
# @param filter_scheme_for_build_action [:building, :testing]
|
56
|
-
# The xcodebuild action the scheme is being filtered for. The currently supported values are
|
57
|
-
# `:building` and `:testing`, with the only difference being `BuildActionEntry` are not
|
58
|
-
# filtered out when building for testing, since test action macro expansion could
|
59
|
-
# depend on a build entry being present.
|
60
|
-
# @param each_target [Proc] A proc called each time a target was determined to have changed or not.
|
61
|
-
# @return [Xcodeproj::XCScheme] a scheme whose unchanged targets have been removed.
|
62
|
-
def filtered_scheme(scheme_path:, change_level: :full_transitive, filter_when_scheme_has_changed: false, log_changes: false,
|
63
|
-
filter_scheme_for_build_action:, 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 = Hash[annotate_targets!
|
87
|
-
.map { |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: 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: 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 = Hash[projects.map do |project|
|
167
|
-
[project, find_project_modification_in_changesets(project: project) || workspace_modification]
|
168
|
-
end]
|
169
|
-
|
170
|
-
require 'tsort'
|
171
|
-
targets = projects.flat_map(&:targets)
|
172
|
-
targets_by_uuid = Hash[targets.map { |t| [t.uuid, t] }]
|
173
|
-
targets_by_name = Hash[targets.map { |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: target)
|
207
|
-
|
208
|
-
h[target] = AnnotatedTarget.new(
|
209
|
-
target: target,
|
210
|
-
dependencies: target_deps[target].map { |td| h.fetch(td) },
|
211
|
-
change_reason: 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: 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: 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: 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
|
-
Refinement.map_find(augmenting_paths, &find_in_changesets) ||
|
318
|
-
Refinement.map_find(target_each_file_path(target: 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: 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
|
-
Refinement.map_find(project_each_file_path(project: 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.each_with_object({}) do |(name, augmenting_paths), h|
|
365
|
-
h[name] = 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
|