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