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 +4 -4
- data/Gemfile +1 -0
- data/autoproj-ci.gemspec +3 -2
- data/bin/autoproj-ci +5 -0
- data/lib/autoproj-ci.rb +11 -4
- data/lib/autoproj/ci/rebuild.rb +74 -0
- data/lib/autoproj/ci/version.rb +3 -1
- data/lib/autoproj/cli/autoproj-stub.sh.erb +12 -0
- data/lib/autoproj/cli/ci.rb +243 -69
- data/lib/autoproj/cli/main_ci.rb +137 -47
- data/lib/autoproj/cli/standalone_ci.rb +125 -0
- data/rubocop.yml +4 -0
- metadata +25 -8
- data/bin/console +0 -14
- data/bin/setup +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7607b476bb61d53de2e2992f1d8ca7d91c2164090d9e22fe136aedce9004c735
|
4
|
+
data.tar.gz: 8f77955a24f0bc516b9251c1dd7cbad4326a22d5d1b42a50010b4975696f3adc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 62be513aba6072efdbe008fcedea7e95044298c4b46d53d624db42e32a8be121272e10e322ed2b3ab31c41cf8a1e05291e1ca51ae613eeec98cbb44d067954a7
|
7
|
+
data.tar.gz: 2b24d8860e43bb9de8330fd5989ff8920892c320c894369efc46ed4941dad9e5fb52e3cb36311f698b013bb8337dd07ef0dac8bd27fbc11e4ebecc2602a28cb4
|
data/Gemfile
CHANGED
data/autoproj-ci.gemspec
CHANGED
@@ -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 = "
|
29
|
-
spec.executables = spec.files.grep(%r{^
|
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
|
data/bin/autoproj-ci
ADDED
data/lib/autoproj-ci.rb
CHANGED
@@ -1,6 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'autoproj/cli/main_ci'
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
data/lib/autoproj/ci/version.rb
CHANGED
data/lib/autoproj/cli/ci.rb
CHANGED
@@ -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
|
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
|
44
|
+
memo = {}
|
26
45
|
results = packages.each_with_object({}) do |pkg, h|
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|
-
|
37
|
-
|
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
|
77
|
+
def cache_push(dir)
|
45
78
|
packages = resolve_packages
|
79
|
+
metadata = consolidated_report['packages']
|
46
80
|
|
47
|
-
|
48
|
-
|
49
|
-
memo = Hash.new
|
81
|
+
memo = {}
|
50
82
|
results = packages.each_with_object({}) do |pkg, h|
|
51
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
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
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
112
|
-
|
113
|
-
|
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
|
-
|
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
|
-
|
123
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
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.
|
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
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
334
|
+
false
|
335
|
+
end
|
168
336
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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' =>
|
349
|
+
{ 'packages' => result }
|
175
350
|
end
|
176
351
|
end
|
177
352
|
end
|
178
353
|
end
|
179
|
-
|
data/lib/autoproj/cli/main_ci.rb
CHANGED
@@ -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 <
|
7
|
-
desc 'build [ARGS]',
|
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
|
-
|
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
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
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
|
-
|
104
|
+
cli = CI.new
|
105
|
+
_, options = cli.validate_options(dir, self.options)
|
106
|
+
report = options.delete(:report)
|
42
107
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
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
|
-
|
63
|
-
cli = CI.new
|
137
|
+
cli = CI.new
|
64
138
|
|
65
|
-
|
66
|
-
|
139
|
+
_, options = cli.validate_options(dir, self.options)
|
140
|
+
report = options.delete(:report)
|
67
141
|
|
68
|
-
|
142
|
+
results = cli.cache_push(dir, **options)
|
69
143
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
def
|
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
|
-
|
87
|
-
|
88
|
-
|
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
|
data/rubocop.yml
ADDED
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.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sylvain Joyeux
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
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/
|
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
|
-
|
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
|
data/bin/console
DELETED
@@ -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__)
|