mirror of
https://github.com/YTLitePlus/YTLitePlus.git
synced 2026-03-13 06:26:30 +00:00
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:
parent
cbe0251fc3
commit
6eeac05d8c
5 changed files with 842 additions and 31 deletions
104
.github/workflows/build.yml
vendored
Normal file
104
.github/workflows/build.yml
vendored
Normal 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
3
.gitignore
vendored
|
|
@ -8,3 +8,6 @@ Resources/
|
|||
*.ipa
|
||||
.vscode
|
||||
_codeql_detected_source_root
|
||||
|
||||
# v2 build system work directory
|
||||
_work/
|
||||
|
|
|
|||
71
build.sh
Normal file → Executable file
71
build.sh
Normal file → Executable 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
89
config.yml
Normal 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
606
lib.sh
Executable 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 "──────────────────────────────────────"
|
||||
}
|
||||
Loading…
Reference in a new issue