class Drupid::Updater

Helper class to build or update a Drupal installation.

Attributes

log[R]

The updater’s log.

makefile[R]

A Drupid::Makefile object.

platform[R]

A Drupid::Platform object.

site[R]

(For multisite platforms) the site to be synchronized.

Public Class Methods

new(makefile, platform, options = { :site => nil, :force => false }) click to toggle source

Creates a new updater for a given makefile and a given platform. For multisite platforms, optionally specify a site to synchronize.

   # File lib/drupid/updater.rb
38 def initialize(makefile, platform, options = { :site => nil, :force => false })
39   @makefile = makefile
40   @platform = platform
41   @site = options[:site]
42   @log = Log.new
43   @force_changes = options[:force]
44   #
45   @libraries_paths = Array.new
46   @core_projects = Array.new
47   @derivative_builds = Array.new
48   @excluded_projects = Hash.new
49 end

Public Instance Methods

apply_changes(options = {}) click to toggle source

Applies any pending changes. This is the method that actually modifies the platform. Note that applying changes may be destructive (projects may be upgraded, downgraded, deleted from the platform, moved and/or patched). Always backup your site before calling this method! If :force is set to true, changes are applied even if there are errors.

See also: Drupid::Updater.sync

Options: force, no_lockfile

    # File lib/drupid/updater.rb
351 def apply_changes(options = {})
352   raise "No changes can be applied because there are errors." if log.errors? and not options[:force]
353   log.apply_pending_actions
354   @derivative_builds.each { |updater| updater.apply_changes(options.merge(:no_lockfile => true)) }
355   @log.clear
356   @derivative_builds.clear
357 end
exclude(project_list) click to toggle source

Adds the given list of project names to the exclusion set of this updater.

   # File lib/drupid/updater.rb
62 def exclude(project_list)
63   project_list.each { |p| @excluded_projects[p] = true }
64 end
excluded() click to toggle source

Returns a list of names of projects that must be considered as processed when synchronizing. This always include all Drupal core projects, but other projects may be added.

Requires: this updater’s platform must have been analyzed (see Drupid::Platform.analyze).

   # File lib/drupid/updater.rb
57 def excluded
58   @excluded_projects.keys
59 end
excluded?(project_name) click to toggle source

Returns true if the given project is in the exclusion set of this updater; returns false otherwise.

   # File lib/drupid/updater.rb
68 def excluded?(project_name)
69   @excluded_projects.has_key?(project_name)
70 end
get_drupal() click to toggle source
    # File lib/drupid/updater.rb
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
pending_actions?() click to toggle source

Returns true if there are actions that have not been applied to the platform (including actions in derivative builds); returns false otherwise.

   # File lib/drupid/updater.rb
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
prepare_derivative_build(project) click to toggle source

Enqueues a derivative build based on the specified project (which is typically, but not necessarily, an installation profile). Does nothing if the project does not contain any makefile whose name coincides with the name of the project.

   # File lib/drupid/updater.rb
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
sync(options = {}) click to toggle source

Tries to reconcile the makefile with the platform by resolving unmet dependencies and determining which projects must be installed, upgraded, downgraded, moved or removed. This method does not return anything. The result of the synchronization can be inspected by accessing Drupid::Updater#log.

This method does not modify the platform at all, it only preflights changes and caches the needed stuff locally. For changes to be applied, Drupid::Updater#apply_changes must be invoked after this method has been invoked.

If :nofollow is set to true, then this method does not try to resolve missing dependencies: it only checks the projects that are explicitly mentioned in the makefile. If :nocore is set to true, only contrib projects are synchronized; otherwise, Drupal core is synchronized, too.

See also: Drupid::Updater#apply_changes

Options: nofollow, nocore, nolibs

    # File lib/drupid/updater.rb
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
sync_drupal_core() click to toggle source

Synchronizes Drupal core. Returns true if the synchronization is successful; returns false otherwise.

    # File lib/drupid/updater.rb
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
sync_libraries() click to toggle source

Synchronizes libraries between the makefile and the platform.

    # File lib/drupid/updater.rb
