summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--meta-oe/classes/check-version-mismatch.bbclass399
-rw-r--r--meta-oe/conf/version-check.conf22
2 files changed, 421 insertions, 0 deletions
diff --git a/meta-oe/classes/check-version-mismatch.bbclass b/meta-oe/classes/check-version-mismatch.bbclass
new file mode 100644
index 0000000000..7b46151b03
--- /dev/null
+++ b/meta-oe/classes/check-version-mismatch.bbclass
@@ -0,0 +1,399 @@
1inherit qemu
2
3ENABLE_VERSION_MISMATCH_CHECK ?= "${@'1' if bb.utils.contains('MACHINE_FEATURES', 'qemu-usermode', True, False, d) else '0'}"
4DEBUG_VERSION_MISMATCH_CHECK ?= "1"
5CHECK_VERSION_PV ?= ""
6
7DEPENDS:append:class-target = "${@' qemu-native' if bb.utils.to_boolean(d.getVar('ENABLE_VERSION_MISMATCH_CHECK')) else ''}"
8
9QEMU_EXEC ?= "${@qemu_wrapper_cmdline(d, '${STAGING_DIR_HOST}', ['${STAGING_DIR_HOST}${libdir}','${STAGING_DIR_HOST}${base_libdir}', '${PKGD}${libdir}', '${PKGD}${base_libdir}'])}"
10
11python do_package_check_version_mismatch() {
12 import re
13 import subprocess
14 import shutil
15 import signal
16
17 classes_skip = ["nopackage", "image", "native", "cross", "crosssdk", "cross-canadian"]
18 for cs in classes_skip:
19 if bb.data.inherits_class(cs, d):
20 bb.note(f"Skip do_package_check_version_mismatch as {cs} is inherited.")
21 return
22
23 if not bb.utils.to_boolean(d.getVar('ENABLE_VERSION_MISMATCH_CHECK')):
24 bb.note("Skip do_package_check_version_mismatch as ENABLE_VERSION_MISMATCH_CHECK is disabled.")
25 return
26
27 __regexp_version_broad_match__ = re.compile(r"(?:\s|^|-|_|/|=| go|\()" +
28 r"(?P<version>v?[0-9][0-9.][0-9+.\-_~\(\)]*?|UNKNOWN)" +
29 r"(?:[+\-]release.*|[+\-]stable.*|)" +
30 r"(?P<extra>[+\-]unknown|[+\-]dirty|[+\-]rc?\d{1,3}|\+cargo-[0-9.]+|" +
31 r"[a-z]|-?[pP][0-9]{1,3}|-?beta[^\s]*|-?alpha[^\s]*|)" +
32 r"(?P<extra2>[+\-]dev|[+\-]devel|)" +
33 r"(?:,|:|\.|\)|-[0-9a-g]{6,42}|)" +
34 r"(?=\s|$)"
35 )
36 __regexp_exclude_year__ = re.compile(r"^(19|20)[0-9]{2}$")
37 __regexp_single_number_ending_with_dot__ = re.compile(r"^\d\.$")
38
39 def is_shared_library(filepath):
40 return re.match(r'.*\.so(\.\d+)*$', filepath) is not None
41
42 def get_possible_versions(output_contents, full_cmd=None, max_lines=None):
43 #
44 # Algorithm:
45 # 1. Check version line by line.
46 # 2. Skip some lines which we know that do not contain version information, e.g., License, Copyright.
47 # 3. Do broad match, finding all possible versions.
48 # 4. If there's a version found by any match, do exclude match (e.g., exclude years)
49 # 5. If there's a valid version, do stripping and converting and then add to possible_versions.
50 # 6. Return possible_versions
51 #
52 possible_versions = []
53 content_lines = output_contents.split("\n")
54 if max_lines:
55 content_lines = content_lines[0:max_lines]
56 if full_cmd:
57 base_cmd = os.path.basename(full_cmd)
58 __regex_help_format__ = re.compile(r"-[^\s].*")
59 for line in content_lines:
60 line = line.strip()
61 # skip help lines
62 if __regex_help_format__.match(line):
63 continue
64 # avoid command itself affecting output
65 if full_cmd:
66 if line.startswith(base_cmd):
67 line = line[len(base_cmd):]
68 elif line.startswith(full_cmd):
69 line = line[len(full_cmd):]
70 # skip specific lines
71 skip_keywords_start = ["copyright", "license", "compiled", "build", "built"]
72 skip_line = False
73 for sks in skip_keywords_start:
74 if line.lower().startswith(sks):
75 skip_line = True
76 break
77 if skip_line:
78 continue
79
80 # try broad match
81 for match in __regexp_version_broad_match__.finditer(line):
82 version = match.group("version")
83 #print(f"version = {version}")
84 # do exclude match
85 exclude_match = __regexp_exclude_year__.match(version)
86 if exclude_match:
87 continue
88 exclude_match = __regexp_single_number_ending_with_dot__.match(version)
89 if exclude_match:
90 continue
91 # do some stripping and converting
92 if version.startswith("("):
93 version = version[1:-1]
94 if version.startswith("v"):
95 version = version[1:]
96 if version.endswith(")") and "(" not in version:
97 version = version[:-1]
98 # handle extra version info
99 version = version + match.group("extra") + match.group("extra2")
100 possible_versions.append(version)
101 return possible_versions
102
103 def is_version_mismatch(rvs, pv):
104 got_match = False
105 if pv.startswith("git"):
106 return False
107 if "-pre" in pv:
108 pv = pv.split("-pre")[0]
109 if pv.startswith("v"):
110 pv = pv[1:]
111 for rv in rvs:
112 if rv == pv:
113 got_match = True
114 break
115 pv = pv.split("+git")[0]
116 # handle % character in pv which means matching any chars
117 if '%' in pv:
118 escaped_pv = re.escape(pv)
119 regex_pattern = escaped_pv.replace('%', '.*')
120 regex_pattern = f'^{regex_pattern}$'
121 if re.fullmatch(regex_pattern, rv):
122 got_match = True
123 break
124 else:
125 continue
126 # handle cases such as 2.36.0-r0 v.s. 2.36.0
127 if "-r" in rv:
128 rv = rv.split("-r")[0]
129 chars_to_replace = ["-", "+", "_", "~"]
130 # convert to use "." as the version seperator
131 for cr in chars_to_replace:
132 rv = rv.replace(cr, ".")
133 pv = pv.replace(cr, ".")
134 if rv == pv:
135 got_match = True
136 break
137 # handle case such as 5.2.37(1) v.s. 5.2.37
138 if "(" in rv:
139 rv = rv.split("(")[0]
140 if rv == pv:
141 got_match = True
142 break
143 # handle case such as 4.4.3p1
144 if "p" in pv and "p" in rv.lower():
145 pv = pv.lower().replace(".p", "p")
146 rv = rv.lower().replace(".p", "p")
147 if pv == rv:
148 got_match = True
149 break
150 # handle cases such as 6.00 v.s. 6.0
151 if rv.startswith(pv):
152 if rv == pv + "0" or rv == pv + ".0":
153 got_match = True
154 break
155 elif pv.startswith(rv):
156 if pv == rv + "0" or pv == rv + ".0":
157 got_match = True
158 break
159 # handle cases such as 21306 v.s. 2.13.6
160 if "." in pv and not "." in rv:
161 pv_components = pv.split(".")
162 if rv.startswith(pv_components[0]):
163 pv_num = 0
164 for i in range(0, len(pv_components)):
165 pv_num = pv_num * 100 + int(pv_components[i])
166 if pv_num == int(rv):
167 got_match = True
168 break
169 if got_match:
170 return False
171 else:
172 return True
173
174 # helper function to get PKGV, useful for recipes such as perf
175 def get_pkgv(pn):
176 pkgdestwork = d.getVar("PKGDESTWORK")
177 recipe_data_fn = pkgdestwork + "/" + pn
178 pn_data = oe.packagedata.read_pkgdatafile(recipe_data_fn)
179 if not "PACKAGES" in pn_data:
180 return d.getVar("PV")
181 packages = pn_data["PACKAGES"].split()
182 for pkg in packages:
183 pkg_fn = pkgdestwork + "/runtime/" + pkg
184 pkg_data = oe.packagedata.read_pkgdatafile(pkg_fn)
185 if "PKGV" in pkg_data:
186 return pkg_data["PKGV"]
187
188 #
189 # traverse PKGD, find executables and run them to get runtime version information and compare it with recipe version information
190 #
191 enable_debug = bb.utils.to_boolean(d.getVar("DEBUG_VERSION_MISMATCH_CHECK"))
192 pkgd = d.getVar("PKGD")
193 pn = d.getVar("PN")
194 pv = d.getVar("CHECK_VERSION_PV")
195 if not pv:
196 pv = get_pkgv(pn)
197 qemu_exec = d.getVar("QEMU_EXEC").strip()
198 executables = []
199 possible_versions_all = []
200 data_lines = []
201
202 if enable_debug:
203 debug_directory = d.getVar("TMPDIR") + "/check-version-mismatch"
204 debug_data_file = debug_directory + "/" + pn
205 os.makedirs(debug_directory, exist_ok=True)
206 data_lines.append("pv: %s\n" % pv)
207
208 got_quick_match_result = False
209 # handle python3-xxx recipes quickly
210 __regex_python_module_version__ = re.compile(r"(?:^|.*:)Version: (?P<version>.*)$")
211 if "python3-" in pn:
212 version_check_cmd = "find %s -name 'METADATA' | xargs grep '^Version: '" % pkgd
213 try:
214 output = subprocess.check_output(version_check_cmd, shell=True).decode("utf-8")
215 data_lines.append("version_check_cmd: %s\n" % version_check_cmd)
216 data_lines.append("output:\n'''\n%s'''\n" % output)
217 possible_versions = []
218 for line in output.split("\n"):
219 match = __regex_python_module_version__.match(line)
220 if match:
221 possible_versions.append(match.group("version"))
222 possible_versions = sorted(set(possible_versions))
223 data_lines.append("possible versions: %s\n" % possible_versions)
224 if is_version_mismatch(possible_versions, pv):
225 data_lines.append("FINAL RESULT: MISMATCH (%s v.s. %s)\n\n" % (possible_versions, pv))
226 bb.warn("Possible runtime versions %s do not match recipe version %s" % (possible_versions, pv))
227 else:
228 data_lines.append("FINAL RESULT: MATCH (%s v.s. %s)\n\n" % (possible_versions, pv))
229 got_quick_match_result = True
230 except:
231 data_lines.append("version_check_cmd: %s\n" % version_check_cmd)
232 data_lines.append("result: RUN_FAILED\n\n")
233 if got_quick_match_result:
234 if enable_debug:
235 with open(debug_data_file, "w") as f:
236 f.writelines(data_lines)
237 return
238
239 # handle .pc files
240 version_check_cmd = "find %s -name '*.pc' | xargs grep -i version" % pkgd
241 try:
242 output = subprocess.check_output(version_check_cmd, shell=True).decode("utf-8")
243 data_lines.append("version_check_cmd: %s\n" % version_check_cmd)
244 data_lines.append("output:\n'''\n%s'''\n" % output)
245 possible_versions = get_possible_versions(output)
246 possible_versions = sorted(set(possible_versions))
247 data_lines.append("possible versions: %s\n" % possible_versions)
248 if is_version_mismatch(possible_versions, pv):
249 if pn.startswith("lib"):
250 data_lines.append("FINAL RESULT: MISMATCH (%s v.s. %s)\n\n" % (possible_versions, pv))
251 bb.warn("Possible runtime versions %s do not match recipe version %s" % (possible_versions, pv))
252 got_quick_match_result = True
253 else:
254 data_lines.append("result: MISMATCH (%s v.s. %s)\n\n" % (possible_versions, pv))
255 else:
256 data_lines.append("FINAL RESULT: MATCH (%s v.s. %s)\n\n" % (possible_versions, pv))
257 got_quick_match_result = True
258 except:
259 data_lines.append("version_check_cmd: %s\n" % version_check_cmd)
260 data_lines.append("result: RUN_FAILED\n\n")
261 if got_quick_match_result:
262 if enable_debug:
263 with open(debug_data_file, "w") as f:
264 f.writelines(data_lines)
265 return
266
267 skipped_directories = [".debug", "ptest", "installed-tests", "tests", "test", "__pycache__", "testcases"]
268 pkgd_libdir = pkgd + d.getVar("libdir")
269 pkgd_base_libdir = pkgd + d.getVar("base_libdir")
270 extra_exec_libdirs = []
271 for root, dirs, files in os.walk(pkgd):
272 for dname in dirs:
273 fdir = os.path.join(root, dname)
274 if os.path.isdir(fdir) and fdir != pkgd_libdir and fdir != pkgd_base_libdir:
275 if fdir.startswith(pkgd_libdir) or fdir.startswith(pkgd_base_libdir):
276 for sd in skipped_directories:
277 if fdir.endswith("/" + sd) or ("/" + sd + "/") in fdir:
278 break
279 else:
280 extra_exec_libdirs.append(fdir)
281 for fname in files:
282 fpath = os.path.join(root, fname)
283 if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
284 for sd in skipped_directories:
285 if ("/" + sd + "/") in fpath:
286 break
287 else:
288 if is_shared_library(fpath):
289 # we don't check shared libraries
290 continue
291 else:
292 executables.append(fpath)
293 if enable_debug:
294 data_lines.append("executables: %s\n" % executables)
295
296 found_match = False
297 some_cmd_succeed = False
298 if not executables:
299 bb.debug(1, "No executable found for %s" % pn)
300 data_lines.append("FINAL RESULT: NO_EXECUTABLE_FOUND\n\n")
301 else:
302 # first we extend qemu_exec to include library path if needed
303 if extra_exec_libdirs:
304 qemu_exec += ":" + ":".join(extra_exec_libdirs)
305 for fexec in executables:
306 for version_option in ["--version", "-V", "-v", "--help"]:
307 version_check_cmd_full = "%s %s %s" % (qemu_exec, fexec, version_option)
308 version_check_cmd = version_check_cmd_full
309 #version_check_cmd = "%s %s" % (os.path.relpath(fexec, pkgd), version_option)
310
311 try:
312 cwd_temp = d.getVar("TMPDIR") + "/check-version-mismatch/cwd-temp/" + pn
313 os.makedirs(cwd_temp, exist_ok=True)
314 # avoid pseudo to manage any file we create
315 sp_env = os.environ.copy()
316 sp_env["PSEUDO_UNLOAD"] = "1"
317 output = subprocess.check_output(version_check_cmd_full,
318 shell=True,
319 stderr=subprocess.STDOUT,
320 cwd=cwd_temp,
321 timeout=10,
322 env=sp_env).decode("utf-8")
323 some_cmd_succeed = True
324 data_lines.append("version_check_cmd: %s\n" % version_check_cmd)
325 data_lines.append("output:\n'''\n%s'''\n" % output)
326 if version_option == "--help":
327 max_lines = 5
328 else:
329 max_lines = None
330 possible_versions = get_possible_versions(output, full_cmd=fexec, max_lines=max_lines)
331 if "." in pv:
332 possible_versions = [item for item in possible_versions if "." in item or item == "UNKNOWN"]
333 data_lines.append("possible versions: %s\n" % possible_versions)
334 if not possible_versions:
335 data_lines.append("result: NO_RUNTIME_VERSION_FOUND\n\n")
336 continue
337 possible_versions_all.extend(possible_versions)
338 possible_versions_all = sorted(set(possible_versions_all))
339 if is_version_mismatch(possible_versions, pv):
340 data_lines.append("result: MISMATCH (%s v.s. %s)\n\n" % (possible_versions, pv))
341 else:
342 found_match = True
343 data_lines.append("result: MATCH (%s v.s. %s)\n\n" % (possible_versions, pv))
344 break
345 except:
346 data_lines.append("version_check_cmd: %s\n" % version_check_cmd)
347 data_lines.append("result: RUN_FAILED\n\n")
348 finally:
349 shutil.rmtree(cwd_temp)
350 if found_match:
351 break
352 if executables:
353 if found_match:
354 data_lines.append("FINAL RESULT: MATCH (%s v.s. %s)\n" % (possible_versions_all, pv))
355 elif len(possible_versions_all) == 0:
356 if some_cmd_succeed:
357 bb.debug(1, "No valid runtime version found")
358 data_lines.append("FINAL RESULT: NO_VALID_RUNTIME_VERSION_FOUND\n")
359 else:
360 bb.debug(1, "All version check command failed")
361 data_lines.append("FINAL RESULT: RUN_FAILED\n")
362 else:
363 bb.warn("Possible runtime versions %s do not match recipe version %s" % (possible_versions_all, pv))
364 data_lines.append("FINAL RESULT: MISMATCH (%s v.s. %s)\n" % (possible_versions_all, pv))
365
366 if enable_debug:
367 with open(debug_data_file, "w") as f:
368 f.writelines(data_lines)
369
370 # clean up stale processes
371 process_name_common_prefix = "%s %s" % (' '.join(qemu_exec.split()[1:]), pkgd)
372 find_stale_process_cmd = "ps -e -o pid,args | grep -v grep | grep -F '%s'" % process_name_common_prefix
373 try:
374 stale_process_output = subprocess.check_output(find_stale_process_cmd, shell=True).decode("utf-8")
375 stale_process_pids = []
376 for line in stale_process_output.split("\n"):
377 line = line.strip()
378 if not line:
379 continue
380 pid = line.split()[0]
381 stale_process_pids.append(pid)
382 for pid in stale_process_pids:
383 os.kill(int(pid), signal.SIGKILL)
384 except Exception as e:
385 bb.debug(1, "No stale process")
386}
387
388addtask do_package_check_version_mismatch after do_package before do_build
389
390do_build[rdeptask] += "do_package_check_version_mismatch"
391do_rootfs[recrdeptask] += "do_package_check_version_mismatch"
392
393SSTATETASKS += "do_package_check_version_mismatch"
394do_package_check_version_mismatch[sstate-inputdirs] = ""
395do_package_check_version_mismatch[sstate-outputdirs] = ""
396python do_package_check_version_mismatch_setscene () {
397 sstate_setscene(d)
398}
399addtask do_package_check_version_mismatch_setscene
diff --git a/meta-oe/conf/version-check.conf b/meta-oe/conf/version-check.conf
new file mode 100644
index 0000000000..c41df0d496
--- /dev/null
+++ b/meta-oe/conf/version-check.conf
@@ -0,0 +1,22 @@
1INHERIT += "check-version-mismatch"
2# we need ps command to clean stale processes
3HOSTTOOLS += "ps"
4
5# Special cases that need to be handled.
6# % has the same meaning as in bbappend files, that is, match any chars.
7
8# oe-core
9CHECK_VERSION_PV:pn-rust-llvm = "${LLVM_RELEASE}"
10CHECK_VERSION_PV:pn-igt-gpu-tools = "${PV}-${PV}"
11CHECK_VERSION_PV:pn-vim = "${@'.'.join(d.getVar('PV').split('.')[:-1])}"
12CHECK_VERSION_PV:pn-vim-tiny = "${@'.'.join(d.getVar('PV').split('.')[:-1])}"
13CHECK_VERSION_PV:pn-ncurses = "${PV}.%"
14CHECK_VERSION_PV:pn-alsa-tools = "%"
15CHECK_VERSION_PV:pn-gst-examples = "%"
16CHECK_VERSION_PV:pn-libedit = "${@d.getVar('PV').split('-')[1]}"
17
18# meta-oe
19CHECK_VERSION_PV:pn-iozone3 = "3.${PV}"
20CHECK_VERSION_PV:pn-can-utils = "%"
21CHECK_VERSION_PV:pn-luajit = "${PV}.%"
22CHECK_VERSION_PV:pn-sg3-utils = "%"