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 ext
.reject
{ |line
| line
.strip
== '' }.map
do |line
|
138 line
.strip
.split(/\s+/, 2)
142 def externals
=(items
)
143 unless items
.is_a
? String
144 items
= items
.map
{|name
,uri
| "#{name.ljust(29)} #{uri.chomp('/')}"}.join("\n")
146 Tempfile
.open("svn-set-prop") do |file
|
149 system("svn propset -q svn:externals -F \"#{file.path}\" \"#{root}/vendor/plugins\"")
156 attr_reader
:name, :uri
158 def initialize(uri
, name
=nil)
164 name
=~
/\// ? new(name
) : Repositories
.instance
.find_plugin(name
)
168 "#{@name.ljust(30)}#{@uri}"
172 @uri =~
/svn(?:\+ssh)?:\/\
/*/
176 @uri =~
/^git:\/\
// || @uri =~
/\.git$/
180 File
.directory
?("#{rails_env.root}/vendor/plugins/#{name}") \
181 or rails_env
.externals
.detect
{ |name
, repo
| self.uri
== repo
}
184 def install(method
=nil, options
= {})
185 method
||= rails_env
.best_install_method
?
187 method
= :export if svn_url
?
188 method
= :git if git_url
?
191 uninstall
if installed
? and options
[:force]
194 send("install_using_#{method}", options
)
197 puts
"already installed: #{name} (#{uri}). pass --force to reinstall"
202 path
= "#{rails_env.root}/vendor/plugins/#{name}"
203 if File
.directory
?(path
)
204 puts
"Removing 'vendor/plugins/#{name}'" if $verbose
208 puts
"Plugin doesn't exist: #{path}"
210 # clean up svn:externals
211 externals
= rails_env
.externals
212 externals
.reject
!{|n
,u
| name
== n
or name
== u
}
213 rails_env
.externals
= externals
217 tmp
= "#{rails_env.root}/_tmp_about.yml"
219 cmd
= "svn export #{@uri} \"#{rails_env.root}/#{tmp}\""
223 open(svn_url
? ? tmp
: File
.join(@uri, 'about.yml')) do |stream
|
225 end rescue "No about.yml found in #{uri}"
227 FileUtils
.rm_rf tmp
if svn_url
?
233 install_hook_file
= "#{rails_env.root}/vendor/plugins/#{name}/install.rb"
234 load install_hook_file
if File
.exist
? install_hook_file
237 def run_uninstall_hook
238 uninstall_hook_file
= "#{rails_env.root}/vendor/plugins/#{name}/uninstall.rb"
239 load uninstall_hook_file
if File
.exist
? uninstall_hook_file
242 def install_using_export(options
= {})
243 svn_command
:export, options
246 def install_using_checkout(options
= {})
247 svn_command
:checkout, options
250 def install_using_externals(options
= {})
251 externals
= rails_env
.externals
252 externals
.push([@name, uri
])
253 rails_env
.externals
= externals
254 install_using_checkout(options
)
257 def install_using_http(options
= {})
258 root
= rails_env
.root
259 mkdir_p
"#{root}/vendor/plugins/#{@name}"
260 Dir
.chdir
"#{root}/vendor/plugins/#{@name}" do
261 puts
"fetching from '#{uri}'" if $verbose
262 fetcher
= RecursiveHTTPFetcher
.new(uri
, -1)
263 fetcher
.quiet
= true if options
[:quiet]
268 def install_using_git(options
= {})
269 root
= rails_env
.root
270 install_path
= mkdir_p
"#{root}/vendor/plugins/#{name}"
271 Dir
.chdir install_path
do
272 init_cmd
= "git init"
273 init_cmd
+= " -q" if options
[:quiet] and not $verbose
274 puts init_cmd
if $verbose
276 base_cmd
= "git pull --depth 1 #{uri}"
277 base_cmd
+= " -q" if options
[:quiet] and not $verbose
278 base_cmd
+= " #{options[:revision]}" if options
[:revision]
279 puts base_cmd
if $verbose
281 puts
"removing: .git" if $verbose
289 def svn_command(cmd
, options
= {})
290 root
= rails_env
.root
291 mkdir_p
"#{root}/vendor/plugins"
292 base_cmd
= "svn #{cmd} #{uri} \"#{root}/vendor/plugins/#{name}\""
293 base_cmd
+= ' -q' if options
[:quiet] and not $verbose
294 base_cmd
+= " -r #{options[:revision]}" if options
[:revision]
295 puts base_cmd
if $verbose
300 @name = File
.basename(url
)
301 if @name == 'trunk' || @name.empty
?
302 @name = File
.basename(File
.dirname(url
))
304 @name.gsub
!(/\.git$/, '') if @name =~
/\.git$/
308 @rails_env || RailsEnvironment
.default
315 def initialize(cache_file
= File
.join(find_home
, ".rails-plugin-sources"))
316 @cache_file = File
.expand_path(cache_file
)
321 @repositories.each(&block
)
325 unless find
{|repo
| repo
.uri
== uri
}
326 @repositories.push(Repository
.new(uri
)).last
331 @repositories.reject
!{|repo
| repo
.uri
== uri
}
335 @repositories.detect
{|repo
| repo
.uri
== uri
}
342 def find_plugin(name
)
343 @repositories.each
do |repo
|
344 repo
.each
do |plugin
|
345 return plugin
if plugin
.name
== name
352 contents
= File
.exist
?(@cache_file) ? File
.read(@cache_file) : defaults
353 contents
= defaults
if contents
.empty
?
354 @repositories = contents
.split(/\n/).reject
do |line
|
355 line
=~
/^\s*#/ or line
=~
/^\s*$/
356 end.map
{ |source
| Repository
.new(source
.strip
) }
360 File
.open(@cache_file, 'w') do |f
|
370 http://dev.rubyonrails.com/svn/rails/plugins/
375 ['HOME', 'USERPROFILE'].each
do |homekey
|
376 return ENV[homekey
] if ENV[homekey
]
378 if ENV['HOMEDRIVE'] && ENV['HOMEPATH']
379 return "#{ENV['HOMEDRIVE']}:#{ENV['HOMEPATH']}"
382 File
.expand_path("~")
383 rescue StandardError
=> ex
384 if File
::ALT_SEPARATOR
393 @instance ||= Repositories
.new
396 def self.each(&block
)
397 self.instance
.each(&block
)
403 attr_reader
:uri, :plugins
406 @uri = uri
.chomp('/') << "/"
413 puts
"Discovering plugins in #{@uri}"
417 @plugins = index
.reject
{ |line
| line
!~
/\/$/ }
418 @plugins.map
! { |name
| Plugin
.new(File
.join(@uri, name
), name
) }
430 @index ||= RecursiveHTTPFetcher
.new(@uri).ls
435 # load default environment and parse arguments
440 attr_reader
:environment, :script_name, :sources
442 @environment = RailsEnvironment
.default
443 @rails_root = RailsEnvironment
.default
.root
444 @script_name = File
.basename($0)
448 def environment
=(value
)
450 RailsEnvironment
.default
= value
454 OptionParser
.new
do |o
|
455 o
.set_summary_indent(' ')
456 o
.banner
= "Usage: #{@script_name} [OPTIONS] command"
457 o
.define_head
"Rails plugin manager."
460 o
.separator
"GENERAL OPTIONS"
462 o
.on("-r", "--root=DIR", String
,
463 "Set an explicit rails app directory.",
464 "Default: #{@rails_root}") { |rails_root
| @rails_root = rails_root
; self.environment
= RailsEnvironment
.new(@rails_root) }
465 o
.on("-s", "--source=URL1,URL2", Array
,
466 "Use the specified plugin repositories instead of the defaults.") { |sources
| @sources = sources
}
468 o
.on("-v", "--verbose", "Turn on verbose output.") { |verbose
| $verbose = verbose
}
469 o
.on("-h", "--help", "Show this help message.") { puts o
; exit
}
472 o
.separator
"COMMANDS"
474 o
.separator
" discover Discover plugin repositories."
475 o
.separator
" list List available plugins."
476 o
.separator
" install Install plugin(s) from known repositories or URLs."
477 o
.separator
" update Update installed plugins."
478 o
.separator
" remove Uninstall plugins."
479 o
.separator
" source Add a plugin source repository."
480 o
.separator
" unsource Remove a plugin repository."
481 o
.separator
" sources List currently configured plugin repositories."
484 o
.separator
"EXAMPLES"
485 o
.separator
" Install a plugin:"
486 o
.separator
" #{@script_name} install continuous_builder\n"
487 o
.separator
" Install a plugin from a subversion URL:"
488 o
.separator
" #{@script_name} install http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder\n"
489 o
.separator
" Install a plugin from a git URL:"
490 o
.separator
" #{@script_name} install git://github.com/SomeGuy/my_awesome_plugin.git\n"
491 o
.separator
" Install a plugin and add a svn:externals entry to vendor/plugins"
492 o
.separator
" #{@script_name} install -x continuous_builder\n"
493 o
.separator
" List all available plugins:"
494 o
.separator
" #{@script_name} list\n"
495 o
.separator
" List plugins in the specified repository:"
496 o
.separator
" #{@script_name} list --source=http://dev.rubyonrails.com/svn/rails/plugins/\n"
497 o
.separator
" Discover and prompt to add new repositories:"
498 o
.separator
" #{@script_name} discover\n"
499 o
.separator
" Discover new repositories but just list them, don't add anything:"
500 o
.separator
" #{@script_name} discover -l\n"
501 o
.separator
" Add a new repository to the source list:"
502 o
.separator
" #{@script_name} source http://dev.rubyonrails.com/svn/rails/plugins/\n"
503 o
.separator
" Remove a repository from the source list:"
504 o
.separator
" #{@script_name} unsource http://dev.rubyonrails.com/svn/rails/plugins/\n"
505 o
.separator
" Show currently configured repositories:"
506 o
.separator
" #{@script_name} sources\n"
510 def parse
!(args
=ARGV)
511 general
, sub
= split_args(args
)
512 options
.parse
!(general
)
514 command
= general
.shift
515 if command
=~
/^(list|discover|install|source|unsource|sources|remove|update|info)$/
516 command
= Commands
.const_get(command
.capitalize
).new(self)
519 puts
"Unknown command: #{command}"
527 left
<< args
.shift
while args
[0] and args
[0] =~
/^-/
528 left
<< args
.shift
if args
[0]
532 def self.parse
!(args
=ARGV)
533 Plugin
.new
.parse
!(args
)
539 def initialize(base_command
)
540 @base_command = base_command
547 OptionParser
.new
do |o
|
548 o
.set_summary_indent(' ')
549 o
.banner
= "Usage: #{@base_command.script_name} list [OPTIONS] [PATTERN]"
550 o
.define_head
"List available plugins."
552 o
.separator
"Options:"
554 o
.on( "-s", "--source=URL1,URL2", Array
,
555 "Use the specified plugin repositories.") {|sources
| @sources = sources
}
557 "List locally installed plugins.") {|local
| @local, @remote = local
, false}
559 "List remotely available plugins. This is the default behavior",
560 "unless --local is provided.") {|remote
| @remote = remote
}
566 unless @sources.empty
?
567 @sources.map
!{ |uri
| Repository
.new(uri
) }
569 @sources = Repositories
.instance
.all
572 @sources.map
{|r
| r
.plugins
}.flatten
.each
do |plugin
|
573 if @local or !plugin
.installed
?
578 cd
"#{@base_command.environment.root}/vendor/plugins"
579 Dir
["*"].select
{|p
| File
.directory
?(p
)}.each
do |name
|
588 def initialize(base_command
)
589 @base_command = base_command
593 OptionParser
.new
do |o
|
594 o
.set_summary_indent(' ')
595 o
.banner
= "Usage: #{@base_command.script_name} sources [OPTIONS] [PATTERN]"
596 o
.define_head
"List configured plugin repositories."
598 o
.separator
"Options:"
600 o
.on( "-c", "--check",
601 "Report status of repository.") { |sources
| @sources = sources
}
607 Repositories
.each
do |repo
|
615 def initialize(base_command
)
616 @base_command = base_command
620 OptionParser
.new
do |o
|
621 o
.set_summary_indent(' ')
622 o
.banner
= "Usage: #{@base_command.script_name} source REPOSITORY [REPOSITORY [REPOSITORY]...]"
623 o
.define_head
"Add new repositories to the default search list."
631 if Repositories
.instance
.add(uri
)
632 puts
"added: #{uri.ljust(50)}" if $verbose
635 puts
"failed: #{uri.ljust(50)}"
638 Repositories
.instance
.save
639 puts
"Added #{count} repositories."
645 def initialize(base_command
)
646 @base_command = base_command
650 OptionParser
.new
do |o
|
651 o
.set_summary_indent(' ')
652 o
.banner
= "Usage: #{@base_command.script_name} unsource URI [URI [URI]...]"
653 o
.define_head
"Remove repositories from the default search list."
655 o
.on_tail("-h", "--help", "Show this help message.") { puts o
; exit
}
663 if Repositories
.instance
.remove(uri
)
665 puts
"removed: #{uri.ljust(50)}"
667 puts
"failed: #{uri.ljust(50)}"
670 Repositories
.instance
.save
671 puts
"Removed #{count} repositories."
677 def initialize(base_command
)
678 @base_command = base_command
684 OptionParser
.new
do |o
|
685 o
.set_summary_indent(' ')
686 o
.banner
= "Usage: #{@base_command.script_name} discover URI [URI [URI]...]"
687 o
.define_head
"Discover repositories referenced on a page."
689 o
.separator
"Options:"
691 o
.on( "-l", "--list",
692 "List but don't prompt or add discovered repositories.") { |list
| @list, @prompt = list
, !@list }
693 o
.on( "-n", "--no-prompt",
694 "Add all new repositories without prompting.") { |v
| @prompt = !v
}
700 args
= ['http://wiki.rubyonrails.org/rails/pages/Plugins'] if args
.empty
?
702 scrape(uri
) do |repo_uri
|
706 $stdout.print
"Add #{repo_uri}? [Y/n] "
707 throw :next_uri if $stdin.gets
!~
/^y?$/i
716 Repositories
.instance
.add(repo_uri
)
717 puts
"discovered: #{repo_uri}" if $verbose or !@prompt
721 Repositories
.instance
.save
726 puts
"Scraping #{uri}" if $verbose
728 content
= open(uri
).each
do |line
|
730 if line
=~
/<a[^>]*href=['"]([^'"]*)['"]/ || line
=~
/(svn:\/\
/[^<|\n]*)/
732 if uri
=~
/^\w+:\/\
// && uri
=~
/\/plugins\
// && uri
!~
/\/browser\
// && uri
!~
/^http:\/\
/wiki\.rubyonrails/ && uri
!~
/http:\/\
/instiki/
733 uri
= extract_repository_uri(uri
)
734 yield uri
unless dupes
.include?(uri
) || Repositories
.instance
.exist
?(uri
)
739 puts
"Problems scraping '#{uri}': #{$!.to_s}"
744 def extract_repository_uri(uri
)
745 uri
.match(/(svn|https?):.*\/plugins\
//i
)[0]
750 def initialize(base_command
)
751 @base_command = base_command
753 @options = { :quiet => false, :revision => nil, :force => false }
757 OptionParser
.new
do |o
|
758 o
.set_summary_indent(' ')
759 o
.banner
= "Usage: #{@base_command.script_name} install PLUGIN [PLUGIN [PLUGIN] ...]"
760 o
.define_head
"Install one or more plugins."
762 o
.separator
"Options:"
763 o
.on( "-x", "--externals",
764 "Use svn:externals to grab the plugin.",
765 "Enables plugin updates and plugin versioning.") { |v
| @method = :externals }
766 o
.on( "-o", "--checkout",
767 "Use svn checkout to grab the plugin.",
768 "Enables updating but does not add a svn:externals entry.") { |v
| @method = :checkout }
769 o
.on( "-e", "--export",
770 "Use svn export to grab the plugin.",
771 "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 }
772 o
.on( "-q", "--quiet",
773 "Suppresses the output from installation.",
774 "Ignored if -v is passed (./script/plugin -v install ...)") { |v
| @options[:quiet] = true }
775 o
.on( "-r REVISION", "--revision REVISION",
776 "Checks out the given revision from subversion or git.",
777 "Ignored if subversion/git is not used.") { |v
| @options[:revision] = v
}
778 o
.on( "-f", "--force",
779 "Reinstalls a plugin if it's already installed.") { |v
| @options[:force] = true }
781 o
.separator
"You can specify plugin names as given in 'plugin list' output or absolute URLs to "
782 o
.separator
"a plugin repository."
786 def determine_install_method
787 best
= @base_command.environment
.best_install_method
788 @method = :http if best
== :http and @method == :export
790 when (best
== :http and @method != :http)
791 msg
= "Cannot install using subversion because `svn' cannot be found in your PATH"
792 when (best
== :export and (@method != :export and @method != :http))
793 msg
= "Cannot install using #{@method} because this project is not under subversion."
794 when (best
!= :externals and @method == :externals)
795 msg
= "Cannot install using externals because vendor/plugins is not under subversion."
806 environment
= @base_command.environment
807 install_method
= determine_install_method
808 puts
"Plugins will be installed using #{install_method}" if $verbose
810 ::Plugin.find(name
).install(install_method
, @options)
812 rescue StandardError
=> e
813 puts
"Plugin not found: #{args.inspect}"
814 puts e
.inspect
if $verbose
820 def initialize(base_command
)
821 @base_command = base_command
825 OptionParser
.new
do |o
|
826 o
.set_summary_indent(' ')
827 o
.banner
= "Usage: #{@base_command.script_name} update [name [name]...]"
828 o
.on( "-r REVISION", "--revision REVISION",
829 "Checks out the given revision from subversion.",
830 "Ignored if subversion is not used.") { |v
| @revision = v
}
831 o
.define_head
"Update plugins."
837 root
= @base_command.environment
.root
839 args
= Dir
["vendor/plugins/*"].map
do |f
|
840 File
.directory
?("#{f}/.svn") ? File
.basename(f
) : nil
841 end.compact
if args
.empty
?
844 if File
.directory
?(name
)
845 puts
"Updating plugin: #{name}"
846 system("svn #{$verbose ? '' : '-q'} up \"#{name}\" #{@revision ? "-r
#{@revision}" : ''}")
848 puts
"Plugin doesn't exist: #{name}"
855 def initialize(base_command
)
856 @base_command = base_command
860 OptionParser
.new
do |o
|
861 o
.set_summary_indent(' ')
862 o
.banner
= "Usage: #{@base_command.script_name} remove name [name]..."
863 o
.define_head
"Remove plugins."
869 root
= @base_command.environment
.root
871 ::Plugin.new(name
).uninstall
877 def initialize(base_command
)
878 @base_command = base_command
882 OptionParser
.new
do |o
|
883 o
.set_summary_indent(' ')
884 o
.banner
= "Usage: #{@base_command.script_name} info name [name]..."
885 o
.define_head
"Shows plugin info at {url}/about.yml."
892 puts
::Plugin.find(name
).info
899 class RecursiveHTTPFetcher
901 def initialize(urls_to_fetch
, level
= 1, cwd
= ".")
904 @urls_to_fetch = RUBY_VERSION >= '1.9' ? urls_to_fetch
.lines
: urls_to_fetch
.to_a
909 @urls_to_fetch.collect
do |url
|
910 if url
=~
/^svn(\+ssh)?:\/\
/.*/
911 `svn ls #{url}`.split("\n").map
{|entry
| "/#{entry}"} rescue nil
913 open(url
) do |stream
|
914 links("", stream
.read
)
921 @cwd = File
.join(@cwd, dir
)
922 FileUtils
.mkdir_p(@cwd)
926 @cwd = File
.dirname(@cwd)
929 def links(base_url
, contents
)
931 contents
.scan(/href\s*=\s*\"*[^\">]*/i
) do |link
|
932 link
= link
.sub(/href="/i
, "")
933 next if link
=~
/svnindex.xsl$/
934 next if link
=~
/^(\w*:|)\/\
// || link
=~
/^\./
935 links
<< File
.join(base_url
, link
)
941 puts
"+ #{File.join(@cwd, File.basename(link))}" unless @quiet
942 open(link
) do |stream
|
943 File
.open(File
.join(@cwd, File
.basename(link
)), "wb") do |file
|
944 file
.write(stream
.read
)
949 def fetch(links
= @urls_to_fetch)
951 (l
=~
/\/$/ || links
== @urls_to_fetch) ? fetch_dir(l
) : download(l
)
957 push_d(File
.basename(url
)) if @level > 0
958 open(url
) do |stream
|
959 contents
= stream
.read
960 fetch(links(url
, contents
))
967 Commands
::Plugin.parse
!