Class: Drupid::Updater

Inherits:
Object
  • Object
show all
Includes:
Utils
Defined in:
lib/drupid/updater.rb

Overview

Helper class to build or update a Drupal installation.

Defined Under Namespace

Classes: AbstractAction, DeleteAction, InstallLibraryAction, InstallProjectAction, Log, MoveAction, UpdateLibraryAction, UpdateProjectAction

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Utils

#blah, #bzr, #compare_paths, #curl, #cvs, #debug, #dont_debug, #git, #hg, #ignore_interrupts, #interactive_shell, #odie, #ofail, #ohai, #owarn, #runBabyRun, #svn, #tempdir, #uncompress, #which, #writeFile

Constructor Details

#initialize(makefile, platform, options = { :site => nil, :force => false }) ⇒ Updater

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



39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/drupid/updater.rb', line 39

def initialize(makefile, platform, options = { :site => nil, :force => false })
  @makefile = makefile
  @platform = platform
  @site = options[:site]
  @log = Log.new
  @force_changes = options[:force]
  #
  @libraries_paths = Array.new
  @core_projects = Array.new
  @derivative_builds = Array.new
  @excluded_projects = Hash.new
end

Instance Attribute Details

#logObject (readonly)

The updater’s log.



35
36
37
# File 'lib/drupid/updater.rb', line 35

def log
  @log
end

#makefileObject (readonly)

A Drupid::Makefile object.



29
30
31
# File 'lib/drupid/updater.rb', line 29

def makefile
  @makefile
end

#platformObject (readonly)

A Drupid::Platform object.



31
32
33
# File 'lib/drupid/updater.rb', line 31

def platform
  @platform
end

#siteObject (readonly)

(For multisite platforms) the site to be synchronized.



33
34
35
# File 'lib/drupid/updater.rb', line 33

def site
  @site
end

Instance Method Details

#apply_changes(options = {}) ⇒ Object

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



352
353
354
355
356
357
358
# File 'lib/drupid/updater.rb', line 352

def apply_changes(options = {})
  raise "No changes can be applied because there are errors." if log.errors? and not options[:force]
  log.apply_pending_actions
  @derivative_builds.each { |updater| updater.apply_changes(options.merge(:no_lockfile => true)) }
  @log.clear
  @derivative_builds.clear
end

#exclude(project_list) ⇒ Object

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



63
64
65
# File 'lib/drupid/updater.rb', line 63

def exclude(project_list)
  project_list.each { |p| @excluded_projects[p] = true }
end

#excludedObject

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



58
59
60
# File 'lib/drupid/updater.rb', line 58

def excluded
  @excluded_projects.keys
end

#excluded?(project_name) ⇒ Boolean

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

Returns:

  • (Boolean)


69
70
71
# File 'lib/drupid/updater.rb', line 69

def excluded?(project_name)
  @excluded_projects.has_key?(project_name)
end

#get_drupalObject



152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/drupid/updater.rb', line 152

def get_drupal
  drupal = @makefile.drupal_project
  unless drupal # Nothing to do
    owarn 'No Drupal project specified.'
    return false
  end
  return false unless _fetch_and_patch(drupal)
  # Extract information about core projects, which must not be synchronized
  temp_platform = Platform.new(drupal.local_path)
  temp_platform.analyze
  @core_projects = temp_platform.core_project_names
  return true
end

#pending_actions?Boolean

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

Returns:

  • (Boolean)


76
77
78
79
80
81
82
# File 'lib/drupid/updater.rb', line 76

def pending_actions?
  return true if @log.pending_actions?
  @derivative_builds.each do |d|
    return true if d.pending_actions?
  end
  return false
end

#prepare_derivative_build(project) ⇒ Object

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.



88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/drupid/updater.rb', line 88

def prepare_derivative_build(project)
  mf = project.makefile
  return false if mf.nil?
  debug "Preparing derivative build for #{mf.basename}"
  submake = Makefile.new(mf)
  subplatform = Platform.new(@platform.local_path)
  subplatform.contrib_path = @platform.dest_path(project)
  new_updater = Updater.new(submake, subplatform, site)
  new_updater.exclude(project.extensions)
  new_updater.exclude(@platform.profiles)
  @derivative_builds << new_updater
  return true
