drupid 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,585 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Copyright (c) 2012 Lifepillar
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ # Portions Copyright 2009-2011 Max Howell and other contributors.
24
+ #
25
+ # Redistribution and use in source and binary forms, with or without
26
+ # modification, are permitted provided that the following conditions
27
+ # are met:
28
+ #
29
+ # 1. Redistributions of source code must retain the above copyright
30
+ # notice, this list of conditions and the following disclaimer.
31
+ # 2. Redistributions in binary form must reproduce the above copyright
32
+ # notice, this list of conditions and the following disclaimer in the
33
+ # documentation and/or other materials provided with the distribution.
34
+ #
35
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
36
+ # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
37
+ # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
38
+ # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
39
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
40
+ # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
41
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
42
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
43
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
44
+ # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
45
+
46
+ require 'pathname'
47
+ require 'uri'
48
+
49
+ module Drupid
50
+
51
+ def self.makeDownloader uri, dest, name, download_specs = {}
52
+ case download_specs[:type]
53
+ when 'file'
54
+ DownloadStrategy::Curl.new uri, dest, name, download_specs
55
+ when 'git'
56
+ DownloadStrategy::Git.new uri, dest, name, download_specs
57
+ when 'svn'
58
+ DownloadStrategy::Subversion.new uri, dest, name, download_specs
59
+ when 'cvs'
60
+ DownloadStrategy::CVS.new uri, dest, name, download_specs
61
+ when 'bzr'
62
+ DownloadStrategy::Bazaar.new uri, dest, name, download_specs
63
+ else
64
+ (DownloadStrategy.detect uri).new uri, dest, name, download_specs
65
+ end
66
+ end
67
+
68
+ module DownloadStrategy
69
+
70
+ def self.detect url
71
+ case url
72
+ when %r[^file://] then Curl
73
+ # We use a special URL pattern for cvs
74
+ when %r[^cvs://] then CVS
75
+ # Standard URLs
76
+ when %r[^bzr://] then Bazaar
77
+ when %r[^git://] then Git
78
+ when %r[^https?://.+\.git$] then Git
79
+ when %r[^hg://] then Mercurial
80
+ when %r[^svn://] then Subversion
81
+ when %r[^svn\+http://] then Subversion
82
+ when %r[^fossil://] then Fossil
83
+ # Some well-known source hosts
84
+ when %r[^https?://(.+?\.)?googlecode\.com/hg] then Mercurial
85
+ when %r[^https?://(.+?\.)?googlecode\.com/svn] then Subversion
86
+ when %r[^https?://(.+?\.)?sourceforge\.net/svnroot/] then Subversion
87
+ when %r[^http://svn.apache.org/repos/] then Subversion
88
+ when %r[^http://www.apache.org/dyn/closer.cgi] then CurlApacheMirror
89
+ # Common URL patterns
90
+ when %r[^https?://svn\.] then Subversion
91
+ when %r[\.git$] then Git
92
+ when %r[\/] then Curl
93
+ else Drush
94
+ end
95
+ end
96
+
97
+ class CurlError < RuntimeError
98
+ end
99
+
100
+ # [url] The URL to download from
101
+ # [dest] The target directory for the download
102
+ # [name] The name (without extension) to assign to the downloaded entity
103
+ # [download_specs] A hash of optional download parameters.
104
+ class Base
105
+ include Drupid::Utils
106
+
107
+ attr :url
108
+ attr :dest
109
+ attr :name
110
+ attr :staged_path
111
+
112
+ def initialize url, dest, name, download_specs = {}
113
+ @url = url
114
+ @dest = Pathname.new(dest).expand_path
115
+ @name = name
116
+ @specs = download_specs
117
+ @staged_path = nil
118
+ debug "#{self.class.to_s.split(/::/).last} downloader created for url=#{@url}, dest=#{@dest}, name=#{@name}"
119
+ debug "Download specs: #{download_specs}" unless download_specs.empty?
120
+ end
121
+ end
122
+
123
+
124
+ class Curl < Base
125
+ attr :tarball_path
126
+
127
+ def initialize url, dest, name = nil, download_specs = {}
128
+ super
129
+ if @name.to_s.empty?
130
+ @tarball_path = @dest + File.basename(@url)
131
+ else
132
+ # Do not add an extension if the provided name has an extension
133
+ n = @name.match(/\.\w+$/) ? @name : @name+ext
134
+ @tarball_path = @dest + n
135
+ end
136
+ if @specs.has_key?('file_type')
137
+ @tarball_path = @tarball_path.sub_ext('.' + @specs['file_type'])
138
+ end
139
+ end
140
+
141
+ protected
142
+
143
+ # Private method, can be overridden if needed.
144
+ def _fetch
145
+ if @specs.has_key?('post_data')
146
+ curl @url, '-d', @specs['post_data'], '-o', @tarball_path
147
+ else
148
+ curl @url, '-o', @tarball_path
149
+ end
150
+ end
151
+
152
+ public
153
+
154
+ # Retrieves a file from this object's URL.
155
+ def fetch
156
+ @tarball_path.rmtree if @tarball_path.exist?
157
+ begin
158
+ debug "Pathname.mkpath may raise harmless exceptions"
159
+ @dest.mkpath unless @dest.exist?
160
+ _fetch
161
+ rescue Exception => e
162
+ ignore_interrupts { @tarball_path.unlink if @tarball_path.exist? }
163
+ if e.kind_of? ErrorDuringExecution
164
+ raise CurlError, "Download failed: #{@url}"
165
+ else
166
+ raise
167
+ end
168
+ end
169
+ return @tarball_path
170
+ end
171
+
172
+ # Stages this download into the specified directory.
173
+ # Invokes #fetch to retrieve the file if needed.
174
+ def stage wd = @dest
175
+ fetch unless @tarball_path.exist?
176
+ debug "Pathname.mkpath may raise harmless exceptions"
177
+ wd.mkpath unless wd.exist?
178
+ target = wd + @tarball_path.basename
179
+ type = @tarball_path.compression_type
180
+ if type
181
+ tempdir do # uncompress inside a temporary directory
182
+ uncompress @tarball_path, :type => type
183
+ # Move extracted archive into the destination
184
+ content = Pathname.pwd.children
185
+ if 1 == content.size and content.first.directory?
186
+ src = content.first
187
+ target = wd + src.basename
188
+ FileUtils.mv src.to_s, wd.to_s, :force => true, :verbose => $DEBUG
189
+ else # the archive did not have a root folder or it expanded to a file instead of a folder
190
+ # We cannot move the temporary directory we are in, so we copy its content
191
+ src = Pathname.pwd
192
+ target = wd + src.basename
193
+ target.rmtree if target.exist? # Overwrite
194
+ target.mkpath
195
+ src.ditto target
196
+ end
197
+ debug "Temporary staging target: #{target}"
198
+ end
199
+ elsif wd != @dest
200
+ FileUtils.mv @tarball_path.to_s, wd.to_s, :force => true, :verbose => $DEBUG
201
+ end
202
+ if @name and @name != target.basename.to_s
203
+ new_path = target.dirname + @name
204
+ new_path.rmtree if new_path.exist? # Overwrite
205
+ File.rename target.to_s, new_path.to_s
206
+ target = target.dirname+@name
207
+ end
208
+ @staged_path = target
209
+ end
210
+
211
+ private
212
+
213
+ def ext
214
+ # GitHub uses odd URLs for zip files, so check for those
215
+ rx=%r[https?://(www\.)?github\.com/.*/(zip|tar)ball/]
216
+ if rx.match @url
217
+ if $2 == 'zip'
218
+ '.zip'
219
+ else
220
+ '.tgz'
221
+ end
222
+ else
223
+ Pathname.new(@url).extname # uses extended extname that supports double extensions
224
+ end
225
+ end
226
+
227
+ end # Curl
228
+
229
+
230
+ class Drush < Curl
231
+ def initialize url, dest, name, download_specs = {}
232
+ super
233
+ @tarball_path = @dest + @name
234
+ end
235
+
236
+ def _fetch
237
+ output = Drupid::Drush.pm_download url, :destination => dest
238
+ p = Drupid::Drush.download_path(output)
239
+ if p
240
+ @tarball_path = Pathname.new(p).realpath
241
+ else
242
+ raise "Download failed for project #{name} (using Drush):\n#{output}"
243
+ end
244
+ end
245
+ end # Drush
246
+
247
+
248
+ # Detect and download from Apache Mirror
249
+ class CurlApacheMirror < Curl
250
+ def _fetch
251
+ # Fetch mirror list site
252
+ require 'open-uri'
253
+ mirror_list = open(@url).read()
254
+
255
+ # Parse out suggested mirror
256
+ # Yep, this is ghetto, grep the first <strong></strong> element content
257
+ mirror_url = mirror_list[/<strong>([^<]+)/, 1]
258
+
259
+ raise "Couldn't determine mirror. Try again later." if mirror_url.nil?
260
+
261
+ blah "Best Mirror #{mirror_url}"
262
+ # Start download from that mirror
263
+ curl mirror_url, '-o', @tarball_path
264
+ end
265
+ end # CurlApacheMirror
266
+
267
+
268
+ class Git < Base
269
+ def initialize url, dest, name, download_specs = {}
270
+ super
271
+ @clone = @dest + @name
272
+ end
273
+
274
+ def support_depth?
275
+ !(@specs.has_key?('revision')) and host_supports_depth?
276
+ end
277
+
278
+ def host_supports_depth?
279
+ @url =~ %r(git://) or @url =~ %r(https://github.com/)
280
+ end
281
+
282
+ def fetch
283
+ raise "You must install Git." unless which "git"
284
+
285
+ blah "Cloning #{@url}"
286
+
287
+ if @clone.exist?
288
+ Dir.chdir(@clone) do
289
+ # Check for interrupted clone from a previous install
290
+ unless system 'git', 'status', '-s'
291
+ blah "Removing invalid .git repo from cache"
292
+ FileUtils.rm_rf @clone
293
+ end
294
+ end
295
+ end
296
+
297
+ unless @clone.exist?
298
+ clone_args = ['clone']
299
+ clone_args << '--depth' << '1' if support_depth?
300
+
301
+ if @specs.has_key?('branch')
302
+ clone_args << '--branch' << @specs['branch']
303
+ elsif @specs.has_key?('tag')
304
+ clone_args << '--branch' << @specs['tag']
305
+ end
306
+
307
+ clone_args << @url << @clone
308
+ git(*clone_args)
309
+ else
310
+ blah "Updating #{@clone}"
311
+ Dir.chdir(@clone) do
312
+ git 'config', 'remote.origin.url', @url
313
+
314
+ rof =
315
+ if @specs.has_key?('branch')
316
+ "+refs/heads/#{@specs['branch']}:refs/remotes/origin/#{@specs['branch']}"
317
+ elsif @specs.has_key?('tag')
318
+ "+refs/tags/#{@specs['tag']}:refs/tags/#{@specs['tag']}"
319
+ else
320
+ '+refs/heads/master:refs/remotes/origin/master'
321
+ end
322
+ git 'config', 'remote.origin.fetch', rof
323
+
324
+ git_args = %w[fetch origin]
325
+ git(*git_args)
326
+ end
327
+ end
328
+ end
329
+
330
+ # Stages this download into the specified directory.
331
+ # Invokes #fetch to retrieve the file if needed.
332
+ def stage wd = @dest
333
+ fetch unless @clone.exist?
334
+ debug "Pathname.mkpath may raise harmless exceptions"
335
+ wd.mkpath unless wd.exist?
336
+ target = wd + @clone.basename
337
+ Dir.chdir @clone do
338
+ if @specs.has_key?('branch')
339
+ git 'checkout', "origin/#{@specs['branch']}", '--'
340
+ elsif @specs.has_key?('tag')
341
+ git 'checkout', @specs['tag'], '--'
342
+ elsif @specs.has_key?('revision')
343
+ git 'checkout', @specs['revision'], '--'
344
+ else
345
+ # otherwise the checkout-index won't checkout HEAD
346
+ # https://github.com/mxcl/homebrew/issues/7124
347
+ # must specify origin/HEAD, otherwise it resets to the current local HEAD
348
+ git 'reset', '--hard', 'origin/HEAD'
349
+ end
350
+ # http://stackoverflow.com/questions/160608/how-to-do-a-git-export-like-svn-export
351
+ git 'checkout-index', '-a', '-f', "--prefix=#{target}/"
352
+ # check for submodules
353
+ if File.exist?('.gitmodules')
354
+ git 'submodule', 'init'
355
+ git 'submodule', 'update'
356
+ sub_cmd = "git checkout-index -a -f \"--prefix=#{target}/$path/\""
357
+ git 'submodule', '--quiet', 'foreach', '--recursive', sub_cmd
358
+ end
359
+ end
360
+ @staged_path = target
361
+ end
362
+ end # Git
363
+
364
+
365
+ class Subversion < Base
366
+ def initialize url, dest, name, download_specs = {}
367
+ super
368
+ @co = @dest + @name
369
+ end
370
+
371
+ def fetch
372
+ @url.sub!(/^svn\+/, '') if @url =~ %r[^svn\+http://]
373
+ blah "Checking out #{@url}"
374
+ if @specs.has_key?('revision')
375
+ fetch_repo @co, @url, @specs['revision']
376
+ # elsif @specs.has_key?('revisions')
377
+ # # nil is OK for main_revision, as fetch_repo will then get latest
378
+ # main_revision = @ref.delete :trunk
379
+ # fetch_repo @co, @url, main_revision, true
380
+ #
381
+ # get_externals do |external_name, external_url|
382
+ # fetch_repo @co+external_name, external_url, @ref[external_name], true
383
+ # end
384
+ else
385
+ fetch_repo @co, @url
386
+ end
387
+ end
388
+
389
+ def stage wd = @dest
390
+ fetch unless @co.exist?
391
+ debug "Pathname.mkpath may raise harmless exceptions"
392
+ wd.mkpath unless wd.exist?
393
+ target = wd + @co.basename
394
+ svn 'export', '--force', @co, target
395
+ end
396
+
397
+ def get_externals
398
+ output = svn 'propget', 'svn:externals', @url
399
+ output.chomp.each_line do |line|
400
+ name, url = line.split(/\s+/)
401
+ yield name, url
402
+ end
403
+ end
404
+
405
+ def fetch_repo target, url, revision=nil, ignore_externals=false
406
+ # Use "svn up" when the repository already exists locally.
407
+ # This saves on bandwidth and will have a similar effect to verifying the
408
+ # cache as it will make any changes to get the right revision.
409
+ svncommand = target.exist? ? 'up' : 'checkout'
410
+ args = [svncommand]
411
+ args << '--non-interactive' unless @specs.has_key?('interactive') and 'true' == @specs.has_key?('interactive')
412
+ args << '--trust-server-cert'
413
+ # SVN shipped with XCode 3.1.4 can't force a checkout.
414
+ #args << '--force' unless MacOS.leopard? and svn == '/usr/bin/svn'
415
+ args << url if !target.exist?
416
+ args << target
417
+ args << '-r' << revision if revision
418
+ args << '--ignore-externals' if ignore_externals
419
+ svn(*args)
420
+ end
421
+ end # Subversion
422
+
423
+
424
+ class CVS < Base
425
+ def initialize url, dest, name, download_specs = {}
426
+ super
427
+ @co = @dest + @name
428
+ end
429
+
430
+ def fetch
431
+ blah "Checking out #{@url}"
432
+
433
+ # URL of cvs cvs://:pserver:anoncvs@www.gccxml.org:/cvsroot/GCC_XML:gccxml
434
+ # will become:
435
+ # cvs -d :pserver:anoncvs@www.gccxml.org:/cvsroot/GCC_XML login
436
+ # cvs -d :pserver:anoncvs@www.gccxml.org:/cvsroot/GCC_XML co gccxml
437
+ mod, url = split_url(@url)
438
+
439
+ unless @co.exist?
440
+ Dir.chdir @dest do
441
+ cvs '-d', url, 'login'
442
+ cvs '-d', url, 'checkout', '-d', @name, mod
443
+ end
444
+ else
445
+ blah "Updating #{@co}"
446
+ Dir.chdir(@co) { cvs 'up' }
447
+ end
448
+ end
449
+
450
+ def stage wd = @dest
451
+ fetch unless @co.exist?
452
+ debug "Pathname.mkpath may raise harmless exceptions"
453
+ wd.mkpath unless wd.exist?
454
+ target = wd + @co.basename
455
+ FileUtils.cp_r Dir[@co+"{.}"], target
456
+
457
+ require 'find'
458
+ Find.find(Dir.pwd) do |path|
459
+ if FileTest.directory?(path) && File.basename(path) == "CVS"
460
+ Find.prune
461
+ FileUtil.rm_r path, :force => true
462
+ end
463
+ end
464
+ end
465
+
466
+ private
467
+ def split_url(in_url)
468
+ parts=in_url.sub(%r[^cvs://], '').split(/:/)
469
+ mod=parts.pop
470
+ url=parts.join(':')
471
+ [ mod, url ]
472
+ end
473
+ end # CVS
474
+
475
+
476
+ class Mercurial < Base
477
+ def initialize url, dest, name, download_specs = {}
478
+ super
479
+ @clone = @dest + @name
480
+ end
481
+
482
+ def fetch
483
+ blah "Cloning #{@url}"
484
+
485
+ unless @clone.exist?
486
+ url=@url.sub(%r[^hg://], '')
487
+ hg 'clone', url, @clone
488
+ else
489
+ blah "Updating #{@clone}"
490
+ Dir.chdir(@clone) do
491
+ hg 'pull'
492
+ hg 'update'
493
+ end
494
+ end
495
+ end
496
+
497
+ def stage wd = @dest
498
+ fetch unless @co.exist?
499
+ debug "Pathname.mkpath may raise harmless exceptions"
500
+ wd.mkpath unless wd.exist?
501
+ dst = wd + @co.basename
502
+ Dir.chdir @clone do
503
+ #if @spec and @ref
504
+ # blah "Checking out #{@spec} #{@ref}"
505
+ # Dir.chdir @clone do
506
+ # safe_system 'hg', 'archive', '-y', '-r', @ref, '-t', 'files', dst
507
+ # end
508
+ #else
509
+ hg 'archive', '-y', '-t', 'files', dst
510
+ #end
511
+ end
512
+ end
513
+ end # Mercurial
514
+
515
+
516
+ class Bazaar < Base
517
+ def initialize url, dest, name, download_specs = {}
518
+ super
519
+ @clone = @dest + @name
520
+ end
521
+
522
+ def fetch
523
+ blah "Cloning #{@url}"
524
+ unless @clone.exist?
525
+ url=@url.sub(%r[^bzr://], '')
526
+ # 'lightweight' means history-less
527
+ bzr 'checkout', '--lightweight', url, @clone
528
+ else
529
+ blah "Updating #{@clone}"
530
+ Dir.chdir(@clone) { bzr 'update' }
531
+ end
532
+ end
533
+
534
+ def stage
535
+ # FIXME: The export command doesn't work on checkouts
536
+ # See https://bugs.launchpad.net/bzr/+bug/897511
537
+ FileUtils.cp_r Dir[@clone+"{.}"], Dir.pwd
538
+ FileUtils.rm_r Dir[Dir.pwd+"/.bzr"]
539
+
540
+ #dst=Dir.getwd
541
+ #Dir.chdir @clone do
542
+ # if @spec and @ref
543
+ # ohai "Checking out #{@spec} #{@ref}"
544
+ # Dir.chdir @clone do
545
+ # safe_system 'bzr', 'export', '-r', @ref, dst
546
+ # end
547
+ # else
548
+ # safe_system 'bzr', 'export', dst
549
+ # end
550
+ #end
551
+ end
552
+ end # Bazaar
553
+
554
+
555
+ class Fossil < Base
556
+ def initialize url, dest, name, download_specs = {}
557
+ super
558
+ @clone = @dest + @name
559
+ end
560
+
561
+ def fetch
562
+ raise "You must install fossil first" unless which "fossil"
563
+
564
+ blah "Cloning #{@url}"
565
+ unless @clone.exist?
566
+ url=@url.sub(%r[^fossil://], '')
567
+ runBabyRun 'fossil', ['clone', url, @clone]
568
+ else
569
+ blah "Updating #{@clone}"
570
+ runBabyRun 'fossil', ['pull', '-R', @clone]
571
+ end
572
+ end
573
+
574
+ def stage
575
+ # TODO: The 'open' and 'checkout' commands are very noisy and have no '-q' option.
576
+ runBabyRun 'fossil', ['open', @clone]
577
+ #if @spec and @ref
578
+ # ohai "Checking out #{@spec} #{@ref}"
579
+ # safe_system 'fossil', 'checkout', @ref
580
+ #end
581
+ end
582
+ end # Fossil
583
+
584
+ end # module DownloadStrategy
585
+ end # module Drupid