mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-05-31 12:14:15 -04:00
457 lines
18 KiB
Python
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()
|