diff options
Diffstat (limited to 'BuildTools/scons2ninja.rb')
-rwxr-xr-x | BuildTools/scons2ninja.rb | 562 |
1 files changed, 562 insertions, 0 deletions
diff --git a/BuildTools/scons2ninja.rb b/BuildTools/scons2ninja.rb new file mode 100755 index 0000000..6184f36 --- /dev/null +++ b/BuildTools/scons2ninja.rb @@ -0,0 +1,562 @@ +#!/usr/bin/env ruby + +################################################################################ +# +# 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. +# +################################################################################ + +require 'pathname' +require 'open3' + + +################################################################################ +# Helper for building Ninja files +################################################################################ + +class NinjaBuilder + attr_reader :targets + + def initialize + @header = "" + @variables = "" + @rules = "" + @build = "" + @pools = "" + @flags = Hash.new{ |h,k| h[k] = Hash.new() } + @targets = [] + end + + def header(text) + @header << text << "\n" + end + + def rule(name, opts = {}) + @rules << "rule #{name}\n" + opts.each { |k, v| @rules << " " << k.to_s << " = " << v.to_s << "\n" } + @rules << "\n" + end + + def pool(name, opts = {}) + @pools << "pool #{name}\n" + opts.each { |k, v| @pools << " " << k.to_s << " = " << v.to_s << "\n" } + @pools << "\n" + end + + def variable(name, value) + @variables << "#{name} = #{value}\n" + end + + def build(target, rule, sources = nil, opts = {}) + @build << "build " << str(target) << ": " << rule + @build << " " << str(sources) if sources + @build << " | " << str(opts[:deps]) if opts[:deps] + @build << " || " << str(opts[:order_deps]) if opts[:order_deps] + @build << "\n" + opts.each do |var, value| + next if [:deps, :order_deps].include? var + var = var.to_s + value = str(value) + value = get_flags_variable(var, value) if var.end_with? "flags" + @build << " #{var} = #{value}\n" + end + @targets += list(target) + end + + def header_targets + @targets.select { |target| target.end_with? '.h' or target.end_with? '.hh' } + end + + def to_s + result = "" + result << @header << "\n" + result << @variables << "\n" + @flags.each { |_, prefix| prefix.each { |k, v| result << "#{v} = #{k}\n" } } + result << "\n" + result << @pools << "\n" + result << @rules << "\n" + result << @build << "\n" + result + end + + private + def str(list) + return list.map{ |x| escape(x) }.join(' ') if list.is_a? Enumerable + return @targets.select { |x| list.match(x) }.map { |x| escape(x) }.join(' ') if list.is_a? Regexp + list + end + + def escape(s) + s.gsub(/ /, '$ ') + end + + def get_flags_variable(type, flags) + return '' if flags.empty? + type_flags = @flags[type] + unless id = type_flags[flags] + id = "#{type}_#{type_flags.size()}" + type_flags[flags] = id + end + "$#{id}" + end +end + +################################################################################ +# Helper methods & variables +################################################################################ + +if RUBY_PLATFORM =~ /(win32|mingw32)/ + LIB_PREFIX = "" + LIB_SUFFIX = "" + EXE_SUFFIX = ".exe" +else + LIB_PREFIX = "lib" + LIB_SUFFIX = ".a" + EXE_SUFFIX = "" +end + +def list(l) + return [] if nil + return l if l.is_a? Enumerable + [l] +end + +def get_unary_flags(prefix, flags) + flags.select {|x| /^#{prefix}/i.match(x)}.map { |x| x[prefix.size .. -1] } +end + +def extract_unary_flags(prefix, flags) + flag, flags = flags.partition { |x| /^#{prefix}/i.match(x) } + [flag.map { |x| x[prefix.size .. -1] }, flags] +end + +def extract_unary_flag(prefix, flags) + flag, flags = extract_unary_flags(prefix, flags) + [flag[0], flags] +end + +def extract_binary_flag(prefix, flags) + i = flags.index(prefix) + flag = flags[i + 1] + flags.delete_at(i) + flags.delete_at(i) + [flag, flags] +end + +BINARY_FLAGS = ["-framework", "-arch", "-x", "--output-format", "-isystem", "-include"] + +def get_non_flags(flags) + skip = false + result = [] + flags.each do |f| + if skip + skip = false + elsif BINARY_FLAGS.include? f + skip = true + elsif not f.start_with? "/" and not f.start_with? "-" + result << f + end + end + result +end + +def extract_non_flags(flags) + non_flags = get_non_flags(flags) + [non_flags, flags - non_flags] +end + +def to_native_path(path) + path.gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR) +end + +def from_native_path(path) + path.gsub(File::ALT_SEPARATOR || File::SEPARATOR, File::SEPARATOR) +end + +def get_dependencies(target, build_targets) + result = [] + queue = $dependencies[target].dup + while queue.size > 0 + n = queue.pop + result << n + queue += $dependencies[n].dup + end + # Filter out Value() results + result.select {|x| build_targets.include? x or File.exists? x } +end + +def get_built_libs(libs, libpaths, outputs) + canonical_outputs = outputs.map {|p| File.expand_path(p) } + result = [] + libpaths.each do |libpath| + libs.each do |lib| + lib_libpath = Pathname.new(libpath) + "#{LIB_PREFIX}#{lib}#{LIB_SUFFIX}" + if canonical_outputs.include? lib_libpath.expand_path.to_s + result << to_native_path(lib_libpath.to_s) + end + end + end + result +end + +script = to_native_path($0) + +################################################################################ +# Configuration +################################################################################ + +$ninja_post = [] +$scons_cmd = "scons" +$scons_dependencies = Dir['SConstruct'] + Dir['**/SConscript'] + +def ninja_post (&block) + $ninja_post << block +end + + +CONFIGURATION_FILE = '.scons2ninja.conf' + +load CONFIGURATION_FILE + +$scons_dependencies = $scons_dependencies.map {|x| to_native_path(x) } + +################################################################################ +# Rules +################################################################################ + +ninja = NinjaBuilder.new + +ninja.pool 'scons_pool', depth: 1 + +if RUBY_PLATFORM =~ /(win32|mingw32)/ + 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 '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} $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' +end + +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 'generator', + command: "ruby #{script} ${generator_args}", + pool: 'scons_pool', + generator: '1', + description: 'Regenerating build.ninja' + + +################################################################################ +# Build Statements +################################################################################ + +generator_args = ARGV.join(' ') +scons_generate_cmd = "#{$scons_cmd} #{generator_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 = Hash.new {|h, k| h[k] = [] } +previous_file = nil +Open3.popen3(scons_generate_cmd) do |stdin, f, stderr, thread| + stage = :preamble + skip_nth_line = -1 + stack = ['.'] + f.each_line do |line| + # Skip lines if requested from previous command + skip_nth_line -= 1 if skip_nth_line >= 0 + next if skip_nth_line == 0 + + line.chop! + + break if line.start_with? 'scons: done building targets' + + case stage + # Pass all lines from the SCons configuration step to output + when :preamble + if /^scons: Building targets .../.match(line) + stage = :build + else + puts line + end + + when :build + if line.start_with? '+-' + stage = :dependencies + # Ignore response files from MSVS + elsif /^Using tempfile/.match(line) + skip_nth_line = 2 + else + build_lines << line + end + + when :dependencies + # Work around bug in SCons that splits output over multiple lines + next unless /^[\s|]+\+\-/.match(line) + + level = line.index('+-') / 2 + file = line[level*2+2..-1] + file = file[1..-2] if file.start_with? '[' + + # Check if we use the 'fixed' format which escapes filenames + file = eval('"' + file[1..-2].gsub('"', '\\"') + '"') if file.start_with? '\'' + + if level < stack.length + stack = stack[0..level-1] + elsif level > stack.length + raise "Internal Error" if level != stack.length + 1 + stack << previous_file + end + # Skip absolute paths + $dependencies[stack[-1]] << file unless Pathname.new(file).absolute? + previous_file = file + end + end + + unless thread.value.success? + print "Error calling '#{scons_generate_cmd}': " + print stderr.read + exit(-1) + end +end + +# Pass 2: Parse build rules +tools = {} +build_lines.each do |line| + # Custom python function + if m = /^(\w+)\(\[([^\]]*)\]/.match(line) + out = m[2].split(',').map { |x| x[1..-2] } + out.each do |x| + # 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', nil, deps: get_dependencies(x, ninja.targets) + end + + # TextFile + elsif m = /^Creating '([^']+)'/.match(line) + out = m[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', nil, deps: get_dependencies(out, ninja.targets) + + # Install + elsif m = /^Install file: "(.*)" as "(.*)"/.match(line) + ninja.build m[2], 'install', m[1] + + elsif m = /^Install directory: "(.*)" as "(.*)"/.match(line) + Dir["#{m[1]}/**"].each do |file| + source = Pathname.new(file) + native_source = to_native_path(source.to_s) + target = Pathname.new(m[2]) + source.relative_path_from(Pathname.new(m[1])) + native_target = to_native_path(target.to_s) + ninja.build native_target, 'install', native_source + end + + # Tools + else + command = line.split + flags = command[1..-1] + tool = File.basename(command[0], File.extname(command[0])) + tool = "cxx" if ["clang++", "g++"].include? tool + tool = "cc" if ["clang", "gcc"].include? tool + tool = "glink" if ["cc", "cxx"].include? tool and not flags.include? "-c" + tool.gsub!(/-qt4$/, '') + tools[tool] = command[0] + + case tool + + ############################################################ + # clang/gcc tools + ############################################################ + + when '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 + + when '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 + + when '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) + dependencies = get_built_libs(libs, libpaths, ninja.targets) + ninja.build out, 'link', files, deps: dependencies, linkflags: flags + + when 'ar' + objects, flags = flags.partition { |x| x.end_with? ".o" } + libs, flags = flags.partition { |x| x.end_with? ".a" } + out = libs[0] + ninja.build out, 'ar', objects, arflags: flags + + when 'ranlib' + + + ############################################################ + # MSVC tools + ############################################################ + + when '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 + + when 'lib' + out, flags = extract_unary_flag("/out:", flags) + files, flags = extract_non_flags(flags) + ninja.build out, 'lib', files, libflags: flags + + when 'link' + objects, flags = flags.partition { |x| x.end_with? ".obj" } + out, flags = extract_unary_flag("/out:", flags) + libs, flags = flags.partition { |x| not x.start_with? "/" and x.end_with? ".lib" } + libpaths = get_unary_flags("/libpath:", flags) + dependencies = get_built_libs(libs, libpaths, ninja.targets) + ninja.build out, 'link', objects, deps: dependencies, + libs: libs, linkflags: flags + + when '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 + + ############################################################ + # Qt tools + ############################################################ + + when 'moc' + out, flags = extract_binary_flag("-o", flags) + files, flags = extract_non_flags(flags) + ninja.build out, 'moc', files, mocflags: flags + + when 'uic' + out, flags = extract_binary_flag("-o", flags) + files, flags = extract_non_flags(flags) + ninja.build out, 'uic', files, uicflags: flags + + when 'lrelease' + out, flags = extract_binary_flag("-qm", flags) + files, flags = extract_non_flags(flags) + ninja.build out, 'lrelease', files, lreleaseflags: flags + + when 'rcc' + out, flags = extract_binary_flag("-o", flags) + name, flags = extract_binary_flag("-name", flags) + files, flags = extract_non_flags(flags) + deps = get_dependencies(out, ninja.targets) - files + ninja.build out, 'rcc', files, deps: deps, name: name, rccflags: flags + + ############################################################ + # OS X tools + ############################################################ + + when 'ibtool' + out, flags = extract_binary_flag("--compile", flags) + files, flags = extract_non_flags(flags) + ninja.build out, 'ibtool', files, ibtoolflags: flags + + else + raise "Unknown tool: '#{line}'" + end + end +end + +# Phony target for all generated headers, used as an order-only dependency 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] + $scons_dependencies + +# Header & variables +ninja.header "# This file is generated by #{script}" +ninja.variable "ninja_required_version", "1.3" +ninja.variable "generator_args", generator_args +tools.each { |k, v| ninja.variable k, v } + +# Extra customizations +$ninja_post.each { |p| p.call(ninja) } + +################################################################################ +# Result +################################################################################ + +File.open('build.ninja', 'w') { |f| f.write ninja } |