11#! /usr/bin/env bash
2- # Archive DecoderSec and produce unsigned .ipa file(s) for GitHub Actions / local CI.
2+ # Archive DecoderSec and produce installable .ipa file(s) for GitHub Actions / local CI.
33#
44# Modes:
5- # SIGNING=unsigned — archive with CODE_SIGNING_ALLOWED=NO, pack Payload → .ipa
5+ # SIGNING=unsigned — archive unsigned, then ad-hoc codesign + zip Payload → .ipa
66# SIGNING=manual — use cert/profile already installed in the keychain
77# BOTH=1 (default) — emit full VPN IPA + Lite (no PacketTunnel) from one archive
88# LITE=1 — Lite only (UI / config browsing)
99# LITE=0 BOTH=0 — full VPN IPA only
1010#
11+ # Why we strip EverywhereCore.framework from the IPA:
12+ # Upstream EverywhereCore is a *static* .a wrapped as a .framework. Xcode links
13+ # Evcore* into DecoderSecTunnel (and the app) at build time. Embedding that .a
14+ # as Frameworks/EverywhereCore.framework/EverywhereCore makes iOS refuse install
15+ # ("invalid framework binary"). The tiny Xcode "codeless stub" is also useless
16+ # at runtime when everything is already statically linked. So we delete it.
17+ #
1118# Env:
1219# CONFIGURATION Debug|Release (default Release)
1320# DERIVED_DATA path (default ./DerivedData)
@@ -29,9 +36,8 @@ EXPORT_OPTIONS="${EXPORT_OPTIONS:-$ROOT/Config/ExportOptions-ad-hoc.plist}"
2936LITE=" ${LITE:- 0} "
3037BOTH=" ${BOTH:- 1} "
3138
32- # Unsigned archives often embed a ~33KB Xcode "codeless" stub instead of the
33- # real EverywhereCore binary. Reject anything under this floor.
34- MIN_CORE_BYTES=" ${MIN_CORE_BYTES:- 1000000} "
39+ APP_ENTITLEMENTS=" $ROOT /DecoderSec/DecoderSec.entitlements"
40+ TUNNEL_ENTITLEMENTS=" $ROOT /DecoderSecTunnel/DecoderSecTunnel.entitlements"
3541
3642mkdir -p " $( dirname " $ARCHIVE_PATH " ) " " $IPA_DIR " " $DERIVED_DATA "
3743rm -f " $IPA_DIR " /* .ipa
@@ -97,68 +103,140 @@ if [[ -z "$APP_PATH" ]]; then
97103 exit 1
98104fi
99105
100- find_real_everywhere_core () {
101- # Prefer the SPM binary artifact that Xcode downloaded for this build.
102- local candidates=()
103- while IFS= read -r -d ' ' f; do
104- candidates+=(" $f " )
105- done < <( find " $DERIVED_DATA /SourcePackages" \
106- " $ROOT /DerivedData/SourcePackages" \
107- " $HOME /Library/Developer/Xcode/DerivedData" \
108- -type d -path ' */EverywhereCore.xcframework/ios-arm64/EverywhereCore.framework' \
109- -print0 2> /dev/null || true)
110-
111- local best=" " best_size=0 size
112- for f in " ${candidates[@]+" ${candidates[@]} " } " ; do
113- [[ -f " $f /EverywhereCore" ]] || continue
114- size=$( wc -c < " $f /EverywhereCore" | tr -d ' ' )
115- if (( size > best_size )) ; then
116- best_size=$size
117- best=$f
118- fi
119- done
120- printf ' %s' " $best "
106+ is_macho_dylib () {
107+ local bin=" $1 "
108+ [[ -f " $bin " ]] || return 1
109+ local info
110+ info=" $( file -b " $bin " 2> /dev/null || true) "
111+ # Reject static .a wrapped as "framework" binary — iOS will not install it.
112+ if grep -qi ' ar archive' <<< " $info" ; then
113+ return 1
114+ fi
115+ grep -qiE ' Mach-O|dynamically linked shared library|dylib' <<< " $info"
121116}
122117
123118repair_app_bundle () {
124119 local app=" $1 "
125- local core_src core_dst size
126120
127121 # Drop Xcode source icon bundle — runtime uses Assets.car / PNG icons.
128- # Some Windows unpackers choke on nested .icon packages.
129122 rm -rf " $app /AppIcon.icon"
130123
131- core_dst= " $app /Frameworks/ EverywhereCore.framework "
132- mkdir -p " $app / Frameworks"
133- core_src= " $( find_real_everywhere_core ) "
134- if [[ -z " $core_src " ]]; then
135- echo " error: could not locate EverywhereCore.xcframework ios-arm64 slice under DerivedData " >&2
136- exit 1
124+ # EverywhereCore is a static library. Embedding it (or Xcode's stub) as a
125+ # Frameworks/*.framework binary breaks or confuses installers. Evcore is
126+ # already linked into DecoderSecTunnel; remove the embed.
127+ if [[ -d " $app /Frameworks/EverywhereCore.framework " ]]; then
128+ echo " → removing static/stub EverywhereCore.framework (linked into binaries at build time) "
129+ rm -rf " $app /Frameworks/EverywhereCore.framework "
137130 fi
138- size=$( wc -c < " $core_src /EverywhereCore" | tr -d ' ' )
139- echo " → embedding EverywhereCore from $core_src ($size bytes)"
140- if (( size < MIN_CORE_BYTES )) ; then
141- echo " error: source EverywhereCore is only $size bytes (stub?)" >&2
142- exit 1
131+ # Clean empty Frameworks dir
132+ if [[ -d " $app /Frameworks" ]] && [[ -z " $( ls -A " $app /Frameworks" 2> /dev/null || true) " ]]; then
133+ rmdir " $app /Frameworks" || true
143134 fi
144- rm -rf " $core_dst "
145- ditto " $core_src " " $core_dst "
146- # Strip extended attributes that break Sideloadly / Windows unzip.
135+
136+ # Fix any remaining framework Info.plist MinimumOSVersion=100.0 (gomobile artifact)
137+ while IFS= read -r -d ' ' plist; do
138+ if command -v plutil > /dev/null 2>&1 ; then
139+ local min
140+ min=" $( plutil -extract MinimumOSVersion raw " $plist " 2> /dev/null || true) "
141+ if [[ " $min " == " 100.0" || " $min " == " 100" ]]; then
142+ plutil -replace MinimumOSVersion -string " 15.0" " $plist "
143+ echo " → fixed MinimumOSVersion in $plist "
144+ fi
145+ fi
146+ done < <( find " $app " -name ' Info.plist' -print0)
147+
147148 if command -v xattr > /dev/null 2>&1 ; then
148149 xattr -cr " $app " 2> /dev/null || true
149150 fi
150- size=$( wc -c < " $core_dst /EverywhereCore" | tr -d ' ' )
151- if (( size < MIN_CORE_BYTES )) ; then
152- echo " error: embedded EverywhereCore is only $size bytes after copy" >&2
151+ }
152+
153+ adhoc_sign_bundle () {
154+ local app=" $1 "
155+ if ! command -v codesign > /dev/null 2>&1 ; then
156+ echo " error: codesign missing — cannot produce installable IPA on this host" >&2
157+ exit 1
158+ fi
159+
160+ echo " → ad-hoc codesign (required for Sideloadly / device install)"
161+
162+ # Nested Mach-O first: dylibs, then frameworks, then appex, then app.
163+ while IFS= read -r -d ' ' bin; do
164+ codesign --force --sign - --timestamp=none " $bin " 2> /dev/null || true
165+ done < <( find " $app " -type f \( -name ' *.dylib' -o -name ' *.so' \) -print0 2> /dev/null || true)
166+
167+ while IFS= read -r -d ' ' fw; do
168+ codesign --force --sign - --timestamp=none " $fw "
169+ done < <( find " $app /Frameworks" -maxdepth 1 -type d -name ' *.framework' -print0 2> /dev/null || true)
170+
171+ while IFS= read -r -d ' ' appex; do
172+ if [[ -f " $TUNNEL_ENTITLEMENTS " ]]; then
173+ codesign --force --sign - --timestamp=none --entitlements " $TUNNEL_ENTITLEMENTS " " $appex "
174+ else
175+ codesign --force --sign - --timestamp=none " $appex "
176+ fi
177+ done < <( find " $app /PlugIns" -maxdepth 1 -type d -name ' *.appex' -print0 2> /dev/null || true)
178+
179+ if [[ -f " $APP_ENTITLEMENTS " ]]; then
180+ codesign --force --sign - --timestamp=none --entitlements " $APP_ENTITLEMENTS " " $app "
181+ else
182+ codesign --force --sign - --timestamp=none " $app "
183+ fi
184+
185+ if [[ ! -d " $app /_CodeSignature" ]]; then
186+ echo " error: ad-hoc codesign did not produce _CodeSignature" >&2
153187 exit 1
154188 fi
155- echo " ✓ EverywhereCore embedded ($size bytes)"
189+ # --no-strict: ad-hoc + NE entitlements without a profile still verify structurally.
190+ if ! codesign --verify --no-strict " $app " > /dev/null 2>&1 ; then
191+ echo " warning: codesign --verify reported issues (continuing; Sideloadly will re-sign)" >&2
192+ codesign -dv " $app " 2>&1 | tail -8 || true
193+ fi
194+ echo " ✓ ad-hoc signature present"
195+ }
196+
197+ validate_installable_app () {
198+ local app=" $1 "
199+ local lite=" $2 "
200+
201+ [[ -f " $app /Info.plist" ]] || { echo " error: missing Info.plist" ; return 1; }
202+ [[ -x " $app /DecoderSec" || -f " $app /DecoderSec" ]] || { echo " error: missing DecoderSec binary" ; return 1; }
203+
204+ # Must not ship a static .a as a framework binary.
205+ if [[ -f " $app /Frameworks/EverywhereCore.framework/EverywhereCore" ]]; then
206+ if ! is_macho_dylib " $app /Frameworks/EverywhereCore.framework/EverywhereCore" ; then
207+ echo " error: EverywhereCore.framework binary is not a dylib (static .a breaks install)" >&2
208+ file " $app /Frameworks/EverywhereCore.framework/EverywhereCore" >&2 || true
209+ return 1
210+ fi
211+ fi
212+
213+ if [[ " $lite " != " 1" ]]; then
214+ [[ -d " $app /PlugIns/DecoderSecTunnel.appex" ]] || {
215+ echo " error: full IPA missing DecoderSecTunnel.appex" >&2
216+ return 1
217+ }
218+ [[ -f " $app /PlugIns/DecoderSecTunnel.appex/DecoderSecTunnel" ]] || {
219+ echo " error: missing tunnel binary" >&2
220+ return 1
221+ }
222+ else
223+ if [[ -d " $app /PlugIns" ]]; then
224+ echo " error: lite IPA still has PlugIns/" >&2
225+ return 1
226+ fi
227+ fi
228+
229+ # Ad-hoc signature must exist for install tools.
230+ if [[ ! -d " $app /_CodeSignature" ]]; then
231+ echo " error: app is not codesigned (ad-hoc required for install)" >&2
232+ return 1
233+ fi
156234}
157235
158236pack_ipa () {
159237 local lite=" $1 "
160238 local name=" $2 "
161- local stage app_name app_copy
239+ local stage app_name app_copy out check
162240 stage=" $( mktemp -d) "
163241 mkdir -p " $stage /Payload"
164242 ditto " $APP_PATH " " $stage /Payload/$( basename " $APP_PATH " ) "
@@ -171,46 +249,23 @@ pack_ipa() {
171249 rm -rf " $app_copy /PlugIns"
172250 fi
173251
174- # Sideload-friendly IPA: strip macOS extras (-X). Always keep Payload/ as zip root.
175- local out=" $IPA_DIR /$name "
252+ adhoc_sign_bundle " $app_copy "
253+ validate_installable_app " $app_copy " " $lite "
254+
255+ out=" $IPA_DIR /$name "
176256 rm -f " $out "
177- if [[ ! -f " $app_copy /Info.plist" ]]; then
178- echo " error: staged app missing Info.plist before zip" >&2
179- rm -rf " $stage "
180- exit 1
181- fi
182257 (
183258 cd " $stage "
184- # -X strips uid/gid/xattr; -9 deflates large binaries. Do NOT use ditto here:
185- # ditto -ck without keepParent can omit the Payload/ root and break Sideloadly.
186259 zip -r -X -9 " $out " Payload
187260 )
188261
189- # Validate: must unpack cleanly and contain Payload/*.app/Info.plist
190- local check
191262 check=" $( mktemp -d) "
192263 unzip -q " $out " -d " $check "
193- if [[ ! -f " $check /Payload/$app_name /Info.plist" ]]; then
194- echo " error: packed IPA missing Payload/$app_name /Info.plist" >&2
195- echo " archive top-level:" >&2
196- unzip -l " $out " | head -30 >&2
197- rm -rf " $stage " " $check "
198- exit 1
199- fi
200- local core_size=0
201- if [[ -f " $check /Payload/$app_name /Frameworks/EverywhereCore.framework/EverywhereCore" ]]; then
202- core_size=$( wc -c < " $check /Payload/$app_name /Frameworks/EverywhereCore.framework/EverywhereCore" | tr -d ' ' )
203- fi
204- if (( core_size < MIN_CORE_BYTES )) ; then
205- echo " error: IPA EverywhereCore is $core_size bytes — refusing to ship a stub" >&2
206- rm -rf " $stage " " $check "
207- exit 1
208- fi
209- # zip integrity
264+ validate_installable_app " $check /Payload/$app_name " " $lite "
210265 unzip -t " $out " > /dev/null
211266 rm -rf " $stage " " $check "
212267
213- echo " ✓ unsigned IPA: $out ($( du -h " $out " | awk ' {print $1}' ) , EverywhereCore= ${core_size} bytes )"
268+ echo " ✓ IPA: $out ($( du -h " $out " | awk ' {print $1}' ) )"
214269}
215270
216271if [[ " $IS_BOTH " == " 1" ]]; then
0 commit comments