autoproj-ci 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 807429c3a708c91b9d1f061bd3138d6ce7f6bfa63b60cb56505b8c817dc0ea9a
4
- data.tar.gz: 0cc4aa3955d61d2b30d5aad85a49c888b4312dd12c9685ca17e8f5fc261d8fde
3
+ metadata.gz: 7607b476bb61d53de2e2992f1d8ca7d91c2164090d9e22fe136aedce9004c735
4
+ data.tar.gz: 8f77955a24f0bc516b9251c1dd7cbad4326a22d5d1b42a50010b4975696f3adc
5
5
  SHA512:
6
- metadata.gz: dfd1ed123ab40e60b71f41925940dbb947b16d91abb5e6c8e68f66ef18283ebb97e9953c4a4f1151776d0cfe09f25d807ee884dbfcc314cee56852e5f9ccfdb2
7
- data.tar.gz: 3824b52d7b226df53b6304d2a751357081052e50478fcd8cab4d844a5384b4d1b6b79b98fd61060f96557b8286b9eb16626836094c86768c37552b901af6f608
6
+ metadata.gz: 62be513aba6072efdbe008fcedea7e95044298c4b46d53d624db42e32a8be121272e10e322ed2b3ab31c41cf8a1e05291e1ca51ae613eeec98cbb44d067954a7
7
+ data.tar.gz: 2b24d8860e43bb9de8330fd5989ff8920892c320c894369efc46ed4941dad9e5fb52e3cb36311f698b013bb8337dd07ef0dac8bd27fbc11e4ebecc2602a28cb4
data/Gemfile CHANGED
@@ -8,6 +8,7 @@ group :vscode do
8
8
  gem 'ruby-debug-ide', '>= 0.6.0'
9
9
  gem 'debase', '>= 0.2.2.beta10'
10
10
  gem 'solargraph'
11
+ gem 'rubocop-rock'
11
12
  end
12
13
 
13
14
  git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
@@ -25,13 +25,14 @@ Gem::Specification.new do |spec|
25
25
  spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
26
26
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
27
  end
28
- spec.bindir = "exe"
29
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.bindir = "bin"
29
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
30
30
  spec.require_paths = ["lib"]
31
31
 
32
32
  spec.add_dependency 'autoproj'
33
33
  spec.add_development_dependency "flexmock"
34
34
  spec.add_development_dependency "bundler"
35
35
  spec.add_development_dependency "rake"
36
+ spec.add_development_dependency "timecop"
36
37
  spec.add_development_dependency "minitest", "~> 5.0"
37
38
  end
@@ -0,0 +1,5 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'autoproj/cli/standalone_ci'
4
+ Autoproj::CLI::StandaloneCI.start(ARGV)
5
+
@@ -1,6 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'autoproj/cli/main_ci'
2
4
 
3
- class Autoproj::CLI::Main
4
- desc 'ci', 'subcommands tuned for usage in CI environments'
5
- subcommand 'ci', Autoproj::CLI::MainCI
6
- end
5
+ module Autoproj
6
+ module CLI
7
+ # Toplevel CLI interface from Autoproj
8
+ class Main
9
+ desc 'ci', 'subcommands tuned for usage in CI environments'
10
+ subcommand 'ci', Autoproj::CLI::MainCI
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'autoproj'
4
+ require 'tmpdir'
5
+
6
+ module Autoproj
7
+ module CI
8
+ # Utilities to re-create a system image from the results of a CI build
9
+ module Rebuild
10
+ # Create a single tarball containing all the artifacts of a given build
11
+ #
12
+ # The generated tarball is 'rooted' at the filesystem root, i.e. it is meant
13
+ # to be unpacked from /
14
+ def self.prepare_synthetic_buildroot(
15
+ installation_manifest_path, versions_path, cache_root_path, output_dir
16
+ )
17
+ manifest = Autoproj::InstallationManifest.new(installation_manifest_path)
18
+ manifest.load
19
+ versions = YAML.safe_load(File.read(versions_path))
20
+
21
+ versions.each do |entry|
22
+ name, entry = entry.first
23
+ next if /^pkg_set:/.match?(name)
24
+
25
+ unpack_package(
26
+ output_dir,
27
+ cache_root_path, name,
28
+ manifest.packages.fetch(name),
29
+ entry.fetch('fingerprint')
30
+ )
31
+ end
32
+ end
33
+
34
+ def self.dpkg_create_package_install(status_path, rules, orig: nil)
35
+ installed, = Autoproj::PackageManagers::AptDpkgManager
36
+ .parse_dpkg_status(status_path, virtual: false)
37
+
38
+ if orig
39
+ orig_installed, = Autoproj::PackageManagers::AptDpkgManager
40
+ .parse_dpkg_status(orig, virtual: false)
41
+ installed -= orig_installed
42
+ end
43
+
44
+ installed.find_all do |pkg_name|
45
+ package_matches_rules?(pkg_name, rules)
46
+ end
47
+ end
48
+
49
+ def self.package_matches_rules?(pkg_name, rules)
50
+ rules.each do |mode, r|
51
+ return mode if r.match?(pkg_name)
52
+ end
53
+ true
54
+ end
55
+
56
+ # Unpack a single package in its place within the
57
+ def self.unpack_package(output_path, cache_root_path, name, pkg, fingerprint)
58
+ cache_file_path = File.join(cache_root_path, name, fingerprint)
59
+ unless File.file?(cache_file_path)
60
+ raise "no cache file found for fingerprint '#{fingerprint}', "\
61
+ "package '#{name}' in #{cache_root_path}"
62
+ end
63
+
64
+ package_prefix = File.join(output_path, pkg.prefix)
65
+ FileUtils.mkdir_p(package_prefix)
66
+ unless system('tar', 'xzf', cache_file_path,
67
+ chdir: package_prefix, out: '/dev/null')
68
+ raise "failed to unpack #{cache_file_path} in #{package_prefix} "\
69
+ "for package #{name} failed"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Autoproj
2
4
  # autoproj-ci is an Autoproj plugin to provide functionality useful for CI