end

#sync(options = {}) ⇒ Object

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



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/drupid/updater.rb', line 122

def sync(options = {})
  @log.clear
  @platform.analyze
  # These paths are needed because Drupal allows libraries to be installed
  # inside modules. Hence, we must ignore them when synchronizing those modules.
  @makefile.each_library do |l|
    @libraries_paths << @platform.local_path + @platform.contrib_path + l.target_path
  end
  # We always need a local copy of Drupal core (either the version specified
  # in the makefile or the latest version), even if we are not going to
  # synchronize the core, in order to extract the list of core projects.
  if get_drupal
    if options[:nocore]
      blah "Skipping core"
    else
      sync_drupal_core
    end
  else
    return
  end
  sync_projects(options)
  sync_libraries unless options[:nolibs]
  # Process derivative builds
  @derivative_builds.each do |updater|
    updater.sync(options.merge(:nocore => true))
    @log.merge(updater.log)
  end
  return
end

#sync_drupal_coreObject

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



169
170
171
172
173
174
175
176
# File 'lib/drupid/updater.rb', line 169

def sync_drupal_core
  if @platform.drupal_project
    _compare_versions @makefile.drupal_project, @platform.drupal_project
  else
    log.action(InstallProjectAction.new(@platform, @makefile.drupal_project))
  end
  return true
end

#sync_librariesObject

Synchronizes libraries between the makefile and the platform.



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/drupid/updater.rb', line 290

def sync_libraries
  debug 'Syncing libraries'
  processed_paths = []
  @makefile.each_library do |lib|
    sync_library(lib)
    processed_paths << lib.target_path
  end
  # Determine libraries that should be deleted from the 'libraries' folder.
  # The above is a bit of an overstatement, as it is basically impossible
  # to detect a "library" in a reliable way. What we actually do is just
  # deleting "spurious" paths inside the 'libraries' folder.
  # Note also that Drupid is not smart enough to find libraries installed
  # inside modules or themes.
  Pathname.glob(@platform.libraries_path.to_s + '/**/*').each do |p|
    next unless p.directory?
    q = p.relative_path_from(@platform.local_path + @platform.contrib_path)
    # If q is not a prefix of any processed path, or viceversa, delete it
    if processed_paths.find_all { |pp| pp.fnmatch(q.to_s + '*') or q.fnmatch(pp.to_s + '*') }.empty?
      l = Library.new(p.basename)
      l.local_path = p
      log.action(DeleteAction.new(@platform, l))
      # Do not need to delete subdirectories
      processed_paths << q
    end
  end
end

#sync_library(lib) ⇒ Object



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/drupid/updater.rb', line 317

def sync_library(lib)
  return false unless _fetch_and_patch(lib)

  platform_lib = Library.new(lib.name)
  relpath = @platform.contrib_path + lib.target_path
  libpath = @platform.local_path + relpath
  platform_lib.local_path = libpath
  if platform_lib.exist?
    begin
      diff = lib.file_level_compare_with platform_lib
    rescue => ex
      odie "Failed to verify the integrity of library #{lib.extended_name}: #{ex}"
    end
    if diff.empty?
      log.notice("#{Tty.white}[OK]#{Tty.reset}  #{lib.extended_name} (#{relpath})")
    else
      log.action(UpdateLibraryAction.new(platform, lib))
      log.notice(diff.join("\n"))
    end
  else
    log.action(InstallLibraryAction.new(platform, lib))
  end
  return true
end

#sync_project(project) ⇒ Object

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.



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/drupid/updater.rb', line 234

