Add v2 redesign: config.yml, lib.sh, build.sh, and GitHub Actions workflow

Co-authored-by: Balackburn <93828569+Balackburn@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-02-12 19:16:08 +00:00
parent cbe0251fc3
commit 6eeac05d8c
5 changed files with 842 additions and 31 deletions

104
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,104 @@
# build.yml — GitHub Actions workflow for YTLitePlus v2
#
# Shares all core logic with build.sh via lib.sh.
# config.yml is the single source of truth for tweaks.
name: Build YTLitePlus (v2)
on:
workflow_dispatch:
inputs:
ipa_url:
description: "Direct URL to decrypted YouTube IPA (falls back to IPA_URL secret)"
required: false
type: string
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build patched IPA
runs-on: macos-latest
permissions:
contents: write
steps:
# ── Checkout ──────────────────────────────────────────────
- name: Checkout repository
uses: actions/checkout@v4
# ── Dependencies ──────────────────────────────────────────
- name: Install system dependencies
run: |
brew install jq
pip3 install pyyaml
- name: Install optool
run: |
# Build optool from source for dylib injection
git clone --depth=1 https://github.com/alexzielenski/optool.git /tmp/optool
cd /tmp/optool
git submodule update --init --recursive
xcodebuild -project optool.xcodeproj -scheme optool \
-configuration Release SYMROOT=/tmp/optool/build \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
sudo cp /tmp/optool/build/Release/optool /usr/local/bin/optool
# ── Resolve IPA URL ───────────────────────────────────────
- name: Resolve IPA URL
id: ipa
run: |
URL="${{ inputs.ipa_url }}"
if [ -z "$URL" ]; then
URL="${{ secrets.IPA_URL }}"
fi
if [ -z "$URL" ]; then
echo "::error::No IPA URL provided. Pass it as a workflow input or set the IPA_URL repository secret."
exit 1
fi
# Mask the URL in logs
echo "::add-mask::${URL}"
echo "url=${URL}" >> "$GITHUB_OUTPUT"
# ── Build ─────────────────────────────────────────────────
- name: Run build pipeline
run: |
chmod +x build.sh lib.sh
./build.sh "${{ steps.ipa.outputs.url }}"
# ── Release ───────────────────────────────────────────────
- name: Get version info
id: version
run: |
# Extract YouTube version from the patched IPA for the release tag
VERSION_TAG="v2-$(date +'%Y%m%d-%H%M%S')"
echo "tag=${VERSION_TAG}" >> "$GITHUB_OUTPUT"
SHA256=$(shasum -a 256 YouTube-patched.ipa | cut -d' ' -f1 || sha256sum YouTube-patched.ipa | cut -d' ' -f1)
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag }}
name: "YTLitePlus ${{ steps.version.outputs.tag }}"
body: |
## YTLitePlus — Automated Build
**SHA256:** `${{ steps.version.outputs.sha256 }}`
Built from commit ${{ github.sha }}.
files: YouTube-patched.ipa
draft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ── Summary ───────────────────────────────────────────────
- name: Job Summary
run: |
echo '### 📺 YTLitePlus Build Complete' >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo "**SHA256:** \`${{ steps.version.outputs.sha256 }}\`" >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo 'A draft release has been created. Review and publish it from the [Releases page](../../releases).' >> $GITHUB_STEP_SUMMARY

3
.gitignore vendored
View file

@ -8,3 +8,6 @@ Resources/
*.ipa
.vscode
_codeql_detected_source_root
# v2 build system work directory
_work/

71
build.sh Normal file → Executable file
View file