289 def sync_libraries
290   debug 'Syncing libraries'
291   processed_paths = []
292   @makefile.each_library do |lib|
293     sync_library(lib)
294     processed_paths << lib.target_path
295   end
296   # Determine libraries that should be deleted from the 'libraries' folder.
297   # The above is a bit of an overstatement, as it is basically impossible
298   # to detect a "library" in a reliable way. What we actually do is just
299   # deleting "spurious" paths inside the 'libraries' folder.
300   # Note also that Drupid is not smart enough to find libraries installed
301   # inside modules or themes.
302   Pathname.glob(@platform.libraries_path.to_s + '/**/*').each do |p|
303     next unless p.directory?
304     q = p.relative_path_from(@platform.local_path + @platform.contrib_path)
305     # If q is not a prefix of any processed path, or viceversa, delete it
306     if processed_paths.find_all { |pp| pp.fnmatch(q.to_s + '*') or q.fnmatch(pp.to_s + '*') }.empty?
307       l = Library.new(p.basename)
308       l.local_path = p
309       log.action(DeleteAction.new(@platform, l))
310       # Do not need to delete subdirectories
311       processed_paths << q
312     end
313   end
314 end
sync_library(lib) click to toggle source
    # File lib/drupid/updater.rb
316 def sync_library(lib)
317   return false unless _fetch_and_patch(lib)
318 
319   platform_lib = Library.new(lib.name)
320   relpath = @platform.contrib_path + lib.target_path
321   libpath = @platform.local_path + relpath
322   platform_lib.local_path = libpath
323   if platform_lib.exist?
324     begin
325       diff = lib.file_level_compare_with platform_lib
326     rescue => ex
327       odie "Failed to verify the integrity of library #{lib.extended_name}: #{ex}"
328     end
329     if diff.empty?
330       log.notice("#{Tty.white}[OK]#{Tty.reset}  #{lib.extended_name} (#{relpath})")
331     else
332       log.action(UpdateLibraryAction.new(platform, lib))
333       log.notice(diff.join("\n"))
334     end
335   else
336     log.action(InstallLibraryAction.new(platform, lib))
337   end
338   return true
339 end
sync_project(project) click to toggle source

Performs the necessary synchronization actions for the given project. Returns true if the dependencies of the given project must be synchronized, too; returns false otherwise.

    # File lib/drupid/updater.rb
233 def sync_project(project)
234   return false unless _fetch_and_patch(project)
235 
236   # Does this project contains a makefile? If so, enqueue a derivative build.
237   has_makefile = prepare_derivative_build(project)
238 
239   # Ignore libraries that may be installed inside this project
240   pp = @platform.local_path + @platform.dest_path(project)
241   @libraries_paths.each do |lp|
242     if lp.fnmatch?(pp.to_s + '/*')
243       project.ignore_path(lp.relative_path_from(pp))
244       @log.notice("Ignoring #{project.ignore_paths.last} inside #{project.extended_name}")
245     end
246   end
247 
248   # Does the project exist in the platform? If so, compare the two.
249   if @platform.has_project?(project.name)
250     platform_project = @platform.get_project(project.name)
251     # Fix project location
252     new_path = @platform.dest_path(project)
253     if @platform.local_path + new_path != platform_project.local_path
254       log.action(MoveAction.new(@platform, platform_project, new_path))
255       if (@platform.local_path + new_path).exist?
256         if @force_changes
257           owarn "Overwriting existing path: #{new_path}"
258         else
259           log.error("#{new_path} already exists. Use --force to overwrite.")
260         end
261       end
262     end
263     # Compare versions and log suitable actions
264     _compare_versions project, platform_project
265   else
266     # If analyzing the platform does not allow us to detect the project (e.g., Fusion),
267     # we try to see if the directory exists where it is supposed to be.
268     proj_path = @platform.local_path + @platform.dest_path(project)
269     if proj_path.exist?
270       begin
271         platform_project = PlatformProject.new(@platform, proj_path)
272         _compare_versions project, platform_project
273       rescue => ex
274         log.action(UpdateProjectAction.new(@platform, project))
275         if @force_changes
276           owarn "Overwriting existing path: #{proj_path}"
277         else
278           log.error("#{proj_path} exists, but was not recognized as a project (use --force to overwrite it): #{ex}")
279         end
280       end
281     else # new project
282       log.action(InstallProjectAction.new(@platform, project))
283     end
284   end
285   return (not has_makefile)
286 end
sync_projects(options = {}) click to toggle source

Synchronizes projects between the makefile and the platform.

Options: nofollow

    # File lib/drupid/updater.rb
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     log.action(DeleteAction.new(platform, proj))
214     if which('drush').nil?
215       if @force_changes
216         owarn "Forcing deletion."
217       else
218         log.error "#{proj.extended_name}: use --force to force deletion or install Drush >=6.0."
219       end
220     elsif proj.installed?(site)
221       if @force_changes
222         owarn "Deleting an installed/enabled project."
223       else
224         log.error "#{proj.extended_name} cannot be deleted because it is installed"
225       end
226     end
227   end
228 end
updatedb() click to toggle source

