drupid 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,423 @@
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
+
25
+ class ParseMakefileError < RuntimeError
26
+ end
27
+
28
+ # Representation of a Drush makefile.
29
+ #
30
+ # See also: http://drupal.org/node/625094
31
+ class Makefile
32
+ include Drupid::Utils
33
+
34
+ # The absolute path to the makefile.
35
+ attr :path
36
+ # The value of the core field of the makefile (e.g, '7.x')
37
+ attr :core
38
+ # The value of the api field of the makefile (e.g., '2')
39
+ attr :api
40
+ # The path for contrib modules and themes (e.g., 'sites/all'),
41
+ # relative to #path.
42
+ attr_accessor :contrib_path
43
+
44
+ # Creates a new Makefile object. The path must be the path
45
+ # to a .make file (which does not need to exist).
46
+ def initialize(path)
47
+ @path = Pathname.new(path)
48
+ raise "Not an absolute path: #{@path}" unless @path.absolute?
49
+ @core = nil
50
+ @api = nil
51
+ @projects = Hash.new # (String -> Project)
52
+ @libraries = Hash.new # (String -> Library)
53
+ @contrib_path = Pathname.new('sites/all')
54
+ debug "Parsing #{@path}"
55
+ self.reload if @path.exist?
56
+ end
57
+
58
+ # Reloads the makefile.
59
+ # This method is invoked automatically at creation time
60
+ # if a path to an existing makefile is provided.
61
+ def reload
62
+ @core = nil
63
+ @api = nil
64
+ @projects = Hash.new
65
+ @libraries = Hash.new
66
+
67
+ proj_patches = Hash.new
68
+ libs_patches = Hash.new
69
+ core_num = nil
70
+ mf = File.open(@path.to_s, "r").read
71
+ # Parse includes directives
72
+ while mf.match(/^([ \t]*includes\[.*\]\s*=\s*"?([^\s"]+)"?[ \t]*)$/) do
73
+ # TODO: add support for remote includes
74
+ url = $2
75
+ blah "Including makefile #{url}"
76
+ inc = File.open(url, "r").read
77
+ mf.sub!($1, inc)
78
+ end
79
+ if mf.match(/core *= *["']? *(\d+)\.?(\d+)?/) # Get the core number immediately
80
+ @core = $~[1] + '.x'
81
+ core_num = $~[1].to_i
82
+ vers = $~[2] ? $~[1] + '.' + $~[2] : nil
83
+ # Create Drupal project
84
+ @projects['drupal'] = Project.new('drupal', core_num, vers)
85
+ end
86
+ raise ParseMakefileError, "The makefile does not contain the mandatory 'core' field" unless core_num
87
+ lineno = 0
88
+ mf.each_line do |line|
89
+ lineno += 1
90
+ next if line =~ /^\s*$/
91
+ next if line =~ /^\s*;/
92
+ next if line =~ /^\s*core/
93
+ # match[1] : the key ('core', 'version', 'api', 'projects', 'libraries', 'includes')
94
+ # match[2] : the (optional) key arguments (stuff between square brackets)
95
+ # match[3] : the same as match[2], but without the leftmost [ and the rightmost ]
96
+ # match[4] : the value
97
+ # Examples:
98
+ # (a) Given 'projects[ctools][version] = 1.0-rc1', we have
99
+ # match[1] == 'projects'
100
+ # match[2] == '[ctools][version]'
101
+ # match[3] == 'ctools][version'
102
+ # match[4] == '1.0-rc1'
103
+ # (b) Given 'core = 7.x', we have:
104
+ # match[1] == 'core'
105
+ # match[3] == nil
106
+ # match[4] == '7.x'
107
+ match = line.match(/^\s*([^\s\[=]+)\s*(\[\s*(.*?)\s*\])?\s*=\s*["']?([^\s"'(]+)/)
108
+ raise ParseMakefileError, "Could not parse line: #{line.strip} (line #{lineno})" if match.nil? or match.size != 5
109
+ key = match[1]
110
+ args = (match[3]) ? match[3].split(/\]\s*\[/) : []
111
+ value = match[4].strip
112
+ case key
113
+ when 'api'
114
+ @api = value
115
+ when 'projects'
116
+ if 0 == args.size # e.g., projects[] = views
117
+ name = value
118
+ @projects[name] = Project.new(name, core_num)
119
+ else
120
+ name = args[0]
121
+ @projects[name] = Project.new(name, core_num) unless @projects.has_key?(name)
122
+ case args.size
123
+ when 1 # e.g., projects[views] = 2.8
124
+ @projects[name].version = @core+'-'+value.sub(/^#{@core}-/,'')
125
+ when 2 # e.g., projects[views][version] = 2.8 or projects[calendar][patch][] = 'http://...'
126
+ case args[1]
127
+ when 'version'
128
+ @projects[name].version = @core+'-'+value.sub(/^#{@core}-/,'')
129
+ when 'patch'
130
+ patch_key = File.basename(value)
131
+ patch_url = _normalize_path(value)
132
+ @projects[name].add_patch(patch_url, patch_key)
133
+ when 'subdir'
134
+ @projects[name].subdir = value
135
+ when 'location'
136
+ @projects[name].location = _normalize_path(value)
137
+ when 'directory_name'
138
+ @projects[name].directory_name = value
139
+ when 'type'
140
+ if 'core' == value
141
+ @projects[name].core_project = true
142
+ else
143
+ raise ParseMakefileError, "Illegal value: #{args[1]} (line #{lineno})" unless value =~ /^(module|profile|theme)$/
144
+ @projects[name].proj_type = value
145
+ end
146
+ when 'l10n_path'
147
+ # TODO: add support for tokens
148
+ @projects[name].l10n_path = _normalize_path(value)
149
+ when 'l10n_url'
150
+ @projects[name].l10n_url = _normalize_path(value)
151
+ when 'overwrite'
152
+ @projects[name].overwrite = true if value =~ /TRUE/i
153
+ else
154
+ raise ParseMakefileError, "Unknown key: #{args[1]} (line #{lineno})"
155
+ end
156
+ when 3 # e.g., projects[mytheme][download][type] = "svn"
157
+ name = args[0]
158
+ subkey = args[1]
159
+ case subkey
160
+ when 'download'
161
+ case args[2]
162
+ when 'type'
163
+ @projects[name].download_type = value
164
+ when 'url'
165
+ @projects[name].download_url = _normalize_path(value)
166
+ else
167
+ @projects[name].add_download_spec(args[2], value)
168
+ end
169
+ else
170
+ raise ParseMakefileError, "Unknown key: #{subkey} (line #{lineno})"
171
+ end
172
+ when 4 # e.g., projects[calendar][patch][rfc-fixes][md5] = "..."
173
+ name = args[0]
174
+ subkey = args[1]
175
+ case subkey
176
+ when 'patch'
177
+ patch_key = args[2]
178
+ proj_patches[name] ||= Hash.new
179
+ proj_patches[name][patch_key] ||= Hash.new
180
+ case args[3]
181
+ when 'url'
182
+ proj_patches[name][patch_key]['url'] = _normalize_path(value)
183
+ when 'md5'
184
+ proj_patches[name][patch_key]['md5'] = value
185
+ else
186
+ raise ParseMakefileError, "Unknown key: #{subkey} (line #{lineno})"
187
+ end
188
+ else
189
+ raise ParseMakefileError, "Unknown key: #{subkey} (line #{lineno})"
190
+ end
191
+ else # > 4 arguments
192
+ raise ParseMakefileError, "Too many arguments (line #{lineno})"
193
+ end # case
194
+ end # if
195
+ when 'libraries'
196
+ if 0 == args.size
197
+ raise ParseMakefileError, "Too few arguments (line #{lineno})"
198
+ else
199
+ name = args[0]
200
+ @libraries[name] = Library.new(name) unless @libraries.has_key?(name)
201
+ case args.size
202
+ when 1
203
+ raise ParseMakefileError, "Too few arguments (line #{lineno})"
204
+ when 2
205
+ case args[1]
206
+ when 'patch'
207
+ patch_key = File.basename(value)
208
+ patch_url = _normalize_path(value)
209
+ @libraries[name].add_patch(patch_url, patch_key)
210
+ when 'subdir'
211
+ @libraries[name].subdir = value
212
+ when 'destination'
213
+ @libraries[name].destination = value
214
+ when 'directory_name'
215
+ @libraries[name].directory_name = value
216
+ else
217
+ raise ParseMakefileError, "Unknown key: #{args[1]} (line #{lineno})"
218
+ end
219
+ when 3 # e.g., libraries[jquery_ui][download][type] = "file"
220
+ name = args[0]
221
+ subkey = args[1]
222
+ case subkey
223
+ when 'download'
224
+ case args[2]
225
+ when 'type'
226
+ @libraries[name].download_type = value
227
+ when 'url'
228
+ @libraries[name].download_url = _normalize_path(value)
229
+ else
230
+ @libraries[name].add_download_spec(args[2], value)
231
+ end
232
+ else
233
+ raise ParseMakefileError, "Unknown key: #{subkey} (line #{lineno})"
234
+ end
235
+ when 4
236
+ name = args[0]
237
+ subkey = args[1]
238
+ case subkey
239
+ when 'patch'
240
+ patch_key = args[2]
241
+ libs_patches[name] ||= Hash.new
242
+ libs_patches[name][patch_key] ||= Hash.new
243
+ case args[3]
244
+ when 'url'
245
+ libs_patches[name][patch_key]['url'] = _normalize_path(value)
246
+ when 'md5'
247
+ libs_patches[name][patch_key]['md5'] = value
248
+ else
249
+ raise ParseMakefileError, "Unknown key: #{subkey} (line #{lineno})"
250
+ end
251
+ else
252
+ raise ParseMakefileError, "Unknown key: #{subkey} (line #{lineno})"
253
+ end
254
+ else # > 4 arguments
255
+ raise ParseMakefileError, "Too many arguments (line #{lineno})"
256
+ end
257
+ end
258
+ when 'includes'
259
+ owarn "Unexpected 'includes' directive (line #{lineno})"
260
+ else
261
+ owarn "Could not parse key: #{key} (line #{lineno})"
262
+ end
263
+ end
264
+ # Add missing patches
265
+ proj_patches.each do |proj_name, v|
266
+ v.each do |desc,prop|
267
+ @projects[proj_name].add_patch(prop['url'], desc, prop['md5'])
268
+ end
269
+ end
270
+ libs_patches.each do |lib_name, v|
271
+ v.each do |desc,prop|
272
+ @libraries[lib_name].add_patch(prop['url'], desc, prop['md5'])
273
+ end
274
+ end
275
+ return self
276
+ end
277
+
278
+ # Adds a project to this specification.
279
+ def add_project(p)
280
+ @projects[p.name] = p
281
+ end
282
+
283
+ # Returns the project with the specified name,
284
+ # or nil if the project is not in this specification.
285
+ def get_project(name)
286
+ @projects[name]
287
+ end
288
+
289
+ # Returns the library with the specified name.
290
+ # or nil if the library is not in this specification.
291
+ def get_library(name)
292
+ @libraries[name]
293
+ end
294
+
295
+ # Removes the project with the specified name from this specification.
296
+ def delete_project(name)
297
+ @projects.delete(name)
298
+ end
299
+
300
+ # Iterates over the projects in this specification (excluding drupal).
301
+ def each_project
302
+ # For convenience, return the projects in lexicographical order.
303
+ names = @projects.keys.sort!
304
+ names.each do |n|
305
+ yield @projects[n] unless @projects[n].drupal?
306
+ end
307
+ end
308
+
309
+ # Returns a Drupid::Project object for the Drupal core specified
310
+ # in the makefile, or nil if the makefile does not specify a Drupal distribution.
311
+ def drupal_project
312
+ @projects['drupal']
313
+ end
314
+
315
+ # Iterates over the libraries in this specification.
316
+ def each_library
317
+ # For convenience, return the libraries in lexicographical order.
318
+ names = @libraries.keys.sort!
319
+ names.each do |n|
320
+ yield @libraries[n]
321
+ end
322
+ end
323
+
324
+ # Returns a list of the names of the projects mentioned
325
+ # in this specification (excluding drupal).
326
+ def project_names
327
+ @projects.values.reject { |p| p.drupal? }.map { |p| p.name }
328
+ end
329
+
330
+ # Returns a list of the names of the libraries mentioned
331
+ # in this specification.
332
+ def library_names
333
+ @libraries.keys
334
+ end
335
+
336
+ # Writes this makefile to disk.
337
+ # An alternative location may be specified as an argument.
338
+ def save(alt_path = @path)
339
+ File.open(alt_path.to_s, "w").write(to_s)
340
+ end
341
+
342
+ # Returns this makefile as a string.
343
+ def to_s
344
+ s = String.new
345
+ s << "core = #{@core}\n"
346
+ s << "api = #{@api}\n"
347
+ s << _project_to_record(drupal_project) if drupal_project
348
+ s << "\n" unless @projects.empty?
349
+ self.each_project { |p| s << _project_to_record(p) }
350
+ s << "\n" unless @libraries.empty?
351
+ self.each_library { |l| s << _library_to_record(l) }
352
+ s
353
+ end
354
+
355
+ private
356
+
357
+ def _normalize_path(u)
358
+ return u if u =~ /:\/\// # URL
359
+ if u =~ /^\// # Local absolute path
360
+ return 'file://' + u
361
+ else # Relative path
362
+ return 'file://' + (path.parent + u).to_s
363
+ end
364
+ end
365
+
366
+ def _relativize_path(u)
367
+ return u unless u =~ /^file:\/\//
368
+ return Pathname.new(u.sub(/file:\/\//,'')).relative_path_from(path.dirname).to_s
369
+ end
370
+
371
+ def _project_to_record(p)
372
+ fields = Array.new
373
+ fields << "[type] = \"#{p.proj_type}\"" if p.proj_type =~ /module|profile|theme/
374
+ fields << "[version] = \"#{p.version.short}\"" if p.has_version?
375
+ fields << "[location] = \"#{_relativize_path(p.location)}\"" if p.location
376
+ fields << "[download][type] = \"#{p.download_type}\"" if p.download_type
377
+ fields << "[download][url] = \"#{_relativize_path(p.download_url)}\"" if p.download_url
378
+ temp = []
379
+ p.download_specs.each do |spec,ref|
380
+ temp << "[download][#{spec}] = \"#{ref}\""
381
+ end
382
+ fields = fields + temp.sort!
383
+ p.each_patch do |pa|
384
+ fields << "[patch][#{pa.descr}][url] = \"#{_relativize_path(pa.url)}\""
385
+ fields << "[patch][#{pa.descr}][md5] = \"#{pa.md5}\"" if pa.md5
386
+ end
387
+ fields << "[l10n_path] = \"#{_relativize_path(p.l10n_path)}\"" if p.l10n_path
388
+ fields << "[l10n_url] = \"#{_relativize_path(p.l10n_url)}\"" if p.l10n_url
389
+ fields << "[subdir] = \"#{p.subdir}\"" if '.' != p.subdir.to_s
390
+ fields << "[directory_name] = \"#{p.directory_name}\"" if p.directory_name != p.name
391
+ return "projects[] = \"#{p.name}\"\n" if 0 == fields.size
392
+ s = ''
393
+ fields.each do |f|
394
+ s << "projects[#{p.name}]" + f + "\n"
395
+ end
396
+ return s
397
+ end
398
+
399
+ def _library_to_record(l)
400
+ fields = Array.new
401
+ fields << "[download][type] = \"#{l.download_type}\"" if l.download_type
402
+ fields << "[download][url] = \"#{_relativize_path(l.download_url)}\"" if l.download_url
403
+ temp = []
404
+ l.download_specs.each do |spec,ref|
405
+ temp << "[download][#{spec}] = \"#{ref}\""
406
+ end
407
+ fields = fields + temp.sort!
408
+ l.each_patch do |pa|
409
+ fields << "[patch][#{pa.descr}][url] = \"#{pa.url}\""
410
+ fields << "[patch][#{pa.descr}][md5] = \"#{pa.md5}\"" if pa.md5
411
+ end
412
+ fields << "[destination] = \"#{l.destination}\""
413
+ fields << "[subdir] = \"#{l.subdir}\"" if '.' != l.subdir.to_s
414
+ fields << "[directory_name] = \"#{l.directory_name}\""
415
+ s = ''
416
+ fields.each do |f|
417
+ s << "libraries[#{l.name}]" + f + "\n"
418
+ end
419
+ return s
420
+ end
421
+
422
+ end # class Makefile
423
+ end # Drupid
@@ -0,0 +1,92 @@
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
+ class Patch
25
+ include Drupid::Utils
26
+
27
+ attr :url
28
+ attr :md5
29
+ attr :descr
30
+ attr :cached_location
31
+
32
+ def initialize url, descr, md5 = nil
33
+ @url = url
34
+ @descr = descr
35
+ @md5 = md5
36
+ @cached_location = nil
37
+ end
38
+
39
+ # Downloads the patch into the current directory.
40
+ def fetch
41
+ dst = Pathname.pwd+File.basename(@url.to_s)
42
+ begin
43
+ curl @url.to_s, '-o', dst
44
+ rescue
45
+ raise "Patch #{File.basename(@url.to_s)} could not be fetched."
46
+ end
47
+ @cached_location = dst
48
+ debug "Patch downloaded into #{@cached_location}"
49
+ end
50
+
51
+ # Applies this patch in the current directory.
52
+ # Raises an error if the patch cannot be applied.
53
+ def apply
54
+ debug "Applying patch at #{Dir.pwd}"
55
+ raise "Patch not fetched." if !(@cached_location and @cached_location.exist?)
56
+ patch_levels = ['-p1', '-p0']
57
+ patched = false
58
+ output = ''
59
+ # First try with git apply
60
+ patch_levels.each do |pl|
61
+ begin
62
+ runBabyRun 'git', ['apply', '--check', pl, @cached_location], :redirect_stderr_to_stdout => true
63
+ runBabyRun 'git', ['apply', pl, @cached_location], :redirect_stderr_to_stdout => true
64
+ patched = true
65
+ break
66
+ rescue => ex
67
+ output << ex.to_s
68
+ end
69
+ end
70
+ if not patched
71
+ patch_levels.each do |pl|
72
+ begin
73
+ runBabyRun 'patch', ['--no-backup-if-mismatch', '-f', pl, '-d', Dir.pwd, '-i', @cached_location], :redirect_stderr_to_stdout => true
74
+ patched = true
75
+ break
76
+ rescue => ex
77
+ output << ex.to_s
78
+ end
79
+ end
80
+ end
81
+ if not patched
82
+ if descr and descr != @cached_location.basename.to_s
83
+ d = " (#{descr})"
84
+ else
85
+ d = ''
86
+ end
87
+ raise "Patch #{@cached_location.basename}#{d} could not be applied.\n" + output
88
+ end
89
+ return true
90
+ end
91
+ end
92
+ end