build.py 16 KB

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