From d079f3a5d1d34a5f0450009a938ded1e4cfcf0d6 Mon Sep 17 00:00:00 2001 From: Ben Pastene Date: Mon, 23 Mar 2020 18:13:10 +0000 Subject: [PATCH] gn_helpers: Add support for imported args files. The libassistant build calls gn_helpers to read the out dir's args.gn file (while it's being invoked by ninja, mind you!) and passes its contents through gn_helpers, applies a filtering, then creates a sub build dir (within the original build dir) using its modified set of GN args. This is problematic since crbug.com/937821 is trying to transition CrOS bots to using import() lines in their args file, which breaks libassistant's build. This fixes that by adding import support to gn_helpers. Bug: 937821 Change-Id: I790bc27368611f31b63a0135c0e5919f35b21e6e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2107709 Reviewed-by: Takuto Ikuta Commit-Queue: Ben Pastene Cr-Original-Commit-Position: refs/heads/master@{#752503} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: 00156a211ee71ca14e6dcb1084b88b9ad92944e9 --- gn_helpers.py | 31 ++++++++++++++++++++++ gn_helpers_unittest.py | 59 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/gn_helpers.py b/gn_helpers.py index 53543e669..b90c2fbbd 100644 --- a/gn_helpers.py +++ b/gn_helpers.py @@ -19,9 +19,14 @@ To use in a random python file in the build: Where the sequence of parameters to join is the relative path from your source file to the build directory.""" +import os +import re import sys +IMPORT_RE = re.compile(r'^import\("//(\S+)"\)') + + class GNException(Exception): pass @@ -170,6 +175,31 @@ class GNValueParser(object): def IsDone(self): return self.cur == len(self.input) + def ReplaceImports(self): + """Replaces import(...) lines with the contents of the imports. + + Recurses on itself until there are no imports remaining, in the case of + nested imports. + """ + lines = self.input.splitlines() + if not any(line.startswith('import(') for line in lines): + return + for line in lines: + if not line.startswith('import('): + continue + regex_match = IMPORT_RE.match(line) + if not regex_match: + raise GNException('Not a valid import string: %s' % line) + import_path = os.path.join( + os.path.dirname(__file__), os.pardir, regex_match.group(1)) + with open(import_path) as f: + imported_args = f.read() + self.input = self.input.replace(line, imported_args) + # Call ourselves again if we've just replaced an import() with additional + # imports. + self.ReplaceImports() + + def ConsumeWhitespace(self): while not self.IsDone() and self.input[self.cur] in ' \t\n': self.cur += 1 @@ -218,6 +248,7 @@ class GNValueParser(object): """ d = {} + self.ReplaceImports() self.ConsumeWhitespace() self.ConsumeComment() while not self.IsDone(): diff --git a/gn_helpers_unittest.py b/gn_helpers_unittest.py index 43c084b3a..99d720b90 100644 --- a/gn_helpers_unittest.py +++ b/gn_helpers_unittest.py @@ -2,9 +2,13 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -import gn_helpers +import mock +import textwrap import unittest +import gn_helpers + + class UnitTest(unittest.TestCase): def test_ToGNString(self): self.assertEqual( @@ -122,5 +126,58 @@ class UnitTest(unittest.TestCase): self.assertEqual(gn_helpers.FromGNArgs('foo_=true'), {'foo_': True}) + def test_ReplaceImports(self): + # Should be a no-op on args inputs without any imports. + parser = gn_helpers.GNValueParser( + textwrap.dedent(""" + some_arg1 = "val1" + some_arg2 = "val2" + """)) + parser.ReplaceImports() + self.assertEquals( + parser.input, + textwrap.dedent(""" + some_arg1 = "val1" + some_arg2 = "val2" + """)) + + # A single "import(...)" line should be replaced with the contents of the + # file being imported. + parser = gn_helpers.GNValueParser( + textwrap.dedent(""" + some_arg1 = "val1" + import("//some/args/file.gni") + some_arg2 = "val2" + """)) + fake_import = 'some_imported_arg = "imported_val"' + with mock.patch('__builtin__.open', mock.mock_open(read_data=fake_import)): + parser.ReplaceImports() + self.assertEquals( + parser.input, + textwrap.dedent(""" + some_arg1 = "val1" + some_imported_arg = "imported_val" + some_arg2 = "val2" + """)) + + # No trailing parenthesis should raise an exception. + with self.assertRaises(gn_helpers.GNException): + parser = gn_helpers.GNValueParser( + textwrap.dedent('import("//some/args/file.gni"')) + parser.ReplaceImports() + + # No double quotes should raise an exception. + with self.assertRaises(gn_helpers.GNException): + parser = gn_helpers.GNValueParser( + textwrap.dedent('import(//some/args/file.gni)')) + parser.ReplaceImports() + + # A path that's not source absolute should raise an exception. + with self.assertRaises(gn_helpers.GNException): + parser = gn_helpers.GNValueParser( + textwrap.dedent('import("some/relative/args/file.gni")')) + parser.ReplaceImports() + + if __name__ == '__main__': unittest.main()