autobuild 1.8.3 → 1.9.0.b1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Manifest.txt +16 -7
  3. data/Rakefile +2 -0
  4. data/lib/autobuild/config.rb +21 -6
  5. data/lib/autobuild/configurable.rb +2 -2
  6. data/lib/autobuild/environment.rb +52 -27
  7. data/lib/autobuild/exceptions.rb +48 -22
  8. data/lib/autobuild/import/archive.rb +37 -16
  9. data/lib/autobuild/import/cvs.rb +26 -28
  10. data/lib/autobuild/import/darcs.rb +9 -8
  11. data/lib/autobuild/import/git.rb +324 -217
  12. data/lib/autobuild/import/hg.rb +6 -9
  13. data/lib/autobuild/import/svn.rb +190 -47
  14. data/lib/autobuild/importer.rb +80 -35
  15. data/lib/autobuild/package.rb +16 -35
  16. data/lib/autobuild/packages/autotools.rb +8 -8
  17. data/lib/autobuild/packages/cmake.rb +18 -12
  18. data/lib/autobuild/packages/genom.rb +1 -1
  19. data/lib/autobuild/packages/gnumake.rb +11 -12
  20. data/lib/autobuild/packages/orogen.rb +1 -1
  21. data/lib/autobuild/packages/ruby.rb +9 -5
  22. data/lib/autobuild/reporting.rb +10 -6
  23. data/lib/autobuild/subcommand.rb +110 -50
  24. data/lib/autobuild/test.rb +104 -0
  25. data/lib/autobuild/timestamps.rb +3 -3
  26. data/lib/autobuild/tools.rb +1 -1
  27. data/lib/autobuild/utility.rb +22 -10
  28. data/lib/autobuild/version.rb +1 -1
  29. data/test/data/gitrepo-with-extra-commit-and-tag.tar +0 -0
  30. data/test/data/gitrepo.tar +0 -0
  31. data/test/data/gitrepo/test +0 -0
  32. data/test/data/gitrepo/test2 +0 -0
  33. data/test/data/gitrepo/test3 +0 -0
  34. data/test/data/svnroot.tar +0 -0
  35. data/test/import/test_cvs.rb +51 -0
  36. data/test/import/test_git.rb +364 -0
  37. data/test/import/test_svn.rb +144 -0
  38. data/test/import/test_tar.rb +76 -0
  39. data/test/suite.rb +7 -0
  40. data/test/test_config.rb +1 -5
  41. data/test/test_environment.rb +88 -0
  42. data/test/test_reporting.rb +2 -14
  43. data/test/test_subcommand.rb +7 -22
  44. metadata +17 -14
  45. data/test/test_import_cvs.rb +0 -59
  46. data/test/test_import_svn.rb +0 -56
  47. data/test/test_import_tar.rb +0 -83
  48. data/test/tools.rb +0 -44
@@ -49,16 +49,14 @@ module Autobuild
49
49
  end
50
50
  end
51
51
 
52
- def update(package,only_local=false)
53
- if only_local
54
- Autobuild.warn "The importer #{self.class} does not support local updates, skipping #{self}"
52
+ def update(package, options = Hash.new)
53
+ if options[:only_local]
54
+ package.warn "%s: the Mercurial importer does not support local updates, skipping"
55
55
  return
56
56
  end
57
57
  validate_importdir(package)
58
- Dir.chdir(package.importdir) do
59
- Subprocess.run(package, :import, Autobuild.tool('hg'), 'pull', repository)
60
- Subprocess.run(package, :import, Autobuild.tool('hg'), 'update', branch)
61
- end
58
+ package.run(:import, Autobuild.tool('hg'), 'pull', repository, retry: true, working_directory: package.importdir)
59
+ package.run(:import, Autobuild.tool('hg'), 'update', branch, working_directory: package.importdir)
62
60
  end
63
61
 
64
62
  def checkout(package)
@@ -67,8 +65,7 @@ module Autobuild
67
65
  FileUtils.mkdir_p base_dir
