diff --git a/build_tests/typical/install.rb b/build_tests/typical/install.rb index 4077c9b..4cc3ad0 100644 --- a/build_tests/typical/install.rb +++ b/build_tests/typical/install.rb @@ -1,5 +1,14 @@ +project_name "install_test" + build do Environment.new do |env| - env.Install("inst.exe", "install.rb") + env["CPPPATH"] += glob("src/**") + env.Program("^/program.exe", glob("src/**/*.c")) + env.InstallDirectory("${prefix}/bin") + env.Install("${prefix}/bin", "^/program.exe") + env.InstallDirectory("${prefix}/share") + env.Install("${prefix}/share/proj/install.rb", "install.rb") + env.Install("${prefix}/mult", ["install.rb", "copy.rb"]) + env.Install("${prefix}/src", "src") end end diff --git a/lib/rscons.rb b/lib/rscons.rb index 65fab52..100c80c 100644 --- a/lib/rscons.rb +++ b/lib/rscons.rb @@ -26,6 +26,7 @@ module Rscons :Directory, :Disassemble, :Install, + :InstallDirectory, :Library, :Object, :Preprocess, @@ -145,9 +146,9 @@ require_relative "rscons/builders/mixins/program" # default builders require_relative "rscons/builders/cfile" require_relative "rscons/builders/command" +require_relative "rscons/builders/copy" require_relative "rscons/builders/directory" require_relative "rscons/builders/disassemble" -require_relative "rscons/builders/install" require_relative "rscons/builders/library" require_relative "rscons/builders/object" require_relative "rscons/builders/preprocess" diff --git a/lib/rscons/application.rb b/lib/rscons/application.rb index e2db4ed..c325b04 100644 --- a/lib/rscons/application.rb +++ b/lib/rscons/application.rb @@ -1,3 +1,5 @@ +require "set" + module Rscons # Functionality for an instance of the rscons application invocation. @@ -23,6 +25,18 @@ module Rscons def initialize @n_threads = Util.determine_n_threads @vars = VarSet.new + @operations = Set.new + end + + # Check whether a requested operation is active. + # + # @param op [String] + # Operation name. + # + # @return [Boolean] + # Whether the requested operation is active. + def operation(op) + @operations.include?(op) end # Run the specified operation. @@ -37,6 +51,7 @@ module Rscons # @return [Integer] # Process exit code (0 on success). def run(operation, script, operation_options) + @operations << operation @script = script case operation when "build" @@ -58,6 +73,10 @@ module Rscons configure(operation_options) when "distclean" distclean + when "install" + run("build", script, operation_options) + when "uninstall" + uninstall else $stderr.puts "Unknown operation: #{operation}" 1 @@ -129,34 +148,37 @@ module Rscons # @return [Integer] # Exit code. def configure(options) - # Default options. - options[:build_dir] ||= "build" - options[:prefix] ||= "/usr/local" - cache = Cache.instance - cache["failed_commands"] = [] - cache["configuration_data"] = {} - if project_name = @script.project_name - Ansi.write($stdout, "Configuring ", :cyan, project_name, :reset, "...\n") - else - $stdout.puts "Configuring project..." - end - Ansi.write($stdout, "Setting build directory... ", :green, options[:build_dir], :reset, "\n") - Ansi.write($stdout, "Setting prefix... ", :green, options[:prefix], :reset, "\n") rv = 0 - co = ConfigureOp.new("#{options[:build_dir]}/configure") + options = options.merge(project_name: @script.project_name) + co = ConfigureOp.new(options) begin @script.configure(co) rescue ConfigureOp::ConfigureFailure rv = 1 end - co.close - cache["configuration_data"]["build_dir"] = options[:build_dir] - cache["configuration_data"]["prefix"] = options[:prefix] - cache["configuration_data"]["configured"] = rv == 0 - cache.write + co.close(rv == 0) rv end + # Remove installed files. + # + # @return [Integer] + # Exit code. + def uninstall + cache = Cache.instance + cache.targets(true).each do |target| + FileUtils.rm_f(target) + end + # remove all created directories if they are empty + cache.directories(true).sort {|a, b| b.size <=> a.size}.each do |directory| + next unless File.directory?(directory) + if (Dir.entries(directory) - ['.', '..']).empty? + Dir.rmdir(directory) rescue nil + end + end + 0 + end + end end diff --git a/lib/rscons/builders/copy.rb b/lib/rscons/builders/copy.rb new file mode 100644 index 0000000..a9edd1b --- /dev/null +++ b/lib/rscons/builders/copy.rb @@ -0,0 +1,60 @@ +require "pathname" + +module Rscons + module Builders + # The Copy builder copies files/directories to new locations. + class Copy < Builder + + # Run the builder to produce a build target. + def run(options) + install_builder = self.class.name == "Install" + if (not install_builder) or Rscons.application.operation("install") + target_is_dir = (@sources.length > 1) || + Dir.exists?(@sources.first) || + Dir.exists?(@target) + outdir = target_is_dir ? @target : File.dirname(@target) + # Collect the list of files to copy over. + file_map = {} + if target_is_dir + @sources.each do |src| + if Dir.exists? src + Dir.glob("#{src}/**/*", File::FNM_DOTMATCH).select do |f| + File.file?(f) + end.each do |subfile| + subpath = Pathname.new(subfile).relative_path_from(Pathname.new(src)).to_s + file_map[subfile] = "#{outdir}/#{subpath}" + end + else + file_map[src] = "#{outdir}/#{File.basename(src)}" + end + end + else + file_map[sources.first] = target + end + printed_message = false + file_map.each do |src, dest| + # Check the cache and copy if necessary + unless @cache.up_to_date?(dest, :Copy, [src], @env) + unless printed_message + message = "#{name} #{Util.short_format_paths(@sources)} => #{@target}" + print_run_message(message, nil) + printed_message = true + end + @cache.mkdir_p(File.dirname(dest), install: install_builder) + FileUtils.cp(src, dest, :preserve => true) + end + @cache.register_build(dest, :Copy, [src], @env, install: install_builder) + end + (target_is_dir ? Dir.exists?(@target) : File.exists?(@target)) ? true : false + else + true + end + end + + end + + # The Install builder is identical to the Copy builder. + class Install < Copy; end + + end +end diff --git a/lib/rscons/builders/directory.rb b/lib/rscons/builders/directory.rb index 13c71e1..bde16eb 100644 --- a/lib/rscons/builders/directory.rb +++ b/lib/rscons/builders/directory.rb @@ -1,22 +1,33 @@ module Rscons module Builders + # The Directory builder creates a directory. class Directory < Builder # Run the builder to produce a build target. def run(options) - if File.directory?(@target) - true - elsif File.exists?(@target) - Ansi.write($stderr, :red, "Error: `#{@target}' already exists and is not a directory", :reset, "\n") - false + install_builder = self.class.name == "InstallDirectory" + if (not install_builder) or Rscons.application.operation("install") + if File.directory?(@target) + true + elsif File.exists?(@target) + Ansi.write($stderr, :red, "Error: `#{@target}' already exists and is not a directory", :reset, "\n") + false + else + print_run_message("Creating directory => #{@target}", nil) + @cache.mkdir_p(@target, install: install_builder) + true + end else - print_run_message("Creating directory => #{@target}", nil) - @cache.mkdir_p(@target) true end end end + + # The InstallDirectory class is identical to Directory but only performs + # an action for an "install" operation. + class InstallDirectory < Directory; end + end end diff --git a/lib/rscons/builders/install.rb b/lib/rscons/builders/install.rb deleted file mode 100644 index 34881f6..0000000 --- a/lib/rscons/builders/install.rb +++ /dev/null @@ -1,55 +0,0 @@ -require "pathname" - -module Rscons - module Builders - # The Install builder copies files/directories to new locations. - class Install < Builder - - # Run the builder to produce a build target. - def run(options) - target_is_dir = (@sources.length > 1) || - Dir.exists?(@sources.first) || - Dir.exists?(@target) - outdir = target_is_dir ? @target : File.dirname(@target) - # Collect the list of files to copy over. - file_map = {} - if target_is_dir - @sources.each do |src| - if Dir.exists? src - Dir.glob("#{src}/**/*", File::FNM_DOTMATCH).select do |f| - File.file?(f) - end.each do |subfile| - subpath = Pathname.new(subfile).relative_path_from(Pathname.new(src)).to_s - file_map[subfile] = "#{outdir}/#{subpath}" - end - else - file_map[src] = "#{outdir}/#{File.basename(src)}" - end - end - else - file_map[sources.first] = target - end - printed_message = false - file_map.each do |src, dest| - # Check the cache and copy if necessary - unless @cache.up_to_date?(dest, :Copy, [src], @env) - unless printed_message - message = "#{name} #{Util.short_format_paths(@sources)} => #{@target}" - print_run_message(message, nil) - printed_message = true - end - @cache.mkdir_p(File.dirname(dest)) - FileUtils.cp(src, dest, :preserve => true) - end - @cache.register_build(dest, :Copy, [src], @env) - end - (target_is_dir ? Dir.exists?(@target) : File.exists?(@target)) ? true : false - end - - end - - # The Copy builder is identical to the Install builder. - class Copy < Install; end - - end -end diff --git a/lib/rscons/configure_op.rb b/lib/rscons/configure_op.rb index cf6f810..a996534 100644 --- a/lib/rscons/configure_op.rb +++ b/lib/rscons/configure_op.rb @@ -10,20 +10,48 @@ module Rscons # Create a ConfigureOp. # - # @param work_dir [String] - # Work directory for configure operation. - def initialize(work_dir) - @work_dir = work_dir + # @param options [Hash] + # Optional parameters. + # @param build_dir [String] + # Build directory. + # @param prefix [String] + # Install prefix. + # @param project_name [String] + # Project name. + def initialize(options) + # Default options. + options[:build_dir] ||= "build" + options[:prefix] ||= "/usr/local" + @work_dir = "#{options[:build_dir]}/configure" FileUtils.mkdir_p(@work_dir) @log_fh = File.open("#{@work_dir}/config.log", "wb") + cache = Cache.instance + cache["failed_commands"] = [] + cache["configuration_data"] = {} + cache["configuration_data"]["build_dir"] = options[:build_dir] + cache["configuration_data"]["prefix"] = options[:prefix] + if project_name = options[:project_name] + Ansi.write($stdout, "Configuring ", :cyan, project_name, :reset, "...\n") + else + $stdout.puts "Configuring project..." + end + Ansi.write($stdout, "Setting build directory... ", :green, options[:build_dir], :reset, "\n") + Ansi.write($stdout, "Setting prefix... ", :green, options[:prefix], :reset, "\n") + store_merge("prefix" => options[:prefix]) end # Close the log file handle. # + # @param success [Boolean] + # Whether all configure operations were successful. + # # @return [void] - def close + def close(success) @log_fh.close @log_fh = nil + cache = Cache.instance + cache["configuration_data"]["configured"] = success + cache.write end # Check for a working C compiler. diff --git a/spec/build_tests_spec.rb b/spec/build_tests_spec.rb index 7f70335..029d4e1 100644 --- a/spec/build_tests_spec.rb +++ b/spec/build_tests_spec.rb @@ -1,6 +1,7 @@ require 'fileutils' require "open3" require "set" +require "tmpdir" describe Rscons do @@ -305,29 +306,50 @@ EOF expect(IO.read('foo.yml')).to eq("---\nkey: value\n") end - it 'cleans built files' do - test_dir("simple") - result = run_rscons - expect(result.stderr).to eq "" - expect(`./simple.exe`).to match /This is a simple C program/ - expect(File.exists?('build/e.1/simple.o')).to be_truthy - result = run_rscons(op: %w[clean]) - expect(File.exists?('build/e.1/simple.o')).to be_falsey - expect(File.exists?('build/e.1')).to be_falsey - expect(File.exists?('simple.exe')).to be_falsey - expect(File.exists?('simple.c')).to be_truthy - end + context "clean operation" do + it 'cleans built files' do + test_dir("simple") + result = run_rscons + expect(result.stderr).to eq "" + expect(`./simple.exe`).to match /This is a simple C program/ + expect(File.exists?('build/e.1/simple.o')).to be_truthy + result = run_rscons(op: %w[clean]) + expect(File.exists?('build/e.1/simple.o')).to be_falsey + expect(File.exists?('build/e.1')).to be_falsey + expect(File.exists?('simple.exe')).to be_falsey + expect(File.exists?('simple.c')).to be_truthy + end - it 'does not clean created directories if other non-rscons-generated files reside there' do - test_dir("simple") - result = run_rscons - expect(result.stderr).to eq "" - expect(`./simple.exe`).to match /This is a simple C program/ - expect(File.exists?('build/e.1/simple.o')).to be_truthy - File.open('build/e.1/dum', 'w') { |fh| fh.puts "dum" } - result = run_rscons(op: %w[clean]) - expect(File.exists?('build/e.1')).to be_truthy - expect(File.exists?('build/e.1/dum')).to be_truthy + it 'does not clean created directories if other non-rscons-generated files reside there' do + test_dir("simple") + result = run_rscons + expect(result.stderr).to eq "" + expect(`./simple.exe`).to match /This is a simple C program/ + expect(File.exists?('build/e.1/simple.o')).to be_truthy + File.open('build/e.1/dum', 'w') { |fh| fh.puts "dum" } + result = run_rscons(op: %w[clean]) + expect(File.exists?('build/e.1')).to be_truthy + expect(File.exists?('build/e.1/dum')).to be_truthy + end + + it "removes built files but not installed files" do + test_dir "typical" + + Dir.mktmpdir do |prefix| + result = run_rscons(rsconscript: "install.rb", op: %W[configure --prefix=#{prefix}]) + expect(result.stderr).to eq "" + + result = run_rscons(rsconscript: "install.rb", op: %W[install]) + expect(result.stderr).to eq "" + expect(File.exists?("#{prefix}/bin/program.exe")).to be_truthy + expect(File.exists?("build/e.1/src/one/one.o")).to be_truthy + + result = run_rscons(rsconscript: "install.rb", op: %W[clean]) + expect(result.stderr).to eq "" + expect(File.exists?("#{prefix}/bin/program.exe")).to be_truthy + expect(File.exists?("build/e.1/src/one/one.o")).to be_falsey + end + end end it 'allows Ruby classes as custom builders to be used to construct files' do @@ -1105,28 +1127,6 @@ EOF end end - context "Install buildler" do - it "copies a file to the target file name" do - test_dir("typical") - - result = run_rscons(rsconscript: "install.rb") - expect(result.stderr).to eq "" - expect(lines(result.stdout)).to include *["Install install.rb => inst.exe"] - - result = run_rscons(rsconscript: "install.rb") - expect(result.stderr).to eq "" - expect(result.stdout).to eq "" - - expect(File.exists?("inst.exe")).to be_truthy - expect(File.read("inst.exe", mode: "rb")).to eq(File.read("install.rb", mode: "rb")) - - FileUtils.rm("inst.exe") - result = run_rscons(rsconscript: "install.rb") - expect(result.stderr).to eq "" - expect(lines(result.stdout)).to include *["Install install.rb => inst.exe"] - end - end - context "phony targets" do it "allows specifying a Symbol as a target name and reruns the builder if the sources or command have changed" do test_dir("simple") @@ -1584,7 +1584,7 @@ EOF end end - context "configure" do + context "configure operation" do it "raises a method not found error for configure methods called outside a configure block" do test_dir "configure" result = run_rscons(rsconscript: "scope.rb") @@ -2124,4 +2124,94 @@ EOF end end + context "install operation" do + it "invokes a configure operation if the project is not yet configured" do + test_dir "typical" + + result = run_rscons(rsconscript: "install.rb", op: %W[install]) + expect(result.stdout).to match /Configuring install_test/ + end + + it "invokes a build operation" do + test_dir "typical" + + Dir.mktmpdir do |prefix| + result = run_rscons(rsconscript: "install.rb", op: %W[configure --prefix=#{prefix}]) + expect(result.stderr).to eq "" + result = run_rscons(rsconscript: "install.rb", op: %W[install]) + expect(result.stderr).to eq "" + expect(result.stdout).to match /Compiling/ + expect(result.stdout).to match /Linking/ + end + end + + it "installs the requested directories and files" do + test_dir "typical" + + Dir.mktmpdir do |prefix| + result = run_rscons(rsconscript: "install.rb", op: %W[configure --prefix=#{prefix}]) + expect(result.stderr).to eq "" + + result = run_rscons(rsconscript: "install.rb", op: %W[install]) + expect(result.stderr).to eq "" + expect(result.stdout).to match /Creating directory/ + expect(result.stdout).to match /Install install.rb =>/ + expect(result.stdout).to match /Install src =>/ + expect(Dir.entries(prefix)).to match_array %w[. .. bin src share mult] + expect(File.directory?("#{prefix}/bin")).to be_truthy + expect(File.directory?("#{prefix}/src")).to be_truthy + expect(File.directory?("#{prefix}/share")).to be_truthy + expect(File.exists?("#{prefix}/bin/program.exe")).to be_truthy + expect(File.exists?("#{prefix}/src/one/one.c")).to be_truthy + expect(File.exists?("#{prefix}/share/proj/install.rb")).to be_truthy + expect(File.exists?("#{prefix}/mult/install.rb")).to be_truthy + expect(File.exists?("#{prefix}/mult/copy.rb")).to be_truthy + + result = run_rscons(rsconscript: "install.rb", op: %W[install]) + expect(result.stderr).to eq "" + expect(result.stdout).to eq "" + end + end + + it "does not install when only a build is performed" do + test_dir "typical" + + Dir.mktmpdir do |prefix| + result = run_rscons(rsconscript: "install.rb", op: %W[configure --prefix=#{prefix}]) + expect(result.stderr).to eq "" + + result = run_rscons(rsconscript: "install.rb", op: %W[build]) + expect(result.stderr).to eq "" + expect(result.stdout).to_not match /Install/ + expect(Dir.entries(prefix)).to match_array %w[. ..] + + result = run_rscons(rsconscript: "install.rb", op: %W[install]) + expect(result.stderr).to eq "" + expect(result.stdout).to match /Install/ + end + end + end + + context "uninstall operation" do + it "removes installed files but not built files" do + test_dir "typical" + + Dir.mktmpdir do |prefix| + result = run_rscons(rsconscript: "install.rb", op: %W[configure --prefix=#{prefix}]) + expect(result.stderr).to eq "" + + result = run_rscons(rsconscript: "install.rb", op: %W[install]) + expect(result.stderr).to eq "" + expect(File.exists?("#{prefix}/bin/program.exe")).to be_truthy + expect(File.exists?("build/e.1/src/one/one.o")).to be_truthy + + result = run_rscons(rsconscript: "install.rb", op: %W[uninstall]) + expect(result.stderr).to eq "" + expect(File.exists?("#{prefix}/bin/program.exe")).to be_falsey + expect(File.exists?("build/e.1/src/one/one.o")).to be_truthy + expect(Dir.entries(prefix)).to match_array %w[. ..] + end + end + end + end