3
5
  # (automated builds & tests)
4
6
  module CI
5
- VERSION = "0.2.0"
7
+ VERSION = '0.3.0'
6
8
  end
7
9
  end
@@ -0,0 +1,12 @@
1
+ #! /bin/sh -e
2
+
3
+ if test "x$1" != "xexec"; then
4
+ echo "This is a minimal stub of autoproj meant only to"\
5
+ "allow for running `autoproj exec`" >& 2
6
+ exit 1
7
+ fi
8
+
9
+ shift
10
+
11
+ . <%= workspace_dir %>/env.sh
12
+ exec "$@"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'autoproj/cli/inspection_tool'
2
4
  require 'tmpdir'
3
5
 
@@ -10,51 +12,102 @@ module Autoproj
10
12
  # same pattern, and registers its subcommand in {MainCI} while implementing
11
13
  # the functionality in this class
12
14
  class CI < InspectionTool
15
+ PHASES = %w[import build test].freeze
16
+
13
17
  def resolve_packages
14
18
  initialize_and_load
15
19
  source_packages, * = finalize_setup(
16
- [], non_imported_packages: :ignore)
20
+ [], non_imported_packages: :ignore
21
+ )
17
22
  source_packages.map do |pkg_name|
18
23
  ws.manifest.find_autobuild_package(pkg_name)
19
24
  end
20
25
  end
21
26
 
22
- def cache_pull(dir, silent: true)
27
+ def cache_state(dir, ignore: [])
28
+ packages = resolve_packages
29
+
30
+ memo = {}
31
+ packages.each_with_object({}) do |pkg, h|
32
+ state = package_cache_state(dir, pkg, memo: memo)
33
+ if ignore.include?(pkg.name)
34
+ state = state.merge('cached' => false, 'metadata' => false)
35
+ end
36
+
37
+ h[pkg.name] = state
38
+ end
39
+ end
40
+
41
+ def cache_pull(dir, ignore: [])
23
42
  packages = resolve_packages
24
43
 
25
- memo = Hash.new
44
+ memo = {}
26
45
  results = packages.each_with_object({}) do |pkg, h|
27
- state, fingerprint = pull_package_from_cache(dir, pkg, memo: memo)
28
- puts "pulled #{pkg.name} (#{fingerprint})" if state && !silent
46
+ if ignore.include?(pkg.name)
47
+ pkg.message '%s: ignored by command line'
48
+ fingerprint = pkg.fingerprint(memo: memo)
49
+ h[pkg.name] = {
50
+ 'cached' => false,
51
+ 'fingerprint' => fingerprint
52
+ }
53
+ next
54
+ end
29
55
 
30
- h[pkg.name] = {
56
+ state, fingerprint, metadata =
57
+ pull_package_from_cache(dir, pkg, memo: memo)
58
+ if state
59
+ pkg.message "%s: pulled #{fingerprint}", :green
60
+ else
61
+ pkg.message "%s: #{fingerprint} not in cache, "\
62
+ 'or not pulled from cache'
63
+ end
64
+
65
+ h[pkg.name] = metadata.merge(
31
66
  'cached' => state,
32
67
  'fingerprint' => fingerprint
33
- }
68
+ )
34
69
  end
35
70
 
36
- unless silent
37
- hit = results.count { |_, info| info['cached'] }
38
- puts "#{hit} hits, #{results.size - hit} misses"
39
- end
71
+ hit = results.count { |_, info| info['cached'] }
72
+ Autoproj.message "#{hit} hits, #{results.size - hit} misses"
40
73
 
41
74
  results
42
75
  end
43
76
 
44
- def cache_push(dir, silent: true)
77
+ def cache_push(dir)
45
78
  packages = resolve_packages
79
+ metadata = consolidated_report['packages']
46
80
 
47
- built = load_built_flags
48
-
49
- memo = Hash.new
81
+ memo = {}
50
82
  results = packages.each_with_object({}) do |pkg, h|