68
66
  end
69
67
 
70
- Subprocess.run(package, :import,
71
- Autobuild.tool('hg'), 'clone', '-u', branch, repository, package.importdir)
68
+ package.run(:import, Autobuild.tool('hg'), 'clone', '-u', branch, repository, package.importdir, retry: true)
72
69
  end
73
70
  end
74
71
 
@@ -1,5 +1,6 @@
1
1
  require 'autobuild/subcommand'
2
2
  require 'autobuild/importer'
3
+ require 'rexml/document'
3
4
 
4
5
  module Autobuild
5
6
  class SVN < Importer
@@ -11,67 +12,209 @@ module Autobuild
11
12
  # This importer uses the 'svn' tool to perform the import. It defaults
12
13
  # to 'svn' and can be configured by doing
13
14
  # Autobuild.programs['svn'] = 'my_svn_tool'
14
- def initialize(source, options = {})
15
-
16
- @source = [*source].join("/")
15
+ def initialize(svnroot, options = {})
16
+ svnroot = [*svnroot].join("/")
17
17
  svnopts, common = Kernel.filter_options options,
18
18
  :svnup => [], :svnco => [], :revision => nil,
19
- :repository_id => "svn:#{@source}"
20
- @program = Autobuild.tool('svn')
21
- @options_up = [*svnopts[:svnup]]
22
- @options_co = [*svnopts[:svnco]]
23
- if rev = svnopts[:revision]
24
- @options_up << "--revision" << rev
25
- @options_co << "--revision" << rev
26
- end
19
+ :repository_id => "svn:#{svnroot}"
20
+ common[:repository_id] = svnopts.delete(:repository_id)
21
+ relocate(svnroot, svnopts)
27
22
  super(common.merge(repository_id: svnopts[:repository_id]))
28
23
  end
29
24
 
30
- private
25
+ # @deprecated use {svnroot} instead
26
+ #
27
+ # @return [String]
28
+ def source; svnroot end
29
+
30
+ # Returns the SVN root
31
+ #
32
+ # @return [String]
33
+ attr_reader :svnroot
31
34
 
32
- def update(package,only_local=false) # :nodoc:
35
+ attr_reader :revision
36
+
37
+ def relocate(root, options = Hash.new)
38
+ @svnroot = [*root].join("/")
39
+ @options_up = [*options[:svnup]]
40
+ @options_co = [*options[:svnco]]
41
+ @revision = options[:revision]
42
+ if revision
43
+ @options_co << '--revision' << revision
44
+ # We do not add it to @options_up as the behaviour depends on
45
+ # the parameters given to {update} and to the state of the
46
+ # working copy
47
+ end
48
+ end
49
+
50
+ # Returns the SVN revision of the package
51
+ #
52
+ # @param [Package] package
53
+ # @return [Integer]
54
+ # @raises ConfigException if 'svn info' did not return a Revision field
55
+ # @raises (see svn_info)
56
+ def svn_revision(package)
57
+ svninfo = svn_info(package)
58
+ revision = svninfo.grep(/^Revision: /).first
59
+ if !revision
60
+ raise ConfigException.new(package, 'import'), "cannot get SVN information for #{package.importdir}"
61
+ end
62
+ revision =~ /Revision: (\d+)/
63
+ Integer($1)
64
+ end
65
+
66
+ # Returns the URL of the remote SVN repository
67
+ #
68
+ # @param [Package] package
69
+ # @return [String]
70
+ # @raises ConfigException if 'svn info' did not return a URL field
71
+ # @raises (see svn_info)
72
+ def svn_url(package)
73
+ svninfo = svn_info(package)
74
+ url = svninfo.grep(/^URL: /).first
75
+ if !url
76
+ raise ConfigException.new(package, 'import'), "cannot get SVN information for #{package.importdir}"
77
+ end
78
+ url.chomp =~ /URL: (.+)/
79
+ $1
80
+ end
81
+
82
+ # Returns true if the SVN working copy at package.importdir has local
83
+ # modifications
84
+ #
85
+ # @param [Package] package the package we want to test against
86
+ # @param [Boolean] with_untracked_files if true, the presence of files
87
+ # neither ignored nor under version control will count has local
88
+ # modification.
89
+ # @return [Boolean]
90
+ def has_local_modifications?(package, with_untracked_files = false)
91
+ status = run_svn(package, 'status', '--xml')
92
+
93
+ not_modified = %w{external ignored none normal}
94
+ if !with_untracked_files
95
+ not_modified << "unversioned"
96
+ end
97
+
98
+ REXML::Document.new(status.join("")).
99
+ elements.enum_for(:each, '//wc-status').
100
+ any? do |status_item|
101
+ !not_modified.include?(status_item.attributes['item'].to_s)
102
+ end
103
+ end
104
+
105
+ # Returns status information for package
106
+ #
107
+ # Given that subversion is not a distributed VCS, the only status
108
+ # returned are either {Status::UP_TO_DATE} or {Status::SIMPLE_UPDATE}.
109
+ # Moreover, if the status is local-only,
110
+ # {Package::Status#remote_commits} will not be filled (querying the log
111
+ # requires accessing the SVN server)
112
+ #
113
+ # @return [Package::Status]
114
+ def status(package, only_local = false)
115
+ status = Status.new
116
+ status.uncommitted_code = has_local_modifications?(package)
33
117
  if only_local
