autobuild 1.8.3 → 1.9.0.b1

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