require 'fileutils' class Dir class << self alias_method :orig_bracket, :[] end def self.[](*args) orig_bracket(*args).sort end end describe Rscons do BUILD_TEST_RUN_DIR = "build_test_run" def rm_rf(dir) FileUtils.rm_rf(dir) if File.exists?(dir) sleep 0.2 FileUtils.rm_rf(dir) if File.exists?(dir) sleep 0.5 FileUtils.rm_rf(dir) if File.exists?(dir) sleep 1.0 FileUtils.rm_rf(dir) if File.exists?(dir) raise "Could not remove #{dir}" end end end end end before(:all) do rm_rf(BUILD_TEST_RUN_DIR) @owd = Dir.pwd end before(:each) do @saved_stdout = $stdout $stdout = StringIO.new @saved_stderr = $stderr $stderr = StringIO.new end after(:each) do $stdout = @saved_stdout $stderr = @saved_stderr Dir.chdir(@owd) rm_rf(BUILD_TEST_RUN_DIR) end def test_dir(build_test_directory) FileUtils.cp_r("build_tests/#{build_test_directory}", BUILD_TEST_RUN_DIR) Dir.chdir(BUILD_TEST_RUN_DIR) end def file_sub(fname) contents = File.read(fname) replaced = '' contents.each_line do |line| replaced += yield(line) end File.open(fname, 'wb') do |fh| fh.write(replaced) end end def lines rv = ($stdout.string + $stderr.string).lines.map(&:rstrip) $stdout.string = "" $stderr.string = "" rv end ########################################################################### # Tests ########################################################################### it 'builds a C program with one source file' do test_dir('simple') Rscons::Environment.new do |env| env.Program('simple', Dir['*.c']) end expect(File.exists?('simple.o')).to be_truthy expect(`./simple`).to eq "This is a simple C program\n" end it 'prints commands as they are executed' do test_dir('simple') env = Rscons::Environment.new(echo: :command) do |env| env["LD"] = "gcc" env.Program('simple', Dir['*.c']) end expect(lines).to eq [ 'gcc -c -o simple.o -MMD -MF simple.mf simple.c', "gcc -o simple#{env["PROGSUFFIX"]} simple.o", ] end it 'prints short representations of the commands being executed' do test_dir('header') env = Rscons::Environment.new do |env| env.Program('header', Dir['*.c']) end expect(lines).to eq [ 'CC header.o', "LD header#{env["PROGSUFFIX"]}", ] end it 'builds a C program with one source file and one header file' do test_dir('header') Rscons::Environment.new do |env| env.Program('header', Dir['*.c']) end expect(File.exists?('header.o')).to be_truthy expect(`./header`).to eq "The value is 2\n" end it 'rebuilds a C module when a header it depends on changes' do test_dir('header') make_env = lambda do Rscons::Environment.new do |env| env.Program('header', Dir['*.c']) end end make_env[] expect(`./header`).to eq "The value is 2\n" file_sub('header.h') {|line| line.sub(/2/, '5')} make_env[] expect(`./header`).to eq "The value is 5\n" end it 'does not rebuild a C module when its dependencies have not changed' do test_dir('header') make_env = lambda do Rscons::Environment.new do |env| env.Program('header', Dir['*.c']) end end env = make_env[] expect(`./header`).to eq "The value is 2\n" expect(lines).to eq [ 'CC header.o', "LD header#{env["PROGSUFFIX"]}", ] make_env[] expect(lines).to eq [] end it "does not rebuild a C module when only the file's timestamp has changed" do test_dir('header') make_env = lambda do Rscons::Environment.new do |env| env.Program('header', Dir['*.c']) end end env = make_env[] expect(`./header`).to eq "The value is 2\n" expect(lines).to eq [ 'CC header.o', "LD header#{env["PROGSUFFIX"]}", ] sleep 0.05 file_sub('header.c') {|line| line} make_env[] expect(lines).to eq [] end it 're-links a program when the link flags have changed' do test_dir('simple') env = Rscons::Environment.new(echo: :command) do |env| env.Program('simple', Dir['*.c']) end expect(lines).to eq [ 'gcc -c -o simple.o -MMD -MF simple.mf simple.c', "gcc -o simple#{env["PROGSUFFIX"]} simple.o", ] e2 = Rscons::Environment.new(echo: :command) do |env| env["LIBPATH"] += ["libdir"] env.Program('simple', Dir['*.c']) end expect(lines).to eq ["gcc -o simple#{env["PROGSUFFIX"]} simple.o -Llibdir"] end it 'builds object files in a different build directory' do test_dir('build_dir') Rscons::Environment.new do |env| env.append('CPPPATH' => Dir['src/**/*/']) env.build_dir(%r{^src/([^/]+)/}, 'build_\\1/') env.Program('build_dir', Dir['src/**/*.c']) end expect(`./build_dir`).to eq "Hello from two()\n" expect(File.exists?('build_one/one.o')).to be_truthy expect(File.exists?('build_two/two.o')).to be_truthy end it "supports trailing slashes at the end of build_dir sources and destinations" do test_dir("build_dir") Rscons::Environment.new do |env| env.append("CPPPATH" => Dir["src/**/*/"]) env.build_dir("src/one/", "build_one/") env.build_dir("src/two", "build_two") env.Program("build_dir", Dir["src/**/*.c"]) end expect(`./build_dir`).to eq "Hello from two()\n" expect(File.exists?("build_one/one.o")).to be_truthy expect(File.exists?("build_two/two.o")).to be_truthy end it 'uses build directories before build root' do test_dir('build_dir') env = Rscons::Environment.new do |env| env.append('CPPPATH' => Dir['src/**/*/']) env.build_dir("src", "build") env.build_root = "build_root" env.Program('build_dir', Dir['src/**/*.c']) end expect(lines).to eq ["CC build/one/one.o", "CC build/two/two.o", "LD build_dir#{env["PROGSUFFIX"]}"] end it 'uses build_root if no build directories match' do test_dir('build_dir') Rscons::Environment.new do |env| env.append('CPPPATH' => Dir['src/**/*/']) env.build_dir("src2", "build") env.build_root = "build_root" env.Program('build_dir.exe', Dir['src/**/*.c']) end expect(lines).to eq ["CC build_root/src/one/one.o", "CC build_root/src/two/two.o", "LD build_dir.exe"] end it "expands target and source paths starting with ^/ to be relative to the build root" do test_dir('build_dir') env = Rscons::Environment.new(echo: :command) do |env| env.append('CPPPATH' => Dir['src/**/*/']) env.build_root = "build_root" FileUtils.mkdir_p(env.build_root) FileUtils.mv("src/one/one.c", "build_root") env.Object("^/one.o", "^/one.c") env.Program('build_dir', Dir['src/**/*.c'] + ["^/one.o"]) end expect(lines).to eq [ %q{gcc -c -o build_root/one.o -MMD -MF build_root/one.mf -Isrc/one/ -Isrc/two/ build_root/one.c}, %q{gcc -c -o build_root/src/two/two.o -MMD -MF build_root/src/two/two.mf -Isrc/one/ -Isrc/two/ src/two/two.c}, %Q{gcc -o build_dir#{env["PROGSUFFIX"]} build_root/src/two/two.o build_root/one.o}, ] end it 'cleans built files' do test_dir('build_dir') Rscons::Environment.new do |env| env.append('CPPPATH' => Dir['src/**/*/']) env.build_dir(%r{^src/([^/]+)/}, 'build_\\1/') env.Program('build_dir', Dir['src/**/*.c']) end expect(`./build_dir`).to eq "Hello from two()\n" expect(File.exists?('build_one/one.o')).to be_truthy expect(File.exists?('build_two/two.o')).to be_truthy Rscons.clean expect(File.exists?('build_one/one.o')).to be_falsey expect(File.exists?('build_two/two.o')).to be_falsey expect(File.exists?('build_one')).to be_falsey expect(File.exists?('build_two')).to be_falsey expect(File.exists?('src/one/one.c')).to be_truthy end it 'does not clean created directories if other non-rscons-generated files reside there' do test_dir('build_dir') Rscons::Environment.new do |env| env.append('CPPPATH' => Dir['src/**/*/']) env.build_dir(%r{^src/([^/]+)/}, 'build_\\1/') env.Program('build_dir', Dir['src/**/*.c']) end expect(`./build_dir`).to eq "Hello from two()\n" expect(File.exists?('build_one/one.o')).to be_truthy expect(File.exists?('build_two/two.o')).to be_truthy File.open('build_two/tmp', 'w') { |fh| fh.puts "dum" } Rscons.clean expect(File.exists?('build_one/one.o')).to be_falsey expect(File.exists?('build_two/two.o')).to be_falsey expect(File.exists?('build_one')).to be_falsey expect(File.exists?('build_two')).to be_truthy expect(File.exists?('src/one/one.c')).to be_truthy end it 'allows Ruby classes as custom builders to be used to construct files' do test_dir('custom_builder') class MySource < Rscons::Builder def run(target, sources, cache, env, vars) File.open(target, 'w') do |fh| fh.puts < ['-DONE']) env.Program('two_sources', ['one.o', 'two.c']) end expect(lines).to eq [ 'gcc -c -o one.o -MMD -MF one.mf -DONE one.c', 'gcc -c -o two.o -MMD -MF two.mf two.c', "gcc -o two_sources#{env["PROGSUFFIX"]} one.o two.o", ] expect(File.exists?("two_sources#{env["PROGSUFFIX"]}")).to be_truthy expect(`./two_sources`).to eq "This is a C program with two sources.\n" end it 'builds a static library archive' do test_dir('library') env = Rscons::Environment.new(echo: :command) do |env| env.Program('library', ['lib.a', 'three.c']) env.Library("lib.a", ['one.c', 'two.c'], 'CPPFLAGS' => ['-Dmake_lib']) end expect(lines).to eq [ 'gcc -c -o one.o -MMD -MF one.mf -Dmake_lib one.c', 'gcc -c -o two.o -MMD -MF two.mf -Dmake_lib two.c', 'ar rcs lib.a one.o two.o', 'gcc -c -o three.o -MMD -MF three.mf three.c', "gcc -o library#{env["PROGSUFFIX"]} lib.a three.o", ] expect(File.exists?("library#{env["PROGSUFFIX"]}")).to be_truthy expect(`ar t lib.a`).to eq "one.o\ntwo.o\n" end it 'supports build hooks to override construction variables' do test_dir("build_dir") env = Rscons::Environment.new(echo: :command) do |env| env.append('CPPPATH' => Dir['src/**/*/']) env.build_dir(%r{^src/([^/]+)/}, 'build_\\1/') env.add_build_hook do |build_op| if build_op[:target] =~ %r{build_one/.*\.o} build_op[:vars]["CFLAGS"] << "-O1" elsif build_op[:target] =~ %r{build_two/.*\.o} build_op[:vars]["CFLAGS"] << "-O2" end end env.Program('build_hook.exe', Dir['src/**/*.c']) end expect(`./build_hook.exe`).to eq "Hello from two()\n" expect(lines).to match_array [ 'gcc -c -o build_one/one.o -MMD -MF build_one/one.mf -Isrc/one/ -Isrc/two/ -O1 src/one/one.c', 'gcc -c -o build_two/two.o -MMD -MF build_two/two.mf -Isrc/one/ -Isrc/two/ -O2 src/two/two.c', 'gcc -o build_hook.exe build_one/one.o build_two/two.o', ] end it 'rebuilds when user-specified dependencies change' do test_dir('simple') env = Rscons::Environment.new do |env| env.Program('simple.exe', Dir['*.c']).depends("file.ld") File.open("file.ld", "w") do |fh| fh.puts("foo") end end expect(lines).to eq ["CC simple.o", "LD simple.exe"] expect(File.exists?('simple.o')).to be_truthy expect(`./simple.exe`).to eq "This is a simple C program\n" e2 = Rscons::Environment.new do |env| program = env.Program('simple.exe', Dir['*.c']) env.depends(program, "file.ld") File.open("file.ld", "w") do |fh| fh.puts("bar") end end expect(lines).to eq ["LD simple.exe"] e3 = Rscons::Environment.new do |env| env.Program('simple.exe', Dir['*.c']) File.unlink("file.ld") end expect(lines).to eq ["LD simple.exe"] Rscons::Environment.new do |env| env.Program('simple.exe', Dir['*.c']) end expect(lines).to eq [] end unless ENV["omit_gdc_tests"] it "supports building D sources" do test_dir("d") Rscons::Environment.new(echo: :command) do |env| env.Program("hello-d.exe", Dir["*.d"]) end expect(lines).to eq [ "gdc -c -o main.o main.d", "gdc -o hello-d.exe main.o", ] expect(`./hello-d.exe`.rstrip).to eq "Hello from D!" end end it "supports disassembling object files" do test_dir("simple") Rscons::Environment.new do |env| env.Object("simple.o", "simple.c") env.Disassemble("simple.txt", "simple.o") end expect(File.exists?("simple.txt")).to be_truthy expect(File.read("simple.txt")).to match /Disassembly of section .text:/ end it "supports preprocessing C sources" do test_dir("simple") Rscons::Environment.new do |env| env.Preprocess("simplepp.c", "simple.c") env.Program("simple", "simplepp.c") end expect(File.read("simplepp.c")).to match /# \d+ "simple.c"/ expect(`./simple`).to eq "This is a simple C program\n" end it "supports preprocessing C++ sources" do test_dir("simple_cc") Rscons::Environment.new do |env| env.Preprocess("simplepp.cc", "simple.cc") env.Program("simple", "simplepp.cc") end expect(File.read("simplepp.cc")).to match /# \d+ "simple.cc"/ expect(`./simple`).to eq "This is a simple C++ program\n" end it "supports invoking builders with no sources and a build_root defined" do class TestBuilder < Rscons::Builder def run(target, sources, cache, env, vars) target end end test_dir("simple") Rscons::Environment.new do |env| env.build_root = "build" env.add_builder(TestBuilder.new) env.TestBuilder("file") end end it "expands construction variables in builder target and sources before invoking the builder" do test_dir('custom_builder') class MySource < Rscons::Builder def run(target, sources, cache, env, vars) File.open(target, 'w') do |fh| fh.puts < ["-DONE"]) env.Object("two.ooo", "two.c") env.Program("two_sources", %w[one.oooo two.ooo]) end expect(File.exists?("two_sources#{env["PROGSUFFIX"]}")).to be_truthy expect(`./two_sources`).to eq "This is a C program with two sources.\n" end it "supports multiple values for LIBSUFFIX" do test_dir("two_sources") env = Rscons::Environment.new() do |env| env["LIBSUFFIX"] = %w[.aaaa .aaa] env.Library("one.aaaa", "one.c", "CPPFLAGS" => ["-DONE"]) env.Library("two.aaa", "two.c") env.Program("two_sources", %w[one.aaaa two.aaa]) end expect(File.exists?("two_sources#{env["PROGSUFFIX"]}")).to be_truthy expect(`./two_sources`).to eq "This is a C program with two sources.\n" end it "supports multiple values for ASSUFFIX" do test_dir("two_sources") env = Rscons::Environment.new() do |env| env["ASSUFFIX"] = %w[.ssss .sss] env["CFLAGS"] += %w[-S] env.Object("one.ssss", "one.c", "CPPFLAGS" => ["-DONE"]) env.Object("two.sss", "two.c") env.Program("two_sources", %w[one.ssss two.sss], "ASFLAGS" => env["ASFLAGS"] + %w[-x assembler]) end expect(lines).to eq([ "CC one.ssss", "CC two.sss", "AS one.o", "AS two.o", "LD two_sources#{env["PROGSUFFIX"]}", ]) expect(File.exists?("two_sources#{env["PROGSUFFIX"]}")).to be_truthy expect(`./two_sources`).to eq "This is a C program with two sources.\n" end end