34
- Autobuild.warn "The importer #{self.class} does not support local updates, skipping #{self}"
118
+ status.status = Status::UP_TO_DATE
119
+ else
120
+ log = run_svn(package, 'log', '-r', 'BASE:HEAD', '--xml', '.')
121
+ log = REXML::Document.new(log.join("\n"))
122
+ missing_revisions = log.elements.enum_for(:each, 'log/logentry').map do |l|
123
+ rev = l.attributes['revision']
124
+ date = l.elements['date'].first.to_s
125
+ author = l.elements['author'].first.to_s
126
+ msg = l.elements['msg'].first.to_s.split("\n").first
127
+ "#{rev} #{DateTime.parse(date)} #{author} #{msg}"
128
+ end
129
+ status.remote_commits = missing_revisions[1..-1]
130
+ status.status =
131
+ if missing_revisions.empty?
132
+ Status::UP_TO_DATE
133
+ else
134
+ Status::SIMPLE_UPDATE
135
+ end
136
+ end
137
+ status
138
+ end
139
+
140
+ # Helper method to run a SVN command on a package's working copy
141
+ def run_svn(package, *args, &block)
142
+ options = Hash.new
143
+ if args.last.kind_of?(Hash)
144
+ options = args.pop
145
+ end
146
+ options, other_options = Kernel.filter_options options,
147
+ working_directory: package.importdir, retry: true
148
+ options = options.merge(other_options)
149
+ package.run(:import, Autobuild.tool(:svn), *args, options, &block)
150
+ end
151
+
152
+ def validate_importdir(package)
153
+ # This upgrades the local SVN filesystem if needed and checks that
154
+ # it actually is a SVN repository in the first place
155
+ svn_info(package)
156
+ end
157
+
158
+ # Returns the result of the 'svn info' command
159
+ #
160
+ # It automatically runs svn upgrade if needed
161
+ #
162
+ # @param [Package] package
163
+ # @return [Array<String>] the lines returned by svn info, with the
164
+ # trailing newline removed
165
+ # @raises [SubcommandFailed] if svn info failed
166
+ # @raises [ConfigException] if the working copy is not a subversion
167
+ # working copy
168
+ def svn_info(package)
169
+ old_lang, ENV['LC_ALL'] = ENV['LC_ALL'], 'C'
170
+ begin
171
+ svninfo = run_svn(package, 'info')
172
+ rescue SubcommandFailed => e
173
+ if e.output.find { |l| l =~ /svn upgrade/ }
174
+ # Try svn upgrade and info again
175
+ run_svn(package, 'upgrade', retry: false)
176
+ svninfo = run_svn(package, 'info')
177
+ else raise
178
+ end
179
+ end
180
+
181
+ if !svninfo.grep(/is not a working copy/).empty?
182
+ raise ConfigException.new(package, 'import'),
183
+ "#{package.importdir} does not appear to be a Subversion working copy"
184
+ end
185
+ svninfo
186
+ ensure
187
+ ENV['LC_ALL'] = old_lang
188
+ end
189
+
190
+ def update(package, options = Hash.new) # :nodoc:
191
+ if options[:only_local]
192
+ package.warn "%s: the svn importer does not support local updates, skipping"
35
193
  return
