Files
textmate/bin/gen_build
Allan Odgaard 504a42307e Passing ‘-deep’ to code sign should no longer be necessary
This is because we ensure that each target copied to another target, gets signed before we copy it.

We initially used ‘-deep’ but that actually never worked fully, as it didn’t find all executables in our bundle, presumably only embedded bundles like frameworks and plug-ins were found and signed.
2019-11-01 08:46:20 +01:00

956 lines
30 KiB
Ruby
Executable File
Raw Permalink 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/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 '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 bundles 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…',
})
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]}" ]
flags << asset.context[info[:flags]]
flags << asset.context['FLAGS']
order_only = nil
deps = [ @@prelude[@canonical_ext][:outfile] ]
if dirs = asset.context.array_for('include_dirs')
flags << dirs.map { |dir| "-I#{dir}" }.shelljoin
order_only = dirs
end
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)
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