makefile.py 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976
  1. #! /usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  4. # Releases *
  5. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  6. # 1.0: march 18th, 2015
  7. # first release
  8. # 2.0: october 2th, 2015
  9. # added several target definition for rules
  10. # 2.1: october 5th, 2015
  11. # added checking routine formal argument run-time types
  12. # 2.2: october 24th, 2015
  13. # changed subprocess.Popen to subprocess.call in runCommand
  14. # added command tool checking using 'find_executable' function
  15. # added optional argument to Make class initializer to log command utility tool path
  16. # 2.3: april 16th, 2016
  17. # added advance percentage
  18. # 3.0: may 30th, 2016
  19. # compatibility with Python 3:
  20. # print xyz ---> print (xyz)
  21. # change isinstance function arguments ---> function argumentIsString
  22. # subprocess.call: add "universal_newlines=True" argument
  23. # added test (job.mReturnCode != None) lines 727 and 739
  24. # 3.1: may 26th, 2018
  25. # Added tolerance in secondary dependency file syntax:
  26. #
  27. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  28. # http://www.diveintopython3.net/porting-code-to-python-3-with-2to3.html
  29. import subprocess, sys, os, copy
  30. import urllib, shutil, subprocess
  31. import platform, json, operator
  32. import threading, types, traceback
  33. if sys.version_info >= (2, 6) :
  34. import multiprocessing
  35. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  36. # find_executable *
  37. # From: *
  38. # https://gist.github.com/4368898 *
  39. # Public domain code by anatoly techtonik <techtonik@gmail.com> *
  40. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  41. def find_executable(executable, path=None):
  42. """Try to find 'executable' in the directories listed in 'path' (a
  43. string listing directories separated by 'os.pathsep'; defaults to
  44. os.environ['PATH']). Returns the complete filename or None if not
  45. found
  46. """
  47. if path is None:
  48. path = os.environ['PATH']
  49. paths = path.split(os.pathsep)
  50. extlist = ['']
  51. if os.name == 'os2':
  52. (base, ext) = os.path.splitext(executable)
  53. # executable files on OS/2 can have an arbitrary extension, but
  54. # .exe is automatically appended if no dot is present in the name
  55. if not ext:
  56. executable = executable + ".exe"
  57. elif sys.platform == 'win32':
  58. pathext = os.environ['PATHEXT'].lower().split(os.pathsep)
  59. (base, ext) = os.path.splitext(executable)
  60. if ext.lower() not in pathext:
  61. extlist = pathext
  62. for ext in extlist:
  63. execname = executable + ext
  64. if os.path.isfile(execname):
  65. return execname
  66. else:
  67. for p in paths:
  68. f = os.path.join(p, execname)
  69. if os.path.isfile(f):
  70. return f
  71. else:
  72. return None
  73. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  74. # processorCount *
  75. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  76. def processorCount () :
  77. if sys.version_info >= (2, 6) :
  78. coreCount = multiprocessing.cpu_count ()
  79. else:
  80. coreCount = 1
  81. return coreCount
  82. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  83. # argumentIsString *
  84. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  85. def argumentIsString (argument) :
  86. if sys.version_info < (3,0):
  87. return isinstance (argument, types.StringTypes)
  88. else:
  89. return type (argument) is str
  90. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  91. # FOR PRINTING IN COLOR *
  92. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  93. def BLACK () :
  94. return '\033[90m'
  95. #······················································································································*
  96. def RED () :
  97. return '\033[91m'
  98. #······················································································································*
  99. def GREEN () :
  100. return '\033[92m'
  101. #······················································································································*
  102. def YELLOW () :
  103. return '\033[93m'
  104. #······················································································································*
  105. def BLUE () :
  106. return '\033[94m'
  107. #······················································································································*
  108. def MAGENTA () :
  109. return '\033[95m'
  110. #······················································································································*
  111. def CYAN () :
  112. return '\033[96m'
  113. #······················································································································*
  114. def WHITE () :
  115. return '\033[97m'
  116. #······················································································································*
  117. def ENDC () :
  118. return '\033[0m'
  119. #······················································································································*
  120. def BOLD () :
  121. return '\033[1m'
  122. #······················································································································*
  123. def UNDERLINE () :
  124. return '\033[4m'
  125. #······················································································································*
  126. def BLINK () :
  127. return '\033[5m'
  128. #······················································································································*
  129. def BOLD_BLUE () :
  130. return BOLD () + BLUE ()
  131. #······················································································································*
  132. def BOLD_GREEN () :
  133. return BOLD () + GREEN ()
  134. #······················································································································*
  135. def BOLD_RED () :
  136. return BOLD () + RED ()
  137. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  138. # runHiddenCommand *
  139. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  140. def runHiddenCommand (cmd, logUtilityTool=False) :
  141. executable = find_executable (cmd [0])
  142. if executable == None:
  143. print (BOLD_RED () + "*** Cannot find '" + cmd[0] + "' executable ***" + ENDC ())
  144. sys.exit (1)
  145. if logUtilityTool:
  146. print ("Utility tool is '" + executable + "'")
  147. result = ""
  148. childProcess = subprocess.Popen (cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
  149. while True:
  150. line = childProcess.stdout.readline ()
  151. if line != "":
  152. result += line
  153. else:
  154. childProcess.wait ()
  155. if childProcess.returncode != 0 :
  156. sys.exit (childProcess.returncode)
  157. return result
  158. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  159. # runCommand *
  160. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  161. def runCommand (cmd, title, showCommand, logUtilityTool) :
  162. if title != "":
  163. print (BOLD_BLUE () + title + ENDC ())
  164. if (title == "") or showCommand :
  165. cmdAsString = ""
  166. for s in cmd:
  167. if (s == "") or (s.find (" ") >= 0):
  168. cmdAsString += '"' + s + '" '
  169. else:
  170. cmdAsString += s + ' '
  171. print (cmdAsString)
  172. executable = find_executable (cmd [0])
  173. if executable == None:
  174. print (BOLD_RED () + "*** Cannot find '" + cmd[0] + "' executable ***" + ENDC ())
  175. sys.exit (1)
  176. if logUtilityTool:
  177. print ("Utility tool is '" + executable + "'")
  178. returncode = subprocess.call (cmd)
  179. if returncode != 0 :
  180. sys.exit (returncode)
  181. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  182. # runInThread *
  183. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  184. def runInThread (job, displayLock, terminationSemaphore):
  185. executable = find_executable (job.mCommand [0])
  186. if executable == None:
  187. print (BOLD_RED () + "*** Cannot find '" + job.mCommand[0] + "' executable ***" + ENDC ())
  188. job.mReturnCode = 1
  189. terminationSemaphore.release ()
  190. else:
  191. if job.mLogUtilityTool :
  192. print ("Utility tool is '" + executable + "'")
  193. childProcess = subprocess.Popen (job.mCommand, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
  194. while True:
  195. line = childProcess.stdout.readline ()
  196. if line != "":
  197. job.mOutputLines.append (line)
  198. displayLock.acquire ()
  199. sys.stdout.write (line) # Print without newline
  200. displayLock.release ()
  201. else:
  202. childProcess.wait ()
  203. job.mReturnCode = childProcess.returncode
  204. terminationSemaphore.release ()
  205. break
  206. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  207. # modificationDateForFile *
  208. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  209. def modificationDateForFile (dateCacheDictionary, file):
  210. absFilePath = os.path.abspath (file)
  211. if absFilePath in dateCacheDictionary :
  212. return dateCacheDictionary [absFilePath]
  213. elif not os.path.exists (absFilePath):
  214. date = sys.float_info.max # Very far in future
  215. dateCacheDictionary [absFilePath] = date
  216. return date
  217. else:
  218. date = os.path.getmtime (absFilePath)
  219. dateCacheDictionary [absFilePath] = date
  220. return date
  221. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  222. # class PostCommand *
  223. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  224. class PostCommand:
  225. mCommand = []
  226. mTitle = ""
  227. #····················································································································*
  228. def __init__ (self, title = ""):
  229. self.mCommand = []
  230. self.mTitle = title
  231. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  232. # class Job *
  233. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  234. class Job:
  235. mTargets = []
  236. mCommand = []
  237. mTitle = ""
  238. mRequiredFiles = []
  239. mPostCommands = []
  240. mReturnCode = None
  241. mPriority = 0
  242. mState = 0 # 0: waiting for execution
  243. mOutputLines = []
  244. mOpenSourceOnError = False # Do not try to open source file on error
  245. mLogUtilityTool = False
  246. #····················································································································*
  247. def __init__ (self, targets, requiredFiles, command, postCommands, priority, title, openSourceOnError, logUtilityTool):
  248. self.mTargets = copy.deepcopy (targets)
  249. self.mCommand = copy.deepcopy (command)
  250. self.mRequiredFiles = copy.deepcopy (requiredFiles)
  251. self.mTitle = copy.deepcopy (title)
  252. self.mPostCommands = copy.deepcopy (postCommands)
  253. self.mPriority = priority
  254. self.mOutputLines = []
  255. self.mOpenSourceOnError = openSourceOnError
  256. self.mLogUtilityTool = logUtilityTool
  257. #····················································································································*
  258. def run (self, displayLock, terminationSemaphore, showCommand, progressString):
  259. displayLock.acquire ()
  260. if self.mTitle != "":
  261. print (progressString + BOLD_BLUE () + self.mTitle + ENDC ())
  262. if (self.mTitle == "") or showCommand :
  263. cmdAsString = ""
  264. for s in self.mCommand:
  265. if (s == "") or (s.find (" ") >= 0):
  266. cmdAsString += '"' + s + '" '
  267. else:
  268. cmdAsString += s + ' '
  269. print (progressString + cmdAsString)
  270. displayLock.release ()
  271. thread = threading.Thread (target=runInThread, args=(self, displayLock, terminationSemaphore))
  272. thread.start()
  273. #····················································································································*
  274. def runPostCommand (self, displayLock, terminationSemaphore, showCommand):
  275. postCommand = self.mPostCommands [0]
  276. self.mCommand = postCommand.mCommand
  277. displayLock.acquire ()
  278. print (BOLD_BLUE () + postCommand.mTitle + ENDC ())
  279. if showCommand:
  280. cmdAsString = ""
  281. for s in self.mCommand:
  282. if (s == "") or (s.find (" ") >= 0):
  283. cmdAsString += '"' + s + '" '
  284. else:
  285. cmdAsString += s + ' '
  286. print (cmdAsString)
  287. displayLock.release ()
  288. thread = threading.Thread (target=runInThread, args=(self, displayLock, terminationSemaphore))
  289. thread.start()
  290. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  291. # class Rule *
  292. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  293. class Rule:
  294. mTargets = []
  295. mDependences = []
  296. mCommand = []
  297. mSecondaryMostRecentModificationDate = 0.0 # Far in the past
  298. mTitle = ""
  299. mPostCommands = []
  300. mPriority = 0
  301. mDeleteTargetOnError = False # No operation on error
  302. mCleanOperation = 0 # No operation on clean
  303. mOpenSourceOnError = False # Do not try to open source file on error
  304. #····················································································································*
  305. def __init__ (self, targets, title = ""):
  306. # print ("Rule '" + title + "'")
  307. if not type (targets) is list:
  308. print (BOLD_RED () + "*** Rule type instanciation: first argument 'targets' is not a list ***" + ENDC ())
  309. traceback.print_stack ()
  310. sys.exit (1)
  311. else:
  312. for aTarget in targets:
  313. # print (" Target '" + aTarget + "'")
  314. if not argumentIsString (aTarget):
  315. print (BOLD_RED () + "*** Rule type instanciation: an element of first argument 'targets' is not a string ***" + ENDC ())
  316. traceback.print_stack ()
  317. sys.exit (1)
  318. if not argumentIsString (title):
  319. print (BOLD_RED () + "*** Rule type instanciation: second argument 'title' is not a string ***" + ENDC ())
  320. traceback.print_stack ()
  321. sys.exit (1)
  322. self.mTargets = copy.deepcopy (targets)
  323. self.mDependences = []
  324. self.mCommand = []
  325. self.mSecondaryMostRecentModificationDate = 0.0
  326. self.mPostCommands = []
  327. self.mPriority = 0
  328. self.mDeleteTargetOnError = False # No operation on error
  329. self.mOpenSourceOnError = False # Do not try to open source file on error
  330. self.mCleanOperation = 0 # No operation on clean
  331. if title == "":
  332. self.mTitle = "Building"
  333. for s in targets:
  334. self.mTitle += " " + s
  335. else:
  336. self.mTitle = copy.deepcopy (title)
  337. #····················································································································*
  338. def deleteTargetFileOnClean (self):
  339. self.mCleanOperation = 1
  340. #····················································································································*
  341. def deleteTargetDirectoryOnClean (self):
  342. self.mCleanOperation = 2
  343. #····················································································································*
  344. def enterSecondaryDependanceFile (self, secondaryDependanceFile, make):
  345. if not argumentIsString (secondaryDependanceFile):
  346. print (BOLD_RED () + "*** Rule.enterSecondaryDependanceFile: 'secondaryDependanceFile' argument is not a string ***" + ENDC ())
  347. traceback.print_stack ()
  348. sys.exit (1)
  349. if make.mSelectedGoal != "clean":
  350. filePath = os.path.abspath (secondaryDependanceFile)
  351. if not os.path.exists (filePath):
  352. self.mSecondaryMostRecentModificationDate = sys.float_info.max # Very far in future
  353. else:
  354. f = open (filePath, "r")
  355. s = f.read ()
  356. f.close ()
  357. s = s.replace ("\\ ", "\x01") # Replace escaped spaces by \0x01
  358. s = s.replace ("\\\n", "") # Suppress \ at the end of lines
  359. liste = s.split ("\n\n")
  360. # print ("DEP " + secondaryDependanceFile)
  361. for s in liste:
  362. # print ("S " + s)
  363. components = s.split (':')
  364. # print (str (len (components)))
  365. #target = components [0].replace ("\x01", " ")
  366. #print ("------- Optional dependency rules for target '" + target + "'")
  367. #print ("Secondary target '" + target + "'")
  368. if len (components) > 1 :
  369. for src in components [1].split ():
  370. secondarySource = src.replace ("\x01", " ")
  371. # print (" SECONDARY SOURCE '" + secondarySource + "'")
  372. modifDate = modificationDateForFile (make.mModificationDateDictionary, secondarySource)
  373. if self.mSecondaryMostRecentModificationDate < modifDate :
  374. self.mSecondaryMostRecentModificationDate = modifDate
  375. #print (BOLD_BLUE () + str (modifDate) + ENDC ())
  376. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  377. # class Make *
  378. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*
  379. class Make:
  380. mRuleList = []
  381. mJobList = []
  382. mErrorCount = 0
  383. mModificationDateDictionary = {}
  384. mGoals = {}
  385. mSelectedGoal = ""
  386. mLinuxTextEditor = ""
  387. mMacTextEditor = ""
  388. mSimulateClean = False
  389. mLogUtilityTool = True
  390. mShowProgressString = True
  391. #····················································································································*
  392. def __init__ (self, goal, logUtilityTool=False):
  393. if not argumentIsString (goal):
  394. print (BOLD_RED () + "*** Make instanciation: 'goal' argument is not a string ***" + ENDC ())
  395. traceback.print_stack ()
  396. sys.exit (1)
  397. self.mRuleList = []
  398. self.mJobList = []
  399. self.mErrorCount = 0
  400. self.mModificationDateDictionary = {}
  401. self.mGoals = {}
  402. self.mSelectedGoal = goal
  403. self.mLinuxTextEditor = "gEdit"
  404. self.mMacTextEditor = "TextEdit"
  405. self.mLogUtilityTool = logUtilityTool
  406. #····················································································································*
  407. def doNotShowProgressString (self) :
  408. self.mShowProgressString = False
  409. #····················································································································*
  410. def addRule (self, rule):
  411. if not isinstance (rule, Rule):
  412. print (BOLD_RED () + "*** Make.addRule: 'rule' argument is not an instance of Rule type ***" + ENDC ())
  413. traceback.print_stack ()
  414. sys.exit (1)
  415. self.mRuleList.append (copy.deepcopy (rule))
  416. #····················································································································*
  417. def printRules (self):
  418. print (BOLD_BLUE () + "--- Print " + str (len (self.mRuleList)) + " rule" + ("s" if len (self.mRuleList) > 1 else "") + " ---" + ENDC ())
  419. for rule in self.mRuleList:
  420. message = ""
  421. for s in rule.mTargets:
  422. message += " \"" + s + "\""
  423. print (BOLD_GREEN () + "Target:" + message + ENDC ())
  424. for dep in rule.mDependences:
  425. print (" Dependence: \"" + dep + "\"")
  426. s = " Command: "
  427. for cmd in rule.mCommand:
  428. s += " \"" + cmd + "\""
  429. print (s)
  430. print (" Title: \"" + rule.mTitle + "\"")
  431. print (" Delete target on error: " + ("yes" if rule.mDeleteTargetOnError else "no"))
  432. cleanOp = "none"
  433. if rule.mCleanOperation == 1:
  434. cleanOp = "delete target file(s)"
  435. elif rule.mCleanOperation == 2:
  436. dirSet = set ()
  437. for s in rule.mTargets:
  438. path = os.path.dirname (s)
  439. if path != "":
  440. dirSet.add (path)
  441. cleanOp = "delete target directory:"
  442. for s in dirSet:
  443. cleanOp += " \"" + s + "\""
  444. print (" Clean operation: " + cleanOp)
  445. index = 0
  446. for postCommand in rule.mPostCommands:
  447. index = index + 1
  448. s = " Post command " + str (index) + ": "
  449. for cmd in postCommand.mCommand:
  450. s += " \"" + cmd + "\""
  451. print (s)
  452. print (" Title: \"" + postCommand.mTitle + "\"")
  453. print (BOLD_BLUE () + "--- End of print rule ---" + ENDC ())
  454. #····················································································································*
  455. def writeRuleDependancesInDotFile (self, dotFileName):
  456. s = "digraph G {\n"
  457. s += " node [fontname=courier]\n"
  458. arrowSet = set ()
  459. for rule in self.mRuleList:
  460. for target in rule.mTargets:
  461. s += ' "' + target + '" [shape=rectangle]\n'
  462. for dep in rule.mDependences:
  463. arrowSet.add (' "' + target + '" -> "' + dep + '"\n')
  464. for arrow in arrowSet:
  465. s += arrow
  466. s += "}\n"
  467. f = open (dotFileName, "w")
  468. f.write (s)
  469. f.close ()
  470. #····················································································································*
  471. def checkRules (self):
  472. if self.mErrorCount == 0:
  473. ruleList = copy.deepcopy (self.mRuleList)
  474. index = 0
  475. looping = True
  476. #--- loop on rules
  477. while looping:
  478. looping = False
  479. while index < len (ruleList):
  480. aRule = ruleList [index]
  481. index = index + 1
  482. #--- Check dependance files have rule for building, or does exist
  483. depIdx = 0
  484. while depIdx < len (aRule.mDependences):
  485. dep = aRule.mDependences [depIdx]
  486. depIdx = depIdx + 1
  487. hasBuildRule = False
  488. for r in ruleList:
  489. for target in r.mTargets:
  490. if dep == target:
  491. hasBuildRule = True
  492. break
  493. if not hasBuildRule:
  494. looping = True
  495. if not os.path.exists (os.path.abspath (dep)):
  496. self.mErrorCount = self.mErrorCount + 1
  497. print (BOLD_RED () + "Check rules error: '" + dep + "' does not exist, and there is no rule for building it." + ENDC ())
  498. depIdx = depIdx - 1
  499. aRule.mDependences.pop (depIdx)
  500. #--- Rule with no dependances
  501. if len (aRule.mDependences) == 0 :
  502. looping = True
  503. index = index - 1
  504. ruleList.pop (index)
  505. idx = 0
  506. while idx < len (ruleList):
  507. r = ruleList [idx]
  508. idx = idx + 1
  509. for target in aRule.mTargets:
  510. while r.mDependences.count (target) > 0 :
  511. r.mDependences.remove (target)
  512. #--- Error if rules remain
  513. if len (ruleList) > 0:
  514. self.mErrorCount = self.mErrorCount + 1
  515. print (BOLD_RED () + "Check rules error; circulary dependances between:" + ENDC ())
  516. for aRule in ruleList:
  517. targetList = ""
  518. for target in aRule.mTargets:
  519. targetList += " '" + aRule.mTarget + "'"
  520. print (BOLD_RED () + " - " + targetList + ", depends from:" + ENDC ())
  521. for dep in aRule.mDependences:
  522. print (BOLD_RED () + " '" + dep + "'" + ENDC ())
  523. #····················································································································*
  524. def existsJobForTarget (self, target):
  525. for job in self.mJobList:
  526. for aTarget in job.mTargets:
  527. if aTarget == target:
  528. return True
  529. return False
  530. #····················································································································*
  531. def makeJob (self, target): # Return a bool indicating wheither the target should be built
  532. #--- If there are errors, return immediatly
  533. if self.mErrorCount != 0:
  534. return False
  535. #--- Target already in job list ?
  536. if self.existsJobForTarget (target):
  537. return True # yes, return target will be built
  538. #--- Find a rule for making the target
  539. absTarget = os.path.abspath (target)
  540. rule = None
  541. matchCount = 0
  542. for r in self.mRuleList:
  543. for aTarget in r.mTargets:
  544. if target == aTarget:
  545. matchCount = matchCount + 1
  546. rule = r
  547. if matchCount == 0:
  548. absTarget = os.path.abspath (target)
  549. if not os.path.exists (absTarget):
  550. print (BOLD_RED () + "No rule for making '" + target + "'" + ENDC ())
  551. self.mErrorCount = self.mErrorCount + 1
  552. return False # Error or target exists, and no rule for building it
  553. elif matchCount > 1:
  554. print (BOLD_RED () + str (matchCount) + " rules for making '" + target + "'" + ENDC ())
  555. self.mErrorCount = self.mErrorCount + 1
  556. return False # Error
  557. #--- Target file does not exist, and 'rule' variable indicates how build it
  558. appendToJobList = not os.path.exists (absTarget)
  559. #--- Build primary dependences
  560. jobDependenceFiles = []
  561. for dependence in rule.mDependences:
  562. willBeBuilt = self.makeJob (dependence)
  563. if willBeBuilt:
  564. jobDependenceFiles.append (dependence)
  565. appendToJobList = True
  566. #--- Check primary file modification dates
  567. if not appendToJobList:
  568. targetDateModification = os.path.getmtime (absTarget)
  569. for source in rule.mDependences:
  570. sourceDateModification = os.path.getmtime (source)
  571. if targetDateModification < sourceDateModification:
  572. appendToJobList = True
  573. break
  574. #--- Check for secondary dependancy files
  575. if not appendToJobList:
  576. targetDateModification = os.path.getmtime (absTarget)
  577. if targetDateModification < rule.mSecondaryMostRecentModificationDate:
  578. appendToJobList = True
  579. #--- Append to job list
  580. if appendToJobList:
  581. self.mJobList.append (Job (
  582. rule.mTargets,
  583. jobDependenceFiles,
  584. rule.mCommand,
  585. rule.mPostCommands,
  586. rule.mPriority,
  587. rule.mTitle,
  588. rule.mOpenSourceOnError,
  589. self.mLogUtilityTool
  590. ))
  591. #--- Return
  592. return appendToJobList
  593. #····················································································································*
  594. #Job state
  595. # 0: waiting
  596. # 1:running
  597. # 2: waiting for executing post command
  598. # 3:executing for executing post command
  599. # 4: completed
  600. def runJobs (self, maxConcurrentJobs, showCommand):
  601. if self.mErrorCount == 0:
  602. if len (self.mJobList) == 0:
  603. print (BOLD_BLUE () + "Nothing to make." + ENDC ())
  604. else:
  605. #--- Sort jobs following their priorities
  606. self.mJobList = sorted (self.mJobList, key=operator.attrgetter("mPriority"), reverse=True)
  607. #--- Run
  608. if maxConcurrentJobs <= 0:
  609. maxConcurrentJobs = processorCount () - maxConcurrentJobs
  610. jobCount = 0 ;
  611. terminationSemaphore = threading.Semaphore (0)
  612. displayLock = threading.Lock ()
  613. loop = True
  614. returnCode = 0
  615. totalJobCount = float (len (self.mJobList))
  616. launchedJobCount = 0.0
  617. while loop:
  618. #--- Launch jobs in parallel
  619. for job in self.mJobList:
  620. if (returnCode == 0) and (jobCount < maxConcurrentJobs):
  621. if (job.mState == 0) and (len (job.mRequiredFiles) == 0):
  622. #--- Create target directory if does not exist
  623. for aTarget in job.mTargets:
  624. absTargetDirectory = os.path.dirname (os.path.abspath (aTarget))
  625. if not os.path.exists (absTargetDirectory):
  626. displayLock.acquire ()
  627. runCommand (
  628. ["mkdir", "-p", os.path.dirname (aTarget)], "Making \"" + os.path.dirname (aTarget) + "\" directory",
  629. showCommand,
  630. job.mLogUtilityTool
  631. )
  632. displayLock.release ()
  633. #--- Progress string
  634. launchedJobCount += 1.0
  635. if self.mShowProgressString:
  636. progressString = "[{0:3d}%] ".format (int (100.0 * launchedJobCount / totalJobCount))
  637. else:
  638. progressString = ""
  639. #--- Run job
  640. job.run (displayLock, terminationSemaphore, showCommand, progressString)
  641. jobCount = jobCount + 1
  642. job.mState = 1 # Means is running
  643. elif job.mState == 2: # Waiting for executing post command
  644. job.mReturnCode = None # Means post command not terminated
  645. job.runPostCommand (displayLock, terminationSemaphore, showCommand)
  646. jobCount = jobCount + 1
  647. job.mState = 3 # Means post command is running
  648. #--- Wait for a job termination
  649. #print ("wait " + str (jobCount) + " " + str (len (self.mJobList)))
  650. terminationSemaphore.acquire ()
  651. #--- Checks for terminated jobs
  652. index = 0
  653. while index < len (self.mJobList):
  654. job = self.mJobList [index]
  655. index = index + 1
  656. if (job.mState == 1) and (job.mReturnCode != None) and (job.mReturnCode == 0) : # Terminated without error
  657. jobCount = jobCount - 1
  658. for aTarget in job.mTargets:
  659. if not os.path.exists (os.path.abspath (aTarget)): # Warning: target does not exist
  660. displayLock.acquire ()
  661. print (MAGENTA () + BOLD () + "Warning: target \"" + aTarget + "\" was not created by rule execution." + ENDC ())
  662. displayLock.release ()
  663. if len (job.mPostCommands) > 0:
  664. job.mState = 2 # Ready to execute next post command
  665. else:
  666. job.mState = 4 # Completed
  667. index = index - 1 # For removing job from list
  668. elif (job.mState == 1) and (job.mReturnCode != None) and (job.mReturnCode > 0) : # terminated with error : exit
  669. jobCount = jobCount - 1
  670. job.mState = 4 # Means Terminated
  671. index = index - 1 # For removing job from list
  672. if job.mOpenSourceOnError:
  673. for line in job.mOutputLines:
  674. components = line.split (':')
  675. if (len (components) > 1) and os.path.exists (os.path.abspath (components [0])) :
  676. if sys.platform == "darwin":
  677. os.system ("open -a \"" + self.mMacTextEditor + "\" \"" + components [0] + "\"")
  678. elif sys.platform == "linux2":
  679. os.system ("\"" + self.mLinuxTextEditor + "\" \"" + components [0] + "\"")
  680. elif (job.mState == 3) and (job.mReturnCode == 0): # post command is terminated without error
  681. jobCount = jobCount - 1
  682. job.mPostCommands.pop (0) # Remove completed post command
  683. if len (job.mPostCommands) > 0:
  684. job.mState = 2 # Ready to execute next post command
  685. else:
  686. job.mState = 4 # Completed
  687. index = index - 1 # For removing job from list
  688. elif (job.mState == 3) and (job.mReturnCode > 0): # post command is terminated with error
  689. jobCount = jobCount - 1
  690. job.mState = 4 # Completed
  691. index = index - 1 # For removing job from list
  692. elif job.mState == 4: # Completed: delete job
  693. index = index - 1
  694. self.mJobList.pop (index) # Remove terminated job
  695. #displayLock.acquire ()
  696. #print ("Completed '" + job.mTitle + "'")
  697. #--- Remove dependences from this job
  698. idx = 0
  699. while idx < len (self.mJobList):
  700. aJob = self.mJobList [idx]
  701. idx = idx + 1
  702. for aTarget in job.mTargets:
  703. while aJob.mRequiredFiles.count (aTarget) > 0 :
  704. aJob.mRequiredFiles.remove (aTarget)
  705. #print (" Removed from '" + aJob.mTitle + "': " + str (len (aJob.mRequiredFiles)))
  706. #displayLock.release ()
  707. #--- Signal error ?
  708. if (job.mReturnCode > 0) and (returnCode == 0):
  709. self.mErrorCount = self.mErrorCount + 1
  710. print (BOLD_RED () + "Return code: " + str (job.mReturnCode) + ENDC ())
  711. if (returnCode == 0) and (jobCount > 0) :
  712. print ("Wait for job termination...")
  713. returnCode = job.mReturnCode
  714. loop = (len (self.mJobList) > 0) if (returnCode == 0) else (jobCount > 0)
  715. #····················································································································*
  716. def searchFileInRelativeDirectories (self, file, directoryList): # returns "" if not found, register error
  717. matchCount = 0
  718. result = ""
  719. for sourceDir in directoryList:
  720. sourcePath = sourceDir + "/" + file
  721. if os.path.exists (os.path.abspath (sourcePath)):
  722. matchCount = matchCount + 1
  723. prefix = os.path.commonprefix ([os.getcwd (), sourcePath])
  724. result = sourcePath [len (prefix):]
  725. if result [0] == '/' :
  726. result = result [1:]
  727. if matchCount == 0:
  728. print (BOLD_RED () + "Cannot find '" + file + "'" + ENDC ())
  729. self.mErrorCount = self.mErrorCount + 1
  730. elif matchCount > 1:
  731. print (BOLD_RED () + str (matchCount) + " source files for making '" + file + "'" + ENDC ())
  732. self.mErrorCount = self.mErrorCount + 1
  733. result = ""
  734. return result
  735. #····················································································································*
  736. def searchFileInDirectories (self, file, directoryList): # returns "" if not found, register error
  737. matchCount = 0
  738. result = ""
  739. for sourceDir in directoryList:
  740. sourcePath = sourceDir + "/" + file
  741. if os.path.exists (os.path.abspath (sourcePath)):
  742. matchCount = matchCount + 1
  743. result = sourcePath
  744. if matchCount == 0:
  745. print (BOLD_RED () + "Cannot find '" + file + "'" + ENDC ())
  746. self.mErrorCount = self.mErrorCount + 1
  747. elif matchCount > 1:
  748. print (BOLD_RED () + str (matchCount) + " source files for making '" + file + "'" + ENDC ())
  749. self.mErrorCount = self.mErrorCount + 1
  750. result = ""
  751. return result
  752. #····················································································································*
  753. def addGoal (self, goal, targetList, message):
  754. if not argumentIsString (goal):
  755. print (BOLD_RED () + "*** Make.addGoal: 'goal' first argument is not a string ***" + ENDC ())
  756. traceback.print_stack ()
  757. sys.exit (1)
  758. if not type (targetList) is list:
  759. print (BOLD_RED () + "*** Make.addGoal: 'targetList' second argument is not a list ***" + ENDC ())
  760. traceback.print_stack ()
  761. sys.exit (1)
  762. else:
  763. for aTarget in targetList:
  764. if not argumentIsString (aTarget):
  765. print (BOLD_RED () + "*** Make.addGoal: an element of 'targetList' second argument 'targets' is not a string ***" + ENDC ())
  766. traceback.print_stack ()
  767. sys.exit (1)
  768. if not argumentIsString (message):
  769. print (BOLD_RED () + "*** Make.addGoal: 'message' third argument is not a string ***" + ENDC ())
  770. traceback.print_stack ()
  771. sys.exit (1)
  772. if (goal in self.mGoals) or (goal == "clean") :
  773. self.enterError ("The '" + goal + "' goal is already defined")
  774. else:
  775. self.mGoals [goal] = (targetList, message)
  776. #print ('%s' % ', '.join(map(str, self.mGoals)))
  777. #····················································································································*
  778. def printGoals (self):
  779. print (BOLD_BLUE () + "--- Print " + str (len (self.mGoals)) + " goal" + ("s" if len (self.mGoals) > 1 else "") + " ---" + ENDC ())
  780. for goalKey in self.mGoals.keys ():
  781. print (BOLD_GREEN () + "Goal: \"" + goalKey + "\"" + ENDC ())
  782. (targetList, message) = self.mGoals [goalKey]
  783. for target in targetList:
  784. print (" Target: \"" + target + "\"")
  785. print (" Message: \"" + message + "\"")
  786. print (BOLD_BLUE () + "--- End of print goals ---" + ENDC ())
  787. #····················································································································*
  788. def runGoal (self, maxConcurrentJobs, showCommand):
  789. if not isinstance (maxConcurrentJobs, int):
  790. print (BOLD_RED () + "*** Make.runGoal: 'maxConcurrentJobs' first argument is not an integer ***" + ENDC ())
  791. traceback.print_stack ()
  792. sys.exit (1)
  793. if not isinstance (showCommand, bool):
  794. print (BOLD_RED () + "*** Make.runGoal: 'showCommand' second argument is not a boolean ***" + ENDC ())
  795. traceback.print_stack ()
  796. sys.exit (1)
  797. if self.mSelectedGoal in self.mGoals :
  798. (targetList, message) = self.mGoals [self.mSelectedGoal]
  799. for target in targetList:
  800. self.makeJob (target)
  801. self.runJobs (maxConcurrentJobs, showCommand)
  802. if self.mErrorCount > 0:
  803. for rule in self.mRuleList:
  804. for aTarget in rule.mTargets:
  805. if rule.mDeleteTargetOnError and os.path.exists (os.path.abspath (aTarget)):
  806. runCommand (["rm", aTarget], "Delete \"" + aTarget + "\" on error", showCommand, self.mLogUtilityTool)
  807. elif self.mSelectedGoal == "clean" :
  808. filesToRemoveList = []
  809. directoriesToRemoveSet = set ()
  810. for rule in self.mRuleList:
  811. if rule.mCleanOperation == 1: # Delete target
  812. for aTarget in rule.mTargets:
  813. filesToRemoveList.append (aTarget)
  814. elif rule.mCleanOperation == 2: # Delete target directories
  815. for aTarget in rule.mTargets:
  816. dirPath = os.path.dirname (aTarget)
  817. if dirPath == "":
  818. filesToRemoveList.append (aTarget)
  819. else:
  820. directoriesToRemoveSet.add (dirPath)
  821. for dir in directoriesToRemoveSet:
  822. if os.path.exists (os.path.abspath (dir)):
  823. if self.mSimulateClean:
  824. print (MAGENTA () + BOLD () + "Simulated clean command: " + ENDC () + "rm -fr '" + dir + "'")
  825. else:
  826. runCommand (["rm", "-fr", dir], "Removing \"" + dir + "\"", showCommand, self.mLogUtilityTool)
  827. for file in filesToRemoveList:
  828. if os.path.exists (os.path.abspath (file)):
  829. if self.mSimulateClean:
  830. print (MAGENTA () + BOLD () + "Simulated clean command: " + ENDC () + "rm -f '" + file + "'")
  831. else:
  832. runCommand (["rm", "-f", file], "Deleting \"" + file + "\"", showCommand, self.mLogUtilityTool)
  833. else:
  834. errorMessage = "The '" + self.mSelectedGoal + "' goal is not defined; defined goals:"
  835. for key in self.mGoals:
  836. (targetList, message) = self.mGoals [key]
  837. errorMessage += "\n '" + key + "': " + message
  838. print (BOLD_RED () + errorMessage + ENDC ())
  839. self.mErrorCount = self.mErrorCount + 1
  840. #····················································································································*
  841. def simulateClean (self):
  842. self.mSimulateClean = True
  843. #····················································································································*
  844. def enterError (self, message):
  845. print (BOLD_RED () + message + ENDC ())
  846. self.mErrorCount = self.mErrorCount + 1
  847. #····················································································································*
  848. def printErrorCountAndExitOnError (self):
  849. if self.mErrorCount == 1:
  850. print (BOLD_RED () + "1 error." + ENDC ())
  851. sys.exit (1)
  852. elif self.mErrorCount > 1:
  853. print (BOLD_RED () + str (self.mErrorCount) + " errors." + ENDC ())
  854. sys.exit (1)
  855. #····················································································································*
  856. def printErrorCount (self):
  857. if self.mErrorCount == 1:
  858. print (BOLD_RED () + "1 error." + ENDC ())
  859. elif self.mErrorCount > 1:
  860. print (BOLD_RED () + str (self.mErrorCount) + " errors." + ENDC ())
  861. #····················································································································*
  862. def errorCount (self):
  863. return self.mErrorCount
  864. #——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————*