class Drupid::Updater
Helper class to build or update a Drupal installation.
Attributes
The updater’s log.
A Drupid::Makefile
object.
A Drupid::Platform
object.
(For multisite platforms) the site to be synchronized.
Public Class Methods
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
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
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
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
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
# 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
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
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
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
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
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
# 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
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
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
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 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
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