#!/usr/bin/env python # -*- coding: utf-8 -*- ################################################################################ # # scons2ninja: A script to create a Ninja build file from SCons. # # Copyright (c) 2013 Remko Tronçon # Licensed under the simplified BSD license. # See COPYING for details. # ################################################################################ import re, os, os.path, subprocess, sys, fnmatch, shlex ################################################################################ # Helper methods & variables ################################################################################ SCRIPT = sys.argv[0] SCONS_ARGS = ' '.join(sys.argv[1:]) # TODO: Make this a tool-specific map BINARY_FLAGS = ["-framework", "-arch", "-x", "--output-format", "-isystem", "-include"] if sys.platform == 'win32' : LIB_PREFIX = "" LIB_SUFFIX = "" EXE_SUFFIX = ".exe" else : LIB_PREFIX = "lib" LIB_SUFFIX = ".a" EXE_SUFFIX = "" def is_regexp(x) : return 'match' in dir(x) def is_list(l) : return type(l) is list def escape(s) : return s.replace(' ', '$ ').replace(':', '$:') def quote_spaces(s) : if ' ' in s : return '"' + s + '"' else : return s def to_list(l) : if not l : return [] if is_list(l) : return l return [l] def partition(l, f) : x = [] y = [] for v in l : if f(v) : x.append(v) else : y.append(v) return (x, y) def get_unary_flags(prefix, flags) : return [x[len(prefix):] for x in flags if x.lower().startswith(prefix.lower())] def extract_unary_flags(prefix, flags) : f1, f2 = partition(flags, lambda x : x.lower().startswith(prefix.lower())) return ([f[len(prefix):] for f in f1], f2) def extract_unary_flag(prefix, flags) : flag, flags = extract_unary_flags(prefix, flags) return (flag[0], flags) def extract_binary_flag(prefix, flags) : i = flags.index(prefix) flag = flags[i + 1] del flags[i] del flags[i] return (flag, flags) def get_non_flags(flags) : skip = False result = [] for f in flags : if skip : skip = False elif f in BINARY_FLAGS : skip = True elif not f.startswith("/") and not f.startswith("-") : result.append(f) return result def extract_non_flags(flags) : non_flags = get_non_flags(flags) return (non_flags, filter(lambda x : x not in non_flags, flags)) def get_dependencies(target, build_targets) : result = [] queue = list(dependencies.get(target, [])) while len(queue) > 0 : n = queue.pop() # Filter out Value() results if n in build_targets or os.path.exists(n) : result.append(n) queue += list(dependencies.get(n, [])) return result def get_built_libs(libs, libpaths, outputs) : canonical_outputs = [os.path.abspath(p) for p in outputs] result = [] for libpath in libpaths : for lib in libs : lib_libpath = os.path.join(libpath, LIB_PREFIX + lib + LIB_SUFFIX) if os.path.abspath(lib_libpath) in canonical_outputs : result.append(lib_libpath) return result def parse_tool_command(line) : command = shlex.split(line, False, False if sys.platform == 'win32' else True) flags = command[1:] tool = os.path.splitext(os.path.basename(command[0]))[0] if tool.startswith('clang++') or tool.startswith('g++') : tool = "cxx" elif tool.startswith('clang') or tool.startswith('gcc') : tool = "cc" if tool in ["cc", "cxx"] and not "-c" in flags : tool = "glink" tool = tool.replace('-qt4', '') return tool, command, flags def rglob(pattern, root = '.') : return [os.path.join(path, f) for path, dirs, files in os.walk(root) for f in fnmatch.filter(files, pattern)] ################################################################################ # Helper for building Ninja files ################################################################################ class NinjaBuilder : def __init__(self) : self._header = "" self.variables = "" self.rules = "" self._build = "" self.pools = "" self._flags = {} self.targets = [] def header(self, text) : self._header += text + "\n" def rule(self, name, **kwargs) : self.rules += "rule " + name + "\n" for k, v in kwargs.iteritems() : self.rules += " " + str(k) + " = " + str(v) + "\n" self.rules += "\n" def pool(self, name, **kwargs) : self.pools += "pool " + name + "\n" for k, v in kwargs.iteritems() : self.pools += " " + str(k) + " = " + str(v) + "\n" self.pools += "\n" def variable(self, name, value) : self.variables += str(name) + " = " + str(value) + "\n" def build(self, target, rule, sources = None, **kwargs) : self._build += "build " + self.to_string(target) + ": " + rule if sources : self._build += " " + self.to_string(sources) if 'deps' in kwargs and kwargs['deps'] : self._build += " | " + self.to_string(kwargs["deps"]) if 'order_deps' in kwargs : self._build += " || " + self.to_string(kwargs['order_deps']) self._build += "\n" for var, value in kwargs.iteritems() : if var in ['deps', 'order_deps'] : continue value = self.to_string(value, quote = True) if var.endswith("flags") : value = self.get_flags_variable(var, value) self._build += " " + var + " = " + value + "\n" self.targets += to_list(target) def header_targets(self) : return [x for x in self.targets if x.endswith('.h') or x.endswith('.hh')] def serialize(self) : result = "" result += self._header + "\n" result += self.variables + "\n" for prefix in self._flags.values() : for k, v in prefix.iteritems() : result += v + " = " + k + "\n" result += "\n" result += self.pools + "\n" result += self.rules + "\n" result += self._build + "\n" return result def to_string(self, lst, quote = False) : if is_list(lst) : if quote : return ' '.join([quote_spaces(x) for x in lst]) else : return ' '.join([escape(x) for x in lst]) if is_regexp(lst) : return ' '.join([escape(x) for x in self.targets if lst.match(x)]) return escape(lst) def get_flags_variable(self, flags_type, flags) : if len(flags) == 0 : return '' if flags_type not in self._flags : self._flags[flags_type] = {} type_flags = self._flags[flags_type] if flags not in type_flags : type_flags[flags] = flags_type + "_" + str(len(type_flags)) return "$" + type_flags[flags] ################################################################################ # Configuration ################################################################################ ninja_post = [] scons_cmd = "scons" scons_dependencies = ['SConstruct'] + rglob('SConscript') def ninja_custom_command(ninja, line) : return False CONFIGURATION_FILE = '.scons2ninja.conf' execfile(CONFIGURATION_FILE) scons_dependencies = [os.path.normpath(x) for x in scons_dependencies] ################################################################################ # Rules ################################################################################ ninja = NinjaBuilder() ninja.pool('scons_pool', depth = 1) if sys.platform == 'win32' : ninja.rule('cl', deps = 'msvc', command = '$cl /showIncludes $clflags -c $in /Fo$out', description = 'CXX $out') ninja.rule('link', command = '$link $in $linkflags $libs /out:$out', description = 'LINK $out') ninja.rule('link_mt', command = '$link $in $linkflags $libs /out:$out ; $mt $mtflags', description = 'LINK $out') ninja.rule('lib', command = '$lib $libflags /out:$out $in', description = 'AR $out') ninja.rule('rc', command = '$rc $rcflags /Fo$out $in', description = 'RC $out') # SCons doesn't touch files if they didn't change, which makes # ninja rebuild the file over and over again. There's no touch on Windows :( # Could implement it with a script, but for now, delete the file if # this problem occurs. I'll fix it if it occurs too much. ninja.rule('scons', command = scons_cmd + " ${scons_args} $out", pool = 'scons_pool', description = 'GEN $out') ninja.rule('install', command = 'cmd /c copy $in $out') ninja.rule('run', command = '$in') else : ninja.rule('cxx', deps = 'gcc', depfile = '$out.d', command = '$cxx -MMD -MF $out.d $cxxflags -c $in -o $out', description = 'CXX $out') ninja.rule('cc', deps = 'gcc', depfile = '$out.d', command = '$cc -MMD -MF $out.d $ccflags -c $in -o $out', description = 'CC $out') ninja.rule('link', command = '$glink -o $out $in $linkflags', description = 'LINK $out') ninja.rule('ar', command = 'ar $arflags $out $in && ranlib $out', description = 'AR $out') # SCons doesn't touch files if they didn't change, which makes # ninja rebuild the file over and over again. Touching solves this. ninja.rule('scons', command = scons_cmd + " $out && touch $out", pool = 'scons_pool', description = 'GEN $out') ninja.rule('install', command = 'install $in $out') ninja.rule('run', command = './$in') ninja.rule('moc', command = '$moc $mocflags -o $out $in', description = 'MOC $out') ninja.rule('rcc', command = '$rcc $rccflags -name $name -o $out $in', description = 'RCC $out') ninja.rule('uic', command = '$uic $uicflags -o $out $in', description = 'UIC $out') ninja.rule('lrelease', command = '$lrelease $lreleaseflags $in -qm $out', description = 'LRELEASE $out') ninja.rule('ibtool', command = '$ibtool $ibtoolflags --compile $out $in', description = 'IBTOOL $out') ninja.rule('dsymutil', command = '$dsymutil $dsymutilflags -o $out $in', description = 'DSYMUTIL $out') ninja.rule('generator', command = "python " + SCRIPT + " ${scons_args}", depfile = ".scons2ninja.deps", pool = 'scons_pool', generator = '1', description = 'Regenerating build.ninja') ninja.rule('sdef', command = 'sdef $in | sdp -fh --basename $basename -o $outdir', description = 'SDEF $out') ################################################################################ # Build Statements ################################################################################ scons_generate_cmd = scons_cmd + " " + SCONS_ARGS + " --tree=all,prune dump_trace=1" #scons_generate_cmd = 'cmd /c type scons2ninja.in' #scons_generate_cmd = 'cat scons2ninja.in' # Pass 1: Parse dependencies (and prefilter some build rules) build_lines = [] dependencies = {} mtflags = {} previous_file = None f = subprocess.Popen(scons_generate_cmd, stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell=True) stage = 'preamble' skip_nth_line = -1 stack = ['.'] for line in f.stdout : line = line.rstrip() # Skip lines if requested from previous command if skip_nth_line >= 0 : skip_nth_line -= 1 if skip_nth_line == 0 : continue if line.startswith('scons: done building targets') : break if stage == "preamble" : # Pass all lines from the SCons configuration step to output if re.match("^scons: Building targets ...", line) : stage = "build" else : print line elif stage == "build" : if line.startswith('+-') : stage = "dependencies" elif re.match("^Using tempfile", line) : # Ignore response files from MSVS skip_nth_line = 2 else : build_lines.append(line) # Already detect targets that will need 'mt' tool, _, flags = parse_tool_command(line) if tool == 'mt' : target = get_unary_flags("-outputresource:", flags)[0] target = target[0:target.index(';')] mtflags[target] = flags elif stage == "dependencies" : if not re.match('^[\s|]+\+\-', line) : # Work around bug in SCons that splits output over multiple lines continue level = line.index('+-') / 2 filename = line[level*2+2:] if filename.startswith('[') : filename = filename[1:-1] # Check if we use the 'fixed' format which escapes filenamenames if filename.startswith('\'') and filename.endswith('\'') : filename = eval(filename) if level < len(stack) : stack = stack[0:level] elif level > len(stack) : if level != len(stack) + 1 : raise Exception("Internal Error" ) stack.append(previous_filename) # Skip absolute paths if not os.path.isabs(filename) : target = stack[-1] if target not in dependencies : dependencies[target] = [] dependencies[target].append(filename) previous_filename = filename if f.wait() != 0 : print("Error calling '" + scons_generate_cmd + "'") print f.stderr.read() exit(-1) # Pass 2: Parse build rules tools = {} for line in build_lines : # Custom python function m = re.match('^(\w+)\(\[([^\]]*)\]', line) if m : out = [x[1:-1] for x in m.group(2).split(',')] for x in out : # 'Note' = To be more correct, deps should also include $scons_dependencies, # but this regenerates a bit too often, so leaving it out for now. ninja.build(x, 'scons', None, deps = sorted(get_dependencies(x, ninja.targets))) continue # TextFile m = re.match("^Creating '([^']+)'", line) if m : out = m.group(1) # Note: To be more correct, deps should also include $scons_dependencies, # but this regenerates a bit too often, so leaving it out for now. ninja.build(out, 'scons', None, deps = sorted(get_dependencies(out, ninja.targets))) continue # Install m = re.match('^Install file: "(.*)" as "(.*)"', line) if m : ninja.build(m.group(2), 'install', m.group(1)) continue m = re.match('^Install directory: "(.*)" as "(.*)"', line) if m : for source in rglob('*', m.group(1)) : if os.path.isdir(source) : continue target = os.path.join(m.group(2), os.path.relpath(source, m.group(1))) ninja.build(target, 'install', source) continue # Tools tool, command, flags = parse_tool_command(line) tools[tool] = command[0] ############################################################ # clang/gcc tools ############################################################ if tool == 'cc': out, flags = extract_binary_flag("-o", flags) files, flags = extract_non_flags(flags) ninja.build(out, 'cc', files, order_deps = '_generated_headers', ccflags = flags) elif tool == 'cxx': out, flags = extract_binary_flag("-o", flags) files, flags = extract_non_flags(flags) ninja.build(out, 'cxx', files, order_deps = '_generated_headers', cxxflags = flags) elif tool == 'glink': out, flags = extract_binary_flag("-o", flags) files, flags = extract_non_flags(flags) libs = get_unary_flags('-l', flags) libpaths = get_unary_flags("-L", flags) deps = get_built_libs(libs, libpaths, ninja.targets) ninja.build(out, 'link', files, deps = sorted(deps), linkflags = flags) elif tool == 'ar': objects, flags = partition(flags, lambda x: x.endswith('.o')) libs, flags = partition(flags, lambda x: x.endswith('.a')) out = libs[0] ninja.build(out, 'ar', objects, arflags = flags) elif tool == 'ranlib': pass ############################################################ # MSVC tools ############################################################ elif tool == 'cl': out, flags = extract_unary_flag("/Fo", flags) files, flags = extract_non_flags(flags) ninja.build(out, 'cl', files, order_deps = '_generated_headers', clflags = flags) elif tool == 'lib': out, flags = extract_unary_flag("/out:", flags) files, flags = extract_non_flags(flags) ninja.build(out, 'lib', files, libflags = flags) elif tool == 'link': objects, flags = partition(flags, lambda x: x.endswith('.obj') or x.endswith('.res')) out, flags = extract_unary_flag("/out:", flags) libs, flags = partition(flags, lambda x: not x.startswith("/") and x.endswith(".lib")) libpaths = get_unary_flags("/libpath:", flags) deps = get_built_libs(libs, libpaths, ninja.targets) if out in mtflags : ninja.build(out, 'link_mt', objects, deps = sorted(deps), libs = libs, linkflags = flags, mtflags = mtflags[out]) else : ninja.build(out, 'link', objects, deps = sorted(deps), libs = libs, linkflags = flags) elif tool == 'rc': out, flags = extract_unary_flag("/fo", flags) files, flags = extract_non_flags(flags) ninja.build(out, 'rc', files[0], order_deps = '_generated_headers', rcflags = flags) elif tool == 'mt': # Already handled pass ############################################################ # Qt tools ############################################################ elif tool == 'moc': out, flags = extract_binary_flag("-o", flags) files, flags = extract_non_flags(flags) ninja.build(out, 'moc', files, mocflags = flags) elif tool == 'uic': out, flags = extract_binary_flag("-o", flags) files, flags = extract_non_flags(flags) ninja.build(out, 'uic', files, uicflags = flags) elif tool == 'lrelease': out, flags = extract_binary_flag("-qm", flags) files, flags = extract_non_flags(flags) ninja.build(out, 'lrelease', files, lreleaseflags = flags) elif tool == 'rcc': out, flags = extract_binary_flag("-o", flags) name, flags = extract_binary_flag("-name", flags) compress, flags = extract_binary_flag("--compress", flags) threshold, flags = extract_binary_flag("--threshold", flags) files, flags = extract_non_flags(flags) deps = list(set(get_dependencies(out, ninja.targets)) - set(files)) ninja.build(out, 'rcc', files, deps = sorted(deps), name = name, rccflags = ["--compress", compress, "--threshold", threshold]) ############################################################ # OS X tools ############################################################ elif tool == 'ibtool': out, flags = extract_binary_flag("--compile", flags) files, flags = extract_non_flags(flags) ninja.build(out, 'ibtool', files, ibtoolflags = flags) elif tool == 'dsymutil': out, flags = extract_binary_flag("-o", flags) files, flags = extract_non_flags(flags) ninja.build(out, 'dsymutil', files, dsymutilflags = flags) elif tool == 'sdef' : source = flags[0]; outdir, flags = extract_binary_flag("-o", flags) basename, flags = extract_binary_flag("--basename", flags) ninja.build(os.path.join(outdir, basename + ".h"), 'sdef', [source], basename = basename, outdir = outdir) elif tool == 'checker': pass elif not ninja_custom_command(ninja, line) : raise Exception("Unknown tool: '" + line + "'") # Phony target for all generated headers, used as an order-only depency from all C/C++ sources ninja.build('_generated_headers', 'phony', ninja.header_targets()) # Regenerate build.ninja file ninja.build('build.ninja', 'generator', [], deps = [SCRIPT, CONFIGURATION_FILE]) # Header & variables ninja.header("# This file is generated by " + SCRIPT) ninja.variable("ninja_required_version", "1.3") ninja.variable("scons_args", SCONS_ARGS) for k, v in tools.iteritems() : ninja.variable(k, v) # Extra customizations if 'ninja_post' in dir() : ninja_post(ninja) ################################################################################ # Result ################################################################################ f = open(".scons2ninja.deps", "w") f.write("build.ninja: " + " ".join([d for d in scons_dependencies if os.path.exists(d)]) + "\n") f.close() f = open("build.ninja", "w") f.write(ninja.serialize()) f.close()