diff --git a/ue4docker/build.py b/ue4docker/build.py index 97060c36..c8870df7 100644 --- a/ue4docker/build.py +++ b/ue4docker/build.py @@ -112,11 +112,12 @@ def build(): # Provide the user with feedback so they are aware of the Windows-specific values being used logger.info('WINDOWS CONTAINER SETTINGS', False) logger.info('Isolation mode: {}'.format(config.isolation), False) - logger.info('Base OS image tag: {} (host OS is {})'.format(config.basetag, WindowsUtils.systemStringShort()), False) + logger.info('Base OS image tag: {}'.format(config.basetag), False) + logger.info('Host OS: {}'.format(WindowsUtils.systemString()), False) logger.info('Memory limit: {}'.format('No limit' if config.memLimit is None else '{:.2f}GB'.format(config.memLimit)), False) logger.info('Detected max image size: {:.0f}GB'.format(DockerUtils.maxsize()), False) logger.info('Directory to copy DLLs from: {}\n'.format(config.dlldir), False) - + # Verify that the specified base image tag is not a release that has reached End Of Life (EOL) if WindowsUtils.isEndOfLifeWindowsVersion(config.basetag) == True: logger.error('Error: detected EOL base OS image tag: {}'.format(config.basetag), False) @@ -124,10 +125,10 @@ def build(): logger.error('Microsoft no longer supports or maintains container base images for it.', False) logger.error('You will need to use a base image tag for a supported version of Windows.', False) sys.exit(1) - + # Verify that the host OS is not a release that is blacklisted due to critical bugs if config.ignoreBlacklist == False and WindowsUtils.isBlacklistedWindowsVersion() == True: - logger.error('Error: detected blacklisted host OS version: {}'.format(WindowsUtils.systemStringShort()), False) + logger.error('Error: detected blacklisted host OS version: {}'.format(WindowsUtils.systemString()), False) logger.error('', False) logger.error('This version of Windows contains one or more critical bugs that', False) logger.error('render it incapable of successfully building UE4 container images.', False) @@ -136,12 +137,12 @@ def build(): logger.error('For more information, see:', False) logger.error('https://unrealcontainers.com/docs/concepts/windows-containers', False) sys.exit(1) - + # Verify that the user is not attempting to build images with a newer kernel version than the host OS if WindowsUtils.isNewerBaseTag(config.hostBasetag, config.basetag): logger.error('Error: cannot build container images with a newer kernel version than that of the host OS!') sys.exit(1) - + # Check if the user is building a different kernel version to the host OS but is still copying DLLs from System32 differentKernels = WindowsUtils.isInsiderPreview() or config.basetag != config.hostBasetag if config.pullPrerequisites == False and differentKernels == True and config.dlldir == config.defaultDllDir: @@ -239,7 +240,7 @@ def build(): # (This is the only image that does not use any user-supplied tag suffix, since the tag always reflects any customisations) prereqsArgs = ['--build-arg', 'BASEIMAGE=' + config.baseImage] if config.containerPlatform == 'windows': - prereqsArgs = prereqsArgs + ['--build-arg', 'HOST_VERSION=' + WindowsUtils.getWindowsBuild()] + prereqsArgs = prereqsArgs + ['--build-arg', 'HOST_BUILD=' + str(WindowsUtils.getWindowsBuild())] # Build or pull the UE4 build prerequisites image (don't pull it if we're copying Dockerfiles to an output directory) if config.layoutDir is None and config.pullPrerequisites == True: diff --git a/ue4docker/diagnostics/diagnostic_maxsize.py b/ue4docker/diagnostics/diagnostic_maxsize.py index c7e30fe5..0b831eb7 100644 --- a/ue4docker/diagnostics/diagnostic_maxsize.py +++ b/ue4docker/diagnostics/diagnostic_maxsize.py @@ -47,7 +47,7 @@ def run(self, logger, args=[]): return False # Verify that we are running Windows Server or Windows 10 version 1903 or newer - if WindowsUtils.getWindowsVersion()['patch'] < 18362: + if WindowsUtils.getWindowsBuild() < 18362: logger.info('[maxsize] This diagnostic only applies to Windows Server and Windows 10 version 1903 and newer.', False) return True diff --git a/ue4docker/dockerfiles/ue4-build-prerequisites/windows/Dockerfile b/ue4docker/dockerfiles/ue4-build-prerequisites/windows/Dockerfile index f8fa73c4..e3e51967 100644 --- a/ue4docker/dockerfiles/ue4-build-prerequisites/windows/Dockerfile +++ b/ue4docker/dockerfiles/ue4-build-prerequisites/windows/Dockerfile @@ -20,11 +20,11 @@ RUN choco install -y 7zip curl && choco install -y python --version=3.7.5 COPY *.dll C:\GatheredDlls\ # Verify that the DLL files copied from the host can be loaded by the container OS -ARG HOST_VERSION +ARG HOST_BUILD RUN pip install pywin32 COPY copy.py verify-host-dlls.py C:\ RUN C:\copy.py "C:\GatheredDlls\*.dll" C:\Windows\System32\ -RUN python C:\verify-host-dlls.py %HOST_VERSION% C:\GatheredDlls +RUN python C:\verify-host-dlls.py %HOST_BUILD% C:\GatheredDlls # Gather the required DirectX runtime files, since Windows Server Core does not include them RUN curl --progress-bar -L "https://download.microsoft.com/download/8/4/A/84A35BF1-DAFE-4AE8-82AF-AD2AE20B6B14/directx_Jun2010_redist.exe" --output %TEMP%\directx_redist.exe diff --git a/ue4docker/dockerfiles/ue4-build-prerequisites/windows/verify-host-dlls.py b/ue4docker/dockerfiles/ue4-build-prerequisites/windows/verify-host-dlls.py index f58c84a2..757a8442 100644 --- a/ue4docker/dockerfiles/ue4-build-prerequisites/windows/verify-host-dlls.py +++ b/ue4docker/dockerfiles/ue4-build-prerequisites/windows/verify-host-dlls.py @@ -1,4 +1,5 @@ -import glob, os, platform, sys, win32api, winreg +import glob, os, sys, win32api + # Adapted from the code in this SO answer: def getDllVersion(dllPath): @@ -10,20 +11,10 @@ def getDllVersion(dllPath): info['FileVersionLS'] % 65536 ) -def getVersionRegKey(subkey): - key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion') - value = winreg.QueryValueEx(key, subkey) - winreg.CloseKey(key) - return value[0] - -def getOsVersion(): - version = platform.win32_ver()[1] - build = getVersionRegKey('BuildLabEx').split('.')[1] - return '{}.{}'.format(version, build) # Print the host and container OS build numbers print('Host OS build number: {}'.format(sys.argv[1])) -print('Container OS build number: {}'.format(getOsVersion())) +print('Container OS build number: {}'.format(sys.getwindowsversion().build)) sys.stdout.flush() # Verify each DLL file in the directory specified by our command-line argument diff --git a/ue4docker/info.py b/ue4docker/info.py index 2db9935f..03f95df3 100644 --- a/ue4docker/info.py +++ b/ue4docker/info.py @@ -4,7 +4,7 @@ def _osName(dockerInfo): if platform.system() == 'Windows': - return WindowsUtils.systemStringLong() + return WindowsUtils.systemString() elif platform.system() == 'Darwin': return DarwinUtils.systemString() else: diff --git a/ue4docker/infrastructure/BuildConfiguration.py b/ue4docker/infrastructure/BuildConfiguration.py index 29284e8a..ff9d61b1 100644 --- a/ue4docker/infrastructure/BuildConfiguration.py +++ b/ue4docker/infrastructure/BuildConfiguration.py @@ -233,15 +233,15 @@ def _generateWindowsConfig(self): self.basetag = self.args.basetag if self.args.basetag is not None else self.hostBasetag self.baseImage = 'mcr.microsoft.com/windows/servercore:' + self.basetag self.prereqsTag = self.basetag - + # Verify that any user-specified base tag is valid if WindowsUtils.isValidBaseTag(self.basetag) == False: raise RuntimeError('unrecognised Windows Server Core base image tag "{}", supported tags are {}'.format(self.basetag, WindowsUtils.getValidBaseTags())) - + # Verify that any user-specified tag suffix does not collide with our base tags if WindowsUtils.isValidBaseTag(self.suffix) == True: raise RuntimeError('tag suffix cannot be any of the Windows Server Core base image tags: {}'.format(WindowsUtils.getValidBaseTags())) - + # If the user has explicitly specified an isolation mode then use it, otherwise auto-detect if self.args.isolation is not None: self.isolation = self.args.isolation @@ -249,7 +249,7 @@ def _generateWindowsConfig(self): # If we are able to use process isolation mode then use it, otherwise fallback to the Docker daemon's default isolation mode differentKernels = WindowsUtils.isInsiderPreview() or self.basetag != self.hostBasetag - hostSupportsProcess = WindowsUtils.isWindowsServer() or int(self.hostRelease) >= 1809 + hostSupportsProcess = WindowsUtils.supportsProcessIsolation() dockerSupportsProcess = parse_version(DockerUtils.version()['Version']) >= parse_version('18.09.0') if not differentKernels and hostSupportsProcess and dockerSupportsProcess: self.isolation = 'process' diff --git a/ue4docker/infrastructure/WindowsUtils.py b/ue4docker/infrastructure/WindowsUtils.py index aef8175d..33d73006 100644 --- a/ue4docker/infrastructure/WindowsUtils.py +++ b/ue4docker/infrastructure/WindowsUtils.py @@ -1,111 +1,90 @@ from .DockerUtils import DockerUtils -from .PackageUtils import PackageUtils from pkg_resources import parse_version -import os, platform +import platform, sys if platform.system() == 'Windows': import winreg -# Import the `semver` package even when the conflicting `node-semver` package is present -semver = PackageUtils.importFile('semver', os.path.join(PackageUtils.getPackageLocation('semver'), 'semver.py')) - class WindowsUtils(object): - + # The latest Windows build version we recognise as a non-Insider build _latestReleaseBuild = 19042 - + # The list of Windows Server Core base image tags that we recognise, in ascending version number order _validTags = ['ltsc2016', '1709', '1803', 'ltsc2019', '1903', '1909', '2004', '20H2'] - + # The list of Windows Server and Windows 10 host OS releases that are blacklisted due to critical bugs # (See: ) _blacklistedReleases = ['1903', '1909'] - + # The list of Windows Server Core container image releases that are unsupported due to having reached EOL _eolReleases = ['1709', '1803', '1903'] - + @staticmethod - def _getVersionRegKey(subkey): + def _getVersionRegKey(subkey : str) -> str: ''' Retrieves the specified Windows version key from the registry + + @raises FileNotFoundError if registry key doesn't exist ''' key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion') value = winreg.QueryValueEx(key, subkey) winreg.CloseKey(key) return value[0] - + @staticmethod - def requiredHostDlls(basetag): + def requiredHostDlls(basetag: str) -> [str]: ''' Returns the list of required host DLL files for the specified container image base tag ''' - + # `ddraw.dll` is only required under Windows Server 2016 version 1607 common = ['dsound.dll', 'opengl32.dll', 'glu32.dll'] return ['ddraw.dll'] + common if basetag == 'ltsc2016' else common - + @staticmethod - def requiredSizeLimit(): + def requiredSizeLimit() -> float: ''' Returns the minimum required image size limit (in GB) for Windows containers ''' return 400.0 - + @staticmethod - def minimumRequiredVersion(): + def minimumRequiredBuild() -> int: ''' Returns the minimum required version of Windows 10 / Windows Server, which is release 1607 - + (1607 is the first build to support Windows containers, as per: ) ''' - return '10.0.14393' - - @staticmethod - def systemStringShort(): - ''' - Generates a concise human-readable version string for the Windows host system - ''' - return 'Windows {} version {}'.format( - 'Server' if WindowsUtils.isWindowsServer() else '10', - WindowsUtils.getWindowsRelease() - ) - + return 14393 + @staticmethod - def systemStringLong(): + def systemString() -> str: ''' Generates a verbose human-readable version string for the Windows host system ''' - return '{} Version {} (OS Build {}.{})'.format( + return '{} Version {} (Build {}.{})'.format( WindowsUtils._getVersionRegKey('ProductName'), WindowsUtils.getWindowsRelease(), - WindowsUtils.getWindowsVersion()['patch'], + WindowsUtils.getWindowsBuild(), WindowsUtils._getVersionRegKey('UBR') ) - - @staticmethod - def getWindowsVersion(): - ''' - Returns the version information for the Windows host system as a semver instance - ''' - return semver.parse(platform.win32_ver()[1]) - + @staticmethod - def getWindowsRelease(): + def getWindowsRelease() -> str: ''' Determines the Windows 10 / Windows Server release (1607, 1709, 1803, etc.) of the Windows host system ''' return WindowsUtils._getVersionRegKey('ReleaseId') - + @staticmethod - def getWindowsBuild(): + def getWindowsBuild() -> int: ''' - Returns the full Windows version number as a string, including the build number + Returns build number for the Windows host system ''' - version = platform.win32_ver()[1] - build = WindowsUtils._getVersionRegKey('BuildLabEx').split('.')[1] - return '{}.{}'.format(version, build) - + return sys.getwindowsversion().build + @staticmethod def isBlacklistedWindowsVersion(release=None): ''' @@ -115,7 +94,7 @@ def isBlacklistedWindowsVersion(release=None): dockerVersion = parse_version(DockerUtils.version()['Version']) release = WindowsUtils.getWindowsRelease() if release is None else release return release in WindowsUtils._blacklistedReleases and dockerVersion < parse_version('19.03.6') - + @staticmethod def isEndOfLifeWindowsVersion(release=None): ''' @@ -124,39 +103,32 @@ def isEndOfLifeWindowsVersion(release=None): ''' release = WindowsUtils.getWindowsRelease() if release is None else release return release in WindowsUtils._eolReleases - - @staticmethod - def isSupportedWindowsVersion(): - ''' - Verifies that the Windows host system meets our minimum Windows version requirements - ''' - return semver.compare(platform.win32_ver()[1], WindowsUtils.minimumRequiredVersion()) >= 0 - + @staticmethod - def isWindowsServer(): + def isWindowsServer() -> bool: ''' Determines if the Windows host system is Windows Server ''' + # TODO: Replace this with something more reliable return 'Windows Server' in WindowsUtils._getVersionRegKey('ProductName') - + @staticmethod - def isInsiderPreview(): + def isInsiderPreview() -> bool: ''' Determines if the Windows host system is a Windows Insider preview build ''' - version = WindowsUtils.getWindowsVersion() - return version['patch'] > WindowsUtils._latestReleaseBuild - + return WindowsUtils.getWindowsBuild() > WindowsUtils._latestReleaseBuild + @staticmethod - def getReleaseBaseTag(release): + def getReleaseBaseTag(release: str) -> str: ''' Retrieves the tag for the Windows Server Core base image matching the specified Windows 10 / Windows Server release ''' - + # For Windows Insider preview builds, build the latest release tag if WindowsUtils.isInsiderPreview(): return WindowsUtils._validTags[-1] - + # This lookup table is based on the list of valid tags from return { '1709': '1709', @@ -168,24 +140,33 @@ def getReleaseBaseTag(release): '2009': '20H2', '20H2': '20H2' }.get(release, 'ltsc2016') - + @staticmethod - def getValidBaseTags(): + def getValidBaseTags() -> [str]: ''' Returns the list of valid tags for the Windows Server Core base image, in ascending chronological release order ''' return WindowsUtils._validTags - + @staticmethod - def isValidBaseTag(tag): + def isValidBaseTag(tag: str) -> bool: ''' Determines if the specified tag is a valid Windows Server Core base image tag ''' return tag in WindowsUtils._validTags - + @staticmethod - def isNewerBaseTag(older, newer): + def isNewerBaseTag(older: str, newer: str) -> bool: ''' Determines if the base tag `newer` is chronologically newer than the base tag `older` ''' return WindowsUtils._validTags.index(newer) > WindowsUtils._validTags.index(older) + + @staticmethod + def supportsProcessIsolation() -> bool: + ''' + Determines whether the Windows host system supports process isolation for containers + + @see https://docs.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/hyperv-container + ''' + return WindowsUtils.isWindowsServer() or WindowsUtils.getWindowsBuild() >= 17763 diff --git a/ue4docker/main.py b/ue4docker/main.py index 75f47b2c..4a4bc3d7 100644 --- a/ue4docker/main.py +++ b/ue4docker/main.py @@ -32,8 +32,11 @@ def main(): _exitWithError('Error: could not detect Docker daemon version. Please ensure Docker is installed.\n\nError details: {}'.format(error)) # Under Windows, verify that the host is a supported version - if platform.system() == 'Windows' and WindowsUtils.isSupportedWindowsVersion() == False: - _exitWithError('Error: the detected version of Windows ({}) is not supported. Windows 10 / Windows Server version 1607 or newer is required.'.format(platform.win32_ver()[1])) + if platform.system() == 'Windows': + host_build = WindowsUtils.getWindowsBuild() + min_build = WindowsUtils.minimumRequiredBuild() + if host_build < min_build: + _exitWithError('Error: the detected build of Windows ({}) is not supported. {} or newer is required.'.format(host_build, min_build)) # Under macOS, verify that the host is a supported version if platform.system() == 'Darwin' and DarwinUtils.isSupportedMacOsVersion() == False: