calibre/setup/wincross.py
2025-01-24 11:14:21 +01:00

457 lines
18 KiB
Python

#!/usr/bin/env python
# License: GPLv3 Copyright: 2023, Kovid Goyal <kovid at kovidgoyal.net>
# See https://github.com/mstorsjo/msvc-wine/blob/master/vsdownload.py and
# https://github.com/Jake-Shadle/xwin/blob/main/src/lib.rs for the basic logic
# used to download and install the needed VisualStudio packages
import argparse
import concurrent.futures
import glob
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
from collections import defaultdict
from dataclasses import dataclass
from functools import partial
from pprint import pprint
from tempfile import TemporaryDirectory
from urllib.parse import unquote
from urllib.request import urlopen
from zipfile import ZipFile
@dataclass
class File:
filename: str
url: str
size: int
sha256: str
def __init__(self, pf, filename=''):
self.filename=filename or pf['fileName']
self.url=pf['url']
self.size=pf['size']
self.sha256=pf['sha256'].lower()
def package_sort_key(p):
chip = 0 if p.get('chip', '').lower() == 'x64' else 1
language = 0 if p.get('language', '').lower().startswith('en-') else 1
return chip, language
def llvm_arch_to_ms_arch(arch):
return {'x86_64': 'x64', 'aarch64': 'arm64', 'x64': 'x64', 'arm64': 'arm64'}[arch]
class Packages:
def __init__(self, manifest_raw, crt_variant, arch):
arch = llvm_arch_to_ms_arch(arch)
self.manifest = json.loads(manifest_raw)
self.packages = defaultdict(list)
self.cabinet_entries = {}
for p in self.manifest['packages']:
pid = p['id'].lower()
self.packages[pid].append(p)
for v in self.packages.values():
v.sort(key=package_sort_key)
build_tools = self.packages[
'Microsoft.VisualStudio.Product.BuildTools'.lower()][0]
pat = re.compile(r'Microsoft\.VisualStudio\.Component\.VC\.(.+)\.x86\.x64')
latest = (0, 0, 0, 0)
self.crt_version = ''
for dep in build_tools['dependencies']:
m = pat.match(dep)
if m is not None:
parts = m.group(1).split('.')
if len(parts) > 1:
q = tuple(map(int, parts))
if q > latest:
self.crt_version = m.group(1)
latest = q
if not self.crt_version:
raise KeyError('Failed to find CRT version from build tools deps')
self.files_to_download = []
def add_package(key):
p = self.packages.get(key.lower())
if not p:
raise KeyError(f'No package named {key} found')
for pf in p[0]['payloads']:
self.files_to_download.append(File(pf))
# CRT headers
add_package(f'Microsoft.VC.{self.crt_version}.CRT.Headers.base')
# CRT libs
prefix = f'Microsoft.VC.{self.crt_version}.CRT.{arch}.'.lower()
variants = {}
for pid in self.packages:
if pid.startswith(prefix):
parts = pid[len(prefix):].split('.')
if parts[-1] == 'base':
variant = parts[0]
if variant not in variants or 'spectre' in parts:
# spectre variant contains both spectre and regular libs
variants[variant] = pid
add_package(variants[crt_variant])
# ATL headers
add_package(f'Microsoft.VC.{self.crt_version}.ATL.Headers.base')
# ATL libs
add_package(f'Microsoft.VC.{self.crt_version}.ATL.{arch}.Spectre.base')
add_package(f'Microsoft.VC.{self.crt_version}.ATL.{arch}.base')
# WinSDK
pat = re.compile(r'Win(\d+)SDK_(\d.+)', re.IGNORECASE)
latest_sdk = (0, 0, 0)
self.sdk_version = ''
sdk_pid = ''
for pid in self.packages:
m = pat.match(pid)
if m is not None:
ver = tuple(map(int, m.group(2).split('.')))
if ver > latest_sdk:
self.sdk_version = m.group(2)
latest_sdk = ver
sdk_pid = pid
if not self.sdk_version:
raise KeyError('Failed to find SDK package')
# headers are in x86 package and arch specific package
for pf in self.packages[sdk_pid][0]['payloads']:
fname = pf['fileName'].split('\\')[-1]
if fname.startswith('Windows SDK Desktop Headers '):
q = fname[len('Windows SDK Desktop Headers '):]
if q.lower() == 'x86-x86_en-us.msi':
self.files_to_download.append(File(
pf, filename=f'{sdk_pid}_headers.msi'))
elif q.lower() == f'{arch}-x86_en-us.msi':
self.files_to_download.append(File(
pf, filename=f'{sdk_pid}_{arch}_headers.msi'))
elif fname == 'Windows SDK for Windows Store Apps Headers-x86_en-us.msi':
self.files_to_download.append(File(
pf, filename=f'{sdk_pid}_store_headers.msi'))
elif fname.startswith('Windows SDK Desktop Libs '):
q = fname[len('Windows SDK Desktop Libs '):]
if q == f'{arch}-x86_en-us.msi':
self.files_to_download.append(File(
pf, filename=f'{sdk_pid}_libs_x64.msi'))
elif fname == 'Windows SDK for Windows Store Apps Libs-x86_en-us.msi':
self.files_to_download.append(File(
pf, filename=f'{sdk_pid}_store_libs.msi'))
elif (fl := fname.lower()).endswith('.cab'):
self.cabinet_entries[fl] = File(pf, filename=fl)
# UCRT
for pf in self.packages[
'Microsoft.Windows.UniversalCRT.HeadersLibsSources.Msi'.lower()][0]['payloads']:
fname = pf['fileName'].split('\\')[-1]
if fname == 'Universal CRT Headers Libraries and Sources-x86_en-us.msi':
self.files_to_download.append(File(pf))
self.files_to_download[-1].filename = 'ucrt.msi'
elif (fl := fname.lower()).endswith('.cab'):
self.cabinet_entries[fl] = File(pf, filename=fl)
def download_item(dest_dir: str, file: File):
dest = os.path.join(dest_dir, file.filename)
m = hashlib.sha256()
with urlopen(file.url) as src, open(dest, 'wb') as d:
with memoryview(bytearray(shutil.COPY_BUFSIZE)) as buf:
while True:
n = src.readinto(buf)
if not n:
break
elif n < shutil.COPY_BUFSIZE:
with buf[:n] as smv:
d.write(smv)
m.update(smv)
else:
d.write(buf)
m.update(buf)
if m.hexdigest() != file.sha256:
raise SystemExit(f'The hash for {file.filename} does not match.'
f' {m.hexdigest()} != {file.sha256}')
def cabinets_in_msi(path):
raw = subprocess.check_output(['msiinfo', 'export', path, 'Media']).decode('utf-8')
return re.findall(r'\S+\.cab', raw)
def download(dest_dir, manifest_version=17, manifest_type='release', manifest_path='', crt_variant='desktop', arch='x86_64'):
if manifest_path:
manifest = open(manifest_path, 'rb').read()
else:
url = f'https://aka.ms/vs/{manifest_version}/{manifest_type}/channel'
print('Downloading top-level manifest from', url)
tm = json.loads(urlopen(url).read())
print('Got toplevel manifest for', (tm['info']['productDisplayVersion']))
for item in tm['channelItems']:
if item.get('type') == 'Manifest':
url = item['payloads'][0]['url']
print('Downloading actual manifest...')
manifest = urlopen(url).read()
pkgs = Packages(manifest, crt_variant, arch)
os.makedirs(dest_dir, exist_ok=True)
total = sum(x.size for x in pkgs.files_to_download)
print('Downloading', int(total/(1024*1024)), 'MB in', len(pkgs.files_to_download), 'files...')
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
for _ in executor.map(partial(download_item, dest_dir), pkgs.files_to_download):
pass
cabs = []
for x in os.listdir(dest_dir):
if x.lower().endswith('.msi'):
for cab in cabinets_in_msi(os.path.join(dest_dir, x)):
cabs.append(pkgs.cabinet_entries[cab])
total = sum(x.size for x in cabs)
print('Downloading', int(total/(1024*1024)), 'MB in', len(cabs), 'files...')
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
for _ in executor.map(partial(download_item, dest_dir), cabs):
pass
def merge_trees(src, dest):
if not os.path.isdir(src):
return
if not os.path.isdir(dest):
shutil.move(src, dest)
return
destnames = {n.lower():n for n in os.listdir(dest)}
for d in os.scandir(src):
n = d.name
srcname = os.path.join(src, n)
destname = os.path.join(dest, n)
if d.is_dir():
if os.path.isdir(destname):
merge_trees(srcname, destname)
elif n.lower() in destnames:
merge_trees(srcname, os.path.join(dest, destnames[n.lower()]))
else:
shutil.move(srcname, destname)
else:
shutil.move(srcname, destname)
def extract_msi(path, dest_dir):
print('Extracting', os.path.basename(path), '...')
with open(os.path.join(dest_dir, os.path.basename(path) + '.listing'), 'w') as log:
subprocess.check_call(['msiextract', '-C', dest_dir, path], stdout=log)
def extract_zipfile(zf, dest_dir):
tmp = os.path.join(dest_dir, 'extract')
os.mkdir(tmp)
for f in zf.infolist():
name = unquote(f.filename)
dest = os.path.join(dest_dir, name)
extracted = zf.extract(f, tmp)
os.makedirs(os.path.dirname(dest), exist_ok=True)
shutil.move(extracted, dest)
shutil.rmtree(tmp)
def extract_vsix(path, dest_dir):
print('Extracting', os.path.basename(path), '...')
with TemporaryDirectory(dir=dest_dir) as tdir, ZipFile(path, 'r') as zf:
extract_zipfile(zf, tdir)
contents = os.path.join(tdir, 'Contents')
merge_trees(contents, dest_dir)
names = zf.namelist()
with open(os.path.join(dest_dir, os.path.basename(path) + '.listing'), 'w') as ls:
ls.write('\n'.join(names))
def move_unpacked_trees(src_dir, dest_dir):
# CRT
crt_src = os.path.dirname(glob.glob(
os.path.join(src_dir, 'VC/Tools/MSVC/*/include'))[0])
crt_dest = os.path.join(dest_dir, 'crt')
os.makedirs(crt_dest)
merge_trees(os.path.join(crt_src, 'include'), os.path.join(crt_dest, 'include'))
merge_trees(os.path.join(crt_src, 'lib'), os.path.join(crt_dest, 'lib'))
merge_trees(os.path.join(crt_src, 'atlmfc', 'include'),
os.path.join(crt_dest, 'include'))
merge_trees(os.path.join(crt_src, 'atlmfc', 'lib'),
os.path.join(crt_dest, 'lib'))
# SDK
sdk_ver = glob.glob(os.path.join(src_dir, 'win11sdk_*_headers.msi.listing'))[0]
sdk_ver = sdk_ver.split('_')[1]
for x in glob.glob(os.path.join(src_dir, 'Program Files/Windows Kits/*/Include/*')):
if os.path.basename(x).startswith(sdk_ver):
sdk_ver = os.path.basename(x)
break
else:
raise SystemExit(f'Failed to find sdk_ver: {sdk_ver}')
sdk_include_src = glob.glob(os.path.join(
src_dir, f'Program Files/Windows Kits/*/Include/{sdk_ver}'))[0]
sdk_dest = os.path.join(dest_dir, 'sdk')
os.makedirs(sdk_dest)
merge_trees(sdk_include_src, os.path.join(sdk_dest, 'include'))
sdk_lib_src = glob.glob(os.path.join(
src_dir, f'Program Files/Windows Kits/*/Lib/{sdk_ver}'))[0]
merge_trees(sdk_lib_src, os.path.join(sdk_dest, 'lib'))
# UCRT
if os.path.exists(os.path.join(sdk_include_src, 'ucrt')):
return
ucrt_include_src = glob.glob(os.path.join(
src_dir, 'Program Files/Windows Kits/*/Include/*/ucrt'))[0]
merge_trees(ucrt_include_src, os.path.join(sdk_dest, 'include', 'ucrt'))
ucrt_lib_src = glob.glob(os.path.join(
src_dir, 'Program Files/Windows Kits/*/Lib/*/ucrt'))[0]
merge_trees(ucrt_lib_src, os.path.join(sdk_dest, 'lib', 'ucrt'))
def unpack(src_dir, dest_dir):
if os.path.exists(dest_dir):
shutil.rmtree(dest_dir)
extract_dir = os.path.join(dest_dir, 'extract')
os.makedirs(extract_dir)
for x in os.listdir(src_dir):
path = os.path.join(src_dir, x)
ext = os.path.splitext(x)[1].lower()
if ext =='.msi':
extract_msi(path, extract_dir)
elif ext == '.vsix':
extract_vsix(path, extract_dir)
elif ext == '.cab':
continue
else:
raise SystemExit(f'Unknown downloaded file type: {x}')
move_unpacked_trees(extract_dir, dest_dir)
shutil.rmtree(extract_dir)
def symlink_transformed(path, transform=str.lower):
base, name = os.path.split(path)
lname = transform(name)
if lname != name:
npath = os.path.join(base, lname)
if not os.path.lexists(npath):
os.symlink(name, npath)
def clone_tree(src_dir, dest_dir):
os.makedirs(dest_dir)
for dirpath, dirnames, filenames in os.walk(src_dir):
for d in dirnames:
path = os.path.join(dirpath, d)
rpath = os.path.relpath(path, src_dir)
dpath = os.path.join(dest_dir, rpath)
os.makedirs(dpath)
symlink_transformed(dpath)
for f in filenames:
if f.lower().endswith('.pdb'):
continue
path = os.path.join(dirpath, f)
rpath = os.path.relpath(path, src_dir)
dpath = os.path.join(dest_dir, rpath)
os.link(path, dpath)
symlink_transformed(dpath)
def files_in(path):
for dirpath, _, filenames in os.walk(path):
for f in filenames:
yield os.path.relpath(os.path.join(dirpath, f), path)
def create_include_symlinks(path, include_root, include_files):
' Create symlinks for include entries in header files whose case does not match '
with open(path, 'rb') as f:
src = f.read()
for m in re.finditer(rb'^#include\s+([<"])(.+?)[>"]', src, flags=re.M):
spec = m.group(2).decode().replace('\\', '/')
lspec = spec.lower()
if spec == lspec:
continue
is_local = m.group(1).decode() == '"'
found = ''
lmatches = []
for ir, specs in include_files.items():
if spec in specs:
found = ir
break
if lspec in specs:
lmatches.append(ir)
if found and (not is_local or found == include_root):
continue
if lmatches:
if is_local and include_root in lmatches:
fr = include_root
else:
fr = lmatches[0]
symlink_transformed(os.path.join(fr, lspec), lambda n: os.path.basename(spec))
def setup(splat_dir, root_dir, arch):
print('Creating symlinks...')
msarch = llvm_arch_to_ms_arch(arch)
if os.path.exists(root_dir):
shutil.rmtree(root_dir)
os.makedirs(root_dir)
# CRT
clone_tree(os.path.join(splat_dir, 'crt', 'include'), os.path.join(root_dir, 'crt', 'include'))
clone_tree(os.path.join(splat_dir, 'crt', 'lib', 'spectre', msarch), os.path.join(root_dir, 'crt', 'lib'))
# SDK
clone_tree(os.path.join(splat_dir, 'sdk', 'include'), os.path.join(root_dir, 'sdk', 'include'))
for x in glob.glob(os.path.join(splat_dir, 'sdk', 'lib', '*', msarch)):
clone_tree(x, os.path.join(root_dir, 'sdk', 'lib', os.path.basename(os.path.dirname(x))))
include_roots = [x for x in glob.glob(os.path.join(root_dir, 'sdk', 'include', '*')) if os.path.isdir(x)]
include_roots.append(os.path.join(root_dir, 'crt', 'include'))
include_files = {x:set(files_in(x)) for x in include_roots}
for ir, files in include_files.items():
files_to_check = []
for relpath in files:
path = os.path.join(ir, relpath)
if not os.path.islink(path):
files_to_check.append(path)
for path in files_to_check:
create_include_symlinks(path, ir, include_files)
def main(args=sys.argv[1:]):
stages = ('download', 'unpack', 'setup')
p = argparse.ArgumentParser(
description='Setup the headers and libraries for cross-compilation of windows binaries')
p.add_argument(
'stages', metavar='STAGES', nargs='*', help=(
f'The stages to run by default all stages are run. Stages are: {" ".join(stages)}'))
p.add_argument(
'--manifest-version', default=17, type=int, help='The manifest version to use to find the packages to install')
p.add_argument(
'--manifest-path', default='', help='Path to a local manifest file to use. Causes --manifest-version to be ignored.')
p.add_argument(
'--crt-variant', default='desktop', choices=('desktop', 'store', 'onecore'), help='The type of CRT to download')
p.add_argument(
'--arch', default='x86_64', choices=('x86_64', 'aarch64'), help='The architecture to install')
p.add_argument('--dest', default='.', help='The directory to install into')
args = p.parse_args(args)
if args.dest == '.':
args.dest = os.getcwd()
stages = args.stages or stages
dl_dir = os.path.join(args.dest, 'dl')
splat_dir = os.path.join(args.dest, 'splat')
root_dir = os.path.join(args.dest, 'root')
for stage in stages:
if stage == 'download':
download(dl_dir, manifest_version=args.manifest_version, manifest_path=args.manifest_path, crt_variant=args.crt_variant, arch=args.arch)
elif stage == 'unpack':
unpack(dl_dir, splat_dir)
elif stage == 'setup':
setup(splat_dir, root_dir, args.arch)
else:
raise SystemExit(f'Unknown stage: {stage}')
if __name__ == '__main__':
pprint
main()