spinjector 0.0.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +21 -0
- data/CHANGELOG.md +20 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +53 -0
- data/Makefile +11 -0
- data/README.md +27 -2
- data/Rakefile +11 -0
- data/lib/spinjector/entity/configuration.rb +9 -0
- data/lib/spinjector/entity/script.rb +47 -0
- data/lib/spinjector/entity/target.rb +14 -0
- data/lib/spinjector/logger.rb +16 -0
- data/lib/spinjector/project_service.rb +191 -0
- data/lib/spinjector/runner.rb +32 -0
- data/lib/spinjector/script_mapper.rb +36 -0
- data/lib/spinjector/yaml_parser.rb +83 -0
- data/lib/spinjector.rb +10 -162
- data/spinjector.gemspec +9 -3
- metadata +97 -9
- data/Examples/Configuration/hello_world_input +0 -0
- data/Examples/Configuration/hello_world_output +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 770afb3d2da618abf0c2f12be01a8638d6190c5b21085d8ee6cbc89a05159e50
|
4
|
+
data.tar.gz: 6d371c30b8a6fa185a7775bf5b63e34f76269b25beb63cd738d0a0627ca48237
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f627ae1db194c15105e591e3d7f1c43000df84b72b47701e3c587b6962f6ba25978ad6b52763be3f11d1550eebc454bbd5b858adfa112557f1e1f4bbc8583d6e
|
7
|
+
data.tar.gz: 884f0b622a6f4bc9c3d421200e0651970f961831fc4cada4dc87c3e1814ed0cc2d8f85a7ccf4b3adc293f6a57b48a2428241452d4edce84202301aee0a54fc11
|
@@ -0,0 +1,21 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
strategy:
|
9
|
+
matrix:
|
10
|
+
ruby: ["2.6.6", "2.7.3", "3.0.1"]
|
11
|
+
|
12
|
+
steps:
|
13
|
+
- uses: actions/checkout@v2
|
14
|
+
- uses: ruby/setup-ruby@v1
|
15
|
+
with:
|
16
|
+
ruby-version: ${{ matrix.ruby }}
|
17
|
+
- name: Build and test with Rake
|
18
|
+
run: |
|
19
|
+
gem install bundler
|
20
|
+
bundle install --jobs 4 --retry 3
|
21
|
+
bundle exec rake test
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Change Log
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
`spinjector` adheres to [Semantic Versioning](http://semver.org/).
|
4
|
+
|
5
|
+
## [NEXT] - Date
|
6
|
+
|
7
|
+
### Created
|
8
|
+
|
9
|
+
### Fixed
|
10
|
+
|
11
|
+
### Removed
|
12
|
+
|
13
|
+
## [1.0.0] - 2022-12-19
|
14
|
+
|
15
|
+
### Created
|
16
|
+
|
17
|
+
- Make the script idempotent. Successive calls to configure don’t modify the pbxproj file anymore
|
18
|
+
- Added tests
|
19
|
+
|
20
|
+
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
spinjector (0.0.6)
|
5
|
+
optparse (~> 0.1)
|
6
|
+
xcodeproj (~> 1.21)
|
7
|
+
yaml (~> 0.2)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
CFPropertyList (3.0.5)
|
13
|
+
rexml
|
14
|
+
ansi (1.5.0)
|
15
|
+
atomos (0.1.3)
|
16
|
+
builder (3.2.4)
|
17
|
+
claide (1.1.0)
|
18
|
+
colored2 (3.1.2)
|
19
|
+
minitest (5.15.0)
|
20
|
+
minitest-hooks (1.5.0)
|
21
|
+
minitest (> 5.3)
|
22
|
+
minitest-reporters (1.5.0)
|
23
|
+
ansi
|
24
|
+
builder
|
25
|
+
minitest (>= 5.0)
|
26
|
+
ruby-progressbar
|
27
|
+
nanaimo (0.3.0)
|
28
|
+
optparse (0.2.0)
|
29
|
+
rake (12.3.3)
|
30
|
+
rexml (3.2.5)
|
31
|
+
ruby-progressbar (1.11.0)
|
32
|
+
xcodeproj (1.21.0)
|
33
|
+
CFPropertyList (>= 2.3.3, < 4.0)
|
34
|
+
atomos (~> 0.1.3)
|
35
|
+
claide (>= 1.0.2, < 2.0)
|
36
|
+
colored2 (~> 3.1)
|
37
|
+
nanaimo (~> 0.3.0)
|
38
|
+
rexml (~> 3.2.4)
|
39
|
+
yaml (0.2.0)
|
40
|
+
|
41
|
+
PLATFORMS
|
42
|
+
x86_64-darwin-18
|
43
|
+
|
44
|
+
DEPENDENCIES
|
45
|
+
bundler (>= 1.12.0, < 3.0.0)
|
46
|
+
minitest (~> 5.11)
|
47
|
+
minitest-hooks (~> 1.5)
|
48
|
+
minitest-reporters (~> 1.3)
|
49
|
+
rake (~> 12.3)
|
50
|
+
spinjector!
|
51
|
+
|
52
|
+
BUNDLED WITH
|
53
|
+
2.2.26
|
data/Makefile
ADDED
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# spinjector
|
2
|
+
|
2
3
|
Inject Script phase in your Xcode project easily.
|
3
4
|
|
4
5
|
# How to install
|
@@ -8,7 +9,9 @@ gem install spinjector
|
|
8
9
|
```
|
9
10
|
|
10
11
|
# How to use
|
12
|
+
|
11
13
|
## Global configuration file
|
14
|
+
|
12
15
|
First, create a YAML configuration file under `./Configuration/spinjector_configuration.yaml` (default path where spinjector looks for a configuration file).
|
13
16
|
|
14
17
|
```
|
@@ -31,7 +34,9 @@ targets:
|
|
31
34
|
```
|
32
35
|
|
33
36
|
## Script configuration file
|
37
|
+
|
34
38
|
Then, for each script you want to inject in your Xcode project:
|
39
|
+
|
35
40
|
- You can use `scripts` section in the global configuration file to define your script directly (eg. `foo`)...
|
36
41
|
|
37
42
|
- ...Or create a script configuration file (eg. `helloworld.yaml`)
|
@@ -60,16 +65,19 @@ output_file_list_paths: # optional.
|
|
60
65
|
|
61
66
|
dependency_file: # optional.
|
62
67
|
|
63
|
-
execution_position: # optional. [:
|
68
|
+
execution_position: # optional. [:before_compile | :after_compile | :before_headers | :after_headers | :after_all].
|
64
69
|
```
|
65
70
|
|
66
|
-
- If you use the `script_path option
|
71
|
+
- If you use the `script_path option`, create the script file
|
72
|
+
|
67
73
|
```
|
68
74
|
echo Hello World
|
69
75
|
```
|
70
76
|
|
71
77
|
## Execution
|
78
|
+
|
72
79
|
Finally, inject script phases
|
80
|
+
|
73
81
|
```
|
74
82
|
spinjector [-c] <path-to-your-global-configuration-file>
|
75
83
|
```
|
@@ -77,3 +85,20 @@ spinjector [-c] <path-to-your-global-configuration-file>
|
|
77
85
|
Enjoy your build phases
|
78
86
|

|
79
87
|

|
88
|
+
|
89
|
+
## How to contribute
|
90
|
+
|
91
|
+
1. After all your changes are reviewed and merged
|
92
|
+
2. Create a `release` branch
|
93
|
+
3. Update the version in field `s.version` from file `spinjector.gemspec`
|
94
|
+
4. Execute `make publish`
|
95
|
+
|
96
|
+
You may need to configure your account at step `4.` if you've never pushed any gem. You can find all the informations you need on [the official documentation](https://guides.rubygems.org/make-your-own-gem/#your-first-gem).
|
97
|
+
|
98
|
+
## Tests
|
99
|
+
|
100
|
+
To run the tests, simply run:
|
101
|
+
|
102
|
+
```sh
|
103
|
+
rake test
|
104
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
class Script
|
3
|
+
|
4
|
+
attr_reader :name, :source_code, :shell_path, :input_paths, :output_paths, :input_file_list_paths, :output_file_list_paths, :dependency_file, :execution_position, :show_env_vars_in_log
|
5
|
+
|
6
|
+
def initialize(
|
7
|
+
name,
|
8
|
+
source_code,
|
9
|
+
shell_path,
|
10
|
+
input_paths,
|
11
|
+
output_paths,
|
12
|
+
input_file_list_paths,
|
13
|
+
output_file_list_paths,
|
14
|
+
dependency_file,
|
15
|
+
execution_position,
|
16
|
+
show_env_vars_in_log
|
17
|
+
)
|
18
|
+
@name = name
|
19
|
+
@source_code = source_code
|
20
|
+
@shell_path = shell_path
|
21
|
+
@input_paths = input_paths
|
22
|
+
@output_paths = output_paths
|
23
|
+
@input_file_list_paths = input_file_list_paths
|
24
|
+
@output_file_list_paths = output_file_list_paths
|
25
|
+
@dependency_file = dependency_file
|
26
|
+
@execution_position = execution_position
|
27
|
+
@show_env_vars_in_log = show_env_vars_in_log
|
28
|
+
verify()
|
29
|
+
end
|
30
|
+
|
31
|
+
def verify
|
32
|
+
verify_execution_position()
|
33
|
+
end
|
34
|
+
|
35
|
+
def verify_execution_position
|
36
|
+
case execution_position
|
37
|
+
when :before_compile, :before_headers
|
38
|
+
true
|
39
|
+
when :after_compile, :after_headers
|
40
|
+
false
|
41
|
+
when :after_all,
|
42
|
+
false
|
43
|
+
else
|
44
|
+
raise ArgumentError, "Unknown execution position `#{execution_position}`"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
require 'xcodeproj'
|
2
|
+
require_relative 'entity/configuration'
|
3
|
+
require_relative 'entity/script'
|
4
|
+
require_relative 'entity/target'
|
5
|
+
|
6
|
+
# @return [String] prefix used for all the build phase injected by this script
|
7
|
+
# [SPI] stands for Script Phase Injector
|
8
|
+
#
|
9
|
+
BUILD_PHASE_PREFIX = '[SPI] '.freeze
|
10
|
+
|
11
|
+
class ProjectService
|
12
|
+
|
13
|
+
# @param [Xcodeproj::Project] project
|
14
|
+
#
|
15
|
+
def initialize(project, logger)
|
16
|
+
@project = project
|
17
|
+
@logger = logger
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param [Configuration] configuration containing all scripts to add in each target
|
21
|
+
#
|
22
|
+
def update_scripts_in_targets(configuration)
|
23
|
+
@project.targets.each do |target|
|
24
|
+
@insertion_offset_after_compile = 0
|
25
|
+
@insertion_offset_after_headers = 0
|
26
|
+
target_configuration = configuration.targets.find { |conf_target| conf_target.name == target.name }
|
27
|
+
if target_configuration == nil
|
28
|
+
@logger.log "No Spinjector managed build phases in target #{target}"
|
29
|
+
remove_all_scripts(target)
|
30
|
+
next
|
31
|
+
end
|
32
|
+
@logger.log "Configurating target #{target}"
|
33
|
+
scripts_to_apply = target_configuration.scripts_names.map { |name| BUILD_PHASE_PREFIX + name }.to_set
|
34
|
+
native_target_script_phases = target.shell_script_build_phases.select do |bp|
|
35
|
+
!bp.name.nil? && bp.name.start_with?(BUILD_PHASE_PREFIX)
|
36
|
+
end
|
37
|
+
native_target_script_phases.each do |script_phase|
|
38
|
+
if scripts_to_apply.include?(script_phase.name)
|
39
|
+
# Update existing script phase with new values
|
40
|
+
script_configuration = target_configuration.scripts.find { |script|
|
41
|
+
BUILD_PHASE_PREFIX + script.name == script_phase.name
|
42
|
+
}
|
43
|
+
update_script_in_target(script_phase, script_configuration, target)
|
44
|
+
scripts_to_apply.delete(script_phase.name)
|
45
|
+
elsif
|
46
|
+
target.build_phases.delete(script_phase)
|
47
|
+
# Remove now defunct script phase
|
48
|
+
end
|
49
|
+
end
|
50
|
+
# We may miss scripts that are yet to be added to the pbxproj target, this is fixed in the following method
|
51
|
+
reorder_and_add_missing_script_phases_of(target, target_configuration)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# @param [Xcodeproj::Project::Object::PBXNativeTarget] target to remove the script phases
|
58
|
+
# @return [<Xcodeproj::Project::Object::PBXShellScriptBuildPhase>] the newly created build phase
|
59
|
+
#
|
60
|
+
def spinjector_managed_phases(target)
|
61
|
+
spinjector_managed_phases = target.shell_script_build_phases.select do |bp|
|
62
|
+
!bp.name.nil? && bp.name.start_with?(BUILD_PHASE_PREFIX)
|
63
|
+
end
|
64
|
+
return spinjector_managed_phases
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param [Xcodeproj::Project::Object::PBXNativeTarget] target to remove the script phases
|
68
|
+
#
|
69
|
+
def remove_all_scripts(target)
|
70
|
+
# Delete all the spinjector managed scripts from the selected target
|
71
|
+
spinjector_managed_phases(target).each do |script_phase|
|
72
|
+
target.build_phases.delete(script_phase)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param [Xcodeproj::Project::Object::PBXNativeTarget] target to add the script phases
|
77
|
+
# @param [Target] the target configuration describing the scripts to be added
|
78
|
+
#
|
79
|
+
def reorder_and_add_missing_script_phases_of(target, target_configuration)
|
80
|
+
target_configuration.scripts.each do |script|
|
81
|
+
current_phase = spinjector_managed_phases(target).find { |phase| phase.name == BUILD_PHASE_PREFIX + script.name }
|
82
|
+
if current_phase == nil && should_add_script_in_target(script, target)
|
83
|
+
current_phase = add_script_in_target(script, target)
|
84
|
+
end
|
85
|
+
execution_position = script.execution_position
|
86
|
+
reorder_script_phase(target, current_phase, execution_position)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Checks If the target contains a build phase associated to the script execution position
|
91
|
+
# For instance, it checks if the Headers build phase is present when the script has a :after_headers position
|
92
|
+
# @param [Script] script the script phase defined in configuration files to add to the target
|
93
|
+
# @param [Xcodeproj::Project::Object::PBXNativeTarget] target to add the script phases
|
94
|
+
# @return [Boolean] If the script should be added to the target
|
95
|
+
#
|
96
|
+
def should_add_script_in_target(script, target)
|
97
|
+
return true if script.execution_position === :after_all
|
98
|
+
|
99
|
+
target_phase_type = case script.execution_position
|
100
|
+
when :before_compile, :after_compile
|
101
|
+
Xcodeproj::Project::Object::PBXSourcesBuildPhase
|
102
|
+
when :before_headers, :after_headers
|
103
|
+
Xcodeproj::Project::Object::PBXHeadersBuildPhase
|
104
|
+
else
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
|
108
|
+
found_phase_in_target = target.build_phases.find { |bp| bp.is_a?(target_phase_type) }
|
109
|
+
@logger.log("Can't find build phase associated to execution position #{script.execution_position} in target #{target.name}") if found_phase_in_target.nil?
|
110
|
+
return !found_phase_in_target.nil?
|
111
|
+
end
|
112
|
+
|
113
|
+
# @param [Script] script the script phase defined in configuration files to add to the target
|
114
|
+
# @param [Xcodeproj::Project::Object::PBXNativeTarget] target to add the script phases
|
115
|
+
# @return [Xcodeproj::Project::Object::PBXShellScriptBuildPhase] the newly created build phase
|
116
|
+
#
|
117
|
+
def add_script_in_target(script, target)
|
118
|
+
name_with_prefix = BUILD_PHASE_PREFIX + script.name
|
119
|
+
phase = target.new_shell_script_build_phase(name_with_prefix)
|
120
|
+
update_script_in_target(phase, script, target)
|
121
|
+
return phase
|
122
|
+
end
|
123
|
+
|
124
|
+
# @param [Xcodeproj::Project::Object::PBXShellScriptBuildPhase] phase to update with the values from the script
|
125
|
+
# @param [Script] script the script phase defined in configuration files to add to the target
|
126
|
+
# @param [Xcodeproj::Project::Object::PBXNativeTarget] target to add the script phase
|
127
|
+
#
|
128
|
+
def update_script_in_target(existing_phase, script_configuration, target)
|
129
|
+
existing_phase.shell_script = script_configuration.source_code
|
130
|
+
existing_phase.shell_path = script_configuration.shell_path
|
131
|
+
existing_phase.input_paths = script_configuration.input_paths
|
132
|
+
existing_phase.output_paths = script_configuration.output_paths
|
133
|
+
existing_phase.input_file_list_paths = script_configuration.input_file_list_paths
|
134
|
+
existing_phase.output_file_list_paths = script_configuration.output_file_list_paths
|
135
|
+
existing_phase.dependency_file = script_configuration.dependency_file
|
136
|
+
# At least with Xcode 10 `showEnvVarsInLog` is *NOT* set to any value even if it's checked and it only
|
137
|
+
# gets set to '0' if the user has explicitly disabled this.
|
138
|
+
if script_configuration.show_env_vars_in_log == '0'
|
139
|
+
existing_phase.show_env_vars_in_log = script_configuration.show_env_vars_in_log
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# @param [Xcodeproj::Project::Object::PBXNativeTarget] target where build phases should be reordered
|
144
|
+
# @param [Hash] script_phase to reorder
|
145
|
+
# @param [Symbol] execution_position could be :before_compile, :after_compile, :before_headers, :after_headers
|
146
|
+
#
|
147
|
+
def reorder_script_phase(target, script_phase, execution_position)
|
148
|
+
return if execution_position == :any || execution_position.to_s.empty? || script_phase.nil?
|
149
|
+
if execution_position == :after_all
|
150
|
+
target.build_phases.move(script_phase, target.build_phases.count - 1)
|
151
|
+
return
|
152
|
+
end
|
153
|
+
|
154
|
+
offset = -1
|
155
|
+
# Find the point P where to add the script phase
|
156
|
+
target_phase_type = case execution_position
|
157
|
+
when :before_compile
|
158
|
+
Xcodeproj::Project::Object::PBXSourcesBuildPhase
|
159
|
+
when :after_compile
|
160
|
+
offset = @insertion_offset_after_compile
|
161
|
+
@insertion_offset_after_compile += 1
|
162
|
+
Xcodeproj::Project::Object::PBXSourcesBuildPhase
|
163
|
+
when :before_headers
|
164
|
+
Xcodeproj::Project::Object::PBXHeadersBuildPhase
|
165
|
+
when :after_headers
|
166
|
+
offset = @insertion_offset_after_headers
|
167
|
+
@insertion_offset_after_headers += 1
|
168
|
+
Xcodeproj::Project::Object::PBXHeadersBuildPhase
|
169
|
+
else
|
170
|
+
raise ArgumentError, "Unknown execution position `#{execution_position}`"
|
171
|
+
end
|
172
|
+
|
173
|
+
# Get the first build phase index of P
|
174
|
+
target_phase_index = target.build_phases.index do |bp|
|
175
|
+
bp.is_a?(target_phase_type)
|
176
|
+
end
|
177
|
+
return if target_phase_index.nil?
|
178
|
+
|
179
|
+
# Get the script phase we want to reorder index
|
180
|
+
script_phase_index = target.build_phases.index do |bp|
|
181
|
+
bp.is_a?(Xcodeproj::Project::Object::PBXShellScriptBuildPhase) && !bp.name.nil? && bp.name == script_phase.name
|
182
|
+
end
|
183
|
+
if target_phase_index < script_phase_index
|
184
|
+
offset += 1
|
185
|
+
end
|
186
|
+
|
187
|
+
target.build_phases.move_from(script_phase_index, target_phase_index + offset)
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Inject script phases into your Xcode project
|
2
|
+
#
|
3
|
+
# @author Guillaume Berthier
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'xcodeproj'
|
7
|
+
require_relative './project_service'
|
8
|
+
require_relative './yaml_parser'
|
9
|
+
|
10
|
+
class Runner
|
11
|
+
|
12
|
+
attr_reader :project
|
13
|
+
|
14
|
+
def initialize(project_path, configuration_file_path, logger)
|
15
|
+
raise "[Error] No xcodeproj found at #{project_path}" unless File.exist?(project_path)
|
16
|
+
@project = Xcodeproj::Project.open(project_path)
|
17
|
+
@configuration_file_path = configuration_file_path
|
18
|
+
@logger = logger
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
configuration = YAMLParser.new(@configuration_file_path, @logger).configuration
|
23
|
+
|
24
|
+
project_service = ProjectService.new(project, @logger)
|
25
|
+
project_service.update_scripts_in_targets(configuration)
|
26
|
+
|
27
|
+
@project.save()
|
28
|
+
@logger.log "Success."
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'entity/script'
|
2
|
+
|
3
|
+
class ScriptMapper
|
4
|
+
|
5
|
+
def initialize(script_hash)
|
6
|
+
@script_hash = script_hash
|
7
|
+
verify_syntax
|
8
|
+
end
|
9
|
+
|
10
|
+
def map
|
11
|
+
script_code = @script_hash["script"] || load_script(@script_hash["script_path"])
|
12
|
+
Script.new(
|
13
|
+
@script_hash["name"],
|
14
|
+
script_code,
|
15
|
+
@script_hash["shell_path"] || '/bin/sh',
|
16
|
+
@script_hash["input_paths"] || [],
|
17
|
+
@script_hash["output_paths"] || [],
|
18
|
+
@script_hash["input_file_list_paths"] || [],
|
19
|
+
@script_hash["output_file_list_paths"] || [],
|
20
|
+
@script_hash["dependency_file"],
|
21
|
+
@script_hash["execution_position"] || :before_compile,
|
22
|
+
@script_hash["show_env_vars_in_log"]
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def verify_syntax
|
27
|
+
raise "[Error] Invalid script description #{@script_hash}" unless @script_hash.is_a?(Hash)
|
28
|
+
raise "[Error] Script must have a name and an associated script" unless @script_hash.has_key?("name") && @script_hash.has_key?("script") || @script_hash.has_key?("script_path")
|
29
|
+
raise "[Error] Invalid name in script #{@script_hash}" unless !@script_hash["name"].nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
def load_script(path)
|
33
|
+
raise "[Error] File #{path} does not exist" unless !path.nil? && File.exist?(path)
|
34
|
+
File.read(path)
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'set'
|
3
|
+
require_relative 'entity/configuration'
|
4
|
+
require_relative 'entity/script'
|
5
|
+
require_relative 'entity/target'
|
6
|
+
require_relative 'script_mapper'
|
7
|
+
|
8
|
+
class YAMLParser
|
9
|
+
|
10
|
+
# The configuration to use in order to add scripts in your project
|
11
|
+
#
|
12
|
+
attr_reader :configuration
|
13
|
+
|
14
|
+
def initialize(yaml_file_path, logger)
|
15
|
+
@logger = logger
|
16
|
+
@inlined_scripts_ids = Set[]
|
17
|
+
@inlined_scripts_names = Set[]
|
18
|
+
@other_scripts_paths = Set[]
|
19
|
+
@other_scripts_names = Set[]
|
20
|
+
@configuration_description = load_yml_content(yaml_file_path)
|
21
|
+
@configuration = Configuration.new(targets)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def targets
|
27
|
+
if @configuration_description["targets"].nil?
|
28
|
+
@logger.log "[Warning] There is no target in your configuration file."
|
29
|
+
return
|
30
|
+
end
|
31
|
+
@configuration_description["targets"].map do |target_name, script_entries|
|
32
|
+
if script_entries.nil?
|
33
|
+
@logger.log "[Warning] There is no scripts in your configuration file under target #{target_name}"
|
34
|
+
return
|
35
|
+
end
|
36
|
+
scripts = script_entries.map do |entry|
|
37
|
+
get_script(entry)
|
38
|
+
end
|
39
|
+
Target.new(target_name, scripts)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_script(entry)
|
44
|
+
script =
|
45
|
+
if !@configuration_description["scripts"].nil? && !@configuration_description["scripts"][entry].nil?
|
46
|
+
get_script_by_name(entry)
|
47
|
+
elsif File.exist?(entry)
|
48
|
+
get_script_by_path(entry)
|
49
|
+
else
|
50
|
+
raise "[Error] Script #{entry} does not exist" unless !script.nil?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_script_by_name(name)
|
55
|
+
@inlined_scripts_ids.add(name)
|
56
|
+
script_description = @configuration_description["scripts"][name]
|
57
|
+
script = ScriptMapper.new(script_description).map()
|
58
|
+
@inlined_scripts_names.add(script.name)
|
59
|
+
error = "[Error] Multiple scripts with same name \'#{script.name}\'"
|
60
|
+
raise error unless @inlined_scripts_ids.count == @inlined_scripts_names.count
|
61
|
+
raise error unless !@inlined_scripts_names.intersect?(@other_scripts_names)
|
62
|
+
return script
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_script_by_path(path)
|
66
|
+
@other_scripts_paths.add(path)
|
67
|
+
script_description = load_yml_content(path)
|
68
|
+
script = ScriptMapper.new(script_description).map()
|
69
|
+
@other_scripts_names.add(script.name)
|
70
|
+
error = "[Error] Multiple scripts with same name \'#{script.name}\'"
|
71
|
+
raise error unless @other_scripts_paths.count == @other_scripts_names.count
|
72
|
+
raise error unless !@inlined_scripts_names.intersect?(@other_scripts_names)
|
73
|
+
return script
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param [String] configuration_path
|
77
|
+
# @return [Hash] the hash in the configuration file
|
78
|
+
#
|
79
|
+
def load_yml_content(configuration_path)
|
80
|
+
raise "[Error] YAML file #{configuration_path} not found." unless File.exist?(configuration_path)
|
81
|
+
YAML.load(File.read(configuration_path)) || {}
|
82
|
+
end
|
83
|
+
end
|
data/lib/spinjector.rb
CHANGED
@@ -5,12 +5,8 @@
|
|
5
5
|
|
6
6
|
require 'optparse'
|
7
7
|
require 'xcodeproj'
|
8
|
-
|
9
|
-
|
10
|
-
# @return [String] prefix used for all the build phase injected by this script
|
11
|
-
# [SPI] stands for Script Phase Injector
|
12
|
-
#
|
13
|
-
BUILD_PHASE_PREFIX = '[SPI] '.freeze
|
8
|
+
require_relative 'spinjector/runner'
|
9
|
+
require_relative 'spinjector/logger'
|
14
10
|
|
15
11
|
CONFIGURATION_FILE_PATH = 'Configuration/spinjector_configuration.yaml'.freeze
|
16
12
|
|
@@ -18,165 +14,17 @@ options = {}
|
|
18
14
|
OptionParser.new do |opts|
|
19
15
|
opts.banner = "Usage: spinjector [options]"
|
20
16
|
|
21
|
-
|
22
17
|
opts.on("-cName", "--configuration-path=Name", "Inject scripts using configuration file at Name location. Default is ./#{CONFIGURATION_FILE_PATH}") do |config_path|
|
23
18
|
options[:configuration_path] = config_path
|
24
19
|
end
|
25
20
|
end.parse!
|
26
21
|
|
27
|
-
# @param [Xcodeproj::Project] project
|
28
|
-
#
|
29
|
-
def remove_all_spi_script_phases(project)
|
30
|
-
project.targets.each do |target|
|
31
|
-
# Delete script phases no longer present in the target.
|
32
|
-
native_target_script_phases = target.shell_script_build_phases.select do |bp|
|
33
|
-
!bp.name.nil? && bp.name.start_with?(BUILD_PHASE_PREFIX)
|
34
|
-
end
|
35
|
-
native_target_script_phases.each do |script_phase|
|
36
|
-
target.build_phases.delete(script_phase)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
# @param [Xcodeproj::Project] project
|
42
|
-
#
|
43
|
-
def inject_script_phases(project, configuration_file_path)
|
44
|
-
configuration_file = load_yml_content(configuration_file_path)
|
45
|
-
# Check if there are scripts in the global configuration file
|
46
|
-
implicit_scripts = configuration_file["scripts"] || []
|
47
|
-
configuration_file["targets"].each do |target_name, script_paths|
|
48
|
-
script_phases = (script_paths || []).flat_map do |script_path|
|
49
|
-
if !implicit_scripts.empty? and !implicit_scripts[script_path].nil?
|
50
|
-
# Target uses script from the global configuration file
|
51
|
-
script = implicit_scripts[script_path]
|
52
|
-
else
|
53
|
-
# Target uses script from a dedicated configuration file
|
54
|
-
script = load_yml_content(script_path)
|
55
|
-
end
|
56
|
-
raise "[Error] Could not find script #{script_path}" unless !script.nil?
|
57
|
-
script
|
58
|
-
end
|
59
|
-
warn "[Warning] No script phases found for #{target_name} target. You can add them in your configuration file at #{configuration_file_path}" unless !script_phases.empty?
|
60
|
-
target = app_target(project, target_name)
|
61
|
-
create_script_phases(script_phases, target)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
# @param [String] configuration_path
|
66
|
-
# @return [Hash] the hash in the configuration file
|
67
|
-
#
|
68
|
-
def load_yml_content(configuration_path)
|
69
|
-
raise "[Error] YAML file #{configuration_path} not found." unless File.exist?(configuration_path)
|
70
|
-
YAML.load(File.read(configuration_path)) || {}
|
71
|
-
end
|
72
|
-
|
73
|
-
# @param [Xcodeproj::Project] project
|
74
|
-
# @param [String] target_name
|
75
|
-
# @return [Xcodeproj::Project::Object::PBXNativeTarget] the target named by target_name
|
76
|
-
#
|
77
|
-
def app_target(project, target_name)
|
78
|
-
target = project.targets.find { |t| t.name == target_name }
|
79
|
-
raise "[Error] Invalid #{target_name} target." unless !target.nil?
|
80
|
-
return target
|
81
|
-
end
|
82
|
-
|
83
|
-
# @param [Array<Hash>] script_phases the script phases defined in configuration files
|
84
|
-
# @param [Xcodeproj::Project::Object::PBXNativeTarget] target to add the script phases
|
85
|
-
#
|
86
|
-
def create_script_phases(script_phases, target)
|
87
|
-
script_phases.each do |script_phase|
|
88
|
-
name_with_prefix = BUILD_PHASE_PREFIX + script_phase["name"]
|
89
|
-
phase = target.new_shell_script_build_phase(name_with_prefix)
|
90
|
-
shell_script = get_script(script_phase)
|
91
|
-
phase.shell_script = shell_script
|
92
|
-
phase.shell_path = script_phase["shell_path"] || '/bin/sh'
|
93
|
-
phase.input_paths = script_phase["input_paths"]
|
94
|
-
phase.output_paths = script_phase["output_paths"]
|
95
|
-
phase.input_file_list_paths = script_phase["input_file_list_paths"]
|
96
|
-
phase.output_file_list_paths = script_phase["output_file_list_paths"]
|
97
|
-
phase.dependency_file = script_phase["dependency_file"]
|
98
|
-
# At least with Xcode 10 `showEnvVarsInLog` is *NOT* set to any value even if it's checked and it only
|
99
|
-
# gets set to '0' if the user has explicitly disabled this.
|
100
|
-
if (show_env_vars_in_log = script_phase.fetch("show_env_vars_in_log", '1')) == '0'
|
101
|
-
phase.show_env_vars_in_log = show_env_vars_in_log
|
102
|
-
end
|
103
|
-
|
104
|
-
execution_position = script_phase["execution_position"] || :before_compile
|
105
|
-
reorder_script_phase(target, phase, execution_position)
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
# @param [Hash] script_phase the script phase defined in configuration files
|
110
|
-
# @return [String] script to add in script phase
|
111
|
-
#
|
112
|
-
# If script is in a dedicated file, find the URL under <script_path>.
|
113
|
-
# Otherwise, it may be written directly in the configuration file, find it under <script>.
|
114
|
-
#
|
115
|
-
def get_script(script_phase)
|
116
|
-
if !script_phase["script"].nil? and !script_phase["script_path"].nil?
|
117
|
-
raise "[Error] Script phase #{script_phase["name"]} contains 2 script sources. Please use one of :script_path or :script option"
|
118
|
-
end
|
119
|
-
if !script_phase["script_path"].nil?
|
120
|
-
raise "[Error] File #{script_phase["script_path"]} does not exist" unless File.exist?(script_phase["script_path"])
|
121
|
-
File.read(script_phase["script_path"])
|
122
|
-
elsif !script_phase["script"].nil?
|
123
|
-
script_phase["script"]
|
124
|
-
else
|
125
|
-
warn "[Warning] Script phase #{script_phase["name"]} contains no script"
|
126
|
-
return ""
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
# @param [Xcodeproj::Project::Object::PBXNativeTarget] target where build phases should be reordered
|
131
|
-
# @param [Hash] script_phase to reorder
|
132
|
-
# @param [Symbol] execution_position could be :before_compile, :after_compile, :before_headers, :after_headers
|
133
|
-
#
|
134
|
-
def reorder_script_phase(target, script_phase, execution_position)
|
135
|
-
return if execution_position == :any || execution_position.to_s.empty?
|
136
|
-
|
137
|
-
# Find the point P where to add the script phase
|
138
|
-
target_phase_type = case execution_position
|
139
|
-
when :before_compile, :after_compile
|
140
|
-
Xcodeproj::Project::Object::PBXSourcesBuildPhase
|
141
|
-
when :before_headers, :after_headers
|
142
|
-
Xcodeproj::Project::Object::PBXHeadersBuildPhase
|
143
|
-
else
|
144
|
-
raise ArgumentError, "Unknown execution position `#{execution_position}`"
|
145
|
-
end
|
146
|
-
|
147
|
-
# Decide whether to add script_phase before or after point P
|
148
|
-
order_before = case execution_position
|
149
|
-
when :before_compile, :before_headers
|
150
|
-
true
|
151
|
-
when :after_compile, :after_headers
|
152
|
-
false
|
153
|
-
else
|
154
|
-
raise ArgumentError, "Unknown execution position `#{execution_position}`"
|
155
|
-
end
|
156
|
-
|
157
|
-
# Get the first build phase index of P
|
158
|
-
target_phase_index = target.build_phases.index do |bp|
|
159
|
-
bp.is_a?(target_phase_type)
|
160
|
-
end
|
161
|
-
return if target_phase_index.nil?
|
162
|
-
|
163
|
-
# Get the script phase we want to reorder index
|
164
|
-
script_phase_index = target.build_phases.index do |bp|
|
165
|
-
bp.is_a?(Xcodeproj::Project::Object::PBXShellScriptBuildPhase) && !bp.name.nil? && bp.name == script_phase.name
|
166
|
-
end
|
167
|
-
|
168
|
-
# Move script phase to P if needed
|
169
|
-
if (order_before && script_phase_index > target_phase_index) ||
|
170
|
-
(!order_before && script_phase_index < target_phase_index)
|
171
|
-
target.build_phases.move_from(script_phase_index, target_phase_index)
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
22
|
project_path = Dir.glob("*.xcodeproj").first
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
23
|
+
raise "[Error] No xcodeproj found in #{Dir.pwd}" if project_path.nil?
|
24
|
+
|
25
|
+
runner = Runner.new(
|
26
|
+
project_path,
|
27
|
+
options[:configuration_path] || CONFIGURATION_FILE_PATH,
|
28
|
+
VerboseLogger.new
|
29
|
+
)
|
30
|
+
runner.run
|
data/spinjector.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'spinjector'
|
3
|
-
s.version = '0.0
|
3
|
+
s.version = '1.0.0'
|
4
4
|
s.executables << 'spinjector'
|
5
5
|
s.summary = "Inject script phases into your Xcode project"
|
6
6
|
s.description = ""
|
@@ -8,10 +8,16 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.email = 'guillaume.berthier@fabernovel.com'
|
9
9
|
# use `git ls-files -coz -x *.gem` for development
|
10
10
|
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
11
|
-
s.homepage = 'https://github.com/
|
11
|
+
s.homepage = 'https://github.com/faberNovel/spinjector'
|
12
12
|
s.license = 'MIT'
|
13
13
|
|
14
14
|
s.add_dependency 'optparse', '~> 0.1'
|
15
15
|
s.add_dependency 'xcodeproj', '~> 1.21'
|
16
16
|
s.add_dependency 'yaml', '~> 0.2'
|
17
|
-
|
17
|
+
|
18
|
+
s.add_development_dependency 'bundler', '>= 1.12.0', '< 3.0.0'
|
19
|
+
s.add_development_dependency 'rake', '~> 12.3'
|
20
|
+
s.add_development_dependency 'minitest', '~> 5.11'
|
21
|
+
s.add_development_dependency 'minitest-reporters', '~> 1.3'
|
22
|
+
s.add_development_dependency 'minitest-hooks', '~> 1.5'
|
23
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spinjector
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Guillaume Berthier, Fabernovel
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-12-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: optparse
|
@@ -52,6 +52,82 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.12.0
|
62
|
+
- - "<"
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: 3.0.0
|
65
|
+
type: :development
|
66
|
+
prerelease: false
|
67
|
+
version_requirements: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: 1.12.0
|
72
|
+
- - "<"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 3.0.0
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rake
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '12.3'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '12.3'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: minitest
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '5.11'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '5.11'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: minitest-reporters
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '1.3'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '1.3'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: minitest-hooks
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '1.5'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '1.5'
|
55
131
|
description: ''
|
56
132
|
email: guillaume.berthier@fabernovel.com
|
57
133
|
executables:
|
@@ -59,9 +135,9 @@ executables:
|
|
59
135
|
extensions: []
|
60
136
|
extra_rdoc_files: []
|
61
137
|
files:
|
138
|
+
- ".github/workflows/ci.yml"
|
62
139
|
- ".gitignore"
|
63
|
-
-
|
64
|
-
- Examples/Configuration/hello_world_output
|
140
|
+
- CHANGELOG.md
|
65
141
|
- Examples/Configuration/helloworld.yaml
|
66
142
|
- Examples/Configuration/helloworld_explicit_script.yaml
|
67
143
|
- Examples/Configuration/helloworld_short.yaml
|
@@ -69,15 +145,27 @@ files:
|
|
69
145
|
- Examples/Images/build_phases.png
|
70
146
|
- Examples/Images/hello_world_explicit.png
|
71
147
|
- Examples/Scripts/helloworld.sh
|
148
|
+
- Gemfile
|
149
|
+
- Gemfile.lock
|
150
|
+
- Makefile
|
72
151
|
- README.md
|
152
|
+
- Rakefile
|
73
153
|
- bin/spinjector
|
74
154
|
- lib/spinjector.rb
|
155
|
+
- lib/spinjector/entity/configuration.rb
|
156
|
+
- lib/spinjector/entity/script.rb
|
157
|
+
- lib/spinjector/entity/target.rb
|
158
|
+
- lib/spinjector/logger.rb
|
159
|
+
- lib/spinjector/project_service.rb
|
160
|
+
- lib/spinjector/runner.rb
|
161
|
+
- lib/spinjector/script_mapper.rb
|
162
|
+
- lib/spinjector/yaml_parser.rb
|
75
163
|
- spinjector.gemspec
|
76
|
-
homepage: https://github.com/
|
164
|
+
homepage: https://github.com/faberNovel/spinjector
|
77
165
|
licenses:
|
78
166
|
- MIT
|
79
167
|
metadata: {}
|
80
|
-
post_install_message:
|
168
|
+
post_install_message:
|
81
169
|
rdoc_options: []
|
82
170
|
require_paths:
|
83
171
|
- lib
|
@@ -92,8 +180,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
180
|
- !ruby/object:Gem::Version
|
93
181
|
version: '0'
|
94
182
|
requirements: []
|
95
|
-
rubygems_version: 3.
|
96
|
-
signing_key:
|
183
|
+
rubygems_version: 3.3.3
|
184
|
+
signing_key:
|
97
185
|
specification_version: 4
|
98
186
|
summary: Inject script phases into your Xcode project
|
99
187
|
test_files: []
|
File without changes
|
File without changes
|