Files
textmate/bin/gen_build
Allan Odgaard c5ecf8f6c6 Handle *.c++ files in gen_build
We already handle these when mapping extension to file type.
2013-08-24 01:19:29 +02:00

967 lines
32 KiB
Ruby
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -wKU
# == Synopsis
#
# gen_build: create build.ninja based on target files
#
# == Usage
#
# --help:
# show help.
#
# --output/-o «file»:
# the «file» to write build info into.
#
# --build-directory/-C «directory»:
# where build files should go.
#
# --define/-d «name»=«value»:
# define a variable that ends up in build.ninja.
require 'getoptlong'
require 'rdoc/usage'
require 'shellwords'
require 'set'
require 'pp'
GLOB_KEYS = %w{ CP_Resources CP_SharedSupport CP_PlugIns CP_Library/QuickLook EXPORT HTML_FOOTER HTML_HEADER MARKDOWN_HEADER MARKDOWN_FOOTER PRELUDE SOURCES TARGETS TESTS TEST_SOURCES }
STRING_KEYS = %w{ TARGET_NAME BUNDLE_EXTENSION FLAGS C_FLAGS CXX_FLAGS OBJC_FLAGS OBJCXX_FLAGS LN_FLAGS PLIST_FLAGS BUILD }
COMPILER_INFO = {
'c' => { :rule => 'build_c', :compiler => '$CC', :flags => '$C_FLAGS', },
'm' => { :rule => 'build_m', :compiler => '$CC', :flags => '$OBJC_FLAGS', },
'cc' => { :rule => 'build_cc', :compiler => '$CXX', :flags => '$CXX_FLAGS', },
'mm' => { :rule => 'build_mm', :compiler => '$CXX', :flags => '$OBJCXX_FLAGS', },
}
class Targets
include Enumerable
attr_reader :root
def initialize(path)
targets = all_targets(path)
linked = Set.new(targets.collect { |target| target['LINK'].to_a }.flatten)
@root = targets.first
@all_targets = targets
@all = [ ]
targets.each do |target|
next if target['SOURCES'].to_a.empty?
@all << target
if linked.include?(target[:name]) || target['BUILD'] == 'lib'
target[:type] = :library
else
has_info_plist = target['CP_Resources'].to_a.find { |path| path =~ /\bInfo\.plist$/ }
target[:type] = has_info_plist ? :bundle : :executable
end
end
end
def target_named(name)
res = self.find { |target| target[:name] == name }
return res unless res.nil?
abort "*** no target named #{name}. Did you update submodules?\n"
end
def each(&block)
@all.each(&block)
end
def dependencies
paths = [ ]
@all_targets.each do |target|
paths << target[:full_path] << target[:depeends_on_directories].to_a
target.each do |key, value|
paths << value.reject { |path| path =~ /^@/ }.map do |path|
File.dirname(File.join(target[:base], path))
end if GLOB_KEYS.include?(key) && key != 'PRELUDE'
end
end
paths.flatten.sort.uniq
end
private
def parse(path, root_dir, parent = { })
base = File.dirname(path)
dict = parent.merge({ :name => File.basename(base), :base => base, :path => base.sub(/^#{Regexp.escape root_dir}\//, ''), :full_path => path })
dict.delete('TARGETS')
data = File.read(path)
assignments = data.scan(/^([\w\/]+)\s*(\+=|-=|=)[ \t]*(.*)$/)
assignments.each do |arr|
key, op, value = *arr
value = Shellwords.shellwords(value) unless STRING_KEYS.include? key
if GLOB_KEYS.include?(key)
globs = value.reject { |v| v =~ /^@/ }
targets = value.reject { |v| v !~ /^@/ }
Dir.chdir(File.dirname(path)) do
value = Dir.glob(globs)
value = value.concat(targets)
end
end
oldValue = dict[key]
if STRING_KEYS.include? key
if op == '+='
value = oldValue.nil? ? value : "#{oldValue} #{value}"
elsif op == '-='
value = oldValue.to_s.gsub(/(^| )#{Regexp.escape value}( |$)/, ' ')
end
else
if op == '+='
value = oldValue.dup.concat(value) unless oldValue.nil?
elsif op == '-='
abort "Operator -= not implemented for non-string keys"
end
end
dict[key] = value
end
if dict.has_key?('TARGET_NAME')
dict[:name] = dict['TARGET_NAME']
dict.delete('TARGET_NAME')
end
dict
end
def recursive_parse(target, root_dir)
res = [ target ]
target["TARGETS"].to_a.each do |subtarget|
res << recursive_parse(parse(File.join(target[:base], subtarget), root_dir, target), root_dir)
end
res
end
def all_targets(path)
recursive_parse(parse(path, File.dirname(path)), File.dirname(path)).flatten
end
end
def esc_path(path)
if path.class == String
return path if path =~ /^\$builddir/
path.gsub(/[ :$]/, '$\\&')
else
path.map { |obj| esc_path(obj) }
end
end
def build_path(path)
"$builddir/#{path.gsub(/[ :$]/, '$\\&')}"
end
def prepend(prefix, array, sep = ' ')
array.map do |item|
item =~ %r{/} ? item : esc_path("#{prefix}#{item}")
end.join(sep)
end
def flags_for_target(target)
res = ""
target.each do |key, value|
res << " #{key} = #{value}\n" unless key.class != String || key !~ /FLAGS$/ || value == TARGETS.root[key]
end
res
end
def source_type(src)
srcExt = src[/\b[a-z\+]+$/]
srcExt == 'c++' ? 'cc' : srcExt
end
def compiler_for(src)
COMPILER_INFO[source_type(src)][:rule]
end
def pch_for(src, target)
srcExt = source_type(src)
target['PRELUDE'].each do |pch|
if srcExt == source_type(pch)
res = build_path(pch)
res << "-no-arc" if srcExt == 'm' && target['OBJC_FLAGS'] =~ /-fno-objc-arc/
res << "-no-arc" if srcExt == 'mm' && target['OBJCXX_FLAGS'] =~ /-fno-objc-arc/
return res
end
end
abort "*** no pre-compiled header for #{src} (target: #{target[:name]})"
end
def all_values_for_key(target, key)
all_targets = { }
new_targets = [ target ]
until new_targets.empty?
target = new_targets.pop
unless all_targets.include? target[:base]
all_targets[target[:base]] = target
target['LINK'].to_a.each do |name|
TARGETS.each { |target| new_targets << target if name == target[:name] }
end
end
end
all_targets.map { |path, target| target[key].to_a }.flatten.sort.uniq
end
def headers(target)
res = ''
return res if target['EXPORT'].to_s.empty?
headers = [ ]
target['EXPORT'].to_a.each do |path|
src = File.join(target[:path], path)
dst = build_path("include/#{target[:name]}/#{File.basename(path)}")
res << "build #{dst}: cp #{esc_path src}\n"
headers << dst
end
headers = headers.concat(target['LINK'].map { |lib| "#{esc_path lib}/headers" }) unless target['LINK'].nil?
res << "build #{target[:name]}/headers: phony | #{headers.join(' ')}\n"
res
end
def sources(target)
res = ''
objects = [ ]
headers = target['LINK'].to_a.map { |goal| "#{goal}/headers" }
target['SOURCES'].each do |path|
src = File.join(target[:path], path)
case src
when /^(.*)\.(c|cc|c\+\+|m|mm)$/ then
base, ext = $1, $2
cc = compiler_for(src)
dst = build_path("#{base}.o")
res << "build #{dst}: #{cc} #{esc_path src} | #{pch_for(src, target)}.gch || #{esc_path(headers).join(' ')}\n"
res << " depfile = #{dst}.d\n"
res << " PCH_FLAGS = -D#{target[:name].gsub(/[^A-Za-z_]/, '_')}_EXPORTS -include #{pch_for(src, target)}\n"
res << flags_for_target(target)
objects << dst
when /^(.*)\.(rl)$/ then
base, ext = $1, $2
dst = build_path("#{base}.cc")
res << "build #{dst}: gen_ragel #{esc_path src} || #{esc_path(headers).join(' ')}\n"
ext = 'cc'
cc = compiler_for(dst)
obj = build_path("#{base}.o")
res << "build #{obj}: #{cc} #{dst} | #{pch_for(dst, target)}.gch || #{esc_path(headers).join(' ')}\n"
res << " depfile = #{obj}.d\n"
res << " PCH_FLAGS = -D#{target[:name].gsub(/[^A-Za-z_]/, '_')}_EXPORTS -include #{pch_for(dst, target)} -iquote#{esc_path File.dirname(src)}\n"
res << flags_for_target(target)
objects << obj
when /^(.*)\.capnp$/ then
base = $1
res << "build #{src}.c++ #{src}.h: gen_capnp #{esc_path src} || #{esc_path(headers).join(' ')}\n"
src = "#{src}.c++"
cc = compiler_for(src)
dst = build_path("#{base}.o")
res << "build #{dst}: #{cc} #{esc_path src} | #{pch_for(src, target)}.gch || #{esc_path(headers).join(' ')}\n"
res << " depfile = #{dst}.d\n"
res << " PCH_FLAGS = -D#{target[:name].gsub(/[^A-Za-z_]/, '_')}_EXPORTS -include #{pch_for(src, target)}\n"
res << flags_for_target(target)
objects << dst
else
STDERR << "*** unhandled source type #{path}\n"
end
end
target[:objects] = objects
res
end
def clean(target)
res = ''
dst = build_path(File.join(target[:path]))
res << "build #{dst}/#{target[:name]}.clean: clean\n"
res << " path = #{dst}\n"
res << "build #{target[:name]}/clean: phony #{dst}/#{target[:name]}.clean\n"
end
def tests(target)
headers = target['LINK'].to_a.map { |goal| "#{goal}/headers" }
fws = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
libs = prepend('-l', all_values_for_key(target, 'LIBS'))
res = ''
if !target['TESTS'].to_s.empty?
dst = build_path("#{target[:path]}/test_#{target[:name]}")
src = target['TESTS'].map { |path| esc_path(File.join(target[:path], path)) }.join(' ')
ext = target['TESTS'].any? { |path| path =~ /\.mm$/ } ? 'mm' : 'cc'
test_src = "#{dst}.#{ext}"
res << "build #{test_src}: gen_oak_test #{src} | bin/gen_test\n"
res << "build #{dst}.o: #{compiler_for(test_src)} #{test_src} | #{pch_for(test_src, target)}.gch || #{target[:name]}/headers\n"
res << " depfile = #{dst}.o.d\n"
res << " PCH_FLAGS = -include #{pch_for(test_src, target)}\n"
res << flags_for_target(target)
res << "build #{dst}: link_oak_test #{all_values_for_key(target, :objects).join(' ')} #{dst}.o\n"
res << flags_for_target(target)
res << " RAVE_FLAGS = #{fws} #{libs}\n"
res << "build #{dst}.run: run_test #{dst}\n"
res << "build #{dst}.always-run: always_run_test #{dst}\n"
res << "build #{dst}.coerce: skip_test #{dst}\n"
res << " seal = #{dst}.run\n"
res << "build #{target[:name]}: phony #{dst}.run\n"
res << "build #{target[:name]}/test: phony #{dst}.always-run\n"
res << "build #{target[:name]}/coerce: phony #{dst}.coerce\n"
target[:test] = "#{dst}.run"
end
if !target['TEST_SOURCES'].to_s.empty?
dst = build_path("#{target[:path]}/cxx_test_#{target[:name]}")
src = target['TEST_SOURCES'].map { |path| esc_path(File.join(target[:path], path)) }.join(' ')
ext = target['TEST_SOURCES'].any? { |path| path =~ /\.mm$/ } ? 'mm' : 'cc'
test_src = "#{dst}.#{ext}"
res << "build #{test_src}: gen_cxx_test #{src}\n"
res << "build #{dst}.o: #{compiler_for(test_src)} #{test_src} | #{pch_for(test_src, target)}.gch || #{target[:name]}/headers\n"
res << " depfile = #{dst}.o.d\n"
res << " PCH_FLAGS = -Ibin/CxxTest -include #{pch_for(test_src, target)}\n"
res << flags_for_target(target)
res << "build #{dst}: link_cxx_test #{all_values_for_key(target, :objects).join(' ')} #{dst}.o\n"
res << flags_for_target(target)
res << " RAVE_FLAGS = #{fws} #{libs}\n"
res << "build #{dst}.run: run_test #{dst}\n"
res << "build #{dst}.always-run: always_run_test #{dst}\n"
res << "build #{target[:name]}/cxx_test: phony #{dst}.always-run\n"
end
res
end
def resources_helper(target, key, symbol)
res = ''
rsrc = [ ]
all_resources = [ ]
depeends_on_directories = Array.new(target[:depeends_on_directories].to_a)
target[key].to_a.each do |path|
if path =~ /^@/
src = TARGETS.target_named($')
abort "*** no resources in target: #{path} (from target: #{target[:name]}).\n" unless src.has_key?(:rsrc_info)
src[:rsrc_info].each do |info|
rsrc << { :src => info[:src], :dst => esc_path(info[:install_name]) }
end
else
src = File.join(target[:path], path)
abs = File.join(target[:base], path)
if File.directory?(abs) && !File.symlink?(abs) && abs !~ /\.xcdatamodeld$/
depeends_on_directories << abs
Dir.chdir(abs) { Dir.glob("**/*") }.each do |file|
if File.directory?(File.join(abs, file))
depeends_on_directories << File.join(abs, file)
else
all_resources << { :path => File.dirname(src), :file => File.join(File.basename(src), file) }
end
end
else
all_resources << { :path => File.dirname(src), :file => File.basename(src) }
end
end
end
target[:depeends_on_directories] = depeends_on_directories.sort.uniq
all_resources.each do |dict|
path, file = dict[:path], dict[:file]
src = File.join(path, file)
case file
when /(.+)\.xib$/ then
dst = build_path(File.join(path, "#$1.nib"))
res << "build #{dst}: compile_xib #{esc_path src}\n"
rsrc << { :src => dst, :dst => esc_path("#$1.nib") }
when /(.+)\.strings$/ then
dst = build_path(File.join(path, file))
res << "build #{dst}: cp_as_utf16 #{esc_path src}\n"
rsrc << { :src => dst, :dst => esc_path(file) }
when /(.+)\.xcdatamodeld$/ then
dst = build_path(File.join(path, "#$1.mom"))
res << "build #{dst}: compile_mom #{esc_path src}\n"
rsrc << { :src => dst, :dst => esc_path("#$1.mom") }
when /\bInfo\.plist$/ then
dst = build_path(File.join(path, file))
res << "build #{dst}: process_plist #{esc_path src} | bin/process_plist\n"
res << " RAVE_FLAGS = -dTARGET_NAME='#{target[:name]}'\n"
res << flags_for_target(target)
target[:info_plist] = dst
when /(.+)\.md(own)?$/ then
dst = build_path(File.join(path, "#$1.html"))
prefix, suffix = target['MARKDOWN_HEADER'].to_a, target['MARKDOWN_FOOTER'].to_a
res << "build #{dst}: markdown #{prefix.map { |path| esc_path(File.join(target[:path], path))}.join(' ')} #{esc_path src} #{suffix.map { |path| esc_path(File.join(target[:path], path))}.join(' ')} | bin/gen_html\n"
header, footer = target['HTML_HEADER'], target['HTML_FOOTER']
flags = ''
flags << " -h #{esc_path(File.join(target[:path], header.first))}" if header.to_a.size == 1
flags << " -f #{esc_path(File.join(target[:path], footer.first))}" if footer.to_a.size == 1
res << " md_flags =#{flags}\n"
rsrc << { :src => dst, :dst => esc_path("#$1.html") }
else
rsrc << { :src => esc_path(src), :dst => esc_path(file) }
end
end
help_files = rsrc.find_all { |dict| dict[:src] =~ %r{[^/]+ Help/.+\.html$} }
unless help_files.empty?
src = "#$&/#{target[:name]}.helpindex" if help_files.first[:src] =~ /.* Help(?=\/)/
dst = "#$&/#{target[:name]}.helpindex" if help_files.first[:dst] =~ /.* Help(?=\/)/
res << "build #{src}: index_help | #{help_files.map { |dict| dict[:src] }.join(' ')}\n"
res << " HELP_BOOK = #{File.dirname(src)}\n"
rsrc << { :src => src, :dst => dst }
end
target[symbol] = rsrc
res
end
def resources(target)
res = ''
target.keys.each do |key|
res << resources_helper(target, key, key.to_sym) if key.class == String && key =~ /^CP_/
end
res
end
def object_archive(target)
dst = build_path("#{target[:path]}/lib#{target[:name]}.a")
target[:lib] = dst
res = ''
res << "build #{dst}: link_static #{target[:objects].join(' ')} | #{target[:test]}\n"
res << flags_for_target(target)
end
def dynamic_lib(target)
dst = target[:dylib] = build_path("#{target[:path]}/#{target[:name]}.dylib")
if target[:CP_Resources].to_a.empty?
install_name = "@rpath/#{target[:name]}.dylib"
else
install_name = "@rpath/#{target[:name]}.framework/Versions/A/#{target[:name]}"
end
link_with = all_values_for_key(target, 'LINK').map { |name| TARGETS.target_named(name)[:dylib] }
res = ''
res << "build #{dst}: link_dynamic #{target[:objects].join(' ')} | #{target[:test]} #{link_with.join(' ')}\n"
res << flags_for_target(target)
fws = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
libs = prepend('-l', all_values_for_key(target, 'LIBS'))
res << " RAVE_FLAGS = -install_name #{esc_path install_name} #{link_with.join(' ')} #{fws} #{libs}\n"
end
def framework_bundle(target, dst_dir = nil)
res = ''
dst = File.join(dst_dir || build_path("#{target[:path]}"), "#{target[:name]}.framework")
deps = [ ]
target.each do |key, files|
if key.class == Symbol && key.to_s =~ /^CP_(.+)$/
install_dir = $1
files.each do |rsrc|
deps << "#{dst}/Versions/A/#{install_dir}/#{rsrc[:dst]}"
res << "build #{deps.last}: cp #{rsrc[:src]}\n"
end
end
end
deps << "#{dst}/Versions/A/Resources/Info.plist"
res << "build #{deps.last}: cp #{target[:info_plist]}\n"
deps << "#{dst}/Versions/A/#{target[:name]}"
res << "build #{deps.last}: cp #{target[:dylib]}\n"
deps << "#{dst}/Versions/Current"
res << "build #{deps.last}: ln\n link = A\n"
deps << "#{dst}/Resources"
res << "build #{deps.last}: ln\n link = Versions/Current/Resources\n"
deps << "#{dst}/#{target[:name]}"
res << "build #{deps.last}: ln\n link = Versions/Current/#{target[:name]}\n"
res << "build #{dst}: phony | #{deps.join(' ')}\n"
end
def static_executable(target)
objects = [ ]
all_values_for_key(target, 'LINK').each do |name|
objects << TARGETS.target_named(name)[:lib]
end
objects << target[:objects]
res = ''
dst = build_path("#{target[:path]}/#{target[:name]}")
target[:static_executable] = dst
target[:rsrc_info] = [ { :install_name => "#{target[:name]}", :src => dst } ]
res << "build #{dst}: link_executable #{objects.flatten.join(' ')}\n"
res << flags_for_target(target)
fws = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
libs = prepend('-l', all_values_for_key(target, 'LIBS'))
res << " RAVE_FLAGS = #{fws} #{libs}\n"
res << "build #{dst}.run: run_executable #{dst}\n"
res << "build #{dst}.debug: debug_executable #{dst}\n"
res << "build #{target[:name]}: phony #{dst}\n"
res << "build #{target[:name]}/run: phony #{dst}.run\n"
res << "build #{target[:name]}/debug: phony #{dst}.debug\n"
res
end
def dynamic_executable(target)
res = ''
link_with = all_values_for_key(target, 'LINK').map { |name| TARGETS.target_named(name)[:dylib] }
dst = build_path("#{target[:path]}/#{target[:name]}.dyn")
target[:dynamic_executable] = dst
res << "build #{dst}: link_executable #{target[:objects].join(' ')} | #{link_with.join(' ')}\n"
res << flags_for_target(target)
fws = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
libs = prepend('-l', all_values_for_key(target, 'LIBS'))
res << " RAVE_FLAGS = #{link_with.join(' ')} #{fws} #{libs}\n"
res
end
def app_bundle(target)
res = ''
deps = [ ]
all_files = [ ]
ext = target['BUNDLE_EXTENSION'] || 'app'
dst = build_path("#{target[:path]}/#{target[:name]}.#{ext}")
target[:bundle] = dst
fw_dst = File.join(dst, "Contents/Frameworks")
all_values_for_key(target, 'LINK').each do |name|
tmp = TARGETS.target_named(name)
if tmp[:CP_Resources].to_a.empty?
res << "build #{fw_dst}/#{tmp[:name]}.dylib: cp #{tmp[:dylib]}\n"
deps << "#{fw_dst}/#{tmp[:name]}.dylib"
else
res << framework_bundle(tmp, fw_dst)
deps << "#{fw_dst}/#{tmp[:name]}.framework"
end
end
target.each do |key, files|
if key.class == Symbol && key.to_s =~ /^CP_(.+)$/
install_dir = $1
files.each do |rsrc|
deps << "#{dst}/Contents/#{install_dir}/#{rsrc[:dst]}"
res << "build #{deps.last}: cp #{rsrc[:src]}\n"
all_files << { :install_name => "#{target[:name]}.#{ext}/Contents/#{install_dir}/#{rsrc[:dst]}", :src => rsrc[:src] }
end
end
end
deps << "#{dst}/Contents/Info.plist"
res << "build #{deps.last}: cp #{target[:info_plist]}\n"
all_files << { :install_name => "#{target[:name]}.#{ext}/Contents/Info.plist", :src => target[:info_plist] }
deps << "#{dst}/Contents/MacOS/#{target[:name]}"
res << "build #{deps.last}: cp #{target[:dynamic_executable]}\n"
all_files << { :install_name => "#{target[:name]}.#{ext}/Contents/MacOS/#{target[:name]}", :src => target[:dynamic_executable] }
res << "build #{dst}: phony | #{deps.join(' ')}\n"
res << "build #{dst}.sign: sign_executable #{dst}\n"
res << "build #{dst}.run: run_application #{dst} | #{dst}.sign\n"
res << " appname = #{target[:name]}\n"
res << "build #{dst}.debug: debug_executable #{dst}/Contents/MacOS/#{target[:name]} | #{dst}.sign\n"
res << "build #{target[:name]}: phony #{dst}.sign\n"
res << "build #{target[:name]}/run: phony #{dst}.run\n"
res << "build #{target[:name]}/debug: phony #{dst}.debug\n"
target[:rsrc_info] = all_files
res
end
def upload(target)
res = ''
exe = target[:static_executable]
dir, _ = File.split(exe)
name = "#{target[:name]}_${APP_VERSION}"
tbz = "#{dir}/archive/#{name}.tbz"
meta = "#{dir}/meta/#{name}"
res << "build #{exe}.sign: sign_executable #{exe}\n"
res << "build #{tbz}: tbz_archive #{exe} | #{exe}.sign\n"
res << "build #{meta}.upload: upload #{tbz}\n"
res << "build #{target[:name]}/deploy: phony #{meta}.upload\n"
end
def deploy(target)
res = ''
dir, _ = File.split(target[:bundle])
name = "#{target[:name]}_${APP_VERSION}"
dsym = "#{dir}/dsym/#{name}.tbz"
tbz = "#{dir}/archive/#{name}.tbz"
meta = "#{dir}/meta/#{name}"
res << "build #{dsym}: dsym #{target[:bundle]}\n"
res << "build #{tbz}: tbz_archive #{target[:bundle]} | #{target[:bundle]}.sign\n"
res << "build #{meta}.upload: upload #{tbz}\n"
res << "build #{meta}.deploy: deploy #{meta}.upload | #{dsym}\n"
res << "build #{target[:name]}/dsym: phony #{dsym}\n"
res << "build #{target[:name]}/tbz: phony #{tbz}\n"
res << "build #{target[:name]}/deploy: phony #{meta}.deploy\n"
end
def all_targets(buildfile, builddir)
res = Set.new
return res unless File.exists? buildfile
Dir.chdir(File.dirname(buildfile)) do
targets = %x{ ${TM_NINJA:-ninja} -f #{[ buildfile ].shelljoin } -t targets all }
return nil if $? != 0
targets.each do |line|
if line =~ /.*(?=:(?! phony))/
path = $&
res << path if path =~ /^#{Regexp.escape(builddir)}/ # ignore targets outside builddir
end
end
end
res
end
# =======================
# = Program Starts Here =
# =======================
opts = GetoptLong.new(
[ '--help', '-h', GetoptLong::NO_ARGUMENT ],
[ '--output', '-o', GetoptLong::REQUIRED_ARGUMENT ],
[ '--build-directory', '-C', GetoptLong::REQUIRED_ARGUMENT ],
[ '--define', '-d', GetoptLong::REQUIRED_ARGUMENT ]
)
outfile, builddir = '/dev/stdout', File.expand_path('~/build')
variables = [ ]
opts.each do |opt, arg|
case opt
when '--help'
RDoc::usage
when '--output'
outfile = arg
when '--build-directory'
builddir = arg
when '--define'
variables << [ $1, $2 ] if arg =~ /^(\w+)\s*=\s*(.*)$/
end
end
abort "No root target file specified" if ARGV.empty?
manifest = ARGV[0]
TARGETS = Targets.new(manifest)
all_old_targets = all_targets(outfile, builddir)
open(outfile, "w") do |io|
userfile = "#{ENV['USER']}.ninja"
io << "###########################################\n"
io << "# AUTOMATICALLY GENERATED -- DO NOT EDIT! #\n"
io << "###########################################\n"
io << "\n"
io << "builddir = #{builddir}\n"
io << "include $builddir/build.ninja\n"
io << "include #{userfile}\n" if userfile != 'build.ninja' && File.exists?(File.join(File.dirname(outfile), userfile))
end
open("#{builddir}/build.ninja", "w") do |io|
user_variables = TARGETS.root.to_a.reject { |arr| arr[0].class != String || GLOB_KEYS.include?(arr[0]) }.sort { |lhs, rhs| lhs[0] <=> rhs[0] }
tmp = variables.dup.concat(user_variables)
width = tmp.map { |arr| arr[0].length }.max { |lhs, rhs| lhs <=> rhs }
io << variables.map { |arr| format("%-#{width}s = %s\n", *arr) }
io << "\n"
io << user_variables.map { |arr| format("%-#{width}s = %s\n", *arr) }
io << "\n"
args = variables.map { |key, value| "-d'#{key}=#{value =~ /\$/ ? value.gsub(/\$/, '$$') : "$#{key}"}'" }.join(' ')
io << "rule configure\n"
io << " command = bin/gen_build -C \"$builddir\" #{args} -o $out $in\n"
io << " depfile = $builddir/$out.d\n"
io << " generator = 1\n"
io << " description = Generate $out\n"
io << "\n"
io << "build build.ninja: configure #{manifest} | bin/gen_build\n"
io << "\n"
io << DATA.read << "\n"
COMPILER_INFO.each do |key, value|
io << "rule #{value[:rule]}\n"
io << " command = '#{value[:compiler]}' $PCH_FLAGS $FLAGS #{value[:flags]} $RAVE_FLAGS -o $out -MMD -MF $out.d -I$builddir/include $in\n"
io << " depfile = $depfile\n"
io << " deps = gcc\n"
io << " description = Compile $in\n\n"
end
TARGETS.root['PRELUDE'].each do |src|
type = { 'c' => '-x c-header', 'm' => '-x objective-c-header', 'cc' => '-x c++-header', 'mm' => '-x objective-c++-header' }
ext = source_type(src)
cc = COMPILER_INFO[ext][:compiler]
io << "build #{pch_for(src, TARGETS.root)}.gch: #{compiler_for(src)} #{src} | #{cc}\n"
io << " depfile = #{pch_for(src, TARGETS.root)}.gch.d\n"
io << " RAVE_FLAGS = #{type[ext]}\n\n"
if ext =~ /^mm?$/
io << "build #{pch_for(src, TARGETS.root)}-no-arc.gch: #{compiler_for(src)} #{src} | #{cc}\n"
io << " depfile = #{pch_for(src, TARGETS.root)}-no-arc.gch.d\n"
io << " RAVE_FLAGS = #{type[ext]} -fno-objc-arc\n\n"
end
end
# =======================================================
# = Do a topological sort based on the dependency graph =
# =======================================================
dag = ''
TARGETS.each do |target|
dag << "#{target[:name]}\t#{target[:name]}\n" if target['LINK'].nil?
target['LINK'].each { |dep| dag << "#{target[:name]}\t#{dep}\n" } unless target['LINK'].nil?
target.each do |key, value|
value.each { |rsrc| dag << "#{target[:name]}\t#$'\n" if rsrc =~ /^@/ } if key =~ /^CP_/
end
end
dag = open('|/usr/bin/tsort -q', 'r+') { |tsort| tsort << dag; tsort.close_write; tsort.read }
# =========================================================================
# = Now iterate the targets (in the sorted order) to generate build rules =
# =========================================================================
dag.scan(/.+/).reverse_each do |name|
target = TARGETS.target_named(name)
io << clean(target)
io << sources(target)
io << resources(target)
if target[:type] == :library
io << headers(target)
io << object_archive(target)
io << tests(target)
io << dynamic_lib(target)
elsif target[:type] == :executable
io << static_executable(target)
io << upload(target)
else
io << dynamic_executable(target)
io << app_bundle(target)
io << deploy(target)
end
end
io << "\n"
io << "build run: phony #{variables.find { |k, _| k == 'APP_NAME' }.last}/run\n"
io << "build deploy: phony #{variables.find { |k, _| k == 'APP_NAME' }.last}/deploy\n"
io << "build debug: phony #{variables.find { |k, _| k == 'APP_NAME' }.last}/debug\n"
io << "default run\n"
end
open("#{builddir}/build.ninja.d", "w") do |io|
deps = TARGETS.dependencies.map { |path| path.gsub(/ /, '\\ ') }
io << outfile << ": " << deps.join(" \\\n ") << "\n"
end
all_new_targets = all_targets(outfile, builddir)
if all_old_targets && all_new_targets
targets_lost = all_old_targets - all_new_targets
targets_lost.each do |path|
if File.exists?(path)
STDERR << "Remove old target #{path.sub(/#{Regexp.escape(builddir)}/, '$builddir')}’…\n"
File.unlink(path)
end
end
end
# =======================
__END__
rule touch
command = touch $out
generator = true
description = Touch $out
rule cp
command = cp -p $in $out
description = Copy $in
rule cp_as_utf16
command = if [[ $$(head -c2 $in) == $$'\xFF\xFE' || $$(head -c2 $in) == $$'\xFE\xFF' ]]; then cp -p $in $out; else iconv -f utf-8 -t utf-16 < $in > $out~ && mv $out~ $out; fi
description = Copy $in as UTF-16…
rule ln
command = ln -fs $link $out
description = Link $out
rule gen_ragel
command = ragel -o $out $in
description = Generate source from $in
rule gen_capnp
command = capnp compile -oc++ $in
generator = 1
description = Generate source from $in
rule gen_cxx_test
command = bin/CxxTest/bin/cxxtestgen --have-std -o $out --runner=unix $in
description = Generate test $out
rule link_cxx_test
command = '$CXX' -o $out $in $LN_FLAGS $RAVE_FLAGS
description = Link test $out
rule gen_oak_test
command = bin/gen_test $in > $out~ && mv $out~ $out
description = Generate test $out
rule link_oak_test
command = '$CXX' -o $out $in $LN_FLAGS $RAVE_FLAGS
description = Link test $out
rule run_test
command = $in && touch $out
description = Run test $in
rule always_run_test
command = $in
description = Run test $in
rule skip_test
command = touch "$seal"
description = Skip test $in
rule link_static
command = rm -f $out && ar -cqs $out $in
description = Archive objects $out
rule link_dynamic
command = '$CXX' -o $out $in $LN_FLAGS $RAVE_FLAGS -dynamiclib -current_version 1.0.1 -compatibility_version 1.0.0
description = Link dynamic library $out
rule link_executable
command = '$CXX' -o $out $in $LN_FLAGS $RAVE_FLAGS
description = Link executable $out
rule run_executable
command = $in
description = Run $in
rule debug_executable
command = osascript >/dev/null -e $$'tell app "Terminal" \n activate \n do script "gdb $in" \n end tell'
description = Debug $in
rule run_application
command = { $
if pgrep >/dev/null "$appname"; then $
if [[ -x "$$DIALOG" && $$("$$DIALOG" < /dev/null alert --title "Relaunch $appname?" --body "Would you like to quit $appname and start the newly built version?" --button1 Relaunch --button2 Cancel|pl) != *"buttonClicked = 0"* ]]; $
then exit; $
fi; $
pkill "$appname"; $
while pgrep >/dev/null "$appname"; do $
if (( ++n == 10 )); then $
test -x "$$DIALOG" && "$$DIALOG" </dev/null alert --title "Relaunch Timed Out" --body "Unable to exit $appname." --button1 OK; $
exit; $
fi; $
sleep .2; $
done; $
fi; $
open $in; $
} &>/dev/null &
description = Run $in
rule sign_executable
command = codesign -fs "$identity" $in && touch $out
description = Sign $in
rule tbz_archive
command = tar $bzip2_flag -cf $out -C "$$(dirname $in)" "$$(basename $in)"
generator = true
description = Archive $in
rule upload
command = bin/upload -k$upload_keyfile -d$upload_destination -m '{"version":"$APP_VERSION","revision":"$APP_REVISION"}' $in > $out~ && mv $out~ $out
generator = true
description = Upload $in
rule deploy
command = curl -sfnd @$in '${rest_api}/releases/nightly' && touch $out
generator = true
description = Deploy…
rule bump_revision
command = ruby -pe '$$_.gsub!(/(APP_REVISION\s*=\s*)(\d+)/) { newRev = $$2.to_i + 1; STDERR << "r#$$2 → r#{newRev}\n"; "#$$1#{newRev}#$$3" }' $in > $in~ && mv $in~ $in && touch $builddir/bumped
description = Increment version number…
build $builddir/always_bump_revision: bump_revision $builddir/build.ninja
build bump: phony $builddir/always_bump_revision
rule clean
command = rm -r '$path'
rule dsym
command = $
DST=$$(/usr/bin/mktemp -dt dsym); $
find $in -name rmate -or -type f -perm +0111 -print|while read line; $
do dsymutil --flat --out "$$DST/$$(basename "$$line" .dylib).dSYM" "$$line"; $
done && tar $bzip2_flag -cf $out -C "$$DST" . && rm -rf "$$DST"
generator = true
description = Archive dSYM info for $in
rule compile_xib
command = "$xcodedir/usr/bin/ibtool" --errors --warnings --notices --output-format human-readable-text --compile $out $in
description = Compile xib $in
rule process_plist
command = bin/process_plist > $out~ $in $PLIST_FLAGS $RAVE_FLAGS && mv $out~ $out
description = Process plist $in
rule markdown
command = bin/gen_html > $out~ $md_flags $in && mv $out~ $out
description = Generate $out
rule index_help
command = /usr/bin/hiutil -Cvaf $out "$HELP_BOOK"
description = Index help book $HELP_BOOK
rule compile_mom
command = "$xcodedir/usr/bin/momc" $in $out
description = Generate $out