Copy some build_utils helpers to a non-android location
Specifically, moves:
* atomic_output
* add_depfile_arg()
* parse_gn_list()
* write_depfile()
Into //build/action_helpers.py
This will remove some oddness of non-Android scripts importing a file
from //build/android.
I will remove the copies in build_utils.py in a follow-up.
Bug: 1428082
Change-Id: If7dfe5306a7907987417e345637758c716a75ab5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4382406
Commit-Queue: Andrew Grieve <[email protected]>
Reviewed-by: Sam Maier <[email protected]>
Auto-Submit: Andrew Grieve <[email protected]>
Owners-Override: Andrew Grieve <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1124347}
diff --git a/build/action_helpers.py b/build/action_helpers.py
new file mode 100644
index 0000000..7a9db14
--- /dev/null
+++ b/build/action_helpers.py
@@ -0,0 +1,125 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Helper functions useful when writing scripts used by action() targets."""
+
+import contextlib
+import filecmp
+import os
+import pathlib
+import shutil
+import tempfile
+
+import gn_helpers
+
+
[email protected]
+def atomic_output(path, mode='w+b', only_if_changed=True):
+ """Prevent half-written files and dirty mtimes for unchanged files.
+
+ Args:
+ path: Path to the final output file, which will be written atomically.
+ mode: The mode to open the file in (str).
+ only_if_changed: Whether to maintain the mtime if the file has not changed.
+ Returns:
+ A Context Manager that yields a NamedTemporaryFile instance. On exit, the
+ manager will check if the file contents is different from the destination
+ and if so, move it into place.
+
+ Example:
+ with action_helpers.atomic_output(output_path) as tmp_file:
+ subprocess.check_call(['prog', '--output', tmp_file.name])
+ """
+ # Create in same directory to ensure same filesystem when moving.
+ dirname = os.path.dirname(path) or '.'
+ os.makedirs(dirname, exist_ok=True)
+ with tempfile.NamedTemporaryFile(mode,
+ suffix=os.path.basename(path),
+ dir=dirname,
+ delete=False) as f:
+ try:
+ yield f
+
+ # file should be closed before comparison/move.
+ f.close()
+ if not (only_if_changed and os.path.exists(path)
+ and filecmp.cmp(f.name, path)):
+ shutil.move(f.name, path)
+ finally:
+ if os.path.exists(f.name):
+ os.unlink(f.name)
+
+
+def add_depfile_arg(parser):
+ if hasattr(parser, 'add_option'):
+ func = parser.add_option
+ else:
+ func = parser.add_argument
+ func('--depfile', help='Path to depfile (refer to "gn help depfile")')
+
+
+def write_depfile(depfile_path, first_gn_output, inputs=None):
+ """Writes a ninja depfile.
+
+ See notes about how to use depfiles in //build/docs/writing_gn_templates.md.
+
+ Args:
+ depfile_path: Path to file to write.
+ first_gn_output: Path of first entry in action's outputs.
+ inputs: List of inputs to add to depfile.
+ """
+ assert depfile_path != first_gn_output # http://crbug.com/646165
+ assert not isinstance(inputs, str) # Easy mistake to make
+
+ if inputs:
+ for path in inputs:
+ # Ensure relative paths os that build dirs are hermetic.
+ assert not os.path.isabs(path), f'Found abs path in depfile: {path}'
+
+ def _escape(value):
+ return value.replace(' ', '\\ ')
+
+ sb = []
+ sb.append(_escape(first_gn_output))
+ if inputs:
+ # Sort and uniquify to ensure file is hermetic.
+ # One path per line to keep it human readable.
+ sb.append(': \\\n ')
+ sb.append(' \\\n '.join(sorted(_escape(p) for p in set(inputs))))
+ else:
+ sb.append(': ')
+ sb.append('\n')
+
+ path = pathlib.Path(depfile_path)
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(''.join(sb))
+
+
+def parse_gn_list(value):
+ """Converts a "GN-list" command-line parameter into a list.
+
+ Conversions handled:
+ * None -> []
+ * '' -> []
+ * 'asdf' -> ['asdf']
+ * '["a", "b"]' -> ['a', 'b']
+ * ['["a", "b"]', 'c'] -> ['a', 'b', 'c'] (action='append')
+
+ This allows passing args like:
+ gn_list = [ "one", "two", "three" ]
+ args = [ "--items=$gn_list" ]
+ """
+ # Convert None to [].
+ if not value:
+ return []
+ # Convert a list of GN lists to a flattened list.
+ if isinstance(value, list):
+ ret = []
+ for arg in value:
+ ret.extend(parse_gn_list(arg))
+ return ret
+ # Convert normal GN list.
+ if value.startswith('['):
+ return gn_helpers.GNValueParser(value).ParseList()
+ # Convert a single string value to a list.
+ return [value]