]> git.proxmox.com Git - mirror_edk2.git/blob - BaseTools/Scripts/PatchCheck.py
UefiCpuPkg: Move AsmRelocateApLoopStart from Mpfuncs.nasm to AmdSev.nasm
[mirror_edk2.git] / BaseTools / Scripts / PatchCheck.py
1 ## @file
2 # Check a patch for various format issues
3 #
4 # Copyright (c) 2015 - 2021, Intel Corporation. All rights reserved.<BR>
5 # Copyright (C) 2020, Red Hat, Inc.<BR>
6 # Copyright (c) 2020, ARM Ltd. All rights reserved.<BR>
7 #
8 # SPDX-License-Identifier: BSD-2-Clause-Patent
9 #
10
11 from __future__ import print_function
12
13 VersionNumber = '0.1'
14 __copyright__ = "Copyright (c) 2015 - 2016, Intel Corporation All rights reserved."
15
16 import email
17 import argparse
18 import os
19 import re
20 import subprocess
21 import sys
22
23 import email.header
24
25 class Verbose:
26 SILENT, ONELINE, NORMAL = range(3)
27 level = NORMAL
28
29 class EmailAddressCheck:
30 """Checks an email address."""
31
32 def __init__(self, email, description):
33 self.ok = True
34
35 if email is None:
36 self.error('Email address is missing!')
37 return
38 if description is None:
39 self.error('Email description is missing!')
40 return
41
42 self.description = "'" + description + "'"
43 self.check_email_address(email)
44
45 def error(self, *err):
46 if self.ok and Verbose.level > Verbose.ONELINE:
47 print('The ' + self.description + ' email address is not valid:')
48 self.ok = False
49 if Verbose.level < Verbose.NORMAL:
50 return
51 count = 0
52 for line in err:
53 prefix = (' *', ' ')[count > 0]
54 print(prefix, line)
55 count += 1
56
57 email_re1 = re.compile(r'(?:\s*)(.*?)(\s*)<(.+)>\s*$',
58 re.MULTILINE|re.IGNORECASE)
59
60 def check_email_address(self, email):
61 email = email.strip()
62 mo = self.email_re1.match(email)
63 if mo is None:
64 self.error("Email format is invalid: " + email.strip())
65 return
66
67 name = mo.group(1).strip()
68 if name == '':
69 self.error("Name is not provided with email address: " +
70 email)
71 else:
72 quoted = len(name) > 2 and name[0] == '"' and name[-1] == '"'
73 if name.find(',') >= 0 and not quoted:
74 self.error('Add quotes (") around name with a comma: ' +
75 name)
76
77 if mo.group(2) == '':
78 self.error("There should be a space between the name and " +
79 "email address: " + email)
80
81 if mo.group(3).find(' ') >= 0:
82 self.error("The email address cannot contain a space: " +
83 mo.group(3))
84
85 if ' via Groups.Io' in name and mo.group(3).endswith('@groups.io'):
86 self.error("Email rewritten by lists DMARC / DKIM / SPF: " +
87 email)
88
89 class CommitMessageCheck:
90 """Checks the contents of a git commit message."""
91
92 def __init__(self, subject, message, author_email):
93 self.ok = True
94
95 if subject is None and message is None:
96 self.error('Commit message is missing!')
97 return
98
99 MergifyMerge = False
100 if "mergify[bot]@users.noreply.github.com" in author_email:
101 if "Merge branch" in subject:
102 MergifyMerge = True
103
104 self.subject = subject
105 self.msg = message
106
107 print (subject)
108
109 self.check_contributed_under()
110 if not MergifyMerge:
111 self.check_signed_off_by()
112 self.check_misc_signatures()
113 self.check_overall_format()
114 self.report_message_result()
115
116 url = 'https://github.com/tianocore/tianocore.github.io/wiki/Commit-Message-Format'
117
118 def report_message_result(self):
119 if Verbose.level < Verbose.NORMAL:
120 return
121 if self.ok:
122 # All checks passed
123 return_code = 0
124 print('The commit message format passed all checks.')
125 else:
126 return_code = 1
127 if not self.ok:
128 print(self.url)
129
130 def error(self, *err):
131 if self.ok and Verbose.level > Verbose.ONELINE:
132 print('The commit message format is not valid:')
133 self.ok = False
134 if Verbose.level < Verbose.NORMAL:
135 return
136 count = 0
137 for line in err:
138 prefix = (' *', ' ')[count > 0]
139 print(prefix, line)
140 count += 1
141
142 # Find 'contributed-under:' at the start of a line ignoring case and
143 # requires ':' to be present. Matches if there is white space before
144 # the tag or between the tag and the ':'.
145 contributed_under_re = \
146 re.compile(r'^\s*contributed-under\s*:', re.MULTILINE|re.IGNORECASE)
147
148 def check_contributed_under(self):
149 match = self.contributed_under_re.search(self.msg)
150 if match is not None:
151 self.error('Contributed-under! (Note: this must be ' +
152 'removed by the code contributor!)')
153
154 @staticmethod
155 def make_signature_re(sig, re_input=False):
156 if re_input:
157 sub_re = sig
158 else:
159 sub_re = sig.replace('-', r'[-\s]+')
160 re_str = (r'^(?P<tag>' + sub_re +
161 r')(\s*):(\s*)(?P<value>\S.*?)(?:\s*)$')
162 try:
163 return re.compile(re_str, re.MULTILINE|re.IGNORECASE)
164 except Exception:
165 print("Tried to compile re:", re_str)
166 raise
167
168 sig_block_re = \
169 re.compile(r'''^
170 (?: (?P<tag>[^:]+) \s* : \s*
171 (?P<value>\S.*?) )
172 |
173 (?: \[ (?P<updater>[^:]+) \s* : \s*
174 (?P<note>.+?) \s* \] )
175 \s* $''',
176 re.VERBOSE | re.MULTILINE)
177
178 def find_signatures(self, sig):
179 if not sig.endswith('-by') and sig != 'Cc':
180 sig += '-by'
181 regex = self.make_signature_re(sig)
182
183 sigs = regex.findall(self.msg)
184
185 bad_case_sigs = filter(lambda m: m[0] != sig, sigs)
186 for s in bad_case_sigs:
187 self.error("'" +s[0] + "' should be '" + sig + "'")
188
189 for s in sigs:
190 if s[1] != '':
191 self.error('There should be no spaces between ' + sig +
192 " and the ':'")
193 if s[2] != ' ':
194 self.error("There should be a space after '" + sig + ":'")
195
196 EmailAddressCheck(s[3], sig)
197
198 return sigs
199
200 def check_signed_off_by(self):
201 sob='Signed-off-by'
202 if self.msg.find(sob) < 0:
203 self.error('Missing Signed-off-by! (Note: this must be ' +
204 'added by the code contributor!)')
205 return
206
207 sobs = self.find_signatures('Signed-off')
208
209 if len(sobs) == 0:
210 self.error('Invalid Signed-off-by format!')
211 return
212
213 sig_types = (
214 'Reviewed',
215 'Reported',
216 'Tested',
217 'Suggested',
218 'Acked',
219 'Cc'
220 )
221
222 def check_misc_signatures(self):
223 for sig in self.sig_types:
224 self.find_signatures(sig)
225
226 cve_re = re.compile('CVE-[0-9]{4}-[0-9]{5}[^0-9]')
227
228 def check_overall_format(self):
229 lines = self.msg.splitlines()
230
231 if len(lines) >= 1 and lines[0].endswith('\r\n'):
232 empty_line = '\r\n'
233 else:
234 empty_line = '\n'
235
236 lines.insert(0, empty_line)
237 lines.insert(0, self.subject + empty_line)
238
239 count = len(lines)
240
241 if count <= 0:
242 self.error('Empty commit message!')
243 return
244
245 if count >= 1 and re.search(self.cve_re, lines[0]):
246 #
247 # If CVE-xxxx-xxxxx is present in subject line, then limit length of
248 # subject line to 92 characters
249 #
250 if len(lines[0].rstrip()) >= 93:
251 self.error(
252 'First line of commit message (subject line) is too long (%d >= 93).' %
253 (len(lines[0].rstrip()))
254 )
255 else:
256 #
257 # If CVE-xxxx-xxxxx is not present in subject line, then limit
258 # length of subject line to 75 characters
259 #
260 if len(lines[0].rstrip()) >= 76:
261 self.error(
262 'First line of commit message (subject line) is too long (%d >= 76).' %
263 (len(lines[0].rstrip()))
264 )
265
266 if count >= 1 and len(lines[0].strip()) == 0:
267 self.error('First line of commit message (subject line) ' +
268 'is empty.')
269
270 if count >= 2 and lines[1].strip() != '':
271 self.error('Second line of commit message should be ' +
272 'empty.')
273
274 for i in range(2, count):
275 if (len(lines[i]) >= 76 and
276 len(lines[i].split()) > 1 and
277 not lines[i].startswith('git-svn-id:') and
278 not lines[i].startswith('Reviewed-by') and
279 not lines[i].startswith('Acked-by:') and
280 not lines[i].startswith('Tested-by:') and
281 not lines[i].startswith('Reported-by:') and
282 not lines[i].startswith('Suggested-by:') and
283 not lines[i].startswith('Signed-off-by:') and
284 not lines[i].startswith('Cc:')):
285 #
286 # Print a warning if body line is longer than 75 characters
287 #
288 print(
289 'WARNING - Line %d of commit message is too long (%d >= 76).' %
290 (i + 1, len(lines[i]))
291 )
292 print(lines[i])
293
294 last_sig_line = None
295 for i in range(count - 1, 0, -1):
296 line = lines[i]
297 mo = self.sig_block_re.match(line)
298 if mo is None:
299 if line.strip() == '':
300 break
301 elif last_sig_line is not None:
302 err2 = 'Add empty line before "%s"?' % last_sig_line
303 self.error('The line before the signature block ' +
304 'should be empty', err2)
305 else:
306 self.error('The signature block was not found')
307 break
308 last_sig_line = line.strip()
309
310 (START, PRE_PATCH, PATCH) = range(3)
311
312 class GitDiffCheck:
313 """Checks the contents of a git diff."""
314
315 def __init__(self, diff):
316 self.ok = True
317 self.format_ok = True
318 self.lines = diff.splitlines(True)
319 self.count = len(self.lines)
320 self.line_num = 0
321 self.state = START
322 self.new_bin = []
323 while self.line_num < self.count and self.format_ok:
324 line_num = self.line_num
325 self.run()
326 assert(self.line_num > line_num)
327 self.report_message_result()
328
329 def report_message_result(self):
330 if Verbose.level < Verbose.NORMAL:
331 return
332 if self.ok:
333 print('The code passed all checks.')
334 if self.new_bin:
335 print('\nWARNING - The following binary files will be added ' +
336 'into the repository:')
337 for binary in self.new_bin:
338 print(' ' + binary)
339
340 def run(self):
341 line = self.lines[self.line_num]
342
343 if self.state in (PRE_PATCH, PATCH):
344 if line.startswith('diff --git'):
345 self.state = START
346 if self.state == PATCH:
347 if line.startswith('@@ '):
348 self.state = PRE_PATCH
349 elif len(line) >= 1 and line[0] not in ' -+' and \
350 not line.startswith('\r\n') and \
351 not line.startswith(r'\ No newline ') and not self.binary:
352 for line in self.lines[self.line_num + 1:]:
353 if line.startswith('diff --git'):
354 self.format_error('diff found after end of patch')
355 break
356 self.line_num = self.count
357 return
358
359 if self.state == START:
360 if line.startswith('diff --git'):
361 self.state = PRE_PATCH
362 self.filename = line[13:].split(' ', 1)[0]
363 self.is_newfile = False
364 self.force_crlf = True
365 self.force_notabs = True
366 if self.filename.endswith('.sh') or \
367 self.filename.startswith('BaseTools/BinWrappers/PosixLike/') or \
368 self.filename.startswith('BaseTools/BinPipWrappers/PosixLike/') or \
369 self.filename == 'BaseTools/BuildEnv':
370 #
371 # Do not enforce CR/LF line endings for linux shell scripts.
372 # Some linux shell scripts don't end with the ".sh" extension,
373 # they are identified by their path.
374 #
375 self.force_crlf = False
376 if self.filename == '.gitmodules' or \
377 self.filename == 'BaseTools/Conf/diff.order':
378 #
379 # .gitmodules and diff orderfiles are used internally by git
380 # use tabs and LF line endings. Do not enforce no tabs and
381 # do not enforce CR/LF line endings.
382 #
383 self.force_crlf = False
384 self.force_notabs = False
385 if os.path.basename(self.filename) == 'GNUmakefile' or \
386 os.path.basename(self.filename) == 'Makefile':
387 self.force_notabs = False
388 elif len(line.rstrip()) != 0:
389 self.format_error("didn't find diff command")
390 self.line_num += 1
391 elif self.state == PRE_PATCH:
392 if line.startswith('@@ '):
393 self.state = PATCH
394 self.binary = False
395 elif line.startswith('GIT binary patch') or \
396 line.startswith('Binary files'):
397 self.state = PATCH
398 self.binary = True
399 if self.is_newfile:
400 self.new_bin.append(self.filename)
401 elif line.startswith('new file mode 160000'):
402 #
403 # New submodule. Do not enforce CR/LF line endings
404 #
405 self.force_crlf = False
406 else:
407 ok = False
408 self.is_newfile = self.newfile_prefix_re.match(line)
409 for pfx in self.pre_patch_prefixes:
410 if line.startswith(pfx):
411 ok = True
412 if not ok:
413 self.format_error("didn't find diff hunk marker (@@)")
414 self.line_num += 1
415 elif self.state == PATCH:
416 if self.binary:
417 pass
418 elif line.startswith('-'):
419 pass
420 elif line.startswith('+'):
421 self.check_added_line(line[1:])
422 elif line.startswith('\r\n'):
423 pass
424 elif line.startswith(r'\ No newline '):
425 pass
426 elif not line.startswith(' '):
427 self.format_error("unexpected patch line")
428 self.line_num += 1
429
430 pre_patch_prefixes = (
431 '--- ',
432 '+++ ',
433 'index ',
434 'new file ',
435 'deleted file ',
436 'old mode ',
437 'new mode ',
438 'similarity index ',
439 'copy from ',
440 'copy to ',
441 'rename ',
442 )
443
444 line_endings = ('\r\n', '\n\r', '\n', '\r')
445
446 newfile_prefix_re = \
447 re.compile(r'''^
448 index\ 0+\.\.
449 ''',
450 re.VERBOSE)
451
452 def added_line_error(self, msg, line):
453 lines = [ msg ]
454 if self.filename is not None:
455 lines.append('File: ' + self.filename)
456 lines.append('Line: ' + line)
457
458 self.error(*lines)
459
460 old_debug_re = \
461 re.compile(r'''
462 DEBUG \s* \( \s* \( \s*
463 (?: DEBUG_[A-Z_]+ \s* \| \s*)*
464 EFI_D_ ([A-Z_]+)
465 ''',
466 re.VERBOSE)
467
468 def check_added_line(self, line):
469 eol = ''
470 for an_eol in self.line_endings:
471 if line.endswith(an_eol):
472 eol = an_eol
473 line = line[:-len(eol)]
474
475 stripped = line.rstrip()
476
477 if self.force_crlf and eol != '\r\n' and (line.find('Subproject commit') == -1):
478 self.added_line_error('Line ending (%s) is not CRLF' % repr(eol),
479 line)
480 if self.force_notabs and '\t' in line:
481 self.added_line_error('Tab character used', line)
482 if len(stripped) < len(line):
483 self.added_line_error('Trailing whitespace found', line)
484
485 mo = self.old_debug_re.search(line)
486 if mo is not None:
487 self.added_line_error('EFI_D_' + mo.group(1) + ' was used, '
488 'but DEBUG_' + mo.group(1) +
489 ' is now recommended', line)
490
491 split_diff_re = re.compile(r'''
492 (?P<cmd>
493 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $
494 )
495 (?P<index>
496 ^ index \s+ .+ $
497 )
498 ''',
499 re.IGNORECASE | re.VERBOSE | re.MULTILINE)
500
501 def format_error(self, err):
502 self.format_ok = False
503 err = 'Patch format error: ' + err
504 err2 = 'Line: ' + self.lines[self.line_num].rstrip()
505 self.error(err, err2)
506
507 def error(self, *err):
508 if self.ok and Verbose.level > Verbose.ONELINE:
509 print('Code format is not valid:')
510 self.ok = False
511 if Verbose.level < Verbose.NORMAL:
512 return
513 count = 0
514 for line in err:
515 prefix = (' *', ' ')[count > 0]
516 print(prefix, line)
517 count += 1
518
519 class CheckOnePatch:
520 """Checks the contents of a git email formatted patch.
521
522 Various checks are performed on both the commit message and the
523 patch content.
524 """
525
526 def __init__(self, name, patch):
527 self.patch = patch
528 self.find_patch_pieces()
529
530 email_check = EmailAddressCheck(self.author_email, 'Author')
531 email_ok = email_check.ok
532
533 msg_check = CommitMessageCheck(self.commit_subject, self.commit_msg, self.author_email)
534 msg_ok = msg_check.ok
535
536 diff_ok = True
537 if self.diff is not None:
538 diff_check = GitDiffCheck(self.diff)
539 diff_ok = diff_check.ok
540
541 self.ok = email_ok and msg_ok and diff_ok
542
543 if Verbose.level == Verbose.ONELINE:
544 if self.ok:
545 result = 'ok'
546 else:
547 result = list()
548 if not msg_ok:
549 result.append('commit message')
550 if not diff_ok:
551 result.append('diff content')
552 result = 'bad ' + ' and '.join(result)
553 print(name, result)
554
555
556 git_diff_re = re.compile(r'''
557 ^ diff \s+ --git \s+ a/.+ \s+ b/.+ $
558 ''',
559 re.IGNORECASE | re.VERBOSE | re.MULTILINE)
560
561 stat_re = \
562 re.compile(r'''
563 (?P<commit_message> [\s\S\r\n]* )
564 (?P<stat>
565 ^ --- $ [\r\n]+
566 (?: ^ \s+ .+ \s+ \| \s+ \d+ \s+ \+* \-*
567 $ [\r\n]+ )+
568 [\s\S\r\n]+
569 )
570 ''',
571 re.IGNORECASE | re.VERBOSE | re.MULTILINE)
572
573 subject_prefix_re = \
574 re.compile(r'''^
575 \s* (\[
576 [^\[\]]* # Allow all non-brackets
577 \])* \s*
578 ''',
579 re.VERBOSE)
580
581 def find_patch_pieces(self):
582 if sys.version_info < (3, 0):
583 patch = self.patch.encode('ascii', 'ignore')
584 else:
585 patch = self.patch
586
587 self.commit_msg = None
588 self.stat = None
589 self.commit_subject = None
590 self.commit_prefix = None
591 self.diff = None
592
593 if patch.startswith('diff --git'):
594 self.diff = patch
595 return
596
597 pmail = email.message_from_string(patch)
598 parts = list(pmail.walk())
599 assert(len(parts) == 1)
600 assert(parts[0].get_content_type() == 'text/plain')
601 content = parts[0].get_payload(decode=True).decode('utf-8', 'ignore')
602
603 mo = self.git_diff_re.search(content)
604 if mo is not None:
605 self.diff = content[mo.start():]
606 content = content[:mo.start()]
607
608 mo = self.stat_re.search(content)
609 if mo is None:
610 self.commit_msg = content
611 else:
612 self.stat = mo.group('stat')
613 self.commit_msg = mo.group('commit_message')
614 #
615 # Parse subject line from email header. The subject line may be
616 # composed of multiple parts with different encodings. Decode and
617 # combine all the parts to produce a single string with the contents of
618 # the decoded subject line.
619 #
620 parts = email.header.decode_header(pmail.get('subject'))
621 subject = ''
622 for (part, encoding) in parts:
623 if encoding:
624 part = part.decode(encoding)
625 else:
626 try:
627 part = part.decode()
628 except:
629 pass
630 subject = subject + part
631
632 self.commit_subject = subject.replace('\r\n', '')
633 self.commit_subject = self.commit_subject.replace('\n', '')
634 self.commit_subject = self.subject_prefix_re.sub('', self.commit_subject, 1)
635
636 self.author_email = pmail['from']
637
638 class CheckGitCommits:
639 """Reads patches from git based on the specified git revision range.
640
641 The patches are read from git, and then checked.
642 """
643
644 def __init__(self, rev_spec, max_count):
645 commits = self.read_commit_list_from_git(rev_spec, max_count)
646 if len(commits) == 1 and Verbose.level > Verbose.ONELINE:
647 commits = [ rev_spec ]
648 self.ok = True
649 blank_line = False
650 for commit in commits:
651 if Verbose.level > Verbose.ONELINE:
652 if blank_line:
653 print()
654 else:
655 blank_line = True
656 print('Checking git commit:', commit)
657 email = self.read_committer_email_address_from_git(commit)
658 self.ok &= EmailAddressCheck(email, 'Committer').ok
659 patch = self.read_patch_from_git(commit)
660 self.ok &= CheckOnePatch(commit, patch).ok
661 if not commits:
662 print("Couldn't find commit matching: '{}'".format(rev_spec))
663
664 def read_commit_list_from_git(self, rev_spec, max_count):
665 # Run git to get the commit patch
666 cmd = [ 'rev-list', '--abbrev-commit', '--no-walk' ]
667 if max_count is not None:
668 cmd.append('--max-count=' + str(max_count))
669 cmd.append(rev_spec)
670 out = self.run_git(*cmd)
671 return out.split() if out else []
672
673 def read_patch_from_git(self, commit):
674 # Run git to get the commit patch
675 return self.run_git('show', '--pretty=email', '--no-textconv',
676 '--no-use-mailmap', commit)
677
678 def read_committer_email_address_from_git(self, commit):
679 # Run git to get the committer email
680 return self.run_git('show', '--pretty=%cn <%ce>', '--no-patch',
681 '--no-use-mailmap', commit)
682
683 def run_git(self, *args):
684 cmd = [ 'git' ]
685 cmd += args
686 p = subprocess.Popen(cmd,
687 stdout=subprocess.PIPE,
688 stderr=subprocess.STDOUT)
689 Result = p.communicate()
690 return Result[0].decode('utf-8', 'ignore') if Result[0] and Result[0].find(b"fatal")!=0 else None
691
692 class CheckOnePatchFile:
693 """Performs a patch check for a single file.
694
695 stdin is used when the filename is '-'.
696 """
697
698 def __init__(self, patch_filename):
699 if patch_filename == '-':
700 patch = sys.stdin.read()
701 patch_filename = 'stdin'
702 else:
703 f = open(patch_filename, 'rb')
704 patch = f.read().decode('utf-8', 'ignore')
705 f.close()
706 if Verbose.level > Verbose.ONELINE:
707 print('Checking patch file:', patch_filename)
708 self.ok = CheckOnePatch(patch_filename, patch).ok
709
710 class CheckOneArg:
711 """Performs a patch check for a single command line argument.
712
713 The argument will be handed off to a file or git-commit based
714 checker.
715 """
716
717 def __init__(self, param, max_count=None):
718 self.ok = True
719 if param == '-' or os.path.exists(param):
720 checker = CheckOnePatchFile(param)
721 else:
722 checker = CheckGitCommits(param, max_count)
723 self.ok = checker.ok
724
725 class PatchCheckApp:
726 """Checks patches based on the command line arguments."""
727
728 def __init__(self):
729 self.parse_options()
730 patches = self.args.patches
731
732 if len(patches) == 0:
733 patches = [ 'HEAD' ]
734
735 self.ok = True
736 self.count = None
737 for patch in patches:
738 self.process_one_arg(patch)
739
740 if self.count is not None:
741 self.process_one_arg('HEAD')
742
743 if self.ok:
744 self.retval = 0
745 else:
746 self.retval = -1
747
748 def process_one_arg(self, arg):
749 if len(arg) >= 2 and arg[0] == '-':
750 try:
751 self.count = int(arg[1:])
752 return
753 except ValueError:
754 pass
755 self.ok &= CheckOneArg(arg, self.count).ok
756 self.count = None
757
758 def parse_options(self):
759 parser = argparse.ArgumentParser(description=__copyright__)
760 parser.add_argument('--version', action='version',
761 version='%(prog)s ' + VersionNumber)
762 parser.add_argument('patches', nargs='*',
763 help='[patch file | git rev list]')
764 group = parser.add_mutually_exclusive_group()
765 group.add_argument("--oneline",
766 action="store_true",
767 help="Print one result per line")
768 group.add_argument("--silent",
769 action="store_true",
770 help="Print nothing")
771 self.args = parser.parse_args()
772 if self.args.oneline:
773 Verbose.level = Verbose.ONELINE
774 if self.args.silent:
775 Verbose.level = Verbose.SILENT
776
777 if __name__ == "__main__":
778 sys.exit(PatchCheckApp().retval)