"""The build system is in this file. Skip to the end (class Loader, function load) for all available build rule types. See dep.py for information on the non-intuitive way these classes as "metarules" link together and generate real code. """ import dep import util import glob import os import sys def sq(str): """escape a string for use in a shell""" if isinstance(str, list): return [sq(s) for s in str] else: return util.shellquote(str) def mq(str): """escape a string for use in makefiles""" if isinstance(str, list): return [mq(s) for s in str] else: return str.replace(" ", "\ ") class Types: HEADER="buildsys/.h" GCC_SOURCE="buildsys/gcc" # .c, .cpp, etc OBJECT="buildsys/.o" LINKABLE="buildsys/.a" BINFILE="buildsys/bin" SCRIPT="buildsys/script" DIR="buildsys/dir" PLACEHOLDER="buildsys/placeholder" MISC="buildsys/misc" ANY="buildsys/*" class CompilerInfo: def __init__(self): (self.kernel, nodename, release, version, self.arch) = os.uname() self.lflags_fortran = "-lg2c" def compiler_program(self, extension): # for instance, turn "gcc %s -c %s -o ..." into just "gcc" return self.command_from_ext[extension].split(" ")[0] class GCCCompiler(CompilerInfo): def __init__(self): CompilerInfo.__init__(self) use_gfortran = True check_gfortran_existence = os.popen("which gfortran") check_gfortran_existence_result = check_gfortran_existence.read() check_gfortran_existence.close() check_gfortran_existence_result = check_gfortran_existence_result[:len(check_gfortran_existence_result) - 1] if len(check_gfortran_existence_result) == 0 or check_gfortran_existence_result[0] != '/': # '/' test for Macs check_gfortran_existence = os.popen("which gfortran-4") check_gfortran_existence_result = check_gfortran_existence.read() check_gfortran_existence.close() check_gfortran_existence_result = check_gfortran_existence_result[:len(check_gfortran_existence_result) - 1] if len(check_gfortran_existence_result) == 0 or check_gfortran_existence_result[0] != '/': # '/' test for Macs use_gfortran = False print "!!! Using g77. GFortran will be preferred eventually." self.name = "gcc" self.mode_dictionary = { "verbose": "-g -DDEBUG -DVERBOSE", "debug": "-g3 -DDEBUG -O0", "check": "-O2 -finline-functions -finline-limit=2000 -g -DDEBUG", "fast": "-O2 -finline-functions -finline-limit=2000 -fomit-frame-pointer -g -DNDEBUG", "unsafe": "-O3 -ffast-math -g -fomit-frame-pointer -DNDEBUG", "profile" : "-O2 -pg -finline-limit=12 -DPROFILE -DNDEBUG", "small": "-Os -DNDEBUG", "trace": "-g -DNDEBUG" } self.command_from_ext = { "c" : "gcc %s -c %s -o %s -Wall", "cc" : "g++ -Wall -Woverloaded-virtual -fno-exceptions -Wparentheses -fno-exceptions %s -c %s -o %s", "f" : "gfortran %s -c %s -o %s -Wall -Wno-uninitialized" } if use_gfortran: self.lflags_fortran = "-lgfortran" else: self.lflags_fortran = "-lg2c" self.command_from_ext["f"] = "g77 %s -c %s -o %s -Wall -Wno-uninitialized" self.linker = "g++" self.lflags_start = "" self.lflags_end = "-lm -lpthread %s" % (self.lflags_fortran) class GCC4Compiler(GCCCompiler): def __init__(self): GCCCompiler.__init__(self) self.name = "gcc4" self.command_from_ext["c"] = "gcc4 %s -c %s -o %s -Wall"; self.command_from_ext["cc"] = "g++4 -Wall -Woverloaded-virtual -fno-exceptions -Wparentheses -fno-exceptions %s -c %s -o %s"; self.linker = "g++4" class GCCdash4Compiler(GCCCompiler): def __init__(self): GCCCompiler.__init__(self) self.name = "gcc-4" self.command_from_ext["c"] = "gcc-4 %s -c %s -o %s -Wall"; self.command_from_ext["cc"] = "g++-4 -Wall -Woverloaded-virtual -fno-exceptions -Wparentheses -fno-exceptions %s -c %s -o %s"; self.linker = "g++-4" class ICCCompiler(CompilerInfo): def __init__(self): CompilerInfo.__init__(self) self.name = "icc" self.mode_dictionary = { "verbose": "-g -DDEBUG -DVERBOSE", "debug": "-g -DDEBUG", "check": "-O2 -g -DDEBUG", "fast": "-O3 -g -fomit-frame-pointer -DNDEBUG", "unsafe": "-O3 -ffast-math -g -fomit-frame-pointer -DNDEBUG", "profile" : "-O2 -pg -finline-limit=8 -DPROFILE -DNDEBUG", "small": "-Os" } self.command_from_ext = { "c" : "icc %s -c %s -o %s", "cc" : "icc %s -c %s -o %s -fno-exceptions", "f" : "ifort %s -c %s -o %s -Wall" } self.linker = "icc" self.lflags_start = "" self.lflags_end = "-lm -lpthread %s" % (self.lflags_fortran) class MPICompiler(CompilerInfo): def __init__(self): CompilerInfo.__init__(self) self.name = "mpi" self.mode_dictionary = { "verbose": "-g -DDEBUG -DVERBOSE", "debug": "-g -DDEBUG", "check": "-O2 -g -DDEBUG", "fast": "-O3 -g -fomit-frame-pointer -DNDEBUG", "unsafe": "-O3 -ffast-math -g -fomit-frame-pointer -DNDEBUG", "profile" : "-O2 -pg -finline-limit=8 -DPROFILE -DNDEBUG", "small": "-Os" } self.command_from_ext = { "c" : "mpicc %s -c %s -o %s", "cc" : "mpiCC -fno-exceptions %s -c %s -o %s", "f" : "mpif77 %s -c %s -o %s -Wall" } self.linker = "mpiCC" self.lflags_start = "" self.lflags_end = "-lm -lpthread %s" % (self.lflags_fortran) compiler_choices = [GCCCompiler(), GCC4Compiler(), GCCdash4Compiler(), MPICompiler(), ICCCompiler()] compilers = dict([(c.name, c) for c in compiler_choices]) class MakeBuildSys(dep.DepSys): """Build system. """ def __init__(self, source_dir, bin_dir, keep_dir): """Initialize a 'Build System' or 'Makefile'. source_dir - source code directory bin_dir - binaries generated by build system go here keep_dir - generated files that you want to keep a while go here """ dep.DepSys.__init__(self) self.source_dir = source_dir self.bin_dir = bin_dir self.keep_dir = keep_dir self.entries = [] def begin(self, state): return MakeBuildSysEntry(self, state) def add_realrule(self, realrule): self.entries.append(realrule) def to_makefile(self): def shorten(fname): if "/bin/" in fname: return fname[fname.rindex("/bin/")+1:] else: return fname lines = [] self.entries.reverse() i = 0 for (outfiles_orig, infiles, commands) in self.entries: i += 1 outfiles = [] for outfile in outfiles_orig: if not (outfile in infiles): outfiles.append(outfile) if not outfiles: outfiles = [("pseudo_%d" % i)] if commands: lines.append("%s: %s" % (" ".join(mq(outfiles)), " ".join(mq(infiles)))) outfiles_short = [shorten(outfile) for outfile in outfiles] lines.extend(["\t@echo '... Making %s'" % (" ".join(outfiles_short))]) lines.extend(["\t@" + c for c in commands]) elif infiles: lines.append("%s: %s" % (" ".join(mq(outfiles)), " ".join(mq(infiles)))) lines.extend(["\t@echo '*** Done with %s'" % (" ".join(infiles))]) self.entries.reverse() lines.append("clean:") lines.append("\t@echo 'Removing the bin/ directory.'"); lines.append("\trm -rf %s/*" % (sq(self.bin_dir))) return lines class MakeBuildSysEntry(dep.DepSysEntry): def __init__(self, sys, state): dep.DepSysEntry.__init__(self, sys, state) self.commands = [] def _make_name(self, simplename, parameterization): # NOTE: I'm completely ignoring the file list (self.state.files) KEEP_DIR = "KEEP/" if simplename.startswith(KEEP_DIR): # "!" means that file should be placed in semi-permanent storage. dirname = self.keep_dir(*parameterization) simplename = simplename[len(KEEP_DIR):] else: dirname = self.bin_dir(*parameterization) return os.path.join(dirname, simplename) def __name_pmz(self, pmz): """names a parameterization""" def name_part(param_name): if param_name in pmz: return self.state.params[param_name] else: return "COMMON" allowed_sequence = ["arch", "kernel", "mode", "compiler", "cflags"] (arch, kernel, mode, compiler, cflags) = [name_part(x) for x in allowed_sequence] for param in pmz: if not param in allowed_sequence: raise Exception("I can't deal with extra parameterization yet: '%s' not in '%s'" % (param, allowed_sequence)) return "%s_%s_%s_%s_%s" % (arch, kernel, mode, compiler, util.sanitize_basename(cflags)) def bin_dir(self, *pmz): return os.path.join(self.sys.bin_dir, self.__name_pmz(pmz)) def keep_dir(self, *pmz): return os.path.join(self.sys.keep_dir, self.__name_pmz(pmz)) def source_file(self, real_path, fake_path): return dep.DestFile(real_path, fake_path) def makefile(self): return self.source_file(os.path.abspath("./Makefile"), "Makefile") def ensure_writable(self, name): self.command("mkdir -p %s" % (sq(os.path.dirname(name)))) def command(self, str): self.commands.append(str) def end(self, files): infiles = {} for file in dep.filemap_to_files(self.state.files): infiles[file.name] = None outfiles = [file.name for (classname, file) in files] self.sys.add_realrule((outfiles, list(infiles.keys()), self.commands)) class SourceRule(dep.Metarule): def __init__(self, type, real_path, fake_path): dep.Metarule.__init__(self) self.real_path = real_path self.fake_path = fake_path self.type = type def doit(self, realrule, files, params): return [(self.type, realrule.source_file(self.real_path, self.fake_path))] class CompileRule(dep.Metarule): def __init__(self, source, headers, cflags): dep.Metarule.__init__(self, source=[source], headers=headers) self.cflags = cflags def doit(self, realrule, files, params): compiler = compilers[params["compiler"]] source = files["source"].single(Types.GCC_SOURCE) dot = source.simplename.rindex(".") sourceextension = source.simplename[dot+1:] simplename = source.simplename[:dot] + ".o" object = realrule.file("obj/" + simplename.replace("/", "_"), "arch", "kernel", "mode", "compiler", "cflags") # TODO: -I flags my_includes = "-I%s -I%s -I%s" % ( sq(realrule.bin_dir("arch", "kernel", "compiler")), sq(realrule.bin_dir("arch", "kernel", "compiler", "cflags")), sq(realrule.sys.source_dir)) mode = params["mode"] my_flags = my_includes + " " + compiler.mode_dictionary[params["mode"]] \ + " " + self.cflags + " " + params["cflags"] if not sourceextension in compiler.command_from_ext: raise Exception("Don't know how to compile files of type '*.%s'." % sourceextension) command_template = compiler.command_from_ext[sourceextension] (source_dirname, source_basename) = os.path.split(source.name) compile_cmd = command_template % (my_flags, sq(source_basename), sq(object.name)) realrule.command("cd " + sq(source_dirname) + " && " + compile_cmd) return [(Types.OBJECT, object)] class ArchiveRule(dep.Metarule): def __init__(self, name, objects): dep.Metarule.__init__(self, objects=objects) self.name = name def doit(self, realrule, files, params): objects = files["objects"].many(Types.OBJECT) if len(objects) != 0: libfile = realrule.file("lib" + self.name + ".a", "arch", "kernel", "mode", "compiler") archive_and_export_rule = "ar r %s %s" % \ (sq(libfile), \ " ".join([sq(x.name) for x in objects])) if params["prefix"] != "": destination_parent_dir = params["prefix"].rstrip("/").rstrip(os.sep) \ + "/lib/" archive_and_export_rule += \ " && mkdir -p " + \ destination_parent_dir + \ " && cp " + sq(libfile) + " " + \ destination_parent_dir realrule.command(archive_and_export_rule) return [(Types.LINKABLE, libfile)] else: # no compilable code was found -- just don't create an object return [] #return [(Types.OBJECT, object) for object in objects] class HeaderSummaryRule(dep.Metarule): def __init__(self, name, objects): dep.Metarule.__init__(self, objects=objects) self.name = name def doit(self, realrule, files, params): libfile = realrule.file("lib" + self.name + ".h") generate_header_rule = "touch %s" % (sq(libfile)) # If the user specified the --prefix option, then export the header files. if params["prefix"] != "": for each_header in files["objects"].to_names(): destination_subpath = each_header \ [len(os.path.realpath \ (os.path.dirname \ (os.path.realpath \ (os.path.dirname \ (sys.argv[0])))).rstrip(os.sep) \ + os.sep):] destination_subdir = os.path.dirname(destination_subpath) destination_parent_dir = params["prefix"].rstrip("/").rstrip(os.sep) \ + "/include/fastlib2/" destination_newdir = destination_parent_dir + destination_subdir # This seems like a hack, but I do not have a good clean # solution. if os.path.basename(each_header) == "basic_types.h": destination_newdir = os.path.dirname \ (os.path.dirname \ (os.path.dirname(destination_newdir))) + \ "/fastlib/base/base" generate_header_rule += " && mkdir -p " + destination_newdir + \ " && cp " + each_header + " " + \ destination_newdir realrule.command(generate_header_rule) return [(Types.PLACEHOLDER, libfile)] class BinRule(dep.Metarule): def __init__(self, name, deplibs, cflags): self.name = name self.cflags = cflags dep.Metarule.__init__(self, deplibs=deplibs) def doit(self, realrule, files, params): compiler = compilers[params["compiler"]] binfile = realrule.file(self.name, "arch", "kernel", "mode", "compiler", "cflags") # TO-DO: Link flags necessary? lflags_start = compiler.lflags_start lflags_end = compiler.lflags_end cflags = compiler.mode_dictionary[params["mode"]] reversed_libs = list(files["deplibs"].to_names()) reversed_libs.reverse() realrule.command(compiler.linker + " -o %s %s %s %s %s %s %s" % ( sq(binfile.name), cflags, lflags_start, " ".join(sq(reversed_libs)), lflags_end, self.cflags, params["cflags"])) return [(Types.BINFILE, binfile)] class LibRule(dep.Metarule): """LibRule returns all relevant archive files for this library and libraries it depends on. In addition, a LibRule exposes header files. """ # TODO: "sources" will also natively support C++ files def __init__(self, name, sources, headers, deplibs, cflags = ""): source_rules = sources self.header_rules = headers # TODO: Be careful about depending on extra stuff? for dep_lib in deplibs: if isinstance(dep_lib, LibRule): self.header_rules.append(dep_lib.header_summary_rule) self.header_summary_rule = HeaderSummaryRule(name, self.header_rules) self.compile_rules = [CompileRule(source_rule, self.header_rules, cflags) for source_rule in source_rules] self.archive_rule = ArchiveRule(name, self.compile_rules) dep.Metarule.__init__(self, archive=[self.archive_rule], deplibs=deplibs) def doit(self, realrule, files, params): return files["deplibs"].to_pairs() + files["archive"].to_pairs() class MakefileRule(dep.Metarule): """ Plug created so that symlinks are re-created whenever the Makefile changes. """ def __init__(self): dep.Metarule.__init__(self) def doit(self, realrule, files, params): file = realrule.makefile() return [(Types.MISC, file)] class SymlinkRule(dep.Metarule): def __init__(self, filerules, dest_dir): dep.Metarule.__init__(self, filerules=filerules, makefile=[MakefileRule()]) self.dest_dir = dest_dir def doit(self, realrule, files, params): all = [] pairs = files["filerules"].to_pairs() for (classname, file) in pairs: sourcename = file.name shortname = os.path.basename(file.name) destname = os.path.join(self.dest_dir, shortname) # the symlink's simplename will be the same as the original's destfile = realrule.source_file(destname, shortname) realrule.command("rm -f %s" % sq(destname)) realrule.command("ln -s -f %s %s" % (sq(sourcename), sq(destname))) all.append((classname, destfile)) realrule.command("echo '*** Created %d symlinks in %s.'" % (len(pairs), self.dest_dir)) return all class WgetRule(dep.Metarule): def __init__(self, url, type, real_path, fake_path): self.real_path = real_path self.fake_path = fake_path self.url = url self.type = type dep.Metarule.__init__(self) def doit(self, realrule, files, params): (real_dir, filename) = os.path.split(self.real_path) echo_message = "Downloading the file using " + params["downloader"] + "..." realrule.command("echo '" + echo_message + "'") if os.path.basename(params["downloader"]) == "wget": realrule.command("cd %s && %s %s" % ( sq(real_dir), params["downloader"], sq(filename))) else: realrule.command("cd %s && %s %s -o %s" % ( sq(real_dir), params["downloader"], sq(filename), sq(self.url))) return [(self.type, realrule.source_file(self.real_path, self.fake_path))] # Parameter loader BUILD_FILE = "build.py" class Loader: """Responsible for loading build files and helping them stitch together. There are concepts of real and fake paths: - Real path: Where the file is located exactly on the file system. - Fake path: Where results generated by this file are relative to the build system. If the real path is within the build directories, then the fake path is just the relative path from the root of the build system. However, we also support building small code trees that are not in the build system. In this, the "fake" path starts with "outside" to indicate it is outside the build system, and has the full path. Example: Say my build path is ~/fastlib/c (this is real_rootpath) Someone wants to build ~/fastlib/c/foo/bar, the fake path is "foo/bar". But if someone wants to build ~/foo/bar, the fake path is "outside/home/yourname/foo/bar". See also ../script/fl-build. """ def __init__(self, real_rootpath): self.real_root = real_rootpath.rstrip(os.sep) self.thebuildsys = MakeBuildSys( self.real_root, os.path.join(self.real_root, "bin"), os.path.join(self.real_root, "bin_keep")) self.loader_map = {} def pathjoin(self, left, right, defaultprefix, lsep = "/", rsep = "/"): def helper(left, right): if right and right[0] == ".": right_index = (right + "/").index(rsep) right_first = right[:right_index] right_rest = right[right_index+1:] if right_first == ".": return helper(left, right_rest) else: try: left_index = left.rindex(lsep) except: left_index = 0 return helper(left[:left_index], right_rest) else: return left + lsep + right.replace(rsep, lsep) left = left.rstrip(lsep) right = right.rstrip(rsep) if not right: return left elif right[0] != ".": if right[0] == rsep: return right.replace(rsep, lsep) else: return defaultprefix + right.replace(rsep, lsep) else: return helper(left, right) def pathjoin_real(self, left, right): return self.pathjoin(left, right, self.real_root + "/", os.sep, "/") def pathjoin_fake(self, left, right): return self.pathjoin(left, right, "", "/", "/") def find_rule(self, rule_path, cur_real_path, cur_fake_path, posttext = ""): #print "trying: %s" % fullname (path_rel, rule_name) = rule_path.split(":") fake_path = self.pathjoin_fake(cur_fake_path, path_rel) real_path = self.pathjoin_real(cur_real_path, path_rel) if rule_name == "": rule_name = fake_path.split("/")[-1] if not fake_path in self.loader_map: self.load(real_path, fake_path, posttext) elif posttext: print "XXX Ignored Post-text: ", posttext if rule_name == "VERYSPECIALPYTHONSTRING": dependencies = {} for target_string in self.loader_map[fake_path]: cur_rule_object = Loader.find_rule(self, ":" + target_string, cur_real_path, cur_fake_path, posttext) if not target_string in dependencies: dependencies[target_string] = [cur_rule_object] else: dependencies[target_string] += [cur_rule_object] def convert_to_list(sysentry, files, params): ret = [] for file_key in files: ret += files[file_key].to_pairs() return ret new_custom_rule = dep.CustomRule(dependencies, convert_to_list) self.loader_map[fake_path][rule_name] = new_custom_rule return new_custom_rule if not rule_name in self.loader_map[fake_path]: raise Exception("Rule '%s' not found in '%s'." % (rule_name, real_path)) #print "Found [%s][%s] aka [%s]" % (fullname, defaultpath, path) return self.loader_map[fake_path][rule_name] def load(self, real_path, fake_path, posttext = ""): """Loads build rules, by exposing a 'register' function. real_path - path on file system fake_path - path according to build system posttext - optionally inject text at the end of a build file """ # !!!!! LOOK AT ME! All the functions in build rules are defined here! self.loader_map[fake_path] = {} selfname = fake_path.split("/")[-1] def register(name, rule): if name in self.loader_map[fake_path]: raise Exception("Duplicate rule %s:%s" % (fake_path, name)) self.loader_map[fake_path][name] = rule return rule def find(rule_name): return self.find_rule(rule_name, real_path, fake_path) def pathify_real(name): return self.pathjoin_real(real_path, "./" + name) def pathify_fake(name): return self.pathjoin_fake(fake_path, "./" + name) def sourcerule(type, name): if isinstance(name, str): # If there is a colon, it is a rule; otherwise, it is just a file. if ":" in name: return find(name) else: return SourceRule(type, pathify_real(name), pathify_fake(name)) else: return name def sourcerules(type, names): return [sourcerule(type, name) for name in names] def lglob(mask, *exclude): (relpath, namemask) = os.path.split(mask) full_path = os.path.join(real_path, mask) basenames = [os.path.basename(f) for f in glob.glob(os.path.join(real_path, mask))] return [os.path.join(relpath, basename) for basename in basenames if not basename in exclude] def customrule(name, dependencies, doit_fn): return register(name, dep.CustomRule(dependencies, doit_fn)) def librule(name = selfname, sources = [], headers = [], deplibs = [], tests = [], cflags = ""): long_name = pathify_fake(name).replace("/", "_") lib = LibRule(long_name, sourcerules(Types.GCC_SOURCE, sources), sourcerules(Types.HEADER , headers), sourcerules(Types.LINKABLE, deplibs), cflags) register(name, lib) for test_source in tests: # TO-DO: Recursively run tests test_name = test_source[:test_source.rindex(".")] test_lib = LibRule(long_name + "_" + test_name, [sourcerule(Types.GCC_SOURCE, test_source)], [], [lib], cflags) test_bin = BinRule(pathify_fake(test_name), [test_lib], cflags) register(test_name, test_bin) return lib def wgetrule(name, url, type = Types.ANY, fname = None): if fname == None: fname = url[url.rindex('/')+1:] register(name, WgetRule(url, type, pathify_real(fname), pathify_fake(fname))) def binrule(name, deplibs = [], sources = [], headers = [], cflags = "", linkables = None): if linkables: print "!!! Warning: 'linkables' is deprecated, it has been renamed 'deplibs'" deplibs += linkables # (source, headers, cflags) longname = pathify_fake(name) if sources: lib = librule(name = name + "__auto", sources = sources, headers = headers, deplibs = deplibs, cflags = cflags) deplibs = deplibs + [lib] register(name, BinRule(longname, sourcerules(Types.LINKABLE, deplibs), cflags)) build_file_path = os.path.join(real_path, BUILD_FILE) print "... Reading %s" % (build_file_path) try: text = util.readfile(build_file_path) except IOError: print "!!! Build file '%s' not found, assuming blank." % build_file_path text = "" # remove DOS line feeds and add posttext text = text.replace("\r", "") + "\n" + posttext exec text in {"register" : register, "Types" : Types, "find" : find, "dep" : dep, "lglob" : lglob, "sourcerule" : sourcerule, "sourcerules" : sourcerules, "librule" : librule, "binrule" : binrule, "customrule" : customrule, "wgetrule" : wgetrule, "compilers" : compilers, "os" : os, "sq" : sq}