51
- unless built[pkg.name]
83
+ if !(pkg_metadata = metadata[pkg.name])
84
+ pkg.message '%s: no metadata in build report', :magenta
85
+ next
86
+ elsif !(build_info = pkg_metadata['build'])
87
+ pkg.message '%s: no build info in build report', :magenta
88
+ next
89
+ elsif build_info['cached']
90
+ pkg.message '%s: was pulled from cache, not pushing'
91
+ next
92
+ elsif !build_info['success']
93
+ pkg.message '%s: build failed, not pushing', :magenta
52
94
  next
53
95
  end
54
96
 
55
- state, fingerprint = push_package_to_cache(dir, pkg, memo: memo)
56
- puts "pushed #{pkg.name} (#{fingerprint})" if state && !silent
97
+ # Remove cached flags before saving
98
+ pkg_metadata = pkg_metadata.dup
99
+ PHASES.each do |phase_name|
100
+ pkg_metadata[phase_name]&.delete('cached')
101
+ end
57
102
 
103
+ state, fingerprint = push_package_to_cache(
104
+ dir, pkg, pkg_metadata, force: true, memo: memo
105
+ )
106
+ if state
107
+ pkg.message "%s: pushed #{fingerprint}", :green
108
+ else
109
+ pkg.message "%s: #{fingerprint} already in cache"
110
+ end
58
111
 
59
112
  h[pkg.name] = {
60
113
  'updated' => state,
@@ -62,34 +115,81 @@ module Autoproj
62
115
  }
63
116
  end
64
117
 
65
- unless silent
66
- hit = results.count { |_, info| info['updated'] }
67
- puts "#{hit} updated packages, #{results.size - hit} reused entries"
68
- end
118
+ hit = results.count { |_, info| info['updated'] }
119
+ Autoproj.message "#{hit} updated packages, #{results.size - hit} "\
120
+ 'reused entries'
69
121
 
70
122
  results
71
123
  end
72
124
 
125
+ # Checks if a package's test results should be processed with xunit-viewer
126
+ #
127
+ # @param [String] results_dir the directory where the
128
+ # @param [String] xunit_output path to the xunit-viewer output. An
129
+ # existing file is re-generated only if force is true
130
+ # @param [Boolean] force re-generation of the xunit-viewer output
131
+ def need_xunit_processing?(results_dir, xunit_output, force: false)
132
+ # We don't re-generate if the xunit-processed files were cached
133
+ return if !force && File.file?(xunit_output)
134
+
135
+ # We only check whether there are xml files in the
136
+ # package's test dir. That's the only check we do ... if
137
+ # the XML files are not JUnit, we'll finish with an empty
138
+ # xunit html file
139
+ Dir.enum_for(:glob, File.join(results_dir, '*.xml'))
140
+ .first
141
+ end
142
+
143
+ # Process the package's test results with xunit-viewer
144
+ #
145
+ # @param [String] xunit_viewer path to xunit-viewer
146
+ # @param [Boolean] force re-generation of the xunit-viewer output. If
147
+ # false, packages that already have a xunit-viewer output will be skipped
148
+ def process_test_results_xunit(force: false, xunit_viewer: 'xunit-viewer')
149
+ consolidated_report['packages'].each_value do |info|
150
+ next unless info['test']
151
+ next unless (results_dir = info['test']['target_dir'])
152
+
153
+ xunit_output = "#{results_dir}.html"
154
+ next unless need_xunit_processing?(results_dir, xunit_output,
155
+ force: force)
156
+
157
+ success = system(xunit_viewer,
158
+ "--results=#{results_dir}",
159
+ "--output=#{xunit_output}")
160
+ unless success
161
+ Autoproj.warn 'xunit-viewer conversion failed '\
162
+ "for '#{results_dir}'"
163
+ end
164
+ end
165
+ end
166
+
167
+ # Post-processing of test results
168
+ def process_test_results(force: false, xunit_viewer: 'xunit-viewer')
169
+ process_test_results_xunit(force: force, xunit_viewer: xunit_viewer)
170
+ end
171
+
73
172
  # Build a report in a given directory
74
173
  #
75
174
  # The method itself will not archive the directory, only gather the
76
175
  # information in a consistent way
77
- def build_report(dir)
176
+ def create_report(dir)
78
177
  initialize_and_load
79
178
  finalize_setup([], non_imported_packages: :ignore)
80
179
 
81
180
  report = consolidated_report
82
- FileUtils.mkdir_p dir
181
+ FileUtils.mkdir_p(dir)
83
182
  File.open(File.join(dir, 'report.json'), 'w') do |io|
84
183
  JSON.dump(report, io)
85
184
  end
86
185
 
87
- installation_manifest = InstallationManifest.
88
- from_workspace_root(@ws.root_dir)
186
+ installation_manifest = InstallationManifest
187
+ .from_workspace_root(@ws.root_dir)
89
188
  logs = File.join(dir, 'logs')
189
+
90
190
  # Pre-create the logs, or cp_r will have a different behavior
91
191
  # if the directory exists or not
