setup.rb 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. require 'rbconfig'
  2. require 'fileutils'
  3. require 'pp'
  4. require 'optparse'
  5. require 'yaml'
  6. module Package
  7. class SpecificationError < StandardError; end
  8. # forward declaration of the specification classes so we can keep all
  9. # constants here
  10. class PackageSpecification_1_0; end
  11. # Default semantics
  12. PackageSpecification = PackageSpecification_1_0
  13. #TODO: could get this collected automatically with Class#inherited etc
  14. SEMANTICS = { "1.0" => PackageSpecification_1_0 }
  15. KINDS = [
  16. :bin, :lib, :ext, :data, :conf, :doc
  17. ]
  18. #{{{ list of files to be ignored stolen from setup.rb
  19. mapping = { '.' => '\.', '$' => '\$', '#' => '\#', '*' => '.*' }
  20. ignore_files = %w[core RCSLOG tags TAGS .make.state .nse_depinfo
  21. #* .#* cvslog.* ,* .del-* *.olb *~ *.old *.bak *.BAK *.orig *.rej _$* *$
  22. *.org *.in .* ]
  23. #end of robbery
  24. IGNORE_FILES = ignore_files.map do |x|
  25. Regexp.new('\A' + x.gsub(/[\.\$\#\*]/){|c| mapping[c]} + '\z')
  26. end
  27. def self.config(name)
  28. # XXX use pathname
  29. prefix = Regexp.quote(Config::CONFIG["prefix"])
  30. exec_prefix = Regexp.quote(Config::CONFIG["exec_prefix"])
  31. Config::CONFIG[name].gsub(/\A\/?(#{prefix}|#{exec_prefix})\/?/, '')
  32. end
  33. SITE_DIRS = {
  34. :bin => config("bindir"),
  35. :lib => config("sitelibdir"),
  36. :ext => config("sitearchdir"),
  37. :data => config("datadir"),
  38. :conf => config("sysconfdir"),
  39. :doc => File.join(config("datadir"), "doc"),
  40. }
  41. VENDOR_DIRS = {
  42. :bin => config("bindir"),
  43. :lib => config("rubylibdir"),
  44. :ext => config("archdir"),
  45. :data => config("datadir"),
  46. :conf => config("sysconfdir"),
  47. :doc => File.join(config("datadir"), "doc"),
  48. }
  49. MODES = {
  50. :bin => 0755,
  51. :lib => 0644,
  52. :ext => 0755, # was: 0555,
  53. :data => 0644,
  54. :conf => 0644,
  55. :doc => 0644,
  56. }
  57. SETUP_OPTIONS = {:parse_cmdline => true, :load_conf => true, :run_tasks => true}
  58. def self.setup(version, options = {}, &instructions)
  59. prefixes = dirs = nil
  60. options = SETUP_OPTIONS.dup.update(options)
  61. if options[:load_conf] && File.exist?("config.save")
  62. config = YAML.load_file "config.save"
  63. prefixes = config[:prefixes]
  64. dirs = config[:dirs]
  65. end
  66. pkg = package_specification_with_semantics(version).new(prefixes, dirs)
  67. pkg.parse_command_line if options[:parse_cmdline]
  68. pkg.instance_eval(&instructions)
  69. pkg.run_tasks if options[:run_tasks]
  70. # pkg.install
  71. pkg
  72. end
  73. def self.package_specification_with_semantics(version)
  74. #XXX: implement the full x.y(.z)? semantics
  75. r = SEMANTICS[version]
  76. raise SpecificationError, "Unknown version #{version}." unless r
  77. r
  78. end
  79. module Actions
  80. class InstallFile
  81. attr_reader :source, :destination, :mode
  82. def initialize(source, destination, mode, options)
  83. @source = source
  84. @destination = destination
  85. @mode = mode
  86. @options = options
  87. end
  88. def install
  89. FileUtils.install @source, File.join(@options.destdir, @destination),
  90. {:verbose => @options.verbose,
  91. :noop => @options.noop, :mode => @mode }
  92. end
  93. def hash
  94. [@source.hash, @destination.hash].hash
  95. end
  96. def eql?(other)
  97. self.class == other.class &&
  98. @source == other.source &&
  99. @destination == other.destination &&
  100. @mode == other.mode
  101. end
  102. def <=>(other)
  103. FULL_ORDER[self, other] || self.destination <=> other.destination
  104. end
  105. end
  106. class MkDir
  107. attr_reader :directory
  108. def initialize(directory, options)
  109. @directory = directory
  110. @options = options
  111. end
  112. def install
  113. FileUtils.mkdir_p File.join(@options.destdir, @directory),
  114. {:verbose => @options.verbose,
  115. :noop => @options.noop }
  116. end
  117. def <=>(other)
  118. FULL_ORDER[self, other] || self.directory <=> other.directory
  119. end
  120. end
  121. class FixShebang
  122. attr_reader :destination
  123. def initialize(destination, options)
  124. @options = options
  125. @destination = destination
  126. end
  127. def install
  128. path = File.join(@options.destdir, @destination)
  129. fix_shebang(path)
  130. end
  131. # taken from rpa-base, originally based on setup.rb's
  132. # modify: #!/usr/bin/ruby
  133. # modify: #! /usr/bin/ruby
  134. # modify: #!ruby
  135. # not modify: #!/usr/bin/env ruby
  136. SHEBANG_RE = /\A\#!\s*\S*ruby\S*/
  137. #TODO allow the ruby-prog to be placed in the shebang line to be passed as
  138. # an option
  139. def fix_shebang(path)
  140. tmpfile = path + '.tmp'
  141. begin
  142. #XXX: needed at all?
  143. # it seems that FileUtils doesn't expose its default output
  144. # @fileutils_output = $stderr
  145. # we might want to allow this to be redirected.
  146. $stderr.puts "shebang:open #{tmpfile}" if @options.verbose
  147. unless @options.noop
  148. File.open(path) do |r|
  149. File.open(tmpfile, 'w', 0755) do |w|
  150. first = r.gets
  151. return unless SHEBANG_RE =~ first
  152. w.print first.sub(SHEBANG_RE, '#!' + Config::CONFIG['ruby-prog'])
  153. w.write r.read
  154. end
  155. end
  156. end
  157. FileUtils.mv(tmpfile, path, :verbose => @options.verbose,
  158. :noop => @options.noop)
  159. ensure
  160. FileUtils.rm_f(tmpfile, :verbose => @options.verbose,
  161. :noop => @options.noop)
  162. end
  163. end
  164. def <=>(other)
  165. FULL_ORDER[self, other] || self.destination <=> other.destination
  166. end
  167. def hash
  168. @destination.hash
  169. end
  170. def eql?(other)
  171. self.class == other.class && self.destination == other.destination
  172. end
  173. end
  174. order = [MkDir, InstallFile, FixShebang]
  175. FULL_ORDER = lambda do |me, other|
  176. a, b = order.index(me.class), order.index(other.class)
  177. if a && b
  178. (r = a - b) == 0 ? nil : r
  179. else
  180. -1 # arbitrary
  181. end
  182. end
  183. class ActionList < Array
  184. def directories!(options)
  185. dirnames = []
  186. map! { |d|
  187. if d.kind_of?(InstallFile) && !dirnames.include?(File.dirname(d.destination))
  188. dirnames << File.dirname(d.destination)
  189. [MkDir.new(File.dirname(d.destination), options), d]
  190. else
  191. d
  192. end
  193. }
  194. flatten!
  195. end
  196. def run(task)
  197. each { |action| action.__send__ task }
  198. end
  199. end
  200. end # module Actions
  201. Options = Struct.new(:noop, :verbose, :destdir)
  202. class PackageSpecification_1_0
  203. TASKS = %w[config setup install test show]
  204. # default options for translate(foo => bar)
  205. TRANSLATE_DEFAULT_OPTIONS = { :inherit => true }
  206. def self.declare_file_type(args, &handle_arg)
  207. str_arr_p = lambda{|x| Array === x && x.all?{|y| String === y}}
  208. # strict type checking --- we don't want this to be extended arbitrarily
  209. unless args.size == 1 && Hash === args.first &&
  210. args.first.all?{|f,r| [Proc, String, NilClass].include?(r.class) &&
  211. (String === f || str_arr_p[f])} or
  212. args.all?{|x| String === x || str_arr_p[x]}
  213. raise SpecificationError,
  214. "Unspecified semantics for the given arguments: #{args.inspect}"
  215. end
  216. if args.size == 1 && Hash === args.first
  217. args.first.to_a.each do |file, rename_info|
  218. if Array === file
  219. # ignoring boring files
  220. handle_arg.call(file, true, rename_info)
  221. else
  222. # we do want "boring" files given explicitly
  223. handle_arg.call([file], false, rename_info)
  224. end
  225. end
  226. else
  227. args.each do |a|
  228. if Array === a
  229. a.each{|file| handle_arg.call(file, true, nil)}
  230. else
  231. handle_arg.call(a, false, nil)
  232. end
  233. end
  234. end
  235. end
  236. #{{{ define the file tagging methods
  237. KINDS.each { |kind|
  238. define_method(kind) { |*args| # if this were 1.9 we could also take a block
  239. bin_callback = lambda do |kind_, type, dest, options|
  240. next if kind_ != :bin || type == :dir
  241. @actions << Actions::FixShebang.new(dest, options)
  242. end
  243. #TODO: refactor
  244. self.class.declare_file_type(args) do |files, ignore_p, opt_rename_info|
  245. files.each do |file|
  246. next if ignore_p && IGNORE_FILES.any?{|re| re.match(file)}
  247. add_file(kind, file, opt_rename_info, &bin_callback)
  248. end
  249. end
  250. }
  251. }
  252. def unit_test(*files)
  253. @unit_tests.concat files.flatten
  254. end
  255. attr_accessor :actions, :options
  256. def self.metadata(name)
  257. define_method(name) { |*args|
  258. if args.size == 1
  259. @metadata[name] = args.first
  260. end
  261. @metadata[name]
  262. }
  263. end
  264. metadata :name
  265. metadata :version
  266. metadata :author
  267. def translate_dir(kind, dir)
  268. replaced_dir_parts = dir.split(%r{/})
  269. kept_dir_parts = []
  270. loop do
  271. replaced_path = replaced_dir_parts.join("/")
  272. target, options = @translate[kind][replaced_path]
  273. options ||= TRANSLATE_DEFAULT_OPTIONS
  274. if target && (replaced_path == dir || options[:inherit])
  275. dir = (target != '' ? File.join(target, *kept_dir_parts) :
  276. File.join(*kept_dir_parts))
  277. break
  278. end
  279. break if replaced_dir_parts.empty?
  280. kept_dir_parts.unshift replaced_dir_parts.pop
  281. end
  282. dir
  283. end
  284. def add_file(kind, filename, new_filename_info, &callback)
  285. #TODO: refactor!!!
  286. if File.directory? filename #XXX setup.rb and rpa-base defined File.dir?
  287. # to cope with some win32 issue
  288. dir = filename.sub(/\A\.\//, "").sub(/\/\z/, "")
  289. dest = File.join(@prefixes[kind], @dirs[kind], translate_dir(kind, dir))
  290. @actions << Actions::MkDir.new(dest, @options)
  291. callback.call(kind, :dir, dest, @options) if block_given?
  292. else
  293. if new_filename_info
  294. case new_filename_info
  295. when Proc
  296. dest_name = new_filename_info.call(filename.dup)
  297. else
  298. dest_name = new_filename_info.dup
  299. end
  300. else
  301. dest_name = filename.dup
  302. end
  303. dirname = File.dirname(dest_name)
  304. dirname = "" if dirname == "."
  305. dest_name = File.join(translate_dir(kind, dirname), File.basename(dest_name))
  306. dest = File.join(@prefixes[kind], @dirs[kind], dest_name)
  307. @actions << Actions::InstallFile.new(filename, dest, MODES[kind], @options)
  308. callback.call(kind, :file, dest, @options) if block_given?
  309. end
  310. end
  311. def initialize(prefixes = nil, dirs = nil)
  312. @prefix = Config::CONFIG["prefix"].gsub(/\A\//, '')
  313. @translate = {}
  314. @prefixes = (prefixes || {}).dup
  315. KINDS.each { |kind|
  316. @prefixes[kind] = @prefix unless prefixes
  317. @translate[kind] = {}
  318. }
  319. @dirs = (dirs || {}).dup
  320. @dirs.update SITE_DIRS unless dirs
  321. @actions = Actions::ActionList.new
  322. @metadata = {}
  323. @unit_tests = []
  324. @options = Options.new
  325. @options.verbose = true
  326. @options.noop = false # XXX for testing
  327. @options.destdir = ''
  328. @tasks = []
  329. end
  330. def aoki
  331. (KINDS - [:ext]).each { |kind|
  332. translate(kind, kind.to_s => "", :inherit => true)
  333. __send__ kind, Dir["#{kind}/**/*"]
  334. }
  335. translate(:ext, "ext/*" => "", :inherit => true)
  336. ext Dir["ext/**/*.#{Config::CONFIG['DLEXT']}"]
  337. end
  338. def install
  339. puts "Installing #{name || "unknown package"} #{version}..." if options.verbose
  340. actions.uniq!
  341. actions.sort!
  342. actions.directories!(options)
  343. #pp self
  344. actions.run :install
  345. end
  346. def test
  347. unless @unit_tests.empty?
  348. puts "Testing #{name || "unknown package"} #{version}..." if options.verbose
  349. require 'test/unit'
  350. unless options.noop
  351. t = Test::Unit::AutoRunner.new(true)
  352. t.process_args(@unit_tests)
  353. t.run
  354. end
  355. end
  356. end
  357. def config
  358. File.open("config.save", "w") { |f|
  359. YAML.dump({:prefixes => @prefixes, :dirs => @dirs}, f)
  360. }
  361. end
  362. def show
  363. KINDS.each { |kind|
  364. puts "#{kind}\t#{File.join(options.destdir, @prefixes[kind], @dirs[kind])}"
  365. }
  366. end
  367. def translate(kind, additional_translations)
  368. default_opts = TRANSLATE_DEFAULT_OPTIONS.dup
  369. key_val_pairs = additional_translations.to_a
  370. option_pairs = key_val_pairs.select{|(k,v)| Symbol === k}
  371. default_opts.update(Hash[*option_pairs.flatten])
  372. (key_val_pairs - option_pairs).each do |key, val|
  373. add_translation(kind, key, val, default_opts)
  374. end
  375. end
  376. def add_translation(kind, src, dest, options)
  377. if is_glob?(src)
  378. dirs = expand_dir_glob(src)
  379. else
  380. dirs = [src]
  381. end
  382. dirs.each do |dirname|
  383. dirname = dirname.sub(%r{\A\./}, "").sub(%r{/\z}, "")
  384. @translate[kind].update({dirname => [dest, options]})
  385. end
  386. end
  387. def is_glob?(x)
  388. /(^|[^\\])[*?{\[]/.match(x)
  389. end
  390. def expand_dir_glob(src)
  391. Dir[src].select{|x| File.directory?(x)}
  392. end
  393. def clean_path(path)
  394. path.gsub(/\A\//, '').gsub(/\/+\Z/, '').squeeze("/")
  395. end
  396. def parse_command_line
  397. opts = OptionParser.new(nil, 24, ' ') { |opts|
  398. opts.banner = "Usage: setup.rb [options] [task]"
  399. opts.separator ""
  400. opts.separator "Tasks:"
  401. opts.separator " config configures paths"
  402. opts.separator " show shows paths"
  403. opts.separator " setup compiles ruby extentions and others XXX"
  404. opts.separator " install installs files"
  405. opts.separator " test runs unit tests"
  406. opts.separator ""
  407. opts.separator "Specific options:"
  408. opts.on "--prefix=PREFIX",
  409. "path prefix of target environment [#@prefix]" do |prefix|
  410. @prefix.replace clean_path(prefix) # Shared!
  411. end
  412. opts.separator ""
  413. KINDS.each { |kind|
  414. opts.on "--#{kind}prefix=PREFIX",
  415. "path prefix for #{kind} files [#{@prefixes[kind]}]" do |prefix|
  416. @prefixes[kind] = clean_path(prefix)
  417. end
  418. }
  419. opts.separator ""
  420. KINDS.each { |kind|
  421. opts.on "--#{kind}dir=PREFIX",
  422. "directory for #{kind} files [#{@dirs[kind]}]" do |prefix|
  423. @dirs[kind] = clean_path(prefix)
  424. end
  425. }
  426. opts.separator ""
  427. KINDS.each { |kind|
  428. opts.on "--#{kind}=PREFIX",
  429. "absolute directory for #{kind} files [#{File.join(@prefixes[kind], @dirs[kind])}]" do |prefix|
  430. @prefixes[kind] = clean_path(prefix)
  431. end
  432. }
  433. opts.separator ""
  434. opts.separator "Predefined path configurations:"
  435. opts.on "--site", "install into site-local directories (default)" do
  436. @dirs.update SITE_DIRS
  437. end
  438. opts.on "--vendor", "install into distribution directories (for packagers)" do
  439. @dirs.update VENDOR_DIRS
  440. end
  441. opts.separator ""
  442. opts.separator "General options:"
  443. opts.on "--destdir=DESTDIR",
  444. "install all files relative to DESTDIR (/)" do |destdir|
  445. @options.destdir = destdir
  446. end
  447. opts.on "--dry-run", "only display what to do if given [#{@options.noop}]" do
  448. @options.noop = true
  449. end
  450. opts.on "--no-harm", "only display what to do if given" do
  451. @options.noop = true
  452. end
  453. opts.on "--[no-]verbose", "output messages verbosely [#{@options.verbose}]" do |verbose|
  454. @options.verbose = verbose
  455. end
  456. opts.on_tail("-h", "--help", "Show this message") do
  457. puts opts
  458. exit
  459. end
  460. }
  461. opts.parse! ARGV
  462. if (ARGV - TASKS).empty? # Only existing tasks?
  463. @tasks = ARGV
  464. @tasks = ["install"] if @tasks.empty?
  465. else
  466. abort "Unknown task(s) #{(ARGV-TASKS).join ", "}."
  467. end
  468. end
  469. def run_tasks
  470. @tasks.each { |task| __send__ task }
  471. end
  472. end
  473. end # module Package
  474. #XXX incomplete setup.rb support for the hooks
  475. require 'rbconfig'
  476. def config(x)
  477. Config::CONFIG[x]
  478. end
  479. #{{{ small example
  480. if $0 == __FILE__
  481. Package.setup("1.0") {
  482. name "Crypt::ISAAC"
  483. lib "crypt/ISAAC.rb"
  484. unit_test Dir["test/TC*.rb"]
  485. }
  486. end
  487. # vim: sw=2 sts=2 et ts=8