mirror of
https://github.com/textmate/textmate.git
synced 2026-01-14 01:08:04 -05:00
960 lines
30 KiB
Ruby
Executable File
960 lines
30 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.
|
||
require 'optparse'
|
||
require 'shellwords'
|
||
require 'fileutils'
|
||
require 'set'
|
||
|
||
BIN = File.dirname(__FILE__)
|
||
|
||
class Context
|
||
def initialize(options = { }, dir = nil, parent = nil)
|
||
@options, @dir, @parent = options, dir, parent
|
||
end
|
||
|
||
def derive(options, dir = nil)
|
||
Context.new(options, dir, self)
|
||
end
|
||
|
||
def to_h
|
||
chain.inject(Hash.new) do |hash, obj|
|
||
obj.options.each do |key, value|
|
||
hash[key] = hash.key?(key) ? value + "\n" + hash[key] : value
|
||
end
|
||
hash
|
||
end
|
||
end
|
||
|
||
def keys
|
||
chain.flat_map { |obj| obj.options.keys }.uniq
|
||
end
|
||
|
||
def [](key)
|
||
res = chain.map { |obj| obj.options[key] }.reject { |value| value.nil? }
|
||
res.empty? ? nil : res.join("\n")
|
||
end
|
||
|
||
def array_for(key)
|
||
chain.flat_map do |obj|
|
||
value = obj.options[key]
|
||
value.is_a?(Array) ? value : Shellwords.shellwords(value.to_s)
|
||
end
|
||
end
|
||
|
||
def path_for(key)
|
||
if instance = chain.find { |obj| obj.options.key?(key) }
|
||
instance.dir ? File.join(instance.dir, instance.options[key]) : instance.options[key]
|
||
end
|
||
end
|
||
|
||
def paths_for(key)
|
||
chain.select { |obj| obj.options.key?(key) }.flat_map do |instance|
|
||
files = Dir.chdir(instance.dir || '.') do
|
||
globs = instance.array_for(key).reject { |glob| glob =~ /^@(\w+)/ }
|
||
globs.flat_map { |glob| Dir.glob(glob) }
|
||
end
|
||
files.map { |file| instance.dir ? File.join(instance.dir, file) : file }
|
||
end
|
||
end
|
||
|
||
def target_names_for(key)
|
||
self.array_for(key).map { |name| name =~ /^@(\w+)/ ? $1 : nil }.reject { |name| name.nil? }
|
||
end
|
||
|
||
protected
|
||
|
||
def chain
|
||
res = [ self ]
|
||
res << res.last.parent until res.last.parent.nil?
|
||
res
|
||
end
|
||
|
||
attr_reader :options, :dir, :parent
|
||
end
|
||
|
||
class Asset
|
||
attr_accessor :file, :context
|
||
|
||
def initialize(file, context = Context.new)
|
||
@file, @context = file, context
|
||
end
|
||
end
|
||
|
||
class Target
|
||
attr_reader :dir, :context
|
||
|
||
def Target.read(file)
|
||
res = { }
|
||
File.read(file).scan(/^([\w\/]+)\s*(\+=|-=|=)[ \t]*(.*)$/).each do |key, op, value|
|
||
if op == '+=' && res.key?(key)
|
||
res[key] << ' ' << value
|
||
else
|
||
res[key] = value
|
||
end
|
||
end
|
||
res
|
||
end
|
||
|
||
def initialize(dir, context)
|
||
@dir, @context = dir, context
|
||
@files = [ ]
|
||
end
|
||
|
||
def setup_build(buildfile, all_targets)
|
||
abort "*** setup_build already called" unless @files.empty?
|
||
|
||
buildfile.assign("#{self.name}_dir", File.join(buildfile.dir, self.dir))
|
||
|
||
targets = [ self.name, *link_dependencies(all_targets) ].map { |name| all_targets[name] }
|
||
objects = targets.flat_map { |target| target.objects(buildfile, all_targets) }
|
||
tests = targets.flat_map { |target| target.test_results(buildfile, all_targets) }
|
||
|
||
sign_flags, sign_deps = [], []
|
||
if entitlements = @context.path_for('CS_ENTITLEMENTS')
|
||
sign_flags << "--entitlements #{entitlements.shellescape}"
|
||
sign_deps << entitlements
|
||
end
|
||
if codesign_flags = @context['CODESIGN_FLAGS']
|
||
sign_flags << codesign_flags
|
||
end
|
||
|
||
is_bundle = targets.any? { |target| target.context.keys.any? { |key| key =~ /^CP_/ } }
|
||
|
||
if is_bundle
|
||
bundle = "#{self.name}.#{@context['BUNDLE_EXTENSION'] || 'app'}"
|
||
bundle_path = File.join(buildfile.dir, self.dir, bundle)
|
||
|
||
resources = [ ]
|
||
|
||
targets.each do |target|
|
||
target.resources(buildfile, all_targets).each do |file, path, deps|
|
||
# Do not copy the main Info.plist from other targets
|
||
next if path == 'Contents/Info.plist' && target != self
|
||
resources << buildfile.build('copy', file, File.join(buildfile.dir, self.dir, bundle, path), nil, deps)
|
||
@files << [ file, File.join(bundle, path), deps ]
|
||
end
|
||
end
|
||
|
||
executable = "#{bundle_path}/Contents/MacOS/#{self.name}"
|
||
signature = "#{bundle_path}/Contents/_CodeSignature/CodeResources"
|
||
|
||
buildfile.build('link', objects, executable, link_flags(all_targets))
|
||
buildfile.meta(bundle_path, executable, *resources)
|
||
buildfile.build('sign', bundle_path, signature, sign_flags, sign_deps)
|
||
buildfile.meta(self.name, signature)
|
||
|
||
@files << [ executable, "#{bundle}/Contents/MacOS/#{self.name}", signature ]
|
||
@files << [ signature, "#{bundle}/Contents/_CodeSignature/CodeResources" ]
|
||
|
||
if File.extname(bundle) == '.app'
|
||
buildfile.build('run_application', bundle_path, "#{bundle_path}.run", nil, signature)
|
||
buildfile.meta("#{self.name}/run", "#{bundle_path}.run")
|
||
buildfile.assign("#{self.name}_app", bundle_path)
|
||
buildfile.assign("#{self.name}_sign", signature)
|
||
end
|
||
else
|
||
signature = File.join(buildfile.dir, self.dir, "#{self.name}.sign")
|
||
executable = File.join(buildfile.dir, self.dir, self.name)
|
||
|
||
buildfile.build('link', objects, executable, link_flags(all_targets))
|
||
buildfile.build('sign', executable, signature, sign_flags, sign_deps)
|
||
buildfile.build('run', executable, "#{executable}.run", nil, signature)
|
||
buildfile.meta(self.name, signature)
|
||
buildfile.meta("#{self.name}/run", "#{executable}.run")
|
||
|
||
buildfile.assign("#{self.name}_exe", executable)
|
||
buildfile.assign("#{self.name}_sign", signature)
|
||
|
||
@files << [ executable, self.name, signature ]
|
||
end
|
||
|
||
buildfile.meta("#{self.name}/tests", *tests)
|
||
end
|
||
|
||
protected
|
||
|
||
attr_reader :files
|
||
|
||
def name
|
||
@context['TARGET_NAME']
|
||
end
|
||
|
||
def link_dependencies(all_targets)
|
||
if @link_dependencies.nil?
|
||
queue = [ self.name ]
|
||
seen = Set.new(queue)
|
||
while name = queue.shift
|
||
new_names = Set.new(all_targets[name].context.array_for('LINK'))
|
||
queue += (new_names - seen).to_a
|
||
seen += new_names
|
||
end
|
||
@link_dependencies = seen.delete(self.name).to_a.sort
|
||
end
|
||
@link_dependencies
|
||
end
|
||
|
||
def include_directory(buildfile)
|
||
if @include_directory.nil?
|
||
@include_directory = File.join(buildfile.dir, self.dir, 'include')
|
||
|
||
headers = @context.paths_for('EXPORT').map do |file|
|
||
buildfile.build('copy', file, File.join(@include_directory, self.name, File.basename(file)))
|
||
end
|
||
|
||
buildfile.meta(@include_directory, *headers)
|
||
end
|
||
@include_directory
|
||
end
|
||
|
||
def objects_for_sources(buildfile, all_targets, sources, include_dirs: nil)
|
||
dirs = (link_dependencies(all_targets) + @context.array_for('IMPORT')).uniq.map do |name|
|
||
all_targets[name].include_directory(buildfile)
|
||
end
|
||
|
||
dirs += include_dirs unless include_dirs.nil?
|
||
|
||
context = @context.derive({ 'include_dirs' => dirs })
|
||
|
||
sources.flat_map do |file|
|
||
buildfile.transform(Asset.new(file, context)).map do |asset|
|
||
abort "*** No rule to make object file from: #{file}" unless File.extname(asset.file) == '.o'
|
||
asset.file
|
||
end
|
||
end
|
||
end
|
||
|
||
def objects(buildfile, all_targets)
|
||
@objects ||= objects_for_sources(buildfile, all_targets, @context.paths_for('SOURCES'))
|
||
end
|
||
|
||
def resources(buildfile, all_targets)
|
||
if @resources.nil?
|
||
@resources = [ ]
|
||
|
||
@context.keys.each do |key|
|
||
if key =~ /^CP_(.+)/
|
||
asset_section = "Contents/#$1"
|
||
queue = @context.paths_for(key).map { |file| [ file, asset_section ] }
|
||
|
||
while info = queue.shift do
|
||
file, dest = *info
|
||
|
||
# Any Info.plist destined for Resources will instead be used as the bundle’s main Info.plist
|
||
dest = 'Contents' if File.basename(file) == 'Info.plist' && dest == 'Contents/Resources'
|
||
|
||
buildfile.transform(Asset.new(file, @context)).each do |new_asset|
|
||
if File.directory?(new_asset.file)
|
||
queue += Dir.entries(new_asset.file).reject { |path| path =~ /^\./ }.map do |entry|
|
||
[ File.join(new_asset.file, entry), File.join(dest, File.basename(file)) ]
|
||
end
|
||
else
|
||
@resources << [ new_asset.file, File.join(dest, File.basename(new_asset.file)) ]
|
||
end
|
||
end
|
||
end
|
||
|
||
@context.target_names_for(key).each do |name|
|
||
all_targets[name].files.each do |src, dst, deps|
|
||
@resources << [ src, File.join(asset_section, dst), deps ]
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
@resources
|
||
end
|
||
|
||
def link_flags(all_targets)
|
||
targets = [ self.name, *link_dependencies(all_targets) ].map { |name| all_targets[name] }
|
||
|
||
mixed_libs = targets.inject(Set.new) { |set, target| set += target.context.array_for('LIBS') }
|
||
frameworks = targets.inject(Set.new) { |set, target| set += target.context.array_for('FRAMEWORKS') }
|
||
ln_flags = targets.inject(Set.new) { |set, target| set += target.context.array_for('LN_FLAGS') }
|
||
|
||
static, dynamic = mixed_libs.partition { |lib| lib =~ /\.a$/ }
|
||
|
||
hash = { '' => [ *ln_flags, *static ], '-l' => dynamic, '-framework ' => frameworks }
|
||
hash.flat_map do |prefix, args|
|
||
args.map { |arg| prefix + arg.sub(/^(?=.*?\$).*$/, '"\&"') }
|
||
end
|
||
end
|
||
|
||
def test_results(buildfile, all_targets)
|
||
if @test_results.nil?
|
||
@test_results = []
|
||
|
||
cxx_test_sources = @context.paths_for('TESTS')
|
||
unless cxx_test_sources.empty?
|
||
ext = cxx_test_sources.any? { |file| File.extname(file) == '.mm' } ? 'mm' : 'cc'
|
||
|
||
test_source = File.join(buildfile.dir, self.dir, "test_#{self.name}.#{ext}")
|
||
test_executable = File.join(buildfile.dir, self.dir, "test_#{self.name}")
|
||
test_passed = File.join(buildfile.dir, self.dir, "test_#{self.name}.passed")
|
||
test_ignore_passed = File.join(buildfile.dir, self.dir, "test_#{self.name}.ignore")
|
||
|
||
targets = [ self.name, *link_dependencies(all_targets) ].map { |name| all_targets[name] }
|
||
test_objects = targets.inject(Array.new) { |arr, target| arr += target.objects(buildfile, all_targets) }
|
||
test_objects += objects_for_sources(buildfile, all_targets, [ test_source ], include_dirs: [ self.include_directory(buildfile) ])
|
||
|
||
buildfile.build('gen_test', cxx_test_sources, test_source)
|
||
buildfile.build('link', test_objects, test_executable, link_flags(all_targets))
|
||
buildfile.build('run_test', test_executable, test_passed)
|
||
buildfile.build('always_run_test', test_executable, test_ignore_passed)
|
||
|
||
buildfile.meta("#{self.name}/test", test_ignore_passed)
|
||
@test_results << test_passed
|
||
end
|
||
|
||
objc_test_sources = @context.paths_for('TEST_SOURCES')
|
||
unless objc_test_sources.empty?
|
||
ext = objc_test_sources.any? { |file| File.extname(file) == '.mm' } ? 'mm' : 'cc'
|
||
|
||
test_source = File.join(buildfile.dir, self.dir, "cxx_test_#{self.name}.#{ext}")
|
||
test_executable = File.join(buildfile.dir, self.dir, "cxx_test_#{self.name}")
|
||
test_passed = File.join(buildfile.dir, self.dir, "cxx_test_#{self.name}.passed")
|
||
test_ignore_passed = File.join(buildfile.dir, self.dir, "cxx_test_#{self.name}.ignore")
|
||
|
||
targets = [ self.name, *link_dependencies(all_targets) ].map { |name| all_targets[name] }
|
||
test_objects = targets.inject(Array.new) { |arr, target| arr += target.objects(buildfile, all_targets) }
|
||
test_objects += objects_for_sources(buildfile, all_targets, [ test_source ], include_dirs: [ "#{BIN}/CxxTest", self.include_directory(buildfile) ])
|
||
|
||
buildfile.build('gen_cxx_test', objc_test_sources, test_source)
|
||
buildfile.build('link', test_objects, test_executable, link_flags(all_targets))
|
||
buildfile.build('run_test', test_executable, test_passed)
|
||
buildfile.build('always_run_test', test_executable, test_ignore_passed)
|
||
|
||
buildfile.meta("#{self.name}/cxx_test", test_ignore_passed)
|
||
@test_results << test_passed
|
||
end
|
||
end
|
||
@test_results
|
||
end
|
||
end
|
||
|
||
class Buildfile
|
||
attr_reader :dir
|
||
|
||
def initialize(context, dir, variables = {})
|
||
@context, @dir, @variables = context, dir, variables
|
||
@rules, @build, @default_targets = [], [], []
|
||
end
|
||
|
||
def add_basic_rules
|
||
self.rule('copy', {
|
||
'command' => '/bin/cp -Xp $in $out && touch $out',
|
||
'description' => 'Copy ‘$in’…',
|
||
})
|
||
|
||
self.rule('gen_test', {
|
||
'command' => "#{BIN}/gen_test > $out~ $in && mv $out~ $out",
|
||
'description' => 'Generate test ‘$out’…',
|
||
})
|
||
|
||
self.rule('gen_cxx_test', {
|
||
'command' => "#{BIN}/CxxTest/bin/cxxtestgen --have-std -o $out --runner=unix $in",
|
||
'description' => 'Generate test ‘$out’…',
|
||
})
|
||
|
||
self.rule('run_test', {
|
||
'command' => '$in $test_runner_flags && touch $out',
|
||
'description' => 'Run ‘$in’…',
|
||
})
|
||
|
||
self.rule('always_run_test', {
|
||
'command' => '$in $test_runner_flags',
|
||
'description' => 'Run ‘$in’…',
|
||
})
|
||
|
||
self.rule('link', {
|
||
'command' => 'xcrun clang++ $args -o $out $in',
|
||
'description' => 'Link executable ‘$out’…',
|
||
})
|
||
|
||
self.rule('sign', {
|
||
'command' => 'xcrun codesign $args -fs "$identity" $in && touch $out',
|
||
'description' => 'Sign ‘$in’…',
|
||
})
|
||
|
||
self.rule('index_help', {
|
||
'command' => '/usr/bin/hiutil -Cvaf $out "$HELP_BOOK"',
|
||
'description' => 'Index help book ‘$HELP_BOOK’…',
|
||
})
|
||
|
||
self.rule('run', {
|
||
'command' => '$in',
|
||
'description' => 'Run ‘$in’…',
|
||
'pool' => 'console',
|
||
})
|
||
|
||
self.rule('run_application', {
|
||
'command' => <<~SHELL.gsub(/\s+/, ' '),
|
||
{
|
||
app_name=$$(basename $in .app);
|
||
if pgrep "$$app_name"; then
|
||
if [[ -x "$$DIALOG" && $$("$$DIALOG" alert --title "Relaunch $$app_name?" --body "Would you like to quit $$app_name and start the newly built version?" --button1 Relaunch --button2 Cancel|pl) != *"buttonClicked = 0"* ]];
|
||
then exit;
|
||
fi;
|
||
pkill "$$app_name";
|
||
while pgrep "$$app_name"; do
|
||
if (( ++n == 10 )); then
|
||
test -x "$$DIALOG" && "$$DIALOG" alert --title "Relaunch Timed Out" --body "Unable to exit $$app_name." --button1 OK;
|
||
exit;
|
||
fi;
|
||
sleep .2;
|
||
done;
|
||
fi;
|
||
open $in --args -disableSessionRestore NO;
|
||
} </dev/null &>/dev/null &
|
||
SHELL
|
||
'description' => 'Run ‘$in’…',
|
||
})
|
||
end
|
||
|
||
def default(*names)
|
||
@default_targets += names
|
||
end
|
||
|
||
def rule(name, values)
|
||
@rules << [ name, values ]
|
||
end
|
||
|
||
def build(rule, src, dst, args = nil, deps = nil, outs = nil, order_only = nil)
|
||
@build << [ rule, src, dst, args, deps, outs, order_only ]
|
||
dst
|
||
end
|
||
|
||
def meta(name, *deps)
|
||
build('phony', deps, name)
|
||
end
|
||
|
||
def transform(initial_asset)
|
||
derived_assets = [ ]
|
||
|
||
queue = [ initial_asset ]
|
||
while asset = queue.shift
|
||
if compiler = compiler_for(asset.file)
|
||
if compiler.filter?
|
||
derived_assets += compiler.transform(asset, self)
|
||
else
|
||
queue += compiler.transform(asset, self)
|
||
end
|
||
else
|
||
derived_assets << asset
|
||
end
|
||
end
|
||
|
||
derived_assets
|
||
end
|
||
|
||
def assign(key, value)
|
||
@variables[key] = value
|
||
end
|
||
|
||
def to_s
|
||
variables = @context ? @variables.merge(@context.to_h) : @variables
|
||
|
||
width = variables.map { |key, value| key.length }.max { |lhs, rhs| lhs <=> rhs }
|
||
res = variables.map { |key, value| format("%-#{width}s = %s\n", key, value) }.join
|
||
res << "\n" unless variables.empty?
|
||
|
||
@rules.map do |name, values|
|
||
flags = values.map { |k, v| " #{k} = #{v}" }.join("\n")
|
||
res << "rule #{name}\n#{flags}\n\n"
|
||
end
|
||
|
||
esc = lambda { |x| Array(x).map { |file| file.gsub(/[ :$]/, '$\&') }.join(' ') }
|
||
@build.each do |rule, src, dst, args, deps, outs, order_only|
|
||
res << "build #{esc.call(dst)}"
|
||
res << " | #{esc.call(outs)}" unless outs.to_a.empty?
|
||
res << ": #{rule} #{esc.call(src)}"
|
||
res << " | #{esc.call(deps)}" unless deps.to_s.empty?
|
||
res << " || #{esc.call(order_only)}" unless order_only.to_a.empty?
|
||
res << "\n"
|
||
args = Array(args).join("\n").gsub(/\s+/, ' ').strip
|
||
res << " args = #{args}\n" unless args.empty?
|
||
res << "\n"
|
||
end
|
||
|
||
unless @default_targets.empty?
|
||
res << "default #{@default_targets.join(' ')}\n\n"
|
||
end
|
||
|
||
res
|
||
end
|
||
|
||
private
|
||
|
||
def compiler_for(file)
|
||
Plugin.plugins_of_type(Compiler).each do |klass|
|
||
(klass.transforms || {}).each do |ext, dest_ext|
|
||
if file =~ /\b#{Regexp.escape ext}$/
|
||
return klass.new(self, @context, ext, ext, dest_ext)
|
||
end
|
||
end
|
||
|
||
(klass.extensions || {}).each do |canonical_ext, extensions|
|
||
extensions.each do |ext|
|
||
if file =~ /\b#{Regexp.escape ext}$/
|
||
return klass.new(self, @context, ext, canonical_ext, klass.transforms[canonical_ext])
|
||
end
|
||
end
|
||
end
|
||
end
|
||
nil
|
||
end
|
||
end
|
||
|
||
class CleanupTargets
|
||
def initialize(ninja_file, builddir)
|
||
@ninja_file, @builddir = ninja_file, builddir
|
||
@old_targets = self.targets(ninja_file, builddir)
|
||
end
|
||
|
||
def targets(ninja_file, builddir)
|
||
return nil if ninja_file == '/dev/stdout' || !File.exists?(ninja_file)
|
||
|
||
targets = %x{ ${TM_NINJA:-ninja} -C #{File.dirname(ninja_file).shellescape} -f #{ninja_file.shellescape} -t targets all }
|
||
return nil if $? != 0
|
||
|
||
res = Set.new
|
||
targets.each_line do |line|
|
||
res << $& if line =~ /^#{Regexp.escape(builddir)}.*(?=:(?! (phony|(tbz|zip)_archive)))/
|
||
end
|
||
res
|
||
end
|
||
|
||
def run(dry_run: false)
|
||
new_targets = self.targets(@ninja_file, @builddir)
|
||
|
||
if @old_targets && new_targets
|
||
targets_lost = @old_targets - new_targets
|
||
targets_lost.each do |path|
|
||
if File.exists?(path)
|
||
STDERR << "Remove old target ‘#{path.sub(/#{Regexp.escape(@builddir)}/, '$builddir')}’…\n"
|
||
# FIXME Does not seem to work with symbolic links (Proxy.png → Settings.png)
|
||
File.unlink(path) unless dry_run == true
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
class Plugin
|
||
@@plugins = []
|
||
|
||
def self.inherited(subclass)
|
||
@@plugins << subclass
|
||
end
|
||
|
||
def self.plugins_of_type(klass)
|
||
@@plugins.select { |candidate| candidate < klass }
|
||
end
|
||
end
|
||
|
||
class Compiler < Plugin
|
||
class << self
|
||
def transforms(hash = nil)
|
||
@transforms = hash || @transforms
|
||
end
|
||
|
||
def extensions(hash = nil)
|
||
@extensions = hash || @extensions
|
||
end
|
||
end
|
||
|
||
@@did_setup = []
|
||
|
||
def initialize(buildfile, context, ext, canonical_ext, dest_ext)
|
||
@ext, @canonical_ext, @dest_ext = ext, canonical_ext, dest_ext
|
||
unless @@did_setup.include?(self.class)
|
||
@@did_setup << self.class
|
||
self.setup(buildfile, context)
|
||
end
|
||
end
|
||
|
||
def filter?
|
||
@canonical_ext == @dest_ext
|
||
end
|
||
|
||
def derive_asset(asset, dest, options = nil)
|
||
Asset.new(File.expand_path(asset.file.chomp(@ext) + @dest_ext, dest), options ? asset.context.derive(options) : asset.context)
|
||
end
|
||
end
|
||
|
||
class CompileRagel < Compiler
|
||
transforms '.rl' => '.cc'
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.rule('gen_ragel', {
|
||
'command' => 'ragel -o $out $in',
|
||
'description' => 'Generate source from ‘$in’…',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
config = { 'FLAGS' => Shellwords.shelljoin([ '-iquote', File.dirname(asset.file) ]) }
|
||
new_asset = self.derive_asset(asset, buildfile.dir, config)
|
||
buildfile.build('gen_ragel', asset.file, new_asset.file)
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
class CompileCapnp < Compiler
|
||
transforms '.capnp' => '.capnp.c++'
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.rule('gen_capnp', {
|
||
'command' => 'PATH="$capnp_prefix/bin:$$PATH" capnp compile -oc++ $in',
|
||
'description' => 'Generate source from ‘$in’…',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
new_asset_source = Asset.new(asset.file.chomp(@ext) + '.capnp.c++', asset.context)
|
||
new_asset_header = Asset.new(asset.file.chomp(@ext) + '.capnp.h', asset.context)
|
||
buildfile.build('gen_capnp', asset.file, [ new_asset_source.file, new_asset_header.file ])
|
||
[ new_asset_source ]
|
||
end
|
||
end
|
||
|
||
class CompileClang < Compiler
|
||
transforms '.c' => '.o', '.m' => '.o', '.cc' => '.o', '.mm' => '.o'
|
||
extensions '.cc' => [ '.c++', '.cxx', '.cpp' ]
|
||
|
||
COMPILER_INFO = {
|
||
'.c' => { :rule => 'build_c', :compiler => 'xcrun clang', :flags => 'C_FLAGS', :pch => '-x c-header' },
|
||
'.m' => { :rule => 'build_m', :compiler => 'xcrun clang', :flags => 'OBJC_FLAGS', :pch => '-x objective-c-header' },
|
||
'.cc' => { :rule => 'build_cc', :compiler => 'xcrun clang++', :flags => 'CXX_FLAGS', :pch => '-x c++-header' },
|
||
'.mm' => { :rule => 'build_mm', :compiler => 'xcrun clang++', :flags => 'OBJCXX_FLAGS', :pch => '-x objective-c++-header' },
|
||
}
|
||
|
||
@@prelude = { }
|
||
|
||
def setup(buildfile, context)
|
||
COMPILER_INFO.each do |_, info|
|
||
buildfile.rule(info[:rule], {
|
||
'command' => "#{info[:compiler]} $args -o $out -MMD -MF $out.d $in",
|
||
'depfile' => "$out.d",
|
||
'deps' => "gcc",
|
||
'description' => "Compile ‘$in’…",
|
||
})
|
||
end
|
||
|
||
context.paths_for('PRELUDE').each do |file|
|
||
ext = File.extname(file)
|
||
if info = COMPILER_INFO[ext]
|
||
flags = [ info[:pch] ]
|
||
flags << context[info[:flags]]
|
||
flags << context['FLAGS']
|
||
|
||
outfile = "#{File.expand_path(file, buildfile.dir)}.gch"
|
||
buildfile.build(info[:rule], file, outfile, flags)
|
||
@@prelude[ext] = { :outfile => outfile, :include => outfile.chomp('.gch') }
|
||
end
|
||
end
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
info = COMPILER_INFO[@canonical_ext]
|
||
|
||
flags = [ "-include #{@@prelude[@canonical_ext][:include]}" ]
|
||
deps = [ @@prelude[@canonical_ext][:outfile] ]
|
||
|
||
order_only = nil
|
||
|
||
if dirs = asset.context.array_for('include_dirs')
|
||
flags << dirs.map { |dir| "-I#{dir}" }.shelljoin
|
||
order_only = dirs
|
||
end
|
||
|
||
flags << asset.context[info[:flags]]
|
||
flags << asset.context['FLAGS']
|
||
|
||
new_asset = self.derive_asset(asset, buildfile.dir)
|
||
buildfile.build(info[:rule], asset.file, new_asset.file, flags, deps, nil, order_only)
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
class CompileXib < Compiler
|
||
transforms '.xib' => '.nib'
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.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’…',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
new_asset = self.derive_asset(asset, buildfile.dir)
|
||
buildfile.build('compile_xib', asset.file, new_asset.file)
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
class CompileXCAssets < Compiler
|
||
transforms '.xcassets' => '.car'
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.rule('xcassets', {
|
||
'command' => 'xcrun actool --errors --warnings --notices --output-format human-readable-text --platform macosx --minimum-deployment-target $APP_MIN_OS --compile "$$(dirname $out)" $in',
|
||
'description' => 'Compile xcassets ‘$in’…',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
deps = Dir.glob("#{asset.file}/**/*")
|
||
|
||
new_asset = Asset.new(File.join(buildfile.dir, File.dirname(asset.file), 'Assets.car'))
|
||
buildfile.build('xcassets', asset.file, new_asset.file, nil, deps)
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
class CompileStrings < Compiler
|
||
transforms '.strings' => '.strings'
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.rule('copy_as_utf16', {
|
||
'command' => 'if [[ $$(head -c2 $in) == $$\'\\xFF\\xFE\' || $$(head -c2 $in) == $$\'\\xFE\\xFF\' ]]; then /bin/cp -XRp $in $out; else iconv -f utf-8 -t utf-16 < $in > $out~ && mv $out~ $out; fi',
|
||
'description' => 'Copy ‘$in’ as UTF-16…',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
new_asset = self.derive_asset(asset, buildfile.dir)
|
||
buildfile.build('copy_as_utf16', asset.file, new_asset.file)
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
class CompilePropertyList < Compiler
|
||
transforms 'Info.plist' => 'Info.plist'
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.rule('process_plist', {
|
||
'command' => "#{BIN}/process_plist > $out~ $args $in && mv $out~ $out",
|
||
'description' => 'Process plist ‘$in’…',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
flags = [ "-dTARGET_NAME='#{asset.context['TARGET_NAME']}'" ]
|
||
flags << asset.context['PLIST_FLAGS']
|
||
|
||
new_asset = self.derive_asset(asset, buildfile.dir)
|
||
buildfile.build('process_plist', asset.file, new_asset.file, flags, [ "#{BIN}/process_plist" ])
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
class CompileMarkdown < Compiler
|
||
transforms '.md' => '.html'
|
||
extensions '.md' => [ '.mdown' ]
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.rule('markdown', {
|
||
'command' => "#{BIN}/gen_html > $out~ $args $in && mv $out~ $out",
|
||
'description' => 'Generate ‘$out’…',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
header = asset.context.path_for('HTML_HEADER')
|
||
footer = asset.context.path_for('HTML_FOOTER')
|
||
|
||
deps = [ "#{BIN}/gen_html" ]
|
||
deps << header unless header.nil?
|
||
deps << footer unless footer.nil?
|
||
|
||
flags = []
|
||
flags << "-h #{header.shellescape}" unless header.nil?
|
||
flags << "-f #{footer.shellescape}" unless footer.nil?
|
||
|
||
md_header = asset.context.path_for('MARKDOWN_HEADER')
|
||
md_footer = asset.context.path_for('MARKDOWN_FOOTER')
|
||
infiles = [ md_header, asset.file, md_footer ].reject { |file| file.nil? }
|
||
|
||
new_asset = self.derive_asset(asset, buildfile.dir)
|
||
buildfile.build('markdown', infiles, new_asset.file, flags, deps)
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
# ========
|
||
# = Main =
|
||
# ========
|
||
|
||
if __FILE__ == $PROGRAM_NAME
|
||
outfile = '/dev/stdout'
|
||
builddir = 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?
|
||
ravefile = ARGV.shift
|
||
|
||
Dir.chdir(File.dirname(ravefile)) do
|
||
# ================
|
||
# = Load Targets =
|
||
# ================
|
||
|
||
config = Context.new(Target.read(ravefile))
|
||
files = []
|
||
|
||
all_targets = config.paths_for('TARGETS').map do |file|
|
||
dir, options = File.dirname(file), Target.read(file)
|
||
name, target = (options['TARGET_NAME'] ||= File.basename(dir)), Target.new(dir, config.derive(options, dir))
|
||
files << [ file, target ]
|
||
[ name, target ]
|
||
end.to_h
|
||
|
||
# ====================================
|
||
# = Check Existence of Named Targets =
|
||
# ====================================
|
||
|
||
files.each do |file, target|
|
||
target.context.array_for('LINK').each do |name|
|
||
unless all_targets.key?(name =~ /^@(.*)/ ? $1 : name)
|
||
abort "#{file}: no such target: #{name} (required by LINK)"
|
||
end
|
||
end
|
||
|
||
target.context.keys.select { |key| key =~ /^CP_/ }.each do |key|
|
||
target.context.array_for(key).each do |name|
|
||
if name =~ /^@(.*)/ && !all_targets.key?($1)
|
||
abort "#{file}: no such target: #{name} (required by #{key})"
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
# =================================
|
||
# = Topological Sort Root Targets =
|
||
# =================================
|
||
|
||
link_targets = Set.new(all_targets.flat_map { |_, target| target.context.array_for('LINK') })
|
||
root_targets = all_targets.reject { |name, _| link_targets.include?(name) }
|
||
|
||
nodes = root_targets.map do |name, target|
|
||
keys = target.context.keys.select { |key| key =~ /^CP_/ }
|
||
deps = keys.flat_map { |key| target.context.target_names_for(key) }
|
||
[ name, deps ]
|
||
end.to_h
|
||
|
||
edge_count = nodes.map { |name, _| [ name, 0 ] }.to_h
|
||
nodes.values.flatten.each { |target| edge_count[target] += 1 }
|
||
|
||
ordered = [ ]
|
||
|
||
until edge_count.empty?
|
||
roots = edge_count.select { |_, deps| deps.zero? }
|
||
abort "*** Fatal: Dependency cycle" if roots.empty?
|
||
|
||
roots.each do |root, _|
|
||
ordered << root
|
||
edge_count.delete(root)
|
||
nodes[root].each { |target| edge_count[target] -= 1 }
|
||
end
|
||
end
|
||
|
||
# =====================
|
||
# = Create build file =
|
||
# =====================
|
||
|
||
buildfile = Buildfile.new(config, builddir)
|
||
buildfile.add_basic_rules
|
||
|
||
ordered.reverse.each do |name|
|
||
root_targets[name].setup_build(buildfile, all_targets)
|
||
end
|
||
|
||
# =====================================
|
||
# = Calculate build file dependencies =
|
||
# =====================================
|
||
|
||
dependencies = all_targets.flat_map do |name, target|
|
||
target.context.keys.select { |key| key =~ /^(SOURCES|TESTS|TEST_SOURCES|CP_.*)$/ }.flat_map do |key|
|
||
all_targets[name].context.paths_for(key).map { |file| File.dirname(file) }
|
||
end
|
||
end.uniq
|
||
dependencies += [ '.', ravefile, *config.paths_for('TARGETS') ]
|
||
|
||
# ============================
|
||
# = Write build file to disk =
|
||
# ============================
|
||
|
||
cleaner = CleanupTargets.new(outfile, builddir)
|
||
|
||
FileUtils.mkdir_p(builddir)
|
||
File.write("#{builddir}/build.ninja", buildfile.to_s)
|
||
File.write("#{builddir}/build.ninja.d", "build.ninja: #{dependencies.sort.map { |dep| dep.gsub(/ /, '\\ ') }.join(" \\\n ")}\n")
|
||
|
||
bootstrap = Buildfile.new(nil, builddir, variables)
|
||
bootstrap.assign('ninja_required_version', '1.5')
|
||
bootstrap.assign('builddir', builddir)
|
||
|
||
strip_variables = Set.new(config.keys) << 'ninja_required_version' << 'builddir'
|
||
args = variables.reject { |key, value| strip_variables.include?(key) }
|
||
args = args.map { |key, value| "-d'#{key}=#{value =~ /\$/ ? value.gsub(/\$/, '$$') : "$#{key}"}'" }.join(' ')
|
||
|
||
bootstrap.rule('gen_build', {
|
||
'command' => "#{__FILE__} -C \"$builddir\" #{args} -o $out $in",
|
||
'depfile' => '$builddir/$out.d',
|
||
'generator' => 'true',
|
||
'description' => 'Generate ‘$out’…',
|
||
})
|
||
|
||
bootstrap.build('gen_build', 'target', 'build.ninja', nil, __FILE__)
|
||
|
||
open("#{outfile}", "w") do |io|
|
||
userfile = "#{ENV['USER']}.ninja"
|
||
io << bootstrap.to_s
|
||
io << "include $builddir/build.ninja\n"
|
||
io << "include #{userfile}\n" if userfile != 'build.ninja' && File.exists?(File.join(File.dirname(outfile), userfile))
|
||
end
|
||
|
||
cleaner.run(dry_run: false)
|
||
end
|
||
end
|