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 +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__)
|