WinterGram/scripts/build-wintergram.sh

451 lines
13 KiB
Bash
Executable file

#!/bin/bash
# WinterGram build wrapper.
set -euo pipefail
cd "$(dirname "$0")/.."
REPO="$(pwd)"
OUT_DIR="build"
SIDELOAD_NAME="WinterGram.ipa"
LC_NAME="WinterGram-LiveContainer.ipa"
SIM_NAME="WinterGram-Simulator.ipa"
WNT_BUNDLE_ID="dev.reekeer.wintergram"
BAZEL="./build-input/bazel-8.4.2-darwin-arm64"
DEVICE_SRC="bazel-bin/Telegram/Telegram.ipa"
_XCODE_DEV_DIR="${DEVELOPER_DIR:-$(xcode-select -p 2>/dev/null)}"
BAZEL_XCODE_ACTION_ENV="--action_env=DEVELOPER_DIR=${_XCODE_DEV_DIR} --host_action_env=DEVELOPER_DIR=${_XCODE_DEV_DIR}"
# Xcode 27 compatibility
BAZEL_SDK_COMPAT_ARGS="--features=-treat_warnings_as_errors --@build_bazel_rules_swift//swift:copt=-no-warnings-as-errors --copt=-Wno-deprecated-declarations"
MODE="all"
INSTALL_REQUESTED=0
INSTALL_DEVICE=0
INSTALL_SIM=0
RUN_APP=0
CLEAN=0
OPEN_BUILD_DIR=0
DEVICE_SELECTOR=""
usage() {
cat <<EOF
WinterGram build wrapper
Usage:
$0 [mode] [options]
Modes:
all Build all deliverables. Default.
sideload, device,
ios Build device sideload IPA -> build/$SIDELOAD_NAME
livecontainer, lc Build unsigned LiveContainer IPA -> build/$LC_NAME
sim, simulator Build simulator IPA -> build/$SIM_NAME
Options:
--install Install after build. With ios/device/sideload mode, installs on a connected
iPhone/iPad via xcrun devicectl. With sim mode, installs into the active
booted Simulator. With no explicit mode, keeps the old simulator shortcut.
--device <id|name> Device selector for devicectl. Optional if exactly one iPhone/iPad is connected.
--run Launch the app after --install (implies --install).
--clean Remove ./build before building.
--open-build-dir Open ./build in Finder after build.
-h, --help Show this help.
Examples:
$0 ios --install
$0 ios --install --run
$0 ios --install --device "Del's iPhone"
$0 --install
$0 --install --run
$0 sideload --clean
EOF
}
die() {
echo "ERROR: $*" >&2
exit 1
}
ipa_size() {
du -h "$1" | cut -f1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "missing command: $1"
}
# --- args ------------------------------------------------------------------
MODE_WAS_EXPLICIT=0
while [ "$#" -gt 0 ]; do
case "$1" in
all|sideload|device|ios|livecontainer|lc|sim|simulator)
MODE="$1"
MODE_WAS_EXPLICIT=1
;;
--install)
INSTALL_REQUESTED=1
;;
--run)
RUN_APP=1
INSTALL_REQUESTED=1
;;
--device)
shift
[ "$#" -gt 0 ] || die "--device requires a value"
DEVICE_SELECTOR="$1"
;;
--clean)
CLEAN=1
;;
--open-build-dir)
OPEN_BUILD_DIR=1
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown argument: $1"
;;
esac
shift
done
if [ "$INSTALL_REQUESTED" -eq 1 ]; then
case "$MODE" in
sim|simulator)
INSTALL_SIM=1
;;
sideload|device|ios)
INSTALL_DEVICE=1
;;
all)
if [ "$MODE_WAS_EXPLICIT" -eq 1 ]; then
die "--install with 'all' is ambiguous. Use 'ios --install' for a device or 'sim --install' for Simulator."
fi
MODE="sim"
INSTALL_SIM=1
;;
livecontainer|lc)
die "--install does not support LiveContainer IPA. Use 'ios --install' for direct device install."
;;
esac
fi
if [ "$CLEAN" -eq 1 ]; then
echo "==> Cleaning ./$OUT_DIR ..."
rm -rf "$OUT_DIR"
fi
mkdir -p "$OUT_DIR"
# --- simulator -------------------------------------------------------------
build_sim() {
echo "==> [Simulator] build (debug_sim_arm64) ..."
python3 build-system/Make/Make.py --overrideXcodeVersion \
--cacheDir "$HOME/telegram-bazel-cache" \
build \
--configurationPath build-system/wintergram-development-configuration.json \
--codesigningInformationPath build-system/fake-codesigning \
--disableProvisioningProfiles \
--disableExtensions \
--bazelArguments="$BAZEL_XCODE_ACTION_ENV $BAZEL_SDK_COMPAT_ARGS" \
--buildNumber=1 --configuration=debug_sim_arm64
[ -f "$DEVICE_SRC" ] || die "simulator artifact not found at $DEVICE_SRC"
cp -f "$DEVICE_SRC" "$OUT_DIR/$SIM_NAME"
echo "==> [Simulator] done: $OUT_DIR/$SIM_NAME ($(ipa_size "$OUT_DIR/$SIM_NAME"))"
}
ensure_booted_simulator() {
require_cmd xcrun
if ! xcrun simctl list devices booted | grep -q "(Booted)"; then
die "no active booted Simulator found. Open Simulator.app and boot a device first."
fi
}
install_sim() {
ensure_booted_simulator
local IPA="$OUT_DIR/$SIM_NAME"
[ -f "$IPA" ] || die "simulator IPA not found at $IPA"
echo "==> [Simulator] installing into active booted Simulator ..."
local TMP_DIR
TMP_DIR="$(mktemp -d)"
unzip -q "$IPA" -d "$TMP_DIR"
local APP_PATH
APP_PATH="$(find "$TMP_DIR/Payload" -maxdepth 1 -type d -name "*.app" | head -n 1)"
[ -n "${APP_PATH:-}" ] || {
rm -rf "$TMP_DIR"
die "no .app found inside $IPA"
}
xcrun simctl install booted "$APP_PATH"
local INSTALLED_BUNDLE_ID
INSTALLED_BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print CFBundleIdentifier' "$APP_PATH/Info.plist" 2>/dev/null || true)"
echo "==> [Simulator] installed: $(basename "$APP_PATH")"
if [ "$RUN_APP" -eq 1 ]; then
[ -n "$INSTALLED_BUNDLE_ID" ] || {
rm -rf "$TMP_DIR"
die "could not read CFBundleIdentifier from app Info.plist"
}
echo "==> [Simulator] launching $INSTALLED_BUNDLE_ID ..."
xcrun simctl launch booted "$INSTALLED_BUNDLE_ID" || true
fi
rm -rf "$TMP_DIR"
}
select_ios_device() {
require_cmd xcrun
if [ -n "$DEVICE_SELECTOR" ]; then
echo "$DEVICE_SELECTOR"
return 0
fi
local JSON_PATH
JSON_PATH="$(mktemp)"
if ! xcrun devicectl list devices --timeout 20 --json-output "$JSON_PATH" >/dev/null; then
rm -f "$JSON_PATH"
die "could not list connected devices via xcrun devicectl. Unlock the phone, trust this Mac, and try again."
fi
local SELECTED
if ! SELECTED="$(python3 - "$JSON_PATH" 2>&1 <<'PY'
import json
import sys
path = sys.argv[1]
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
devices = data.get("result", {}).get("devices", [])
def get(obj, dotted, default=""):
current = obj
for part in dotted.split("."):
if not isinstance(current, dict):
return default
current = current.get(part)
return current if current is not None else default
candidates = []
for device in devices:
platform = str(get(device, "hardwareProperties.platform")).lower()
if platform not in ("ios", "ipados"):
continue
identifier = (
device.get("identifier")
or get(device, "hardwareProperties.udid")
or get(device, "hardwareProperties.serialNumber")
)
if not identifier:
continue
name = (
get(device, "deviceProperties.name")
or device.get("name")
or get(device, "hardwareProperties.marketingName")
or identifier
)
transport = get(device, "connectionProperties.transportType")
pair_state = get(device, "connectionProperties.pairingState")
candidates.append((str(identifier), str(name), str(transport), str(pair_state)))
if len(candidates) == 1:
print(candidates[0][0])
sys.exit(0)
if not candidates:
print("no connected iPhone/iPad found by devicectl", file=sys.stderr)
else:
print("multiple iPhone/iPad devices found; pass --device with one of these:", file=sys.stderr)
for identifier, name, transport, pair_state in candidates:
details = ", ".join(part for part in (transport, pair_state) if part)
suffix = f" ({details})" if details else ""
print(f" {identifier} {name}{suffix}", file=sys.stderr)
sys.exit(2)
PY
)"; then
rm -f "$JSON_PATH"
die "$SELECTED"
fi
rm -f "$JSON_PATH"
echo "$SELECTED"
}
install_device() {
require_cmd xcrun
local IPA="$OUT_DIR/$SIDELOAD_NAME"
[ -f "$IPA" ] || die "device IPA not found at $IPA"
echo "==> [Device] preparing app for install ..."
local TMP_DIR
TMP_DIR="$(mktemp -d)"
unzip -q "$IPA" -d "$TMP_DIR"
local APP_PATH
APP_PATH="$(find "$TMP_DIR/Payload" -maxdepth 1 -type d -name "*.app" | head -n 1)"
[ -n "${APP_PATH:-}" ] || {
rm -rf "$TMP_DIR"
die "no .app found inside $IPA"
}
local INSTALLED_BUNDLE_ID
INSTALLED_BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print CFBundleIdentifier' "$APP_PATH/Info.plist" 2>/dev/null || true)"
local DEVICE
DEVICE="$(select_ios_device)"
echo "==> [Device] installing $(basename "$APP_PATH") on $DEVICE ..."
xcrun devicectl device install app --device "$DEVICE" "$APP_PATH"
echo "==> [Device] installed: $(basename "$APP_PATH")"
if [ "$RUN_APP" -eq 1 ]; then
[ -n "$INSTALLED_BUNDLE_ID" ] || {
rm -rf "$TMP_DIR"
die "could not read CFBundleIdentifier from app Info.plist"
}
echo "==> [Device] launching $INSTALLED_BUNDLE_ID on $DEVICE ..."
xcrun devicectl device process launch --device "$DEVICE" --terminate-existing "$INSTALLED_BUNDLE_ID" || true
fi
rm -rf "$TMP_DIR"
}
# --- device build shared by sideload + livecontainer -----------------------
ensure_cert() {
# Device builds need a codesigning identity in the keychain, even a throwaway one.
if ! security find-certificate -c "Apple Distribution: Telegram FZ-LLC (C67CF9S4VU)" >/dev/null 2>&1; then
echo "==> Importing throwaway signing cert into login keychain ..."
security import build-system/fake-codesigning/certs/SelfSigned.p12 -P "" -A >/dev/null 2>&1 || true
fi
}
build_device() {
ensure_cert
echo "==> [Device] generating fake provisioning profiles for ${WNT_BUNDLE_ID} ..."
python3 scripts/generate-fake-profiles.py build-system/fake-codesigning-generated
echo "==> [Device] build (debug_arm64, ${WNT_BUNDLE_ID}, extensions disabled) ..."
python3 build-system/Make/Make.py --overrideXcodeVersion \
--cacheDir "$HOME/telegram-bazel-cache" \
build \
--configurationPath build-system/wintergram-development-configuration.json \
--codesigningInformationPath build-system/fake-codesigning-generated \
--disableExtensions \
--bazelArguments="$BAZEL_XCODE_ACTION_ENV $BAZEL_SDK_COMPAT_ARGS" \
--buildNumber=1 --configuration=debug_arm64
[ -f "$DEVICE_SRC" ] || die "device artifact not found at $DEVICE_SRC"
}
make_sideload() {
cp -f "$DEVICE_SRC" "$OUT_DIR/$SIDELOAD_NAME"
echo "==> [Sideload] done: $OUT_DIR/$SIDELOAD_NAME ($(ipa_size "$OUT_DIR/$SIDELOAD_NAME"))"
}
make_livecontainer() {
echo "==> [LiveContainer] repackaging unsigned ..."
local TMP_DIR
TMP_DIR="$(mktemp -d)"
unzip -q "$DEVICE_SRC" -d "$TMP_DIR"
find "$TMP_DIR/Payload/Telegram.app" -type f \( -name Telegram -o -name "*.dylib" \) -print0 2>/dev/null \
| xargs -0 codesign --remove-signature 2>/dev/null || true
find "$TMP_DIR/Payload/Telegram.app" -type d -name "*.framework" -print0 2>/dev/null | while IFS= read -r -d '' fw; do
local bin
bin="$fw/$(basename "$fw" .framework)"
[ -f "$bin" ] && codesign --remove-signature "$bin" 2>/dev/null || true
done
find "$TMP_DIR" -type d \( -name _CodeSignature -o -name SC_Info \) -exec rm -rf {} + 2>/dev/null || true
find "$TMP_DIR/Payload/Telegram.app" -name "embedded.mobileprovision" -delete 2>/dev/null || true
rm -f "$OUT_DIR/$LC_NAME"
local ZIP_ITEMS="Payload"
[ -d "$TMP_DIR/SwiftSupport" ] && ZIP_ITEMS="$ZIP_ITEMS SwiftSupport"
(
cd "$TMP_DIR"
zip -qr "$REPO/$OUT_DIR/$LC_NAME" $ZIP_ITEMS
)
rm -rf "$TMP_DIR"
echo "==> [LiveContainer] done: $OUT_DIR/$LC_NAME ($(ipa_size "$OUT_DIR/$LC_NAME"))"
}
# --- main ------------------------------------------------------------------
case "$MODE" in
sim|simulator)
build_sim
;;
sideload|device|ios)
build_device
make_sideload
;;
livecontainer|lc)
build_device
make_livecontainer
;;
all)
# Derive device deliverables before the simulator build overwrites the artifact.
build_device
make_sideload
make_livecontainer
build_sim
;;
*)
die "unknown mode: $MODE"
;;
esac
if [ "$INSTALL_SIM" -eq 1 ]; then
install_sim
fi
if [ "$INSTALL_DEVICE" -eq 1 ]; then
install_device
fi
echo
echo "==> WinterGram deliverables in ./$OUT_DIR/:"
ls -1 "$OUT_DIR"/WinterGram*.ipa 2>/dev/null | sed 's#^# #' || true
if [ "$OPEN_BUILD_DIR" -eq 1 ]; then
open "$OUT_DIR"
fi