tools: python amalgamation for tools/tracebox etc
Before this change:
the business logic for tools/{tracebox, record_android_trace, ...}
was in the file itself and the manifest was replaced in-place
when invoking tools/roll-prebuilts.
This still made it impossible to share code between tools
without copy/pasting.
With this change:
- Move the business logic to python/xxx
- Add an amalgamator that follows includes. Only the form
'from perfetto.xxx import yyy' is supported.
- Keep the amalgamated files in tools/traceconv
No code sharing / major refactorings are made by this change.
They can happen as a follow-up though
Change-Id: I7420387881e6ef1e109abae6380dde7c06ac1b27
diff --git a/tools/gen_amalgamated_python_tools b/tools/gen_amalgamated_python_tools
new file mode 100755
index 0000000..4b21069
--- /dev/null
+++ b/tools/gen_amalgamated_python_tools
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+import logging
+import re
+
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+AMALGAMATION_MAP = {
+ 'python/tools/record_android_trace.py': 'tools/record_android_trace',
+ 'python/tools/tracebox.py': 'tools/tracebox',
+ 'python/tools/traceconv.py': 'tools/traceconv',
+ 'python/tools/trace_processor.py': 'tools/trace_processor',
+ 'python/tools/cpu_profile.py': 'tools/cpu_profile',
+ 'python/tools/heap_profile.py': 'tools/heap_profile',
+}
+
+
+def amalgamate_file(fname, stack=None, done=None, in_progress=None):
+ stack = [] if stack is None else stack
+ done = set() if done is None else done
+ in_progress = set() if in_progress is None else in_progress
+ if fname in in_progress:
+ cycle = ' > '.join(stack + [fname])
+ logging.fatal('Cycle detected in %s', cycle)
+ sys.exit(1)
+ if fname in done:
+ return []
+ logging.debug('Processing %s', fname)
+ done.add(fname)
+ in_progress.add(fname)
+ with open(fname, encoding='utf-8') as f:
+ lines = f.readlines()
+ outlines = []
+ for line in lines:
+ if line.startswith('from perfetto') or line.startswith('import perfetto'):
+ if not re.match('from perfetto[.][.\w]+\s+import\s+[*]$', line):
+ logging.fatal('Error in %s on line \"%s\"', fname, line.rstrip())
+ logging.fatal('Only "from perfetto.foo import *" is supported in '
+ 'sources that are used in //tools and get amalgamated')
+ sys.exit(1)
+ pkg = line.split()[1]
+ fpath = os.path.join('python', pkg.replace('.', os.sep) + '.py')
+ outlines.append('\n# ----- Amalgamator: begin of %s\n' % fpath)
+ outlines += amalgamate_file(fpath, stack + [fname], done, in_progress)
+ outlines.append('\n# ----- Amalgamator: end of %s\n' % fpath)
+ elif '__file__' in line and not 'amalgamator:nocheck' in line:
+ logging.fatal('__file__ is not allowed in sources that get amalgamated.'
+ 'In %s on line \"%s\"', fname, line.rstrip())
+ sys.exit(1)
+
+ else:
+ outlines.append(line)
+ in_progress.remove(fname)
+ logging.debug('%s: %d lines', fname, len(outlines))
+ return outlines
+
+
+def amalgamate(src, dst, check_only=False):
+ lines = amalgamate_file(src)
+ banner = '''
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+# DO NOT EDIT. Auto-generated by %s
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+'''
+ lines.insert(lines.index('\n'), banner % os.path.relpath(__file__, ROOT_DIR))
+ new_content = ''.join(lines)
+
+ if check_only:
+ if not os.path.exists(dst):
+ return False
+ with open(dst, encoding='utf-8') as f:
+ return f.read() == new_content
+
+ logging.info('Amalgamating %s -> %s', src, dst)
+ with open(dst + '.tmp', 'w', encoding='utf-8') as f:
+ f.write(new_content)
+ os.chmod(dst + '.tmp', 0o755)
+ os.rename(dst + '.tmp', dst)
+ return True
+
+
+def main():
+ check_only = '--check-only' in sys.argv
+ logging.basicConfig(
+ format='%(levelname)-8s: %(message)s',
+ level=logging.DEBUG if '-v' in sys.argv else logging.INFO)
+ os.chdir(ROOT_DIR) # Make the execution cwd-independent.
+ success = True
+ for src, dst in AMALGAMATION_MAP.items():
+ success = success and amalgamate(src, dst, check_only)
+ return 0 if success else 1
+
+
+if __name__ == '__main__':
+ sys.exit(main())