92
- FileUtils.mkdir_p logs
192
+ FileUtils.mkdir_p(logs)
93
193
  installation_manifest.each_package do |pkg|
94
194
  glob = Dir.glob(File.join(pkg.logdir, '*'))
95
195
  FileUtils.cp_r(glob, logs) if File.directory?(pkg.logdir)
@@ -101,79 +201,153 @@ module Autoproj
101
201
  File.join(dir, pkg.name, fingerprint)
102
202
  end
103
203
 
204
+ def package_cache_state(dir, pkg, memo: {})
205
+ fingerprint = pkg.fingerprint(memo: memo)
206
+ path = package_cache_path(dir, pkg, fingerprint: fingerprint, memo: memo)
207
+
208
+ {
209
+ 'path' => path,
210
+ 'cached' => File.file?(path),
211
+ 'metadata' => File.file?("#{path}.json"),
212
+ 'fingerprint' => fingerprint
213
+ }
214
+ end
215
+
216
+ class PullError < RuntimeError
217
+ end
218
+
104
219
  def pull_package_from_cache(dir, pkg, memo: {})
105
220
  fingerprint = pkg.fingerprint(memo: memo)
106
221
  path = package_cache_path(dir, pkg, fingerprint: fingerprint, memo: memo)
107
- unless File.file?(path)
108
- return [false, fingerprint]
222
+ return [false, fingerprint, {}] unless File.file?(path)
223
+
224
+ metadata_path = "#{path}.json"
225
+ metadata =
226
+ if File.file?(metadata_path)
227
+ JSON.parse(File.read(metadata_path))
228
+ else
229
+ {}
230
+ end
231
+
232
+ # Do not pull packages for which we should run tests
233
+ tests_enabled = pkg.test_utility.enabled?
234
+ tests_invoked = metadata['test'] && metadata['test']['invoked']
235
+ if tests_enabled && !tests_invoked
236
+ pkg.message '%s: has tests that have never '\
237
+ 'been invoked, not pulling from cache'
238
+ return [false, fingerprint, {}]
109
239
  end
110
240
 
111
- FileUtils.mkdir_p pkg.prefix
112
- result = system("tar", "xzf", path, chdir: pkg.prefix, out: '/dev/null')
113
- unless result
114
- raise "tar failed when pulling cache file for #{pkg.name}"
241
+ FileUtils.mkdir_p(pkg.prefix)
242
+ unless system('tar', 'xzf', path, chdir: pkg.prefix, out: '/dev/null')
243
+ raise PullError, "tar failed when pulling cache file for #{pkg.name}"
115
244
  end
116
- [true, pkg.fingerprint(memo: memo)]
245
+
246
+ [true, fingerprint, metadata]
117
247
  end
118
248
 
119
- def push_package_to_cache(dir, pkg, memo: {})
249
+ def push_package_to_cache(dir, pkg, metadata, force: false, memo: {})
120
250
  fingerprint = pkg.fingerprint(memo: memo)
121
251
  path = package_cache_path(dir, pkg, fingerprint: fingerprint, memo: memo)
122
- if File.file?(path)
123
- return [false, fingerprint]
252
+ temppath = "#{path}.#{Process.pid}.#{rand(256)}"
253
+
254
+ FileUtils.mkdir_p(File.dirname(path))
255
+ if force || !File.file?("#{path}.json")
256
+ File.open(temppath, 'w') { |io| JSON.dump(metadata, io) }
257
+ FileUtils.mv(temppath, "#{path}.json")
124
258
  end
125
259
 
126
- temppath = "#{path}.#{Process.pid}.#{rand(256)}"
127
- FileUtils.mkdir_p File.dirname(path)
128
- result = system("tar", "czf", temppath, ".",
129
- chdir: pkg.prefix, out: '/dev/null')
130
- unless result
131
- raise "tar failed when pushing cache file for #{pkg.name}"
260
+ if !force && File.file?(path)
261
+ # Update modification time for the cleanup process
262
+ FileUtils.touch(path)
263
+ return [false, fingerprint]
132
264
  end
133
265
 
134
- FileUtils.mv temppath, path
266
+ result = system('tar', 'czf', temppath, '.',
267
+ chdir: pkg.prefix, out: '/dev/null')
268
+ raise "tar failed when pushing cache file for #{pkg.name}" unless result
269
+
270
+ FileUtils.mv(temppath, path)
135
271
  [true, fingerprint]
136
272
  end
137
273
 
274
+ def cleanup_build_cache(dir, size_limit)
275
+ all_files = Find.enum_for(:find, dir).map do |path|
276
+ next unless File.file?(path) && File.file?("#{path}.json")
277
+
278
+ [path, File.stat(path)]
279
+ end.compact
280
+
281
+ total_size = all_files.map { |_, s| s.size }.sum
282
+ lru = all_files.sort_by { |_, s| s.mtime }
283
+
284
+ while total_size > size_limit
285
+ path, stat = lru.shift
286
+ Autoproj.message "removing #{path} (size=#{stat.size}, mtime=#{stat.mtime})"
287
+
288
+ FileUtils.rm_f path
289
+ FileUtils.rm_f "#{path}.json"
290
+ total_size -= stat.size
291
+ end
292
+
293
+ Autoproj.message format("current build cache size: %.1f GB", Float(total_size) / 1_000_000_000)
294
+ total_size
295
+ end
296
+
138
297
  def load_built_flags
