diff --git a/ue4docker/build.py b/ue4docker/build.py index 359d3c90..c9733d7a 100644 --- a/ue4docker/build.py +++ b/ue4docker/build.py @@ -119,16 +119,8 @@ def build(): logger.info('Detected max image size: {:.0f}GB'.format(DockerUtils.maxsize()), False) logger.info('Visual Studio: {}'.format(config.visualStudio), False) - # Verify that the specified base image tag is not a release that has reached End Of Life (EOL) - if not config.ignoreEOL and WindowsUtils.isEndOfLifeWindowsVersion(config.basetag): - logger.error('Error: detected EOL base OS image tag: {}'.format(config.basetag), False) - logger.error('This version of Windows has reached End Of Life (EOL), which means', False) - 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: + if config.ignoreBlacklist == False and WindowsUtils.isBlacklistedWindowsHost() == True: 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) @@ -140,9 +132,12 @@ def build(): 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): + newer_check = WindowsUtils.isNewerBaseTag(config.hostBasetag, config.basetag) + if newer_check: logger.error('Error: cannot build container images with a newer kernel version than that of the host OS!') sys.exit(1) + elif newer_check is None: + logger.info('Warning: unable to determine whether host system is new enough to use specified base tag') # Ensure the Docker daemon is configured correctly requiredLimit = WindowsUtils.requiredSizeLimit() diff --git a/ue4docker/diagnostics/base.py b/ue4docker/diagnostics/base.py index d974118f..6575b0c3 100644 --- a/ue4docker/diagnostics/base.py +++ b/ue4docker/diagnostics/base.py @@ -61,8 +61,12 @@ def _generateWindowsBuildArgs(self, logger, basetagOverride=None, isolationOverr # Determine the appropriate container image base tag for the host system release unless the user specified a base tag buildArgs = [] - defaultBaseTag = WindowsUtils.getReleaseBaseTag(WindowsUtils.getWindowsRelease()) - baseTag = basetagOverride if basetagOverride is not None else defaultBaseTag + hostBaseTag = WindowsUtils.getHostBaseTag() + baseTag = basetagOverride if basetagOverride is not None else hostBaseTag + + if baseTag is None: + raise RuntimeError('unable to determine Windows Server Core base image tag from host system. Specify it explicitly using -basetag command-line flag') + buildArgs = ['--build-arg', 'BASETAG={}'.format(baseTag)] # Use the default isolation mode unless requested otherwise @@ -72,7 +76,7 @@ def _generateWindowsBuildArgs(self, logger, basetagOverride=None, isolationOverr # If the user specified process isolation mode and a different base tag to the host system then warn them prefix = self.getPrefix() - if isolation == 'process' and baseTag != defaultBaseTag: + if isolation == 'process' and baseTag != hostBaseTag: logger.info('[{}] Warning: attempting to use different Windows container/host versions'.format(prefix), False) logger.info('[{}] when running in process isolation mode, this will usually break!'.format(prefix), False) diff --git a/ue4docker/diagnostics/diagnostic_20gig.py b/ue4docker/diagnostics/diagnostic_20gig.py index 7f6864c3..97572c22 100644 --- a/ue4docker/diagnostics/diagnostic_20gig.py +++ b/ue4docker/diagnostics/diagnostic_20gig.py @@ -14,7 +14,7 @@ def __init__(self): # Setup our argument parser so we can use its help message output in our description text self._parser = argparse.ArgumentParser(prog='ue4-docker diagnostics 20gig') self._parser.add_argument('--isolation', default=None, choices=['hyperv', 'process'], help="Override the default isolation mode when testing Windows containers") - self._parser.add_argument('-basetag', default=None, choices=WindowsUtils.getValidBaseTags(), help="Override the default base image tag when testing Windows containers") + self._parser.add_argument('-basetag', default=None, choices=WindowsUtils.getKnownBaseTags(), help="Override the default base image tag when testing Windows containers") def getName(self): ''' diff --git a/ue4docker/diagnostics/diagnostic_8gig.py b/ue4docker/diagnostics/diagnostic_8gig.py index a7c86cc7..2fb53322 100644 --- a/ue4docker/diagnostics/diagnostic_8gig.py +++ b/ue4docker/diagnostics/diagnostic_8gig.py @@ -16,7 +16,7 @@ def __init__(self): self._parser.add_argument('--linux', action='store_true', help="Use Linux containers under Windows hosts (useful when testing Docker Desktop or LCOW support)") self._parser.add_argument('--random', action='store_true', help="Create a file filled with random bytes instead of zeroes under Windows") self._parser.add_argument('--isolation', default=None, choices=['hyperv', 'process'], help="Override the default isolation mode when testing Windows containers") - self._parser.add_argument('-basetag', default=None, choices=WindowsUtils.getValidBaseTags(), help="Override the default base image tag when testing Windows containers") + self._parser.add_argument('-basetag', default=None, choices=WindowsUtils.getKnownBaseTags(), help="Override the default base image tag when testing Windows containers") def getName(self): ''' diff --git a/ue4docker/diagnostics/diagnostic_network.py b/ue4docker/diagnostics/diagnostic_network.py index 3d65a075..35338f13 100644 --- a/ue4docker/diagnostics/diagnostic_network.py +++ b/ue4docker/diagnostics/diagnostic_network.py @@ -13,7 +13,7 @@ def __init__(self): self._parser = argparse.ArgumentParser(prog='ue4-docker diagnostics network') self._parser.add_argument('--linux', action='store_true', help="Use Linux containers under Windows hosts (useful when testing Docker Desktop or LCOW support)") self._parser.add_argument('--isolation', default=None, choices=['hyperv', 'process'], help="Override the default isolation mode when testing Windows containers") - self._parser.add_argument('-basetag', default=None, choices=WindowsUtils.getValidBaseTags(), help="Override the default base image tag when testing Windows containers") + self._parser.add_argument('-basetag', default=None, choices=WindowsUtils.getKnownBaseTags(), help="Override the default base image tag when testing Windows containers") def getName(self): ''' diff --git a/ue4docker/infrastructure/BuildConfiguration.py b/ue4docker/infrastructure/BuildConfiguration.py index 38db5fba..da3eda1b 100644 --- a/ue4docker/infrastructure/BuildConfiguration.py +++ b/ue4docker/infrastructure/BuildConfiguration.py @@ -108,7 +108,6 @@ def addArguments(parser): parser.add_argument('--combine', action='store_true', help='Combine generated Dockerfiles into a single multi-stage build Dockerfile') parser.add_argument('--monitor', action='store_true', help='Monitor resource usage during builds (useful for debugging)') parser.add_argument('-interval', type=float, default=20.0, help='Sampling interval in seconds when resource monitoring has been enabled using --monitor (default is 20 seconds)') - parser.add_argument('--ignore-eol', action='store_true', help='Run builds even on EOL versions of Windows (advanced use only)') parser.add_argument('--ignore-blacklist', action='store_true', help='Run builds even on blacklisted versions of Windows (advanced use only)') parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output during builds (useful for debugging)') @@ -166,7 +165,6 @@ def __init__(self, parser, argv): self.excludedComponents = set(self.args.exclude) self.baseImage = None self.prereqsTag = None - self.ignoreEOL = self.args.ignore_eol self.ignoreBlacklist = self.args.ignore_blacklist self.verbose = self.args.verbose self.layoutDir = self.args.layout @@ -250,30 +248,25 @@ def _generateWindowsConfig(self): self.opts['buildgraph_args'] = self.opts.get('buildgraph_args', '') + f' -set:VS{self.visualStudio}=true' # Determine base tag for the Windows release of the host system - self.hostRelease = WindowsUtils.getWindowsRelease() - self.hostBasetag = WindowsUtils.getReleaseBaseTag(self.hostRelease) + self.hostBasetag = WindowsUtils.getHostBaseTag() # Store the tag for the base Windows Server Core image self.basetag = self.args.basetag if self.args.basetag is not None else self.hostBasetag + + if self.basetag is None: + raise RuntimeError('unable to determine Windows Server Core base image tag from host system. Specify it explicitly using -basetag command-line flag') + self.baseImage = 'mcr.microsoft.com/windows/servercore:' + self.basetag self.dllSrcImage = WindowsUtils.getDllSrcImage(self.basetag) self.prereqsTag = self.basetag + '-vs' + self.visualStudio - # 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 else: # 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 + differentKernels = self.basetag != self.hostBasetag dockerSupportsProcess = parse_version(DockerUtils.version()['Version']) >= parse_version('18.09.0') if not differentKernels and dockerSupportsProcess: self.isolation = 'process' diff --git a/ue4docker/infrastructure/WindowsUtils.py b/ue4docker/infrastructure/WindowsUtils.py index c4bdbdf1..a68732ec 100644 --- a/ue4docker/infrastructure/WindowsUtils.py +++ b/ue4docker/infrastructure/WindowsUtils.py @@ -1,6 +1,7 @@ from .DockerUtils import DockerUtils from pkg_resources import parse_version import platform, sys +from typing import Optional if platform.system() == 'Windows': import winreg @@ -10,18 +11,22 @@ class WindowsUtils(object): # The oldest Windows build we support _minimumRequiredBuild = 17763 - # The latest Windows build version we recognise as a non-Insider build - _latestReleaseBuild = 19042 + # This lookup table is based on the list of valid tags from + # and list of build-to-release mapping from https://docs.microsoft.com/en-us/windows/release-health/release-information + _knownTagsByBuildNumber = { + 17763: 'ltsc2019', + 18362: '1903', + 18363: '1909', + 19041: '2004', + 19042: '20H2', + 19043: '21H1', + } - # The list of Windows Server Core base image tags that we recognise, in ascending version number order - _validTags = ['ltsc2019', '1903', '1909', '2004', '20H2'] + _knownTags = list(_knownTagsByBuildNumber.values()) # 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 = ['1903', '1909'] + _blacklistedHosts = [18362, 18363] @staticmethod def _getVersionRegKey(subkey : str) -> str: @@ -54,25 +59,21 @@ def systemString() -> str: ''' Generates a verbose human-readable version string for the Windows host system ''' - return '{} Version {} (Build {}.{})'.format( + return '{} (Build {}.{})'.format( WindowsUtils._getVersionRegKey('ProductName'), - WindowsUtils.getWindowsRelease(), WindowsUtils.getWindowsBuild(), WindowsUtils._getVersionRegKey('UBR') ) @staticmethod - def getWindowsRelease() -> str: + def getHostBaseTag() -> Optional[str]: ''' - Determines the Windows 10 / Windows Server release (1607, 1709, 1803, etc.) of the Windows host system + Retrieves the tag for the Windows Server Core base image matching the host Windows system ''' - try: - # Starting with Windows 20H2 (also known as 2009), Microsoft stopped updating ReleaseId - # and instead updates DisplayVersion - return WindowsUtils._getVersionRegKey('DisplayVersion') - except FileNotFoundError: - # Fallback to ReleaseId for pre-20H2 releases that didn't have DisplayVersion - return WindowsUtils._getVersionRegKey('ReleaseId') + + hostBuild = WindowsUtils.getWindowsBuild() + + return WindowsUtils._knownTagsByBuildNumber.get(hostBuild) @staticmethod def getWindowsBuild() -> int: @@ -82,23 +83,14 @@ def getWindowsBuild() -> int: return sys.getwindowsversion().build @staticmethod - def isBlacklistedWindowsVersion(release=None): + def isBlacklistedWindowsHost() -> bool: ''' - Determines if the specified Windows release is one with bugs that make it unsuitable for use + Determines if host Windows version is one with bugs that make it unsuitable for use (defaults to checking the host OS release if one is not specified) ''' 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): - ''' - Determines if the specified Windows release is one that has reached End Of Life (EOL) - (defaults to checking the host OS release if one is not specified) - ''' - release = WindowsUtils.getWindowsRelease() if release is None else release - return release in WindowsUtils._eolReleases + build = WindowsUtils.getWindowsBuild() + return build in WindowsUtils._blacklistedHosts and dockerVersion < parse_version('19.03.6') @staticmethod def isWindowsServer() -> bool: @@ -108,28 +100,6 @@ def isWindowsServer() -> bool: # TODO: Replace this with something more reliable return 'Windows Server' in WindowsUtils._getVersionRegKey('ProductName') - @staticmethod - def isInsiderPreview() -> bool: - ''' - Determines if the Windows host system is a Windows Insider preview build - ''' - return WindowsUtils.getWindowsBuild() > WindowsUtils._latestReleaseBuild - - @staticmethod - 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 { - '1809': 'ltsc2019', - }.get(release, release) - @staticmethod def getDllSrcImage(basetag: str) -> str: ''' @@ -142,22 +112,18 @@ def getDllSrcImage(basetag: str) -> str: return f'mcr.microsoft.com/windows:{tag}' @staticmethod - 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: str) -> bool: + def getKnownBaseTags() -> [str]: ''' - Determines if the specified tag is a valid Windows Server Core base image tag + Returns the list of known tags for the Windows Server Core base image, in ascending chronological release order ''' - return tag in WindowsUtils._validTags + return WindowsUtils._knownTags @staticmethod - def isNewerBaseTag(older: str, newer: str) -> bool: + def isNewerBaseTag(older: str, newer: str) -> Optional[bool]: ''' Determines if the base tag `newer` is chronologically newer than the base tag `older` ''' - return WindowsUtils._validTags.index(newer) > WindowsUtils._validTags.index(older) + try: + return WindowsUtils._knownTags.index(newer) > WindowsUtils._knownTags.index(older) + except ValueError: + return None