summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'BuildTools/scons2ninja.rb')
-rwxr-xr-xBuildTools/scons2ninja.rb562
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 }