139
298
  path = @ws.build_report_path
140
299
  return {} unless File.file?(path)
141
300
 
142
- report = JSON.load(File.read(path))
143
- report['build_report']['packages'].
144
- each_with_object({}) do |pkg_report, h|
301
+ report = JSON.parse(File.read(path))
302
+ report['build_report']['packages']
303
+ .each_with_object({}) do |pkg_report, h|
145
304
  h[pkg_report['name']] = pkg_report['built']
146
305
  end
147
306
  end
148
307
 
308
+ def load_report(path, root_name, default: { 'packages' => {} })
309
+ return default unless File.file?(path)
310
+
311
+ JSON.parse(File.read(path)).fetch(root_name)
312
+ end
313
+
149
314
  def consolidated_report
150
- cache_pull = File.join(@ws.root_dir, 'cache-pull.json')
151
- cache_report = if File.file?(cache_pull)
152
- JSON.load(File.read(cache_pull))
153
- else
154
- {}
155
- end
315
+ # NOTE: keys must match PHASES
316
+ new_reports = {
317
+ 'import' => @ws.import_report_path,
318
+ 'build' => @ws.build_report_path,
319
+ 'test' => @ws.utility_report_path('test')
320
+ }
156
321
 
157
- packages =
158
- if File.file?(@ws.build_report_path)
159
- path = @ws.build_report_path
160
- report = JSON.load(File.read(path))
161
- report['build_report']['packages']
162
- elsif File.file?(@ws.import_report_path)
163
- path = @ws.import_report_path
164
- report = JSON.load(File.read(path))
165
- report['import_report']['packages']
322
+ # We start with the cached info (if any) and override with
323
+ # information from the other phase reports
324
+ cache_report_path = File.join(@ws.root_dir, 'cache-pull.json')
325
+ result = load_report(cache_report_path, 'cache_pull_report')['packages']
326
+ result.delete_if do |_name, info|
327
+ next true unless info.delete('cached')
328
+
329
+ PHASES.each do |phase_name|
330
+ if (phase_info = info[phase_name])
331
+ phase_info['cached'] = true
332
+ end
166
333
  end
167
- return unless packages
334
+ false
335
+ end
168
336
 
169
- packages = packages.each_with_object({}) do |pkg_info, h|
170
- name = pkg_info.delete('name')
171
- h[name] = cache_report[name] || { 'cached' => false }
172
- h[name].merge!(pkg_info)
337
+ new_reports.each do |phase_name, path|
338
+ report = load_report(path, "#{phase_name}_report")
339
+ report['packages'].each do |pkg_name, pkg_info|
340
+ result[pkg_name] ||= {}
341
+ if pkg_info['invoked']
342
+ result[pkg_name][phase_name] = pkg_info.merge(
343
+ 'cached' => false,
344
+ 'timestamp' => report['timestamp']
345
+ )
346
+ end
347
+ end
173
348
  end
174
- { 'packages' => packages }
349
+ { 'packages' => result }
175
350
  end
176
351
  end
177
352
  end
178
353
  end
179
-
@@ -1,95 +1,185 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'autoproj'
4
+ require 'autoproj/cli/standalone_ci'
2
5
 
3
6
  module Autoproj
4
7
  module CLI
5
8
  # CLI interface for autoproj-ci
6
- class MainCI < Thor
7
- desc 'build [ARGS]', "Just like autoproj build, but can use a build cache"
9
+ class MainCI < StandaloneCI
10
+ desc 'build [ARGS]', 'Just like autoproj build, but can use a build cache'
8
11
  option :cache, type: 'string',
9
- desc: 'path to the build cache'
12
+ desc: 'path to the build cache'
13
+ option :cache_ignore, type: :array, default: [],
14
+ desc: 'list of packages to not pull from cache'
10
15
  option :report, type: 'string', default: 'cache-pull.json',
11
- desc: 'a file which describes what has been pulled'
16
+ desc: 'a file which describes what has been pulled'
12
17
  def build(*args)
13
18
  if (cache = options.delete(:cache))
14
19
  cache = File.expand_path(cache)
15
- results = cache_pull(cache)
16
- pulled_packages = results.
17
- map { |name, pkg| name if pkg['cached'] }.
18
- compact
20
+ require 'autoproj/cli/base'
21
+ Autoproj::CLI::Base.validate_options(args, options)
22
+ results = cache_pull(cache, ignore: options.delete(:cache_ignore))
23
+ pulled_packages = results
24
+ .map { |name, pkg| name if pkg['cached'] }
25
+ .compact
19
26
  not_args = ['--not', *pulled_packages] unless pulled_packages.empty?
