implement install and uninstall operations - close #82

add "prefix" construction variable - close #99
add InstallDirectory builder - close #100
This commit is contained in:
Josh Holtrop 2019-04-23 22:01:09 -04:00
parent 328babe1f4
commit fd054a07c4
8 changed files with 299 additions and 133 deletions

View File

@ -1,5 +1,14 @@
project_name "install_test"
build do build do
Environment.new do |env| 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
end end

View File

@ -26,6 +26,7 @@ module Rscons
:Directory, :Directory,
:Disassemble, :Disassemble,
:Install, :Install,
:InstallDirectory,
:Library, :Library,
:Object, :Object,
:Preprocess, :Preprocess,
@ -145,9 +146,9 @@ require_relative "rscons/builders/mixins/program"
# default builders # default builders
require_relative "rscons/builders/cfile" require_relative "rscons/builders/cfile"
require_relative "rscons/builders/command" require_relative "rscons/builders/command"
require_relative "rscons/builders/copy"
require_relative "rscons/builders/directory" require_relative "rscons/builders/directory"
require_relative "rscons/builders/disassemble" require_relative "rscons/builders/disassemble"
require_relative "rscons/builders/install"
require_relative "rscons/builders/library" require_relative "rscons/builders/library"
require_relative "rscons/builders/object" require_relative "rscons/builders/object"
require_relative "rscons/builders/preprocess" require_relative "rscons/builders/preprocess"

View File

@ -1,3 +1,5 @@
require "set"
module Rscons module Rscons
# Functionality for an instance of the rscons application invocation. # Functionality for an instance of the rscons application invocation.
@ -23,6 +25,18 @@ module Rscons
def initialize def initialize
@n_threads = Util.determine_n_threads @n_threads = Util.determine_n_threads
@vars = VarSet.new @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 end
# Run the specified operation. # Run the specified operation.
@ -37,6 +51,7 @@ module Rscons
# @return [Integer] # @return [Integer]
# Process exit code (0 on success). # Process exit code (0 on success).
def run(operation, script, operation_options) def run(operation, script, operation_options)
@operations << operation
@script = script @script = script
case operation case operation
when "build" when "build"
@ -58,6 +73,10 @@ module Rscons
configure(operation_options) configure(operation_options)
when "distclean" when "distclean"
distclean distclean
when "install"
run("build", script, operation_options)
when "uninstall"
uninstall
else else
$stderr.puts "Unknown operation: #{operation}" $stderr.puts "Unknown operation: #{operation}"
1 1
@ -129,34 +148,37 @@ module Rscons
# @return [Integer] # @return [Integer]
# Exit code. # Exit code.
def configure(options) 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 rv = 0
co = ConfigureOp.new("#{options[:build_dir]}/configure") options = options.merge(project_name: @script.project_name)
co = ConfigureOp.new(options)
begin begin
@script.configure(co) @script.configure(co)
rescue ConfigureOp::ConfigureFailure rescue ConfigureOp::ConfigureFailure
rv = 1 rv = 1
end end
co.close co.close(rv == 0)
cache["configuration_data"]["build_dir"] = options[:build_dir]
cache["configuration_data"]["prefix"] = options[:prefix]
cache["configuration_data"]["configured"] = rv == 0
cache.write
rv rv
end 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
end end

View File

@ -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

View File

@ -1,22 +1,33 @@
module Rscons module Rscons
module Builders module Builders
# The Directory builder creates a directory. # The Directory builder creates a directory.
class Directory < Builder class Directory < Builder
# Run the builder to produce a build target. # Run the builder to produce a build target.
def run(options) def run(options)
if File.directory?(@target) install_builder = self.class.name == "InstallDirectory"
true if (not install_builder) or Rscons.application.operation("install")
elsif File.exists?(@target) if File.directory?(@target)
Ansi.write($stderr, :red, "Error: `#{@target}' already exists and is not a directory", :reset, "\n") true
false 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 else
print_run_message("Creating directory => #{@target}", nil)
@cache.mkdir_p(@target)
true true
end end
end 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
end end