36
194
  end
37
- Dir.chdir(package.importdir) do
38
- old_lang, ENV['LC_ALL'] = ENV['LC_ALL'], 'C'
39
- svninfo = []
40
- begin
41
- Subprocess.run(package, :import, @program, 'info') do |line|
42
- svninfo << line
43
- end
44
- rescue SubcommandFailed => e
45
- if svninfo.find { |l| l =~ /svn upgrade/ }
46
- # Try svn upgrade and info again
47
- Subprocess.run(package, :import, @program, 'upgrade')
48
- svninfo.clear
49
- Subprocess.run(package, :import, @program, 'info') do |line|
50
- svninfo << line
51
- end
52
- else raise
53
- end
195
+
196
+ url = svn_url(package)
197
+ if url != svnroot
198
+ raise ConfigException.new(package, 'import'), "current checkout found at #{package.importdir} is from #{url}, was expecting #{svnroot}"
199
+ end
200
+
201
+ options_up = @options_up.dup
202
+ if revision
203
+ if options[:reset] || svn_revision(package) < revision
204
+ options_up << '--revision' << revision
205
+ elsif revision
206
+ # Don't update if the current revision is greater-or-equal
207
+ # than the target revision
208
+ return
54
209
  end
55
- ENV['LC_ALL'] = old_lang
56
- unless url = svninfo.grep(/^URL: /).first
57
- if svninfo.grep(/is not a working copy/).empty?
58
- raise ConfigException.new(package, 'import'), "#{package.importdir} is not a Subversion working copy"
59
- else
60
- raise ConfigException.new(package, 'import'), "Bug: cannot get SVN information for #{package.importdir}"
61
- end
62
- end
63
- url.chomp =~ /URL: (.+)/
64
- source = $1
65
- if source != @source
66
- raise ConfigException.new(package, 'import'), "current checkout found at #{package.importdir} is from #{source}, was expecting #{@source}"
67
- end
68
- Subprocess.run(package, :import, @program, 'up', "--non-interactive", *@options_up)
69
210
  end
211
+
212
+ run_svn(package, 'up', "--non-interactive", *options_up)
70
213
  end
71
214
 
72
215
  def checkout(package) # :nodoc:
73
- options = [ @program, 'co', "--non-interactive" ] + @options_co + [ @source, package.importdir ]
74
- Subprocess.run(package, :import, *options)
216
+ run_svn(package, 'co', "--non-interactive", *@options_co, svnroot, package.importdir,
217
+ working_directory: nil)
75
218
  end
76
219
  end
77
220
 
@@ -53,8 +53,8 @@ class Importer
53
53
  # repository and not in the remote one (would be pushed by an update)
54
54
  attr_accessor :local_commits
55
55
 
56
- def initialize
57
- @status = -1
56
+ def initialize(status = -1)
57
+ @status = status
58
58
  @uncommitted_code = false
59
59
  @remote_commits = Array.new
60
60
  @local_commits = Array.new
@@ -141,6 +141,18 @@ class Importer
141
141
  end
142
142
  end
143
143
 
144
+ def update_retry_count(original_error, retry_count)
145
+ if !original_error.respond_to?(:retry?) ||
146
+ !original_error.retry?
147
+ return
148
+ end
149
+
150
+ retry_count += 1
151
+ if retry_count <= self.retry_count
152
+ retry_count
153
+ end
154
+ end
155
+
144
156
  def perform_update(package,only_local=false)