20
27
  end
21
28
 
22
- Process.exec(Gem.ruby, $PROGRAM_NAME, 'build', "--interactive=f", *args, *not_args)
29
+ args << "--progress=#{options[:progress] ? 't' : 'f'}"
30
+ args << "--color=#{options[:color] ? 't' : 'f'}"
31
+ Process.exec(Gem.ruby, $PROGRAM_NAME, 'build',
32
+ '--interactive=f', *args, *not_args)
33
+ end
34
+
35
+ desc 'test [ARGS]', 'Like autoproj test, but selects only packages '\
36
+ 'that have been built'
37
+ def test(*args)
38
+ require 'autoproj/cli/ci'
39
+ cli = CI.new
40
+ cli.validate_options([], options.dup)
41
+ report = cli.consolidated_report
42
+
43
+ built_packages = report['packages'].find_all do |_name, info|
44
+ info['build'] && !info['build']['cached'] && info['build']['success']
45
+ end
46
+ return if built_packages.empty?
47
+
48
+ built_package_names = built_packages.map(&:first)
49
+ Process.exec(Gem.ruby, $PROGRAM_NAME, 'test',
50
+ 'exec', '--interactive=f', *args, *built_package_names)
51
+ end
52
+
53
+ desc 'process-test-results [ARGS]',
54
+ 'Process test output (assumed to be in JUnit XML) through xunit-viewer'
55
+ option :force, desc: 're-generates existing output', default: false
56
+ option :xunit_viewer, desc: 'path to xunit-viewer', default: 'xunit-viewer'
57
+ def process_test_results
58
+ require 'autoproj/cli/ci'
59
+ cli = CI.new
60
+ cli.validate_options([], options.dup)
61
+ cli.process_test_results(
62
+ force: options[:force],
63
+ xunit_viewer: options[:xunit_viewer]
64
+ )
65
+ end
66
+
67
+ desc 'status DIR', 'Display the cache status'
68
+ option :cache, type: 'string',
69
+ desc: 'path to the build cache'
70
+ def status(dir)
71
+ cache = File.expand_path(dir)
72
+ require 'autoproj/cli/ci'
73
+ cli = CI.new
74
+ cli.validate_options(dir, options)
75
+ results = cli.cache_state(cache)
76
+ results.keys.sort.each do |name|
77
+ status = results[name]
78
+ fields = []
79
+ fields <<
80
+ if status['cached']
81
+ Autoproj.color('cache hit', :green)
82
+ else
83
+ Autoproj.color('cache miss', :red)
84
+ end
85
+ fields << "fingerprint=#{status['fingerprint']}"
86
+ puts "#{name}: #{fields.join(', ')}"
87
+ end
23
88
  end
24
89
 
25
90
  desc 'cache-pull CACHE_DIR',
26
- "This command gets relevant artifacts from a build cache and "\
27
- "populates the current workspace's prefix with them. It is meant "\
28
- "to be executed after a full checkout of the workspace"
91
+ 'This command gets relevant artifacts from a build cache and '\
92
+ 'populates the current workspace\'s prefix with them. It is meant '\
93
+ 'to be executed after a full checkout of the workspace'
29
94
  option :report, type: 'string', default: 'cache-pull.json',
30
- desc: 'a file which describes what has been done'
31
- def cache_pull(dir)
95
+ desc: 'a file which describes what has been done'
96
+ option :ignore, type: :array, default: [],
97
+ desc: 'list of packages to not pull from cache'
98
+ def cache_pull(dir, ignore: [])
32
99
  dir = File.expand_path(dir)
33
100
 
34
101
  require 'autoproj/cli/ci'
35
102
  results = nil
36
- Autoproj.report(silent: true) do
37
- cli = CI.new
38
- args, options = cli.validate_options(dir, self.options)
39
- report = options.delete(:report)
40
103
 
41
- results = cli.cache_pull(*dir, silent: false, **options)
104
+ cli = CI.new
105
+ _, options = cli.validate_options(dir, self.options)
106
+ report = options.delete(:report)
42
107
 
43
- if report && !report.empty?
44
- File.open(report, 'w') do |io|
45
- JSON.dump(results, io)
46
- end
108
+ # options[:ignore] is not set if we call from another
109
+ # command, e.g. build
110
+ ignore += (options.delete(:ignore) || [])
111
+ results = cli.cache_pull(*dir, ignore: ignore, **options)
112
+
113
+ if report && !report.empty?
114
+ File.open(report, 'w') do |io|
115
+ JSON.dump(
116
+ {
117
+ 'cache_pull_report' => {
118
+ 'packages' => results
119
+ }
120
+ }, io
121
+ )
47
122
  end
48
123
  end
49
124
  results
50
125
  end
51
126
 
52
127
  desc 'cache-push CACHE_DIR',
53
- "This command writes the packages successfully built in the last "\
54
- "build to the given build cache, so that they can be reused with "\
55
- "cache-pull"
128
+ 'This command writes the packages successfully built in the last '\
129
+ 'build to the given build cache, so that they can be reused with '\
130
+ 'cache-pull'
56
131
  option :report, type: 'string', default: 'cache-push.json',
