commit 89d8bebc58e8c69d8fb03685e4875eba3bc9d86e
Author: Marc Dequènes (Duck) <Duck@DuckCorp.org>
Date:   Wed Jul 22 14:27:47 2009 +0200

    [evol] files/code reorganization, and added a proper installer

diff --git a/.gitignore b/.gitignore
index 030b6c8..dab3a97 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,10 +3,8 @@
 \#*#
 # generated by installer
 .config
-lib/ldapshadows/config.rb
+lib/ldap_shadows/config.rb
 data/locale
 #
-conf
+conf/ldap_shadows/private.conf
 var
-#
-config/private.conf
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 0000000..5855c07
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,19 @@
+
+# for a real install in /usr/local
+ruby setup.rb config
+ruby setup.rb setup
+ruby setup.rb install
+ruby setup.rb clean
+
+# for packagers
+ruby setup.rb config --installdirs=std
+ruby setup.rb setup
+ruby setup.rb install --prefix=debian/cyborghood
+ruby setup.rb clean
+
+# for in-place tests
+ruby setup.rb config --bindir=bin --rbdir=lib --datadir=data --sysconfdir=conf --localstatedir=var --shebang=never
+ruby setup.rb setup
+# and when finished...
+ruby setup.rb clean
+
diff --git a/bin/shadowwalker b/bin/shadowwalker
new file mode 100755
index 0000000..62fc11b
--- /dev/null
+++ b/bin/shadowwalker
@@ -0,0 +1,169 @@
+#!/usr/bin/ruby -Ku
+
+#--
+# LdapShadows, a Medium-level LDAP Access Library and Tool.
+# Copyright (c) 2009 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+# to allow in-place run for test
+$: << File.join(File.dirname(__FILE__), "..", "lib")
+
+require 'ldap_shadows'
+require 'yaml'
+require 'cmdparse2'
+
+config_str_prv = IO.read(File.join(LdapShadows::Config::CFG_DIR, "private.conf"))
+config_str = IO.read(File.join(LdapShadows::Config::CFG_DIR, "test.conf"))
+config = YAML.load(config_str_prv).merge(YAML.load(config_str)).recursive_symbolize_keys!
+ActiveLdap::Base.setup_connection(config[:ldap])
+
+cmdparser = CmdParse::CommandParser.new(true)
+cmdparser.program_name = ""
+cmdparser.program_version = [0, 0, 1]
+
+cmdparser.options = CmdParse::OptionParserWrapper.new do |opt|
+  opt.separator "Global options:"
+  opt.on("--debug", "Output debug info without being formated") {|t| $debug_opt = true }
+  opt.on("--expert", "Output extra info for expert users") {|t| $expert_opt = true }
+  opt.on("--handles", "Output with handles (objects/field/... keys used for manipulations)") {|t| $handles_opt = true }
+end
+
+cmdparser.add_command(CmdParse::HelpCommand.new)
+cmdparser.add_command(CmdParse::VersionCommand.new)
+
+I18n.load_path += Dir[File.join(LdapShadows::Config::DATA_DIR, "*.yml")]
+I18n.default_locale = :en
+
+ldapctl = LdapShadows::Controller.new
+config[:aspects].each_pair do |aspect_name, aspect_data|
+  ldapctl.set_aspect(aspect_name, aspect_data)
+end
+config[:objects].each_pair do |obj_name, obj_data|
+  ldapctl.load_object(obj_name, obj_data)
+end
+ldapctl.load_relations
+
+cmd = CmdParse::Command.new('list', false)
+cmd.short_desc = "list objects"
+cmd.set_execution_block do |args|
+  if args.size != 1
+    STDERR.puts "syntax error: no object name given"
+    exit 1
+  end
+
+  obj_hdl = args[0]
+  obj_klass = ldapctl.find_klass(obj_hdl)
+  if obj_klass.nil?
+    STDERR.puts "No such object '#{obj_hdl}'."
+    exit 2
+  end
+
+  obj_human_name = I18n.t(obj_hdl, :scope => 'objects', :default => "Object '#{obj_hdl}'")
+  puts "=== List of #{obj_human_name.pluralize} ==="
+  obj_klass.find(:all).each do |obj|
+    str = obj.human_name
+    str += " [#{obj.name}]" if $handles_opt
+    str += ": #{obj.description}" unless obj.description.empty?
+    puts str
+  end
+end
+cmdparser.add_command(cmd)
+
+def translate_data_key(name)
+  if name.index(":")
+    type, key = name.split(":")
+    case type
+    when 'rel'
+      I18n.t(key, :scope => 'relations', :default => name)
+    else
+      raise "Cannot translate unknown data key type"
+    end
+  else
+    att = ActiveLdap::Base.schema.attribute(name)
+    I18n.t(att.human_attribute_name, :scope => 'attribute_types', :default => att.human_attribute_description)
+  end
+end
+
+def display_data(attr_data)
+  attr_data.each_pair do |key, val|
+    field_name = translate_data_key(key)
+    field_name += " [#{key}]" if $handles_opt
+    puts field_name + ": " + (val.is_a?(Array) ? val.sort.collect{|v| v.to_s }.join(", ") : val.to_s)
+  end
+end
+
+cmd = CmdParse::Command.new('show', false)
+cmd.short_desc = "show object information"
+cmd.set_execution_block do |args|
+  if args.size < 1
+    STDERR.puts "syntax error: no object name given"
+    exit 1
+  end
+  if args.size < 2
+    STDERR.puts "syntax error: no item name given"
+    exit 1
+  end
+
+  obj_hdl = args[0]
+  obj_klass = ldapctl.find_klass(obj_hdl)
+  if obj_klass.nil?
+    STDERR.puts "No such object '#{obj_hdl}'."
+    exit 2
+  end
+
+  item_hdl = args[1]
+  begin
+    item = obj_klass.find(item_hdl)
+  rescue ActiveLdap::EntryNotFound
+    STDERR.puts "No such item '#{obj_hdl}/#{item_hdl}'"
+    exit 2
+  end
+
+  obj_human_name = I18n.t(obj_hdl, :scope => 'objects', :default => "Object '#{obj_hdl}'")
+  name = item.human_name
+  name += " [#{item.name}]" if $handles_opt
+  puts "=== #{obj_human_name}: #{name} ==="
+
+  if $debug_opt
+    puts item.to_s
+    puts "--- Detected Info ---"
+    puts "aspects: " + item.aspects.sort.join(", ")
+
+    puts "--- Family ---"
+    puts "parent: " + item.family_parent_dn.to_s
+    puts "siblings: " + item.family_siblings_dn.join(", ")
+    puts "children: " + item.family_children_dn.join(", ")
+
+    puts "--- Relations ---"
+    item.relations.each do |rel|
+      puts "#{rel}: " + item.send(rel).collect{|g| g.name }.join(", ")
+    end
+  else
+    obj_info, obj_aspects = item.organized_data(:expert => $expert_opt, :skip_binary => true)
+
+    display_data(obj_info)
+
+    obj_aspects.each_pair do |aspect_name, aspect_data|
+      aspect_display_name = I18n.t(aspect_name, :scope => 'aspects', :default => "Aspect '#{aspect_name}'")
+      puts "--- #{aspect_display_name} ---"
+      display_data(aspect_data)
+    end
+  end
+end
+cmdparser.add_command(cmd)
+
+cmdparser.parse
+
diff --git a/conf/ldap_shadows/test.conf b/conf/ldap_shadows/test.conf
new file mode 100644
index 0000000..9d645ca
--- /dev/null
+++ b/conf/ldap_shadows/test.conf
@@ -0,0 +1,131 @@
+---
+objects:
+  bot:
+    mapping:
+      dn_attribute: uid
+      prefix: ''
+      classes: ['bot']
+      sort_by: uid
+    presentation:
+      optional_classes: []
+      allowed_aspects: ['primary', 'mail', 'fs', 'shell', 'ftp', 'web', 'jabber']
+      hidden_attributes: ['objectClass', 'uid']
+      expert_attributes: ['uidNumber', 'gidNumber', 'gecos']
+  individual:
+    mapping:
+      dn_attribute: uid
+      prefix: ''
+      classes: ['individual']
+      sort_by: uid
+    presentation:
+      optional_classes: []
+      allowed_aspects: ['primary', 'mail', 'fs', 'shell', 'ftp', 'web', 'jabber']
+      hidden_attributes: ['objectClass', 'uid']
+      expert_attributes: ['uidNumber', 'gidNumber', 'gecos']
+  group:
+    mapping:
+      dn_attribute: cn
+      prefix: ''
+      classes: ['posixGroup', 'groupOfMembers']
+      sort_by: cn
+    presentation:
+      optional_classes: []
+      allowed_aspects: []
+      hidden_attributes: ['objectClass', 'uniqueMember']
+      expert_attributes: ['gidNumber']
+      associated_relations: ['individualsPrimaryMembers', 'botsPrimaryMembers', 'individualsSecondaryMembers', 'botsSecondaryMembers']
+    relations:
+      individualsPrimaryMembers:
+        type: :belongs_to
+        object: individual
+        many: gidNumber
+        foreign_key: gidNumber
+      botsPrimaryMembers:
+        type: :belongs_to
+        object: bot
+        many: gidNumber
+        foreign_key: gidNumber
+      individualsSecondaryMembers:
+        type: :has_many
+        object: individual
+        foreign_key: uniqueMember
+        primary_key: dn
+      botsSecondaryMembers:
+        type: :has_many
+        object: bot
+        foreign_key: uniqueMember
+        primary_key: dn
+  entity:
+    mapping:
+      dn_attribute: o
+      prefix: ''
+      classes: ['entity']
+      excluded_classes: ['dcObject']
+      sort_by: o
+    presentation:
+      name_attribute: o
+      hidden_attributes: ['objectClass', 'founder']
+      associated_relations: ['foundersIndividuals', 'foundersEntities']
+    relations:
+      foundersIndividuals:
+        type: :has_many
+        object: individual
+        foreign_key: founder
+        primary_key: dn
+      foundersEntities:
+        type: :has_many
+        object: entity
+        foreign_key: founder
+        primary_key: dn
+aspects:
+  mail:
+    mapping:
+      classes: ['emailUser']
+    presentation:
+    relations:
+  fs:
+    mapping:
+      classes: ['fsUser']
+    presentation:
+      associated_relations: ['primaryGroup', 'secondaryGroups']
+    relations:
+      primaryGroup:
+        type: :has_many
+        object: group
+        foreign_key: gidNumber
+        primary_key: gidNumber
+      secondaryGroups:
+        type: :belongs_to
+        object: group
+        many: uniqueMember
+        foreign_key: dn
+  shell:
+    mapping:
+      classes: ['shellUser']
+      depend_aspects: ['primary', 'fs']
+    presentation:
+      associated_attributes: ['loginShell']
+    relations:
+  ftp:
+    mapping:
+      classes: ['ftpUser']
+      depend_aspects: ['primary', 'fs']
+    presentation:
+    relations:
+  web:
+    mapping:
+      classes: ['webUser']
+      depend_aspects: ['primary']
+    presentation:
+    relations:
+  jabber:
+    mapping:
+      classes: ['jabberUser']
+    presentation:
+    relations:
+  primary:
+    mapping:
+      classes: ['primaryAccount']
+    presentation:
+      associated_attributes: ['uid']
+    relations:
diff --git a/config/test.conf b/config/test.conf
deleted file mode 100644
index 9d645ca..0000000
--- a/config/test.conf
+++ /dev/null
@@ -1,131 +0,0 @@
----
-objects:
-  bot:
-    mapping:
-      dn_attribute: uid
-      prefix: ''
-      classes: ['bot']
-      sort_by: uid
-    presentation:
-      optional_classes: []
-      allowed_aspects: ['primary', 'mail', 'fs', 'shell', 'ftp', 'web', 'jabber']
-      hidden_attributes: ['objectClass', 'uid']
-      expert_attributes: ['uidNumber', 'gidNumber', 'gecos']
-  individual:
-    mapping:
-      dn_attribute: uid
-      prefix: ''
-      classes: ['individual']
-      sort_by: uid
-    presentation:
-      optional_classes: []
-      allowed_aspects: ['primary', 'mail', 'fs', 'shell', 'ftp', 'web', 'jabber']
-      hidden_attributes: ['objectClass', 'uid']
-      expert_attributes: ['uidNumber', 'gidNumber', 'gecos']
-  group:
-    mapping:
-      dn_attribute: cn
-      prefix: ''
-      classes: ['posixGroup', 'groupOfMembers']
-      sort_by: cn
-    presentation:
-      optional_classes: []
-      allowed_aspects: []
-      hidden_attributes: ['objectClass', 'uniqueMember']
-      expert_attributes: ['gidNumber']
-      associated_relations: ['individualsPrimaryMembers', 'botsPrimaryMembers', 'individualsSecondaryMembers', 'botsSecondaryMembers']
-    relations:
-      individualsPrimaryMembers:
-        type: :belongs_to
-        object: individual
-        many: gidNumber
-        foreign_key: gidNumber
-      botsPrimaryMembers:
-        type: :belongs_to
-        object: bot
-        many: gidNumber
-        foreign_key: gidNumber
-      individualsSecondaryMembers:
-        type: :has_many
-        object: individual
-        foreign_key: uniqueMember
-        primary_key: dn
-      botsSecondaryMembers:
-        type: :has_many
-        object: bot
-        foreign_key: uniqueMember
-        primary_key: dn
-  entity:
-    mapping:
-      dn_attribute: o
-      prefix: ''
-      classes: ['entity']
-      excluded_classes: ['dcObject']
-      sort_by: o
-    presentation:
-      name_attribute: o
-      hidden_attributes: ['objectClass', 'founder']
-      associated_relations: ['foundersIndividuals', 'foundersEntities']
-    relations:
-      foundersIndividuals:
-        type: :has_many
-        object: individual
-        foreign_key: founder
-        primary_key: dn
-      foundersEntities:
-        type: :has_many
-        object: entity
-        foreign_key: founder
-        primary_key: dn
-aspects:
-  mail:
-    mapping:
-      classes: ['emailUser']
-    presentation:
-    relations:
-  fs:
-    mapping:
-      classes: ['fsUser']
-    presentation:
-      associated_relations: ['primaryGroup', 'secondaryGroups']
-    relations:
-      primaryGroup:
-        type: :has_many
-        object: group
-        foreign_key: gidNumber
-        primary_key: gidNumber
-      secondaryGroups:
-        type: :belongs_to
-        object: group
-        many: uniqueMember
-        foreign_key: dn
-  shell:
-    mapping:
-      classes: ['shellUser']
-      depend_aspects: ['primary', 'fs']
-    presentation:
-      associated_attributes: ['loginShell']
-    relations:
-  ftp:
-    mapping:
-      classes: ['ftpUser']
-      depend_aspects: ['primary', 'fs']
-    presentation:
-    relations:
-  web:
-    mapping:
-      classes: ['webUser']
-      depend_aspects: ['primary']
-    presentation:
-    relations:
-  jabber:
-    mapping:
-      classes: ['jabberUser']
-    presentation:
-    relations:
-  primary:
-    mapping:
-      classes: ['primaryAccount']
-    presentation:
-      associated_attributes: ['uid']
-    relations:
diff --git a/data/ldap_shadows/en.yml b/data/ldap_shadows/en.yml
new file mode 100644
index 0000000..91f3823
--- /dev/null
+++ b/data/ldap_shadows/en.yml
@@ -0,0 +1,58 @@
+---
+en:
+  objects:
+    individual: "Individual"
+    bot: "Bot"
+    group: "Group"
+    entity: "Entity"
+  attribute_types:
+    allowGlobalDirectory: "Disclose Own Contact Information in GLobal Directory"
+    cn: "Full Name"
+    birthday: "Birthday"
+    birthlocation: "Birth Location"
+    description: "Description"
+    foundingDate: "Founding Date"
+    gecos: "GECOS"
+    gidNumber: "Primary Group (numeric)"
+    givenName: "FirstName"
+    homeDirectory: "Home Directory"
+    homePostalAddress: "Home Postal Address"
+    host: "Shell Allowed Hosts"
+    jid: "Jabber ID"
+    jpegPhoto: "Photo"
+    keyFingerPrint: "GPG/PGP Key Fingerprint"
+    labeledURI: "Web Site"
+    loginShell: "Shell Interpreter"
+    mail: "eMail address(es)"
+    mailForward: "eMail Forward Adress(es)"
+    mailQuota: "Maximum Mailbox Size"
+    manager: "Manager(s)"
+    mobile: "Mobile Phone"
+    occupation: "Job / Studies"
+    o: "Organization(s)"
+    owner: "Owner(s)"
+    preferredLanguage: "Language Preference"
+    sn: "Surname"
+    sshAuthKey: "SSH Public Key(s)"
+    uid: "Identifier (Login)"
+    uidNumber: "Identifier (numeric)"
+    uniqueAbbreviation: "Unique Abbreviation"
+    uniqueMember: "Group member"
+    webVirtualHost: "Hosted Web Sites"
+  relations:
+    primaryGroup: "Primary Group"
+    secondaryGroups: "Secondary Groups"
+    individualsPrimaryMembers: "Primary Member(s) Individual(s)"
+    botsPrimaryMembers: "Primary Member(s) Bot(s)"
+    individualsSecondaryMembers: "Secondary Member(s) Individual(s)"
+    botsSecondaryMembers: "Secondary Member(s) Bot(s)"
+    foundersIndividuals: "Founder(s) Individual(s)"
+    foundersEntities: "Founder(s) Entity(ies)"
+  aspects:
+    primary: "Primary Account"
+    ftp: "FTP Account"
+    web: "Web Account"
+    shell: "Shell Account"
+    fs: "FileSystem Account"
+    mail: "eMail Account"
+    jabber: "Jabber Account"
diff --git a/lib/ldap_shadows.rb b/lib/ldap_shadows.rb
new file mode 100644
index 0000000..dce0b82
--- /dev/null
+++ b/lib/ldap_shadows.rb
@@ -0,0 +1,23 @@
+#--
+# LdapShadows, a Medium-level LDAP Access Library and Tool.
+# Copyright (c) 2009 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+require 'ldap_shadows/info'
+require 'ldap_shadows/config'
+require 'ldap_shadows/lang_additions'
+require 'ldap_shadows/controller'
+
diff --git a/lib/ldap_shadows/controller.rb b/lib/ldap_shadows/controller.rb
new file mode 100755
index 0000000..1a66007
--- /dev/null
+++ b/lib/ldap_shadows/controller.rb
@@ -0,0 +1,107 @@
+#--
+# LdapShadows, a Medium-level LDAP Access Library and Tool.
+# Copyright (c) 2009 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+
+$KCODE = 'UTF8'
+require 'jcode'
+require 'active_ldap'
+require 'ldap_shadows/object'
+
+
+module LdapShadows
+  class Controller
+    def initialize(mod_container = LdapShadows::Objects)
+      @mod_container = mod_container
+      @object_definitions = {}
+      @aspects = {}
+    end
+
+    def set_aspect(aspect_name, aspect_def)
+      @aspects[aspect_name] = aspect_def
+    end
+
+    def get_aspect(aspect_name)
+      @aspects[aspect_name.to_sym]
+    end
+
+    def self.object_name_to_klass_name(obj_name)
+      "LdapObject" + obj_name.to_s.capitalize
+    end
+
+    def load_object(obj_name, obj_def)
+      obj_mapping = obj_def[:mapping]
+      klass_name = self.class.object_name_to_klass_name(obj_name)
+
+      # create class
+      @mod_container.module_eval(<<-EOS)
+        class #{klass_name} < LdapShadows::LdapObject; end
+      EOS
+
+      # configure class
+      klass = find_klass(obj_name)
+      klass.presentation = obj_def[:presentation]
+      klass.mapper = self
+      klass.ldap_mapping obj_mapping.reject {|key, val| not ActiveLdap::Base::VALID_LDAP_MAPPING_OPTIONS.include?(key) }
+
+      # store definition for later relations processing
+      @object_definitions[obj_name] = obj_def
+    end
+
+    def find_klass(obj_name)
+      klass_name = self.class.object_name_to_klass_name(obj_name)
+      return nil unless @mod_container.const_defined?(klass_name)
+      @mod_container.const_get(klass_name)
+    end
+
+    # run it _once_ when all objects are loaded
+    def load_relations
+      @object_definitions.each_pair do |obj_name, obj_def|
+        obj_rel = {}
+        obj_rel.merge!(obj_def[:relations]) if obj_def.include?(:relations)
+        if obj_def[:presentation].has_key?(:allowed_aspects)
+          obj_def[:presentation][:allowed_aspects].each do |rel|
+            rel_data = get_aspect(rel)
+            obj_rel.merge!(rel_data[:relations]) if rel_data.has_key?(:relations) and rel_data[:relations]
+          end
+        end
+        next if obj_rel.empty?
+
+        klass = find_klass(obj_name)
+
+        obj_rel.each_pair do |field_name, rel|
+          foreign_klass = find_klass(rel[:object])
+          rel[:class_name] = foreign_klass.to_s
+
+          case rel[:type]
+          when :belongs_to
+            klass.belongs_to field_name, rel.reject {|key, val| not ActiveLdap::Associations::ClassMethods::VALID_BELONGS_TO_OPTIONS.include?(key) }
+          when :has_many
+            klass.has_many field_name, rel.reject {|key, val| not ActiveLdap::Associations::ClassMethods::VALID_HAS_MANY_OPTIONS.include?(key) }
+          else
+            raise "bug in '#{obj_name}' object relations (wrong type)"
+          end
+        end
+      end
+    end
+  end
+
+  # default location for mapped objects
+  module Objects
+  end
+end
+
diff --git a/lib/ldap_shadows/info.rb b/lib/ldap_shadows/info.rb
new file mode 100644
index 0000000..7ceca5d
--- /dev/null
+++ b/lib/ldap_shadows/info.rb
@@ -0,0 +1,4 @@
+module LdapShadows
+  PRODUCT = "LDAP Shadows"
+  VERSION = "0.1.0~dev"
+end
diff --git a/lib/ldap_shadows/lang_additions.rb b/lib/ldap_shadows/lang_additions.rb
new file mode 100644
index 0000000..0d92866
--- /dev/null
+++ b/lib/ldap_shadows/lang_additions.rb
@@ -0,0 +1,36 @@
+#--
+# LdapShadows, a Medium-level LDAP Access Library and Tool.
+# Copyright (c) 2009 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+# for Rails
+class Hash
+  def recursive_symbolize_keys!
+    symbolize_keys!
+    values.select { |v| v.is_a?(Hash) }.each { |h| h.recursive_symbolize_keys! }
+    self
+  end
+end
+
+# for ActiveLDAP
+module ActiveLdap
+  class DistinguishedName
+    def shift
+      @rdns.shift
+    end
+  end
+end
+
diff --git a/lib/ldap_shadows/object.rb b/lib/ldap_shadows/object.rb
new file mode 100755
index 0000000..b8bf9bf
--- /dev/null
+++ b/lib/ldap_shadows/object.rb
@@ -0,0 +1,212 @@
+#--
+# LdapShadows, a Medium-level LDAP Access Library and Tool.
+# Copyright (c) 2009 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+
+module LdapShadows
+  class LdapObject < ActiveLdap::Base
+    class_inheritable_accessor :presentation, :mapper
+
+    ldap_mapping :prefix => '', :classes => ['top'], :scope => :sub
+
+    # temporary method until active_ldap is fixed: return a DN object (see #23932)
+    def dn_obj
+      ActiveLdap::DistinguishedName.parse(self.dn)
+    end
+
+    def name
+      name = self[dn_attribute].is_a?(Array) ? self[dn_attribute][0] : self[dn_attribute]
+      name.strip
+    end
+
+    def human_name
+      [self.class.presentation[:name_attribute], 'displayName', 'cn'].each do |attr|
+        if self.has_attribute?(attr) and self.attribute_present?(attr)
+          name = self[attr].is_a?(Array) ? self[attr][0] : self[attr]
+          return name.strip
+        end
+      end
+      return ""
+    end
+
+    def description
+      [self.class.presentation[:desc_attribute], 'description'].each do |attr|
+        if self.has_attribute?(attr) and self.attribute_present?(attr)
+          return self[attr].is_a?(Array) ? self[attr][0] : self[attr]
+        end
+      end
+      return ""
+    end
+
+    def possible_relations
+      self.associations.collect {|assoc| assoc.to_s } - ['children']
+    end
+
+    def relations
+      rel_list = []
+
+      rel_list += self.class.presentation[:associated_relations] if self.class.presentation[:associated_relations]
+
+      aspects.each do |aspect|
+        aspect_data = self.class.mapper.get_aspect(aspect)
+        if defined?(aspect_data[:presentation][:associated_relations]) and aspect_data[:presentation][:associated_relations]
+          rel_list += aspect_data[:presentation][:associated_relations]
+        end
+      end
+
+      rel_list & possible_relations
+    end
+
+    def aspects
+      present_aspects = []
+      (self.class.presentation[:allowed_aspects] || []).each do |aspect|
+        aspect_data = self.class.mapper.get_aspect(aspect)
+        aspect_mapping = aspect_data[:mapping]
+        present_aspects << aspect if self.classes & aspect_mapping[:classes] == aspect_mapping[:classes]
+      end
+
+      present_aspects
+    end
+
+    def objectclasses_attr_list(objectclass_list)
+      objectclass_list = [objectclass_list] unless objectclass_list.is_a? Array
+      list = []
+      objectclass_list.each do |objectclass|
+        objectclass_obj = ActiveLdap::Base.schema.object_class(objectclass)
+        attr_list = objectclass_obj.must + objectclass_obj.may
+        list += attr_list.collect{|attr| attr.human_attribute_name }
+      end
+      list
+    end
+
+    def organized_data(options = {})
+      options.symbolize_keys!
+      options[:expert] ||= false;
+      options[:skip_binary] ||= false;
+
+      ignored_attrs = self.class.presentation[:hidden_attributes] || []
+      ignored_attrs += (self.class.presentation[:expert_attributes] || []) unless options[:expert]
+      attr_list = self.attributes.keys - ignored_attrs
+
+      aspects = self.aspects
+      rel_list = self.possible_relations
+
+      # first pass to take aspects forced relations into account
+      obj_aspects = {}
+      aspects.each do |aspect|
+        aspect_data = self.class.mapper.get_aspect(aspect)
+
+        if defined?(aspect_data[:presentation][:associated_attributes]) and aspect_data[:presentation][:associated_attributes]
+          taken_attr_list = aspect_data[:presentation][:associated_attributes] & (attr_list + ignored_attrs)
+          unless taken_attr_list.empty?
+            obj_aspects[aspect] ||= {}
+            obj_aspects[aspect].merge!(fetch_attributes_data(taken_attr_list, options))
+            attr_list -= taken_attr_list
+          end
+        end
+
+        if defined?(aspect_data[:presentation][:associated_relations]) and aspect_data[:presentation][:associated_relations]
+          taken_rel_list = aspect_data[:presentation][:associated_relations] & rel_list
+          unless taken_rel_list.empty?
+            obj_aspects[aspect] ||= {}
+            obj_aspects[aspect].merge!(fetch_relations_data(taken_rel_list, options))
+            rel_list -= taken_rel_list
+          end
+        end
+      end
+
+      # manage general attributes
+      obj_info = {}
+      if self.class.presentation[:associate_unclaimed_attributes]
+        taken_attr_list = attr_list
+      else
+        taken_attr_list = []
+        if self.class.presentation.has_key?(:associated_attributes)
+          taken_attr_list += self.class.presentation[:associated_attributes]
+        end
+        taken_attr_list += objectclasses_attr_list(self.required_classes + (self.class.presentation[:optional_classes] || []))
+      end
+      taken_attr_list = taken_attr_list.uniq & attr_list
+      obj_info = fetch_attributes_data(taken_attr_list, options)
+      attr_list -= taken_attr_list
+
+      # manage general relations
+      if self.class.presentation[:associated_relations]
+        taken_rel_list = self.class.presentation[:associated_relations] & rel_list
+        unless taken_rel_list.empty?
+          obj_info.merge!(fetch_relations_data(taken_rel_list, options))
+          rel_list -= taken_rel_list
+        end
+      end
+
+      # second pass to dispath the remaining attributes
+      unless attr_list.empty?
+        aspects.each do |aspect|
+          aspect_data = self.class.mapper.get_aspect(aspect)
+
+          taken_attr_list = (objectclasses_attr_list(aspect_data[:mapping][:classes]) & attr_list)
+          obj_aspects[aspect] ||= {}
+          obj_aspects[aspect].merge!(fetch_attributes_data(taken_attr_list, options))
+          attr_list -= taken_attr_list
+
+          break if attr_list.empty?
+        end
+      end
+
+      [obj_info, obj_aspects]
+    end
+
+    def family_parent_dn
+      pdn = self.dn_obj.dup
+      pdn.shift
+      pdn
+    end
+
+    def family_children
+      LdapObject.find(:all, :base => self.dn, :scope => :one)
+    end
+
+    def family_children_dn
+      self.family_children.collect {|obj| obj.dn }
+    end
+
+    def family_siblings
+      # cannot substract, as the ruby object signature may be different
+      LdapObject.find(:all, :base => self.family_parent_dn.to_s, :scope => :one).select{|obj| obj.dn != self.dn }
+    end
+
+    def family_siblings_dn
+      self.family_siblings.collect {|obj| obj.dn }
+    end
+
+    protected
+
+    def fetch_attributes_data(attr_list, options = {})
+      attr_data = self.attributes.select {|key, val| attr_list.include?(key) and not (options[:skip_binary] and ActiveLdap::Base.schema.attribute(key).binary?) }
+      Hash[attr_data]
+    end
+
+    def fetch_relations_data(rel_list, options = {})
+      rel_data = rel_list.collect do |rel|
+        data = self.send(rel).collect{|g| g.name }
+        data.empty? ? nil : ["rel:" + rel, data]
+      end
+      Hash[rel_data.compact]
+    end
+  end
+end
+
diff --git a/locale/en.yml b/locale/en.yml
deleted file mode 100644
index 91f3823..0000000
--- a/locale/en.yml
+++ /dev/null
@@ -1,58 +0,0 @@
----
-en:
-  objects:
-    individual: "Individual"
-    bot: "Bot"
-    group: "Group"
-    entity: "Entity"
-  attribute_types:
-    allowGlobalDirectory: "Disclose Own Contact Information in GLobal Directory"
-    cn: "Full Name"
-    birthday: "Birthday"
-    birthlocation: "Birth Location"
-    description: "Description"
-    foundingDate: "Founding Date"
-    gecos: "GECOS"
-    gidNumber: "Primary Group (numeric)"
-    givenName: "FirstName"
-    homeDirectory: "Home Directory"
-    homePostalAddress: "Home Postal Address"
-    host: "Shell Allowed Hosts"
-    jid: "Jabber ID"
-    jpegPhoto: "Photo"
-    keyFingerPrint: "GPG/PGP Key Fingerprint"
-    labeledURI: "Web Site"
-    loginShell: "Shell Interpreter"
-    mail: "eMail address(es)"
-    mailForward: "eMail Forward Adress(es)"
-    mailQuota: "Maximum Mailbox Size"
-    manager: "Manager(s)"
-    mobile: "Mobile Phone"
-    occupation: "Job / Studies"
-    o: "Organization(s)"
-    owner: "Owner(s)"
-    preferredLanguage: "Language Preference"
-    sn: "Surname"
-    sshAuthKey: "SSH Public Key(s)"
-    uid: "Identifier (Login)"
-    uidNumber: "Identifier (numeric)"
-    uniqueAbbreviation: "Unique Abbreviation"
-    uniqueMember: "Group member"
-    webVirtualHost: "Hosted Web Sites"
-  relations:
-    primaryGroup: "Primary Group"
-    secondaryGroups: "Secondary Groups"
-    individualsPrimaryMembers: "Primary Member(s) Individual(s)"
-    botsPrimaryMembers: "Primary Member(s) Bot(s)"
-    individualsSecondaryMembers: "Secondary Member(s) Individual(s)"
-    botsSecondaryMembers: "Secondary Member(s) Bot(s)"
-    foundersIndividuals: "Founder(s) Individual(s)"
-    foundersEntities: "Founder(s) Entity(ies)"
-  aspects:
-    primary: "Primary Account"
-    ftp: "FTP Account"
-    web: "Web Account"
-    shell: "Shell Account"
-    fs: "FileSystem Account"
-    mail: "eMail Account"
-    jabber: "Jabber Account"
diff --git a/post-clean.rb b/post-clean.rb
new file mode 100644
index 0000000..1868d34
--- /dev/null
+++ b/post-clean.rb
@@ -0,0 +1,22 @@
+# post-clean.rb - setup config generation cleanup
+
+#--
+# LdapShadows, a Medium-level LDAP Access Library and Tool.
+# Copyright (c) 2009 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+conffile = "lib/cyborghood/config.rb"
+File.unlink(conffile) if FileTest.exists?(conffile)
diff --git a/post-config.rb b/post-config.rb
new file mode 100644
index 0000000..e8a7813
--- /dev/null
+++ b/post-config.rb
@@ -0,0 +1,68 @@
+# post-config.rb - setup config generation
+
+#--
+# LdapShadows, a Medium-level LDAP Access Library and Tool.
+# Copyright (c) 2009 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+# General header.
+header = <<HEADER
+# config.rb - Coin-diff setup configuration
+
+#--
+# LdapShadows, a Medium-level LDAP Access Library and Tool.
+# Copyright (c) 2009 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+# This file is automatically generated by the installer.
+# Do not edit by hand.
+
+HEADER
+
+# Generate config.rb containing general compile/setup time configuration
+# information (in the CoinDiff::Config module).
+File.open('lib/ldap_shadows/config.rb', 'w') do |file|
+
+  file.print header
+  file.print <<-CONFIG
+module LdapShadows
+  class Config
+    BIN_DIR   = '#{config('bindir')}'
+    CFG_DIR   = '#{File.join(config('sysconfdir'), 'ldap_shadows')}'
+    DATA_DIR  = '#{File.join(config('datadir'), 'ldap_shadows')}'
+    LIB_DIR   = '#{config('rbdir')}'
+    LOG_DIR   = '#{File.join(config('localstatedir'), 'log', 'ldap_shadows')}'
+    RUN_DIR   = '#{File.join(config('localstatedir'), 'run', 'ldap_shadows')}'
+    VAR_DIR   = '#{File.join(config('localstatedir'), 'lib', 'ldap_shadows')}'
+    L10N_DIR  = '#{File.join(config('datadir'), 'locale')}'
+  end
+end
+  CONFIG
+
+end # File.open
diff --git a/setup.rb b/setup.rb
new file mode 100644
index 0000000..f9dd758
--- /dev/null
+++ b/setup.rb
@@ -0,0 +1,1586 @@
+#
+# setup.rb
+#
+# Copyright (c) 2000-2005 Minero Aoki
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU LGPL, Lesser General Public License version 2.1.
+#
+
+unless Enumerable.method_defined?(:map)   # Ruby 1.4.6
+  module Enumerable
+    alias map collect
+  end
+end
+
+unless File.respond_to?(:read)   # Ruby 1.6
+  def File.read(fname)
+    open(fname) {|f|
+      return f.read
+    }
+  end
+end
+
+unless Errno.const_defined?(:ENOTEMPTY)   # Windows?
+  module Errno
+    class ENOTEMPTY
+      # We do not raise this exception, implementation is not needed.
+    end
+  end
+end
+
+def File.binread(fname)
+  open(fname, 'rb') {|f|
+    return f.read
+  }
+end
+
+# for corrupted Windows' stat(2)
+def File.dir?(path)
+  File.directory?((path[-1,1] == '/') ? path : path + '/')
+end
+
+
+class ConfigTable
+
+  include Enumerable
+
+  def initialize(rbconfig)
+    @rbconfig = rbconfig
+    @items = []
+    @table = {}
+    # options
+    @install_prefix = nil
+    @config_opt = nil
+    @verbose = true
+    @no_harm = false
+  end
+
+  attr_accessor :install_prefix
+  attr_accessor :config_opt
+
+  attr_writer :verbose
+
+  def verbose?
+    @verbose
+  end
+
+  attr_writer :no_harm
+
+  def no_harm?
+    @no_harm
+  end
+
+  def [](key)
+    lookup(key).resolve(self)
+  end
+
+  def []=(key, val)
+    lookup(key).set val
+  end
+
+  def names
+    @items.map {|i| i.name }
+  end
+
+  def each(&block)
+    @items.each(&block)
+  end
+
+  def key?(name)
+    @table.key?(name)
+  end
+
+  def lookup(name)
+    @table[name] or setup_rb_error "no such config item: #{name}"
+  end
+
+  def add(item)
+    @items.push item
+    @table[item.name] = item
+  end
+
+  def remove(name)
+    item = lookup(name)
+    @items.delete_if {|i| i.name == name }
+    @table.delete_if {|name, i| i.name == name }
+    item
+  end
+
+  def load_script(path, inst = nil)
+    if File.file?(path)
+      MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path
+    end
+  end
+
+  def savefile
+    '.config'
+  end
+
+  def load_savefile
+    begin
+      File.foreach(savefile()) do |line|
+        k, v = *line.split(/=/, 2)
+        self[k] = v.strip
+      end
+    rescue Errno::ENOENT
+      setup_rb_error $!.message + "\n#{File.basename($0)} config first"
+    end
+  end
+
+  def save
+    @items.each {|i| i.value }
+    File.open(savefile(), 'w') {|f|
+      @items.each do |i|
+        f.printf "%s=%s\n", i.name, i.value if i.value? and i.value
+      end
+    }
+  end
+
+  def load_standard_entries
+    standard_entries(@rbconfig).each do |ent|
+      add ent
+    end
+  end
+
+  def standard_entries(rbconfig)
+    c = rbconfig
+
+    rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT'])
+
+    major = c['MAJOR'].to_i
+    minor = c['MINOR'].to_i
+    teeny = c['TEENY'].to_i
+    version = "#{major}.#{minor}"
+
+    # ruby ver. >= 1.4.4?
+    newpath_p = ((major >= 2) or
+                 ((major == 1) and
+                  ((minor >= 5) or
+                   ((minor == 4) and (teeny >= 4)))))
+
+    if c['rubylibdir']
+      # V > 1.6.3
+      libruby         = "#{c['prefix']}/lib/ruby"
+      librubyver      = c['rubylibdir']
+      librubyverarch  = c['archdir']
+      siteruby        = c['sitedir']
+      siterubyver     = c['sitelibdir']
+      siterubyverarch = c['sitearchdir']
+    elsif newpath_p
+      # 1.4.4 <= V <= 1.6.3
+      libruby         = "#{c['prefix']}/lib/ruby"
+      librubyver      = "#{c['prefix']}/lib/ruby/#{version}"
+      librubyverarch  = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
+      siteruby        = c['sitedir']
+      siterubyver     = "$siteruby/#{version}"
+      siterubyverarch = "$siterubyver/#{c['arch']}"
+    else
+      # V < 1.4.4
+      libruby         = "#{c['prefix']}/lib/ruby"
+      librubyver      = "#{c['prefix']}/lib/ruby/#{version}"
+      librubyverarch  = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
+      siteruby        = "#{c['prefix']}/lib/ruby/#{version}/site_ruby"
+      siterubyver     = siteruby
+      siterubyverarch = "$siterubyver/#{c['arch']}"
+    end
+    parameterize = lambda {|path|
+      path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix')
+    }
+
+    if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg }
+      makeprog = arg.sub(/'/, '').split(/=/, 2)[1]
+    else
+      makeprog = 'make'
+    end
+
+    [
+      ExecItem.new('installdirs', 'std/site/home',
+                   'std: install under libruby; site: install under site_ruby; home: install under $HOME')\
+          {|val, table|
+            case val
+            when 'std'
+              table['rbdir'] = '$librubyver'
+              table['sodir'] = '$librubyverarch'
+            when 'site'
+              table['rbdir'] = '$siterubyver'
+              table['sodir'] = '$siterubyverarch'
+            when 'home'
+              setup_rb_error '$HOME was not set' unless ENV['HOME']
+              table['prefix'] = ENV['HOME']
+              table['rbdir'] = '$libdir/ruby'
+              table['sodir'] = '$libdir/ruby'
+            end
+          },
+      PathItem.new('prefix', 'path', c['prefix'],
+                   'path prefix of target environment'),
+      PathItem.new('bindir', 'path', parameterize.call(c['bindir']),
+                   'the directory for commands'),
+      PathItem.new('libdir', 'path', parameterize.call(c['libdir']),
+                   'the directory for libraries'),
+      PathItem.new('datadir', 'path', parameterize.call(c['datadir']),
+                   'the directory for shared data'),
+      PathItem.new('mandir', 'path', parameterize.call(c['mandir']),
+                   'the directory for man pages'),
+      PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']),
+                   'the directory for system configuration files'),
+      PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']),
+                   'the directory for local state data'),
+      PathItem.new('libruby', 'path', libruby,
+                   'the directory for ruby libraries'),
+      PathItem.new('librubyver', 'path', librubyver,
+                   'the directory for standard ruby libraries'),
+      PathItem.new('librubyverarch', 'path', librubyverarch,
+                   'the directory for standard ruby extensions'),
+      PathItem.new('siteruby', 'path', siteruby,
+          'the directory for version-independent aux ruby libraries'),
+      PathItem.new('siterubyver', 'path', siterubyver,
+                   'the directory for aux ruby libraries'),
+      PathItem.new('siterubyverarch', 'path', siterubyverarch,
+                   'the directory for aux ruby binaries'),
+      PathItem.new('rbdir', 'path', '$siterubyver',
+                   'the directory for ruby scripts'),
+      PathItem.new('sodir', 'path', '$siterubyverarch',
+                   'the directory for ruby extentions'),
+      PathItem.new('rubypath', 'path', rubypath,
+                   'the path to set to #! line'),
+      ProgramItem.new('rubyprog', 'name', rubypath,
+                      'the ruby program using for installation'),
+      ProgramItem.new('makeprog', 'name', makeprog,
+                      'the make program to compile ruby extentions'),
+      SelectItem.new('shebang', 'all/ruby/never', 'ruby',
+                     'shebang line (#!) editing mode'),
+      BoolItem.new('without-ext', 'yes/no', 'no',
+                   'does not compile/install ruby extentions')
+    ]
+  end
+  private :standard_entries
+
+  def load_multipackage_entries
+    multipackage_entries().each do |ent|
+      add ent
+    end
+  end
+
+  def multipackage_entries
+    [
+      PackageSelectionItem.new('with', 'name,name...', '', 'ALL',
+                               'package names that you want to install'),
+      PackageSelectionItem.new('without', 'name,name...', '', 'NONE',
+                               'package names that you do not want to install')
+    ]
+  end
+  private :multipackage_entries
+
+  ALIASES = {
+    'std-ruby'         => 'librubyver',
+    'stdruby'          => 'librubyver',
+    'rubylibdir'       => 'librubyver',
+    'archdir'          => 'librubyverarch',
+    'site-ruby-common' => 'siteruby',     # For backward compatibility
+    'site-ruby'        => 'siterubyver',  # For backward compatibility
+    'bin-dir'          => 'bindir',
+    'bin-dir'          => 'bindir',
+    'rb-dir'           => 'rbdir',
+    'so-dir'           => 'sodir',
+    'data-dir'         => 'datadir',
+    'ruby-path'        => 'rubypath',
+    'ruby-prog'        => 'rubyprog',
+    'ruby'             => 'rubyprog',
+    'make-prog'        => 'makeprog',
+    'make'             => 'makeprog'
+  }
+
+  def fixup
+    ALIASES.each do |ali, name|
+      @table[ali] = @table[name]
+    end
+  end
+
+  def options_re
+    /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/
+  end
+
+  def parse_opt(opt)
+    m = options_re().match(opt) or setup_rb_error "config: unknown option #{opt}"
+    m.to_a[1,2]
+  end
+
+  def dllext
+    @rbconfig['DLEXT']
+  end
+
+  def value_config?(name)
+    lookup(name).value?
+  end
+
+  class Item
+    def initialize(name, template, default, desc)
+      @name = name.freeze
+      @template = template
+      @value = default
+      @default = default
+      @description = desc
+    end
+
+    attr_reader :name
+    attr_reader :description
+
+    attr_accessor :default
+    alias help_default default
+
+    def help_opt
+      "--#{@name}=#{@template}"
+    end
+
+    def value?
+      true
+    end
+
+    def value
+      @value
+    end
+
+    def resolve(table)
+      @value.gsub(%r<\$([^/]+)>) { table[$1] }
+    end
+
+    def set(val)
+      @value = check(val)
+    end
+
+    private
+
+    def check(val)
+      setup_rb_error "config: --#{name} requires argument" unless val
+      val
+    end
+  end
+
+  class BoolItem < Item
+    def config_type
+      'bool'
+    end
+
+    def help_opt
+      "--#{@name}"
+    end
+
+    private
+
+    def check(val)
+      return 'yes' unless val
+      case val
+      when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes'
+      when /\An(o)?\z/i, /\Af(alse)\z/i  then 'no'
+      else
+        setup_rb_error "config: --#{@name} accepts only yes/no for argument"
+      end
+    end
+  end
+
+  class PathItem < Item
+    def config_type
+      'path'
+    end
+
+    private
+
+    def check(path)
+      setup_rb_error "config: --#{@name} requires argument"  unless path
+      path[0,1] == '$' ? path : File.expand_path(path)
+    end
+  end
+
+  class ProgramItem < Item
+    def config_type
+      'program'
+    end
+  end
+
+  class SelectItem < Item
+    def initialize(name, selection, default, desc)
+      super
+      @ok = selection.split('/')
+    end
+
+    def config_type
+      'select'
+    end
+
+    private
+
+    def check(val)
+      unless @ok.include?(val.strip)
+        setup_rb_error "config: use --#{@name}=#{@template} (#{val})"
+      end
+      val.strip
+    end
+  end
+
+  class ExecItem < Item
+    def initialize(name, selection, desc, &block)
+      super name, selection, nil, desc
+      @ok = selection.split('/')
+      @action = block
+    end
+
+    def config_type
+      'exec'
+    end
+
+    def value?
+      false
+    end
+
+    def resolve(table)
+      setup_rb_error "$#{name()} wrongly used as option value"
+    end
+
+    undef set
+
+    def evaluate(val, table)
+      v = val.strip.downcase
+      unless @ok.include?(v)
+        setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})"
+      end
+      @action.call v, table
+    end
+  end
+
+  class PackageSelectionItem < Item
+    def initialize(name, template, default, help_default, desc)
+      super name, template, default, desc
+      @help_default = help_default
+    end
+
+    attr_reader :help_default
+
+    def config_type
+      'package'
+    end
+
+    private
+
+    def check(val)
+      unless File.dir?("packages/#{val}")
+        setup_rb_error "config: no such package: #{val}"
+      end
+      val
+    end
+  end
+
+  class MetaConfigEnvironment
+    def initialize(config, installer)
+      @config = config
+      @installer = installer
+    end
+
+    def config_names
+      @config.names
+    end
+
+    def config?(name)
+      @config.key?(name)
+    end
+
+    def bool_config?(name)
+      @config.lookup(name).config_type == 'bool'
+    end
+
+    def path_config?(name)
+      @config.lookup(name).config_type == 'path'
+    end
+
+    def value_config?(name)
+      @config.lookup(name).config_type != 'exec'
+    end
+
+    def add_config(item)
+      @config.add item
+    end
+
+    def add_bool_config(name, default, desc)
+      @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc)
+    end
+
+    def add_path_config(name, default, desc)
+      @config.add PathItem.new(name, 'path', default, desc)
+    end
+
+    def set_config_default(name, default)
+      @config.lookup(name).default = default
+    end
+
+    def remove_config(name)
+      @config.remove(name)
+    end
+
+    # For only multipackage
+    def packages
+      raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer
+      @installer.packages
+    end
+
+    # For only multipackage
+    def declare_packages(list)
+      raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer
+      @installer.packages = list
+    end
+  end
+
+end   # class ConfigTable
+
+
+# This module requires: #verbose?, #no_harm?
+module FileOperations
+
+  def mkdir_p(dirname, prefix = nil)
+    dirname = prefix + File.expand_path(dirname) if prefix
+    $stderr.puts "mkdir -p #{dirname}" if verbose?
+    return if no_harm?
+
+    # Does not check '/', it's too abnormal.
+    dirs = File.expand_path(dirname).split(%r<(?=/)>)
+    if /\A[a-z]:\z/i =~ dirs[0]
+      disk = dirs.shift
+      dirs[0] = disk + dirs[0]
+    end
+    dirs.each_index do |idx|
+      path = dirs[0..idx].join('')
+      Dir.mkdir path unless File.dir?(path)
+    end
+  end
+
+  def rm_f(path)
+    $stderr.puts "rm -f #{path}" if verbose?
+    return if no_harm?
+    force_remove_file path
+  end
+
+  def rm_rf(path)
+    $stderr.puts "rm -rf #{path}" if verbose?
+    return if no_harm?
+    remove_tree path
+  end
+
+  def remove_tree(path)
+    if File.symlink?(path)
+      remove_file path
+    elsif File.dir?(path)
+      remove_tree0 path
+    else
+      force_remove_file path
+    end
+  end
+
+  def remove_tree0(path)
+    Dir.foreach(path) do |ent|
+      next if ent == '.'
+      next if ent == '..'
+      entpath = "#{path}/#{ent}"
+      if File.symlink?(entpath)
+        remove_file entpath
+      elsif File.dir?(entpath)
+        remove_tree0 entpath
+      else
+        force_remove_file entpath
+      end
+    end
+    begin
+      Dir.rmdir path
+    rescue Errno::ENOTEMPTY
+      # directory may not be empty
+    end
+  end
+
+  def move_file(src, dest)
+    force_remove_file dest
+    begin
+      File.rename src, dest
+    rescue
+      File.open(dest, 'wb') {|f|
+        f.write File.binread(src)
+      }
+      File.chmod File.stat(src).mode, dest
+      File.unlink src
+    end
+  end
+
+  def force_remove_file(path)
+    begin
+      remove_file path
+    rescue
+    end
+  end
+
+  def remove_file(path)
+    File.chmod 0777, path
+    File.unlink path
+  end
+
+  def install(from, dest, mode, prefix = nil)
+    $stderr.puts "install #{from} #{dest}" if verbose?
+    return if no_harm?
+
+    realdest = prefix ? prefix + File.expand_path(dest) : dest
+    realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest)
+    str = File.binread(from)
+    if diff?(str, realdest)
+      verbose_off {
+        rm_f realdest if File.exist?(realdest)
+      }
+      File.open(realdest, 'wb') {|f|
+        f.write str
+      }
+      File.chmod mode, realdest
+
+      File.open("#{objdir_root()}/InstalledFiles", 'a') {|f|
+        if prefix
+          f.puts realdest.sub(prefix, '')
+        else
+          f.puts realdest
+        end
+      }
+    end
+  end
+
+  def diff?(new_content, path)
+    return true unless File.exist?(path)
+    new_content != File.binread(path)
+  end
+
+  def command(*args)
+    $stderr.puts args.join(' ') if verbose?
+    system(*args) or raise RuntimeError,
+        "system(#{args.map{|a| a.inspect }.join(' ')}) failed"
+  end
+
+  def ruby(*args)
+    command config('rubyprog'), *args
+  end
+  
+  def make(task = nil)
+    command(*[config('makeprog'), task].compact)
+  end
+
+  def extdir?(dir)
+    File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb")
+  end
+
+  def files_of(dir)
+    Dir.open(dir) {|d|
+      return d.select {|ent| File.file?("#{dir}/#{ent}") }
+    }
+  end
+
+  DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn )
+
+  def directories_of(dir)
+    Dir.open(dir) {|d|
+      return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT
+    }
+  end
+
+end
+
+
+# This module requires: #srcdir_root, #objdir_root, #relpath
+module HookScriptAPI
+
+  def get_config(key)
+    @config[key]
+  end
+
+  alias config get_config
+
+  # obsolete: use metaconfig to change configuration
+  def set_config(key, val)
+    @config[key] = val
+  end
+
+  #
+  # srcdir/objdir (works only in the package directory)
+  #
+
+  def curr_srcdir
+    "#{srcdir_root()}/#{relpath()}"
+  end
+
+  def curr_objdir
+    "#{objdir_root()}/#{relpath()}"
+  end
+
+  def srcfile(path)
+    "#{curr_srcdir()}/#{path}"
+  end
+
+  def srcexist?(path)
+    File.exist?(srcfile(path))
+  end
+
+  def srcdirectory?(path)
+    File.dir?(srcfile(path))
+  end
+  
+  def srcfile?(path)
+    File.file?(srcfile(path))
+  end
+
+  def srcentries(path = '.')
+    Dir.open("#{curr_srcdir()}/#{path}") {|d|
+      return d.to_a - %w(. ..)
+    }
+  end
+
+  def srcfiles(path = '.')
+    srcentries(path).select {|fname|
+      File.file?(File.join(curr_srcdir(), path, fname))
+    }
+  end
+
+  def srcdirectories(path = '.')
+    srcentries(path).select {|fname|
+      File.dir?(File.join(curr_srcdir(), path, fname))
+    }
+  end
+
+end
+
+
+class ToplevelInstaller
+
+  Version   = '3.4.1'
+  Copyright = 'Copyright (c) 2000-2005 Minero Aoki'
+
+  TASKS = [
+    [ 'all',      'do config, setup, then install' ],
+    [ 'config',   'saves your configurations' ],
+    [ 'show',     'shows current configuration' ],
+    [ 'setup',    'compiles ruby extentions and others' ],
+    [ 'install',  'installs files' ],
+    [ 'test',     'run all tests in test/' ],
+    [ 'clean',    "does `make clean' for each extention" ],
+    [ 'distclean',"does `make distclean' for each extention" ]
+  ]
+
+  def ToplevelInstaller.invoke
+    config = ConfigTable.new(load_rbconfig())
+    config.load_standard_entries
+    config.load_multipackage_entries if multipackage?
+    config.fixup
+    klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller)
+    klass.new(File.dirname($0), config).invoke
+  end
+
+  def ToplevelInstaller.multipackage?
+    File.dir?(File.dirname($0) + '/packages')
+  end
+
+  def ToplevelInstaller.load_rbconfig
+    if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg }
+      ARGV.delete(arg)
+      load File.expand_path(arg.split(/=/, 2)[1])
+      $".push 'rbconfig.rb'
+    else
+      require 'rbconfig'
+    end
+    ::Config::CONFIG
+  end
+
+  def initialize(ardir_root, config)
+    @ardir = File.expand_path(ardir_root)
+    @config = config
+    # cache
+    @valid_task_re = nil
+  end
+
+  def config(key)
+    @config[key]
+  end
+
+  def inspect
+    "#<#{self.class} #{__id__()}>"
+  end
+
+  def invoke
+    run_metaconfigs
+    case task = parsearg_global()
+    when nil, 'all'
+      parsearg_config
+      init_installers
+      exec_config
+      exec_setup
+      exec_install
+    else
+      case task
+      when 'config', 'test'
+        ;
+      when 'clean', 'distclean'
+        @config.load_savefile if File.exist?(@config.savefile)
+      else
+        @config.load_savefile
+      end
+      __send__ "parsearg_#{task}"
+      init_installers
+      __send__ "exec_#{task}"
+    end
+  end
+  
+  def run_metaconfigs
+    @config.load_script "#{@ardir}/metaconfig"
+  end
+
+  def init_installers
+    @installer = Installer.new(@config, @ardir, File.expand_path('.'))
+  end
+
+  #
+  # Hook Script API bases
+  #
+
+  def srcdir_root
+    @ardir
+  end
+
+  def objdir_root
+    '.'
+  end
+
+  def relpath
+    '.'
+  end
+
+  #
+  # Option Parsing
+  #
+
+  def parsearg_global
+    while arg = ARGV.shift
+      case arg
+      when /\A\w+\z/
+        setup_rb_error "invalid task: #{arg}" unless valid_task?(arg)
+        return arg
+      when '-q', '--quiet'
+        @config.verbose = false
+      when '--verbose'
+        @config.verbose = true
+      when '--help'
+        print_usage $stdout
+        exit 0
+      when '--version'
+        puts "#{File.basename($0)} version #{Version}"
+        exit 0
+      when '--copyright'
+        puts Copyright
+        exit 0
+      else
+        setup_rb_error "unknown global option '#{arg}'"
+      end
+    end
+    nil
+  end
+
+  def valid_task?(t)
+    valid_task_re() =~ t
+  end
+
+  def valid_task_re
+    @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/
+  end
+
+  def parsearg_no_options
+    unless ARGV.empty?
+      task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1)
+      setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}"
+    end
+  end
+
+  alias parsearg_show       parsearg_no_options
+  alias parsearg_setup      parsearg_no_options
+  alias parsearg_test       parsearg_no_options
+  alias parsearg_clean      parsearg_no_options
+  alias parsearg_distclean  parsearg_no_options
+
+  def parsearg_config
+    evalopt = []
+    set = []
+    @config.config_opt = []
+    while i = ARGV.shift
+      if /\A--?\z/ =~ i
+        @config.config_opt = ARGV.dup
+        break
+      end
+      name, value = *@config.parse_opt(i)
+      if @config.value_config?(name)
+        @config[name] = value
+      else
+        evalopt.push [name, value]
+      end
+      set.push name
+    end
+    evalopt.each do |name, value|
+      @config.lookup(name).evaluate value, @config
+    end
+    # Check if configuration is valid
+    set.each do |n|
+      @config[n] if @config.value_config?(n)
+    end
+  end
+
+  def parsearg_install
+    @config.no_harm = false
+    @config.install_prefix = ''
+    while a = ARGV.shift
+      case a
+      when '--no-harm'
+        @config.no_harm = true
+      when /\A--prefix=/
+        path = a.split(/=/, 2)[1]
+        path = File.expand_path(path) unless path[0,1] == '/'
+        @config.install_prefix = path
+      else
+        setup_rb_error "install: unknown option #{a}"
+      end
+    end
+  end
+
+  def print_usage(out)
+    out.puts 'Typical Installation Procedure:'
+    out.puts "  $ ruby #{File.basename $0} config"
+    out.puts "  $ ruby #{File.basename $0} setup"
+    out.puts "  # ruby #{File.basename $0} install (may require root privilege)"
+    out.puts
+    out.puts 'Detailed Usage:'
+    out.puts "  ruby #{File.basename $0} <global option>"
+    out.puts "  ruby #{File.basename $0} [<global options>] <task> [<task options>]"
+
+    fmt = "  %-24s %s\n"
+    out.puts
+    out.puts 'Global options:'
+    out.printf fmt, '-q,--quiet',   'suppress message outputs'
+    out.printf fmt, '   --verbose', 'output messages verbosely'
+    out.printf fmt, '   --help',    'print this message'
+    out.printf fmt, '   --version', 'print version and quit'
+    out.printf fmt, '   --copyright',  'print copyright and quit'
+    out.puts
+    out.puts 'Tasks:'
+    TASKS.each do |name, desc|
+      out.printf fmt, name, desc
+    end
+
+    fmt = "  %-24s %s [%s]\n"
+    out.puts
+    out.puts 'Options for CONFIG or ALL:'
+    @config.each do |item|
+      out.printf fmt, item.help_opt, item.description, item.help_default
+    end
+    out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's"
+    out.puts
+    out.puts 'Options for INSTALL:'
+    out.printf fmt, '--no-harm', 'only display what to do if given', 'off'
+    out.printf fmt, '--prefix=path',  'install path prefix', ''
+    out.puts
+  end
+
+  #
+  # Task Handlers
+  #
+
+  def exec_config
+    @installer.exec_config
+    @config.save   # must be final
+  end
+
+  def exec_setup
+    @installer.exec_setup
+  end
+
+  def exec_install
+    @installer.exec_install
+  end
+
+  def exec_test
+    @installer.exec_test
+  end
+
+  def exec_show
+    @config.each do |i|
+      printf "%-20s %s\n", i.name, i.value if i.value?
+    end
+  end
+
+  def exec_clean
+    @installer.exec_clean
+  end
+
+  def exec_distclean
+    @installer.exec_distclean
+  end
+
+end   # class ToplevelInstaller
+
+
+class ToplevelInstallerMulti < ToplevelInstaller
+
+  include FileOperations
+
+  def initialize(ardir_root, config)
+    super
+    @packages = directories_of("#{@ardir}/packages")
+    raise 'no package exists' if @packages.empty?
+    @root_installer = Installer.new(@config, @ardir, File.expand_path('.'))
+  end
+
+  def run_metaconfigs
+    @config.load_script "#{@ardir}/metaconfig", self
+    @packages.each do |name|
+      @config.load_script "#{@ardir}/packages/#{name}/metaconfig"
+    end
+  end
+
+  attr_reader :packages
+
+  def packages=(list)
+    raise 'package list is empty' if list.empty?
+    list.each do |name|
+      raise "directory packages/#{name} does not exist"\
+              unless File.dir?("#{@ardir}/packages/#{name}")
+    end
+    @packages = list
+  end
+
+  def init_installers
+    @installers = {}
+    @packages.each do |pack|
+      @installers[pack] = Installer.new(@config,
+                                       "#{@ardir}/packages/#{pack}",
+                                       "packages/#{pack}")
+    end
+    with    = extract_selection(config('with'))
+    without = extract_selection(config('without'))
+    @selected = @installers.keys.select {|name|
+                  (with.empty? or with.include?(name)) \
+                      and not without.include?(name)
+                }
+  end
+
+  def extract_selection(list)
+    a = list.split(/,/)
+    a.each do |name|
+      setup_rb_error "no such package: #{name}"  unless @installers.key?(name)
+    end
+    a
+  end
+
+  def print_usage(f)
+    super
+    f.puts 'Inluded packages:'
+    f.puts '  ' + @packages.sort.join(' ')
+    f.puts
+  end
+
+  #
+  # Task Handlers
+  #
+
+  def exec_config
+    run_hook 'pre-config'
+    each_selected_installers {|inst| inst.exec_config }
+    run_hook 'post-config'
+    @config.save   # must be final
+  end
+
+  def exec_setup
+    run_hook 'pre-setup'
+    each_selected_installers {|inst| inst.exec_setup }
+    run_hook 'post-setup'
+  end
+
+  def exec_install
+    run_hook 'pre-install'
+    each_selected_installers {|inst| inst.exec_install }
+    run_hook 'post-install'
+  end
+
+  def exec_test
+    run_hook 'pre-test'
+    each_selected_installers {|inst| inst.exec_test }
+    run_hook 'post-test'
+  end
+
+  def exec_clean
+    rm_f @config.savefile
+    run_hook 'pre-clean'
+    each_selected_installers {|inst| inst.exec_clean }
+    run_hook 'post-clean'
+  end
+
+  def exec_distclean
+    rm_f @config.savefile
+    run_hook 'pre-distclean'
+    each_selected_installers {|inst| inst.exec_distclean }
+    run_hook 'post-distclean'
+  end
+
+  #
+  # lib
+  #
+
+  def each_selected_installers
+    Dir.mkdir 'packages' unless File.dir?('packages')
+    @selected.each do |pack|
+      $stderr.puts "Processing the package `#{pack}' ..." if verbose?
+      Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}")
+      Dir.chdir "packages/#{pack}"
+      yield @installers[pack]
+      Dir.chdir '../..'
+    end
+  end
+
+  def run_hook(id)
+    @root_installer.run_hook id
+  end
+
+  # module FileOperations requires this
+  def verbose?
+    @config.verbose?
+  end
+
+  # module FileOperations requires this
+  def no_harm?
+    @config.no_harm?
+  end
+
+end   # class ToplevelInstallerMulti
+
+
+class Installer
+
+  FILETYPES = %w( bin lib ext data conf man )
+
+  include FileOperations
+  include HookScriptAPI
+
+  def initialize(config, srcroot, objroot)
+    @config = config
+    @srcdir = File.expand_path(srcroot)
+    @objdir = File.expand_path(objroot)
+    @currdir = '.'
+  end
+
+  def inspect
+    "#<#{self.class} #{File.basename(@srcdir)}>"
+  end
+
+  def noop(rel)
+  end
+
+  #
+  # Hook Script API base methods
+  #
+
+  def srcdir_root
+    @srcdir
+  end
+
+  def objdir_root
+    @objdir
+  end
+
+  def relpath
+    @currdir
+  end
+
+  #
+  # Config Access
+  #
+
+  # module FileOperations requires this
+  def verbose?
+    @config.verbose?
+  end
+
+  # module FileOperations requires this
+  def no_harm?
+    @config.no_harm?
+  end
+
+  def verbose_off
+    begin
+      save, @config.verbose = @config.verbose?, false
+      yield
+    ensure
+      @config.verbose = save
+    end
+  end
+
+  #
+  # TASK config
+  #
+
+  def exec_config
+    exec_task_traverse 'config'
+  end
+
+  alias config_dir_bin noop
+  alias config_dir_lib noop
+
+  def config_dir_ext(rel)
+    extconf if extdir?(curr_srcdir())
+  end
+
+  alias config_dir_data noop
+  alias config_dir_conf noop
+  alias config_dir_man noop
+
+  def extconf
+    ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt
+  end
+
+  #
+  # TASK setup
+  #
+
+  def exec_setup
+    exec_task_traverse 'setup'
+  end
+
+  def setup_dir_bin(rel)
+    files_of(curr_srcdir()).each do |fname|
+      update_shebang_line "#{curr_srcdir()}/#{fname}"
+    end
+  end
+
+  alias setup_dir_lib noop
+
+  def setup_dir_ext(rel)
+    make if extdir?(curr_srcdir())
+  end
+
+  alias setup_dir_data noop
+  alias setup_dir_conf noop
+  alias setup_dir_man noop
+
+  def update_shebang_line(path)
+    return if no_harm?
+    return if config('shebang') == 'never'
+    old = Shebang.load(path)
+    if old
+      $stderr.puts "warning: #{path}: Shebang line includes too many args.  It is not portable and your program may not work." if old.args.size > 1
+      new = new_shebang(old)
+      return if new.to_s == old.to_s
+    else
+      return unless config('shebang') == 'all'
+      new = Shebang.new(config('rubypath'))
+    end
+    $stderr.puts "updating shebang: #{File.basename(path)}" if verbose?
+    open_atomic_writer(path) {|output|
+      File.open(path, 'rb') {|f|
+        f.gets if old   # discard
+        output.puts new.to_s
+        output.print f.read
+      }
+    }
+  end
+
+  def new_shebang(old)
+    if /\Aruby/ =~ File.basename(old.cmd)
+      Shebang.new(config('rubypath'), old.args)
+    elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby'
+      Shebang.new(config('rubypath'), old.args[1..-1])
+    else
+      return old unless config('shebang') == 'all'
+      Shebang.new(config('rubypath'))
+    end
+  end
+
+  def open_atomic_writer(path, &block)
+    tmpfile = File.basename(path) + '.tmp'
+    begin
+      File.open(tmpfile, 'wb', &block)
+      File.rename tmpfile, File.basename(path)
+    ensure
+      File.unlink tmpfile if File.exist?(tmpfile)
+    end
+  end
+
+  class Shebang
+    def Shebang.load(path)
+      line = nil
+      File.open(path) {|f|
+        line = f.gets
+      }
+      return nil unless /\A#!/ =~ line
+      parse(line)
+    end
+
+    def Shebang.parse(line)
+      cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ')
+      new(cmd, args)
+    end
+
+    def initialize(cmd, args = [])
+      @cmd = cmd
+      @args = args
+    end
+
+    attr_reader :cmd
+    attr_reader :args
+
+    def to_s
+      "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}")
+    end
+  end
+
+  #
+  # TASK install
+  #
+
+  def exec_install
+    rm_f 'InstalledFiles'
+    exec_task_traverse 'install'
+  end
+
+  def install_dir_bin(rel)
+    install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755
+  end
+
+  def install_dir_lib(rel)
+    install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644
+  end
+
+  def install_dir_ext(rel)
+    return unless extdir?(curr_srcdir())
+    install_files rubyextentions('.'),
+                  "#{config('sodir')}/#{File.dirname(rel)}",
+                  0555
+  end
+
+  def install_dir_data(rel)
+    install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644
+  end
+
+  def install_dir_conf(rel)
+    # FIXME: should not remove current config files
+    # (rename previous file to .old/.org)
+    install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644
+  end
+
+  def install_dir_man(rel)
+    install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644
+  end
+
+  def install_files(list, dest, mode)
+    mkdir_p dest, @config.install_prefix
+    list.each do |fname|
+      install fname, dest, mode, @config.install_prefix
+    end
+  end
+
+  def libfiles
+    glob_reject(%w(*.y *.output), targetfiles())
+  end
+
+  def rubyextentions(dir)
+    ents = glob_select("*.#{@config.dllext}", targetfiles())
+    if ents.empty?
+      setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first"
+    end
+    ents
+  end
+
+  def targetfiles
+    mapdir(existfiles() - hookfiles())
+  end
+
+  def mapdir(ents)
+    ents.map {|ent|
+      if File.exist?(ent)
+      then ent                         # objdir
+      else "#{curr_srcdir()}/#{ent}"   # srcdir
+      end
+    }
+  end
+
+  # picked up many entries from cvs-1.11.1/src/ignore.c
+  JUNK_FILES = %w( 
+    core RCSLOG tags TAGS .make.state
+    .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb
+    *~ *.old *.bak *.BAK *.orig *.rej _$* *$
+
+    *.org *.in .*
+  )
+
+  def existfiles
+    glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.')))
+  end
+
+  def hookfiles
+    %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt|
+      %w( config setup install clean ).map {|t| sprintf(fmt, t) }
+    }.flatten
+  end
+
+  def glob_select(pat, ents)
+    re = globs2re([pat])
+    ents.select {|ent| re =~ ent }
+  end
+
+  def glob_reject(pats, ents)
+    re = globs2re(pats)
+    ents.reject {|ent| re =~ ent }
+  end
+
+  GLOB2REGEX = {
+    '.' => '\.',
+    '$' => '\$',
+    '#' => '\#',
+    '*' => '.*'
+  }
+
+  def globs2re(pats)
+    /\A(?:#{
+      pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|')
+    })\z/
+  end
+
+  #
+  # TASK test
+  #
+
+  TESTDIR = 'test'
+
+  def exec_test
+    unless File.directory?('test')
+      $stderr.puts 'no test in this package' if verbose?
+      return
+    end
+    $stderr.puts 'Running tests...' if verbose?
+    begin
+      require 'test/unit'
+    rescue LoadError
+      setup_rb_error 'test/unit cannot loaded.  You need Ruby 1.8 or later to invoke this task.'
+    end
+    runner = Test::Unit::AutoRunner.new(true)
+    runner.to_run << TESTDIR
+    runner.run
+  end
+
+  #
+  # TASK clean
+  #
+
+  def exec_clean
+    exec_task_traverse 'clean'
+    rm_f @config.savefile
+    rm_f 'InstalledFiles'
+  end
+
+  alias clean_dir_bin noop
+  alias clean_dir_lib noop
+  alias clean_dir_data noop
+  alias clean_dir_conf noop
+  alias clean_dir_man noop
+
+  def clean_dir_ext(rel)
+    return unless extdir?(curr_srcdir())
+    make 'clean' if File.file?('Makefile')
+  end
+
+  #
+  # TASK distclean
+  #
+
+  def exec_distclean
+    exec_task_traverse 'distclean'
+    rm_f @config.savefile
+    rm_f 'InstalledFiles'
+  end
+
+  alias distclean_dir_bin noop
+  alias distclean_dir_lib noop
+
+  def distclean_dir_ext(rel)
+    return unless extdir?(curr_srcdir())
+    make 'distclean' if File.file?('Makefile')
+  end
+
+  alias distclean_dir_data noop
+  alias distclean_dir_conf noop
+  alias distclean_dir_man noop
+
+  #
+  # Traversing
+  #
+
+  def exec_task_traverse(task)
+    run_hook "pre-#{task}"
+    FILETYPES.each do |type|
+      if type == 'ext' and config('without-ext') == 'yes'
+        $stderr.puts 'skipping ext/* by user option' if verbose?
+        next
+      end
+      traverse task, type, "#{task}_dir_#{type}"
+    end
+    run_hook "post-#{task}"
+  end
+
+  def traverse(task, rel, mid)
+    dive_into(rel) {
+      run_hook "pre-#{task}"
+      __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '')
+      directories_of(curr_srcdir()).each do |d|
+        traverse task, "#{rel}/#{d}", mid
+      end
+      run_hook "post-#{task}"
+    }
+  end
+
+  def dive_into(rel)
+    return unless File.dir?("#{@srcdir}/#{rel}")
+
+    dir = File.basename(rel)
+    Dir.mkdir dir unless File.dir?(dir)
+    prevdir = Dir.pwd
+    Dir.chdir dir
+    $stderr.puts '---> ' + rel if verbose?
+    @currdir = rel
+    yield
+    Dir.chdir prevdir
+    $stderr.puts '<--- ' + rel if verbose?
+    @currdir = File.dirname(rel)
+  end
+
+  def run_hook(id)
+    path = [ "#{curr_srcdir()}/#{id}",
+             "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) }
+    return unless path
+    begin
+      instance_eval File.read(path), path, 1
+    rescue
+      raise if $DEBUG
+      setup_rb_error "hook #{path} failed:\n" + $!.message
+    end
+  end
+
+end   # class Installer
+
+
+class SetupError < StandardError; end
+
+def setup_rb_error(msg)
+  raise SetupError, msg
+end
+
+if $0 == __FILE__
+  begin
+    ToplevelInstaller.invoke
+  rescue SetupError
+    raise if $DEBUG
+    $stderr.puts $!.message
+    $stderr.puts "Try 'ruby #{$0} --help' for detailed usage."
+    exit 1
+  end
+end
diff --git a/test.rb b/test.rb
deleted file mode 100755
index f38ed14..0000000
--- a/test.rb
+++ /dev/null
@@ -1,441 +0,0 @@
-#!/usr/bin/ruby -Ku
-
-$KCODE = 'UTF8'
-require 'jcode'
-require 'yaml'
-require 'active_ldap'
-require 'cmdparse2'
-require 'pathname'
-
-class Hash
-  def recursive_symbolize_keys!
-    symbolize_keys!
-    values.select { |v| v.is_a?(Hash) }.each { |h| h.recursive_symbolize_keys! }
-    self
-  end
-end
-
-config_str_prv = IO.read("config/private.conf")
-config_str = IO.read("config/test.conf")
-config = YAML.load(config_str_prv).merge(YAML.load(config_str)).recursive_symbolize_keys!
-ActiveLdap::Base.setup_connection(config[:ldap])
-
-cmdparser = CmdParse::CommandParser.new(true)
-cmdparser.program_name = ""
-cmdparser.program_version = [0, 0, 1]
-
-cmdparser.options = CmdParse::OptionParserWrapper.new do |opt|
-  opt.separator "Global options:"
-  opt.on("--debug", "Output debug info without being formated") {|t| $debug_opt = true }
-  opt.on("--expert", "Output extra info for expert users") {|t| $expert_opt = true }
-  opt.on("--handles", "Output with handles (objects/field/... keys used for manipulations)") {|t| $handles_opt = true }
-end
-
-cmdparser.add_command(CmdParse::HelpCommand.new)
-cmdparser.add_command(CmdParse::VersionCommand.new)
-
-
-module ActiveLdap
-  class DistinguishedName
-    def shift
-      @rdns.shift
-    end
-  end
-end
-
-module LdapMapper
-  class LdapObject < ActiveLdap::Base
-    class_inheritable_accessor :presentation, :mapper
-
-    ldap_mapping :prefix => '', :classes => ['top'], :scope => :sub
-
-    # temporary method until active_ldap is fixed: return a DN object (see #23932)
-    def dn_obj
-      ActiveLdap::DistinguishedName.parse(self.dn)
-    end
-
-    def name
-      name = self[dn_attribute].is_a?(Array) ? self[dn_attribute][0] : self[dn_attribute]
-      name.strip
-    end
-
-    def human_name
-      [self.class.presentation[:name_attribute], 'displayName', 'cn'].each do |attr|
-        if self.has_attribute?(attr) and self.attribute_present?(attr)
-          name = self[attr].is_a?(Array) ? self[attr][0] : self[attr]
-          return name.strip
-        end
-      end
-      return ""
-    end
-
-    def description
-      [self.class.presentation[:desc_attribute], 'description'].each do |attr|
-        if self.has_attribute?(attr) and self.attribute_present?(attr)
-          return self[attr].is_a?(Array) ? self[attr][0] : self[attr]
-        end
-      end
-      return ""
-    end
-
-    def possible_relations
-      self.associations.collect {|assoc| assoc.to_s } - ['children']
-    end
-
-    def relations
-      rel_list = []
-
-      rel_list += self.class.presentation[:associated_relations] if self.class.presentation[:associated_relations]
-
-      aspects.each do |aspect|
-        aspect_data = self.class.mapper.get_aspect(aspect)
-        if defined?(aspect_data[:presentation][:associated_relations]) and aspect_data[:presentation][:associated_relations]
-          rel_list += aspect_data[:presentation][:associated_relations]
-        end
-      end
-
-      rel_list & possible_relations
-    end
-
-    def aspects
-      present_aspects = []
-      (self.class.presentation[:allowed_aspects] || []).each do |aspect|
-        aspect_data = self.class.mapper.get_aspect(aspect)
-        aspect_mapping = aspect_data[:mapping]
-        present_aspects << aspect if self.classes & aspect_mapping[:classes] == aspect_mapping[:classes]
-      end
-
-      present_aspects
-    end
-
-    def objectclasses_attr_list(objectclass_list)
-      objectclass_list = [objectclass_list] unless objectclass_list.is_a? Array
-      list = []
-      objectclass_list.each do |objectclass|
-        objectclass_obj = ActiveLdap::Base.schema.object_class(objectclass)
-        attr_list = objectclass_obj.must + objectclass_obj.may
-        list += attr_list.collect{|attr| attr.human_attribute_name }
-      end
-      list
-    end
-
-    def organized_data(options = {})
-      options.symbolize_keys!
-      options[:expert] ||= false;
-      options[:skip_binary] ||= false;
-
-      ignored_attrs = self.class.presentation[:hidden_attributes] || []
-      ignored_attrs += (self.class.presentation[:expert_attributes] || []) unless options[:expert]
-      attr_list = self.attributes.keys - ignored_attrs
-
-      aspects = self.aspects
-      rel_list = self.possible_relations
-
-      # first pass to take aspects forced relations into account
-      obj_aspects = {}
-      aspects.each do |aspect|
-        aspect_data = self.class.mapper.get_aspect(aspect)
-
-        if defined?(aspect_data[:presentation][:associated_attributes]) and aspect_data[:presentation][:associated_attributes]
-          taken_attr_list = aspect_data[:presentation][:associated_attributes] & (attr_list + ignored_attrs)
-          unless taken_attr_list.empty?
-            obj_aspects[aspect] ||= {}
-            obj_aspects[aspect].merge!(fetch_attributes_data(taken_attr_list, options))
-            attr_list -= taken_attr_list
-          end
-        end
-
-        if defined?(aspect_data[:presentation][:associated_relations]) and aspect_data[:presentation][:associated_relations]
-          taken_rel_list = aspect_data[:presentation][:associated_relations] & rel_list
-          unless taken_rel_list.empty?
-            obj_aspects[aspect] ||= {}
-            obj_aspects[aspect].merge!(fetch_relations_data(taken_rel_list, options))
-            rel_list -= taken_rel_list
-          end
-        end
-      end
-
-      # manage general attributes
-      obj_info = {}
-      if self.class.presentation[:associate_unclaimed_attributes]
-        taken_attr_list = attr_list
-      else
-        taken_attr_list = []
-        if self.class.presentation.has_key?(:associated_attributes)
-          taken_attr_list += self.class.presentation[:associated_attributes]
-        end
-        taken_attr_list += objectclasses_attr_list(self.required_classes + (self.class.presentation[:optional_classes] || []))
-      end
-      taken_attr_list = taken_attr_list.uniq & attr_list
-      obj_info = fetch_attributes_data(taken_attr_list, options)
-      attr_list -= taken_attr_list
-
-      # manage general relations
-      if self.class.presentation[:associated_relations]
-        taken_rel_list = self.class.presentation[:associated_relations] & rel_list
-        unless taken_rel_list.empty?
-          obj_info.merge!(fetch_relations_data(taken_rel_list, options))
-          rel_list -= taken_rel_list
-        end
-      end
-
-      # second pass to dispath the remaining attributes
-      unless attr_list.empty?
-        aspects.each do |aspect|
-          aspect_data = self.class.mapper.get_aspect(aspect)
-
-          taken_attr_list = (objectclasses_attr_list(aspect_data[:mapping][:classes]) & attr_list)
-          obj_aspects[aspect] ||= {}
-          obj_aspects[aspect].merge!(fetch_attributes_data(taken_attr_list, options))
-          attr_list -= taken_attr_list
-
-          break if attr_list.empty?
-        end
-      end
-
-      [obj_info, obj_aspects]
-    end
-
-    def family_parent_dn
-      pdn = self.dn_obj.dup
-      pdn.shift
-      pdn
-    end
-
-    def family_children
-      LdapObject.find(:all, :base => self.dn, :scope => :one)
-    end
-
-    def family_children_dn
-      self.family_children.collect {|obj| obj.dn }
-    end
-
-    def family_siblings
-      # cannot substract, as the ruby object signature may be different
-      LdapObject.find(:all, :base => self.family_parent_dn.to_s, :scope => :one).select{|obj| obj.dn != self.dn }
-    end
-
-    def family_siblings_dn
-      self.family_siblings.collect {|obj| obj.dn }
-    end
-
-    protected
-
-    def fetch_attributes_data(attr_list, options = {})
-      attr_data = self.attributes.select {|key, val| attr_list.include?(key) and not (options[:skip_binary] and ActiveLdap::Base.schema.attribute(key).binary?) }
-      Hash[attr_data]
-    end
-
-    def fetch_relations_data(rel_list, options = {})
-      rel_data = rel_list.collect do |rel|
-        data = self.send(rel).collect{|g| g.name }
-        data.empty? ? nil : ["rel:" + rel, data]
-      end
-      Hash[rel_data.compact]
-    end
-  end
-
-  class Controller
-    def initialize(mod_container = LdapMapper::Objects)
-      @mod_container = mod_container
-      @object_definitions = {}
-      @aspects = {}
-    end
-
-    def set_aspect(aspect_name, aspect_def)
-      @aspects[aspect_name] = aspect_def
-    end
-
-    def get_aspect(aspect_name)
-      @aspects[aspect_name.to_sym]
-    end
-
-    def self.object_name_to_klass_name(obj_name)
-      "LdapObject" + obj_name.to_s.capitalize
-    end
-
-    def load_object(obj_name, obj_def)
-      obj_mapping = obj_def[:mapping]
-      klass_name = self.class.object_name_to_klass_name(obj_name)
-
-      # create class
-      @mod_container.module_eval(<<-EOS)
-        class #{klass_name} < LdapMapper::LdapObject; end
-      EOS
-
-      # configure class
-      klass = find_klass(obj_name)
-      klass.presentation = obj_def[:presentation]
-      klass.mapper = self
-      klass.ldap_mapping obj_mapping.reject {|key, val| not ActiveLdap::Base::VALID_LDAP_MAPPING_OPTIONS.include?(key) }
-
-      # store definition for later relations processing
-      @object_definitions[obj_name] = obj_def
-    end
-
-    def find_klass(obj_name)
-      klass_name = self.class.object_name_to_klass_name(obj_name)
-      return nil unless @mod_container.const_defined?(klass_name)
-      @mod_container.const_get(klass_name)
-    end
-
-    # run it _once_ when all objects are loaded
-    def load_relations
-      @object_definitions.each_pair do |obj_name, obj_def|
-        obj_rel = {}
-        obj_rel.merge!(obj_def[:relations]) if obj_def.include?(:relations)
-        if obj_def[:presentation].has_key?(:allowed_aspects)
-          obj_def[:presentation][:allowed_aspects].each do |rel|
-            rel_data = get_aspect(rel)
-            obj_rel.merge!(rel_data[:relations]) if rel_data.has_key?(:relations) and rel_data[:relations]
-          end
-        end
-        next if obj_rel.empty?
-
-        klass = find_klass(obj_name)
-
-        obj_rel.each_pair do |field_name, rel|
-          foreign_klass = find_klass(rel[:object])
-          rel[:class_name] = foreign_klass.to_s
-
-          case rel[:type]
-          when :belongs_to
-            klass.belongs_to field_name, rel.reject {|key, val| not ActiveLdap::Associations::ClassMethods::VALID_BELONGS_TO_OPTIONS.include?(key) }
-          when :has_many
-            klass.has_many field_name, rel.reject {|key, val| not ActiveLdap::Associations::ClassMethods::VALID_HAS_MANY_OPTIONS.include?(key) }
-          else
-            raise "bug in '#{obj_name}' object relations (wrong type)"
-          end
-        end
-      end
-    end
-  end
-
-  # default location for mapped objects
-  module Objects
-  end
-end
-
-
-I18n.load_path += Dir[File.join(Pathname.new(".").realpath, "locale", "*.yml")]
-I18n.default_locale = :en
-
-ldapctl = LdapMapper::Controller.new
-config[:aspects].each_pair do |aspect_name, aspect_data|
-  ldapctl.set_aspect(aspect_name, aspect_data)
-end
-config[:objects].each_pair do |obj_name, obj_data|
-  ldapctl.load_object(obj_name, obj_data)
-end
-ldapctl.load_relations
-
-cmd = CmdParse::Command.new('list', false)
-cmd.short_desc = "list objects"
-cmd.set_execution_block do |args|
-  if args.size != 1
-    STDERR.puts "syntax error: no object name given"
-    exit 1
-  end
-
-  obj_hdl = args[0]
-  obj_klass = ldapctl.find_klass(obj_hdl)
-  if obj_klass.nil?
-    STDERR.puts "No such object '#{obj_hdl}'."
-    exit 2
-  end
-
-  obj_human_name = I18n.t(obj_hdl, :scope => 'objects', :default => "Object '#{obj_hdl}'")
-  puts "=== List of #{obj_human_name.pluralize} ==="
-  obj_klass.find(:all).each do |obj|
-    str = obj.human_name
-    str += " [#{obj.name}]" if $handles_opt
-    str += ": #{obj.description}" unless obj.description.empty?
-    puts str
-  end
-end
-cmdparser.add_command(cmd)
-
-def translate_data_key(name)
-  if name.index(":")
-    type, key = name.split(":")
-    case type
-    when 'rel'
-      I18n.t(key, :scope => 'relations', :default => name)
-    else
-      raise "Cannot translate unknown data key type"
-    end
-  else
-    att = ActiveLdap::Base.schema.attribute(name)
-    I18n.t(att.human_attribute_name, :scope => 'attribute_types', :default => att.human_attribute_description)
-  end
-end
-
-def display_data(attr_data)
-  attr_data.each_pair do |key, val|
-    field_name = translate_data_key(key)
-    field_name += " [#{key}]" if $handles_opt
-    puts field_name + ": " + (val.is_a?(Array) ? val.sort.collect{|v| v.to_s }.join(", ") : val.to_s)
-  end
-end
-
-cmd = CmdParse::Command.new('show', false)
-cmd.short_desc = "show object information"
-cmd.set_execution_block do |args|
-  if args.size < 1
-    STDERR.puts "syntax error: no object name given"
-    exit 1
-  end
-  if args.size < 2
-    STDERR.puts "syntax error: no item name given"
-    exit 1
-  end
-
-  obj_hdl = args[0]
-  obj_klass = ldapctl.find_klass(obj_hdl)
-  if obj_klass.nil?
-    STDERR.puts "No such object '#{obj_hdl}'."
-    exit 2
-  end
-
-  item_hdl = args[1]
-  begin
-    item = obj_klass.find(item_hdl)
-  rescue ActiveLdap::EntryNotFound
-    STDERR.puts "No such item '#{obj_hdl}/#{item_hdl}'"
-    exit 2
-  end
-
-  obj_human_name = I18n.t(obj_hdl, :scope => 'objects', :default => "Object '#{obj_hdl}'")
-  name = item.human_name
-  name += " [#{item.name}]" if $handles_opt
-  puts "=== #{obj_human_name}: #{name} ==="
-
-  if $debug_opt
-    puts item.to_s
-    puts "--- Detected Info ---"
-    puts "aspects: " + item.aspects.sort.join(", ")
-
-    puts "--- Family ---"
-    puts "parent: " + item.family_parent_dn.to_s
-    puts "siblings: " + item.family_siblings_dn.join(", ")
-    puts "children: " + item.family_children_dn.join(", ")
-
-    puts "--- Relations ---"
-    item.relations.each do |rel|
-      puts "#{rel}: " + item.send(rel).collect{|g| g.name }.join(", ")
-    end
-  else
-    obj_info, obj_aspects = item.organized_data(:expert => $expert_opt, :skip_binary => true)
-
-    display_data(obj_info)
-
-    obj_aspects.each_pair do |aspect_name, aspect_data|
-      aspect_display_name = I18n.t(aspect_name, :scope => 'aspects', :default => "Aspect '#{aspect_name}'")
-      puts "--- #{aspect_display_name} ---"
-      display_data(aspect_data)
-    end
-  end
-end
-cmdparser.add_command(cmd)
-
-cmdparser.parse