145
157
  cur_patches = currently_applied_patches(package)
146
158
  needed_patches = self.patches
@@ -176,10 +188,8 @@ class Importer
176
188
  end
177
189
  end
178
190
 
179
- retry_count += 1
180
- if retry_count > self.retry_count
181
- raise
182
- end
191
+ retry_count = update_retry_count(original_error, retry_count)
192
+ raise if !retry_count
183
193
  package.message "update failed in #{package.importdir}, retrying (#{retry_count}/#{self.retry_count})"
184
194
  retry
185
195
  ensure
@@ -201,9 +211,9 @@ class Importer
201
211
  checkout(package)
202
212
  rescue Interrupt
203
213
  raise
204
- rescue ::Exception
205
- retry_count += 1
206
- if retry_count > self.retry_count
214
+ rescue ::Exception => original_error
215
+ retry_count = update_retry_count(original_error, retry_count)
216
+ if !retry_count
207
217
  raise
208
218
  end
209
219
  package.message "checkout of %s failed, deleting the source directory #{package.importdir} and retrying (#{retry_count}/#{self.retry_count})"
@@ -225,13 +235,43 @@ class Importer
225
235
  fallback(e, package, :import, package)
226
236
  end
227
237
 
228
- # Performs the import of +package+
229
- def import(package,only_local = false)
238
+ # Imports the given package
239
+ #
240
+ # The importer will checkout or update code in package.importdir. No update
241
+ # will be done if {update?} returns false.
242
+ #
243
+ # @raises ConfigException if package.importdir exists and is not a directory
244
+ #
245
+ # @option options [Boolean] :only_local (false) if true, will only perform
246
+ # actions that do not require network access. Importers that do not
247
+ # support this mode will simply do nothing
248
+ # @option options [Boolean] :reset (false) if true, the importer's
249
+ # configuration is interpreted as a hard state in which it should put the
250
+ # working copy. Otherwise, it tries to update the local repository with
251
+ # the remote information. For instance, a git importer for which a commit
252
+ # ID is given will, in this mode, reset the repository to the requested ID
253
+ # (if that does not involve losing commits). Otherwise, it will only
254
+ # ensure that the requested commit ID is present in the current HEAD.
255
+ def import(package, options = Hash.new)
256
+ # Backward compatibility
257
+ if !options.kind_of?(Hash)
258
+ options = !!options
259
+ Autoproj.warn "calling #import with a boolean as second argument is deprecated, switch to the named argument interface instead"
260
+ Autoproj.warn " e.g. call import(package, only_local: #{options})"
261
+ Autoproj.warn " #{caller(1).first}"
262
+ options = Hash[only_local: !!options]
263
+ end
264
+
265
+ options = Kernel.validate_options options,
266
+ only_local: false,
267
+ reset: false,
268
+ checkout_only: false
269
+
230
270
  importdir = package.importdir
231
271
  if File.directory?(importdir)
232
272
  package.isolate_errors(false) do
233
- if package.update?
234
- perform_update(package,only_local)
273
+ if !options[:checkout_only] && package.update?
274
+ perform_update(package, options)
235
275
  else
236
276
  if Autobuild.verbose
237
277
  package.message "%s: not updating"
@@ -240,7 +280,7 @@ class Importer
240
280
  end
241
281
  end
242
282
 
243
- elsif File.exists?(importdir)
283
+ elsif File.exist?(importdir)
244
284
  raise ConfigException.new(package, 'import'), "#{importdir} exists but is not a directory"
245
285
  else
246
286
  perform_checkout(package)
@@ -275,23 +315,22 @@ class Importer
275
315
  end
276
316
 
277
317
  def call_patch(package, reverse, file, patch_level)
