diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 2ae6c7b3230..ae3eb29ddc1 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -46,7 +46,7 @@ env: jobs: build: name: Windows Build - runs-on: windows-2019 + runs-on: windows-2022 steps: - uses: actions/checkout@v3 if: github.event_name != 'pull_request_target' diff --git a/README.md b/README.md index 2c3a56144f6..14f087aaf47 100644 --- a/README.md +++ b/README.md @@ -41,14 +41,21 @@ manually, try the [manual build setup][manual-build]. ### Windows - - Download and run [`rustup-init.exe`](https://win.rustup.rs/) then follow the onscreen instructions. + - Download and run [`rustup-init.exe`](https://win.rustup.rs/) + - Make sure to select *Quick install via the Visual Studio Community + installer* or otherwise install Visual Studio 2022. + - In the *Visual Studio Installer* ensure the following components are installed for Visual Studio 2022: + - **Windows 10 SDK (10.0.19041.0)** (`Microsoft.VisualStudio.Component.Windows10SDK.19041`) + - **MSVC v143 - VS 2022 C++ x64/x86 build tools (Latest)** (`Microsoft.VisualStudio.Component.VC.Tools.x86.x64`) + - **C++ ATL for latest v143 build tools (x86 & x64)** (`Microsoft.VisualStudio.Component.VC.ATL`) + - **C++ MFC for latest v143 build tools (x86 & x64)** (`Microsoft.VisualStudio.Component.VC.ATLMFC`) - Install [chocolatey](https://chocolatey.org/) - - Install [Python 3.11](https://apps.microsoft.com/detail/9NRWMJP3717K?hl=en-US&gl=US) + - Install [Python 3.11](https://www.python.org/downloads/windows/) - Run `mach bootstrap` - - *This will install CMake, Git, Ninja, and the Visual Studio 2019 Build Tools - via choco in an Administrator console. It can take quite a while.* - - *If you already have Visual Studio 2019 installed, this may not install all necessary components. - Please follow the Visual Studio 2019 installation instructions in the [manual setup][manual-build].* + - *This will install CMake, Git, Ninja, via choco in an + + *This will install CMake, Git, and Ninja via choco in an + Administrator console. Allow the scripts to run and once + the operation finishes, close the new console.* - Run `refreshenv` See also [Windows Troubleshooting Tips][windows-tips]. diff --git a/python/servo/build_commands.py b/python/servo/build_commands.py index afed8dbc5c2..220b1e0e9bb 100644 --- a/python/servo/build_commands.py +++ b/python/servo/build_commands.py @@ -18,7 +18,7 @@ import sys import urllib from time import time -from typing import Dict +from typing import Dict, Optional import zipfile import notifypy @@ -32,10 +32,10 @@ from mach.registrar import Registrar import servo.platform import servo.util +import servo.visual_studio from servo.command_base import BuildType, CommandBase, call, check_call from servo.gstreamer import windows_dlls, windows_plugins, macos_plugins -from python.servo.visual_studio import find_msvc_redist_dirs @CommandProvider @@ -438,29 +438,36 @@ def package_gstreamer_dlls(servo_exe_dir: str, target: str): return not missing -def package_msvc_dlls(servo_exe_dir, target): - msvc_deps = [ - "msvcp140.dll", - "vcruntime140.dll", - ] - if "aarch64" not in target != "aarch64": - msvc_deps += ["api-ms-win-crt-runtime-l1-1-0.dll"] +def package_msvc_dlls(servo_exe_dir: str, target: str): + def copy_file(dll_path: Optional[str]) -> bool: + if not dll_path or not os.path.exists(dll_path): + print(f"WARNING: Could not find DLL at {dll_path}", file=sys.stderr) + return False + servo_dir_dll = path.join(servo_exe_dir, os.path.basename(dll_path)) + # Avoid permission denied error when overwriting DLLs. + if os.path.isfile(servo_dir_dll): + os.chmod(servo_dir_dll, stat.S_IWUSR) + print(f" • Copying {dll_path}") + shutil.copy(dll_path, servo_exe_dir) + return True - missing = [] - redist_dirs = find_msvc_redist_dirs(target) - for msvc_dll in msvc_deps: - for dll_dir in redist_dirs: - dll = path.join(dll_dir, msvc_dll) - servo_dir_dll = path.join(servo_exe_dir, msvc_dll) - if os.path.isfile(dll): - if os.path.isfile(servo_dir_dll): - # avoid permission denied error when overwrite dll in servo build directory - os.chmod(servo_dir_dll, stat.S_IWUSR) - shutil.copy(dll, servo_exe_dir) - break - else: - missing += [msvc_dll] + vs_platform = { + "x86_64": "x64", + "i686": "x86", + "aarch64": "arm64", + }[target.split('-')[0]] - for msvc_dll in missing: - print(f"Could not find DLL dependency: {msvc_dll}") - return not missing + for msvc_redist_dir in servo.visual_studio.find_msvc_redist_dirs(vs_platform): + if copy_file(os.path.join(msvc_redist_dir, "msvcp140.dll")) and \ + copy_file(os.path.join(msvc_redist_dir, "vcruntime140.dll")): + break + + # Different SDKs install the file into different directory structures within the + # Windows SDK installation directory, so use a glob to search for a path like + # "**\x64\api-ms-win-crt-runtime-l1-1-0.dll". + windows_sdk_dir = servo.visual_studio.find_windows_sdk_installation_path() + dll_name = "api-ms-win-crt-runtime-l1-1-0.dll" + file_to_copy = next(pathlib.Path(windows_sdk_dir).rglob(os.path.join("**", vs_platform, dll_name))) + copy_file(file_to_copy) + + return True diff --git a/python/servo/visual_studio.py b/python/servo/visual_studio.py index 4149076a396..c63ffe2f54c 100644 --- a/python/servo/visual_studio.py +++ b/python/servo/visual_studio.py @@ -12,80 +12,144 @@ import json import os import subprocess import sys -from typing import List +from typing import Generator, List, Optional -import servo.platform +COMPATIBLE_MSVC_VERSIONS = { + "2019": "16.0", + "2022": "17.0", +} +MSVC_REDIST_VERSIONS = ["VC141", "VC142", "VC143", "VC150", "VC160"] + +PROGRAM_FILES = os.environ.get("PROGRAMFILES", "C:\\Program Files") +PROGRAM_FILES_X86 = os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)") -@dataclasses.dataclass(kw_only=True) +@dataclasses.dataclass(frozen=True, kw_only=True) class VisualStudioInstallation: version_number: str installation_path: str vc_install_path: str + def __lt__(self, other): + return self.version_number < other.version_number -def find_highest_msvc_version_ext(): + +def find_vswhere(): + for path in [PROGRAM_FILES, PROGRAM_FILES_X86]: + if not path: + continue + vswhere = os.path.join(path, 'Microsoft Visual Studio', 'Installer', 'vswhere.exe') + if os.path.exists(vswhere): + return vswhere + return None + + +def find_compatible_msvc_with_vswhere() -> Generator[VisualStudioInstallation, None, None]: """Try to find the MSVC installation with the `vswhere.exe` tool. The results are sorted with newer versions first.""" - def vswhere(args): - program_files = (os.environ.get('PROGRAMFILES(X86)') - or os.environ.get('PROGRAMFILES')) + + vswhere = find_vswhere() + if not vswhere: + return + + output = subprocess.check_output([ + vswhere, + '-format', 'json', + '-products', '*', + '-requires', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', + '-requires', 'Microsoft.VisualStudio.Component.Windows10SDK' + ]).decode(errors='ignore') + + for install in json.loads(output): + installed_version = f"{install['installationVersion'].split('.')[0]}.0" + if installed_version not in COMPATIBLE_MSVC_VERSIONS.values(): + continue + installation_path = install['installationPath'] + yield VisualStudioInstallation( + version_number=installed_version, + installation_path=installation_path, + vc_install_path=os.path.join(installation_path, "VC") + ) + + +def find_compatible_msvc_with_path() -> Generator[VisualStudioInstallation, None, None]: + for program_files in [PROGRAM_FILES, PROGRAM_FILES_X86]: if not program_files: - return [] - vswhere = os.path.join(program_files, 'Microsoft Visual Studio', 'Installer', 'vswhere.exe') - if not os.path.exists(vswhere): - return [] - output = subprocess.check_output([vswhere, '-format', 'json'] + args).decode(errors='ignore') - return json.loads(output) - - for install in vswhere(['-products', '*', - '-requires', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', - '-requires', 'Microsoft.VisualStudio.Component.Windows10SDK']): - version = install['installationVersion'].split('.')[0] + '.0' - yield (install['installationPath'], version) + continue + for (version, version_number) in COMPATIBLE_MSVC_VERSIONS.items(): + for edition in ["Enterprise", "Professional", "Community", "BuildTools"]: + installation_path = os.path.join(program_files, "Microsoft Visual Studio", version, edition) + if os.path.exists(installation_path): + yield VisualStudioInstallation( + version_number=version_number, + installation_path=installation_path, + vc_install_path=os.path.join(installation_path, "VC") + ) -def find_highest_msvc_version(): - prog_files = os.environ.get("ProgramFiles(x86)") - - # TODO(mrobinson): Add support for Visual Studio 2022. - vs_versions = { - "2019": "16.0", - } - - for (version, version_number) in vs_versions.items(): - for edition in ["Enterprise", "Professional", "Community", "BuildTools"]: - vsinstalldir = os.path.join(prog_files, "Microsoft Visual Studio", version, edition) - if os.path.exists(vsinstalldir): - return (vsinstalldir, version_number) - - versions = sorted(find_highest_msvc_version_ext(), key=lambda tup: float(tup[1])) - if not versions: - print("Can't find a Visual Studio installation. " - "Please set the VSINSTALLDIR and VisualStudioVersion environment variables") - sys.exit(1) - return versions[0] - - -def find_msvc() -> VisualStudioInstallation: - vsinstalldir = os.environ.get('VSINSTALLDIR') +def find_compatible_msvc_with_environment_variables() -> Optional[VisualStudioInstallation]: + installation_path = os.environ.get('VSINSTALLDIR') version_number = os.environ.get('VisualStudioVersion') - if not vsinstalldir or not version_number: - (vsinstalldir, version_number) = find_highest_msvc_version() - - vc_install_path = os.environ.get("VCINSTALLDIR", os.path.join(vsinstalldir, "VC")) - if not os.path.exists(vc_install_path): - print(f"Can't find Visual C++ {version_number} installation at {vc_install_path}") - sys.exit(1) - + if not installation_path or not version_number: + return None + vc_install_path = os.environ.get("VCINSTALLDIR", os.path.join(installation_path, "VC")) + if not os.path.exists(installation_path) or not os.path.exists(vc_install_path): + return None return VisualStudioInstallation( version_number=version_number, - installation_path=vsinstalldir, + installation_path=installation_path, vc_install_path=vc_install_path, ) -def find_windows_sdk_installation_path(vs_platform: str) -> str: +def find_msvc_installations() -> List[VisualStudioInstallation]: + # First try to find Visual Studio via `vswhere.exe` and in well-known paths. + installations = list(find_compatible_msvc_with_vswhere()) + installations.extend(find_compatible_msvc_with_path()) + if installations: + return sorted(set(installations), reverse=True) + + # Fall back to using the environment variables, which could theoretically + # point to a version of Visual Studio that is unsupported. + installation = find_compatible_msvc_with_environment_variables() + if installation: + return [installation] + + raise Exception("Can't find a Visual Studio installation. " + "Please set the VSINSTALLDIR and VisualStudioVersion environment variables") + + +def find_msvc_redist_dirs(vs_platform: str) -> Generator[str, None, None]: + installations = sorted(set(list(find_msvc_installations())), reverse=True) + + tried = [] + for installation in installations: + redist_dir = os.path.join(installation.vc_install_path, "Redist", "MSVC") + if not os.path.isdir(redist_dir): + tried.append(redist_dir) + continue + + for subdirectory in os.listdir(redist_dir)[::-1]: + redist_path = os.path.join(redist_dir, subdirectory) + for redist_version in MSVC_REDIST_VERSIONS: + # there are two possible paths + # `x64\Microsoft.VC*.CRT` or `onecore\x64\Microsoft.VC*.CRT` + path1 = os.path.join(vs_platform, "Microsoft.{}.CRT".format(redist_version)) + path2 = os.path.join("onecore", vs_platform, "Microsoft.{}.CRT".format(redist_version)) + for path in [path1, path2]: + path = os.path.join(redist_path, path) + if os.path.isdir(path): + yield path + else: + tried.append(path) + + print("Couldn't locate MSVC redistributable directory. Tried:", file=sys.stderr) + for path in tried: + print(f" * {path}", file=sys.stderr) + raise Exception("Can't find a MSVC redistributatable directory.") + + +def find_windows_sdk_installation_path() -> str: """Try to find the Windows SDK installation path using the Windows registry. Raises an Exception if the path cannot be found in the registry.""" @@ -98,52 +162,6 @@ def find_windows_sdk_installation_path(vs_platform: str) -> str: key_path = r'SOFTWARE\Wow6432Node\Microsoft\Microsoft SDKs\Windows\v10.0' try: with winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, key_path) as key: - path = str(winreg.QueryValueEx(key, "InstallationFolder")[0]) - return os.path.join(path, "Redist", "ucrt", "DLLs", vs_platform) + return str(winreg.QueryValueEx(key, "InstallationFolder")[0]) except FileNotFoundError: raise Exception(f"Couldn't find Windows SDK installation path in registry at path ({key_path})") - - -def find_msvc_redist_dirs(target: str) -> List[str]: - assert 'windows' in servo.platform.host_triple() - - installation = find_msvc() - msvc_redist_dir = None - vs_platforms = { - "x86_64": "x64", - "i686": "x86", - "aarch64": "arm64", - } - target_arch = target.split('-')[0] - vs_platform = vs_platforms[target_arch] - - redist_dir = os.path.join(installation.vc_install_path, "Redist", "MSVC") - if not os.path.isdir(redist_dir): - raise Exception(f"Couldn't locate MSVC redistributable directory {redist_dir}") - - for p in os.listdir(redist_dir)[::-1]: - redist_path = os.path.join(redist_dir, p) - for v in ["VC141", "VC142", "VC150", "VC160"]: - # there are two possible paths - # `x64\Microsoft.VC*.CRT` or `onecore\x64\Microsoft.VC*.CRT` - redist1 = os.path.join(redist_path, vs_platform, "Microsoft.{}.CRT".format(v)) - redist2 = os.path.join(redist_path, "onecore", vs_platform, "Microsoft.{}.CRT".format(v)) - if os.path.isdir(redist1): - msvc_redist_dir = redist1 - break - elif os.path.isdir(redist2): - msvc_redist_dir = redist2 - break - if msvc_redist_dir: - break - - if not msvc_redist_dir: - print("Couldn't locate MSVC redistributable directory") - sys.exit(1) - - redist_dirs = [ - msvc_redist_dir, - find_windows_sdk_installation_path(vs_platform) - ] - - return redist_dirs diff --git a/support/windows/chocolatey.config b/support/windows/chocolatey.config index 53047163304..2ff6d30814f 100644 --- a/support/windows/chocolatey.config +++ b/support/windows/chocolatey.config @@ -1,9 +1,7 @@ - -