-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwire_project.rb
More file actions
260 lines (234 loc) · 11 KB
/
Copy pathwire_project.rb
File metadata and controls
260 lines (234 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
#!/usr/bin/env ruby
# Wires the EverywhereCore SwiftPM package, the Runestone editor
# packages, and the zashboard dashboard resource bundle into
# DecoderSec.xcodeproj (iOS). Idempotent — running it twice is safe.
#
# EverywhereCore ships as a prebuilt xcframework on GitHub Releases;
# SwiftPM downloads and verifies it. The app target embeds it; the
# network extension target links and loads from the host app at runtime.
require 'xcodeproj'
PROJECT_PATH = File.expand_path('../DecoderSec.xcodeproj', __dir__)
DASHBOARD_REL_PATH = 'ThirdParty/zashboard'
DASHBOARD_NAME = 'zashboard'
DEPLOYMENT_TARGET = '15.0'
SHARED_FOLDER = 'Shared'
# Override via env to point a fork of EverywhereCore during CI/local builds.
EVERYWHERE_CORE_REPO = ENV.fetch('EVERYWHERE_CORE_REPO', 'https://github.com/NodePassProject/EverywhereCore')
# Pinned to current EverywhereCore release (Xray v1.260327.0 / v26.3.27,
# sing-box v1.13.18, mihomo v1.19.29). upToNextMajor still floats within 2026.*.
EVERYWHERE_CORE_MIN_VERSION = ENV.fetch('EVERYWHERE_CORE_MIN_VERSION', '2026.08.09')
EVERYWHERE_CORE_REQ = { 'kind' => 'upToNextMajorVersion', 'minimumVersion' => EVERYWHERE_CORE_MIN_VERSION }
EVERYWHERE_CORE_PRODUCT = 'EverywhereCore'
RUNESTONE_URL = 'https://github.com/simonbs/Runestone'
RUNESTONE_REQ = { 'kind' => 'upToNextMajorVersion', 'minimumVersion' => '0.5.0' }
TS_LANG_URL = 'https://github.com/simonbs/TreeSitterLanguages'
TS_LANG_REQ = { 'kind' => 'upToNextMajorVersion', 'minimumVersion' => '0.1.10' }
TS_LANG_PRODUCTS = %w[TreeSitterJSONRunestone TreeSitterYAMLRunestone]
project = Xcodeproj::Project.open(PROJECT_PATH)
app_target = project.targets.find { |t| t.name == 'DecoderSec' } or abort 'DecoderSec target missing'
ne_target = project.targets.find { |t| t.name == 'DecoderSecTunnel' } or abort 'DecoderSecTunnel target missing'
# --- Tear down any prior local-xcframework wiring -------------------------
# Self-healing for repos previously wired against Frameworks/EverywhereCore.xcframework.
stale_xcfw = project.files.select { |f| f.path == 'Frameworks/EverywhereCore.xcframework' }
stale_xcfw.each do |ref|
project.targets.each do |t|
t.frameworks_build_phase.files.select { |bf| bf.file_ref == ref }.each do |bf|
t.frameworks_build_phase.files.delete(bf)
end
t.copy_files_build_phases.each do |cp|
cp.files.select { |bf| bf.file_ref == ref }.each do |bf|
cp.files.delete(bf)
end
end
end
ref.remove_from_project
end
# Strip $(PROJECT_DIR)/Frameworks from FRAMEWORK_SEARCH_PATHS now that
# SwiftPM owns the binary — leaving it is harmless but noisy.
stale_search_path = '$(PROJECT_DIR)/Frameworks'
[app_target, ne_target].each do |target|
target.build_configurations.each do |config|
paths = config.build_settings['FRAMEWORK_SEARCH_PATHS']
next unless paths.is_a?(Array) && paths.include?(stale_search_path)
paths.delete(stale_search_path)
if paths == ['$(inherited)'] || paths.empty?
config.build_settings.delete('FRAMEWORK_SEARCH_PATHS')
else
config.build_settings['FRAMEWORK_SEARCH_PATHS'] = paths
end
end
end
# --- SwiftPM helpers ------------------------------------------------------
def ensure_swift_package(project, url, requirement)
pkg = project.root_object.package_references.find do |p|
p.respond_to?(:repositoryURL) && p.repositoryURL == url
end
unless pkg
pkg = project.new(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference)
pkg.repositoryURL = url
project.root_object.package_references << pkg
end
pkg.requirement = requirement
pkg
end
def add_product_dep(target, project, package_ref, product_name)
dep = target.package_product_dependencies.find do |d|
d.product_name == product_name && d.package == package_ref
end
unless dep
dep = project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency)
dep.package = package_ref
dep.product_name = product_name
target.package_product_dependencies << dep
end
dep
end
def link_product(target, project, dep)
phase = target.frameworks_build_phase
return if phase.files.any? { |bf| bf.product_ref == dep }
bf = project.new(Xcodeproj::Project::Object::PBXBuildFile)
bf.product_ref = dep
phase.files << bf
end
# Inverse of ensure_swift_package + add_product_dep + link_product: strip a
# package's Frameworks build-file entries and product dependencies from every
# target, then remove the dependency and package objects themselves. Objects
# are matched by ISA rather than list membership, so it also sweeps refs left
# orphaned by an earlier partial run; deleting a list entry alone leaves the
# object in the pbxproj, so remove_from_project is explicit (cf. the stale
# xcframework teardown above). A no-op when absent, so re-running self-heals.
def remove_swift_package(project, url)
pkg = project.objects.find do |o|
o.isa == 'XCRemoteSwiftPackageReference' && o.repositoryURL == url
end
return unless pkg
project.objects.select do |o|
o.isa == 'XCSwiftPackageProductDependency' && o.package == pkg
end.each do |dep|
project.targets.each do |target|
target.frameworks_build_phase.files.select { |bf| bf.product_ref == dep }.each do |bf|
target.frameworks_build_phase.files.delete(bf)
end
target.package_product_dependencies.delete(dep)
end
dep.remove_from_project
end
project.root_object.package_references.delete(pkg)
pkg.remove_from_project
end
# --- EverywhereCore (both targets) ---------------------------------------
core_pkg = ensure_swift_package(project, EVERYWHERE_CORE_REPO, EVERYWHERE_CORE_REQ)
core_app_dep = add_product_dep(app_target, project, core_pkg, EVERYWHERE_CORE_PRODUCT)
core_ne_dep = add_product_dep(ne_target, project, core_pkg, EVERYWHERE_CORE_PRODUCT)
link_product(app_target, project, core_app_dep)
link_product(ne_target, project, core_ne_dep)
# No manual Embed Frameworks entry: Xcode auto-embeds the framework slice
# from a SwiftPM binary target into the app bundle when the product is in
# the Frameworks build phase. Adding it to a Copy Files phase by productRef
# double-resolves and fails with "No such file or directory" on the bare
# product name. The network extension links the same product but is not
# embedded — it loads from the host app's Frameworks/ at runtime.
stale_embed = app_target.copy_files_build_phases.find do |p|
p.symbol_dst_subfolder_spec == :frameworks
end
if stale_embed
stale_embed.files.select { |bf|
bf.product_ref&.product_name == EVERYWHERE_CORE_PRODUCT
}.each { |bf| stale_embed.files.delete(bf) }
if stale_embed.files.empty?
app_target.build_phases.delete(stale_embed)
stale_embed.remove_from_project
end
end
# --- Runestone + TreeSitterLanguages (app target only) -------------------
runestone_pkg = ensure_swift_package(project, RUNESTONE_URL, RUNESTONE_REQ)
link_product(app_target, project, add_product_dep(app_target, project, runestone_pkg, 'Runestone'))
ts_lang_pkg = ensure_swift_package(project, TS_LANG_URL, TS_LANG_REQ)
TS_LANG_PRODUCTS.each do |product|
link_product(app_target, project, add_product_dep(app_target, project, ts_lang_pkg, product))
end
# YAML (Argsment/YAML) was wired here but no source imports it — mihomo
# config handling is string-based, not a real YAML parse. Tear down any
# lingering wiring from projects generated before it was retired.
remove_swift_package(project, 'https://github.com/Argsment/YAML')
# --- libresolv.tbd (Go runtime's DNS resolver needs it) ------------------
def link_system_lib(target, project, name, sdk_path)
return if target.frameworks_build_phase.files.any? do |bf|
bf.file_ref&.path == sdk_path
end
ref = project.frameworks_group.files.find { |f| f.path == sdk_path }
unless ref
ref = project.frameworks_group.new_file(sdk_path)
ref.source_tree = 'SDKROOT'
ref.name = name
ref.last_known_file_type = 'sourcecode.text-based-dylib-definition'
end
target.frameworks_build_phase.add_file_reference(ref)
end
link_system_lib(ne_target, project, 'libresolv.tbd', 'usr/lib/libresolv.tbd')
link_system_lib(app_target, project, 'libresolv.tbd', 'usr/lib/libresolv.tbd')
# --- IPHONEOS_DEPLOYMENT_TARGET (project + every target) -----------------
project.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = DEPLOYMENT_TARGET
end
[app_target, ne_target].each do |target|
target.build_configurations.each do |config|
if config.build_settings.key?('IPHONEOS_DEPLOYMENT_TARGET')
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = DEPLOYMENT_TARGET
end
end
end
# --- zashboard folder reference (bundled into the app target) -----------
# `lastKnownFileType = folder` is the magic that makes Xcode treat this
# as a "blue folder" — it copies the whole tree into the .app preserving
# relative paths, which the dashboard's index.html requires
# (./assets/index-*.js). Self-healing cleanup catches the legacy
# yacd-gh-pages reference and any moved zashboard ref.
project.files.select { |f|
next false unless f.path
basename = File.basename(f.path)
(basename == 'yacd-gh-pages' || basename == DASHBOARD_NAME) &&
f.path != DASHBOARD_REL_PATH
}.each do |stale|
project.targets.each do |t|
t.resources_build_phase.files.select { |bf| bf.file_ref == stale }.each do |bf|
t.resources_build_phase.files.delete(bf)
end
end
stale.remove_from_project
end
dashboard_ref = project.main_group.files.find { |f| f.path == DASHBOARD_REL_PATH }
unless dashboard_ref
dashboard_ref = project.new(Xcodeproj::Project::Object::PBXFileReference)
dashboard_ref.path = DASHBOARD_REL_PATH
dashboard_ref.name = DASHBOARD_NAME
dashboard_ref.source_tree = 'SOURCE_ROOT'
dashboard_ref.last_known_file_type = 'folder'
project.main_group << dashboard_ref
end
unless app_target.resources_build_phase.files.any? { |bf| bf.file_ref == dashboard_ref }
app_target.resources_build_phase.add_file_reference(dashboard_ref)
end
# --- Shared/ synced root group (both targets compile its sources) --------
# Sources that both the host app and the Network Extension need —
# Core Data stack, AppGroup helper, ConfigNormalizer, etc. — live in
# Shared/. The folder is registered as a PBXFileSystemSynchronizedRootGroup
# on both targets so every .swift inside auto-joins each target's
# Sources build phase. This is the canonical Xcode-16 way to share
# sources across targets without per-file membership exceptions.
shared_group = project.main_group.children.find do |c|
c.is_a?(Xcodeproj::Project::Object::PBXFileSystemSynchronizedRootGroup) && c.path == SHARED_FOLDER
end
unless shared_group
shared_group = project.new(Xcodeproj::Project::Object::PBXFileSystemSynchronizedRootGroup)
shared_group.path = SHARED_FOLDER
shared_group.source_tree = '<group>'
project.main_group << shared_group
end
[app_target, ne_target].each do |target|
target.file_system_synchronized_groups ||= []
next if target.file_system_synchronized_groups.include?(shared_group)
target.file_system_synchronized_groups << shared_group
end
project.save
puts "Wired EverywhereCore (>= #{EVERYWHERE_CORE_MIN_VERSION}, SwiftPM) + Runestone + zashboard into #{PROJECT_PATH}"