#! /usr/bin/env python ########################################################################## ## ## ## Build script of the Nimrod Compiler ## ## (c) 2008 Andreas Rumpf ## ## ## ########################################################################## import os, os.path, sys, re, shutil, cPickle, time, getopt, glob, zlib from string import split, replace, lower, join, find, strip if sys.version[0] >= "3": # this script does not work with Python 3.0 sys.exit("wrong python version: use Python 1.5.2 - 2.6") True = 0 == 0 # Python 1.5 does not have True and False :-( False = 0 == 1 # --------------------- constants ---------------------------------------- NIMROD_VERSION = '0.7.4' # This string contains Nimrod's version. It is the only place # where the version needs to be updated. The rest is done by # the build process automatically. It is replaced **everywhere** # automatically! # Format is: Major.Minor.Patch # Major part: plan is to use number 1 for the first version that is stable; # higher versions may be incompatible with previous versions # Minor part: incremented if new features are added (but is completely # backwards-compatible) # Patch level: is increased for every patch EXPLAIN = True force = False GENERATE_DIFF = False # if set, a diff.log file is generated when bootstrapping USE_FPC = True BOOTCMD = "%s cc --compile:build/platdef.c %s rod/nimrod.nim" # the command used for bootstrapping # -------------------------------------------------------------------------- def Error(msg): sys.exit("[Koch] *** ERROR: " + msg) def Warn(msg): print "[Koch] *** WARNING: " + msg def Echo(msg): print "[Koch] " + msg def _Info(msg): print "[Koch] " + msg _FINGERPRINTS_FILE = "koch.dat" # in this file all the fingerprints are kept to allow recognizing when a file # has changed. This works reliably, which cannot be said from just taking # filetime-stamps. def FileCmp(filenameA, filenameB): SIZE = 4096*2 result = True a = open(filenameA, "rb") b = open(filenameB, "rb") while True: x = a.read(SIZE) y = b.read(SIZE) if x != y: result = False break elif len(x) < SIZE: # EOF? break a.close() b.close() return result def Subs(frmt, **substitution): import string chars = string.digits+string.letters+"_" d = substitution result = [] i = 0 while i < len(frmt): if frmt[i] == '$': i = i+1 if frmt[i] == '$': result.append('$') i = i+1 elif frmt[i] == '{': i = i+1 j = i while frmt[i] != '}': i = i+1 i = i+1 # skip } result.append(d[frmt[j:i-1]]) elif frmt[i] in string.letters+"_": j = i i = i+1 while i < len(frmt) and frmt[i] in chars: i = i + 1 result.append(d[frmt[j:i]]) else: assert(false) else: result.append(frmt[i]) i = i+1 return join(result, "") def SplitArg(s): if ':' in s: c = ':' elif '=' in s: c = '=' else: return (s, '') i = s.find(c) return (s[:i], s[i+1:]) _baseDir = os.getcwd() BaseDir = _baseDir def Path(a): # Gets a UNIX like path and converts it to a path on this platform. # With UNIX like, I mean: slashes, not backslashes, only relative # paths ('../etc') can be used result = a if os.sep != "/": result = replace(result, "/", os.sep) if os.pardir != "..": result = replace(result, "..", os.pardir) return result def Join(*args): result = [] for a in args[:-1]: result.append(a) if result[-1] != "/": result.append("/") result.append(args[-1]) return replace(join(result, ""), "//", "/") def Exec(command): c = Path(command) Echo(c) result = os.system(c) if result != 0: Error("execution of an external program failed") return result def TryExec(command): c = Path(command) Echo(c) result = os.system(c) return result def RawExec(command): Echo(command) result = os.system(command) if result != 0: Error("execution of an external program failed") return result def Remove(f): try: os.remove(Path(f)) except OSError: Warn("could not remove: %s" % f) def Move(src, dest): try: m = shutil.move except AttributeError: def f(src, dest): shutil.copy(src, dest) Remove(src) m = f s = Path(src) d = Path(dest) try: m(s, d) except IOError, OSError: Warn("could not move %s to %s" % (s, d)) def Copy(src, dest): s = Path(src) d = Path(dest) try: shutil.copyfile(s, d) except IOError, OSError: Warn("could not copy %s to %s" % (s, d)) def RemoveDir(f): try: shutil.rmtree(Path(f)) except OSError: Warn("could not remove: %s" % f) def Exists(f): return os.path.exists(Path(f)) def Chdir(dest): d = Path(dest) try: os.chdir(d) except OSError: Warn("could not switch to directory: " + d) def Mkdir(dest): d = Path(dest) try: os.mkdir(d) except OSError: Warn("could not create directory: " + d) def Glob(pattern): # needed because glob.glob() is buggy on Windows 95: # things like tests/t*.mor won't work global _baseDir (head, tail) = os.path.split(Path(pattern)) result = [] try: os.chdir(os.path.join(_baseDir, head)) try: for f in glob.glob(tail): result.append(os.path.join(head, f)) except OSError: result = [] finally: os.chdir(_baseDir) return result def FilenameNoExt(f): return os.path.splitext(os.path.basename(f))[0] def _Ext(trunc, posixFormat, winFormat): (head, tail) = os.path.split(Path(trunc)) if os.name == "posix": frmt = posixFormat else: frmt = winFormat return os.path.join(head, Subs(frmt, trunc=tail)) def DynExt(trunc): return _Ext(trunc, 'lib${trunc}.so', '${trunc}.dll') def LibExt(trunc): return _Ext(trunc, '${trunc}.a', '${trunc}.lib') def ScriptExt(trunc): return _Ext(trunc, '${trunc}.sh', '${trunc}.bat') def ExeExt(trunc): return _Ext(trunc, '${trunc}', '${trunc}.exe') def MakeExecutable(file): os.chmod(file, 493) class Changed: """ Returns a Changed object. This object evals to true if one of the given files has changed, false otherwise in a boolean context. You have to call the object's success() method if the building has been a success. Example: c = Changed("unique_name", "file1.pas file2.pas file3.pas") if c.check(): Exec("fpc file1.pas") # Exec raises an exception if it fails, thus if we get to here, it was # a success: c.success() """ def __init__(self, id, files, explain=False, fingerprintsfile=_FINGERPRINTS_FILE): # load the fingerprints file: # fingerprints is a dict[target, files] where files is a dict[filename, hash] self.fingers = {} # default value if Exists(fingerprintsfile): try: self.fingers = cPickle.load(open(fingerprintsfile)) except OSError: Error("Cannot read from " + fingerprintsfile) self.filename = fingerprintsfile self.id = id self.files = files self._hashStr = zlib.adler32 # our hash function self.explain = explain def _hashFile(self, f): x = open(f) result = self._hashStr(x.read()) x.close() # for other Python implementations return result def check(self): if type(self.files) == type(""): self.files = split(self.files) result = False target = self.id if not self.fingers.has_key(target): self.fingers[target] = {} if self.explain: _Info("no entries for target '%s'" % target) result = True for d in self.files: if Exists(d): n = self._hashFile(d) if not self.fingers[target].has_key(d) or n != self.fingers[target][d]: result = True if self.explain: _Info("'%s' modified since last build" % d) self.fingers[target][d] = n else: Warn("'%s' does not exist!" % d) result = True return result def update(self, filename): self.fingers[self.id][filename] = self._hashFile(filename) def success(self): cPickle.dump(self.fingers, open(self.filename, "w+")) # -------------------------------------------------------------------------- def CogRule(name, filename, dependson): def processCog(filename): from cogapp import Cog ret = Cog().main([sys.argv[0], "-r", Path(filename)]) return ret c = Changed(name, filename + " " + dependson, EXPLAIN) if c.check() or force: if processCog(filename) == 0: c.update(filename) c.success() else: Error("Cog failed") _nim_exe = os.path.join(os.getcwd(), "bin", ExeExt("nim")) _output_obj = os.path.join(os.getcwd(), "obj") FPC_CMD = (r"fpc -Cs16777216 -gl -bl -Crtoi -Sgidh -vw -Se1 -o%s " r"-FU%s %s") % (_nim_exe, _output_obj, os.path.join(os.getcwd(), "nim", "nimrod.pas")) def buildRod(options): Exec("nim compile --compile:build/platdef.c %s rod/nimrod" % options) Move(ExeExt("rod/nimrod"), ExeExt("bin/nimrod")) def cmd_nim(): CogRule("nversion", "nim/nversion.pas", "koch.py") CogRule("msgs", "nim/msgs.pas", "data/messages.yml") CogRule("ast", "nim/ast.pas", "koch.py data/magic.yml data/ast.yml") CogRule("scanner", "nim/scanner.pas", "data/keywords.txt") CogRule("paslex", "nim/paslex.pas", "data/pas_keyw.yml") CogRule("wordrecg", "nim/wordrecg.pas", "data/keywords.txt") CogRule("commands", "nim/commands.pas", "data/basicopt.txt data/advopt.txt") CogRule("macros", "lib/macros.nim", "koch.py data/ast.yml") c = Changed("nim", Glob("nim/*.pas"), EXPLAIN) if c.check() or force: Exec(FPC_CMD) if Exists(ExeExt("bin/nim")): c.success() return True return False def cmd_rod(options): prereqs = Glob("lib/*.nim") + Glob("rod/*.nim") + [ "lib/nimbase.h", "lib/dlmalloc.c", "lib/dlmalloc.h", "config/nimrod.cfg"] c = Changed("rod", prereqs, EXPLAIN) if c.check() or cmd_nim() or force: buildRod(options) if Exists(ExeExt("bin/nimrod")): c.success() # ------------------- constants ----------------------------------------------- HELP = """\ +-----------------------------------------------------------------+ | Maintenance script for Nimrod | | Version %s| | (c) 2008 Andreas Rumpf | +-----------------------------------------------------------------+ Your Python version: %s Usage: koch.py [options] command [options for command] Options: --force, -f, -B, -b forces rebuild --diff generates a diff.log file when bootstrapping --help, -h shows this help and quits --no_fpc bootstrap without FPC Possible Commands: nim builds the Pascal version of Nimrod rod [options] builds the Nimrod version of Nimrod (with options) clean cleans Nimrod project; removes generated files boot [options] bootstraps with given command line options rodsrc generates Nimrod version from Pascal version web generates the website profile profile the Nimrod compiler csource build the C sources for installation zip build the installation ZIP package inno build the Inno Setup installer """ % (NIMROD_VERSION + ' ' * (44-len(NIMROD_VERSION)), sys.version) def main(args): if len(args) == 0: print HELP else: i = 0 while args[i][:1] == "-": a = args[i] if a in ("--force", "-f", "-B", "-b"): global force force = True elif a in ("-h", "--help", "-?"): print HELP return elif a == "--diff": global GENERATE_DIFF GENERATE_DIFF = True elif a == "--no_fpc": global USE_FPC USE_FPC = False else: Error("illegal option: " + a) i = i + 1 cmd = args[i] if cmd == "rod": cmd_rod(join(args[i+1:])) elif cmd == "nim": cmd_nim() elif cmd == "clean": cmd_clean() elif cmd == "boot": cmd_boot(join(args[i+1:])) elif cmd == "rodsrc": cmd_rodsrc() elif cmd == "web": cmd_web() elif cmd == "profile": cmd_profile() elif cmd == "zip": cmd_zip() elif cmd == "inno": cmd_inno() elif cmd == "csource": cmd_csource() else: Error("illegal command: " + cmd) def cmd_csource(): Exec("nimrod cc -r tools/niminst --var:version=%s csource rod/nimrod" % NIMROD_VERSION) def cmd_zip(): Exec("nimrod cc -r tools/niminst --var:version=%s zip rod/nimrod" % NIMROD_VERSION) def cmd_inno(): Exec("nimrod cc -r tools/niminst --var:version=%s inno rod/nimrod" % NIMROD_VERSION) # -------------------------- bootstrap ---------------------------------------- def readCFiles(): result = {} if GENERATE_DIFF: for f in Glob("rod/nimcache/rod/*.c") + Glob("rod/nimcache/lib/*.c"): x = os.path.split(f)[1] result[x] = open(f).readlines()[1:] return result def genBootDiff(genA, genB): def interestingDiff(a, b): #a = re.sub(r"([a-zA-Z_]+)([0-9]+)", r"\1____", a) #b = re.sub(r"([a-zA-Z_]+)([0-9]+)", r"\1____", b) return a != b BOOTLOG = "bootdiff.log" result = False for f in Glob("diff/*.c"): Remove(f) if Exists(BOOTLOG): Remove(BOOTLOG) if GENERATE_DIFF: lines = [] # lines of the generated logfile if len(genA) != len(genB): Warn("number of generated files differ!") for filename, acontent in genA.iteritems(): bcontent = genB[filename] if bcontent != acontent: lines.append("------------------------------------------------------") lines.append(filename + " differs") # write the interesting lines to the log file: for i in range(min(len(acontent), len(bcontent))): la = acontent[i][:-1] # without newline! lb = bcontent[i][:-1] if interestingDiff(la, lb): lines.append("%6d - %s" % (i, la)) lines.append("%6d + %s" % (i, lb)) if len(acontent) > len(bcontent): cont = acontent marker = "-" else: cont = bcontent marker = "+" for i in range(min(len(acontent), len(bcontent)), len(cont)): lines.append("%6d %s %s" % (i, marker, cont[i])) open(os.path.join("diff", "a_"+filename), "w+").write(join(acontent, "")) open(os.path.join("diff", "b_"+filename), "w+").write(join(bcontent, "")) if lines: result = True open(BOOTLOG, "w+").write(join(lines, "\n")) return result def cmd_rodsrc(): "converts the src/*.pas files into Nimrod syntax" PAS_FILES_BLACKLIST = split("""nsystem nmath nos ntime strutils""") if USE_FPC and detect("fpc -h"): cmd_nim() compiler = "nim" else: compiler = "nimrod" CMD = "%s boot --skip_proj_cfg -o:rod/%s.nim nim/%s" result = False for fi in Glob("nim/*.pas"): f = FilenameNoExt(fi) if f in PAS_FILES_BLACKLIST: continue c = Changed(f+"__rodsrc", fi, EXPLAIN) if c.check() or force: Exec(CMD % (compiler, f, f+".pas")) Exec("%s parse rod/%s.nim" % (compiler, f)) c.success() result = True return result def moveExes(): Move(ExeExt("rod/nimrod"), ExeExt("bin/nimrod")) def cmd_boot(args): def myExec(compiler, args=args): Exec(BOOTCMD % (compiler, args)) # some C compilers (PellesC) output the executable to the # wrong directory. We work around this bug here: if Exists(ExeExt("rod/nimcache/nimrod")): Move(ExeExt("rod/nimcache/nimrod"), ExeExt("rod/nimrod")) writePlatdefC(getNimrodPath()) d = detect("fpc -h") if USE_FPC and d: Echo("'%s' detected" % d) cmd_nim() compiler = "nim" else: compiler = "nimrod" cmd_rodsrc() # regenerate nimrod version of the files # move the new executable to bin directory (is done by cmd_rod()) # use the new executable to compile the files in the bootstrap directory: myExec(compiler) genA = readCFiles() # first generation of generated C files # move the new executable to bin directory: moveExes() # compile again and compare: myExec("nimrod") genB = readCFiles() # second generation of generated C files diff = genBootDiff(genA, genB) if diff: Warn("generated C files are not equal: cycle once again...") # check if the executables are the same (they should!): if FileCmp(Path(ExeExt("rod/nimrod")), Path(ExeExt("bin/nimrod"))): Echo("executables are equal: SUCCESS!") else: Echo("executables are not equal: cycle once again...") diff = True if diff: # move the new executable to bin directory: moveExes() # use the new executable to compile Nimrod: myExec("nimrod") if FileCmp(Path(ExeExt("rod/nimrod")), Path(ExeExt("bin/nimrod"))): Echo("executables are equal: SUCCESS!") else: Warn("executables are still not equal") # ------------------ profile -------------------------------------------------- def cmd_profile(): Exec(BOOTCMD % ("nimrod", "-d:release --profiler:on")) moveExes() Exec(BOOTCMD % ("nimrod", "--compile_only")) # ------------------ web ------------------------------------------------------ def cmd_web(): Exec("nimrod cc -r tools/nimweb.nim web/nimrod --putenv:nimrodversion=%s" % NIMROD_VERSION) # ----------------------------------------------------------------------------- def getVersion(): return NIMROD_VERSION # ------------------------------ clean ---------------------------------------- CLEAN_EXT = "ppu o obj dcu ~pas ~inc ~dsk ~dpr map tds err bak pyc exe rod" def cmd_clean(dir = "."): L = [] for x in split(CLEAN_EXT): L.append(r".*\."+ x +"$") extRegEx = re.compile(join(L, "|")) if Exists("koch.dat"): Remove("koch.dat") for f in Glob("*.pdb"): Remove(f) for f in Glob("*.idb"): Remove(f) for f in Glob("web/*.html"): Remove(f) for f in Glob("doc/*.html"): Remove(f) for f in Glob("rod/*.nim"): Remove(f) # remove generated source code def visit(extRegEx, dirname, names): if os.path.split(dirname)[1] == "nimcache": shutil.rmtree(path=dirname, ignore_errors=True) del names else: for name in names: x = os.path.join(dirname, name) if os.path.isdir(x): continue if (extRegEx.match(name) or (os.path.split(dirname)[1] == "tests" and ('.' not in name))): if find(x, "/dist/") < 0 and find(x, "\\dist\\") < 0: Echo("removing: " + x) Remove(x) os.path.walk(dir, visit, extRegEx) def getHost(): # incomplete list that sys.platform may return: # win32 aix3 aix4 atheos beos5 darwin freebsd2 freebsd3 freebsd4 freebsd5 # freebsd6 freebsd7 generic irix5 irix6 linux2 mac netbsd1 next3 os2emx # riscos sunos5 unixware7 x = replace(lower(re.sub(r"[0-9]+$", r"", sys.platform)), "-", "") if x == "win": return "windows" elif x == "darwin": return "macosx" # probably Mac OS X elif x == "sunos": return "solaris" else: return x def mydirwalker(dir, L): for name in os.listdir(dir): path = os.path.join(dir, name) if os.path.isdir(path): mydirwalker(path, L) else: L.append(path) # --------------- install target ---------------------------------------------- def getOSandProcessor(): host = getHost() if host == "windows": processor = "i386" # BUGFIX else: processor = os.uname()[4] if lower(processor) in ("i686", "i586", "i468", "i386"): processor = "i386" if lower(processor) in ("x86_64", "x86-64", "amd64"): processor = "amd64" if find(lower(processor), "sparc") >= 0: processor = "sparc" return (host, processor) def writePlatdefC(nimrodpath): import os host, processor = getOSandProcessor() f = open(os.path.join(nimrodpath, "build/platdef.c"), "w+") f.write('/* Generated by koch.py */\n' 'char* nimOS(void) { return "%s"; }\n' 'char* nimCPU(void) { return "%s"; }\n' '\n' % (host, processor)) f.close() def detect(cmd, lookFor="version"): try: pipe = os.popen4(cmd)[1] except AttributeError: pipe = os.popen(cmd) result = None for line in pipe.readlines(): if find(lower(line), lookFor) >= 0: result = line[:-1] break pipe.close() if not result: # don't give up yet; it may have written to stderr if os.system(cmd) == 0: result = cmd return result def getNimrodPath(): if os.name == "posix": # Does not work 100% reliably. It is the best solution though. p = replace(sys.argv[0], "./", "") return os.path.split(os.path.join(os.getcwd(), p))[0] else: # Windows return os.path.split(sys.argv[0])[0] # ------------------- main ---------------------------------------------------- if __name__ == "__main__": main(sys.argv[1:])