On Mon, 15 Oct 2001, Sean Russell wrote:

> Hmmm.  Well, Ranges are lazily evaluated; they are not converted internally 
> to arrays, and I don't see any reason why they open-ended ranges wouldn't 
> be possible -- EXCEPT for the to_a issue, which is indeed a stumper.  But, 
> consider, for a moment, a number class tha wraps Integer, which provides 
> constants INFINITY and NEGATIVE_INFINITY, and which supports succ and <=> 
> such that x <=> MyInt::INFINITY is always -1.  You could easily make a 
> range of this, although to_a would hang.
> 
Ok, I agree that its possible (most things are! :-)) but then someone will
do (1..).each and keep on waiting (granted there are always many things
you can do to screw up) 'til mem runs out... ;-)
 
> >> PS.  I was following the "versioned require" discussion for a while, but
> >> it
> ....
> > Many people seem to think that something along those lines would be a
> > good thing. There is not yet a consensus on which semantics to use and how
> > to implement. Maybe the RAA.succ/RubyGems discussion/solution can revive
> > this discussion again?
> 
> I've seen at least one extension which enabled this, but this is the sort 
> of thing that is almost useless unless it is ubiquitous.  IE, the NEED for 
> this is usually in moving software around from one machine to another, so 
> if it isn't part of core Ruby, its usefullness is extremely limited.
> 
I agree and I think Ryan Leavengoods upcoming RubyGems and the outcome
from the Raa.succ DesignFest at RubyConf might make starting points for a
standard way. So something is probably coming along this year...

However, there is one way to do this that would work with only minimal
support from the core/std Ruby: the gems/archives are simply
Marshal.dump'ed objects with a simple Ruby loader. This way people can
introduce custom gems as long as they respond to some standardized
messages. I actually did a tracer bullet along those lines. The gems it
produces has three parts:

1. Minimal Ruby program that unpacks string in 2 and eval's it and then
   loads object in 3 and sends it ARGV
2. Possibly empty string with source code that defines the object in 3 (or
the differences between the object in 3 and a std Gem class but there is a
trade off here)
3. Marshal.dump'ed Gem object that can list, unpack, install itself etc.

Code is below for interested souls... Sorry for the length. (Note that it
seems kind of strange to add the source code in a string (2) that is then
eval'ed instead of including it in the loader directly; my thought with
this was that it might be compressed...)

A potential quirk with this scheme is if the Marshal format changes
between Ruby versions. I don't think it has changed since 1.6 though, but
I'm not sure...

Regards,

Robert

gem.rb
------
require 'ftools'

