drupid 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/drupid +270 -0
- data/lib/drupid/component.rb +236 -0
- data/lib/drupid/download_strategy.rb +585 -0
- data/lib/drupid/drush.rb +185 -0
- data/lib/drupid/extend/pathname.rb +114 -0
- data/lib/drupid/library.rb +52 -0
- data/lib/drupid/makefile.rb +423 -0
- data/lib/drupid/patch.rb +92 -0
- data/lib/drupid/platform.rb +234 -0
- data/lib/drupid/platform_project.rb +91 -0
- data/lib/drupid/project.rb +563 -0
- data/lib/drupid/updater.rb +683 -0
- data/lib/drupid/utils.rb +301 -0
- data/lib/drupid/version.rb +230 -0
- data/lib/drupid.rb +56 -0
- metadata +76 -0
@@ -0,0 +1,683 @@
|
|
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
|
+
module Drupid
|
24
|
+
# Helper class to build or update a Drupal installation.
|
25
|
+
class Updater
|
26
|
+
include Drupid::Utils
|
27
|
+
|
28
|
+
# A Drupid::Makefile object.
|
29
|
+
attr :makefile
|
30
|
+
# A Drupid::Platform object.
|
31
|
+
attr :platform
|
32
|
+
# (For multisite platforms) the site to be synchronized.
|
33
|
+
attr :site
|
34
|
+
# The updater's log.
|
35
|
+
attr :log
|
36
|
+
|
37
|
+
# Creates a new updater for a given makefile and a given platform.
|
38
|
+
# For multisite platforms, optionally specify a site to synchronize.
|
39
|
+
def initialize(makefile, platform, site_name = nil)
|
40
|
+
@makefile = makefile
|
41
|
+
@platform = platform
|
42
|
+
@site = site_name
|
43
|
+
@log = Log.new
|
44
|
+
#
|
45
|
+
@libraries_paths = Array.new
|
46
|
+
@core_projects = Array.new
|
47
|
+
@derivative_builds = Array.new
|
48
|
+
@excluded_projects = Hash.new
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a list of names of projects that must be considered
|
52
|
+
# as processed when synchronizing. This always include all
|
53
|
+
# Drupal core projects, but other projects may be added.
|
54
|
+
#
|
55
|
+
# Requires: this updater's platform must have been analyzed
|
56
|
+
# (see Drupid::Platform.analyze).
|
57
|
+
def excluded
|
58
|
+
@excluded_projects.keys
|
59
|
+
end
|
60
|
+
|
61
|
+
# Adds the given list of project names to the exclusion set of this updater.
|
62
|
+
def exclude(project_list)
|
63
|
+
project_list.each { |p| @excluded_projects[p] = true }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns true if the given project is in the exclusion set of this updater;
|
67
|
+
# returns false otherwise.
|
68
|
+
def excluded?(project_name)
|
69
|
+
@excluded_projects.has_key?(project_name)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns true if there are actions that have not been
|
73
|
+
# applied to the platform (including actions in derivative builds);
|
74
|
+
# returns false otherwise.
|
75
|
+
def pending_actions?
|
76
|
+
return true if @log.pending_actions?
|
77
|
+
@derivative_builds.each do |d|
|
78
|
+
return true if d.pending_actions?
|
79
|
+
end
|
80
|
+
return false
|
81
|
+
end
|
82
|
+
|
83
|
+
# Enqueues a derivative build based on the specified project
|
84
|
+
# (which is typically, but not necessarily, an installation profile).
|
85
|
+
# Does nothing if the project does not contain any makefile whose name
|
86
|
+
# coincides with the name of the project.
|
87
|
+
def prepare_derivative_build(project)
|
88
|
+
mf = project.makefile
|
89
|
+
return false if mf.nil?
|
90
|
+
debug "Preparing derivative build for #{mf.basename}"
|
91
|
+
submake = Makefile.new(mf)
|
92
|
+
subplatform = Platform.new(@platform.local_path)
|
93
|
+
subplatform.contrib_path = @platform.dest_path(project)
|
94
|
+
new_updater = Updater.new(submake, subplatform, site)
|
95
|
+
new_updater.exclude(project.extensions)
|
96
|
+
new_updater.exclude(@platform.profiles)
|
97
|
+
@derivative_builds << new_updater
|
98
|
+
return true
|
99
|
+
end
|
100
|
+
|
101
|
+
# Tries to reconcile the makefile with the platform by resolving unmet
|
102
|
+
# dependencies and determining which projects must be installed, upgraded,
|
103
|
+
# downgraded, moved or removed. This method does not return anything.
|
104
|
+
# The result of the synchronization can be inspected by accessing
|
105
|
+
# Drupid::Updater#log.
|
106
|
+
#
|
107
|
+
# This method does not modify the platform at all, it only preflights changes
|
108
|
+
# and caches the needed stuff locally. For changes to be applied,
|
109
|
+
# Drupid::Updater#apply_changes must be invoked after this method
|
110
|
+
# has been invoked.
|
111
|
+
#
|
112
|
+
# If :nofollow is set to true, then this method does not try to resolve missing
|
113
|
+
# dependencies: it only checks the projects that are explicitly mentioned
|
114
|
+
# in the makefile. If :nocore is set to true, only contrib projects are
|
115
|
+
# synchronized; otherwise, Drupal core is synchronized, too.
|
116
|
+
#
|
117
|
+
#
|
118
|
+
# See also: Drupid::Updater#apply_changes
|
119
|
+
#
|
120
|
+
# Options: nofollow, nocore, nolibs
|
121
|
+
def sync(options = {})
|
122
|
+
@log.clear
|
123
|
+
@platform.analyze
|
124
|
+
# These paths are needed because Drupal allows libraries to be installed
|
125
|
+
# inside modules. Hence, we must ignore them when synchronizing those modules.
|
126
|
+
@makefile.each_library do |l|
|
127
|
+
@libraries_paths << @platform.local_path + @platform.contrib_path + l.target_path
|
128
|
+
end
|
129
|
+
# We always need a local copy of Drupal core (either the version specified
|
130
|
+
# in the makefile or the latest version), even if we are not going to
|
131
|
+
# synchronize the core, in order to extract the list of core projects.
|
132
|
+
if get_drupal
|
133
|
+
if options[:nocore]
|
134
|
+
blah "Skipping core"
|
135
|
+
else
|
136
|
+
sync_drupal_core
|
137
|
+
end
|
138
|
+
else
|
139
|
+
return
|
140
|
+
end
|
141
|
+
sync_projects(options)
|
142
|
+
sync_libraries unless options[:nolibs]
|
143
|
+
# Process derivative builds
|
144
|
+
@derivative_builds.each do |updater|
|
145
|
+
updater.sync(options.merge(:nocore => true))
|
146
|
+
@log.merge(updater.log)
|
147
|
+
end
|
148
|
+
return
|
149
|
+
end
|
150
|
+
|
151
|
+
def get_drupal
|
152
|
+
drupal = @makefile.drupal_project
|
153
|
+
unless drupal # Nothing to do
|
154
|
+
owarn 'No Drupal project specified.'
|
155
|
+
return false
|
156
|
+
end
|
157
|
+
return false unless _fetch_and_patch(drupal)
|
158
|
+
# Extract information about core projects, which must not be synchronized
|
159
|
+
temp_platform = Platform.new(drupal.local_path)
|
160
|
+
temp_platform.analyze
|
161
|
+
@core_projects = temp_platform.core_project_names
|
162
|
+
return true
|
163
|
+
end
|
164
|
+
|
165
|
+
# Synchronizes Drupal core.
|
166
|
+
# Returns true if the synchronization is successful;
|
167
|
+
# returns false otherwise.
|
168
|
+
def sync_drupal_core
|
169
|
+
if @platform.drupal_project
|
170
|
+
_compare_versions @makefile.drupal_project, @platform.drupal_project
|
171
|
+
else
|
172
|
+
log.action(InstallProjectAction.new(@platform, @makefile.drupal_project))
|
173
|
+
end
|
174
|
+
return true
|
175
|
+
end
|
176
|
+
|
177
|
+
# Synchronizes projects between the makefile and the platform.
|
178
|
+
#
|
179
|
+
# Options: nofollow
|
180
|
+
def sync_projects(options = {})
|
181
|
+
exclude(@core_projects) # Skip core projects
|
182
|
+
processed = Array.new(excluded) # List of names of processed projects
|
183
|
+
dep_queue = Array.new # Queue of Drupid::Project objects whose dependencies must be checked. This is always a subset of processed.
|
184
|
+
|
185
|
+
@makefile.each_project do |makefile_project|
|
186
|
+
dep_queue << makefile_project if sync_project(makefile_project)
|
187
|
+
processed += makefile_project.extensions
|
188
|
+
end
|
189
|
+
|
190
|
+
unless options[:nofollow]
|
191
|
+
# Recursively get dependent projects.
|
192
|
+
# An invariant is that each project in the dependency queue has been processed
|
193
|
+
# and cached locally. Hence, it has a version and its path points to the
|
194
|
+
# cached copy.
|
195
|
+
while not dep_queue.empty? do
|
196
|
+
project = dep_queue.shift
|
197
|
+
project.dependencies.each do |dependent_project_name|
|
198
|
+
unless processed.include?(dependent_project_name)
|
199
|
+
debug "Queue dependency: #{dependent_project_name} <- #{project.extended_name}"
|
200
|
+
new_project = Project.new(dependent_project_name, project.core)
|
201
|
+
dep_queue << new_project if sync_project(new_project)
|
202
|
+
@makefile.add_project(new_project)
|
203
|
+
processed += new_project.extensions
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Determine projects that should be deleted
|
210
|
+
pending_delete = @platform.project_names - processed
|
211
|
+
pending_delete.each do |p|
|
212
|
+
proj = platform.get_project(p)
|
213
|
+
if proj.installed?(site)
|
214
|
+
log.error "#{proj.extended_name} cannot be deleted because it is installed"
|
215
|
+
end
|
216
|
+
log.action(DeleteAction.new(platform, proj))
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Performs the necessary synchronization actions for the given project.
|
221
|
+
# Returns true if the dependencies of the given project must be synchronized, too;
|
222
|
+
# returns false otherwise.
|
223
|
+
def sync_project(project)
|
224
|
+
return false unless _fetch_and_patch(project)
|
225
|
+
|
226
|
+
# Does this project contains a makefile? If so, enqueue a derivative build.
|
227
|
+
has_makefile = prepare_derivative_build(project)
|
228
|
+
|
229
|
+
# Ignore libraries that may be installed inside this project
|
230
|
+
pp = @platform.local_path + @platform.dest_path(project)
|
231
|
+
@libraries_paths.each do |lp|
|
232
|
+
if lp.fnmatch?(pp.to_s + '/*')
|
233
|
+
project.ignore_path(lp.relative_path_from(pp))
|
234
|
+
@log.notice("Ignoring #{project.ignore_paths.last} inside #{project.extended_name}")
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Does the project exist in the platform? If so, compare the two.
|
239
|
+
if @platform.has_project?(project.name)
|
240
|
+
platform_project = @platform.get_project(project.name)
|
241
|
+
# Fix project location
|
242
|
+
new_path = @platform.dest_path(project)
|
243
|
+
if @platform.local_path + new_path != platform_project.local_path
|
244
|
+
log.action(MoveAction.new(@platform, platform_project, new_path))
|
245
|
+
if (@platform.local_path + new_path).exist?
|
246
|
+
log.error("#{new_path} already exists. Use --force to overwrite.")
|
247
|
+
end
|
248
|
+
end
|
249
|
+
# Compare versions and log suitable actions
|
250
|
+
_compare_versions project, platform_project
|
251
|
+
|
252
|
+
# If analyzing the platform does not detect the project (e.g., Fusion),
|
253
|
+
# we try to see if the directory exists where it is supposed to be.
|
254
|
+
elsif (@platform.local_path + @platform.dest_path(project)).exist?
|
255
|
+
begin
|
256
|
+
platform_project = PlatformProject.new(@platform, @platform.local_path + @platform.dest_path(project))
|
257
|
+
rescue => ex
|
258
|
+
log.error("#{platform_project.relative_path} exists, but cannot be analyzed: #{ex}")
|
259
|
+
log.action(UpdateProjectAction.new(@platform, project))
|
260
|
+
end
|
261
|
+
_compare_versions project, platform_project
|
262
|
+
else # new project
|
263
|
+
log.action(InstallProjectAction.new(@platform, project))
|
264
|
+
end
|
265
|
+
|
266
|
+
return (not has_makefile)
|
267
|
+
end
|
268
|
+
|
269
|
+
# Synchronizes libraries between the makefile and the platform.
|
270
|
+
def sync_libraries
|
271
|
+
debug 'Syncing libraries'
|
272
|
+
processed_paths = []
|
273
|
+
@makefile.each_library do |lib|
|
274
|
+
sync_library(lib)
|
275
|
+
processed_paths << lib.target_path
|
276
|
+
end
|
277
|
+
# Determine libraries that should be deleted from the 'libraries' folder.
|
278
|
+
# The above is a bit of an overstatement, as it is basically impossible
|
279
|
+
# to detect a "library" in a reliable way. What we actually do is just
|
280
|
+
# deleting "spurious" paths inside the 'libraries' folder.
|
281
|
+
# Note also that Drupid is not smart enough to find libraries installed
|
282
|
+
# inside modules or themes.
|
283
|
+
Pathname.glob(@platform.libraries_path + '**/*').each do |p|
|
284
|
+
next unless p.directory?
|
285
|
+
q = p.relative_path_from(@platform.local_path + @platform.contrib_path)
|
286
|
+
# If q is not a prefix of any processed path, or viceversa, delete it
|
287
|
+
if processed_paths.find_all { |pp| pp.fnmatch(q.to_s + '*') or q.fnmatch(pp.to_s + '*') }.empty?
|
288
|
+
l = Library.new(p.basename)
|
289
|
+
l.local_path = p
|
290
|
+
log.action(DeleteAction.new(@platform, l))
|
291
|
+
# Do not need to delete subdirectories
|
292
|
+
processed_paths << q
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def sync_library(lib)
|
298
|
+
return false unless _fetch_and_patch(lib)
|
299
|
+
|
300
|
+
platform_lib = Library.new(lib.name)
|
301
|
+
relpath = @platform.contrib_path + lib.target_path
|
302
|
+
libpath = @platform.local_path + relpath
|
303
|
+
platform_lib.local_path = libpath
|
304
|
+
if platform_lib.exist?
|
305
|
+
begin
|
306
|
+
diff = lib.file_level_compare_with platform_lib
|
307
|
+
rescue => ex
|
308
|
+
odie "Failed to verify the integrity of library #{lib.extended_name}: #{ex}"
|
309
|
+
end
|
310
|
+
if diff.empty?
|
311
|
+
log.notice("[OK] #{lib.extended_name} (#{relpath})")
|
312
|
+
else
|
313
|
+
log.action(UpdateLibraryAction.new(platform, lib))
|
314
|
+
log.notice(diff.join("\n"))
|
315
|
+
end
|
316
|
+
else
|
317
|
+
log.action(InstallLibraryAction.new(platform, lib))
|
318
|
+
end
|
319
|
+
return true
|
320
|
+
end
|
321
|
+
|
322
|
+
# Applies any pending changes. This is the method that actually
|
323
|
+
# modifies the platform. Note that applying changes may be
|
324
|
+
# destructive (projects may be upgraded, downgraded, deleted from
|
325
|
+
# the platform, moved and/or patched).
|
326
|
+
# *Always* *backup* your site before calling this method!
|
327
|
+
# If :force is set to true, changes are applied even if there are errors.
|
328
|
+
#
|
329
|
+
# See also: Drupid::Updater.sync
|
330
|
+
#
|
331
|
+
# Options: force, no_lockfile
|
332
|
+
def apply_changes(options = {})
|
333
|
+
raise "No changes can be applied because there are errors." if log.errors? and not options[:force]
|
334
|
+
log.apply_pending_actions
|
335
|
+
@derivative_builds.each { |updater| updater.apply_changes(options.merge(:no_lockfile => true)) }
|
336
|
+
@log.clear
|
337
|
+
@derivative_builds.clear
|
338
|
+
end
|
339
|
+
|
340
|
+
private
|
341
|
+
|
342
|
+
# Returns true if the given component is successfully cached and patched;
|
343
|
+
# return false otherwise.
|
344
|
+
def _fetch_and_patch component
|
345
|
+
begin
|
346
|
+
component.fetch
|
347
|
+
rescue => ex
|
348
|
+
@log.error("#{component.extended_name} could not be fetched: #{ex.message}")
|
349
|
+
return false
|
350
|
+
end
|
351
|
+
if component.has_patches?
|
352
|
+
begin
|
353
|
+
component.patch
|
354
|
+
rescue => ex
|
355
|
+
@log.error("#{component.extended_name}: #{ex.message}")
|
356
|
+
return false
|
357
|
+
end
|
358
|
+
end
|
359
|
+
return true
|
360
|
+
end
|
361
|
+
|
362
|
+
# Compare project versions and log suitable actions.
|
363
|
+
def _compare_versions(makefile_project, platform_project)
|
364
|
+
update_action = UpdateProjectAction.new(platform, makefile_project)
|
365
|
+
case makefile_project <=> platform_project
|
366
|
+
when 0 # up to date
|
367
|
+
# Check whether the content of the projects is consistent
|
368
|
+
begin
|
369
|
+
diff = makefile_project.file_level_compare_with platform_project
|
370
|
+
rescue => ex
|
371
|
+
odie "Failed to verify the integrity of #{makefile_project.extended_name}: #{ex}"
|
372
|
+
end
|
373
|
+
p = (makefile_project.drupal?) ? '' : ' (' + (platform.contrib_path + makefile_project.target_path).to_s + ')'
|
374
|
+
if diff.empty?
|
375
|
+
@log.notice("[OK] #{platform_project.extended_name}#{p}")
|
376
|
+
elsif makefile_project.has_patches?
|
377
|
+
log.action(update_action)
|
378
|
+
log.notice "#{makefile_project.extended_name}#{p} will be patched"
|
379
|
+
log.notice(diff.join("\n"))
|
380
|
+
else
|
381
|
+
log.error("#{platform_project.extended_name}#{p}: mismatch with cached copy:\n" + diff.join("\n"))
|
382
|
+
log.action(update_action)
|
383
|
+
end
|
384
|
+
when 1 # upgrade
|
385
|
+
log.action(update_action)
|
386
|
+
when -1 # downgrade
|
387
|
+
log.action(update_action)
|
388
|
+
if platform_project.drupal?
|
389
|
+
if @platform.bootstrapped?
|
390
|
+
log.error("#{platform_project.extended_name} cannot be downgraded because it is bootstrapped")
|
391
|
+
end
|
392
|
+
elsif platform_project.installed?(site)
|
393
|
+
log.error("#{platform_project.extended_name}#{p} must be uninstalled before downgrading")
|
394
|
+
end
|
395
|
+
when nil # One or both projects have no version
|
396
|
+
# Check whether the content of the projects is consistent
|
397
|
+
begin
|
398
|
+
diff = makefile_project.file_level_compare_with platform_project
|
399
|
+
rescue => ex
|
400
|
+
odie "Failed to verify the integrity of #{component.extended_name}: #{ex}"
|
401
|
+
end
|
402
|
+
if diff.empty?
|
403
|
+
log.notice("[OK] #{platform_project.extended_name}#{p}")
|
404
|
+
else
|
405
|
+
log.action(update_action)
|
406
|
+
log.notice(diff.join("\n"))
|
407
|
+
if platform_project.has_version? and (not makefile_project.has_version?)
|
408
|
+
log.error("Cannot upgrade #{makefile_project.name} from known version to unknown version")
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
public
|
415
|
+
|
416
|
+
class Log
|
417
|
+
include Drupid::Utils
|
418
|
+
|
419
|
+
attr :actions
|
420
|
+
attr :errors
|
421
|
+
attr :warnings
|
422
|
+
attr :notices
|
423
|
+
|
424
|
+
# Creates a new log object.
|
425
|
+
def initialize
|
426
|
+
@actions = Array.new
|
427
|
+
@errors = Array.new
|
428
|
+
@warnings = Array.new
|
429
|
+
@notices = Array.new
|
430
|
+
end
|
431
|
+
|
432
|
+
# Adds an action to the log.
|
433
|
+
def action(a)
|
434
|
+
@actions << a
|
435
|
+
puts a.msg
|
436
|
+
end
|
437
|
+
|
438
|
+
def actions?
|
439
|
+
@actions.size > 0
|
440
|
+
end
|
441
|
+
|
442
|
+
def pending_actions?
|
443
|
+
@actions.find_all { |a| a.pending? }.size > 0
|
444
|
+
end
|
445
|
+
|
446
|
+
def apply_pending_actions
|
447
|
+
@actions.find_all { |a| a.pending? }.each do |pa|
|
448
|
+
pa.fire!
|
449
|
+
puts pa.msg
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
# Adds an error message to the log.
|
454
|
+
def error(msg)
|
455
|
+
@errors << msg
|
456
|
+
puts @errors.last
|
457
|
+
end
|
458
|
+
|
459
|
+
# Returns true if this log contains error messages;
|
460
|
+
# returns false otherwise.
|
461
|
+
def errors?
|
462
|
+
@errors.size > 0
|
463
|
+
end
|
464
|
+
|
465
|
+
# Adds a warning to the log.
|
466
|
+
def warning(msg)
|
467
|
+
@warnings << msg
|
468
|
+
puts @warnings.last
|
469
|
+
end
|
470
|
+
|
471
|
+
def warnings?
|
472
|
+
@warnings.size > 0
|
473
|
+
end
|
474
|
+
|
475
|
+
# Adds a notice to the log.
|
476
|
+
def notice(msg)
|
477
|
+
@notices << msg
|
478
|
+
blah @notices.last
|
479
|
+
end
|
480
|
+
|
481
|
+
def notices?
|
482
|
+
@notices.size > 0
|
483
|
+
end
|
484
|
+
|
485
|
+
# Clears the whole log.
|
486
|
+
def clear
|
487
|
+
@errors.clear
|
488
|
+
@warnings.clear
|
489
|
+
@notices.clear
|
490
|
+
end
|
491
|
+
|
492
|
+
# Appends the content of another log to this one.
|
493
|
+
def merge(other)
|
494
|
+
@actions += other.actions
|
495
|
+
@errors += other.errors
|
496
|
+
@warnings += other.warnings
|
497
|
+
@notices += other.notices
|
498
|
+
end
|
499
|
+
|
500
|
+
end # class Log
|
501
|
+
|
502
|
+
|
503
|
+
class AbstractAction
|
504
|
+
include Drupid::Utils
|
505
|
+
attr :platform
|
506
|
+
attr :component
|
507
|
+
|
508
|
+
def initialize(p, c)
|
509
|
+
@platform = p
|
510
|
+
@component = c
|
511
|
+
@pending = true
|
512
|
+
end
|
513
|
+
|
514
|
+
def fire!
|
515
|
+
_install # Implemented by subclasses
|
516
|
+
@pending = false
|
517
|
+
end
|
518
|
+
|
519
|
+
def pending?
|
520
|
+
@pending
|
521
|
+
end
|
522
|
+
end # AbstractAction
|
523
|
+
|
524
|
+
|
525
|
+
class UpdateProjectAction < AbstractAction
|
526
|
+
def initialize(p, proj)
|
527
|
+
raise "#{proj.extended_name} does not exist locally" unless proj.exist?
|
528
|
+
raise "Unknown type for #{proj.extended_name}" unless proj.proj_type
|
529
|
+
super
|
530
|
+
end
|
531
|
+
|
532
|
+
def msg
|
533
|
+
if old_project = platform.get_project(component.name)
|
534
|
+
"#{Tty.blue}[Update]#{Tty.white} #{old_project.extended_name} => #{component.extended_name}#{Tty.reset} (#{platform.dest_path(component)})"
|
535
|
+
else
|
536
|
+
"#{Tty.blue}[Update]#{Tty.white} #{component.extended_name}#{Tty.reset} (#{platform.dest_path(component)})"
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
protected
|
541
|
+
|
542
|
+
# Deploys a project into the specified location.
|
543
|
+
# Note that the content of the
|
544
|
+
# project is copied into new_path, not inside a subdirectory of new_path
|
545
|
+
# (for example, to copy mymodule inside /some/location, new_path
|
546
|
+
# should be set to '/some/location/mymodule').
|
547
|
+
# Returns a new Drupid::Project object for the new location, while
|
548
|
+
# this project remains unchanged.
|
549
|
+
def _install
|
550
|
+
args = Array.new
|
551
|
+
# If the project contains a makefile, it is a candidate for a derivative build.
|
552
|
+
# In such case, protect 'libraries', 'modules' and 'themes' subdirectories
|
553
|
+
# from deletion.
|
554
|
+
if component.makefile
|
555
|
+
args << '-f' << 'P /libraries/***' # this syntax requires rsync >=2.6.7.
|
556
|
+
args << '-f' << 'P /modules/***'
|
557
|
+
args << '-f' << 'P /profiles/***'
|
558
|
+
args << '-f' << 'P /themes/***'
|
559
|
+
end
|
560
|
+
if component.drupal?
|
561
|
+
args = Array.new
|
562
|
+
args << '-f' << 'R /profiles/default/***' # D6
|
563
|
+
args << '-f' << 'R /profiles/minimal/***' # D7
|
564
|
+
args << '-f' << 'R /profiles/standard/***' # D7
|
565
|
+
args << '-f' << 'R /profiles/testing/***' # D7
|
566
|
+
args << '-f' << 'P /profiles/***'
|
567
|
+
args << '-f' << 'R /sites/all/README.txt'
|
568
|
+
args << '-f' << 'R /sites/default/default.settings.php'
|
569
|
+
args << '-f' << 'P /sites/***'
|
570
|
+
end
|
571
|
+
args << '-a'
|
572
|
+
args << '--delete'
|
573
|
+
component.ignore_paths.each { |p| args << "--exclude=#{p}" }
|
574
|
+
dst_path = platform.local_path + platform.dest_path(component)
|
575
|
+
debug "Pathname.mkpath may raise harmless exceptions"
|
576
|
+
dst_path.mkpath unless dst_path.exist?
|
577
|
+
args << component.local_path.to_s + '/'
|
578
|
+
args << dst_path.to_s + '/'
|
579
|
+
begin
|
580
|
+
runBabyRun 'rsync', args
|
581
|
+
rescue => ex
|
582
|
+
odie "Installing or updating #{component.name} failed: #{ex}"
|
583
|
+
end
|
584
|
+
end
|
585
|
+
end # UpdateProjectAction
|
586
|
+
|
587
|
+
|
588
|
+
class InstallProjectAction < UpdateProjectAction
|
589
|
+
def initialize(platform, project)
|
590
|
+
raise "#{project.name} already exists." if platform.get_project(project.name)
|
591
|
+
super
|
592
|
+
end
|
593
|
+
|
594
|
+
def msg
|
595
|
+
"#{Tty.blue}[Install]#{Tty.white} #{component.extended_name}#{Tty.reset} (#{platform.dest_path(component)})"
|
596
|
+
end
|
597
|
+
end # InstallProjectAction
|
598
|
+
|
599
|
+
|
600
|
+
class UpdateLibraryAction < AbstractAction
|
601
|
+
def initialize(platform, library)
|
602
|
+
raise "#{library.extended_name} does not exist locally" unless library.exist?
|
603
|
+
super
|
604
|
+
end
|
605
|
+
|
606
|
+
def msg
|
607
|
+
"#{Tty.blue}[Update]#{Tty.white} Library #{component.extended_name}#{Tty.reset} (#{platform.contrib_path + component.target_path})"
|
608
|
+
end
|
609
|
+
|
610
|
+
protected
|
611
|
+
|
612
|
+
# Deploys a library into the specified location.
|
613
|
+
def _install
|
614
|
+
args = Array.new
|
615
|
+
args << '-a'
|
616
|
+
args << '--delete'
|
617
|
+
component.ignore_paths.each { |p| args << "--exclude=#{p}" }
|
618
|
+
dst_path = platform.local_path + platform.contrib_path + component.target_path
|
619
|
+
debug "Pathname.mkpath may raise harmless exceptions"
|
620
|
+
dst_path.mkpath unless dst_path.exist?
|
621
|
+
args << component.local_path.to_s + '/'
|
622
|
+
args << dst_path.to_s + '/'
|
623
|
+
begin
|
624
|
+
runBabyRun 'rsync', args
|
625
|
+
rescue => ex
|
626
|
+
odie "Installing or updating library #{component.name} failed: #{ex}"
|
627
|
+
end
|
628
|
+
end
|
629
|
+
end # UpdateLibraryAction
|
630
|
+
|
631
|
+
|
632
|
+
class InstallLibraryAction < UpdateLibraryAction
|
633
|
+
def msg
|
634
|
+
"#{Tty.blue}[Install]#{Tty.white} Library #{component.extended_name}#{Tty.reset} (#{platform.contrib_path + component.target_path})"
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
class MoveAction < AbstractAction
|
639
|
+
# new_path must be relative to platform.local_path.
|
640
|
+
def initialize(platform, component, new_path)
|
641
|
+
super(platform, component)
|
642
|
+
@destination = Pathname.new(new_path)
|
643
|
+
end
|
644
|
+
|
645
|
+
def fire!
|
646
|
+
if component.local_path.exist? # may have disappeared in the meantime (e.g., because of an update)
|
647
|
+
dst = platform.local_path + @destination
|
648
|
+
debug "Moving #{component.local_path} to #{dst}"
|
649
|
+
if dst.exist?
|
650
|
+
debug "#{dst} already exists, it will be deleted"
|
651
|
+
dst.rmtree
|
652
|
+
end
|
653
|
+
dst.parent.mkpath
|
654
|
+
FileUtils.mv component.local_path.to_s, dst.to_s
|
655
|
+
else
|
656
|
+
blah "Cannot move #{component.local_path.relative_path_from(platform.local_path)}\n" +
|
657
|
+
"(It does not exist any longer)"
|
658
|
+
end
|
659
|
+
@pending = false
|
660
|
+
end
|
661
|
+
|
662
|
+
def msg
|
663
|
+
src = component.local_path.relative_path_from(platform.local_path)
|
664
|
+
"#{Tty.blue}[Move]#{Tty.reset} From #{src} to #{@destination}"
|
665
|
+
end
|
666
|
+
end # MoveAction
|
667
|
+
|
668
|
+
|
669
|
+
class DeleteAction < AbstractAction
|
670
|
+
def fire!
|
671
|
+
component.local_path.rmtree if component.local_path.exist?
|
672
|
+
@pending = false
|
673
|
+
end
|
674
|
+
|
675
|
+
def msg
|
676
|
+
"#{Tty.yellow}[Delete]#{Tty.white} #{component.extended_name}#{Tty.reset} " +
|
677
|
+
"(#{component.local_path.relative_path_from(platform.local_path)})"
|
678
|
+
end
|
679
|
+
end # DeleteAction
|
680
|
+
|
681
|
+
end # Updater
|
682
|
+
|
683
|
+
end # Drupid
|