@ -1,37 +1,46 @@
#!/bin/bash
# To build, either place the IPA file in the project's root directory, or get the path to the IPA, then run `./build.sh`
#!/usr/bin/env bash
# build.sh — Local build script for YTLitePlus
#
# Usage:
# ./build.sh /path/to/YouTube.ipa # explicit IPA path
# ./build.sh https://example.com/YT.ipa # download from URL
# ./build.sh # auto-detect IPA in current dir
#
# Reads config.yml for tweak list and customization settings.
# Shares all core logic with the CI workflow via lib.sh.
read -p $'\e[34m==> \e[1;39mPath to the decrypted YouTube.ipa or YouTube.app. If nothing is provied, any ipa/app in the project\'s root directory will be used: ' PATHTOYT
set -euo pipefail
# Check if PATHTOYT is empty
if [ -z "$PATHTOYT" ]; then
# Look for ipa/app files in the current directory
IPAS=$(find . -maxdepth 1 -type f \( -name "*.ipa" -o -name "*.app" \))
# Check if there are two or more ipa/app files
COUNT=$(echo "$IPAS" | wc -l)
if [ "$COUNT" -ge 2 ]; then
echo "❌ Error: Multiple IPA/app files found in the project's root directory directory. Make sure there is only one ipa."
exit 1
elif [ -n "$IPAS" ]; then
PATHTOYT=$(echo "$IPAS" | head -n 1)
else
echo "❌ Error: No IPA/app file found in the project's root directory directory."
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "${SCRIPT_DIR}/lib.sh"
###############################################################################
# Resolve the IPA source
###############################################################################
IPA_SOURCE="${1:-}"
if [ -z "$IPA_SOURCE" ]; then
# Auto-detect: look for a single .ipa in the project root
mapfile -t ipa_files < <(find "$SCRIPT_DIR" -maxdepth 1 -name "*.ipa" -type f)
if [ ${#ipa_files[@]} -eq 0 ]; then
die "No IPA file provided and none found in ${SCRIPT_DIR}.\nUsage: ./build.sh /path/to/YouTube.ipa"
elif [ ${#ipa_files[@]} -gt 1 ]; then
die "Multiple IPA files found in ${SCRIPT_DIR}. Provide the path explicitly.\nUsage: ./build.sh /path/to/YouTube.ipa"
fi
IPA_SOURCE="${ipa_files[0]}"
log_info "Auto-detected IPA: ${IPA_SOURCE}"
fi
make package THEOS_PACKAGE_SCHEME=rootless IPA="$PATHTOYT" FINALPACKAGE=1
###############################################################################
# Run the pipeline
###############################################################################
OUTPUT_IPA="${SCRIPT_DIR}/YouTube-patched.ipa"
# SHASUM
if [[ $? -eq 0 ]]; then
open packages
echo "SHASUM256: $(shasum -a 256 packages/*.ipa)"
run_full_pipeline "$IPA_SOURCE" "$OUTPUT_IPA"
else
echo "Failed building YTLitePlus"
fi
# Cleanup intermediate work directory
cleanup_workspace

89
config.yml Normal file
View file

@ -0,0 +1,89 @@
# config.yml — Single source of truth for YTLitePlus
#
# To add a new tweak:
# 1. Add a new entry under "tweaks:" with id, repo, fetch method, and enabled flag.
# 2. For release-based tweaks: set fetch: release
# 3. For source-build tweaks: set fetch: build, and optionally branch/build_cmd
# 4. Set always: true if the tweak must always be injected (e.g. the primary tweak)
# 5. That's it — no other files need to change.
tweaks:
- id: ytlite
enabled: true
repo: dayanch96/YTLite
fetch: release
always: true
- id: youpip
enabled: true
repo: PoomSmart/YouPiP
fetch: release
- id: ytuhd
enabled: true
repo: splaser/YTUHD
fetch: release
- id: ytabconfig
enabled: true
repo: PoomSmart/YTABConfig
fetch: release
- id: return_yt_dislikes
enabled: true
repo: PoomSmart/Return-YouTube-Dislikes
fetch: release
- id: dont_eat_my_content
enabled: true
repo: therealFoxster/DontEatMyContent
fetch: release
- id: ytvideooverlay
enabled: true
repo: PoomSmart/YTVideoOverlay
fetch: release
- id: yougroupsettings
enabled: true
repo: PoomSmart/YouGroupSettings
fetch: release
- id: alderis
enabled: true
repo: hbang/Alderis
fetch: release
- id: flexing
enabled: false
repo: PoomSmart/FLEXing
fetch: build
branch: rootless
build_cmd: make package THEOS_PACKAGE_SCHEME=rootless
- id: youtimestamp
enabled: false
repo: aricloverALT/YouTimeStamp
fetch: build
build_cmd: make package
- id: ytheaders
enabled: false
repo: therealFoxster/YTHeaders
fetch: build
build_cmd: make package
- id: open_youtube_safari
enabled: false
repo: BillyCurtis/OpenYouTubeSafariExtension
fetch: build
build_cmd: make package
customization:
bundle_id: com.custom.youtube
display_name: "YouTube+"
icon: assets/icon.png
min_ios: "16.0"
strip_watch_extension: true
strip_plugins: true
strip_extensions: true

606
lib.sh Executable file
View file

@ -0,0 +1,606 @@
#!/usr/bin/env bash
# lib.sh — Shared core logic for YTLitePlus build system
# Used by both build.sh (local) and the GitHub Actions workflow.
# All tweak fetching, IPA patching, and customization logic lives here.
set -euo pipefail
###############################################################################
# Globals
###############################################################################
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/config.yml"
WORK_DIR="${SCRIPT_DIR}/_work"
TWEAKS_DIR="${WORK_DIR}/tweaks"
DYLIBS_DIR="${WORK_DIR}/dylibs"
FRAMEWORKS_DIR="${WORK_DIR}/frameworks"
PAYLOAD_DIR="${WORK_DIR}/payload"
###############################################################################
# Colour helpers (no-op when stdout is not a terminal)
###############################################################################
if [ -t 1 ]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; BLUE='\033[0;34m'
YELLOW='\033[1;33m'; BOLD='\033[1m'; NC='\033[0m'
else
RED=''; GREEN=''; BLUE=''; YELLOW=''; BOLD=''; NC=''
fi
log_info() { echo -e "${BLUE}==> ${BOLD}$*${NC}"; }
log_ok() { echo -e "${GREEN}$*${NC}"; }
log_warn() { echo -e "${YELLOW}$*${NC}"; }
log_error() { echo -e "${RED}$*${NC}"; }
die() { log_error "$@"; exit 1; }
###############################################################################
# YAML parsing via Python (works on macOS + Linux without extra deps)
###############################################################################
# Returns the number of enabled tweaks
config_tweak_count() {
python3 -c "
import yaml, sys
with open('${CONFIG_FILE}') as f:
cfg = yaml.safe_load(f)
print(sum(1 for t in cfg.get('tweaks', []) if t.get('enabled', False)))
"
}
# Returns a JSON array of enabled tweaks (consumed by bash via jq)
config_enabled_tweaks_json() {
python3 -c "
import yaml, json, sys
with open('${CONFIG_FILE}') as f:
cfg = yaml.safe_load(f)
enabled = [t for t in cfg.get('tweaks', []) if t.get('enabled', False)]
print(json.dumps(enabled))
"
}
# Returns a single customization value by key
config_custom() {
local key="$1"
python3 -c "
import yaml, sys
with open('${CONFIG_FILE}') as f:
cfg = yaml.safe_load(f)
val = cfg.get('customization', {}).get('${key}', '')
print(val if val is not None else '')
"
}
###############################################################################
# Dependency checks
###############################################################################
check_deps() {
local missing=()
for cmd in curl jq python3 unzip zip; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
# We need either insert_dylib or optool for dylib injection
if ! command -v insert_dylib &>/dev/null && ! command -v optool &>/dev/null; then
missing+=("insert_dylib or optool")
fi
# Check if any tweak needs building (fetch: build)
local needs_build
needs_build=$(python3 -c "
import yaml
with open('${CONFIG_FILE}') as f:
cfg = yaml.safe_load(f)
print('yes' if any(t.get('fetch') == 'build' and t.get('enabled', False) for t in cfg.get('tweaks', [])) else 'no')
")
if [ "$needs_build" = "yes" ]; then
command -v make &>/dev/null || missing+=("make")
if [ -z "${THEOS:-}" ]; then
log_warn "THEOS env var not set — source-build tweaks may fail"
fi
fi
# python3 needs pyyaml
python3 -c "import yaml" 2>/dev/null || missing+=("python3-yaml (pip install pyyaml)")
if [ ${#missing[@]} -gt 0 ]; then
die "Missing dependencies: ${missing[*]}"
fi
log_ok "All dependencies satisfied"
}
###############################################################################
# Workspace setup / teardown
###############################################################################
prepare_workspace() {
rm -rf "${WORK_DIR}"
mkdir -p "${TWEAKS_DIR}" "${DYLIBS_DIR}" "${FRAMEWORKS_DIR}" "${PAYLOAD_DIR}"
log_ok "Workspace prepared at ${WORK_DIR}"
}
cleanup_workspace() {
rm -rf "${WORK_DIR}"
}
###############################################################################
# IPA extraction
###############################################################################
# $1 = path or URL to the decrypted YouTube IPA
extract_ipa() {
local ipa_source="$1"
local ipa_path="${WORK_DIR}/YouTube.ipa"
if [[ "$ipa_source" == http* ]]; then
log_info "Downloading IPA from URL…"
curl -fSL "$ipa_source" -o "$ipa_path" || die "Failed to download IPA"
else
[ -f "$ipa_source" ] || die "IPA file not found: $ipa_source"
cp "$ipa_source" "$ipa_path"
fi
log_info "Extracting IPA…"
unzip -q "$ipa_path" -d "${PAYLOAD_DIR}" || die "Failed to unzip IPA"
# Locate YouTube.app inside Payload
APP_DIR=$(find "${PAYLOAD_DIR}/Payload" -maxdepth 1 -name "*.app" -type d | head -n 1)
[ -d "$APP_DIR" ] || die "No .app found inside IPA"
log_ok "Extracted $(basename "$APP_DIR")"
}
###############################################################################
# Tweak fetching — release mode
###############################################################################
# Picks the best asset from a GitHub release.
# Priority: .dylib > .deb > .framework.zip > .zip
# $1 = repo (owner/name), $2 = tweak id, $3 = "always" flag (optional)
fetch_release_tweak() {
local repo="$1" tweak_id="$2"
local api_url="https://api.github.com/repos/${repo}/releases/latest"
local tweak_dir="${TWEAKS_DIR}/${tweak_id}"
mkdir -p "$tweak_dir"
log_info "Fetching latest release for ${repo}"
local release_json
release_json=$(curl -fsSL "$api_url") || die "GitHub API request failed for ${repo}"
# Build a priority-ordered list of candidate asset URLs
local dylib_url deb_url framework_url zip_url chosen_url chosen_name
# Parse assets with jq — pick first match per category
dylib_url=$(echo "$release_json" | jq -r '[.assets[] | select(.name | test("\\.dylib$"; "i"))][0].browser_download_url // empty')
deb_url=$(echo "$release_json" | jq -r '[.assets[] | select(.name | test("\\.deb$"; "i"))][0].browser_download_url // empty')
framework_url=$(echo "$release_json" | jq -r '[.assets[] | select(.name | test("\\.framework\\.zip$"; "i"))][0].browser_download_url // empty')
zip_url=$(echo "$release_json" | jq -r '[.assets[] | select(.name | test("\\.zip$"; "i")) | select(.name | test("\\.framework\\.zip$"; "i") | not)][0].browser_download_url // empty')
if [ -n "$dylib_url" ]; then
chosen_url="$dylib_url"
elif [ -n "$deb_url" ]; then
chosen_url="$deb_url"
elif [ -n "$framework_url" ]; then
chosen_url="$framework_url"
elif [ -n "$zip_url" ]; then
chosen_url="$zip_url"
else
die "No usable asset found in latest release of ${repo}"
fi
chosen_name=$(basename "$chosen_url")
log_info " → Downloading ${chosen_name}"
curl -fSL "$chosen_url" -o "${tweak_dir}/${chosen_name}" || die "Failed to download ${chosen_name}"
# Extract the injectable artefact
extract_tweak_artifact "${tweak_dir}" "${chosen_name}" "${tweak_id}"
}
###############################################################################
# Tweak fetching — build mode
###############################################################################
# $1 = repo, $2 = tweak id, $3 = branch (optional), $4 = build_cmd (optional)
fetch_build_tweak() {
local repo="$1" tweak_id="$2" branch="${3:-}" build_cmd="${4:-make package}"
local clone_dir="${TWEAKS_DIR}/${tweak_id}/src"
log_info "Building ${tweak_id} from source (${repo})…"
local clone_args=("--depth=1")
[ -n "$branch" ] && clone_args+=("--branch" "$branch")
git clone "${clone_args[@]}" "https://github.com/${repo}.git" "$clone_dir" \
|| die "Failed to clone ${repo}"
(
cd "$clone_dir"
eval "$build_cmd"
) || die "Build failed for ${tweak_id}"
# Search for output .dylib or .deb in the build tree
local found_dylib found_deb
found_dylib=$(find "$clone_dir" -name "*.dylib" -type f | head -n 1)
found_deb=$(find "$clone_dir/packages" -name "*.deb" -type f 2>/dev/null | head -n 1)
if [ -n "$found_dylib" ]; then
cp "$found_dylib" "${DYLIBS_DIR}/${tweak_id}.dylib"
log_ok " → Built dylib: ${tweak_id}.dylib"
elif [ -n "$found_deb" ]; then
extract_dylib_from_deb "$found_deb" "$tweak_id"
else
die "No .dylib or .deb output found after building ${tweak_id}"
fi
}
###############################################################################
# Artifact extraction helpers
###############################################################################
# Inspects a downloaded file and places the final .dylib / .framework into
# the appropriate output directory.
extract_tweak_artifact() {
local dir="$1" filename="$2" tweak_id="$3"
local filepath="${dir}/${filename}"
case "$filename" in
*.dylib)
cp "$filepath" "${DYLIBS_DIR}/${tweak_id}.dylib"
log_ok " → Ready: ${tweak_id}.dylib"
;;
*.deb)
extract_dylib_from_deb "$filepath" "$tweak_id"
;;
*.framework.zip)
extract_framework_from_zip "$filepath" "$tweak_id"
;;
*.zip)
extract_from_zip "$filepath" "$tweak_id"
;;
*)
die "Unsupported asset type: ${filename}"
;;
esac
}
# Extract .dylib from a .deb package
# Deb structure: data.tar.* contains the actual files
extract_dylib_from_deb() {
local deb_path="$1" tweak_id="$2"
local extract_dir="${TWEAKS_DIR}/${tweak_id}/deb_extract"
mkdir -p "$extract_dir"
log_info " → Extracting .deb for ${tweak_id}"
# ar extracts the deb; data.tar.* contains the payload
(cd "$extract_dir" && ar -x "$deb_path" 2>/dev/null) || {
# Some debs are just tarballs
tar -xf "$deb_path" -C "$extract_dir" 2>/dev/null || die "Cannot extract deb: $deb_path"
}
# Extract data.tar.* (could be .gz, .xz, .lzma, .zst, or uncompressed)
local data_tar
data_tar=$(find "$extract_dir" -name "data.tar*" -type f | head -n 1)
[ -n "$data_tar" ] || die "No data.tar found in deb for ${tweak_id}"
tar -xf "$data_tar" -C "$extract_dir" 2>/dev/null || die "Failed to extract data.tar for ${tweak_id}"
# Look for .dylib files
local dylib
dylib=$(find "$extract_dir" -name "*.dylib" -type f | head -n 1)
if [ -n "$dylib" ]; then
cp "$dylib" "${DYLIBS_DIR}/${tweak_id}.dylib"
log_ok " → Extracted dylib: ${tweak_id}.dylib"
else
# Look for .framework instead
local fw
fw=$(find "$extract_dir" -name "*.framework" -type d | head -n 1)
if [ -n "$fw" ]; then
cp -R "$fw" "${FRAMEWORKS_DIR}/"
log_ok " → Extracted framework: $(basename "$fw")"
else
die "No .dylib or .framework found in deb for ${tweak_id}"
fi
fi
# Also look for .bundle resources (some tweaks ship localization bundles)
local bundle
bundle=$(find "$extract_dir" -name "*.bundle" -type d | head -n 1)
if [ -n "$bundle" ]; then
cp -R "$bundle" "${WORK_DIR}/bundles_extra/$(basename "$bundle")" 2>/dev/null || {
mkdir -p "${WORK_DIR}/bundles_extra"
cp -R "$bundle" "${WORK_DIR}/bundles_extra/"
}
log_ok " → Extracted bundle: $(basename "$bundle")"
fi
}
# Extract .framework from a zip
extract_framework_from_zip() {
local zip_path="$1" tweak_id="$2"
local extract_dir="${TWEAKS_DIR}/${tweak_id}/fw_extract"
mkdir -p "$extract_dir"
unzip -qo "$zip_path" -d "$extract_dir" || die "Failed to unzip framework for ${tweak_id}"
local fw
fw=$(find "$extract_dir" -name "*.framework" -type d | head -n 1)
[ -n "$fw" ] || die "No .framework found in zip for ${tweak_id}"
cp -R "$fw" "${FRAMEWORKS_DIR}/"
log_ok " → Extracted framework: $(basename "$fw")"
}
# Generic zip extraction — look for .dylib, .framework, etc.
extract_from_zip() {
local zip_path="$1" tweak_id="$2"
local extract_dir="${TWEAKS_DIR}/${tweak_id}/zip_extract"
mkdir -p "$extract_dir"
unzip -qo "$zip_path" -d "$extract_dir" || die "Failed to unzip for ${tweak_id}"
# Try .dylib first, then .framework
local dylib
dylib=$(find "$extract_dir" -name "*.dylib" -type f | head -n 1)
if [ -n "$dylib" ]; then
cp "$dylib" "${DYLIBS_DIR}/${tweak_id}.dylib"
log_ok " → Extracted dylib from zip: ${tweak_id}.dylib"
return
fi
local fw
fw=$(find "$extract_dir" -name "*.framework" -type d | head -n 1)
if [ -n "$fw" ]; then
cp -R "$fw" "${FRAMEWORKS_DIR}/"
log_ok " → Extracted framework from zip: $(basename "$fw")"
return
fi
die "No usable artifact found in zip for ${tweak_id}"
}
###############################################################################
# Fetch all enabled tweaks
###############################################################################
fetch_all_tweaks() {
local tweaks_json
tweaks_json=$(config_enabled_tweaks_json)
local count
count=$(echo "$tweaks_json" | jq 'length')
log_info "Fetching ${count} enabled tweak(s)…"
for i in $(seq 0 $((count - 1))); do
local tweak
tweak=$(echo "$tweaks_json" | jq -r ".[$i]")
local id repo fetch_method branch build_cmd
id=$(echo "$tweak" | jq -r '.id')
repo=$(echo "$tweak" | jq -r '.repo')
fetch_method=$(echo "$tweak" | jq -r '.fetch')
branch=$(echo "$tweak" | jq -r '.branch // empty')
build_cmd=$(echo "$tweak" | jq -r '.build_cmd // empty')
case "$fetch_method" in
release)
fetch_release_tweak "$repo" "$id"
;;
build)
fetch_build_tweak "$repo" "$id" "$branch" "$build_cmd"
;;
*)
die "Unknown fetch method '${fetch_method}' for tweak ${id}"
;;
esac
done
log_ok "All tweaks fetched"
}
###############################################################################
# IPA patching — inject dylibs & frameworks
###############################################################################
inject_tweaks() {
[ -d "$APP_DIR" ] || die "APP_DIR not set — call extract_ipa first"
local injector=""
if command -v optool &>/dev/null; then
injector="optool"
elif command -v insert_dylib &>/dev/null; then
injector="insert_dylib"
else
die "Neither optool nor insert_dylib found"
fi
# Determine the main executable name from Info.plist
local executable
executable=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "${APP_DIR}/Info.plist" 2>/dev/null \
|| python3 -c "
import plistlib
with open('${APP_DIR}/Info.plist', 'rb') as f:
print(plistlib.load(f)['CFBundleExecutable'])
")
local exec_path="${APP_DIR}/${executable}"
[ -f "$exec_path" ] || die "Executable not found: ${exec_path}"
# Create Frameworks directory inside the app if needed
mkdir -p "${APP_DIR}/Frameworks"
# Inject each .dylib
local dylib_count=0
for dylib in "${DYLIBS_DIR}"/*.dylib; do
[ -f "$dylib" ] || continue
local dylib_name
dylib_name=$(basename "$dylib")
log_info "Injecting ${dylib_name}"
cp "$dylib" "${APP_DIR}/Frameworks/${dylib_name}"
case "$injector" in
optool)
optool install -c load -p "@rpath/${dylib_name}" -t "$exec_path" \
|| die "optool injection failed for ${dylib_name}"
;;
insert_dylib)
insert_dylib --inplace --no-strip-codesig \
"@rpath/${dylib_name}" "$exec_path" \
|| die "insert_dylib injection failed for ${dylib_name}"
;;
esac
dylib_count=$((dylib_count + 1))
done
# Copy frameworks
for fw in "${FRAMEWORKS_DIR}"/*.framework; do
[ -d "$fw" ] || continue
local fw_name
fw_name=$(basename "$fw")
log_info "Embedding framework: ${fw_name}"
cp -R "$fw" "${APP_DIR}/Frameworks/"
# Inject the framework binary
local fw_binary="${fw_name%.framework}"
case "$injector" in
optool)
optool install -c load -p "@rpath/${fw_name}/${fw_binary}" -t "$exec_path" \
|| log_warn "optool injection for framework ${fw_name} returned non-zero (may already be loaded)"
;;
insert_dylib)
insert_dylib --inplace --no-strip-codesig \
"@rpath/${fw_name}/${fw_binary}" "$exec_path" \
|| log_warn "insert_dylib for framework ${fw_name} returned non-zero"
;;
esac
done
# Copy any extra bundles (e.g. localization bundles from tweaks)
if [ -d "${WORK_DIR}/bundles_extra" ]; then
for bundle in "${WORK_DIR}/bundles_extra"/*.bundle; do
[ -d "$bundle" ] || continue
log_info "Embedding bundle: $(basename "$bundle")"
cp -R "$bundle" "${APP_DIR}/"
done
fi
log_ok "Injected ${dylib_count} dylib(s) into ${executable}"
}
###############################################################################
# IPA customization — apply settings from config.yml
###############################################################################
apply_customization() {
[ -d "$APP_DIR" ] || die "APP_DIR not set"
local bundle_id display_name min_ios
local strip_watch strip_plugins strip_extensions
bundle_id=$(config_custom "bundle_id")
display_name=$(config_custom "display_name")
min_ios=$(config_custom "min_ios")
strip_watch=$(config_custom "strip_watch_extension")
strip_plugins=$(config_custom "strip_plugins")
strip_extensions=$(config_custom "strip_extensions")
local plist="${APP_DIR}/Info.plist"
# Use python3 for plist manipulation (cross-platform)
python3 - "$plist" "$bundle_id" "$display_name" "$min_ios" <<'PYEOF'
import plistlib, sys
plist_path, bundle_id, display_name, min_ios = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
with open(plist_path, 'rb') as f:
plist = plistlib.load(f)
if bundle_id:
plist['CFBundleIdentifier'] = bundle_id
if display_name:
plist['CFBundleDisplayName'] = display_name
plist['CFBundleName'] = display_name
if min_ios:
plist['MinimumOSVersion'] = min_ios
# Remove UISupportedDevices to allow sideloading on any device
plist.pop('UISupportedDevices', None)
with open(plist_path, 'wb') as f:
plistlib.dump(plist, f)
PYEOF
log_ok "Updated plist: bundle_id=${bundle_id}, name=${display_name}, min_ios=${min_ios}"
# Remove code signature (required for re-signing after modification)
rm -rf "${APP_DIR}/_CodeSignature"
log_ok "Removed _CodeSignature"
# Strip watch extension
if [ "$strip_watch" = "True" ] || [ "$strip_watch" = "true" ]; then
rm -rf "${APP_DIR}/Watch"
log_ok "Stripped Watch extension"
fi
# Strip plugins
if [ "$strip_plugins" = "True" ] || [ "$strip_plugins" = "true" ]; then
rm -rf "${APP_DIR}/PlugIns"
log_ok "Stripped PlugIns"
fi
# Strip app extensions
if [ "$strip_extensions" = "True" ] || [ "$strip_extensions" = "true" ]; then
# Remove all .appex except ones we explicitly embed
if [ -d "${APP_DIR}/PlugIns" ]; then
find "${APP_DIR}/PlugIns" -name "*.appex" -type d -exec rm -rf {} + 2>/dev/null || true
fi
log_ok "Stripped extensions"
fi
# Apply custom icon if specified and file exists
local icon_path
icon_path=$(config_custom "icon")
if [ -n "$icon_path" ] && [ -f "${SCRIPT_DIR}/${icon_path}" ]; then
# Copy icon file over existing AppIcon assets
log_info "Custom icon specified: ${icon_path}"
# Note: Proper icon replacement requires multiple sizes; this is a placeholder
# for a full icon replacement implementation
fi
}
###############################################################################
# Repackage IPA
###############################################################################
# $1 = output path (default: YouTube-patched.ipa)
repackage_ipa() {
local output="${1:-${SCRIPT_DIR}/YouTube-patched.ipa}"
log_info "Repackaging IPA…"
(cd "${PAYLOAD_DIR}" && zip -qry "${output}" Payload/) \
|| die "Failed to create output IPA"
local size
size=$(du -h "$output" | cut -f1)
log_ok "Output IPA: ${output} (${size})"
# Print SHA256 for verification
if command -v shasum &>/dev/null; then
echo -e "${BOLD}SHA256: $(shasum -a 256 "$output" | cut -d' ' -f1)${NC}"
elif command -v sha256sum &>/dev/null; then
echo -e "${BOLD}SHA256: $(sha256sum "$output" | cut -d' ' -f1)${NC}"
fi
}
###############################################################################
# Full pipeline — called by build.sh and the CI workflow
###############################################################################
# $1 = IPA source (path or URL)
# $2 = output IPA path (optional)
run_full_pipeline() {
local ipa_source="$1"
local output_ipa="${2:-${SCRIPT_DIR}/YouTube-patched.ipa}"
log_info "YTLitePlus Build Pipeline"
echo "──────────────────────────────────────"
check_deps
prepare_workspace
extract_ipa "$ipa_source"
fetch_all_tweaks
inject_tweaks
apply_customization
repackage_ipa "$output_ipa"
log_ok "Build complete!"
echo "──────────────────────────────────────"
}