Updates Drupal’s database using drush updatedb. If site is defined, then updates only the specified site; otherwise, iterates over all Platform#site_names and updates each one in turn.

Returns true upon success, false otherwise.

    # File lib/drupid/updater.rb
364 def updatedb
365   ohai "Updating Drupal database..."
366   blah "Platform: #{self.platform.local_path}"
367   res = true
368   site_list = (self.site) ? [self.site] : self.platform.site_names
369   site_list.each do |s|
370     site_path = self.platform.sites_path + s
371     debug "Site path: #{site_path}"
372     unless site_path.exist?
373       debug "Skipping #{site_path} because it does not exist."
374       next
375     end
376     blah "Updating site: #{s}"
377     res = Drush.updatedb(site_path) && res
378   end
379   return res
380 end

Private Instance Methods

_compare_versions(makefile_project, platform_project) click to toggle source

Compare project versions and log suitable actions.

    # File lib/drupid/updater.rb
406 def _compare_versions(makefile_project, platform_project)
407   update_action = UpdateProjectAction.new(platform, makefile_project)
408   case makefile_project <=> platform_project
409   when 0 # up to date
410     # Check whether the content of the projects is consistent
411     begin
412       diff = makefile_project.file_level_compare_with platform_project
413     rescue => ex
414       odie "Failed to verify the integrity of #{makefile_project.extended_name}: #{ex}"
415     end
416     p = (makefile_project.drupal?) ? '' : ' (' + (platform.contrib_path + makefile_project.target_path).to_s + ')'
417     if diff.empty?
418       @log.notice("#{Tty.white}[OK]#{Tty.reset}  #{platform_project.extended_name}#{p}")
419     elsif makefile_project.has_patches?
420       log.action(UpdateProjectAction.new(platform, makefile_project, :label => 'Patched'))
421       log.notice "#{makefile_project.extended_name}#{p} will be patched"
422       log.notice(diff.join("\n"))
423     else
424       log.action(update_action)
425       if @force_changes
426         owarn "Mismatch with cached copy: overwriting local copy."
427       else
428         log.error("#{platform_project.extended_name}#{p}: mismatch with cached copy:\n" + diff.join("\n"))
429       end
430     end
431   when 1 # upgrade
432     log.action(update_action)
433   when -1 # downgrade
434     log.action(UpdateProjectAction.new(platform, makefile_project, :label => 'Update'))
435     if which('drush').nil?
436       if @force_changes
437         owarn "Forcing downgrade."
438       else
439         log.error("#{platform_project.extended_name}: use --force to downgrade or install Drush >=6.0.")
440       end
441     elsif platform_project.drupal?
442       if @platform.bootstrapped?
443         if @force_changes
444           owarn "Downgrading a bootstrapped Drupal platform."
445         else
446           log.error("#{platform_project.extended_name} cannot be downgraded because it is bootstrapped (use --force to override)")
447         end
448       end
449     elsif platform_project.installed?(site)
450       if @force_changes
451         owarn "Downgrading an installed/enabled project."
452       else
453         log.error("#{platform_project.extended_name}#{p} must be uninstalled before downgrading (use --force to override)")
454       end
455     end
456   when nil # One or both projects have no version
457     # Check whether the content of the projects is consistent
458     begin
459       diff = makefile_project.file_level_compare_with platform_project
460     rescue => ex
461       odie "Failed to verify the integrity of #{component.extended_name}: #{ex}"
462     end
463     if diff.empty?
464       log.notice("#{Tty.white}[OK]#{Tty.reset}  #{platform_project.extended_name}#{p}")
465     else
466       log.action(update_action)
467       log.notice(diff.join("\n"))
468       if platform_project.has_version? and (not makefile_project.has_version?)
469         if @force_changes
470           owarn "Upgrading #{makefile_project.name} to unknown version."
471         else
472           log.error("Cannot upgrade #{makefile_project.name} from known version to unknown version")
473         end
474       end
475     end
476   end
477 end
_fetch_and_patch(component) click to toggle source

Returns true if the given component is successfully cached and patched; return false otherwise.

    # File lib/drupid/updater.rb
386 def _fetch_and_patch component
387   begin
388     component.fetch
389   rescue => ex
390     @log.error("#{component.extended_name} could not be fetched.")
391     debug "_fetch_and_patch: #{ex.message}"
392     return false
393   end
394   if component.has_patches?
395     begin
396       component.patch
397     rescue => ex
398       @log.error("#{component.extended_name}: #{ex.message}")
399       return false
400     end
401   end
402   return true   
403 end