123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597 |
- require 'rbconfig'
- require 'fileutils'
- require 'pp'
- require 'optparse'
- require 'yaml'
- module Package
- class SpecificationError < StandardError; end
- # forward declaration of the specification classes so we can keep all
- # constants here
- class PackageSpecification_1_0; end
- # Default semantics
- PackageSpecification = PackageSpecification_1_0
- #TODO: could get this collected automatically with Class#inherited etc
- SEMANTICS = { "1.0" => PackageSpecification_1_0 }
- KINDS = [
- :bin, :lib, :ext, :data, :conf, :doc
- ]
- #{{{ list of files to be ignored stolen from setup.rb
- mapping = { '.' => '\.', '$' => '\$', '#' => '\#', '*' => '.*' }
- ignore_files = %w[core RCSLOG tags TAGS .make.state .nse_depinfo
- #* .#* cvslog.* ,* .del-* *.olb *~ *.old *.bak *.BAK *.orig *.rej _$* *$
- *.org *.in .* ]
- #end of robbery
- IGNORE_FILES = ignore_files.map do |x|
- Regexp.new('\A' + x.gsub(/[\.\$\#\*]/){|c| mapping[c]} + '\z')
- end
- def self.config(name)
- # XXX use pathname
- prefix = Regexp.quote(Config::CONFIG["prefix"])
- exec_prefix = Regexp.quote(Config::CONFIG["exec_prefix"])
- Config::CONFIG[name].gsub(/\A\/?(#{prefix}|#{exec_prefix})\/?/, '')
- end
- SITE_DIRS = {
- :bin => config("bindir"),
- :lib => config("sitelibdir"),
- :ext => config("sitearchdir"),
- :data => config("datadir"),
- :conf => config("sysconfdir"),
- :doc => File.join(config("datadir"), "doc"),
- }
- VENDOR_DIRS = {
- :bin => config("bindir"),
- :lib => config("rubylibdir"),
- :ext => config("archdir"),
- :data => config("datadir"),
- :conf => config("sysconfdir"),
- :doc => File.join(config("datadir"), "doc"),
- }
- MODES = {
- :bin => 0755,
- :lib => 0644,
- :ext => 0755, # was: 0555,
- :data => 0644,
- :conf => 0644,
- :doc => 0644,
- }
- SETUP_OPTIONS = {:parse_cmdline => true, :load_conf => true, :run_tasks => true}
- def self.setup(version, options = {}, &instructions)
- prefixes = dirs = nil
- options = SETUP_OPTIONS.dup.update(options)
- if options[:load_conf] && File.exist?("config.save")
- config = YAML.load_file "config.save"
- prefixes = config[:prefixes]
- dirs = config[:dirs]
- end
- pkg = package_specification_with_semantics(version).new(prefixes, dirs)
- pkg.parse_command_line if options[:parse_cmdline]
- pkg.instance_eval(&instructions)
- pkg.run_tasks if options[:run_tasks]
- # pkg.install
- pkg
- end
- def self.package_specification_with_semantics(version)
- #XXX: implement the full x.y(.z)? semantics
- r = SEMANTICS[version]
- raise SpecificationError, "Unknown version #{version}." unless r
- r
- end
- module Actions
- class InstallFile
- attr_reader :source, :destination, :mode
- def initialize(source, destination, mode, options)
- @source = source
- @destination = destination
- @mode = mode
- @options = options
- end
- def install
- FileUtils.install @source, File.join(@options.destdir, @destination),
- {:verbose => @options.verbose,
- :noop => @options.noop, :mode => @mode }
- end
- def hash
- [@source.hash, @destination.hash].hash
- end
- def eql?(other)
- self.class == other.class &&
- @source == other.source &&
- @destination == other.destination &&
- @mode == other.mode
- end
- def <=>(other)
- FULL_ORDER[self, other] || self.destination <=> other.destination
- end
- end
- class MkDir
- attr_reader :directory
- def initialize(directory, options)
- @directory = directory
- @options = options
- end
- def install
- FileUtils.mkdir_p File.join(@options.destdir, @directory),
- {:verbose => @options.verbose,
- :noop => @options.noop }
- end
- def <=>(other)
- FULL_ORDER[self, other] || self.directory <=> other.directory
- end
- end
- class FixShebang
- attr_reader :destination
- def initialize(destination, options)
- @options = options
- @destination = destination
- end
- def install
- path = File.join(@options.destdir, @destination)
- fix_shebang(path)
- end
- # taken from rpa-base, originally based on setup.rb's
- # modify: #!/usr/bin/ruby
- # modify: #! /usr/bin/ruby
- # modify: #!ruby
- # not modify: #!/usr/bin/env ruby
- SHEBANG_RE = /\A\#!\s*\S*ruby\S*/
- #TODO allow the ruby-prog to be placed in the shebang line to be passed as
- # an option
- def fix_shebang(path)
- tmpfile = path + '.tmp'
- begin
- #XXX: needed at all?
- # it seems that FileUtils doesn't expose its default output
- # @fileutils_output = $stderr
- # we might want to allow this to be redirected.
- $stderr.puts "shebang:open #{tmpfile}" if @options.verbose
- unless @options.noop
- File.open(path) do |r|
- File.open(tmpfile, 'w', 0755) do |w|
- first = r.gets
- return unless SHEBANG_RE =~ first
- w.print first.sub(SHEBANG_RE, '#!' + Config::CONFIG['ruby-prog'])
- w.write r.read
- end
- end
- end
- FileUtils.mv(tmpfile, path, :verbose => @options.verbose,
- :noop => @options.noop)
- ensure
- FileUtils.rm_f(tmpfile, :verbose => @options.verbose,
- :noop => @options.noop)
- end
- end
- def <=>(other)
- FULL_ORDER[self, other] || self.destination <=> other.destination
- end
- def hash
- @destination.hash
- end
- def eql?(other)
- self.class == other.class && self.destination == other.destination
- end
- end
- order = [MkDir, InstallFile, FixShebang]
- FULL_ORDER = lambda do |me, other|
- a, b = order.index(me.class), order.index(other.class)
- if a && b
- (r = a - b) == 0 ? nil : r
- else
- -1 # arbitrary
- end
- end
- class ActionList < Array
- def directories!(options)
- dirnames = []
- map! { |d|
- if d.kind_of?(InstallFile) && !dirnames.include?(File.dirname(d.destination))
- dirnames << File.dirname(d.destination)
- [MkDir.new(File.dirname(d.destination), options), d]
- else
- d
- end
- }
- flatten!
- end
- def run(task)
- each { |action| action.__send__ task }
- end
- end
- end # module Actions
- Options = Struct.new(:noop, :verbose, :destdir)
- class PackageSpecification_1_0
- TASKS = %w[config setup install test show]
- # default options for translate(foo => bar)
- TRANSLATE_DEFAULT_OPTIONS = { :inherit => true }
- def self.declare_file_type(args, &handle_arg)
- str_arr_p = lambda{|x| Array === x && x.all?{|y| String === y}}
- # strict type checking --- we don't want this to be extended arbitrarily
- unless args.size == 1 && Hash === args.first &&
- args.first.all?{|f,r| [Proc, String, NilClass].include?(r.class) &&
- (String === f || str_arr_p[f])} or
- args.all?{|x| String === x || str_arr_p[x]}
- raise SpecificationError,
- "Unspecified semantics for the given arguments: #{args.inspect}"
- end
- if args.size == 1 && Hash === args.first
- args.first.to_a.each do |file, rename_info|
- if Array === file
- # ignoring boring files
- handle_arg.call(file, true, rename_info)
- else
- # we do want "boring" files given explicitly
- handle_arg.call([file], false, rename_info)
- end
- end
- else
- args.each do |a|
- if Array === a
- a.each{|file| handle_arg.call(file, true, nil)}
- else
- handle_arg.call(a, false, nil)
- end
- end
- end
- end
- #{{{ define the file tagging methods
- KINDS.each { |kind|
- define_method(kind) { |*args| # if this were 1.9 we could also take a block
- bin_callback = lambda do |kind_, type, dest, options|
- next if kind_ != :bin || type == :dir
- @actions << Actions::FixShebang.new(dest, options)
- end
- #TODO: refactor
- self.class.declare_file_type(args) do |files, ignore_p, opt_rename_info|
- files.each do |file|
- next if ignore_p && IGNORE_FILES.any?{|re| re.match(file)}
- add_file(kind, file, opt_rename_info, &bin_callback)
- end
- end
- }
- }
- def unit_test(*files)
- @unit_tests.concat files.flatten
- end
- attr_accessor :actions, :options
- def self.metadata(name)
- define_method(name) { |*args|
- if args.size == 1
- @metadata[name] = args.first
- end
- @metadata[name]
- }
- end
- metadata :name
- metadata :version
- metadata :author
- def translate_dir(kind, dir)
- replaced_dir_parts = dir.split(%r{/})
- kept_dir_parts = []
- loop do
- replaced_path = replaced_dir_parts.join("/")
- target, options = @translate[kind][replaced_path]
- options ||= TRANSLATE_DEFAULT_OPTIONS
- if target && (replaced_path == dir || options[:inherit])
- dir = (target != '' ? File.join(target, *kept_dir_parts) :
- File.join(*kept_dir_parts))
- break
- end
- break if replaced_dir_parts.empty?
- kept_dir_parts.unshift replaced_dir_parts.pop
- end
- dir
- end
- def add_file(kind, filename, new_filename_info, &callback)
- #TODO: refactor!!!
- if File.directory? filename #XXX setup.rb and rpa-base defined File.dir?
- # to cope with some win32 issue
- dir = filename.sub(/\A\.\//, "").sub(/\/\z/, "")
- dest = File.join(@prefixes[kind], @dirs[kind], translate_dir(kind, dir))
- @actions << Actions::MkDir.new(dest, @options)
- callback.call(kind, :dir, dest, @options) if block_given?
- else
- if new_filename_info
- case new_filename_info
- when Proc
- dest_name = new_filename_info.call(filename.dup)
- else
- dest_name = new_filename_info.dup
- end
- else
- dest_name = filename.dup
- end
- dirname = File.dirname(dest_name)
- dirname = "" if dirname == "."
- dest_name = File.join(translate_dir(kind, dirname), File.basename(dest_name))
- dest = File.join(@prefixes[kind], @dirs[kind], dest_name)
- @actions << Actions::InstallFile.new(filename, dest, MODES[kind], @options)
- callback.call(kind, :file, dest, @options) if block_given?
- end
- end
- def initialize(prefixes = nil, dirs = nil)
- @prefix = Config::CONFIG["prefix"].gsub(/\A\//, '')
- @translate = {}
- @prefixes = (prefixes || {}).dup
- KINDS.each { |kind|
- @prefixes[kind] = @prefix unless prefixes
- @translate[kind] = {}
- }
- @dirs = (dirs || {}).dup
- @dirs.update SITE_DIRS unless dirs
- @actions = Actions::ActionList.new
- @metadata = {}
- @unit_tests = []
- @options = Options.new
- @options.verbose = true
- @options.noop = false # XXX for testing
- @options.destdir = ''
- @tasks = []
- end
- def aoki
- (KINDS - [:ext]).each { |kind|
- translate(kind, kind.to_s => "", :inherit => true)
- __send__ kind, Dir["#{kind}/**/*"]
- }
- translate(:ext, "ext/*" => "", :inherit => true)
- ext Dir["ext/**/*.#{Config::CONFIG['DLEXT']}"]
- end
- def install
- puts "Installing #{name || "unknown package"} #{version}..." if options.verbose
- actions.uniq!
- actions.sort!
- actions.directories!(options)
- #pp self
- actions.run :install
- end
- def test
- unless @unit_tests.empty?
- puts "Testing #{name || "unknown package"} #{version}..." if options.verbose
- require 'test/unit'
- unless options.noop
- t = Test::Unit::AutoRunner.new(true)
- t.process_args(@unit_tests)
- t.run
- end
- end
- end
- def config
- File.open("config.save", "w") { |f|
- YAML.dump({:prefixes => @prefixes, :dirs => @dirs}, f)
- }
- end
- def show
- KINDS.each { |kind|
- puts "#{kind}\t#{File.join(options.destdir, @prefixes[kind], @dirs[kind])}"
- }
- end
- def translate(kind, additional_translations)
- default_opts = TRANSLATE_DEFAULT_OPTIONS.dup
- key_val_pairs = additional_translations.to_a
- option_pairs = key_val_pairs.select{|(k,v)| Symbol === k}
- default_opts.update(Hash[*option_pairs.flatten])
- (key_val_pairs - option_pairs).each do |key, val|
- add_translation(kind, key, val, default_opts)
- end
- end
- def add_translation(kind, src, dest, options)
- if is_glob?(src)
- dirs = expand_dir_glob(src)
- else
- dirs = [src]
- end
- dirs.each do |dirname|
- dirname = dirname.sub(%r{\A\./}, "").sub(%r{/\z}, "")
- @translate[kind].update({dirname => [dest, options]})
- end
- end
- def is_glob?(x)
- /(^|[^\\])[*?{\[]/.match(x)
- end
- def expand_dir_glob(src)
- Dir[src].select{|x| File.directory?(x)}
- end
- def clean_path(path)
- path.gsub(/\A\//, '').gsub(/\/+\Z/, '').squeeze("/")
- end
- def parse_command_line
- opts = OptionParser.new(nil, 24, ' ') { |opts|
- opts.banner = "Usage: setup.rb [options] [task]"
- opts.separator ""
- opts.separator "Tasks:"
- opts.separator " config configures paths"
- opts.separator " show shows paths"
- opts.separator " setup compiles ruby extentions and others XXX"
- opts.separator " install installs files"
- opts.separator " test runs unit tests"
- opts.separator ""
- opts.separator "Specific options:"
- opts.on "--prefix=PREFIX",
- "path prefix of target environment [#@prefix]" do |prefix|
- @prefix.replace clean_path(prefix) # Shared!
- end
- opts.separator ""
- KINDS.each { |kind|
- opts.on "--#{kind}prefix=PREFIX",
- "path prefix for #{kind} files [#{@prefixes[kind]}]" do |prefix|
- @prefixes[kind] = clean_path(prefix)
- end
- }
- opts.separator ""
- KINDS.each { |kind|
- opts.on "--#{kind}dir=PREFIX",
- "directory for #{kind} files [#{@dirs[kind]}]" do |prefix|
- @dirs[kind] = clean_path(prefix)
- end
- }
- opts.separator ""
- KINDS.each { |kind|
- opts.on "--#{kind}=PREFIX",
- "absolute directory for #{kind} files [#{File.join(@prefixes[kind], @dirs[kind])}]" do |prefix|
- @prefixes[kind] = clean_path(prefix)
- end
- }
- opts.separator ""
- opts.separator "Predefined path configurations:"
- opts.on "--site", "install into site-local directories (default)" do
- @dirs.update SITE_DIRS
- end
- opts.on "--vendor", "install into distribution directories (for packagers)" do
- @dirs.update VENDOR_DIRS
- end
- opts.separator ""
- opts.separator "General options:"
- opts.on "--destdir=DESTDIR",
- "install all files relative to DESTDIR (/)" do |destdir|
- @options.destdir = destdir
- end
- opts.on "--dry-run", "only display what to do if given [#{@options.noop}]" do
- @options.noop = true
- end
- opts.on "--no-harm", "only display what to do if given" do
- @options.noop = true
- end
- opts.on "--[no-]verbose", "output messages verbosely [#{@options.verbose}]" do |verbose|
- @options.verbose = verbose
- end
- opts.on_tail("-h", "--help", "Show this message") do
- puts opts
- exit
- end
- }
- opts.parse! ARGV
- if (ARGV - TASKS).empty? # Only existing tasks?
- @tasks = ARGV
- @tasks = ["install"] if @tasks.empty?
- else
- abort "Unknown task(s) #{(ARGV-TASKS).join ", "}."
- end
- end
- def run_tasks
- @tasks.each { |task| __send__ task }
- end
- end
- end # module Package
- #XXX incomplete setup.rb support for the hooks
- require 'rbconfig'
- def config(x)
- Config::CONFIG[x]
- end
- #{{{ small example
- if $0 == __FILE__
- Package.setup("1.0") {
- name "Crypt::ISAAC"
- lib "crypt/ISAAC.rb"
- unit_test Dir["test/TC*.rb"]
- }
- end
- # vim: sw=2 sts=2 et ts=8
|