├── .github └── workflows │ └── sdks.yml ├── README.md ├── do.sh └── sdkgen.py /.github/workflows/sdks.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: SDKs 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | version: 15 | - "15.5" 16 | - "15.4" 17 | - "15.2" 18 | - "15.0" 19 | - "14.5" 20 | - "14.4" 21 | - "14.3" 22 | - "14.2" 23 | - "14.1" 24 | - "14.0" 25 | - "13.7" 26 | - "13.6" 27 | - "13.5" 28 | - "13.4" 29 | - "13.2" 30 | - "13.1" 31 | 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | - uses: actions/checkout@v3 36 | - name: Set up Python 37 | uses: actions/setup-python@v4 38 | with: 39 | python-version: '3.10' 40 | - name: Install blacktop/ipsw # Blacktop's brew tap doesn't support Linux currently 41 | run: | 42 | git clone https://github.com/blacktop/ipsw 43 | cd ipsw 44 | curl -L https://github.com/blacktop/ipsw/releases/latest/download/ipsw_"$(git describe --tags $(git rev-list --tags --max-count=1) | cut -c2-)"_linux_x86_64.deb -o ipsw.deb 45 | sudo dpkg -i ipsw.deb 46 | cd 47 | - name: Install apfs-fuse 48 | run: | 49 | sudo apt update && sudo apt install -y libfuse3-dev 50 | git clone --recursive https://github.com/sgan81/apfs-fuse 51 | cd apfs-fuse 52 | mkdir build && cd build 53 | cmake ../ 54 | make -j$(nproc --all) install 55 | cd 56 | - name: Set up python dependencies 57 | run: | 58 | python -m pip install --upgrade pip 59 | pip install progressbar2 dyldextractor poetry 60 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 61 | git clone https://github.com/cxnder/ktool.git && cd ktool && chmod +x dev_install.sh && ./dev_install.sh 62 | - name: Build SDK 63 | run: | 64 | python3 sdkgen.py ${{ matrix.version }} 65 | - name: Prep SDK for artifact upload 66 | run: | 67 | mv ${{ matrix.version }}.extracted iPhoneOS${{ matrix.version }}.sdk 68 | zip -9 -r iPhoneOS${{ matrix.version }}.sdk.zip iPhoneOS${{ matrix.version }}.sdk 69 | - name: Upload SDK artifact 70 | uses: actions/upload-artifact@v3 71 | with: 72 | name: ${{ matrix.version }}-Container 73 | path: iPhoneOS${{ matrix.version }}.sdk.zip 74 | 75 | 76 | upload: 77 | needs: build 78 | runs-on: ubuntu-latest 79 | steps: 80 | - uses: actions/checkout@v3 81 | - name: Download artifacts 82 | uses: actions/download-artifact@v3 83 | 84 | - name: Extract SDKs from artifacts # SDKs get put into artifact-named directories by the artifact actions 85 | run: | 86 | set -x 87 | mkdir sdks/ 88 | for container in ./*Container; do 89 | if [[ -d "$container" ]]; then 90 | mv $container/*.zip sdks/ # Note: for some reason you can't cd into the artifact dirs?! 91 | fi 92 | done 93 | 94 | - name: Create Release 95 | id: create_release 96 | uses: actions/create-release@v1 97 | env: 98 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 99 | with: 100 | tag_name: 1.4.0 101 | release_name: iOS 13.0-15.5 SDKs 102 | draft: false 103 | prerelease: false 104 | - name: Attach SDKs to release 105 | env: 106 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 107 | run: | 108 | set -x 109 | sdks=() 110 | for sdk in sdks/*.zip; do 111 | sdks+=("-a" "$sdk") 112 | done 113 | hub release edit "${sdks[@]}" -m "iOS 13.0-15.5 SDKs" "1.4.0" 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sdk-builder 2 | Automated iOS SDK builder 3 | 4 | WIP mostly-functional patched SDK (with headers) builder utilizing [ktool](https://github.com/cxnder/ktool) and GitHub Actions automation to generate the SDKs on GitHub's servers 5 | -------------------------------------------------------------------------------- /do.sh: -------------------------------------------------------------------------------- 1 | python3 sdkgen.py 15.0 -------------------------------------------------------------------------------- /sdkgen.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import multiprocessing 3 | import concurrent.futures 4 | import sys 5 | import os 6 | import glob 7 | import ktool 8 | import time 9 | import json 10 | 11 | def system(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, echo=False): 12 | proc = subprocess.Popen("" + cmd, 13 | shell=True) 14 | proc.communicate() 15 | return proc.returncode == 0 16 | 17 | 18 | def system_with_output(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, echo=False): 19 | proc = subprocess.Popen("" + cmd, 20 | stdout=stdout, 21 | stderr=stderr, 22 | shell=True) 23 | std_out, std_err = proc.communicate() 24 | return proc.returncode, std_out, std_err 25 | 26 | 27 | def system_pipe_output(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, echo=False): 28 | if echo: 29 | print(cmd) 30 | 31 | process = subprocess.Popen(cmd, 32 | stdout=stdout, 33 | stderr=stderr, 34 | shell=True) 35 | 36 | while True: 37 | realtime_output = process.stdout.readline() 38 | realtime_err = process.stderr.readline() 39 | 40 | if realtime_output == '' and realtime_err == '' and process.poll() is not None: 41 | break 42 | 43 | if realtime_output: 44 | print(realtime_output.strip(), flush=True) 45 | if realtime_err: 46 | print(realtime_err.strip(), flush=True, file=sys.stderr) 47 | 48 | 49 | class IPSWAdapter: 50 | def __init__(self): 51 | self.ipsw_path = 'ipsw' 52 | 53 | def try_dl_and_extract(self, version, device, output_folder, max_dl_attempts=5): 54 | attempts = max_dl_attempts 55 | while attempts >= 0: 56 | if self.download(version, device): 57 | break 58 | attempts -= 1 59 | time.sleep(10) 60 | 61 | if self.extract(output_folder): 62 | system('rm *.ipsw') 63 | 64 | 65 | 66 | def extract(self, output_folder, ipsw_name='$(ls *.ipsw | xargs)'): 67 | if not system(f'{self.ipsw_path} extract -d {ipsw_name}'): 68 | return False 69 | if not system(f'mkdir -p {output_folder}'): 70 | return False 71 | if not system(f'mv $(find . -name dyld_shared_cache* | xargs) {output_folder}'): 72 | return False 73 | 74 | def download(self, version, device): 75 | if not system(f'{self.ipsw_path} download ipsw --version {version} --device {device}', echo=True): 76 | return False 77 | return True 78 | 79 | 80 | class DEAdapter: 81 | def __init__(self): 82 | pass 83 | 84 | def extract_all(self, dsc_folder, output_folder): 85 | cwd = os.getcwd() 86 | os.chdir(dsc_folder) 87 | system(f'dyldex_all -j$(nproc --all) dyld_shared_cache_arm64') 88 | system(f'mv binaries/System ./') 89 | os.chdir(cwd) 90 | system(f'mv {dsc_folder}/System/* {output_folder}') 91 | 92 | 93 | def dump(filename): 94 | fd = open(f'{filename}', 'rb') 95 | 96 | library = ktool.load_image(fd, force_misaligned_vm=True) 97 | objc_lib = ktool.load_objc_metadata(library) 98 | 99 | tbd_text = ktool.generate_text_based_stub(library, compatibility=True) 100 | with open(f'{filename}.tbd', 'w') as tbd_out: 101 | tbd_out.write(tbd_text) 102 | 103 | data = library.serialize() 104 | objc_data = objc_lib.serialize() 105 | 106 | framework_data = { 107 | 'filename': filename, 108 | 'comment': 'Info Dumped with ktool + sdk-builder', 109 | 'ktool-version': ktool.util.KTOOL_VERSION, 110 | 'data': data, 111 | 'objc': objc_data 112 | } 113 | 114 | with open(f'{filename}.json', 'w') as fp: 115 | json.dump(framework_data, fp) 116 | 117 | os.makedirs(f'{os.path.dirname(filename)}/Headers', exist_ok=True) 118 | 119 | header_dict = ktool.generate_headers(objc_lib, sort_items=True) 120 | for header_name in header_dict: 121 | with open(f'{os.path.dirname(filename)}/Headers' + '/' + header_name, 122 | 'w') as out: 123 | out.write(str(header_dict[header_name])) 124 | 125 | 126 | def trydump(item): 127 | try: 128 | print(f'Dumping {item}') 129 | dump(item) 130 | except Exception as ex: 131 | print(ex) 132 | print(f'{item} Fail') 133 | 134 | 135 | if __name__ == "__main__": 136 | ipsw = IPSWAdapter() 137 | de = DEAdapter() 138 | 139 | vers = sys.argv[1] 140 | 141 | if not os.path.exists(f'{vers}.dsc'): 142 | ipsw.try_dl_and_extract(f'{vers}', 'iPhone10,3', f'{vers}.dsc') 143 | if not os.path.exists(f'{vers}.bins'): 144 | de.extract_all(f'{vers}.dsc', f'{vers}.bins') 145 | if not os.path.exists(f'{vers}.extracted'): 146 | system(f"cp -r {vers}.bins {vers}.extracted") 147 | 148 | file_batch_list = [] 149 | 150 | for filename in glob.iglob(f'{vers}.extracted/' + '**/**', recursive=True): 151 | if os.path.isfile(filename): 152 | if not os.path.exists(filename + '.tbd'): 153 | if not '.h' in filename and not '.tbd' in filename: 154 | file_batch_list.append(filename) 155 | print(file_batch_list) 156 | public_frameworks = sorted(list(set(file_batch_list))) 157 | executor = concurrent.futures.ProcessPoolExecutor(multiprocessing.cpu_count()-1) 158 | futures = [executor.submit(trydump, (item)) for item in public_frameworks] 159 | concurrent.futures.wait(futures) 160 | --------------------------------------------------------------------------------