autoproj-ci 0.2.0 → 0.3.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.
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