57
- desc: 'a file which describes what has been done'
132
+ desc: 'a file which describes what has been done'
58
133
  def cache_push(dir)
59
134
  dir = File.expand_path(dir)
60
135
 
61
136
  require 'autoproj/cli/ci'
62
- Autoproj.report(silent: true) do
63
- cli = CI.new
137
+ cli = CI.new
64
138
 
65
- args, options = cli.validate_options(dir, self.options)
66
- report = options.delete(:report)
139
+ _, options = cli.validate_options(dir, self.options)
140
+ report = options.delete(:report)
67
141
 
68
- results = cli.cache_push(*dir, silent: false, **options)
142
+ results = cli.cache_push(dir, **options)
69
143
 
70
- if report && !report.empty?
71
- File.open(report, 'w') do |io|
72
- JSON.dump(results, io)
73
- end
144
+ if report && !report.empty?
145
+ File.open(report, 'w') do |io|
146
+ JSON.dump(
147
+ {
148
+ 'cache_push_report' => {
149
+ 'packages' => results
150
+ }
151
+ }, io
152
+ )
74
153
  end
75
154
  end
76
155
  end
77
156
 
78
- desc "build-report PATH",
79
- "Create a tarball containing all the information about this "\
80
- "build, such as cache information (from cache-pull), Autoproj\'s "\
81
- "build report and installation manifest, and the package\'s logfiles"
82
- def build_report(path)
157
+ desc 'build-cache-cleanup CACHE_DIR',
158
+ 'Remove the oldest entries in the cache until it is under a given size limit'
159
+ option :max_size, type: 'numeric', default: 10,
160
+ desc: 'approximate target size limit (in GB, defaults to 10)'
161
+ def build_cache_cleanup(dir)
162
+ dir = File.expand_path(dir)
163
+
164
+ require 'autoproj/cli/ci'
165
+ cli = CI.new
166
+
167
+ _, options = cli.validate_options(dir, self.options)
168
+ cli.cleanup_build_cache(dir, options[:max_size] * 1_000_000_000)
169
+ end
170
+
171
+ desc 'build-report PATH',
172
+ 'create a directory containing all the information about this '\
173
+ 'build, such as cache information (from cache-pull), Autoproj\'s '\
174
+ 'build report and installation manifest, and the package\'s logfiles'
175
+ def create_report(path)
83
176
  path = File.expand_path(path)
84
177
 
85
178
  require 'autoproj/cli/ci'
86
- Autoproj.report(silent: true) do
87
- cli = CI.new
88
- args, options = cli.validate_options(path, self.options)
89
- cli.build_report(*args, **options)
90
- end
179
+ cli = CI.new
180
+ args, options = cli.validate_options(path, self.options)
181
+ cli.create_report(*args, **options)
91
182
  end
92
183
  end
93
184
  end
94
185
  end
