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