def sync_project(project)
  return false unless _fetch_and_patch(project)

  # Does this project contains a makefile? If so, enqueue a derivative build.
  has_makefile = prepare_derivative_build(project)

  # Ignore libraries that may be installed inside this project
  pp = @platform.local_path + @platform.dest_path(project)
  @libraries_paths.each do |lp|
    if lp.fnmatch?(pp.to_s + '/*')
      project.ignore_path(lp.relative_path_from(pp))
      @log.notice("Ignoring #{project.ignore_paths.last} inside #{project.extended_name}")
    end
  end

  # Does the project exist in the platform? If so, compare the two.
  if @platform.has_project?(project.name)
    platform_project = @platform.get_project(project.name)
    # Fix project location
    new_path = @platform.dest_path(project)
    if @platform.local_path + new_path != platform_project.local_path
      log.action(MoveAction.new(@platform, platform_project, new_path))
      if (@platform.local_path + new_path).exist?
        if @force_changes
          owarn "Overwriting existing path: #{new_path}"
        else
          log.error("#{new_path} already exists. Use --force to overwrite.")
        end
      end
    end
    # Compare versions and log suitable actions
    _compare_versions project, platform_project
  else
    # If analyzing the platform does not allow us to detect the project (e.g., Fusion),
    # we try to see if the directory exists where it is supposed to be.
    proj_path = @platform.local_path + @platform.dest_path(project)
    if proj_path.exist?
      begin
        platform_project = PlatformProject.new(@platform, proj_path)
        _compare_versions project, platform_project
      rescue => ex
        log.action(UpdateProjectAction.new(@platform, project))
        if @force_changes
          owarn "Overwriting existing path: #{proj_path}"
        else
          log.error("#{proj_path} exists, but was not recognized as a project (use --force to overwrite it): #{ex}")
        end
      end
    else # new project
      log.action(InstallProjectAction.new(@platform, project))
    end
  end
  return (not has_makefile)
end

#sync_projects(options = {}) ⇒ Object

Synchronizes projects between the makefile and the platform.

Options: nofollow



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/drupid/updater.rb', line 181

def sync_projects(options = {})
  exclude(@core_projects) # Skip core projects
  processed = Array.new(excluded) # List of names of processed projects
  dep_queue = Array.new # Queue of Drupid::Project objects whose dependencies must be checked. This is always a subset of processed.

  @makefile.each_project do |makefile_project|
    dep_queue << makefile_project if sync_project(makefile_project)
    processed += makefile_project.extensions
  end

  unless options[:nofollow]
    # Recursively get dependent projects.
    # An invariant is that each project in the dependency queue has been processed
    # and cached locally. Hence, it has a version and its path points to the
    # cached copy.
    while not dep_queue.empty? do
      project = dep_queue.shift
      project.dependencies.each do |dependent_project_name|
        unless processed.include?(dependent_project_name)
          debug "Queue dependency: #{dependent_project_name} <- #{project.extended_name}"
          new_project = Project.new(dependent_project_name, project.core)
          dep_queue << new_project if sync_project(new_project)
          @makefile.add_project(new_project)
          processed += new_project.extensions
        end
      end
    end
  end

  # Determine projects that should be deleted
  pending_delete = @platform.project_names - processed
  pending_delete.each do |p|
    proj = platform.get_project(p)
    log.action(DeleteAction.new(platform, proj))
    if which('drush').nil?
      if @force_changes
        owarn "Forcing deletion."
      else
        log.error "#{proj.extended_name}: use --force to force deletion or install Drush >=6.0."
      end
    elsif proj.installed?(site)
      if @force_changes
        owarn "Deleting an installed/enabled project."
      else
        log.error "#{proj.extended_name} cannot be deleted because it is installed"
      end
    end
  end
end

#updatedbObject

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.



365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/drupid/updater.rb', line 365

def updatedb
  ohai "Updating Drupal database..."
  blah "Platform: #{self.platform.local_path}"
  res = true
  site_list = (self.site) ? [self.site] : self.platform.site_names
  site_list.each do |s|
    site_path = self.platform.sites_path + s
    debug "Site path: #{site_path}"
    unless site_path.exist?
      debug "Skipping #{site_path} because it does not exist."
      next
    end
    blah "Updating site: #{s}"
    res = Drush.updatedb(site_path) && res
  end
  return res
end