def loader_source_code(src_code_length, length)
  if src_code_length > 0
    eval_src = <<EOS
  f.seek(-#{length+src_code_length}, IO::SEEK_END)
  eval(f.read(src_code_length))
EOS
  else
    eval_src = ""
  end
  str  = <<EOS
require 'gem'
File.open(__FILE__) do |f|
#{eval_src}  f.seek(-#{length}, IO::SEEK_END)
  Gem.load(f.read(#{length})).call(ARGV)
end
__END__
EOS
  str
end

class Gem
  # Most of the attributes modeled after Debian Package system, see
  # http://www.debian.org/doc/FAQ/ch-pkg_basics.html
  #
  attr_accessor :name, :version, :category, :ruby_dependency, :maintainer
  attr_accessor :maintainer_email, :architecture, :dependencies
  attr_accessor :description

  # Possible additions:
  #   Status: planning/pre-alpha/alpha/beta/stable/mature
  #   Type: pure-ruby/C 

  def initialize(name, aHash = {})
    @name, @archive = name, FileArchive.new
    set_from_hash(aHash)
  end

  def set_from_hash(aHash)
    @version = aHash[:version] || "0.1"
    @category = aHash[:category] || ""
    @ruby_dependency = 
      aHash[:ruby_dependency] || aHash[:ruby] || Dependency.new("ruby",
"1.6")
    @maintainer = aHash[:maintainer] || "unknown"
    @maintainer_email = aHash[:maintainer_email] || "unknown"
    @architecture = aHash[:architecture] || aHash[:arch] || "any"
    @dependencies = aHash[:dependencies] || aHash[:deps] || []
    @description = aHash[:description] || aHash[:desc] || ""
  end

  def inspect
    "Name:".ljust(20)            + name                    + "\n" +
    "Version:".ljust(20)         + version                 + "\n" +
    "Category:".ljust(20)        + category                 + "\n" +
    "Ruby dependency:".ljust(20) + ruby_dependency.inspect + "\n" +
    "Maintainer:".ljust(20)      + maintainer +
" <#{maintainer_email}>\n" +
    "Architecture:".ljust(20)    + architecture            + "\n" +
    "Dependencies:".ljust(20)    + dependencies.inspect    + "\n" +
    "Description:\n" +
      format_description(description)
  end

  # Subclasses should add their source code here so that they can be
  # unpacked even if the Gem user does not have pre-installed their source 
  # code.
  def non_standard_ruby_source_code
    "" + @archive.non_standard_ruby_source_code
  end

  def add_file(filename)
    @archive.add_file(filename)
  end

  def marshal
    marshaled_gem = Marshal.dump(self)
    non_std_src = non_standard_ruby_source_code
    loader_source_code(non_std_src.length, marshaled_gem.length) + 
      non_std_src + marshaled_gem
  end

  def Gem.load(str)
    Marshal.load(str)
  end

  def usage
    usage_message(name, version)
  end

  def install
    unpack
    puts "Installing..."
    install_files
  end

  def unpack
    puts "Unpacking..."
    unpack_files
  end

  def list
    puts "Files in gem:"
    if @archive.num_files > 0
      @archive.each {|file, size, str| puts " #{file} (#{size})"}
    else
      puts " None."
    end
  end

  def unpack_files
    @archive.each do |filename, size, str|
      puts "  #{filename}"
      File.makedirs(File.dirname(filename))
      File.open(filename, "w") {|f| f.write str}
    end
  end

  def install_files
    raise NotImplementedError, "Install not yet implemented!"
  end

  def clean
    files.each do |filename, len, str|
      File.delete(filename)
    end
    # Also delete subdirs we have created? Maybe not...
  end

  def call(argv)
    argv.push nil if argv.length == 0
    while argv.length > 0
      case argv[0]
      when "--install", "-i"
	std_command(:install)
      when "--unpack", "-u"
	std_command(:unpack)
      when "--list", "-l"
	std_command(:list)
      when "--clean", "-c"
	std_command(:clean)
      else
	print "Invalid option!\n\n" if argv[0]
	puts usage
	puts "Gem info"
	puts "--------"
	puts inspect
	exit
      end
      argv.shift
    end
  end

  private

  def std_command(command, *args)
    puts inspect
    self.send(command, *args)
  end

  def usage_message(name, version)
    str = <<EOS
Gem: #{name} (#{version})
  -i, --install     Unpack and install files in gem
  -u, --unpack      Unpack files in gem
  -l, --list        List files in gem
  -c, --clean       Delete the files unpacked from the gem
EOS
    str
  end

  def indent(aString, steps = 1)
    spaces = " " * steps
    spaces + aString.split("\n").join(spaces + "\n")
  end

  def format_description(aString)
    indent(aString)
  end
end

# Debian has many different "dependencies" not only the must-have one
# I represent below. Maybe call them Relations and have subclasses for
# depends (must-have), recommends, suggests, conflicts, replaces,
provides?
class Dependency
  include Comparable

  attr_reader :name, :version

  def initialize(name, version = "0.0")
    @name, @version = name, version
  end

  def <=>(other)
    if name == other.name
      version <=> other.version
    else
      name <=> other.name
    end
  end

  def inspect_version
    version == "0.0" ? "" : " (#{version})"
  end

  def inspect
    "#{name}#{inspect_version}"
  end
end

def dep(*args)
  Dependency.new(*args)
end

class Stream
  def initialize
    @data = ""
  end

  def <<(aString)
    @data << aString
  end

  def length
    @data.length
  end

  def [](position, length)
    @data[position, length]
  end

  def delete_bytes(position, length)
    self[position, length] = ""
    self
  end

  # Subclasses should add their source code here so that their streams can
be
  # unpacked even if the Gem user does not have installed the stream
  def non_standard_ruby_source_code
    ""  # This stream is supposed to be included with Ruby so everyone has
it!
  end
end

class FileArchive
  attr_reader :info

  def initialize
    @info, @stream = Hash.new, Stream.new
  end

  def add_file(filename)
    File.open(filename, "r") do |f|
      delete_content(filename) if include?(filename)
      content = f.read
      info[filename] = [stream.length, content.length]
      stream << content
    end
  end

  def num_files
    @info.length
  end

  def each
    @info.each do |filename, (position, length)|
      yield(filename, length, @stream[position, length])
    end
  end

  def get_file(filename)
    stream[*info[filename]]
  end

  def include?(filename)
    info.include?(filename)
  end

  def non_standard_ruby_source_code
    # This archiver is supposed to be included with Ruby so everyone has
it!
    "" + stream.non_standard_ruby_source_code
  end

  private

  attr_reader :stream

  def delete_content(filename)
    position, length = info[filename]
    stream.delete_bytes(position, length)
    delete_info(filename, position, length)
  end

  def delete_info(filename, position, length)
    info.each do |name, (pos, len)|
      info[name] = [pos-length, len] if pos > position
    end
    info.delete(name)
  end
end

if __FILE__ == $0
  g = Gem.new("CryptoLib", 
	      :ruby => dep("ruby", "1.7.1"),
	      :maintainer => "Robert Feldt",
	      :maintainer_email => "feldt / ce.chalmers.se",
	      :category => "algorithms/cryptography",
	      :deps => [dep("zlib"), dep("optparse", "0.4")],
	      :desc => "Cryptography algorithms implemented in pure Ruby
(ie. *NO* C code).")
  puts g.inspect  

  ARGV[1..-1].each do |filename|
    g.add_file(filename)
  end

  if ARGV[0]
    File.open(ARGV[0] + ".gem", "w") {|f| f.write g.marshal}
  end
end