278
- patch = Autobuild.tool('patch')
279
- Dir.chdir(package.importdir) do
280
- Subprocess.run(package, :patch, patch, "-p#{patch_level}", (reverse ? '-R' : nil), '--forward', :input => file)
281
- end
318
+ package.run(:patch, Autobuild.tool('patch'),
319
+ "-p#{patch_level}", (reverse ? '-R' : nil), '--forward', input: file,
320
+ working_directory: package.importdir)
282
321
  end
283
322
 
284
323
  def apply(package, path, patch_level = 0); call_patch(package, false, path, patch_level) end
285
324
  def unapply(package, path, patch_level = 0); call_patch(package, true, path, patch_level) end
286
325
 
287
- def parse_patch_list(patches_file)
326
+ def parse_patch_list(package, patches_file)
288
327
  File.readlines(patches_file).map do |line|
289
328
  line = line.rstrip
290
329
  if line =~ /^(.*)\s+(\d+)$/
291
- path = $1
330
+ path = File.expand_path($1, package.srcdir)
292
331
  level = Integer($2)
293
332
  else
294
- path = line
333
+ path = File.expand_path(line, package.srcdir)
295
334
  level = 0
296
335
  end
297
336
  [path, level, File.read(path)]
@@ -300,13 +339,13 @@ class Importer
300
339
 
301
340
  def currently_applied_patches(package)
302
341
  patches_file = patchlist(package)
303
- if File.exists?(patches_file)
304
- return parse_patch_list(patches_file)
342
+ if File.exist?(patches_file)
343
+ return parse_patch_list(package, patches_file)
305
344
  end
306
345
 
307
346
  patches_file = File.join(package.importdir, "patches-autobuild-stamp")
308
- if File.exists?(patches_file)
309
- cur_patches = parse_patch_list(patches_file)
347
+ if File.exist?(patches_file)
348
+ cur_patches = parse_patch_list(package, patches_file)
310
349
  save_patch_state(package, cur_patches)
311
350
  FileUtils.rm_f patches_file
312
351
  return currently_applied_patches(package)
@@ -319,7 +358,9 @@ class Importer
319
358
  # Get the list of already applied patches
320
359
  cur_patches = currently_applied_patches(package)
321
360
 
322
- if cur_patches.map(&:last) == patches.map(&:last)
361
+ cur_patches_state = cur_patches.map { |_, level, content| [level, content] }
362
+ patches_state = patches.map { |_, level, content| [level, content] }
363
+ if cur_patches_state == patches_state
323
364
  return false
324
365
  end
325
366
 
@@ -342,9 +383,9 @@ class Importer
342
383
  cur_patches.pop
343
384
  end
344
385
 
345
- patches.to_a.each do |p, level, content|
346
- apply(package, p, level)
347
- cur_patches << [p, level, content]
386
+ patches.to_a.each do |new_patch, new_patch_level, content|
387
+ apply(package, new_patch, new_patch_level)
388
+ cur_patches << [new_patch, new_patch_level, content]
348
389
  end
349
390
  ensure
350
391
  save_patch_state(package, cur_patches)
@@ -357,14 +398,18 @@ class Importer
357
398
  patch_dir = patchdir(package)
358
399
  FileUtils.mkdir_p patch_dir
359
400
  cur_patches = cur_patches.each_with_index.map do |(path, level, content), idx|
360
- path = File.join(patch_dir, idx.to_s)
361
- File.open(path, 'w') do |patch_io|
362
- patch_io.write content
363
- end
364
- [path, level]
401
+ path = File.join(patch_dir, idx.to_s)
402
+ File.open(path, 'w') do |patch_io|
403
+ patch_io.write content
404
+ end
405
+ [path, level]
365
406
  end
366
407
  File.open(patchlist(package), 'w') do |f|
367
- f.write(cur_patches.map { |p, l| "#{p} #{l}" }.join("\n"))
408
+ patch_state = cur_patches.map do |path, level|
409
+ path = Pathname.new(path).relative_path_from( Pathname.new(package.srcdir) ).to_s
410
+ "#{path} #{level}"
411
+ end
412
+ f.write(patch_state.join("\n"))
368
413
  end
369
414
  end
370
415