| #!/usr/bin/env python3 |
| |
| # Copyright (C) 2026 Ian Grunert <ian.grunert@gmail.com> |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| # THE POSSIBILITY OF SUCH DAMAGE. |
| |
| """ |
| Validates that all required tools for building the Windows port from a Linux host are installed. |
| """ |
| |
| import os |
| import platform |
| import shutil |
| import subprocess |
| import sys |
| |
| WEBKIT_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..')) |
| WEBKIT_LIBRARIES_WINDOWS = os.path.join(WEBKIT_ROOT, 'WebKitLibraries', 'windows') |
| |
| VCPKG_DIR = os.path.join(WEBKIT_LIBRARIES_WINDOWS, 'vcpkg') |
| XWIN_CACHE_DIR = os.path.join(WEBKIT_LIBRARIES_WINDOWS, '.xwin') |
| |
| # Determine host architecture and set arch-specific constants |
| HOST_MACHINE = platform.machine() |
| if HOST_MACHINE == 'aarch64': |
| CLANG_RT_ARCH = 'aarch64' |
| SDK_LIB_ARCH = 'arm64' |
| XWIN_HOST_ARCH = 'aarch64-unknown-linux-musl' |
| XWIN_ARCH_FLAG = ['--arch', 'aarch64'] |
| CLANG_RT_PLATFORM = 'aarch64-pc-windows-msvc' |
| else: |
| CLANG_RT_ARCH = 'x86_64' |
| SDK_LIB_ARCH = 'x64' |
| XWIN_HOST_ARCH = 'x86_64-unknown-linux-musl' |
| XWIN_ARCH_FLAG = [] |
| CLANG_RT_PLATFORM = 'x86_64-pc-windows-msvc' |
| |
| REQUIRED_EXECUTABLES = [ |
| ('clang-cl-20', 'LLVM MSVC-compatible C/C++ compiler driver'), |
| ('ninja', 'Build system'), |
| ('lld-link-20', 'LLVM PE/COFF linker'), |
| ('llvm-lib-20', 'LLVM library archiver'), |
| ('llvm-rc-20', 'LLVM resource compiler'), |
| ('llvm-mt-20', 'LLVM manifest tool'), |
| ('llvm-ranlib-20', 'LLVM archive indexer'), |
| ('git', 'Version control (needed for vcpkg)'), |
| ('cmake', 'Build system generator (needed for vcpkg)'), |
| ] |
| |
| CLANG_RT_LIBRARY = f'clang_rt.builtins-{CLANG_RT_ARCH}.lib' |
| |
| INSTALLATION_INSTRUCTIONS = { |
| 'clang-cl-20': 'sudo apt-get install clang-20 (from https://apt.llvm.org/) - clang-cl-20 is included with clang-20', |
| 'lld-link-20': 'sudo apt-get install lld-20 (from https://apt.llvm.org/)', |
| 'llvm-lib-20': 'sudo apt-get install llvm-20 (from https://apt.llvm.org/)', |
| 'llvm-rc-20': 'sudo apt-get install llvm-20 (from https://apt.llvm.org/)', |
| 'llvm-mt-20': 'sudo apt-get install llvm-20 (from https://apt.llvm.org/)', |
| 'llvm-ranlib-20': 'sudo apt-get install llvm-20 (from https://apt.llvm.org/)', |
| 'ninja': 'sudo apt-get install ninja-build', |
| 'git': 'sudo apt-get install git', |
| 'cmake': 'sudo apt-get install cmake', |
| 'xwin': f'Download from https://github.com/Jake-Shadle/xwin/releases and extract to {WEBKIT_LIBRARIES_WINDOWS}/', |
| 'Windows SDK': f'Run `xwin {" ".join(XWIN_ARCH_FLAG)} --cache-dir {WEBKIT_LIBRARIES_WINDOWS}/.xwin splat --preserve-ms-arch-notation --output {WEBKIT_LIBRARIES_WINDOWS}/` (requires xwin)', |
| CLANG_RT_LIBRARY: f"Download clang+llvm-20.1.0-{CLANG_RT_PLATFORM}.tar.xz from https://github.com/llvm/llvm-project/releases, extract 'lib/clang/20/lib/windows/{CLANG_RT_LIBRARY}', move to {WEBKIT_LIBRARIES_WINDOWS}/", |
| 'vcpkg': f'Clone https://github.com/microsoft/vcpkg into {VCPKG_DIR} and run bootstrap-vcpkg.sh', |
| } |
| |
| |
| def check_executable(name): |
| """Check if an executable is on PATH. Returns the path if found, None otherwise.""" |
| return shutil.which(name) |
| |
| |
| def check_xwin(): |
| """Check for xwin on PATH or in WebKitLibraries/windows/. Returns the path if found, None otherwise.""" |
| # First check PATH |
| path = shutil.which('xwin') |
| if path: |
| return path |
| |
| # Then check WebKitLibraries/windows/ |
| local_path = os.path.join(WEBKIT_LIBRARIES_WINDOWS, 'xwin') |
| if os.path.isfile(local_path) and os.access(local_path, os.X_OK): |
| return local_path |
| |
| return None |
| |
| |
| def check_clang_rt(): |
| """Check for clang_rt builtins lib in WebKitLibraries/windows/. Returns the path if found, None otherwise.""" |
| path = os.path.join(WEBKIT_LIBRARIES_WINDOWS, CLANG_RT_LIBRARY) |
| if os.path.isfile(path): |
| return path |
| return None |
| |
| |
| |
| def check_vcpkg(): |
| """Check if vcpkg is installed at WebKitLibraries/windows/vcpkg/. Returns the path if found, None otherwise.""" |
| vcpkg_path = os.path.join(VCPKG_DIR, 'vcpkg') |
| if os.path.isfile(vcpkg_path) and os.access(vcpkg_path, os.X_OK): |
| return vcpkg_path |
| return None |
| |
| |
| def download_vcpkg(): |
| """Clone and bootstrap vcpkg into WebKitLibraries/windows/vcpkg/.""" |
| print('Cloning vcpkg...') |
| result = subprocess.run( |
| ['git', 'clone', 'https://github.com/microsoft/vcpkg.git', VCPKG_DIR] |
| ) |
| if result.returncode != 0: |
| print('Error cloning vcpkg.') |
| return None |
| |
| print('Bootstrapping vcpkg...') |
| bootstrap_script = os.path.join(VCPKG_DIR, 'bootstrap-vcpkg.sh') |
| result = subprocess.run([bootstrap_script], cwd=VCPKG_DIR) |
| if result.returncode != 0: |
| print('Error bootstrapping vcpkg.') |
| return None |
| |
| vcpkg_path = os.path.join(VCPKG_DIR, 'vcpkg') |
| print(f'Installed vcpkg to {vcpkg_path}') |
| return vcpkg_path |
| |
| |
| def check_windows_sdk(): |
| """Check if Windows SDK is present. Returns True if found, False otherwise.""" |
| sdk_files = [ |
| os.path.join(WEBKIT_LIBRARIES_WINDOWS, 'sdk', 'include', 'um', 'windows.h'), |
| os.path.join(WEBKIT_LIBRARIES_WINDOWS, 'sdk', 'lib', 'um', SDK_LIB_ARCH, 'kernel32.lib'), |
| os.path.join(WEBKIT_LIBRARIES_WINDOWS, 'crt', 'include', 'vcruntime.h'), |
| ] |
| return all(os.path.isfile(f) for f in sdk_files) |
| |
| |
| def create_lib_symlinks_for_case_sensitivity(): |
| """Create symlinks for library files to handle case-sensitivity on Linux. |
| |
| Windows is case-insensitive, so libraries may be referenced with different |
| case than they exist on disk. We create symlinks for common patterns. |
| """ |
| lib_dirs = [ |
| os.path.join(WEBKIT_LIBRARIES_WINDOWS, 'sdk', 'lib', 'um', SDK_LIB_ARCH), |
| os.path.join(WEBKIT_LIBRARIES_WINDOWS, 'sdk', 'lib', 'ucrt', SDK_LIB_ARCH), |
| os.path.join(WEBKIT_LIBRARIES_WINDOWS, 'crt', 'lib', SDK_LIB_ARCH), |
| ] |
| |
| # Known CamelCase library names that need specific symlinks |
| # Maps lowercase name to the CamelCase variant(s) needed |
| CAMELCASE_LIBS = { |
| 'windowscodecs': ['WindowsCodecs'], |
| 'd2d1': ['D2d1'], |
| 'dwrite': ['Dwrite', 'DWrite'], |
| 'iphlpapi': ['Iphlpapi'], |
| 'shlwapi': ['Shlwapi'], |
| 'dbghelp': ['DbgHelp'], |
| } |
| |
| created = 0 |
| for lib_dir in lib_dirs: |
| if not os.path.isdir(lib_dir): |
| continue |
| # Get all actual .lib files (any case extension) |
| lib_files = {} |
| for filename in os.listdir(lib_dir): |
| if filename.lower().endswith('.lib') and os.path.isfile(os.path.join(lib_dir, filename)): |
| base = filename[:-4] # Remove extension |
| lib_files[filename] = base |
| |
| # For each library, create symlinks with different case patterns |
| for filename, base in lib_files.items(): |
| # Generate case variants: lowercase.lib, Original.lib, UPPERCASE.lib, Titlecase.lib |
| variants = [ |
| base.lower() + '.lib', # all lowercase |
| base + '.lib', # original base + lowercase ext |
| base.upper() + '.lib', # all uppercase base + lowercase ext |
| base.capitalize() + '.lib', # title case (first letter upper, rest lower) |
| ] |
| |
| # Add known CamelCase variants |
| base_lower = base.lower() |
| if base_lower in CAMELCASE_LIBS: |
| for camel in CAMELCASE_LIBS[base_lower]: |
| variants.append(camel + '.lib') |
| |
| for variant in variants: |
| if variant != filename: |
| dst_path = os.path.join(lib_dir, variant) |
| if not os.path.exists(dst_path): |
| os.symlink(filename, dst_path) |
| created += 1 |
| |
| # Also create symlinks for known header case mismatches in the SDK. |
| # Windows is case-insensitive, but .rc files and source code may reference |
| # headers with different case than xwin extracts them. |
| HEADER_CASE_VARIANTS = { |
| # (directory relative to sdk/include, original_name): [variant_names] |
| ('um', 'winver.h'): ['Winver.h'], |
| } |
| for (subdir, original), variants in HEADER_CASE_VARIANTS.items(): |
| inc_dir = os.path.join(WEBKIT_LIBRARIES_WINDOWS, 'sdk', 'include', subdir) |
| original_path = os.path.join(inc_dir, original) |
| if not os.path.isfile(original_path): |
| continue |
| for variant in variants: |
| variant_path = os.path.join(inc_dir, variant) |
| if not os.path.exists(variant_path): |
| os.symlink(original, variant_path) |
| created += 1 |
| |
| if created > 0: |
| print(f'Created {created} case-variant symlinks for library/header files.') |
| |
| |
| def download_windows_sdk(xwin_path): |
| """Download Windows SDK/CRT using xwin splat.""" |
| print('Downloading Windows SDK and CRT (this may take a while)...') |
| xwin_cmd = [xwin_path] + XWIN_ARCH_FLAG + ['--accept-license', '--cache-dir', XWIN_CACHE_DIR, 'splat', '--preserve-ms-arch-notation', '--include-debug-libs', '--output', WEBKIT_LIBRARIES_WINDOWS] |
| result = subprocess.run(xwin_cmd) |
| if result.returncode != 0: |
| print('Error downloading SDK.') |
| return False |
| print('Windows SDK and CRT installed successfully.') |
| |
| # Create lowercase symlinks for case-sensitive Linux filesystems |
| create_lib_symlinks_for_case_sensitivity() |
| |
| return True |
| |
| |
| def download_with_progress(url, dest_path): |
| """Download a file with progress bar display.""" |
| import urllib.request |
| |
| def reporthook(block_num, block_size, total_size): |
| downloaded = block_num * block_size |
| if total_size > 0: |
| percent = min(100, downloaded * 100 // total_size) |
| downloaded_mb = downloaded / (1024 * 1024) |
| total_mb = total_size / (1024 * 1024) |
| print(f'\rDownloading: {percent}% ({downloaded_mb:.1f}/{total_mb:.1f} MB)', end='', flush=True) |
| |
| urllib.request.urlretrieve(url, dest_path, reporthook) |
| print() # Newline after progress |
| |
| |
| def download_xwin(): |
| """Download and install xwin to WebKitLibraries/windows/.""" |
| import tarfile |
| |
| XWIN_URL = f'https://github.com/Jake-Shadle/xwin/releases/download/0.7.0/xwin-0.7.0-{XWIN_HOST_ARCH}.tar.gz' |
| tarball_path = '/tmp/xwin.tar.gz' |
| |
| # Download |
| print(f'Downloading {XWIN_URL}...') |
| download_with_progress(XWIN_URL, tarball_path) |
| |
| # Create target directory |
| os.makedirs(WEBKIT_LIBRARIES_WINDOWS, exist_ok=True) |
| |
| # Extract xwin binary from tarball |
| with tarfile.open(tarball_path, 'r:gz') as tar: |
| for member in tar.getmembers(): |
| if member.name.endswith('/xwin'): |
| member.name = 'xwin' # Strip directory prefix |
| tar.extract(member, WEBKIT_LIBRARIES_WINDOWS) |
| break |
| |
| # Make executable |
| xwin_path = os.path.join(WEBKIT_LIBRARIES_WINDOWS, 'xwin') |
| os.chmod(xwin_path, 0o755) |
| |
| # Cleanup |
| os.remove(tarball_path) |
| |
| print(f'Installed xwin to {xwin_path}') |
| return xwin_path |
| |
| |
| def download_clang_rt(): |
| """Download clang_rt builtins library from LLVM Windows release.""" |
| import tarfile |
| |
| LLVM_VERSION = '20.1.0' |
| CLANG_MAJOR_VERSION = '20' |
| ARCHIVE_NAME = f'clang+llvm-{LLVM_VERSION}-{CLANG_RT_PLATFORM}' |
| LLVM_URL = f'https://github.com/llvm/llvm-project/releases/download/llvmorg-{LLVM_VERSION}/{ARCHIVE_NAME}.tar.xz' |
| archive_path = '/tmp/clang-llvm-win.tar.xz' |
| lib_path_in_archive = f'{ARCHIVE_NAME}/lib/clang/{CLANG_MAJOR_VERSION}/lib/windows/{CLANG_RT_LIBRARY}' |
| |
| # Download with progress bar |
| print(f'Downloading {ARCHIVE_NAME}.tar.xz...') |
| download_with_progress(LLVM_URL, archive_path) |
| |
| # Create target directory |
| os.makedirs(WEBKIT_LIBRARIES_WINDOWS, exist_ok=True) |
| |
| # Extract just the clang_rt library |
| print(f'Extracting {CLANG_RT_LIBRARY}...') |
| with tarfile.open(archive_path, 'r:xz') as tar: |
| member = tar.getmember(lib_path_in_archive) |
| member.name = CLANG_RT_LIBRARY # Strip directory prefix |
| tar.extract(member, WEBKIT_LIBRARIES_WINDOWS) |
| |
| # Cleanup |
| os.remove(archive_path) |
| |
| print(f'Installed {CLANG_RT_LIBRARY} to {WEBKIT_LIBRARIES_WINDOWS}') |
| return True |
| |
| |
| def create_dummy_pwsh(): |
| """Create a dummy pwsh script to satisfy vcpkg's PowerShell check. |
| |
| vcpkg's vcpkg_copy_tool_dependencies.cmake requires PowerShell on Windows. |
| When cross-compiling from Linux, we don't need this functionality, so we |
| provide a dummy that exits successfully. |
| |
| Returns the path to the created pwsh script. |
| """ |
| bin_dir = os.path.join(WEBKIT_LIBRARIES_WINDOWS, 'bin') |
| pwsh_path = os.path.join(bin_dir, 'pwsh') |
| |
| os.makedirs(bin_dir, exist_ok=True) |
| |
| with open(pwsh_path, 'w') as f: |
| f.write('#!/bin/sh\n' |
| 'case "$1" in\n' |
| ' --version) echo "PowerShell 7.5.4";;\n' |
| 'esac\n' |
| 'exit 0\n') |
| os.chmod(pwsh_path, 0o755) |
| |
| return pwsh_path |
| |
| |
| def get_installation_instructions(missing_items): |
| """Build help text for missing items.""" |
| lines = ['\nInstallation instructions:'] |
| for item in missing_items: |
| if item in INSTALLATION_INSTRUCTIONS: |
| lines.append(f' {item}:') |
| lines.append(f' {INSTALLATION_INSTRUCTIONS[item]}') |
| return '\n'.join(lines) |
| |
| |
| def main(): |
| missing = [] |
| all_ok = True |
| |
| print('Checking Windows cross-build dependencies...\n') |
| |
| # Check required executables |
| for name, description in REQUIRED_EXECUTABLES: |
| path = check_executable(name) |
| if path: |
| print(f'[OK] {name} ({description})') |
| print(f' {path}') |
| else: |
| print(f'[MISSING] {name} ({description})') |
| missing.append(name) |
| all_ok = False |
| |
| # Check xwin (special handling with auto-download prompt) |
| xwin_path = check_xwin() |
| if xwin_path: |
| print('[OK] xwin (Windows SDK/CRT sysroot tool)') |
| print(f' {xwin_path}') |
| else: |
| print('[MISSING] xwin (Windows SDK/CRT sysroot tool)') |
| response = input('Would you like to download and install xwin? [Y/n] ') |
| if response.lower() in ('', 'y', 'yes'): |
| xwin_path = download_xwin() |
| print('[OK] xwin (Windows SDK/CRT sysroot tool)') |
| print(f' {xwin_path}') |
| else: |
| missing.append('xwin') |
| all_ok = False |
| |
| # Check Windows SDK (requires xwin to download) |
| if check_windows_sdk(): |
| print('[OK] Windows SDK/CRT sysroot') |
| print(f' {os.path.join(WEBKIT_LIBRARIES_WINDOWS, "sdk")}') |
| # Ensure lowercase symlinks exist for case-sensitive filesystems |
| create_lib_symlinks_for_case_sensitivity() |
| else: |
| print('[MISSING] Windows SDK/CRT sysroot') |
| if xwin_path: |
| response = input('Would you like to download the Windows SDK/CRT using xwin? [Y/n] ') |
| if response.lower() in ('', 'y', 'yes'): |
| if download_windows_sdk(xwin_path): |
| print('[OK] Windows SDK/CRT sysroot') |
| print(f' {os.path.join(WEBKIT_LIBRARIES_WINDOWS, "sdk")}') |
| else: |
| missing.append('Windows SDK') |
| all_ok = False |
| else: |
| missing.append('Windows SDK') |
| all_ok = False |
| else: |
| missing.append('Windows SDK') |
| all_ok = False |
| |
| # Check clang_rt library |
| clang_rt_path = check_clang_rt() |
| if clang_rt_path: |
| print(f'[OK] {CLANG_RT_LIBRARY} (Clang runtime builtins)') |
| print(f' {clang_rt_path}') |
| else: |
| print(f'[MISSING] {CLANG_RT_LIBRARY} (Clang runtime builtins)') |
| response = input(f'Would you like to download {CLANG_RT_LIBRARY}? [Y/n] ') |
| if response.lower() in ('', 'y', 'yes'): |
| if download_clang_rt(): |
| print(f'[OK] {CLANG_RT_LIBRARY} (Clang runtime builtins)') |
| print(f' {os.path.join(WEBKIT_LIBRARIES_WINDOWS, CLANG_RT_LIBRARY)}') |
| else: |
| missing.append(CLANG_RT_LIBRARY) |
| all_ok = False |
| else: |
| missing.append(CLANG_RT_LIBRARY) |
| all_ok = False |
| |
| # Check vcpkg |
| vcpkg_path = check_vcpkg() |
| if vcpkg_path: |
| print('[OK] vcpkg (C/C++ package manager)') |
| print(f' {vcpkg_path}') |
| else: |
| print('[MISSING] vcpkg (C/C++ package manager)') |
| response = input('Would you like to clone and bootstrap vcpkg? [Y/n] ') |
| if response.lower() in ('', 'y', 'yes'): |
| vcpkg_path = download_vcpkg() |
| if vcpkg_path: |
| print('[OK] vcpkg (C/C++ package manager)') |
| print(f' {vcpkg_path}') |
| else: |
| missing.append('vcpkg') |
| all_ok = False |
| else: |
| missing.append('vcpkg') |
| all_ok = False |
| |
| # Create dummy pwsh for vcpkg cross-compilation |
| if all_ok: |
| print() |
| create_dummy_pwsh() |
| print('Created dummy pwsh for vcpkg cross-compilation.') |
| |
| # Print summary |
| print() |
| if all_ok: |
| print('All dependencies satisfied.') |
| return 0 |
| else: |
| print(f'{len(missing)} missing dependency(ies).') |
| print(get_installation_instructions(missing)) |
| return 1 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |