build.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. #!/usr/bin/python
  2. #
  3. # Python 2.7 script for building the BlackBox Component Builder for Windows under Linux Debian 7.
  4. # Looks at all branches and puts the output into the branch's output folder 'unstable/<branch>'
  5. # unless building a final release, which is always put into folder 'stable'.
  6. #
  7. # Ivan Denisov, Josef Templ
  8. #
  9. # use: "build.py -h" to get a short help text
  10. #
  11. # Creates 3 files in case of success:
  12. # 1. a build log file named blackbox-<AppVersion>.<buildnr>-buildlog.html
  13. # 2. a Windows installer file named blackbox-<AppVersion>.<buildnr>-setup.exe
  14. # 3. a zipped package named blackbox-<AppVersion>-<buildnr>.zip
  15. # In case of building a final release, buildnr is not included.
  16. # In case building was started for a branch, updates the branch's last-build-commit hash.
  17. # In case of successfully finishing the build, increments the global build number.
  18. #
  19. # By always rebuilding bbscript.exe it avoids problems with changes in the symbol or object file formats
  20. # and acts as a rigorous test for some parts of BlackBox, in particular for the compiler itself.
  21. # This script uses the general purpose 'bbscript' scripting engine for BlackBox, which
  22. # can be found in the subsystem named 'Script'.
  23. #
  24. # Error handling:
  25. # Stops building when shellExec writes to stderr, unless stopOnError is False.
  26. # Stops building when there is an error reported by bbscript.
  27. # Stops building when there is a Python exception.
  28. # The next build will take place upon the next commit.
  29. #
  30. # TODO git checkout reports a message on stderr but it works, so it is ignored
  31. from subprocess import Popen, PIPE, call
  32. import sys, datetime, fileinput, os.path, argparse, urllib2, time
  33. import xml.etree.ElementTree as ET
  34. buildDate = datetime.datetime.now().isoformat()[:19]
  35. buildDir = "/var/www/zenario/makeapp"
  36. bbName = "bb"
  37. bbDir = buildDir + "/" + bbName
  38. appbuildDir = bbDir + "/appbuild"
  39. localRepository = "/var/www/git/blackbox.git"
  40. unstableDir = "/var/www/zenario/unstable"
  41. stableDir = "/var/www/zenario/stable"
  42. wine = "/usr/local/bin/wine"
  43. xvfb = "xvfb-run --server-args='-screen 1, 1024x768x24' "
  44. bbscript = xvfb + wine + " bbscript.exe"
  45. bbchanges = xvfb + wine + " " + buildDir + "/bbchanges.exe /USE " + bbDir + " /LOAD ScriptChanges"
  46. iscc = "/usr/local/bin/iscc"
  47. windres="/usr/bin/i586-mingw32msvc-windres"
  48. testName = "testbuild"
  49. branch = None
  50. commitHash = None
  51. logFile = None
  52. outputNamePrefix = None # until appVersion and build number are known
  53. buildNumberIncremented = False
  54. parser = argparse.ArgumentParser(description='Build BlackBox')
  55. parser.add_argument('--verbose', action="store_true", default=False, help='turn verbose output on')
  56. parser.add_argument('--test', action="store_true", default=False, help='put all results into local directory "' + testName + '"')
  57. parser.add_argument('--branch', help='select BRANCH for building')
  58. args = parser.parse_args()
  59. def repositoryLocked():
  60. return os.path.exists(localRepository + ".lock")
  61. def hashFilePath():
  62. return buildDir + "/lastBuildHash/" + branch
  63. def getLastHash():
  64. if os.path.exists(hashFilePath()):
  65. hashFile = open(hashFilePath(), "r")
  66. commit = hashFile.readline().strip()
  67. hashFile.close()
  68. return commit
  69. else:
  70. return ""
  71. def getCommitHash():
  72. gitLog = shellExec(localRepository, "git log " + branch + " -1")
  73. global commitHash
  74. commitHash = gitLog.split("\n")[0].split(" ")[1]
  75. return commitHash
  76. def needsRebuild():
  77. return getLastHash() != getCommitHash()
  78. def selectBranch():
  79. global branch
  80. if args.branch != None:
  81. branch = args.branch
  82. getCommitHash()
  83. return branch
  84. else:
  85. branches = shellExec(localRepository, "git branch -a")
  86. for line in branches.split("\n"):
  87. branch = line[2:].strip()
  88. if branch != "" and needsRebuild():
  89. return branch
  90. return None
  91. def openLog():
  92. global logFile
  93. logFile = open(unstableDir + "/logFile.html", "w")
  94. def logVerbose(text):
  95. if args.verbose:
  96. print text # for testing, goes to console
  97. def log(text, startMarkup="", endMarkup=""):
  98. if text != "":
  99. if logFile != None:
  100. for line in text.split("\n"):
  101. logFile.write(startMarkup + line + endMarkup + "<br/>\n")
  102. logFile.flush()
  103. elif args.verbose:
  104. for line in text.split("\n"):
  105. logVerbose(line)
  106. def logErr(text): # use color red
  107. log(text, '<font color="#FF0000">', '</font>')
  108. def logStep(text): # use bold font
  109. log(text, '<b>', '</b>')
  110. def logShell(text): # use color green
  111. log(text, '<font color="#009600">', '</font>')
  112. def shellExec(wd, cmd, stopOnError=True):
  113. cmd = "cd " + wd + " && " + cmd
  114. logShell(cmd)
  115. (stdout, stderr) = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True).communicate()
  116. log(stdout)
  117. if stderr == "":
  118. return stdout
  119. elif not stopOnError:
  120. logErr(stderr)
  121. logErr("--- error ignored ---")
  122. return stdout
  123. else:
  124. logErr(stderr)
  125. logErr("--- build aborted ---")
  126. print "--- build aborted ---\n"
  127. incrementBuildNumber() # if not args.test
  128. cleanup() # if not args.test
  129. renameLog() # if not args.test
  130. sys.exit()
  131. def getAppVerName(appVersion):
  132. x = appVersion
  133. if appVersion.find("-a") >= 0:
  134. x = appVersion.replace("-a", " Alpha ")
  135. elif appVersion.find("-b") >= 0:
  136. x = appVersion.replace("-b", " Beta ")
  137. elif appVersion.find("-rc") >= 0:
  138. x = appVersion.replace("-rc", " Release Candidate ")
  139. return "BlackBox Component Builder " + x
  140. def getVersionInfoVersion(appVersion, buildNum):
  141. version = appVersion.split("-")[0]
  142. v = version.split(".")
  143. v0 = v[0] if len(v) > 0 else "0"
  144. v1 = v[1] if len(v) > 1 else "0"
  145. v2 = v[2] if len(v) > 2 else "0"
  146. return v0 + "." + v1 + "." + v2 + "." + str(buildNum)
  147. def isFinal(appVersion):
  148. return appVersion.find("-") < 0
  149. def prepareCompileAndLink():
  150. logStep("Preparing BlackBox.rc")
  151. vrsn = versionInfoVersion.replace(".", ",")
  152. shellExec(bbDir + "/Win/Rsrc", "mv BlackBox.rc BlackBox.rc_template")
  153. shellExec(bbDir + "/Win/Rsrc", "sed s/{#VERSION}/" + vrsn + "/ < BlackBox.rc_template > BlackBox.rc")
  154. shellExec(bbDir + "/Win/Rsrc", "rm BlackBox.rc_template")
  155. logStep("Creating the BlackBox.res resource file")
  156. shellExec(bbDir + "/Win/Rsrc", windres + " -i BlackBox.rc -o BlackBox.res")
  157. logStep("Preparing bbscript.exe")
  158. shellExec(buildDir, "cp bbscript.exe " + bbDir + "/")
  159. def deleteBbFile(name):
  160. if os.path.exists(bbDir + "/" + name):
  161. shellExec(bbDir, "rm " + name)
  162. def runbbscript(fileName):
  163. deleteBbFile("StdLog.txt");
  164. # fileName is relative to bbscript.exe startup directory, which is bbDir
  165. # if a /USE param is useed it must an absolute path, otherwise some texts cannot be opened, e.g Converters.
  166. cmd = "cd " + bbDir + " && " + bbscript + ' /PAR "' + fileName + '"'
  167. logShell(cmd)
  168. bbres = call(cmd + " >wine_out.txt 2>&1", shell=True) # wine produces irrelevant error messages
  169. if bbres != 0:
  170. shellExec(bbDir, "cat StdLog.txt", False)
  171. cleanup()
  172. logErr("--- build aborted ---")
  173. renameLog() # if not args.test
  174. sys.exit()
  175. def compileAndLink():
  176. logStep("Compiling and linking BlackBox")
  177. runbbscript("Dev/Docu/Build-Tool.odc")
  178. shellExec(bbDir, "mv BlackBox2.exe BlackBox.exe && mv Code System/ && mv Sym System/")
  179. def buildBbscript():
  180. logStep("Incrementally building BlackBox scripting engine bbscript.exe")
  181. runbbscript("appbuild/newbbscript.txt")
  182. shellExec(bbDir, "mv newbbscript.exe bbscript.exe && chmod a+x bbscript.exe")
  183. shellExec(bbDir, "rm -R Code Sym */Code */Sym BlackBox.exe")
  184. def appendSystemProperties():
  185. logStep("Setting system properties in appendProps.txt")
  186. shellExec(appbuildDir, 'sed s/{#AppVersion}/"' + appVersion + '"/ < appendProps.txt > appendProps_.txt')
  187. shellExec(appbuildDir, 'sed s/{#AppVerName}/"' + appVerName + '"/ < appendProps_.txt > appendProps.txt')
  188. shellExec(appbuildDir, "sed s/{#FileVersion}/" + versionInfoVersion + "/ < appendProps.txt > appendProps_.txt")
  189. shellExec(appbuildDir, "sed s/{#BuildNum}/" + str(buildNum) + "/ < appendProps_.txt > appendProps.txt")
  190. shellExec(appbuildDir, "sed s/{#BuildDate}/" + buildDate[:10] + "/ < appendProps.txt > appendProps_.txt")
  191. shellExec(appbuildDir, "sed s/{#CommitHash}/" + commitHash + "/ < appendProps_.txt > appendProps.txt")
  192. logStep("Appending version properties to System/Rsrc/Strings.odc")
  193. runbbscript("appbuild/appendProps.txt")
  194. def updateBbscript():
  195. if not args.test and branch == "master":
  196. logStep("Updating bbscript.exe")
  197. shellExec(bbDir, "mv bbscript.exe " + buildDir + "/")
  198. else:
  199. logStep("Removing bbscript.exe")
  200. shellExec(bbDir, "rm bbscript.exe ")
  201. def get_fixed_version_id(versions_file, target):
  202. tree = ET.parse(versions_file)
  203. root = tree.getroot()
  204. for version in root.findall('version'):
  205. if version.findtext('name') == target:
  206. return version.findtext('id')
  207. return "-1" # unknown
  208. def addChanges():
  209. if branch == "master" or args.test:
  210. logStep("downloading xml files from Redmine")
  211. versions_file = bbDir + "/blackbox_versions.xml"
  212. url = "http://redmine.blackboxframework.org/projects/blackbox/versions.xml"
  213. with open(versions_file, 'wb') as out_file:
  214. out_file.write(urllib2.urlopen(url).read())
  215. minusPos = appVersion.find("-")
  216. target = appVersion if minusPos < 0 else appVersion[0:minusPos]
  217. fixed_version_id = get_fixed_version_id(versions_file, target)
  218. # status_id=5 means 'Closed', limit above 100 is not supported by Redmine
  219. url = "http://redmine.blackboxframework.org/projects/blackbox/issues.xml?status_id=5&fixed_version_id=" + fixed_version_id + "&offset=0&limit=100"
  220. issues_file1 = bbDir + "/blackbox_issues100.xml"
  221. with open(issues_file1, 'wb') as out_file:
  222. out_file.write(urllib2.urlopen(url).read())
  223. url = "http://redmine.blackboxframework.org/projects/blackbox/issues.xml?status_id=5&fixed_version_id=" + fixed_version_id + "&offset=100&limit=100"
  224. issues_file2 = bbDir + "/blackbox_issues200.xml"
  225. with open(issues_file2, 'wb') as out_file:
  226. out_file.write(urllib2.urlopen(url).read())
  227. logStep("converting to BlackBox_" + appVersion + "_Changes.odc/.html")
  228. bbres = call(bbchanges + " >" + bbDir + "/wine_out.txt 2>&1", shell=True)
  229. logStep("removing xml files")
  230. shellExec(".", "rm " + versions_file + " " + issues_file1 + " " + issues_file2)
  231. logStep("moving file BlackBox_" + appVersion + "_Changes.html to outputDir")
  232. shellExec(".", "mv " + bbDir + "/BlackBox_" + appVersion + "_Changes.html " + outputPathPrefix + "-changes.html")
  233. def buildSetupFile():
  234. logStep("Building " + outputNamePrefix + "-setup.exe file using InnoSetup")
  235. deleteBbFile("StdLog.txt");
  236. deleteBbFile("wine_out.txt");
  237. deleteBbFile("README.txt");
  238. shellExec(bbDir, "rm -R Script appbuild")
  239. shellExec(bbDir, iscc + " - < Win/Rsrc/BlackBox.iss" \
  240. + ' "/dAppVersion=' + appVersion
  241. + '" "/dAppVerName=' + appVerName
  242. + '" "/dVersionInfoVersion=' + versionInfoVersion
  243. + '"', False) # a meaningless error is displayed
  244. shellExec(bbDir, "mv Output/setup.exe " + outputPathPrefix + "-setup.exe")
  245. shellExec(bbDir, "rm -R Output")
  246. def buildZipFile():
  247. deleteBbFile("LICENSE.txt")
  248. logStep("Zipping package to file " + outputNamePrefix + ".zip")
  249. shellExec(bbDir, "zip -r " + outputPathPrefix + ".zip *")
  250. def updateCommitHash():
  251. if not args.test:
  252. logStep("Updating commit hash for branch '" + branch + "'")
  253. hashFile = open(hashFilePath(), "w")
  254. hashFile.write(commitHash)
  255. hashFile.close()
  256. def incrementBuildNumber():
  257. global buildNumberIncremented
  258. if not buildNumberIncremented:
  259. logStep("Updating build number to " + str(buildNum + 1))
  260. numberFile.seek(0)
  261. numberFile.write(str(buildNum+1))
  262. numberFile.truncate()
  263. numberFile.close()
  264. buildNumberIncremented = True
  265. def cleanup():
  266. if not args.test:
  267. logStep("Cleaning up")
  268. shellExec(buildDir, "rm -R -f " + bbDir)
  269. def renameLog():
  270. global logFile
  271. logFile.close()
  272. logFile = None
  273. if not args.test and outputNamePrefix != None:
  274. logStep("Renaming 'logFile.html' to '" + outputNamePrefix + "-buildlog.html'")
  275. shellExec(unstableDir, "mv logFile.html " + outputPathPrefix + "-buildlog.html")
  276. if args.test:
  277. buildNumberIncremented = True # avoid side effect when testing
  278. unstableDir = buildDir + "/" + testName
  279. stableDir = unstableDir
  280. if (os.path.exists(bbDir)):
  281. shellExec(buildDir, "rm -R -f " + bbDir)
  282. if (os.path.exists(unstableDir)):
  283. shellExec(buildDir, "rm -R -f " + testName)
  284. shellExec(buildDir, "mkdir " + testName)
  285. if os.path.exists(bbDir): # previous build is still running or was terminated after an error
  286. logVerbose("no build because directory '" + bbDir + "' exists")
  287. sys.exit()
  288. if repositoryLocked():
  289. logVerbose("no build because repository is locked; probably due to sync process")
  290. sys.exit()
  291. if selectBranch() == None:
  292. logVerbose("no build because no new commit in any branch")
  293. sys.exit()
  294. updateCommitHash() # if not args.test
  295. # this file contains the build number to be used for this build; incremented after successfull build
  296. numberFile = open(buildDir + "/" + "number", "r+")
  297. buildNum = int(numberFile.readline().strip())
  298. openLog()
  299. log("<h2>Build " + str(buildNum) + " from '" + branch + "' at " + buildDate + "</h2>")
  300. log("<h3>git commit hash: " + commitHash + "</h3>")
  301. logStep("Cloning repository into temporary folder '" + bbName + "'")
  302. # option -q suppresses the progress reporting on stderr
  303. shellExec(buildDir, "git clone -q --branch " + branch + " " + localRepository + " " + bbDir)
  304. if not os.path.exists(appbuildDir + "/AppVersion.txt"):
  305. cleanup() # if not args.test
  306. logStep('No build because file "appbuild/AppVersion.txt" not in branch')
  307. sys.exit()
  308. print "<br/>Build " + str(buildNum) + " from '" + branch + "' at " + buildDate + "<br/>" # goes to buildlog.html
  309. appVersion = open(appbuildDir + "/AppVersion.txt", "r").readline().strip()
  310. appVerName = getAppVerName(appVersion)
  311. versionInfoVersion = getVersionInfoVersion(appVersion, buildNum)
  312. finalRelease = isFinal(appVersion)
  313. outputNamePrefix = "blackbox-" + appVersion + ("" if finalRelease else ("." + str(buildNum).zfill(3)))
  314. outputDir = stableDir if finalRelease else unstableDir + "/" + branch
  315. outputPathPrefix = outputDir + "/" + outputNamePrefix
  316. if not os.path.exists(outputDir):
  317. shellExec(buildDir, "mkdir " + outputDir)
  318. prepareCompileAndLink()
  319. compileAndLink() #1
  320. buildBbscript()
  321. compileAndLink() #2
  322. buildBbscript()
  323. compileAndLink() #3
  324. appendSystemProperties()
  325. updateBbscript()
  326. addChanges()
  327. buildSetupFile()
  328. buildZipFile()
  329. # if not args.test
  330. incrementBuildNumber()
  331. cleanup()
  332. renameLog()