summaryrefslogtreecommitdiffstats
path: root/meta/classes-global/retain.bbclass
diff options
context:
space:
mode:
Diffstat (limited to 'meta/classes-global/retain.bbclass')
-rw-r--r--meta/classes-global/retain.bbclass182
1 files changed, 182 insertions, 0 deletions
diff --git a/meta/classes-global/retain.bbclass b/meta/classes-global/retain.bbclass
new file mode 100644
index 0000000000..46e8c256cf
--- /dev/null
+++ b/meta/classes-global/retain.bbclass
@@ -0,0 +1,182 @@
1# Creates a tarball of the work directory for a recipe when one of its
2# tasks fails, or any other nominated directories.
3# Useful in cases where the environment in which builds are run is
4# ephemeral or otherwise inaccessible for examination during
5# debugging.
6#
7# To enable, simply add the following to your configuration:
8#
9# INHERIT += "retain"
10#
11# You can specify the recipe-specific directories to save upon failure
12# or always (space-separated) e.g.:
13#
14# RETAIN_DIRS_FAILURE = "${WORKDIR};prefix=workdir" # default
15# RETAIN_DIRS_ALWAYS = "${T}"
16#
17# Naturally you can use overrides to limit it to a specific recipe:
18# RETAIN_DIRS_ALWAYS:pn-somerecipe = "${T}"
19#
20# You can also specify global (non-recipe-specific) directories to save:
21#
22# RETAIN_DIRS_GLOBAL_FAILURE = "${LOG_DIR}"
23# RETAIN_DIRS_GLOBAL_ALWAYS = "${BUILDSTATS_BASE}"
24#
25# If you wish to use a different tarball name prefix than the default of
26# the directory name, you can do so by specifying a ;prefix= followed by
27# the desired prefix (no spaces) in any of the RETAIN_DIRS_* variables.
28# e.g. to always save the log files with a "recipelogs" as the prefix for
29# the tarball of ${T} you would do this:
30#
31# RETAIN_DIRS_ALWAYS = "${T};prefix=recipelogs"
32#
33# Notes:
34# * For this to be useful you also need corresponding logic in your build
35# orchestration tool to pick up any files written out to RETAIN_OUTDIR
36# (with the other assumption being that no files are present there at
37# the start of the build, since there is no logic to purge old files).
38# * Work directories can be quite large, so saving them can take some time
39# and of course space.
40# * Tarball creation is deferred to the end of the build, thus you will
41# get the state at the end, not immediately upon failure.
42# * Extra directories must naturally be populated at the time the retain
43# class goes to save them (build completion); to try ensure this for
44# things that are also saved on build completion (e.g. buildstats), put
45# the INHERIT += "retain" after the INHERIT += lines for the class that
46# is writing out the data that you wish to save.
47# * The tarballs have the tarball name as a top-level directory so that
48# multiple tarballs can be extracted side-by-side easily.
49#
50# Copyright (c) 2020, 2024 Microsoft Corporation
51#
52# SPDX-License-Identifier: GPL-2.0-only
53#
54
55RETAIN_OUTDIR ?= "${TMPDIR}/retained"
56RETAIN_DIRS_FAILURE ?= "${WORKDIR};prefix=workdir"
57RETAIN_DIRS_ALWAYS ?= ""
58RETAIN_DIRS_GLOBAL_FAILURE ?= ""
59RETAIN_DIRS_GLOBAL_ALWAYS ?= ""
60RETAIN_TARBALL_SUFFIX ?= "${DATETIME}.tar.gz"
61RETAIN_ENABLED ?= "1"
62
63
64def retain_retain_dir(desc, tarprefix, path, tarbasepath, d):
65 import datetime
66
67 outdir = d.getVar('RETAIN_OUTDIR')
68 bb.utils.mkdirhier(outdir)
69 suffix = d.getVar('RETAIN_TARBALL_SUFFIX')
70 tarname = '%s_%s' % (tarprefix, suffix)
71 tarfp = os.path.join(outdir, '%s' % tarname)
72 tardir = os.path.relpath(path, tarbasepath)
73 cmdargs = ['tar', 'cfa', tarfp]
74 # Prefix paths within the tarball with the tarball name so that
75 # multiple tarballs can be extracted side-by-side
76 tarname_noext = os.path.splitext(tarname)[0]
77 if tarname_noext.endswith('.tar'):
78 tarname_noext = tarname_noext[:-4]
79 cmdargs += ['--transform', 's:^:%s/:' % tarname_noext]
80 cmdargs += [tardir]
81 try:
82 bb.process.run(cmdargs, cwd=tarbasepath)
83 except bb.process.ExecutionError as e:
84 # It is possible for other tasks to be writing to the workdir
85 # while we are tarring it up, in which case tar will return 1,
86 # but we don't care in this situation (tar returns 2 for other
87 # errors so we we will see those)
88 if e.exitcode != 1:
89 bb.warn('retain: error saving %s: %s' % (desc, str(e)))
90
91
92addhandler retain_task_handler
93retain_task_handler[eventmask] = "bb.build.TaskFailed bb.build.TaskSucceeded"
94
95addhandler retain_build_handler
96retain_build_handler[eventmask] = "bb.event.BuildStarted bb.event.BuildCompleted"
97
98python retain_task_handler() {
99 if d.getVar('RETAIN_ENABLED') != '1':
100 return
101
102 dirs = d.getVar('RETAIN_DIRS_ALWAYS')
103 if isinstance(e, bb.build.TaskFailed):
104 dirs += ' ' + d.getVar('RETAIN_DIRS_FAILURE')
105
106 dirs = dirs.strip().split()
107 if dirs:
108 outdir = d.getVar('RETAIN_OUTDIR')
109 bb.utils.mkdirhier(outdir)
110 dirlist_file = os.path.join(outdir, 'retain_dirs.list')
111 pn = d.getVar('PN')
112 taskname = d.getVar('BB_CURRENTTASK')
113 with open(dirlist_file, 'a') as f:
114 for entry in dirs:
115 f.write('%s %s %s\n' % (pn, taskname, entry))
116}
117
118python retain_build_handler() {
119 outdir = d.getVar('RETAIN_OUTDIR')
120 dirlist_file = os.path.join(outdir, 'retain_dirs.list')
121
122 if isinstance(e, bb.event.BuildStarted):
123 if os.path.exists(dirlist_file):
124 os.remove(dirlist_file)
125 return
126
127 if d.getVar('RETAIN_ENABLED') != '1':
128 return
129
130 savedirs = {}
131 try:
132 with open(dirlist_file, 'r') as f:
133 for line in f:
134 pn, _, path = line.rstrip().split()
135 if not path in savedirs:
136 savedirs[path] = pn
137 os.remove(dirlist_file)
138 except FileNotFoundError:
139 pass
140
141 if e.getFailures():
142 for path in (d.getVar('RETAIN_DIRS_GLOBAL_FAILURE') or '').strip().split():
143 savedirs[path] = ''
144
145 for path in (d.getVar('RETAIN_DIRS_GLOBAL_ALWAYS') or '').strip().split():
146 savedirs[path] = ''
147
148 if savedirs:
149 bb.plain('NOTE: retain: retaining build output...')
150 count = 0
151 for path, pn in savedirs.items():
152 prefix = None
153 if ';' in path:
154 pathsplit = path.split(';')
155 path = pathsplit[0]
156 for param in pathsplit[1:]:
157 if '=' in param:
158 name, value = param.split('=', 1)
159 if name == 'prefix':
160 prefix = value
161 else:
162 bb.error('retain: invalid parameter "%s" in RETAIN_* variable value' % param)
163 return
164 else:
165 bb.error('retain: parameter "%s" missing value in RETAIN_* variable value' % param)
166 return
167 if prefix:
168 itemname = prefix
169 else:
170 itemname = os.path.basename(path)
171 if pn:
172 # Always add the recipe name in front
173 itemname = pn + '_' + itemname
174 if os.path.exists(path):
175 retain_retain_dir(itemname, itemname, path, os.path.dirname(path), d)
176 count += 1
177 else:
178 bb.warn('retain: path %s does not currently exist' % path)
179 if count:
180 item = 'archive' if count == 1 else 'archives'
181 bb.plain('NOTE: retain: saved %d %s to %s' % (count, item, outdir))
182}