95
-
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'thor'
5
+
6
+ require 'autoproj'
7
+ require 'autoproj/ci/rebuild'
8
+
9
+ module Autoproj
10
+ module CLI
11
+ # CI-related commands that can be executed without an Autoproj installation
12
+ class StandaloneCI < Thor
13
+ desc 'rebuild-root CONFIG_DIR CACHE_ROOT OUTPUT',
14
+ 'creates a compressed tarball containing the build products of a '\
15
+ 'finished build, pulled from build cache'
16
+ option 'workspace',
17
+ desc: 'if given, setup a minimal workspace-like structure to '\
18
+ 'support execution in the given path',
19
+ type: :string, default: nil
20
+ def rebuild_root(config_dir, cache_root, output)
21
+ dir = Dir.mktmpdir
22
+ Autoproj::CI::Rebuild.prepare_synthetic_buildroot(
23
+ File.join(config_dir, 'installation-manifest'),
24
+ File.join(config_dir, 'versions.yml'),
25
+ cache_root,
26
+ dir
27
+ )
28
+
29
+ if options[:workspace]
30
+ prepare_workspace(config_dir, dir, options[:workspace])
31
+ end
32
+
33
+ output = File.expand_path(output)
34
+ unless system('tar', 'caf', output, '--owner=root', '--group=root',
35
+ '.', chdir: dir)
36
+ raise "failed to create #{output}"
37
+ end
38
+ ensure
39
+ FileUtils.rm_rf(dir) if dir && File.directory?(dir)
40
+ end
41
+
42
+ desc 'dpkg-filter-status STATUS_PATH [RULES]',
43
+ 'outputs the list of APT packages to install based on a dpkg status '\
44
+ 'file and a set of inclusion/exclusion rules of the form "+ pattern" '\
45
+ 'and "- pattern"'
46
+ option :orig, desc: 'a status file whose installed packages are removed',
47
+ type: :string
48
+ option :file, desc: 'read the rules from a file',
49
+ type: :string
50
+ def dpkg_filter_status(status_path, *rules)
51
+ rules += File.readlines(options[:file]).map(&:strip) if options[:file]
52
+ rules = rules.map do |line|
53
+ next if line.empty? || line.start_with?('#')
54
+
55
+ parse_rule(line)
56
+ end
57
+
58
+ packages = Autoproj::CI::Rebuild.dpkg_create_package_install(
59
+ status_path, rules, orig: options[:orig]
60
+ )
61
+ puts packages.join("\n")
62
+ end
63
+
64
+ no_commands do # rubocop:disable Metrics/BlockLength
65
+ def parse_rule(line)
66
+ unless (m = /^([+-])\s+(.*)/.match(line))
67
+ raise ArgumentError, "invalid rule line '#{line}'"
68
+ end
69
+
70
+ mode = (m[1] == '+')
71
+ begin
72
+ [mode, Regexp.new(m[2])]
73
+ rescue RegexpError => e
74
+ raise ArgumentError, "invalid regexp in '#{line}': #{e}"
75
+ end
76
+ end
77
+
78
+ def prepare_workspace(config_dir, output_dir, workspace_dir)
79
+ FileUtils.mkdir_p File.join(output_dir, workspace_dir, '.autoproj')
80
+
81
+ if File.file?(envsh = File.join(config_dir, 'env.sh'))
82
+ filter_envsh(envsh, output_dir, workspace_dir)
83
+ generate_autoproj_stub(output_dir, workspace_dir)
84
+ end
85
+
86
+ if File.file?(file = File.join(config_dir, 'source.yml'))
87
+ FileUtils.cp(
88
+ file, File.join(output_dir, workspace_dir, '.autoproj')
89
+ )
90
+ end
91
+
92
+ if File.file?(file = File.join(config_dir, 'installation-manifest'))
93
+ FileUtils.cp(
94
+ file,
95
+ File.join(output_dir, workspace_dir, 'installation-manifest')
96
+ )
97
+ end
98
+ end
99
+
100
+ AUTOPROJ_STUB_PATH = File.join(__dir__, 'autoproj-stub.sh.erb').freeze
101
+
102
+ def filter_envsh(source_path, output_dir, workspace_dir)
103
+ filtered = File.readlines(source_path)
104
+ .find_all { |l| !/^(source|\.)/.match?(l) }
105
+ File.open(File.join(output_dir, workspace_dir, 'env.sh'), 'w') do |io|
106
+ io.write(filtered.join)
107
+ end
108
+ end
109
+
110
+ def generate_autoproj_stub(output_dir, workspace_dir)
111
+ dot_autoproj = File.join(output_dir, workspace_dir, '.autoproj')
112
+ FileUtils.mkdir File.join(dot_autoproj, 'bin')
113
+ autoproj_path = File.join(dot_autoproj, 'bin', 'autoproj')
114
+
115
+ erb = ERB.new(File.read(AUTOPROJ_STUB_PATH))
116
+ erb.location = [AUTOPROJ_STUB_PATH, 0]
117
+ File.open(autoproj_path, 'w') do |io|
118
+ io.write erb.result_with_hash(workspace_dir: "/#{workspace_dir}")
119
+ end
120
+ FileUtils.chmod 0o755, autoproj_path
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,4 @@
1
+ ---
2
+ inherit_gem:
3
+ rubocop-rock: defaults.yml
4
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: autoproj-ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sylvain Joyeux
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-30 00:00:00.000000000 Z
11
+ date: 2020-06-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: autoproj
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: timecop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: minitest
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -83,7 +97,8 @@ dependencies:
83
97
  description:
84
98
  email:
85
99
  - sylvain.joyeux@13robotics.com
86
- executables: []
100
+ executables:
101
+ - autoproj-ci
87
102
  extensions: []
88
103
  extra_rdoc_files: []
89
104
  files:
@@ -96,12 +111,15 @@ files:
96
111
  - README.md
97
112
  - Rakefile
98
113
  - autoproj-ci.gemspec
99
- - bin/console
100
- - bin/setup
114
+ - bin/autoproj-ci
101
115
  - lib/autoproj-ci.rb
116
+ - lib/autoproj/ci/rebuild.rb
102
117
  - lib/autoproj/ci/version.rb
118
+ - lib/autoproj/cli/autoproj-stub.sh.erb
103
119
  - lib/autoproj/cli/ci.rb
104
120
  - lib/autoproj/cli/main_ci.rb
121
+ - lib/autoproj/cli/standalone_ci.rb
122
+ - rubocop.yml
105
123
  homepage: https://github.com/rock-core/autoproj-ci
106
124
  licenses:
107
125
  - MIT
@@ -124,8 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
124
142
  - !ruby/object:Gem::Version
125
143
  version: '0'
126
144
  requirements: []
127
- rubyforge_project:
128
- rubygems_version: 2.7.6.2
145
+ rubygems_version: 3.1.2
129
146
  signing_key:
130
147
  specification_version: 4
131
148
  summary: plugin that provide subcommand useful in CI environments
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "autoproj/ci"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here