mirror of
https://github.com/textmate/textmate.git
synced 2026-04-28 03:00:34 -04:00
This suppresses the below warning when compiling xibs with Xcode 7 or later.
warning: This file is set to build for a version older than the deployment target. Functionality may be limited. [9]
We should revisit this when we update our minimum deployment target to 10.8 or higher, as it may not be needed.
972 lines
32 KiB
Ruby
Executable File
972 lines
32 KiB
Ruby
Executable File
#!/System/Library/Frameworks/Ruby.framework/Versions/Current/usr/bin/ruby
|
||
# == 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.
|
||
$KCODE = 'U' if RUBY_VERSION < "1.9"
|
||
require 'optparse'
|
||
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 target['EXPORT'] || 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 << " PCH_FLAGS = -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 << " PCH_FLAGS = -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 << " PCH_FLAGS = -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 << " 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 << " 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]}/upload: phony #{meta}.upload\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.shellescape} -t targets all }
|
||
return nil if $? != 0
|
||
targets.each_line 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 =
|
||
# =======================
|
||
|
||
outfile, builddir = '/dev/stdout', File.expand_path('~/build')
|
||
variables = [ ]
|
||
|
||
OptionParser.new do |opts|
|
||
opts.banner = "Usage: gen_build [options] "
|
||
opts.separator "Synopsis"
|
||
opts.separator "gen_build: create build.ninja based on target files"
|
||
opts.separator "Options:"
|
||
|
||
opts.on("-h","--help", "show help.") do |v|
|
||
puts opts
|
||
exit
|
||
end
|
||
|
||
opts.on("-o","--output FILE", "the «file» to write build info into.") do |v|
|
||
outfile = v
|
||
end
|
||
|
||
opts.on("-C", "--build-directory DIRECTORY", "where build files should go.") do |v|
|
||
builddir = v
|
||
end
|
||
|
||
opts.on("-d", "--define NAME=VALUE", "define a variable that ends up in build.ninja") do |v|
|
||
variables << [ $1, $2 ] if v =~ /^(\w+)\s*=\s*(.*)$/
|
||
end
|
||
end.parse!
|
||
|
||
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 << "ninja_required_version = 1.5\n"
|
||
io << "\n"
|
||
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.each do |key, value|
|
||
user_variables << [ key, value.kind_of?(Array) ? value.join(' ') : value ] unless key.class != String || GLOB_KEYS.include?(key)
|
||
end
|
||
user_variables.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 }
|
||
|
||
variables.each { |arr| io << format("%-#{width}s = %s\n", *arr) }
|
||
io << "\n"
|
||
|
||
user_variables.each { |arr| io << 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 = true\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 = $out.d\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}\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}\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 = PATH="$capnp_prefix/bin:$PATH" capnp compile -oc++ $in
|
||
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 $test_flags && touch $out
|
||
description = Run test ‘$in’…
|
||
|
||
rule always_run_test
|
||
command = $in $test_flags
|
||
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 = lldb $in
|
||
pool = console
|
||
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 --args -disableSessionRestore NO; $
|
||
} &>/dev/null &
|
||
description = Run ‘$in’…
|
||
|
||
rule sign_executable
|
||
command = xcrun codesign --timestamp=none --deep -fs "$identity" $in && touch $out
|
||
description = Sign ‘$in’…
|
||
|
||
rule tbz_archive
|
||
command = tar $bzip2_flag -cf $out~ -C "$$(dirname $in)" "$$(basename $in)" && mv $out~ $out
|
||
generator = true
|
||
description = Archive ‘$in’…
|
||
|
||
rule upload
|
||
command = bin/upload -k$upload_keyfile -d$upload_destination -t'v$APP_VERSION' -m'{"version":"$APP_VERSION"}' $in > $out~ && mv $out~ $out
|
||
pool = console
|
||
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_VERSION\s*=\s*(.*?))(\d+)$$/) { newRev = $$3.to_i + 1; STDERR << "#$$2#$$3 → #$$2#{newRev}\n"; "#$$1#{newRev}" }' $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 xcrun dsymutil --flat --out "$$DST/$$(basename "$$line" .dylib).dSYM" "$$line"; $
|
||
done && tar $bzip2_flag -cf $out~ -C "$$DST" . && rm -rf "$$DST" && mv $out~ $out
|
||
generator = true
|
||
description = Archive dSYM info for ‘$in’…
|
||
|
||
rule compile_xib
|
||
command = xcrun ibtool --errors --warnings --notices --output-format human-readable-text --minimum-deployment-target $APP_MIN_OS --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 = xcrun momc $in $out
|
||
description = Generate ‘$out’…
|