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.
- 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
|