1
# Rails Plugin Manager.
3
# Listing available plugins:
5
# $ ./script/plugin list
6
# continuous_builder http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder
7
# asset_timestamping http://svn.aviditybytes.com/rails/plugins/asset_timestamping
8
# enumerations_mixin http://svn.protocool.com/rails/plugins/enumerations_mixin/trunk
9
# calculations http://techno-weenie.net/svn/projects/calculations/
14
# $ ./script/plugin install continuous_builder asset_timestamping
16
# Finding Repositories:
18
# $ ./script/plugin discover
20
# Adding Repositories:
22
# $ ./script/plugin source http://svn.protocool.com/rails/plugins/
26
# * Maintains a list of subversion repositories that are assumed to have
27
# a plugin directory structure. Manage them with the (source, unsource,
28
# and sources commands)
30
# * The discover command scrapes the following page for things that
31
# look like subversion repositories with plugins:
32
# http://wiki.rubyonrails.org/rails/pages/Plugins
34
# * Unless you specify that you want to use svn, script/plugin uses plain old
35
# HTTP for downloads. The following bullets are true if you specify
36
# that you want to use svn.
38
# * If `vendor/plugins` is under subversion control, the script will
39
# modify the svn:externals property and perform an update. You can
40
# use normal subversion commands to keep the plugins up to date.
42
# * Or, if `vendor/plugins` is not under subversion control, the
43
# plugin is pulled via `svn checkout` or `svn export` but looks
46
# Specifying revisions:
48
# * Subversion revision is a single integer.
50
# * Git revision format:
51
# - full - 'refs/tags/1.8.0' or 'refs/heads/experimental'
52
# - short: 'experimental' (equivalent to 'refs/heads/experimental')
53
# 'tag 1.8.0' (equivalent to 'refs/tags/1.8.0')
56
# This is Free Software, copyright 2005 by Ryan Tomayko (rtomayko@gmail.com)
57
# and is licensed MIT: (http://www.opensource.org/licenses/mit-license.php)
68
class RailsEnvironment
75
def self.find(dir=nil)
78
return new(dir) if File.exist?(File.join(dir, 'config', 'environment.rb'))
79
dir = File.dirname(dir)
87
def self.default=(rails_env)
91
def install(name_uri_or_plugin)
92
if name_uri_or_plugin.is_a? String
93
if name_uri_or_plugin =~ /:\/\//
94
plugin = Plugin.new(name_uri_or_plugin)
96
plugin = Plugins[name_uri_or_plugin]
99
plugin = name_uri_or_plugin
104
puts "Plugin not found: #{name_uri_or_plugin}"
109
require 'active_support/core_ext/kernel'
110
silence_stderr {`svn --version` rescue nil}
111
!$?.nil? && $?.success?
115
use_svn? && File.directory?("#{root}/vendor/plugins/.svn")
119
# this is a bit of a guess. we assume that if the rails environment
120
# is under subversion then they probably want the plugin checked out
121
# instead of exported. This can be overridden on the command line
122
File.directory?("#{root}/.svn")
125
def best_install_method
126
return :http unless use_svn?
128
when use_externals? then :externals
129
when use_checkout? then :checkout
135
return [] unless use_externals?
136
ext = `svn propget svn:externals "#{root}/vendor/plugins"`
137
lines = ext.respond_to?(:lines) ? ext.lines : ext
138
lines.reject{ |line| line.strip == '' }.map do |line|
139
line.strip.split(/\s+/, 2)
143
def externals=(items)
144
unless items.is_a? String
145
items = items.map{|name,uri| "#{name.ljust(29)} #{uri.chomp('/')}"}.join("\n")
147
Tempfile.open("svn-set-prop") do |file|
150
system("svn propset -q svn:externals -F \"#{file.path}\" \"#{root}/vendor/plugins\"")
157
attr_reader :name, :uri
159
def initialize(uri, name=nil)
165
name =~ /\// ? new(name) : Repositories.instance.find_plugin(name)
169
"#{@name.ljust(30)}#{@uri}"
173
@uri =~ /svn(?:\+ssh)?:\/\/*/
177
@uri =~ /^git:\/\// || @uri =~ /\.git$/
181
File.directory?("#{rails_env.root}/vendor/plugins/#{name}") \
182
or rails_env.externals.detect{ |name, repo| self.uri == repo }
185
def install(method=nil, options = {})
186
method ||= rails_env.best_install_method?
188
method = :export if svn_url?
189
method = :git if git_url?
192
uninstall if installed? and options[:force]
195
send("install_using_#{method}", options)
198
puts "already installed: #{name} (#{uri}). pass --force to reinstall"
203
path = "#{rails_env.root}/vendor/plugins/#{name}"
204
if File.directory?(path)
205
puts "Removing 'vendor/plugins/#{name}'" if $verbose
209
puts "Plugin doesn't exist: #{path}"
211
# clean up svn:externals
212
externals = rails_env.externals
213
externals.reject!{|n,u| name == n or name == u}
214
rails_env.externals = externals
218
tmp = "#{rails_env.root}/_tmp_about.yml"
220
cmd = "svn export #{@uri} \"#{rails_env.root}/#{tmp}\""
224
open(svn_url? ? tmp : File.join(@uri, 'about.yml')) do |stream|
226
end rescue "No about.yml found in #{uri}"
228
FileUtils.rm_rf tmp if svn_url?
234
install_hook_file = "#{rails_env.root}/vendor/plugins/#{name}/install.rb"
235
load install_hook_file if File.exist? install_hook_file
238
def run_uninstall_hook
239
uninstall_hook_file = "#{rails_env.root}/vendor/plugins/#{name}/uninstall.rb"
240
load uninstall_hook_file if File.exist? uninstall_hook_file
243
def install_using_export(options = {})
244
svn_command :export, options
247
def install_using_checkout(options = {})
248
svn_command :checkout, options
251
def install_using_externals(options = {})
252
externals = rails_env.externals
253
externals.push([@name, uri])
254
rails_env.externals = externals
255
install_using_checkout(options)
258
def install_using_http(options = {})
259
root = rails_env.root
260
mkdir_p "#{root}/vendor/plugins/#{@name}"
261
Dir.chdir "#{root}/vendor/plugins/#{@name}" do
262
puts "fetching from '#{uri}'" if $verbose
263
fetcher = RecursiveHTTPFetcher.new(uri, -1)
264
fetcher.quiet = true if options[:quiet]
269
def install_using_git(options = {})
270
root = rails_env.root
271
mkdir_p(install_path = "#{root}/vendor/plugins/#{name}")
272
Dir.chdir install_path do
273
init_cmd = "git init"
274
init_cmd += " -q" if options[:quiet] and not $verbose
275
puts init_cmd if $verbose
277
base_cmd = "git pull --depth 1 #{uri}"
278
base_cmd += " -q" if options[:quiet] and not $verbose
279
base_cmd += " #{options[:revision]}" if options[:revision]
280
puts base_cmd if $verbose
282
puts "removing: .git .gitignore" if $verbose
283
rm_rf %w(.git .gitignore)
290
def svn_command(cmd, options = {})
291
root = rails_env.root
292
mkdir_p "#{root}/vendor/plugins"
293
base_cmd = "svn #{cmd} #{uri} \"#{root}/vendor/plugins/#{name}\""
294
base_cmd += ' -q' if options[:quiet] and not $verbose
295
base_cmd += " -r #{options[:revision]}" if options[:revision]
296
puts base_cmd if $verbose
301
@name = File.basename(url)
302
if @name == 'trunk' || @name.empty?
303
@name = File.basename(File.dirname(url))
305
@name.gsub!(/\.git$/, '') if @name =~ /\.git$/
309
@rails_env || RailsEnvironment.default
316
def initialize(cache_file = File.join(find_home, ".rails-plugin-sources"))
317
@cache_file = File.expand_path(cache_file)
322
@repositories.each(&block)
326
unless find{|repo| repo.uri == uri }
327
@repositories.push(Repository.new(uri)).last
332
@repositories.reject!{|repo| repo.uri == uri}
336
@repositories.detect{|repo| repo.uri == uri }
343
def find_plugin(name)
344
@repositories.each do |repo|
345
repo.each do |plugin|
346
return plugin if plugin.name == name
353
contents = File.exist?(@cache_file) ? File.read(@cache_file) : defaults
354
contents = defaults if contents.empty?
355
@repositories = contents.split(/\n/).reject do |line|
356
line =~ /^\s*#/ or line =~ /^\s*$/
357
end.map { |source| Repository.new(source.strip) }
361
File.open(@cache_file, 'w') do |f|
371
http://dev.rubyonrails.com/svn/rails/plugins/
376
['HOME', 'USERPROFILE'].each do |homekey|
377
return ENV[homekey] if ENV[homekey]
379
if ENV['HOMEDRIVE'] && ENV['HOMEPATH']
380
return "#{ENV['HOMEDRIVE']}:#{ENV['HOMEPATH']}"
383
File.expand_path("~")
384
rescue StandardError => ex
385
if File::ALT_SEPARATOR
394
@instance ||= Repositories.new
397
def self.each(&block)
398
self.instance.each(&block)
404
attr_reader :uri, :plugins
407
@uri = uri.chomp('/') << "/"
414
puts "Discovering plugins in #{@uri}"
418
@plugins = index.reject{ |line| line !~ /\/$/ }
419
@plugins.map! { |name| Plugin.new(File.join(@uri, name), name) }
431
@index ||= RecursiveHTTPFetcher.new(@uri).ls
436
# load default environment and parse arguments
441
attr_reader :environment, :script_name, :sources
443
@environment = RailsEnvironment.default
444
@rails_root = RailsEnvironment.default.root
445
@script_name = File.basename($0)
449
def environment=(value)
451
RailsEnvironment.default = value
455
OptionParser.new do |o|
456
o.set_summary_indent(' ')
457
o.banner = "Usage: #{@script_name} [OPTIONS] command"
458
o.define_head "Rails plugin manager."
461
o.separator "GENERAL OPTIONS"
463
o.on("-r", "--root=DIR", String,
464
"Set an explicit rails app directory.",
465
"Default: #{@rails_root}") { |rails_root| @rails_root = rails_root; self.environment = RailsEnvironment.new(@rails_root) }
466
o.on("-s", "--source=URL1,URL2", Array,
467
"Use the specified plugin repositories instead of the defaults.") { |sources| @sources = sources}
469
o.on("-v", "--verbose", "Turn on verbose output.") { |verbose| $verbose = verbose }
470
o.on("-h", "--help", "Show this help message.") { puts o; exit }
473
o.separator "COMMANDS"
475
o.separator " discover Discover plugin repositories."
476
o.separator " list List available plugins."
477
o.separator " install Install plugin(s) from known repositories or URLs."
478
o.separator " update Update installed plugins."
479
o.separator " remove Uninstall plugins."
480
o.separator " source Add a plugin source repository."
481
o.separator " unsource Remove a plugin repository."
482
o.separator " sources List currently configured plugin repositories."
485
o.separator "EXAMPLES"
486
o.separator " Install a plugin:"
487
o.separator " #{@script_name} install continuous_builder\n"
488
o.separator " Install a plugin from a subversion URL:"
489
o.separator " #{@script_name} install http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder\n"
490
o.separator " Install a plugin from a git URL:"
491
o.separator " #{@script_name} install git://github.com/SomeGuy/my_awesome_plugin.git\n"
492
o.separator " Install a plugin and add a svn:externals entry to vendor/plugins"
493
o.separator " #{@script_name} install -x continuous_builder\n"
494
o.separator " List all available plugins:"
495
o.separator " #{@script_name} list\n"
496
o.separator " List plugins in the specified repository:"
497
o.separator " #{@script_name} list --source=http://dev.rubyonrails.com/svn/rails/plugins/\n"
498
o.separator " Discover and prompt to add new repositories:"
499
o.separator " #{@script_name} discover\n"
500
o.separator " Discover new repositories but just list them, don't add anything:"
501
o.separator " #{@script_name} discover -l\n"
502
o.separator " Add a new repository to the source list:"
503
o.separator " #{@script_name} source http://dev.rubyonrails.com/svn/rails/plugins/\n"
504
o.separator " Remove a repository from the source list:"
505
o.separator " #{@script_name} unsource http://dev.rubyonrails.com/svn/rails/plugins/\n"
506
o.separator " Show currently configured repositories:"
507
o.separator " #{@script_name} sources\n"
511
def parse!(args=ARGV)
512
general, sub = split_args(args)
513
options.parse!(general)
515
command = general.shift
516
if command =~ /^(list|discover|install|source|unsource|sources|remove|update|info)$/
517
command = Commands.const_get(command.capitalize).new(self)
520
puts "Unknown command: #{command}"
528
left << args.shift while args[0] and args[0] =~ /^-/
529
left << args.shift if args[0]
533
def self.parse!(args=ARGV)
534
Plugin.new.parse!(args)
540
def initialize(base_command)
541
@base_command = base_command
548
OptionParser.new do |o|
549
o.set_summary_indent(' ')
550
o.banner = "Usage: #{@base_command.script_name} list [OPTIONS] [PATTERN]"
551
o.define_head "List available plugins."
553
o.separator "Options:"
555
o.on( "-s", "--source=URL1,URL2", Array,
556
"Use the specified plugin repositories.") {|sources| @sources = sources}
558
"List locally installed plugins.") {|local| @local, @remote = local, false}
560
"List remotely available plugins. This is the default behavior",
561
"unless --local is provided.") {|remote| @remote = remote}
567
unless @sources.empty?
568
@sources.map!{ |uri| Repository.new(uri) }
570
@sources = Repositories.instance.all
573
@sources.map{|r| r.plugins}.flatten.each do |plugin|
574
if @local or !plugin.installed?
579
cd "#{@base_command.environment.root}/vendor/plugins"
580
Dir["*"].select{|p| File.directory?(p)}.each do |name|
589
def initialize(base_command)
590
@base_command = base_command
594
OptionParser.new do |o|
595
o.set_summary_indent(' ')
596
o.banner = "Usage: #{@base_command.script_name} sources [OPTIONS] [PATTERN]"
597
o.define_head "List configured plugin repositories."
599
o.separator "Options:"
601
o.on( "-c", "--check",
602
"Report status of repository.") { |sources| @sources = sources}
608
Repositories.each do |repo|
616
def initialize(base_command)
617
@base_command = base_command
621
OptionParser.new do |o|
622
o.set_summary_indent(' ')
623
o.banner = "Usage: #{@base_command.script_name} source REPOSITORY [REPOSITORY [REPOSITORY]...]"
624
o.define_head "Add new repositories to the default search list."
632
if Repositories.instance.add(uri)
633
puts "added: #{uri.ljust(50)}" if $verbose
636
puts "failed: #{uri.ljust(50)}"
639
Repositories.instance.save
640
puts "Added #{count} repositories."
646
def initialize(base_command)
647
@base_command = base_command
651
OptionParser.new do |o|
652
o.set_summary_indent(' ')
653
o.banner = "Usage: #{@base_command.script_name} unsource URI [URI [URI]...]"
654
o.define_head "Remove repositories from the default search list."
656
o.on_tail("-h", "--help", "Show this help message.") { puts o; exit }
664
if Repositories.instance.remove(uri)
666
puts "removed: #{uri.ljust(50)}"
668
puts "failed: #{uri.ljust(50)}"
671
Repositories.instance.save
672
puts "Removed #{count} repositories."
678
def initialize(base_command)
679
@base_command = base_command
685
OptionParser.new do |o|
686
o.set_summary_indent(' ')
687
o.banner = "Usage: #{@base_command.script_name} discover URI [URI [URI]...]"
688
o.define_head "Discover repositories referenced on a page."
690
o.separator "Options:"
692
o.on( "-l", "--list",
693
"List but don't prompt or add discovered repositories.") { |list| @list, @prompt = list, !@list }
694
o.on( "-n", "--no-prompt",
695
"Add all new repositories without prompting.") { |v| @prompt = !v }
701
args = ['http://wiki.rubyonrails.org/rails/pages/Plugins'] if args.empty?
703
scrape(uri) do |repo_uri|
707
$stdout.print "Add #{repo_uri}? [Y/n] "
708
throw :next_uri if $stdin.gets !~ /^y?$/i
717
Repositories.instance.add(repo_uri)
718
puts "discovered: #{repo_uri}" if $verbose or !@prompt
722
Repositories.instance.save
727
puts "Scraping #{uri}" if $verbose
729
content = open(uri).each do |line|
731
if line =~ /<a[^>]*href=['"]([^'"]*)['"]/ || line =~ /(svn:\/\/[^<|\n]*)/
733
if uri =~ /^\w+:\/\// && uri =~ /\/plugins\// && uri !~ /\/browser\// && uri !~ /^http:\/\/wiki\.rubyonrails/ && uri !~ /http:\/\/instiki/
734
uri = extract_repository_uri(uri)
735
yield uri unless dupes.include?(uri) || Repositories.instance.exist?(uri)
740
puts "Problems scraping '#{uri}': #{$!.to_s}"
745
def extract_repository_uri(uri)
746
uri.match(/(svn|https?):.*\/plugins\//i)[0]
751
def initialize(base_command)
752
@base_command = base_command
754
@options = { :quiet => false, :revision => nil, :force => false }
758
OptionParser.new do |o|
759
o.set_summary_indent(' ')
760
o.banner = "Usage: #{@base_command.script_name} install PLUGIN [PLUGIN [PLUGIN] ...]"
761
o.define_head "Install one or more plugins."
763
o.separator "Options:"
764
o.on( "-x", "--externals",
765
"Use svn:externals to grab the plugin.",
766
"Enables plugin updates and plugin versioning.") { |v| @method = :externals }
767
o.on( "-o", "--checkout",
768
"Use svn checkout to grab the plugin.",
769
"Enables updating but does not add a svn:externals entry.") { |v| @method = :checkout }
770
o.on( "-e", "--export",
771
"Use svn export to grab the plugin.",
772
"Exports the plugin, allowing you to check it into your local repository. Does not enable updates, or add an svn:externals entry.") { |v| @method = :export }
773
o.on( "-q", "--quiet",
774
"Suppresses the output from installation.",
775
"Ignored if -v is passed (./script/plugin -v install ...)") { |v| @options[:quiet] = true }
776
o.on( "-r REVISION", "--revision REVISION",
777
"Checks out the given revision from subversion or git.",
778
"Ignored if subversion/git is not used.") { |v| @options[:revision] = v }
779
o.on( "-f", "--force",
780
"Reinstalls a plugin if it's already installed.") { |v| @options[:force] = true }
782
o.separator "You can specify plugin names as given in 'plugin list' output or absolute URLs to "
783
o.separator "a plugin repository."
787
def determine_install_method
788
best = @base_command.environment.best_install_method
789
@method = :http if best == :http and @method == :export
791
when (best == :http and @method != :http)
792
msg = "Cannot install using subversion because `svn' cannot be found in your PATH"
793
when (best == :export and (@method != :export and @method != :http))
794
msg = "Cannot install using #{@method} because this project is not under subversion."
795
when (best != :externals and @method == :externals)
796
msg = "Cannot install using externals because vendor/plugins is not under subversion."
807
environment = @base_command.environment
808
install_method = determine_install_method
809
puts "Plugins will be installed using #{install_method}" if $verbose
811
::Plugin.find(name).install(install_method, @options)
813
rescue StandardError => e
814
puts "Plugin not found: #{args.inspect}"
815
puts e.inspect if $verbose
821
def initialize(base_command)
822
@base_command = base_command
826
OptionParser.new do |o|
827
o.set_summary_indent(' ')
828
o.banner = "Usage: #{@base_command.script_name} update [name [name]...]"
829
o.on( "-r REVISION", "--revision REVISION",
830
"Checks out the given revision from subversion.",
831
"Ignored if subversion is not used.") { |v| @revision = v }
832
o.define_head "Update plugins."
838
root = @base_command.environment.root
840
args = Dir["vendor/plugins/*"].map do |f|
841
File.directory?("#{f}/.svn") ? File.basename(f) : nil
842
end.compact if args.empty?
845
if File.directory?(name)
846
puts "Updating plugin: #{name}"
847
system("svn #{$verbose ? '' : '-q'} up \"#{name}\" #{@revision ? "-r #{@revision}" : ''}")
849
puts "Plugin doesn't exist: #{name}"
856
def initialize(base_command)
857
@base_command = base_command
861
OptionParser.new do |o|
862
o.set_summary_indent(' ')
863
o.banner = "Usage: #{@base_command.script_name} remove name [name]..."
864
o.define_head "Remove plugins."
870
root = @base_command.environment.root
872
::Plugin.new(name).uninstall
878
def initialize(base_command)
879
@base_command = base_command
883
OptionParser.new do |o|
884
o.set_summary_indent(' ')
885
o.banner = "Usage: #{@base_command.script_name} info name [name]..."
886
o.define_head "Shows plugin info at {url}/about.yml."
893
puts ::Plugin.find(name).info
900
class RecursiveHTTPFetcher
902
def initialize(urls_to_fetch, level = 1, cwd = ".")
905
@urls_to_fetch = RUBY_VERSION >= '1.9' ? urls_to_fetch.lines : urls_to_fetch.to_a
910
@urls_to_fetch.collect do |url|
911
if url =~ /^svn(\+ssh)?:\/\/.*/
912
`svn ls #{url}`.split("\n").map {|entry| "/#{entry}"} rescue nil
914
open(url) do |stream|
915
links("", stream.read)
922
@cwd = File.join(@cwd, dir)
923
FileUtils.mkdir_p(@cwd)
927
@cwd = File.dirname(@cwd)
930
def links(base_url, contents)
932
contents.scan(/href\s*=\s*\"*[^\">]*/i) do |link|
933
link = link.sub(/href="/i, "")
934
next if link =~ /svnindex.xsl$/
935
next if link =~ /^(\w*:|)\/\// || link =~ /^\./
936
links << File.join(base_url, link)
942
puts "+ #{File.join(@cwd, File.basename(link))}" unless @quiet
943
open(link) do |stream|
944
File.open(File.join(@cwd, File.basename(link)), "wb") do |file|
945
file.write(stream.read)
950
def fetch(links = @urls_to_fetch)
952
(l =~ /\/$/ || links == @urls_to_fetch) ? fetch_dir(l) : download(l)
958
push_d(File.basename(url)) if @level > 0
959
open(url) do |stream|
960
contents = stream.read
961
fetch(links(url, contents))
968
Commands::Plugin.parse!