From bbaf6bb51edf0402faad07191cc042d5297ed2fc Mon Sep 17 00:00:00 2001 From: zedshaw Date: Mon, 6 Mar 2006 05:31:39 +0000 Subject: [PATCH] Implements the new gem based plugins as a separate projects/gem_plugin. Sets up new rake tasks to support testing gems easier (won't work on win32 yet). Uses the plugin system in mongrel_rails (win32 coming soon). git-svn-id: svn+ssh://rubyforge.org/var/svn/mongrel/trunk@83 19e92222-5c0b-0410-8929-a290d50e31e9 --- README | 54 +- Rakefile | 12 +- bin/mongrel_rails | 12 +- doc/site/src/default.template | 22 +- doc/site/src/news.page | 32 + lib/mongrel/command.rb | 6 +- lib/mongrel/plugins.rb | 177 +-- lib/mongrel/rails.rb | 2 +- projects/gem_plugin/COPYING | 504 ++++++ projects/gem_plugin/LICENSE | 58 + projects/gem_plugin/README | 115 ++ projects/gem_plugin/Rakefile | 35 + projects/gem_plugin/lib/gem_plugin.rb | 204 +++ projects/gem_plugin/setup.rb | 1360 +++++++++++++++++ .../gem_plugin/test}/test_plugins.rb | 38 +- projects/gem_plugin/tools/rakehelp.rb | 111 ++ 16 files changed, 2509 insertions(+), 233 deletions(-) create mode 100644 projects/gem_plugin/COPYING create mode 100644 projects/gem_plugin/LICENSE create mode 100644 projects/gem_plugin/README create mode 100644 projects/gem_plugin/Rakefile create mode 100644 projects/gem_plugin/lib/gem_plugin.rb create mode 100644 projects/gem_plugin/setup.rb rename {test => projects/gem_plugin/test}/test_plugins.rb (55%) create mode 100644 projects/gem_plugin/tools/rakehelp.rb diff --git a/README b/README index a86f0315..eeeb5d85 100644 --- a/README +++ b/README @@ -11,11 +11,27 @@ scream without too many portability issues. == Status -The 0.3.6 release supports Ruby On Rails much better than previously, and also -sports the beginning of a command and plugin infrastructure. There is now a more -complete CGIWrapper that handles most of the CGI usage, but still doesn't do the -MIME decoding or file upload/send (it leaves that to CGI). Finally, there's a -great mongrel_rails_service script for running under Win32 as a service. +Mongrel 0.3.9 now supports a fancy RubyGems based plugin system called GemPlugin. +It uses the basic machinery of RubyGems to implement dynamically loaded plugins +based on dependencies. Writing a plugin is pretty easy, but right now it's +not as well documented as it should be. There is a simple example plugin +for adding a status command to your mongrel. Just do: + + > gem install mongrel_status + +And you'll then get a new status command. Then just do: + + > cd myrailsapp + > mongrel_rails start -d + > mongrel_rails status + +And it'll print out the PID your Rails app is running under. + +The GemPlugin project is a sub-project of Mongrel, but it's licensed under +the Ruby license and is usable outside Mongrel + + +== Quick Start After you've installed (either with gem install mongrel or via source) you should have the mongrel_rails command available in your PATH. Then you just do the following: @@ -58,7 +74,11 @@ and do: -r c:\my\path\to\myapp -p 4000 -e production $ mongrel_rails_service start -n myapp -Now hit the port and poof, works. *Stopping the service is a little problematic right now.* +Now hit the port and poof, works. + +Stopping a service is simple: + + $ mongrel_rails_service stop -n myapp If you run into an app that's not running right, my suggestion is to run it with the regular mongrel_rails runner: @@ -68,6 +88,10 @@ the regular mongrel_rails runner: Since that will spit out error messages and stuff to the console. *Use CTRL-Pause/Break to stop.* +Best thing about the win32 support is that you can simply use the Windows Services +in Control Panel->Admin Tools to work with it. You can also install the same +Rails app as different installs. For example I've got myapp_dev, and myapp_prod and +just start/stop which one I want to work with. == Install @@ -131,24 +155,6 @@ create a matching database connection for each processor thread. More on this in future releases. -== The Future - -With the core of Mongrel completed I'm now turning to the next set of features -to make Mongrel useful for hosting web applications in a heavily utilized -production environment. Right now I'm looking at: - -* An idea I've had for an insane caching handler which could speed up quite a -few deployments. - -Overall though the goal of Mongrel is to be just enough HTTP to serve a Ruby -web application that sits behind a more complete web server. Everything -in the next will focus on actually hosting the major web frameworks for Ruby: - -* Camping -- because it's already done (thanks Why). -* Ruby on Rails -- that's where my bread is buttered right now. -* Nitro -- Nitro folks have already hooked this up and started using it. Nice. -* ????? -- Others people might be interested in. - == Contact E-mail zedshaw at zedshaw.com and I'll help. Comments about the API are welcome. diff --git a/Rakefile b/Rakefile index d2ef2128..cfde780c 100644 --- a/Rakefile +++ b/Rakefile @@ -26,11 +26,12 @@ end task :site do sh %{pushd doc/site; webgen; scp -r output/* #{ENV['SSH_USER']}@rubyforge.org:/var/www/gforge-projects/mongrel/; popd } sh %{ scp -r doc/rdoc/* #{ENV['SSH_USER']}@rubyforge.org:/var/www/gforge-projects/mongrel/rdoc/ } + sh %{ cd projects/gem_plugin; rake site } end setup_extension("http11", "http11") -version="0.3.8" +version="0.3.9" summary = "A small fast HTTP library and server that runs Rails, Camping, and Nitro apps." test_file = "test/test_ws.rb" author="Zed A. Shaw" @@ -39,6 +40,7 @@ scripts=['mongrel_rails'] setup_gem(name, version, author, summary, scripts, test_file) do |spec| spec.add_dependency('daemons', '>= 0.4.2') + spec.add_dependency('gem_plugin', ">= 0.1") end desc "Build a binary gem for Win32" @@ -48,9 +50,17 @@ scripts_win32 = scripts + ['mongrel_rails_service'] task :package_win32 do setup_win32_gem(name, version, version, summary, scripts_win32, test_file) do |spec| spec.add_dependency('win32-service', '>= 0.5.0') + spec.add_dependency('gem_plugin', ">= 0.1") spec.files << 'ext/http11/http11.so' spec.extensions = [] spec.platform = Gem::Platform::WIN32 end end +task :gem_plugin_project do + sh %{cd projects/gem_plugin; rake gem_test; } +end + +task :gem_test => [:gem_plugin_project, :package] do + sh %{sudo gem install pkg/mongrel-#{version}} +end diff --git a/bin/mongrel_rails b/bin/mongrel_rails index 1cb339b4..294a41c2 100644 --- a/bin/mongrel_rails +++ b/bin/mongrel_rails @@ -3,7 +3,8 @@ require 'mongrel/rails' require 'yaml' -class Start < Mongrel::Plugin "/commands" + +class Start < GemPlugin::Plugin "/commands" include Mongrel::Command::Base def configure @@ -111,7 +112,7 @@ class Start < Mongrel::Plugin "/commands" end # hook up any rails specific plugins - Mongrel::PluginManager.instance.load + GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE begin STDERR.puts "Server ready." @@ -146,7 +147,7 @@ def send_signal(signal, pid_file) end -class Stop < Mongrel::Plugin "/commands" +class Stop < GemPlugin::Plugin "/commands" include Mongrel::Command::Base def configure @@ -181,7 +182,7 @@ end -class Restart < Mongrel::Plugin "/commands" +class Restart < GemPlugin::Plugin "/commands" include Mongrel::Command::Base def configure @@ -214,5 +215,6 @@ class Restart < Mongrel::Plugin "/commands" end end -Mongrel::PluginManager.instance.load ["rails"] +GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE, "rails" => GemPlugin::EXCLUDE + Mongrel::Command::Registry.instance.run ARGV diff --git a/doc/site/src/default.template b/doc/site/src/default.template index 61cfa765..37fbbcee 100644 --- a/doc/site/src/default.template +++ b/doc/site/src/default.template @@ -46,6 +46,18 @@

NEWS

+
Mar-06-2006
+
+
Mongrel 0.3.9 -- Gem Based Plugins
+ +

This release features not only gem based plugins, but a new sub-project + named GemPlugin to support it. There's a + sample plugin you can try out as well. Read the news for more info. +

+ Download + more

+
+
Mar-04-2006
Mongrel 0.3.8 -- Bug Fixes
@@ -110,16 +122,6 @@ more

-
Feb-13-2006
-
-
Mongrel 0.3.2 Released
- -

Lots of little fixes and enhancements on the 0.3.1 release. Still need to test - this release on win32. - Download - more

-
-

diff --git a/doc/site/src/news.page b/doc/site/src/news.page index 6adfcc96..a95a426e 100644 --- a/doc/site/src/news.page +++ b/doc/site/src/news.page @@ -7,6 +7,38 @@ ordering: 2 h1. Latest News +h2. Mar-06: Mongrel 0.3.9 -- Gem Based Plugins + +This release features the beginning of a plugin system based +on RubyGems to dynamically load installed plugins for Mongrel. +The gist of it is that plugin authors create gems that users +install via *gem install snazzy_plugin*. Users then automagically +get that plugin for Mongrel. + +What I've done with this release is break out this functionality +into a new sub-project called "GemPlugin":gem_plugin_rdoc and +released it with a Ruby license (rather than LGPL like Mongrel). +This will let anyone else who needs a similar plugin system to +*steal* this blind and use it. + +A sample plugin is available that you can install after this update. +Just do: + + $ gem install mongrel_status + +If you run mongrel_rails now you'll see a new command "status". +Change to a Rails appliction directory where you have a daemon +running and it'll print out the PID. + +There will be tons of documentation coming out, and the ability +to write Handlers and Filters this way as well. Feel free +to write any Mongrel commands you want. + +A final note: This isn't hooked into win32 yet. That should +come tomorrow. + +"Download 0.3.9":http://rubyforge.org/frs/?group_id=1306 + h2. Mar-04: Mongrel 0.3.8 -- Bug Fix Release A small release that fixes a few bugs reported to the tracker and adds diff --git a/lib/mongrel/command.rb b/lib/mongrel/command.rb index f8737f01..be25f8f2 100644 --- a/lib/mongrel/command.rb +++ b/lib/mongrel/command.rb @@ -113,8 +113,8 @@ module Mongrel # Builds a list of possible commands from the Command derivates list def commands - pmgr = PluginManager.instance - list = pmgr.available["/commands"] + pmgr = GemPlugin::Manager.instance + list = pmgr.available["/commands"].keys return list.sort end @@ -144,7 +144,7 @@ module Mongrel # command exists, set it up and validate it begin - command = PluginManager.instance.create("/commands/#{cmd_name}", :argv => args) + command = GemPlugin::Manager.instance.create("/commands/#{cmd_name}", :argv => args) rescue STDERR.puts "INVALID COMMAND: #$!" print_command_list diff --git a/lib/mongrel/plugins.rb b/lib/mongrel/plugins.rb index 3a425473..03332fc6 100644 --- a/lib/mongrel/plugins.rb +++ b/lib/mongrel/plugins.rb @@ -1,180 +1,7 @@ -require 'singleton' require 'rubygems' +require_gem 'mongrel_plugin' -module Mongrel - - # Implements the main method of managing plugins for Mongrel. - # "Plugins" in this sense are any classes which get registered - # with Mongrel for possible use when it's operating. These can - # be Handlers, Commands, or other classes. When you create a - # Plugin you register it into a URI-like namespace that makes - # it easy for you (and others) to reference it later during - # configuration. - # - # PluginManager is used as nothing more than a holder of all the - # plugins that have registered themselves. Let's say you have: - # - # class StopNow < Plugin "/commands" - # ... - # end - # - # Then you can get at this plugin with: - # - # cmd = PluginManager.create("/commands/stopnow") - # - # The funky syntax for StopNow is a weird trick borrowed from - # the Camping framework. See the Mongrel::Plugin *function* (yes, - # function). What this basically does is register it - # into the namespace for plugins at /commands. You could go - # as arbitrarily nested as you like. - # - # Why this strange almost second namespace? Why not just use - # the ObjectSpace and/or Modules? The main reason is speed and - # to avoid cluttering the Ruby namespace with what is really a - # configuration statement. This lets implementors put code - # into the Ruby structuring they need, and still have Plugins - # available to Mongrel via simple URI-like names. - # - # The alternative (as pluginfactory does it) is to troll through - # ObjectSpace looking for stuff that *might* be plugins every time - # one is needed. This alternative also means that you are stuck - # naming your commands in specific ways and putting them in specific - # modules in order to configure how Mongrel should use them. - # - # One downside to this is that you need to subclass plugin to - # make it work. In this case use mixins to add other functionality. - class PluginManager - include Singleton - - def initialize - @plugins = URIClassifier.new - @loaded_gems = [] - end - - # Loads all the rubygems that depend on Mongrel so that they - # can be configured into the plugins system. This works by - # checking if the gem depends on Mongrel, and then doing require_gem. - # Since only plugins will configure themselves as plugins then - # everything is safe. - # - # The excludes list is used to prevent mongrel from loading gem plugins - # that aren't ready yet. In the mongrel_rails script this is used to - # load gems that might need rails configured after rails is ready. - def load(excludes=[]) - sdir = File.join(Gem.dir, "specifications") - gems = Gem::SourceIndex.from_installed_gems(sdir) - - gems.each do |path, gem| - found_one = false - gem.dependencies.each do |dep| - # don't load excluded or already loaded gems - if excludes.include? dep.name or @loaded_gems.include? gem.name - found_one = false - break - elsif dep.name == "mongrel" - found_one = true - end - end - - if found_one - require_gem gem.name - @loaded_gems << gem.name - end - - end - end - - - # Not necessary for you to call directly, but this is - # how Mongrel::PluginBase.inherited actually adds a - # plugin to a category. - def register(category, name, klass) - cat, ignored, map = @plugins.resolve(category) - - if not cat or ignored.length > 0 - map = {name => klass} - @plugins.register(category, map) - elsif not map - raise "Unknown category #{category}" - else - map[name] = klass - end - end - - # Resolves the given name (should include /category/name) to - # find the plugin class and create an instance. It uses - # the same URIClassifier that the rest of Mongrel does so it - # is fast. - def create(name, options = {}) - category, plugin, map = @plugins.resolve(name) - - if category and plugin and plugin.length > 0 and map[plugin] - map[plugin].new(options) - else - raise "Plugin #{name} does not exist" - end - end - - # Returns a map of URIs->[handlers] that you can - # use to investigate available handlers. - def available - map = {} - @plugins.uris.each do |u| - cat, name, plugins = @plugins.resolve(u) - map[cat] ||= [] - map[cat] += plugins.keys - end - - return map - end - - end - - # This base class for plugins reallys does nothing - # more than wire up the new class into the right category. - # It is not thread-safe yet but will be soon. - class PluginBase - - attr_reader :options - - - # See Mongrel::Plugin for an explanation. - def PluginBase.inherited(klass) - name = "/" + klass.to_s.downcase - PluginManager.instance.register(@@category, name, klass) - @@category = nil - end - - # See Mongrel::Plugin for an explanation. - def PluginBase.category=(category) - @@category = category - end - - def initialize(options = {}) - @options = options - end - - end - - # This nifty function works with the PluginBase to give you - # the syntax: - # - # class MyThing < Plugin "/things" - # ... - # end - # - # What it does is temporarily sets the PluginBase.category, and then - # returns PluginBase. Since the next immediate thing Ruby does is - # use this returned class to create the new class, PluginBase.inherited - # gets called. PluginBase.inherited then uses the set category, class name, - # and class to register the plugin in the right way. - def Mongrel::Plugin(c) - PluginBase.category = c - PluginBase - end - -end - +# file is just a stub that makes sure the mongrel_plugins gem is loaded and ready diff --git a/lib/mongrel/rails.rb b/lib/mongrel/rails.rb index 20066e7a..e55e2721 100644 --- a/lib/mongrel/rails.rb +++ b/lib/mongrel/rails.rb @@ -1,5 +1,5 @@ require 'mongrel' - +require_gem 'rails' # Implements a handler that can run Rails and serve files out of the # Rails application's public directory. This lets you run your Rails diff --git a/projects/gem_plugin/COPYING b/projects/gem_plugin/COPYING new file mode 100644 index 00000000..b1e3f5a2 --- /dev/null +++ b/projects/gem_plugin/COPYING @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/projects/gem_plugin/LICENSE b/projects/gem_plugin/LICENSE new file mode 100644 index 00000000..da1676d1 --- /dev/null +++ b/projects/gem_plugin/LICENSE @@ -0,0 +1,58 @@ +Ruby is copyrighted free software by Zed A. Shaw +You can redistribute it and/or modify it under either the terms of the GPL +or the conditions below: + + 1. You may make and give away verbatim copies of the source form of the + software without restriction, provided that you duplicate all of the + original copyright notices and associated disclaimers. + + 2. You may modify your copy of the software in any way, provided that + you do at least ONE of the following: + + a) place your modifications in the Public Domain or otherwise + make them Freely Available, such as by posting said + modifications to Usenet or an equivalent medium, or by allowing + the author to include your modifications in the software. + + b) use the modified software only within your corporation or + organization. + + c) rename any non-standard executables so the names do not conflict + with standard executables, which must also be provided. + + d) make other distribution arrangements with the author. + + 3. You may distribute the software in object code or executable + form, provided that you do at least ONE of the following: + + a) distribute the executables and library files of the software, + together with instructions (in the manual page or equivalent) + on where to get the original distribution. + + b) accompany the distribution with the machine-readable source of + the software. + + c) give non-standard executables non-standard names, with + instructions on where to get the original software distribution. + + d) make other distribution arrangements with the author. + + 4. You may modify and include the part of the software into any other + software (possibly commercial). But some files in the distribution + are not written by the author, so that they are not under this terms. + + They are gc.c(partly), utils.c(partly), regex.[ch], st.[ch] and some + files under the ./missing directory. See each file for the copying + condition. + + 5. The scripts and library files supplied as input to or produced as + output from the software do not automatically fall under the + copyright of the software, but belong to whomever generated them, + and may be sold commercially, and may be aggregated with this + software. + + 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. + diff --git a/projects/gem_plugin/README b/projects/gem_plugin/README new file mode 100644 index 00000000..b4c87f73 --- /dev/null +++ b/projects/gem_plugin/README @@ -0,0 +1,115 @@ += GemPlugin: Gem Based Plugin System + +GemPlugin is a system that lets your users install gems and lets you load +them as additional features to use in your software. It originated from the +Mongrel (http://mongrel.rubyforge.org) project but proved useful enough to +break out into a separate project. + +GemPlugin works by listing the gems installed, and doing a require_gem on +any that have the right dependencies. For example, if a gem depends on +"gem_plugin" and "mongrel" then it'll load as a Mongrel plugin. This +makes it so that users of the plugins only need to gem install (and maybe +config a bit), and plugin authors only need to make gems. + + +== Implementers + +To use GemPlugin in your system you only have to require 'gem_plugin' and +then use the GemPlugin::Manager.create, GemPlugin::Manager.load, and +GemPlugin::Manager.available methods to work with them. + +* GemPlugin::Manager.load -- Takes a "depend include/exclude map" and loads plugins based on it. +* GemPlugin::Manager.create -- Takes a URI style name and some options then creates one for you. +* GemPlugin::Manager.available -- Lets you inspect and mess with the internal plugin registry. + +=== Loading Plugins + +As an example from Mongrel it's necessary to load plugins that depend on rails after +the Rails system is configured, but load other plugins right when Mongrel is ready. +To do this we very first do: + + GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE, "rails" => GemPlugin::EXCLUDE + +Later, when it's ready to load Rails plugins as well we do this: + + GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE + +This simply loads any plugins that remain and are ready for use in Rails. + +=== Creating Plugins + +Creating a plugin is cake: + + plug = GemPlugin::Manager.instance.create("/commands/snazzy", "something" => "yeah") + +In this case we're making the snazzy command and passing a couple fake options. + +=== Finding Available Plugins + +Finding plugins is also very easy, you just call GemPlugin::Manager.instance.available +and you get a Hash that maps categories to name => class. For example, if I had +the "/commands/snazzy" plugin registered above, then I'd get the following: + + puts GemPlugin::Manager.instance.available["/commands"].inspect + -> { "/snazzy" => Snazzy} + + +=== Plugins Inside Modules + +Plugins that are placed in modules are also lowercased when registered but +still retain their module. So, if Snazzy was actually MyModule::Snazzy, then +it'd be registered as "/commands/mymodule::snazzy". + + +== Plugin Authors + +People who wish to write gem plugins have a faily easy time of it, but need +to know the particular rules for the target system. To keep this example +concrete we'll assume you want to write a Mongrel command plugin. + +First thing is create your project like normal and setup Rake to make +your gem. Your plugin then needs to be created like so: + + class Snazzy < GemPlugin::Plugin "/commands" + ... + end + +And place this code in a file you will have RubyGems autorequire (I use lib/init.rb). + +Next you need to add the following to whatever Rakefile code you use to create +your gem: + + spec.add_dependency('gem_plugin', '>= 0.1') + spec.add_dependency('mongrel', '>= 0.3.9') + spec.autorequire = 'init.rb' + +This does three things: + +* Tells GemPlugins::Manager.load that this is a GemPlugin. +* Tells Mongrel that this is a Mongrel specific GemPlugin. +* Tells RubyGems to run init.rb when the gem is required, just hooking up your plugin. + +Now, all the users of your plugin have to do is gem install it and then +they get the plugin automagically. + +People writing GemPlugins for other systems would have to check the +documentation from that project to get an idea of what extra +requirements might be needed. For example, you'd probably have to +depend on another project other that *mongrel* and most likely have +a few more things to configure in your init.rb. + +== Plugin Users + +Plugin users have it the easiest of all. They simply do: + + gem install mongrel_command_snazzy + +And that's it. When they run mongrel_rails (given the above example) +this snazzy command get loaded automatically without any intervention. + +The only thing missing in this release is a way for end users to configure +such a plugin. I really think this is the job of the implementers to define. + +== Contact + +E-mail zedshaw at zedshaw.com and I'll help. Comments about the API are welcome. diff --git a/projects/gem_plugin/Rakefile b/projects/gem_plugin/Rakefile new file mode 100644 index 00000000..bc53ef1f --- /dev/null +++ b/projects/gem_plugin/Rakefile @@ -0,0 +1,35 @@ +require 'rake' +require 'rake/testtask' +require 'rake/clean' +require 'rake/gempackagetask' +require 'rake/rdoctask' +require 'tools/rakehelp' +require 'fileutils' +include FileUtils + +setup_tests +setup_clean ["pkg", "lib/*.bundle", "*.gem", ".config"] + +setup_rdoc ['README', 'LICENSE', 'COPYING', 'lib/**/*.rb', 'doc/**/*.rdoc'] + +desc "Does a full compile, test run" +task :default => [:test, :package] + +version="0.1" +summary = "A plugin system based only on rubygems" +test_file = "test/test_plugins.rb" +author="Zed A. Shaw" +name="gem_plugin" +scripts=[] + +setup_gem(name, version, author, summary, scripts, test_file) do |spec| + spec.autorequire = "gem_plugin" +end + +task :gem_test => [:package] do + sh %{sudo gem install pkg/gem_plugin-#{version}} +end + +task :site => [:rerdoc] do + sh %{ scp -r doc/rdoc/* #{ENV['SSH_USER']}@rubyforge.org:/var/www/gforge-projects/mongrel/gem_plugin_rdoc/ } +end diff --git a/projects/gem_plugin/lib/gem_plugin.rb b/projects/gem_plugin/lib/gem_plugin.rb new file mode 100644 index 00000000..3b537f4e --- /dev/null +++ b/projects/gem_plugin/lib/gem_plugin.rb @@ -0,0 +1,204 @@ +require 'singleton' +require 'rubygems' + +# Implements a dynamic plugin loading, configuration, and discovery system +# based on RubyGems and a simple additional name space that looks like a URI. +# +# A plugin is created and put into a category with the following code: +# +# class MyThing < GemPlugin::Plugin "/things" +# ... +# end +# +# What this does is sets up your MyThing in the plugin registry via GemPlugin::Manager. +# You can then later get this plugin with GemPlugin::Manager.create("/things/mything") +# and can also pass in options as a second parameter. +# +# This isn't such a big deal, but the power is really from the GemPlugin::Manager.load +# method. This method will go through the installed gems and require_gem any +# that depend on the gem_plugin RubyGem. You can arbitrarily include or exclude +# gems based on what they also depend on, thus letting you load these gems when appropriate. +# +# Since this system was written originally for the Mongrel project that'll be the +# best examle of using it. +# +# Imagine you have a neat plugin for Mongrel called snazzy_command that give the +# mongrel_rails a new command snazzy (like: mongrel_rails snazzy). You'd like +# people to be able to grab this plugin if they want and use it, because it's snazzy. +# +# First thing you do is create a gem of your project and make sure that it depends +# on "mongrel" AND "gem_plugin". This signals to the GemPlugin system that this is +# a plugin for mongrel. +# +# Next you put this code into a file like lib/init.rb (can be anything really): +# +# class Snazzy < GemPlugin::Plugin "/commands" +# ... +# end +# +# Then when you create your gem you have the following bits in your Rakefile: +# +# spec.add_dependency('mongrel', '>= 0.3.9') +# spec.add_dependency('gem_plugin', '>= 0.1') +# spec.autorequire = 'init.rb' +# +# Finally, you just have to now publish this gem for people to install and Mongrel +# will "magically" be able to install it. +# +# The "magic" part though is pretty simple and done via the GemPlugin::Manager.load +# method. Read that to see how it is really done. +module GemPlugin + + EXCLUDE = true + INCLUDE = false + + # This class is used by people who use gem plugins (but don't necessarily make them) + # to add plugins to their own systems. It provides a way to load plugins, list them, + # and create them as needed. + # + # It is a singleton so you use like this: GemPlugins::Manager.instance.load + class Manager + include Singleton + + def initialize + @plugins = {} + @loaded_gems = [] + end + + + # Responsible for going through the list of available gems and loading + # any plugins requested. It keeps track of what it's loaded already + # and won't load them again. + # + # It accepts one parameter which is a hash of gem depends that should include + # or exclude a gem from being loaded. A gem must depend on gem_plugin to be + # considered, but then each system has to add it's own INCLUDE to make sure + # that only plugins related to it are loaded. + # + # An example again comes from Mongrel. In order to load all Mongrel plugins: + # + # GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE + # + # Which will load all plugins that depend on mongrel AND gem_plugin. Now, one + # extra thing we do is we delay loading Rails Mongrel plugins until after rails + # is configured. Do do this the mongrel_rails script has: + # + # GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE, "rails" => GemPlugin::EXCLUDE + # The only thing to remember is that this is saying "include a plugin if it + # depends on gem_plugin, mongrel, but NOT rails". If a plugin also depends on other + # stuff then it's loaded just fine. Only gem_plugin, mongrel, and rails are + # ever used to determine if it should be included. + def load(needs = {}) + sdir = File.join(Gem.dir, "specifications") + gems = Gem::SourceIndex.from_installed_gems(sdir) + needs = needs.merge({"gem_plugin" => INCLUDE}) + + gems.each do |path, gem| + # don't load gems more than once + next if @loaded_gems.include? gem.name + check = needs.dup + + # rolls through the depends and inverts anything it finds + gem.dependencies.each do |dep| + # this will fail if a gem is depended more than once + if check.has_key? dep.name + check[dep.name] = !check[dep.name] + end + end + + # now since excluded gems start as true, inverting them + # makes them false so we'll skip this gem if any excludes are found + if (check.select {|name,test| !test}).length == 0 + # looks like no needs were set to false, so it's good + require_gem gem.name + @loaded_gems << gem.name + end + + end + end + + + # Not necessary for you to call directly, but this is + # how GemPlugin::Base.inherited actually adds a + # plugin to a category. + def register(category, name, klass) + @plugins[category] ||= {} + @plugins[category][name.downcase] = klass + end + + # Resolves the given name (should include /category/name) to + # find the plugin class and create an instance. You can + # pass a second hash option that is then given to the Plugin + # to configure it. + def create(name, options = {}) + last_slash = name.rindex("/") + category = name[0 ... last_slash] + plugin = name[last_slash .. -1] + + map = @plugins[category] + if not map + raise "Plugin category #{category} does not exist" + elsif not map.has_key? plugin + raise "Plugin #{plugin} does not exist in category #{category}" + else + map[plugin].new(options) + end + end + + + # Returns a map of URIs->{"name" => Plugin} that you can + # use to investigate available handlers. + def available + return @plugins + end + + end + + # This base class for plugins reallys does nothing + # more than wire up the new class into the right category. + # It is not thread-safe yet but will be soon. + class Base + + attr_reader :options + + + # See Mongrel::Plugin for an explanation. + def Base.inherited(klass) + name = "/" + klass.to_s.downcase + Manager.instance.register(@@category, name, klass) + @@category = nil + end + + # See Mongrel::Plugin for an explanation. + def Base.category=(category) + @@category = category + end + + def initialize(options = {}) + @options = options + end + + end + + # This nifty function works with the GemPlugin::Base to give you + # the syntax: + # + # class MyThing < GemPlugin::Plugin "/things" + # ... + # end + # + # What it does is temporarily sets the GemPlugin::Base.category, and then + # returns GemPlugin::Base. Since the next immediate thing Ruby does is + # use this returned class to create the new class, GemPlugin::Base.inherited + # gets called. GemPlugin::Base.inherited then uses the set category, class name, + # and class to register the plugin in the right way. + def GemPlugin::Plugin(c) + Base.category = c + Base + end + +end + + + + diff --git a/projects/gem_plugin/setup.rb b/projects/gem_plugin/setup.rb new file mode 100644 index 00000000..0807023d --- /dev/null +++ b/projects/gem_plugin/setup.rb @@ -0,0 +1,1360 @@ +# +# setup.rb +# +# Copyright (c) 2000-2004 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 + +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 SetupError < StandardError; end + +def setup_rb_error(msg) + raise SetupError, msg +end + +# +# Config +# + +if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } + ARGV.delete(arg) + require arg.split(/=/, 2)[1] + $".push 'rbconfig.rb' +else + require 'rbconfig' +end + +def multipackage_install? + FileTest.directory?(File.dirname($0) + '/packages') +end + + +class ConfigItem + def initialize(name, template, default, desc) + @name = name.freeze + @template = template + @value = default + @default = default.dup.freeze + @description = desc + end + + attr_reader :name + attr_reader :description + + attr_accessor :default + alias help_default default + + def help_opt + "--#{@name}=#{@template}" + end + + def value + @value + end + + def eval(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 < ConfigItem + def config_type + 'bool' + end + + def help_opt + "--#{@name}" + end + + private + + def check(val) + return 'yes' unless val + unless /\A(y(es)?|n(o)?|t(rue)?|f(alse))\z/i =~ val + setup_rb_error "config: --#{@name} accepts only yes/no for argument" + end + (/\Ay(es)?|\At(rue)/i =~ value) ? 'yes' : 'no' + end +end + +class PathItem < ConfigItem + 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 < ConfigItem + def config_type + 'program' + end +end + +class SelectItem < ConfigItem + def initialize(name, template, default, desc) + super + @ok = template.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 PackageSelectionItem < ConfigItem + 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 ConfigTable_class + + def initialize(items) + @items = items + @table = {} + items.each do |i| + @table[i.name] = i + end + ALIASES.each do |ali, name| + @table[ali] = @table[name] + end + end + + include Enumerable + + def each(&block) + @items.each(&block) + end + + def key?(name) + @table.key?(name) + end + + def lookup(name) + @table[name] or raise ArgumentError, "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 new + dup() + end + + def savefile + '.config' + end + + def load + begin + t = dup() + File.foreach(savefile()) do |line| + k, v = *line.split(/=/, 2) + t[k] = v.strip + end + t + rescue Errno::ENOENT + setup_rb_error $!.message + "#{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 + end + } + end + + def [](key) + lookup(key).eval(self) + end + + def []=(key, val) + lookup(key).set val + end + +end + +c = ::Config::CONFIG + +rubypath = c['bindir'] + '/' + c['ruby_install_name'] + +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 + _stdruby = c['rubylibdir'] + _siteruby = c['sitedir'] + _siterubyver = c['sitelibdir'] + _siterubyverarch = c['sitearchdir'] +elsif newpath_p + # 1.4.4 <= V <= 1.6.3 + _stdruby = "$prefix/lib/ruby/#{version}" + _siteruby = c['sitedir'] + _siterubyver = "$siteruby/#{version}" + _siterubyverarch = "$siterubyver/#{c['arch']}" +else + # V < 1.4.4 + _stdruby = "$prefix/lib/ruby/#{version}" + _siteruby = "$prefix/lib/ruby/#{version}/site_ruby" + _siterubyver = _siteruby + _siterubyverarch = "$siterubyver/#{c['arch']}" +end +libdir = '-* dummy libdir *-' +stdruby = '-* dummy rubylibdir *-' +siteruby = '-* dummy site_ruby *-' +siterubyver = '-* dummy site_ruby version *-' +parameterize = lambda {|path| + path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix')\ + .sub(/\A#{Regexp.quote(libdir)}/, '$libdir')\ + .sub(/\A#{Regexp.quote(stdruby)}/, '$stdruby')\ + .sub(/\A#{Regexp.quote(siteruby)}/, '$siteruby')\ + .sub(/\A#{Regexp.quote(siterubyver)}/, '$siterubyver') +} +libdir = parameterize.call(c['libdir']) +stdruby = parameterize.call(_stdruby) +siteruby = parameterize.call(_siteruby) +siterubyver = parameterize.call(_siterubyver) +siterubyverarch = parameterize.call(_siterubyverarch) + +if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } + makeprog = arg.sub(/'/, '').split(/=/, 2)[1] +else + makeprog = 'make' +end + +common_conf = [ + 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', 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 man pages'), + PathItem.new('stdruby', 'path', stdruby, + 'the directory for standard ruby libraries'), + 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') +] +class ConfigTable_class # open again + ALIASES = { + 'std-ruby' => 'stdruby', + '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' + } +end +multipackage_conf = [ + 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') +] +if multipackage_install? + ConfigTable = ConfigTable_class.new(common_conf + multipackage_conf) +else + ConfigTable = ConfigTable_class.new(common_conf) +end + + +module MetaConfigAPI + + def eval_file_ifexist(fname) + instance_eval File.read(fname), fname, 1 if File.file?(fname) + end + + def config_names + ConfigTable.map {|i| i.name } + end + + def config?(name) + ConfigTable.key?(name) + end + + def bool_config?(name) + ConfigTable.lookup(name).config_type == 'bool' + end + + def path_config?(name) + ConfigTable.lookup(name).config_type == 'path' + end + + def value_config?(name) + case ConfigTable.lookup(name).config_type + when 'bool', 'path' + true + else + false + end + end + + def add_config(item) + ConfigTable.add item + end + + def add_bool_config(name, default, desc) + ConfigTable.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) + end + + def add_path_config(name, default, desc) + ConfigTable.add PathItem.new(name, 'path', default, desc) + end + + def set_config_default(name, default) + ConfigTable.lookup(name).default = default + end + + def remove_config(name) + ConfigTable.remove(name) + end + +end + + +# +# File Operations +# + +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 case + 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(fname) + $stderr.puts "rm -f #{fname}" if verbose? + return if no_harm? + + if File.exist?(fname) or File.symlink?(fname) + File.chmod 0777, fname + File.unlink fname + end + end + + def rm_rf(dn) + $stderr.puts "rm -rf #{dn}" if verbose? + return if no_harm? + + Dir.chdir dn + Dir.foreach('.') do |fn| + next if fn == '.' + next if fn == '..' + if File.dir?(fn) + verbose_off { + rm_rf fn + } + else + verbose_off { + rm_f fn + } + end + end + Dir.chdir '..' + Dir.rmdir dn + end + + def move_file(src, dest) + File.unlink dest if File.exist?(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 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(str) + $stderr.puts str if verbose? + system str or raise RuntimeError, "'system #{str}' failed" + end + + def ruby(str) + command config('rubyprog') + ' ' + str + end + + def make(task = '') + command config('makeprog') + ' ' + task + end + + def extdir?(dir) + File.exist?(dir + '/MANIFEST') + end + + def all_files_in(dirname) + Dir.open(dirname) {|d| + return d.select {|ent| File.file?("#{dirname}/#{ent}") } + } + end + + REJECT_DIRS = %w( + CVS SCCS RCS CVS.adm .svn + ) + + def all_dirs_in(dirname) + Dir.open(dirname) {|d| + return d.select {|n| File.dir?("#{dirname}/#{n}") } - %w(. ..) - REJECT_DIRS + } + end + +end + + +# +# Main Installer +# + +module HookUtils + + def run_hook(name) + try_run_hook "#{curr_srcdir()}/#{name}" or + try_run_hook "#{curr_srcdir()}/#{name}.rb" + end + + def try_run_hook(fname) + return false unless File.file?(fname) + begin + instance_eval File.read(fname), fname, 1 + rescue + setup_rb_error "hook #{fname} failed:\n" + $!.message + end + true + end + +end + + +module HookScriptAPI + + def get_config(key) + @config[key] + end + + alias config get_config + + def set_config(key, val) + @config[key] = val + end + + # + # srcdir/objdir (works only in the package directory) + # + + #abstract srcdir_root + #abstract objdir_root + #abstract relpath + + 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.3.1' + Copyright = 'Copyright (c) 2000-2004 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' ], + [ 'clean', "does `make clean' for each extention" ], + [ 'distclean',"does `make distclean' for each extention" ] + ] + + def ToplevelInstaller.invoke + instance().invoke + end + + @singleton = nil + + def ToplevelInstaller.instance + @singleton ||= new(File.dirname($0)) + @singleton + end + + include MetaConfigAPI + + def initialize(ardir_root) + @config = nil + @options = { 'verbose' => true } + @ardir = File.expand_path(ardir_root) + end + + def inspect + "#<#{self.class} #{__id__()}>" + end + + def invoke + run_metaconfigs + case task = parsearg_global() + when nil, 'all' + @config = load_config('config') + parsearg_config + init_installers + exec_config + exec_setup + exec_install + else + @config = load_config(task) + __send__ "parsearg_#{task}" + init_installers + __send__ "exec_#{task}" + end + end + + def run_metaconfigs + eval_file_ifexist "#{@ardir}/metaconfig" + end + + def load_config(task) + case task + when 'config' + ConfigTable.new + when 'clean', 'distclean' + if File.exist?(ConfigTable.savefile) + then ConfigTable.load + else ConfigTable.new + end + else + ConfigTable.load + end + end + + def init_installers + @installer = Installer.new(@config, @options, @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 + valid_task = /\A(?:#{TASKS.map {|task,desc| task }.join '|'})\z/ + + 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' + @options['verbose'] = false + + when '--verbose' + @options['verbose'] = true + + when '-h', '--help' + print_usage $stdout + exit 0 + + when '-v', '--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 parsearg_no_options + unless ARGV.empty? + setup_rb_error "#{task}: unknown options: #{ARGV.join ' '}" + end + end + + alias parsearg_show parsearg_no_options + alias parsearg_setup parsearg_no_options + alias parsearg_clean parsearg_no_options + alias parsearg_distclean parsearg_no_options + + def parsearg_config + re = /\A--(#{ConfigTable.map {|i| i.name }.join('|')})(?:=(.*))?\z/ + @options['config-opt'] = [] + + while i = ARGV.shift + if /\A--?\z/ =~ i + @options['config-opt'] = ARGV.dup + break + end + m = re.match(i) or setup_rb_error "config: unknown option #{i}" + name, value = *m.to_a[1,2] + @config[name] = value + end + end + + def parsearg_install + @options['no-harm'] = false + @options['install-prefix'] = '' + while a = ARGV.shift + case a + when /\A--no-harm\z/ + @options['no-harm'] = true + when /\A--prefix=(.*)\z/ + path = $1 + path = File.expand_path(path) unless path[0,1] == '/' + @options['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} " + out.puts " ruby #{File.basename $0} [] []" + + 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, '-h,--help', 'print this message' + out.printf fmt, '-v,--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:' + ConfigTable.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', '$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_show + ConfigTable.each do |i| + printf "%-20s %s\n", i.name, i.value + end + end + + def exec_clean + @installer.exec_clean + end + + def exec_distclean + @installer.exec_distclean + end + +end + + +class ToplevelInstallerMulti < ToplevelInstaller + + include HookUtils + include HookScriptAPI + include FileOperations + + def initialize(ardir) + super + @packages = all_dirs_in("#{@ardir}/packages") + raise 'no package exists' if @packages.empty? + end + + def run_metaconfigs + eval_file_ifexist "#{@ardir}/metaconfig" + @packages.each do |name| + eval_file_ifexist "#{@ardir}/packages/#{name}/metaconfig" + end + end + + def init_installers + @installers = {} + @packages.each do |pack| + @installers[pack] = Installer.new(@config, @options, + "#{@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 + + # + # multi-package metaconfig API + # + + attr_reader :packages + + def declare_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 + + # + # 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_clean + rm_f ConfigTable.savefile + run_hook 'pre-clean' + each_selected_installers {|inst| inst.exec_clean } + run_hook 'post-clean' + end + + def exec_distclean + rm_f ConfigTable.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 @options['verbose'] + Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") + Dir.chdir "packages/#{pack}" + yield @installers[pack] + Dir.chdir '../..' + end + end + + def verbose? + @options['verbose'] + end + + def no_harm? + @options['no-harm'] + end + +end + + +class Installer + + FILETYPES = %w( bin lib ext data ) + + include HookScriptAPI + include HookUtils + include FileOperations + + def initialize(config, opt, srcroot, objroot) + @config = config + @options = opt + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{self.class} #{File.basename(@srcdir)}>" + end + + # + # Hook Script API base methods + # + + def srcdir_root + @srcdir + end + + def objdir_root + @objdir + end + + def relpath + @currdir + end + + # + # configs/options + # + + def no_harm? + @options['no-harm'] + end + + def verbose? + @options['verbose'] + end + + def verbose_off + begin + save, @options['verbose'] = @options['verbose'], false + yield + ensure + @options['verbose'] = save + end + end + + # + # TASK config + # + + def exec_config + exec_task_traverse 'config' + end + + def config_dir_bin(rel) + end + + def config_dir_lib(rel) + end + + def config_dir_ext(rel) + extconf if extdir?(curr_srcdir()) + end + + def extconf + opt = @options['config-opt'].join(' ') + command "#{config('rubyprog')} #{curr_srcdir()}/extconf.rb #{opt}" + end + + def config_dir_data(rel) + end + + # + # TASK setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin(rel) + all_files_in(curr_srcdir()).each do |fname| + adjust_shebang "#{curr_srcdir()}/#{fname}" + end + end + + def adjust_shebang(path) + return if no_harm? + tmpfile = File.basename(path) + '.tmp' + begin + File.open(path, 'rb') {|r| + first = r.gets + return unless File.basename(config('rubypath')) == 'ruby' + return unless File.basename(first.sub(/\A\#!/, '').split[0]) == 'ruby' + $stderr.puts "adjusting shebang: #{File.basename(path)}" if verbose? + File.open(tmpfile, 'wb') {|w| + w.print first.sub(/\A\#!\s*\S+/, '#! ' + config('rubypath')) + w.write r.read + } + move_file tmpfile, File.basename(path) + } + ensure + File.unlink tmpfile if File.exist?(tmpfile) + end + end + + def setup_dir_lib(rel) + end + + def setup_dir_ext(rel) + make if extdir?(curr_srcdir()) + end + + def setup_dir_data(rel) + end + + # + # TASK install + # + + def exec_install + rm_f 'InstalledFiles' + exec_task_traverse 'install' + end + + def install_dir_bin(rel) + install_files collect_filenames_auto(), "#{config('bindir')}/#{rel}", 0755 + end + + def install_dir_lib(rel) + install_files ruby_scripts(), "#{config('rbdir')}/#{rel}", 0644 + end + + def install_dir_ext(rel) + return unless extdir?(curr_srcdir()) + install_files ruby_extentions('.'), + "#{config('sodir')}/#{File.dirname(rel)}", + 0555 + end + + def install_dir_data(rel) + install_files collect_filenames_auto(), "#{config('datadir')}/#{rel}", 0644 + end + + def install_files(list, dest, mode) + mkdir_p dest, @options['install-prefix'] + list.each do |fname| + install fname, dest, mode, @options['install-prefix'] + end + end + + def ruby_scripts + collect_filenames_auto().select {|n| /\.rb\z/ =~ n } + end + + # picked up many entries from cvs-1.11.1/src/ignore.c + reject_patterns = %w( + core RCSLOG tags TAGS .make.state + .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb + *~ *.old *.bak *.BAK *.orig *.rej _$* *$ + + *.org *.in .* + ) + mapping = { + '.' => '\.', + '$' => '\$', + '#' => '\#', + '*' => '.*' + } + REJECT_PATTERNS = Regexp.new('\A(?:' + + reject_patterns.map {|pat| + pat.gsub(/[\.\$\#\*]/) {|ch| mapping[ch] } + }.join('|') + + ')\z') + + def collect_filenames_auto + mapdir((existfiles() - hookfiles()).reject {|fname| + REJECT_PATTERNS =~ fname + }) + end + + def existfiles + all_files_in(curr_srcdir()) | all_files_in('.') + 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 mapdir(filelist) + filelist.map {|fname| + if File.exist?(fname) # objdir + fname + else # srcdir + File.join(curr_srcdir(), fname) + end + } + end + + def ruby_extentions(dir) + Dir.open(dir) {|d| + ents = d.select {|fname| /\.#{::Config::CONFIG['DLEXT']}\z/ =~ fname } + if ents.empty? + setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" + end + return ents + } + end + + # + # TASK clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f ConfigTable.savefile + rm_f 'InstalledFiles' + end + + def clean_dir_bin(rel) + end + + def clean_dir_lib(rel) + end + + def clean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'clean' if File.file?('Makefile') + end + + def clean_dir_data(rel) + end + + # + # TASK distclean + # + + def exec_distclean + exec_task_traverse 'distclean' + rm_f ConfigTable.savefile + rm_f 'InstalledFiles' + end + + def distclean_dir_bin(rel) + end + + def distclean_dir_lib(rel) + end + + def distclean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'distclean' if File.file?('Makefile') + end + + # + # lib + # + + def exec_task_traverse(task) + run_hook "pre-#{task}" + FILETYPES.each do |type| + if config('without-ext') == 'yes' and type == 'ext' + $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)], '') + all_dirs_in(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 + +end + + +if $0 == __FILE__ + begin + if multipackage_install? + ToplevelInstallerMulti.invoke + else + ToplevelInstaller.invoke + end + 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/test_plugins.rb b/projects/gem_plugin/test/test_plugins.rb similarity index 55% rename from test/test_plugins.rb rename to projects/gem_plugin/test/test_plugins.rb index 82132b13..39a12e60 100644 --- a/test/test_plugins.rb +++ b/projects/gem_plugin/test/test_plugins.rb @@ -1,34 +1,35 @@ require 'test/unit' -require 'mongrel' +require 'gem_plugin' -class ATestPlugin < Mongrel::Plugin "/stuff" +include GemPlugin + +class ATestPlugin < GemPlugin::Plugin "/stuff" end -class First < Mongrel::Plugin "/commands" +class First < GemPlugin::Plugin "/commands" def initialize(options = {}) puts "First with options: #{options.inspect}" end end -class Second < Mongrel::Plugin "/commands" +class Second < GemPlugin::Plugin "/commands" def initialize(options = {}) puts "Second with options: #{options.inspect}" end end -class Last < Mongrel::Plugin "/commands" +class Last < GemPlugin::Plugin "/commands" def initialize(options = {}) puts "Last with options: #{options.inspect}" end end -include Mongrel - class PluginTest < Test::Unit::TestCase def setup - @pmgr = PluginManager.instance + @pmgr = Manager.instance + @pmgr.load({"rails" => EXCLUDE}) @categories = ["/commands"] @names = ["/first", "/second", "/last", "/atestplugin"] end @@ -36,17 +37,17 @@ class PluginTest < Test::Unit::TestCase def test_load_plugins puts "#{@pmgr.available.inspect}" @pmgr.available.each {|cat,plugins| - plugins.each do |p| - puts "TEST: #{cat}#{p}" - assert @names.include?(p) + plugins.each do |n,p| + puts "TEST: #{cat}#{n}" + assert @names.include?(n) end } @pmgr.load @pmgr.available.each do |cat,plugins| - plugins.each do |p| - STDERR.puts "#{cat}#{p}" - plugin = @pmgr.create("#{cat}#{p}", options={"name" => p}) + plugins.each do |n,p| + STDERR.puts "#{cat}#{n}" + plugin = @pmgr.create("#{cat}#{n}", options={"name" => p}) end end end @@ -60,4 +61,13 @@ class PluginTest < Test::Unit::TestCase assert_equal @pmgr.create("/test2/testme").class, ATestPlugin end + + + def test_create + last = @pmgr.create("/commands/last", "test" => "stuff") + assert last != nil, "Didn't make the right plugin" + first = @pmgr.create("/commands/last") + assert first != nil, "Didn't make the right plugin" + end + end diff --git a/projects/gem_plugin/tools/rakehelp.rb b/projects/gem_plugin/tools/rakehelp.rb new file mode 100644 index 00000000..eb72873a --- /dev/null +++ b/projects/gem_plugin/tools/rakehelp.rb @@ -0,0 +1,111 @@ + +def make(makedir) + Dir.chdir(makedir) do + sh(PLATFORM =~ /win32/ ? 'nmake' : 'make') + end +end + + +def extconf(dir) + Dir.chdir(dir) do ruby "extconf.rb" end +end + + +def setup_tests + Rake::TestTask.new do |t| + t.libs << "test" + t.test_files = FileList['test/test*.rb'] + t.verbose = true + end +end + + +def setup_clean otherfiles + files = ['build/*', '**/*.o', '**/*.so', '**/*.a', 'lib/*-*', '**/*.log'] + otherfiles + CLEAN.include(files) +end + + +def setup_rdoc files + Rake::RDocTask.new do |rdoc| + rdoc.rdoc_dir = 'doc/rdoc' + rdoc.options << '--line-numbers' + rdoc.rdoc_files.add(files) + end +end + + +def setup_extension(dir, extension) + ext = "ext/#{dir}" + ext_so = "#{ext}/#{extension}.#{Config::CONFIG['DLEXT']}" + ext_files = FileList[ + "#{ext}/*.c", + "#{ext}/*.h", + "#{ext}/extconf.rb", + "#{ext}/Makefile", + "lib" + ] + + task "lib" do + directory "lib" + end + + desc "Builds just the #{extension} extension" + task extension.to_sym => ["#{ext}/Makefile", ext_so ] + + file "#{ext}/Makefile" => ["#{ext}/extconf.rb"] do + extconf "#{ext}" + end + + file ext_so => ext_files do + make "#{ext}" + cp ext_so, "lib" + end +end + + +def base_gem_spec(pkg_name, pkg_version, author, summary, executables, test_file) + pkg_version = pkg_version + pkg_name = pkg_name + pkg_file_name = "#{pkg_name}-#{pkg_version}" + Gem::Specification.new do |s| + s.name = pkg_name + s.version = pkg_version + s.required_ruby_version = '>= 1.8.3' + s.platform = Gem::Platform::RUBY + s.author = author + s.summary = summary + s.test_file = test_file + s.has_rdoc = true + s.extra_rdoc_files = [ "README" ] + + s.files = %w(COPYING LICENSE README Rakefile) + + Dir.glob("{bin,doc/rdoc,test,lib}/**/*") + + Dir.glob("ext/**/*.{h,c,rb}") + + Dir.glob("examples/**/*.rb") + + Dir.glob("tools/*.rb") + + s.require_path = "lib" + s.extensions = FileList["ext/**/extconf.rb"].to_a + + s.executables = executables + s.bindir = "bin" + end +end + +def setup_gem(pkg_name, pkg_version, author, summary, executables, test_file) + spec = base_gem_spec(pkg_name, pkg_version, author, summary, executables, test_file) + yield spec if block_given? + + Rake::GemPackageTask.new(spec) do |p| + p.gem_spec = spec + p.need_tar = true + end +end + +def setup_win32_gem(pkg_name, pkg_version, author, summary, executables, test_file) + spec = base_gem_spec(pkg_name, pkg_version, author, summary, executables, test_file) + yield spec if block_given? + + Gem::Builder.new(spec).build +end