View File

@ -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

View File

@ -10,20 +10,48 @@ module Rscons
# Create a ConfigureOp. # Create a ConfigureOp.
# #
# @param work_dir [String] # @param options [Hash]
# Work directory for configure operation. # Optional parameters.
def initialize(work_dir) # @param build_dir [String]
@work_dir = work_dir # 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) FileUtils.mkdir_p(@work_dir)
@log_fh = File.open("#{@work_dir}/config.log", "wb") @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 end
# Close the log file handle. # Close the log file handle.
# #
# @param success [Boolean]
# Whether all configure operations were successful.
#
# @return [void] # @return [void]
def close def close(success)
@log_fh.close @log_fh.close
@log_fh = nil @log_fh = nil
cache = Cache.instance
cache["configuration_data"]["configured"] = success
cache.write
end end
# Check for a working C compiler. # Check for a working C compiler.

View File

@ -1,6 +1,7 @@
require 'fileutils' require 'fileutils'
require "open3" require "open3"
require "set" require "set"
require "tmpdir"
describe Rscons do describe Rscons do
@ -305,29 +306,50 @@ EOF
expect(IO.read('foo.yml')).to eq("---\nkey: value\n") expect(IO.read('foo.yml')).to eq("---\nkey: value\n")
end end
it 'cleans built files' do context "clean operation" do
test_dir("simple") it 'cleans built files' do
result = run_rscons test_dir("simple")
expect(result.stderr).to eq "" result = run_rscons
expect(`./simple.exe`).to match /This is a simple C program/ expect(result.stderr).to eq ""
expect(File.exists?('build/e.1/simple.o')).to be_truthy expect(`./simple.exe`).to match /This is a simple C program/
result = run_rscons(op: %w[clean]) expect(File.exists?('build/e.1/simple.o')).to be_truthy
expect(File.exists?('build/e.1/simple.o')).to be_falsey result = run_rscons(op: %w[clean])
expect(File.exists?('build/e.1')).to be_falsey expect(File.exists?('build/e.1/simple.o')).to be_falsey
expect(File.exists?('simple.exe')).to be_falsey expect(File.exists?('build/e.1')).to be_falsey
expect(File.exists?('simple.c')).to be_truthy expect(File.exists?('simple.exe')).to be_falsey
end expect(File.exists?('simple.c')).to be_truthy
end
it 'does not clean created directories if other non-rscons-generated files reside there' do it 'does not clean created directories if other non-rscons-generated files reside there' do
test_dir("simple") test_dir("simple")
result = run_rscons result = run_rscons
expect(result.stderr).to eq "" expect(result.stderr).to eq ""
expect(`./simple.exe`).to match /This is a simple C program/ expect(`./simple.exe`).to match /This is a simple C program/
expect(File.exists?('build/e.1/simple.o')).to be_truthy expect(File.exists?('build/e.1/simple.o')).to be_truthy
File.open('build/e.1/dum', 'w') { |fh| fh.puts "dum" } File.open('build/e.1/dum', 'w') { |fh| fh.puts "dum" }
result = run_rscons(op: %w[clean]) result = run_rscons(op: %w[clean])
expect(File.exists?('build/e.1')).to be_truthy expect(File.exists?('build/e.1')).to be_truthy
expect(File.exists?('build/e.1/dum')).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 end
it 'allows Ruby classes as custom builders to be used to construct files' do it 'allows Ruby classes as custom builders to be used to construct files' do
@ -1105,28 +1127,6 @@ EOF
end end
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 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 it "allows specifying a Symbol as a target name and reruns the builder if the sources or command have changed" do
test_dir("simple") test_dir("simple")
@ -1584,7 +1584,7 @@ EOF
end end
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 it "raises a method not found error for configure methods called outside a configure block" do
test_dir "configure" test_dir "configure"
result = run_rscons(rsconscript: "scope.rb") result = run_rscons(rsconscript: "scope.rb")
@ -2124,4 +2124,94 @@ EOF
end end
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 end