-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathsystem.py
More file actions
565 lines (487 loc) · 21.4 KB
/
system.py
File metadata and controls
565 lines (487 loc) · 21.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
import os
import platform
import sys
import re
import subprocess
import traceback
from importlib import import_module
try:
from _winreg import HKEY_LOCAL_MACHINE
except:
pass
import config
from utility import Utility
from nugetUtility import NugetUtility
from settings import Settings
from logger import Logger, ColoredFormatter
import errors
from errors import error_codes, NO_ERROR
from helper import convertToPlatformPath, getCPUFamily
from consts import MAX_SDK_ROOT_PATH_LENGTH
class System:
"""
Encapsulate determining supported targets, updating python and
system PATH, downloading missing tools and script termination.
"""
#Defined here, so it can be performed logger check if script failes before logger is created
logger = None
recreatedUserDef = False
@classmethod
def preInit(cls):
"""
Determines host OS. Sets supported targets based on present folders.
"""
#Determine host OS
cls.hostOs = platform.system().lower()
cls.hostOsVersion = platform.release().lower()
cls.hostCPU = getCPUFamily(platform.machine())
#Create path variables used in preparation process.
Settings.preInit()
#Set available targets
cls.__setSupportedTargets()
#Add three paths, where can be found templates, in the PYTHONPATH
Utility.addModulePath(Settings.userWorkingPath) #User working directory - from where script is run
Utility.addModulePath(Settings.rootScriptsPath) #Folder where are scripts file
Utility.addModulePath(Settings.templatesPath) #Subfolder in scripts folder, that contains only template files
Utility.addPath(Settings.localBuildToolsPath)
#Determine python executable path and add python's Scripts folder in the system PATH
executablePath = Utility.getExecutablePath('python')
if executablePath != None:
pythonPath = os.path.dirname(executablePath)
pythonScriptsPath = os.path.join(pythonPath,'Scripts')
#Add python's Scripts folder in the PATH
Utility.addPath(pythonScriptsPath)
@classmethod
def setUp(cls):
"""
Initializes Settings, creates logger and updates depot tools path
"""
#Create userdef.py file if missing.
cls.__createUserDefFile()
#Load all settings values
Settings.init()
#After logger settings are loaded (log level, log format, ...), logger can be prepared for usage
Logger.setUp(Settings.logFormat, Settings.noColoredOutput, Settings.logToFile, Settings.overwriteLogFile)
cls.logger = Logger.getLogger('System')
#Set utility logger
Utility.setUp()
#Set up nuget utility
NugetUtility.setUp()
#Remove Google's depot tools from the PATH and add local depot tool path.
cls.__updateDepotToolsPath()
if cls.isWindows:
#Determine Visual Studio and MSVC tools paths
cls.__determineVisualStudioPath()
#Install missing python packages
cls.installPythonModules(config.PYTHON_PACKAGES_TO_INSTALL)
#Show warning if root sdk path is longer than MAX_SDK_ROOT_PATH_LENGTH = 65
if len(Settings.rootSdkPath) > MAX_SDK_ROOT_PATH_LENGTH:
cls.logger.warning(Settings.rootSdkPath + ' is longer than ' + str(MAX_SDK_ROOT_PATH_LENGTH) + '. That may cause build issues for webrtc libraries!')
@classmethod
def updatePythonTools(cls):
"""
Update python's pip tool. In future maybe it would be required to update other tools as well.
TODO: Check when it is best to call it
"""
#Update pip tool.
cmd = 'python.exe -m pip install --upgrade pip'
result = Utility.runSubprocess([cmd])
if result != 0:
cls.logger.error('Failed to update pip!')
@classmethod
def installPythonModules(cls, modulesDict):
"""
Checks if win32file module is installed, and if it is not, install pywin32 python package.
"""
for module in modulesDict.keys():
try:
#Provoke exception if module is not available
globals().update(import_module(module).__dict__)
except:
#Install python package
cmd = 'pip install ' + modulesDict[module]
result = Utility.runSubprocess([cmd], Settings.logLevel == 'DEBUG')
if result != 0:
cls.logger.error('Failed to install package ' + modulesDict[module] + ' required for module ' + module)
@classmethod
def downloadBuildToolsIfNeeded(cls):
"""
Downloads gn and clang-format build tools if missing.
:return ret: True if all tools are present or successfully donloaded.
"""
ret = True
if not Utility.checkIfToolIsInstalled(config.BUILD_TOOL_GN):
cls.logger.warning(config.BUILD_TOOL_GN + ' build tool is not found.')
ret = cls.__downloadBuildTool(config.BUILD_TOOL_GN)
if not Utility.checkIfToolIsInstalled(config.BUILD_TOOL_CLANG_FORMAT) and ret:
cls.logger.warning(config.BUILD_TOOL_CLANG_FORMAT + ' build tool is not found.')
ret = cls.__downloadBuildTool(config.BUILD_TOOL_CLANG_FORMAT)
return ret
@classmethod
def checkTools(cls):
"""
Checks if git, perl and Windows SDK tools are installed on host machine.
:return: NO_ERROR if all tools are installed, otherwise returns error code
"""
ret = NO_ERROR
#Check if Git is installed
if not Utility.checkIfToolIsInstalled('git'):
cls.logger.warning('git' + ' is not installed.')
return errors.ERROR_SYSTEM_MISSING_GIT
#Check if Perl is installed
if not Utility.checkIfToolIsInstalled('perl'):
cls.logger.warning('perl' + ' is not installed.')
#return errors.ERROR_SYSTEM_MISSING_PERL
ret = cls.checkVSDebugTools()
return ret
@classmethod
def checkIsPythonVersionSupported(cls):
"""
Checks if the correct python version is being used.
:return: True/False based on python version.
"""
python_version_number = str(sys.version_info.major) + '.' + str(sys.version_info.minor) + '.' + str(sys.version_info.micro)
if sys.version_info.major is not int(config.SUPPORTED_PYTHON_VERSION[0]):
Logger.printColorMessage('Google GN compilation requires Python version ' +
config.SUPPORTED_PYTHON_VERSION + ' but your python version is ' +
python_version_number + ' Please re-run using the proper version of Python.',
ColoredFormatter.RED)
return False
else:
return True
@classmethod
def checkVSDebugTools(cls):
"""
Checks if VS debug tools are installed.
:return: ERROR_SYSTEM_MISSING_VS_DEBUG_TOOLS if debug tools are not isntalled.
"""
ret = NO_ERROR
# Get Windows SDK Debugger path from Windows registry
winSdkDebugToolPath = Utility.getKeyValueFromRegistry(HKEY_LOCAL_MACHINE, r'SOFTWARE\Wow6432Node\Microsoft\Windows Kits\Installed Roots',"WindowsDebuggersRoot10")
# If path is not found in registry or path is not valid return error
if winSdkDebugToolPath == None or not os.path.isdir(winSdkDebugToolPath):
ret = errors.ERROR_SYSTEM_MISSING_VS_DEBUG_TOOLS
else:
cls.logger.debug('Windows SDK Debug Tools path is ' + winSdkDebugToolPath)
return ret
@classmethod
def checkIfTargetIsSupported(cls, target):
"""
Checks if specific target is supported.
:param target: target to check for
:return: True if target is supported
"""
if target.lower() not in [item.lower() for item in cls.supportedTargets]:
return False
return True
@classmethod
def checkIfTargetsAreSupported(cls, targets):
"""
Checks if specified targets are supported.
:param targets: list of targets to check for
:return: True if targets are supported
"""
cls.logger.debug('Checking if specified targets are supported.')
for target in targets:
if not cls.checkIfTargetIsSupported(target):
return False
return True
@classmethod
def checkIfPlatformIsSupportedForHostCPU(cls, platform):
"""
Checks if host Os supports specific platform.
:param platform: platform to check for
:return: True if platform is supported
"""
if platform.lower() not in Settings.supportedPlatformsForHostOs[cls.hostOs]:
cls.logger.warning(platform + ' is not supported.')
return False
return True
@classmethod
def checkIfPlatformsAreSupported(cls, platforms):
"""
Checks if host OS supports specified platforms.
:param platforms: list of platforms to check for
:return: True if platforms are supported
"""
cls.logger.debug('Checking if specified platforms are supported.')
for platform in platforms:
if not cls.checkIfPlatformIsSupportedForHostCPU(platform):
return False
return True
@classmethod
def checkIfCPUIsSupportedForPlatform(cls, cpu, platform):
"""
Checks if host Os supports specific platform.
:param platform: platform to check for
:return: True if platform is supported
"""
if cpu.lower() not in Settings.supportedCPUsForPlatform[platform]:
cls.logger.warning('CPU ' + cpu + ' is not supported for platform ' + platform)
return False
return True
@classmethod
def isWindows(cls):
"""
Checks if Windows is host OS.
:return: True if Windows
"""
if cls.hostOs.lower() == 'windows':
return True
return False
@classmethod
def stopExecution(cls, error = NO_ERROR, message = ''):
"""
Stops further script execution.
:param error: error code. Default value is NO_ERROR
:param message: error message. If message is empty, it will be used error message related to provided error code
"""
if error:
#If message is not empty, show it, otherwise get message for provided error code
if message != '':
errorMessage = message
else:
errorMessage = error_codes[error]
try:
Utility.terminateSubprocess()
Utility.returnOriginalFile(Settings.mainBuildGnFilePath)
except Exception as error:
Logger.printColorMessage(str(error))
if error == errors.TERMINATED_BY_USER:
Logger.printColorMessage(error_codes[error])
else:
#If logger is not yet initialzed it cannot be use it, so just colorized message is shown
if cls.logger != None:
cls.logger.critical('Script execution has failed')
cls.logger.error(errorMessage)
else:
Logger.printColorMessage('Script execution has failed')
Logger.printColorMessage('Error E'+ str(error) + ': ' + errorMessage)
#If showSettingsValuesOnError is set to True, print current settings values
if Settings.showSettingsValuesOnError:
print ('\n\n\n----------------------- CURRENT SETTINGS -----------------------')
attrs = vars(Settings)
print ('\n '.join('%s: %s' % item for item in attrs.items()))
print ('------------------- CURRENT SETTINGS END -----------------------')
if Settings.showPATHOnError:
print ('\n\n\n----------------------- PATH -----------------------')
print (os.environ['PATH'])
print ('------------------- PATH END -----------------------')
#If showTraceOnError is set to True, print current trace log
if Settings.showTraceOnError:
print ('\n\n\n----------------------- TRACE -----------------------')
traceback.print_stack()
print ('----------------------- TRACE END -----------------------')
sys.exit(error)
@classmethod
def recreateUserDef(cls):
"""
Recreates userdef.py file if it is not just created. If userdef.py file creation has failed,
script will be terminated.
:return ret: NO_ERROR if userdef is created or it was "just" created.
ERROR_SYSTEM_FAILED_DELETING_USERDEF if deleting userdef.py failed.
"""
ret = NO_ERROR
if not cls.recreatedUserDef:
try:
#Deletes userdef.py file
Utility.deleteFiles([Settings.userDefFilePath])
cls.__createUserDefFile()
except Exception as error:
cls.logger.error(str(error))
ret = errors.ERROR_SYSTEM_FAILED_DELETING_USERDEF
return ret
@classmethod
def downloadFromGoogle(cls, bucket, path, isDirectory = False, shouldRecurse = True):
"""
Download content from the google storage buckets
:param bucket: the name of the google bucket to download from
:param path: the path to a sha1 file OR the path to a directory containing sha1 files
:param isDirectory: must be True is path is a file, and False if path is a directory
:param shouldRecurse: only used if path is a directory, True to recursively scan for sha1 files, False to not
:return ret: True if successfully downloaded.
"""
ret = True
#TODO(bengreenier): we can and should derive isDirectory
#Temporary change working directory to local depot tools path
Utility.pushd(Settings.localDepotToolsPath)
operationDetails = path + ' from bucket \'' + bucket
cls.logger.info('Downloading ' + operationDetails + '\'...')
#Run download
flag = '-d' if isDirectory else '-s'
modifier = '-r' if shouldRecurse else ''
cmd = 'python download_from_google_storage.py --bucket ' + bucket + ' ' + flag + ' ' + path + ' ' + modifier
result = Utility.runSubprocess([cmd], Settings.logLevel == 'DEBUG')
#Switch to previous working directory
Utility.popd()
if result != NO_ERROR:
ret = False
cls.logger.error('Failed downloading ' + operationDetails)
return ret
#---------------------------------- Private methods --------------------------------------------
@classmethod
def __createUserDefFile(cls):
"""
Created userdef.py file from defaults.py if it is missing.
"""
#Checks if in user working directory exists files userdefs.py and if not creates it from default.py
if not os.path.isfile(Settings.userDefFilePath):
try:
with open(Settings.defaultFilePath, 'r') as defaultsFile:
tempFileContent = defaultsFile.readlines()
tempFileContent = tempFileContent[4:]
tempFileContent.insert(0,'# ' + config.USERDEF_DESCRIPTION_MESSAGE + '\n')
tempFileContent = "".join(tempFileContent)
with open(Settings.userDefFilePath, 'w') as userDefFile:
userDefFile.write(tempFileContent)
cls.recreatedUserDef = True
except Exception as error:
Logger.printColorMessage(str(error))
cls.stopExecution(errors.ERROR_SYSTEM_FAILED_USERDEF_CREATION)
@classmethod
def __setSupportedTargets(cls):
"""
Sets array of supported targets. By default webrtc is supported.
Ortc is supported if ortc folder exists in sdk root
"""
#Webrtc is always supported
cls.supportedTargets = ['webrtc']
#If ortc folder exists in sdk root folder add ortc in the list of supported targets
if os.path.exists(os.path.join(Settings.rootSdkPath,'ortc')):
cls.supportedTargets += ['ortc']
@classmethod
def __updateDepotToolsPath(cls):
"""
Checks if Google's depot tools is in the PATH, removes it and add local depot tool path in PATH.
"""
#Search if gclient is one of the folders in the PATH
depotToolPath = Utility.searchFileInPATH('gclient')
if depotToolPath != None:
cls.logger.debug('Removing depot tools path \'' + depotToolPath +'\' from the PATH.')
#Remove depot tools path from the PATH
Utility.removePath(depotToolPath)
cls.logger.info('Adding depot tools path \'' + Settings.localDepotToolsPath +'\' to the PATH.')
#Add local depot tools path in the PATH
Utility.addPath(Settings.localDepotToolsPath)
@classmethod
def __downloadBuildTool(cls, toolName):
"""
Download tools from the google storage, Currently used for downloading gn and clang-format
:param toolName: tool name (gn, clang-format)
:return ret: True if successfully downloaded.
"""
ret = True
result = cls.downloadFromGoogle('chromium-' + toolName, os.path.join(Settings.localBuildToolsPath,toolName + '.exe.sha1'), False, False)
if not result:
ret = False
cls.logger.error('Failed downloading ' + toolName)
return ret
@classmethod
def __determineVisualStudioPath(cls):
"""
Determines Visual Studio and MSVC tools paths if they are installed in
Program Files or Program Files (x86). If VS is installed but it is not found
in these folders, it is required to set msvsPath variable in userdef.py file
to point to proper folder.
(e.g. msvsPath = E:\\Development\\Microsoft Visual Studio\\2017\\Community)
"""
if Settings.msvsPath == '' or not os.path.exists(Settings.msvsPath):
vsPath = ''
#Get Program File path
if os.environ['ProgramFiles(x86)'] == '':
vsPath = os.environ['ProgramFiles']
else:
vsPath = os.environ['ProgramFiles(x86)']
#Create exoected VS2017 path
vsPath = os.path.join(vsPath,convertToPlatformPath(config.MSVS_FOLDER_NAME))
#Indetify installed VS2017 version
if os.path.exists(vsPath):
for version in config.MSVS_VERSIONS:
versionPath = os.path.join(vsPath,version)
if os.path.exists(versionPath):
Settings.msvsPath = versionPath
break
else:
cls.logger.warning('Visual studio 2017 is not found at ' + vsPath + '. Please install it, or if it is installed, set msvsPath variable in userdef.py to point to correct path.')
#Determine msvc tools and vcvarsall.bat path
if Settings.msvsPath != '':
Settings.msvcToolsPath = os.path.join(Settings.msvsPath,convertToPlatformPath(config.MSVC_TOOLS_PATH))
Settings.msvcToolsVersion = next(os.walk(Settings.msvcToolsPath))[1][0]
Settings.msvcToolsBinPath = os.path.join(Settings.msvcToolsPath,Settings.msvcToolsVersion,'bin','Host' + cls.hostCPU)
#Settings.vcvarsallPath = os.path.join(Settings.msvsPath,convertToPlatformPath(config.VCVARSALL_PATH))
Settings.vcvarsallPath = os.path.join(Settings.msvsPath,convertToPlatformPath(config.VC_AUXILIARY_BUILD_PATH),'vcvarsall.bat')
#Read Microsoft.VCToolsVersion.default.txt content to get current vc tools version
#Settings.vcToolsVersionPath = os.path.join(Settings.msvsPath,convertToPlatformPath(config.VC_AUXILIARY_BUILD_PATH),'Microsoft.VCToolsVersion.default.txt')
cls.logger.info('Visual studio path is ' + Settings.msvsPath)
cls.logger.debug('MSVC tools path is ' + Settings.msvcToolsPath)
cls.logger.debug('MSVC tools bin path is ' + Settings.msvcToolsBinPath)
@classmethod
def downloadClangClIfMissing(cls):
"""
Downloads clang-cl.exe if missing.
:return ret: True if exists or if it is successfully downloaded.
"""
ret = True
#Check if clang-cl is already downloaded
clangPath = os.path.join(Settings.webrtcPath,convertToPlatformPath(config.CLANG_CL_PATH))
if not os.path.isfile(clangPath):
clangUpdateScriptPath = os.path.join(Settings.webrtcPath,convertToPlatformPath(config.CLANG_UPDATE_SCRIPT_PATH))
cls.logger.info('Clang-cl.exe is not found in third_party tools. It will be downloaded.')
#Make copy of environment variable
my_env = os.environ.copy()
#Update environment variable with DEPOT_TOOLS_WIN_TOOLCHAIN set to 0, to prevent requiring https://chrome-internal.googlesource.com
my_env["DEPOT_TOOLS_WIN_TOOLCHAIN"] = "0"
#Run clangg update script
cmd = 'python ' + clangUpdateScriptPath
result = Utility.runSubprocess([cmd], Settings.logLevel == 'DEBUG', my_env)
#result = subprocess.call(['python', clangUpdateScriptPath], env=my_env)
if result == NO_ERROR:
cls.logger.info('Clang-cl.exe is downloaded successfully.')
Utility.createFolderLinks(config.FOLDERS_TO_LINK_LLVM)
else:
ret = False
cls.logger.error('Downloading Clang-cl.exe has failed.')
else:
cls.logger.debug('Clang-cl.exe is found.')
return ret
@classmethod
def getEnvFromBat(cls):
"""Given a bat command, runs it and returns env vars set by it."""
cmd = '\"' + Settings.vcvarsallPath + '\" '
cpu_arg = "amd64"
""" if (cpu != 'x64'):
# x64 is default target CPU thus any other CPU requires a target set
cpu_arg += '_' + cpu"""
cmd += cpu_arg
cmd += ' && set'
popen = subprocess.Popen(
cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
variables, _ = popen.communicate()
if popen.returncode != 0:
raise Exception('"%s" failed with error %d' % (cmd, popen.returncode))
#else:
#parse variables
return variables
@classmethod
def logEnvIncludeAndLibPaths(cls, platform, cpu, path):
"""
Print include and lib paths for build environment.
:param platform: Platform of interest ('win' or 'winuwp').
:param cpu: CPU of interest.
:param path: Folder path where is save environment file.
"""
if os.path.exists(path):
if platform == 'winuwp':
environmentPath = os.path.join(path, 'environment.store_' + cpu)
else:
environmentPath = os.path.join(path, 'environment.' + cpu)
if os.path.isfile(environmentPath):
textfile = open(environmentPath, "r")
filetext = textfile.read()
textfile.close()
for envVariable in Settings.logNinjaEnvironmentFileVariables:
regex = r'\x00' + envVariable + '=(.*)\x00'
includeStr = re.findall(regex,filetext)
cls.logger.debug('\n\n' + envVariable + ': \n' + str(includeStr))
else:
cls.logger.error('environment file is missing!')