Android: add java_cpp_features GN rule
This adds the java_cpp_features GN rule to autogenerate Java String constants representing C++ features. This refactors parts of java_cpp_strings so java_cpp_features can share it. This aims to address the most common syntaxes for declaring C++ features (string literal names, "brace" and "equals brace" initialization, with & without the "base::" namespace). Design: http://go/autogen-java-features Bug: 1060097 Fixed: 1091031 Test: vpython build/android/gpy/java_cpp_strings_tests.py Test: vpython build/android/gpy/java_cpp_features_tests.py Test: tools/md_browser/md_browser.py docs/android_accessing_cpp_features_in_java.md Change-Id: I5311f72f8837c122186148cf183f4219087df96a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2412840 Auto-Submit: Nate Fischer <ntfschr@chromium.org> Reviewed-by: Andrew Grieve <agrieve@chromium.org> Reviewed-by: Henrique Nakashima <hnakashima@chromium.org> Reviewed-by: Peter Wen <wnwen@chromium.org> Commit-Queue: Andrew Grieve <agrieve@chromium.org> Cr-Commit-Position: refs/heads/master@{#812889}
This commit is contained in:
@ -1345,6 +1345,7 @@ _GENERIC_PYDEPS_FILES = [
|
||||
'build/android/gyp/ijar.pydeps',
|
||||
'build/android/gyp/jacoco_instr.pydeps',
|
||||
'build/android/gyp/java_cpp_enum.pydeps',
|
||||
'build/android/gyp/java_cpp_features.pydeps',
|
||||
'build/android/gyp/java_cpp_strings.pydeps',
|
||||
'build/android/gyp/jetify_jar.pydeps',
|
||||
'build/android/gyp/jinja_template.pydeps',
|
||||
|
@ -333,9 +333,8 @@ def DoGenerate(source_paths):
|
||||
'"// GENERATED_JAVA_ENUM_PACKAGE: foo"?' %
|
||||
source_path)
|
||||
for enum_definition in enum_definitions:
|
||||
package_path = enum_definition.enum_package.replace('.', os.path.sep)
|
||||
file_name = enum_definition.class_name + '.java'
|
||||
output_path = os.path.join(package_path, file_name)
|
||||
output_path = java_cpp_utils.GetJavaFilePath(enum_definition.enum_package,
|
||||
enum_definition.class_name)
|
||||
output = GenerateOutput(source_path, enum_definition)
|
||||
yield output_path, output
|
||||
|
||||
|
110
build/android/gyp/java_cpp_features.py
Executable file
110
build/android/gyp/java_cpp_features.py
Executable file
@ -0,0 +1,110 @@
|
||||
#!/user/bin/env python
|
||||
#
|
||||
# Copyright 2020 The Chromium Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
from util import build_utils
|
||||
from util import java_cpp_utils
|
||||
|
||||
|
||||
class FeatureParserDelegate(java_cpp_utils.CppConstantParser.Delegate):
|
||||
# Ex. 'const base::Feature kConstantName{"StringNameOfTheFeature", ...};'
|
||||
# would parse as:
|
||||
# ExtractConstantName() -> 'ConstantName'
|
||||
# ExtractValue() -> '"StringNameOfTheFeature"'
|
||||
FEATURE_RE = re.compile(r'\s*const (?:base::)?Feature\s+k(\w+)\s*(?:=\s*)?{')
|
||||
VALUE_RE = re.compile(r'\s*("(?:\"|[^"])*")\s*,')
|
||||
|
||||
def ExtractConstantName(self, line):
|
||||
match = FeatureParserDelegate.FEATURE_RE.match(line)
|
||||
return match.group(1) if match else None
|
||||
|
||||
def ExtractValue(self, line):
|
||||
match = FeatureParserDelegate.VALUE_RE.search(line)
|
||||
return match.group(1) if match else None
|
||||
|
||||
def CreateJavaConstant(self, name, value, comments):
|
||||
return java_cpp_utils.JavaString(name, value, comments)
|
||||
|
||||
|
||||
def _GenerateOutput(template, source_paths, template_path, features):
|
||||
description_template = """
|
||||
// This following string constants were inserted by
|
||||
// {SCRIPT_NAME}
|
||||
// From
|
||||
// {SOURCE_PATHS}
|
||||
// Into
|
||||
// {TEMPLATE_PATH}
|
||||
|
||||
"""
|
||||
values = {
|
||||
'SCRIPT_NAME': java_cpp_utils.GetScriptName(),
|
||||
'SOURCE_PATHS': ',\n // '.join(source_paths),
|
||||
'TEMPLATE_PATH': template_path,
|
||||
}
|
||||
description = description_template.format(**values)
|
||||
native_features = '\n\n'.join(x.Format() for x in features)
|
||||
|
||||
values = {
|
||||
'NATIVE_FEATURES': description + native_features,
|
||||
}
|
||||
return template.format(**values)
|
||||
|
||||
|
||||
def _ParseFeatureFile(path):
|
||||
with open(path) as f:
|
||||
feature_file_parser = java_cpp_utils.CppConstantParser(
|
||||
FeatureParserDelegate(), f.readlines())
|
||||
return feature_file_parser.Parse()
|
||||
|
||||
|
||||
def _Generate(source_paths, template_path):
|
||||
with open(template_path) as f:
|
||||
lines = f.readlines()
|
||||
|
||||
template = ''.join(lines)
|
||||
package, class_name = java_cpp_utils.ParseTemplateFile(lines)
|
||||
output_path = java_cpp_utils.GetJavaFilePath(package, class_name)
|
||||
|
||||
features = []
|
||||
for source_path in source_paths:
|
||||
features.extend(_ParseFeatureFile(source_path))
|
||||
|
||||
output = _GenerateOutput(template, source_paths, template_path, features)
|
||||
return output, output_path
|
||||
|
||||
|
||||
def _Main(argv):
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument('--srcjar',
|
||||
required=True,
|
||||
help='The path at which to generate the .srcjar file')
|
||||
|
||||
parser.add_argument('--template',
|
||||
required=True,
|
||||
help='The template file with which to generate the Java '
|
||||
'class. Must have "{NATIVE_FEATURES}" somewhere in '
|
||||
'the template.')
|
||||
|
||||
parser.add_argument('inputs',
|
||||
nargs='+',
|
||||
help='Input file(s)',
|
||||
metavar='INPUTFILE')
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
with build_utils.AtomicOutput(args.srcjar) as f:
|
||||
with zipfile.ZipFile(f, 'w', zipfile.ZIP_STORED) as srcjar:
|
||||
data, path = _Generate(args.inputs, args.template)
|
||||
build_utils.AddToZipHermetic(srcjar, path, data=data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_Main(sys.argv[1:])
|
7
build/android/gyp/java_cpp_features.pydeps
Normal file
7
build/android/gyp/java_cpp_features.pydeps
Normal file
@ -0,0 +1,7 @@
|
||||
# Generated by running:
|
||||
# build/print_python_deps.py --root build/android/gyp --output build/android/gyp/java_cpp_features.pydeps build/android/gyp/java_cpp_features.py
|
||||
../../gn_helpers.py
|
||||
java_cpp_features.py
|
||||
util/__init__.py
|
||||
util/build_utils.py
|
||||
util/java_cpp_utils.py
|
198
build/android/gyp/java_cpp_features_tests.py
Executable file
198
build/android/gyp/java_cpp_features_tests.py
Executable file
@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2020 The Chromium Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
"""Tests for java_cpp_features.py.
|
||||
|
||||
This test suite contains various tests for the C++ -> Java base::Feature
|
||||
generator.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import java_cpp_features
|
||||
from util import java_cpp_utils
|
||||
|
||||
|
||||
class _TestFeaturesParser(unittest.TestCase):
|
||||
def testParseComments(self):
|
||||
test_data = """
|
||||
/**
|
||||
* This should be ignored as well.
|
||||
*/
|
||||
|
||||
// Comment followed by a blank line.
|
||||
|
||||
// Comment followed by unrelated code.
|
||||
int foo() { return 3; }
|
||||
|
||||
// Real comment.
|
||||
const base::Feature kSomeFeature{"SomeFeature",
|
||||
base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
|
||||
// Real comment that spans
|
||||
// multiple lines.
|
||||
const base::Feature kSomeOtherFeature{"SomeOtherFeature",
|
||||
base::FEATURE_ENABLED_BY_DEFAULT};
|
||||
|
||||
// Comment followed by nothing.
|
||||
""".split('\n')
|
||||
feature_file_parser = java_cpp_utils.CppConstantParser(
|
||||
java_cpp_features.FeatureParserDelegate(), test_data)
|
||||
features = feature_file_parser.Parse()
|
||||
self.assertEqual(2, len(features))
|
||||
self.assertEqual('SOME_FEATURE', features[0].name)
|
||||
self.assertEqual('"SomeFeature"', features[0].value)
|
||||
self.assertEqual(1, len(features[0].comments.split('\n')))
|
||||
self.assertEqual('SOME_OTHER_FEATURE', features[1].name)
|
||||
self.assertEqual('"SomeOtherFeature"', features[1].value)
|
||||
self.assertEqual(2, len(features[1].comments.split('\n')))
|
||||
|
||||
def testWhitespace(self):
|
||||
test_data = """
|
||||
// 1 line
|
||||
const base::Feature kShort{"Short", base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
|
||||
// 2 lines
|
||||
const base::Feature kTwoLineFeatureA{"TwoLineFeatureA",
|
||||
base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
const base::Feature kTwoLineFeatureB{
|
||||
"TwoLineFeatureB", base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
|
||||
// 3 lines
|
||||
const base::Feature kFeatureWithAVeryLongNameThatWillHaveToWrap{
|
||||
"FeatureWithAVeryLongNameThatWillHaveToWrap",
|
||||
base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
""".split('\n')
|
||||
feature_file_parser = java_cpp_utils.CppConstantParser(
|
||||
java_cpp_features.FeatureParserDelegate(), test_data)
|
||||
features = feature_file_parser.Parse()
|
||||
self.assertEqual(4, len(features))
|
||||
self.assertEqual('SHORT', features[0].name)
|
||||
self.assertEqual('"Short"', features[0].value)
|
||||
self.assertEqual('TWO_LINE_FEATURE_A', features[1].name)
|
||||
self.assertEqual('"TwoLineFeatureA"', features[1].value)
|
||||
self.assertEqual('TWO_LINE_FEATURE_B', features[2].name)
|
||||
self.assertEqual('"TwoLineFeatureB"', features[2].value)
|
||||
self.assertEqual('FEATURE_WITH_A_VERY_LONG_NAME_THAT_WILL_HAVE_TO_WRAP',
|
||||
features[3].name)
|
||||
self.assertEqual('"FeatureWithAVeryLongNameThatWillHaveToWrap"',
|
||||
features[3].value)
|
||||
|
||||
def testCppSyntax(self):
|
||||
test_data = """
|
||||
// Mismatched name
|
||||
const base::Feature kMismatchedFeature{"MismatchedName",
|
||||
base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
|
||||
namespace myfeature {
|
||||
// In a namespace
|
||||
const base::Feature kSomeFeature{"SomeFeature",
|
||||
base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
}
|
||||
|
||||
// Defined with equals sign
|
||||
const base::Feature kFoo = {"Foo", base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
|
||||
// Build config-specific base::Feature
|
||||
#if defined(OS_ANDROID)
|
||||
const base::Feature kAndroidOnlyFeature{"AndroidOnlyFeature",
|
||||
base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
#endif
|
||||
|
||||
// Value depends on build config
|
||||
const base::Feature kMaybeEnabled{"MaybeEnabled",
|
||||
#if defined(OS_ANDROID)
|
||||
base::FEATURE_DISABLED_BY_DEFAULT
|
||||
#else
|
||||
base::FEATURE_ENABLED_BY_DEFAULT
|
||||
#endif
|
||||
};
|
||||
""".split('\n')
|
||||
feature_file_parser = java_cpp_utils.CppConstantParser(
|
||||
java_cpp_features.FeatureParserDelegate(), test_data)
|
||||
features = feature_file_parser.Parse()
|
||||
self.assertEqual(5, len(features))
|
||||
self.assertEqual('MISMATCHED_FEATURE', features[0].name)
|
||||
self.assertEqual('"MismatchedName"', features[0].value)
|
||||
self.assertEqual('SOME_FEATURE', features[1].name)
|
||||
self.assertEqual('"SomeFeature"', features[1].value)
|
||||
self.assertEqual('FOO', features[2].name)
|
||||
self.assertEqual('"Foo"', features[2].value)
|
||||
self.assertEqual('ANDROID_ONLY_FEATURE', features[3].name)
|
||||
self.assertEqual('"AndroidOnlyFeature"', features[3].value)
|
||||
self.assertEqual('MAYBE_ENABLED', features[4].name)
|
||||
self.assertEqual('"MaybeEnabled"', features[4].value)
|
||||
|
||||
def testNotYetSupported(self):
|
||||
# Negative test for cases we don't yet support, to ensure we don't misparse
|
||||
# these until we intentionally add proper support.
|
||||
test_data = """
|
||||
// Not currently supported: name depends on C++ directive
|
||||
const base::Feature kNameDependsOnOs{
|
||||
#if defined(OS_ANDROID)
|
||||
"MaybeName1",
|
||||
#else
|
||||
"MaybeName2",
|
||||
#endif
|
||||
base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
|
||||
// Not currently supported: feature named with a constant instead of literal
|
||||
const base::Feature kNamedAfterConstant{kNamedStringConstant,
|
||||
base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
""".split('\n')
|
||||
feature_file_parser = java_cpp_utils.CppConstantParser(
|
||||
java_cpp_features.FeatureParserDelegate(), test_data)
|
||||
features = feature_file_parser.Parse()
|
||||
self.assertEqual(0, len(features))
|
||||
|
||||
def testTreatWebViewLikeOneWord(self):
|
||||
test_data = """
|
||||
const base::Feature kSomeWebViewFeature{"SomeWebViewFeature",
|
||||
base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
const base::Feature kWebViewOtherFeature{"WebViewOtherFeature",
|
||||
base::FEATURE_ENABLED_BY_DEFAULT};
|
||||
const base::Feature kFeatureWithPluralWebViews{
|
||||
"FeatureWithPluralWebViews",
|
||||
base::FEATURE_ENABLED_BY_DEFAULT};
|
||||
""".split('\n')
|
||||
feature_file_parser = java_cpp_utils.CppConstantParser(
|
||||
java_cpp_features.FeatureParserDelegate(), test_data)
|
||||
features = feature_file_parser.Parse()
|
||||
self.assertEqual('SOME_WEBVIEW_FEATURE', features[0].name)
|
||||
self.assertEqual('"SomeWebViewFeature"', features[0].value)
|
||||
self.assertEqual('WEBVIEW_OTHER_FEATURE', features[1].name)
|
||||
self.assertEqual('"WebViewOtherFeature"', features[1].value)
|
||||
self.assertEqual('FEATURE_WITH_PLURAL_WEBVIEWS', features[2].name)
|
||||
self.assertEqual('"FeatureWithPluralWebViews"', features[2].value)
|
||||
|
||||
def testSpecialCharacters(self):
|
||||
test_data = r"""
|
||||
const base::Feature kFeatureWithEscapes{"Weird\tfeature\"name\n",
|
||||
base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
const base::Feature kFeatureWithEscapes2{
|
||||
"Weird\tfeature\"name\n",
|
||||
base::FEATURE_ENABLED_BY_DEFAULT};
|
||||
""".split('\n')
|
||||
feature_file_parser = java_cpp_utils.CppConstantParser(
|
||||
java_cpp_features.FeatureParserDelegate(), test_data)
|
||||
features = feature_file_parser.Parse()
|
||||
self.assertEqual('FEATURE_WITH_ESCAPES', features[0].name)
|
||||
self.assertEqual(r'"Weird\tfeature\"name\n"', features[0].value)
|
||||
self.assertEqual('FEATURE_WITH_ESCAPES2', features[1].name)
|
||||
self.assertEqual(r'"Weird\tfeature\"name\n"', features[1].value)
|
||||
|
||||
def testNoBaseNamespacePrefix(self):
|
||||
test_data = """
|
||||
const Feature kSomeFeature{"SomeFeature", FEATURE_DISABLED_BY_DEFAULT};
|
||||
""".split('\n')
|
||||
feature_file_parser = java_cpp_utils.CppConstantParser(
|
||||
java_cpp_features.FeatureParserDelegate(), test_data)
|
||||
features = feature_file_parser.Parse()
|
||||
self.assertEqual('SOME_FEATURE', features[0].name)
|
||||
self.assertEqual('"SomeFeature"', features[0].value)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -14,130 +14,20 @@ from util import build_utils
|
||||
from util import java_cpp_utils
|
||||
|
||||
|
||||
class _String(object):
|
||||
|
||||
def __init__(self, name, value, comments):
|
||||
self.name = java_cpp_utils.KCamelToShouty(name)
|
||||
self.value = value
|
||||
self.comments = '\n'.join(' ' + x for x in comments)
|
||||
|
||||
def Format(self):
|
||||
return '%s\n public static final String %s = %s;' % (
|
||||
self.comments, self.name, self.value)
|
||||
|
||||
|
||||
def ParseTemplateFile(lines):
|
||||
package_re = re.compile(r'^package (.*);')
|
||||
class_re = re.compile(r'.*class (.*) {')
|
||||
package = ''
|
||||
class_name = ''
|
||||
for line in lines:
|
||||
package_line = package_re.match(line)
|
||||
if package_line:
|
||||
package = package_line.groups()[0]
|
||||
class_line = class_re.match(line)
|
||||
if class_line:
|
||||
class_name = class_line.groups()[0]
|
||||
break
|
||||
return package, class_name
|
||||
|
||||
|
||||
# TODO(crbug.com/937282): It should be possible to parse a file for more than
|
||||
# string constants. However, this currently only handles extracting string
|
||||
# constants from a file (and all string constants from that file). Work will
|
||||
# be needed if we want to annotate specific constants or non string constants
|
||||
# in the file to be parsed.
|
||||
class StringFileParser(object):
|
||||
SINGLE_LINE_COMMENT_RE = re.compile(r'\s*(// [^\n]*)')
|
||||
class StringParserDelegate(java_cpp_utils.CppConstantParser.Delegate):
|
||||
STRING_RE = re.compile(r'\s*const char k(.*)\[\]\s*=')
|
||||
VALUE_RE = re.compile(r'\s*("(?:\"|[^"])*")\s*;')
|
||||
|
||||
def __init__(self, lines, path=''):
|
||||
self._lines = lines
|
||||
self._path = path
|
||||
self._in_string = False
|
||||
self._in_comment = False
|
||||
self._package = ''
|
||||
self._current_comments = []
|
||||
self._current_name = ''
|
||||
self._current_value = ''
|
||||
self._strings = []
|
||||
|
||||
def _ExtractVariable(self, line):
|
||||
match = StringFileParser.STRING_RE.match(line)
|
||||
def ExtractConstantName(self, line):
|
||||
match = StringParserDelegate.STRING_RE.match(line)
|
||||
return match.group(1) if match else None
|
||||
|
||||
def _ExtractValue(self, line):
|
||||
match = StringFileParser.VALUE_RE.search(line)
|
||||
def ExtractValue(self, line):
|
||||
match = StringParserDelegate.VALUE_RE.search(line)
|
||||
return match.group(1) if match else None
|
||||
|
||||
def _Reset(self):
|
||||
self._current_comments = []
|
||||
self._current_name = ''
|
||||
self._current_value = ''
|
||||
self._in_string = False
|
||||
self._in_comment = False
|
||||
|
||||
def _AppendString(self):
|
||||
self._strings.append(
|
||||
_String(self._current_name, self._current_value,
|
||||
self._current_comments))
|
||||
self._Reset()
|
||||
|
||||
def _ParseValue(self, line):
|
||||
current_value = self._ExtractValue(line)
|
||||
if current_value is not None:
|
||||
self._current_value = current_value
|
||||
self._AppendString()
|
||||
else:
|
||||
self._Reset()
|
||||
|
||||
def _ParseComment(self, line):
|
||||
comment_line = StringFileParser.SINGLE_LINE_COMMENT_RE.match(line)
|
||||
if comment_line:
|
||||
self._current_comments.append(comment_line.groups()[0])
|
||||
self._in_comment = True
|
||||
self._in_string = True
|
||||
return True
|
||||
else:
|
||||
self._in_comment = False
|
||||
return False
|
||||
|
||||
def _ParseString(self, line):
|
||||
current_name = self._ExtractVariable(line)
|
||||
if current_name is not None:
|
||||
self._current_name = current_name
|
||||
current_value = self._ExtractValue(line)
|
||||
if current_value is not None:
|
||||
self._current_value = current_value
|
||||
self._AppendString()
|
||||
else:
|
||||
self._in_string = True
|
||||
return True
|
||||
else:
|
||||
self._in_string = False
|
||||
return False
|
||||
|
||||
def _ParseLine(self, line):
|
||||
if not self._in_string:
|
||||
if not self._ParseString(line):
|
||||
self._ParseComment(line)
|
||||
return
|
||||
|
||||
if self._in_comment:
|
||||
if self._ParseComment(line):
|
||||
return
|
||||
if not self._ParseString(line):
|
||||
self._Reset()
|
||||
return
|
||||
|
||||
if self._in_string:
|
||||
self._ParseValue(line)
|
||||
|
||||
def Parse(self):
|
||||
for line in self._lines:
|
||||
self._ParseLine(line)
|
||||
return self._strings
|
||||
def CreateJavaConstant(self, name, value, comments):
|
||||
return java_cpp_utils.JavaString(name, value, comments)
|
||||
|
||||
|
||||
def _GenerateOutput(template, source_paths, template_path, strings):
|
||||
@ -166,7 +56,9 @@ def _GenerateOutput(template, source_paths, template_path, strings):
|
||||
|
||||
def _ParseStringFile(path):
|
||||
with open(path) as f:
|
||||
return StringFileParser(f.readlines(), path).Parse()
|
||||
string_file_parser = java_cpp_utils.CppConstantParser(
|
||||
StringParserDelegate(), f.readlines())
|
||||
return string_file_parser.Parse()
|
||||
|
||||
|
||||
def _Generate(source_paths, template_path):
|
||||
@ -174,10 +66,8 @@ def _Generate(source_paths, template_path):
|
||||
lines = f.readlines()
|
||||
|
||||
template = ''.join(lines)
|
||||
package, class_name = ParseTemplateFile(lines)
|
||||
package_path = package.replace('.', os.path.sep)
|
||||
file_name = class_name + '.java'
|
||||
output_path = os.path.join(package_path, file_name)
|
||||
package, class_name = java_cpp_utils.ParseTemplateFile(lines)
|
||||
output_path = java_cpp_utils.GetJavaFilePath(package, class_name)
|
||||
strings = []
|
||||
for source_path in source_paths:
|
||||
strings.extend(_ParseStringFile(source_path))
|
||||
@ -189,17 +79,15 @@ def _Generate(source_paths, template_path):
|
||||
def _Main(argv):
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
'--srcjar',
|
||||
required=True,
|
||||
help='When specified, a .srcjar at the given path is '
|
||||
'created instead of individual .java files.')
|
||||
parser.add_argument('--srcjar',
|
||||
required=True,
|
||||
help='The path at which to generate the .srcjar file')
|
||||
|
||||
parser.add_argument(
|
||||
'--template',
|
||||
required=True,
|
||||
help='Can be used to provide a context into which the'
|
||||
'new string constants will be inserted.')
|
||||
parser.add_argument('--template',
|
||||
required=True,
|
||||
help='The template file with which to generate the Java '
|
||||
'class. Must have "{NATIVE_STRINGS}" somewhere in '
|
||||
'the template.')
|
||||
|
||||
parser.add_argument(
|
||||
'inputs', nargs='+', help='Input file(s)', metavar='INPUTFILE')
|
||||
|
@ -12,6 +12,7 @@ This test suite contains various tests for the C++ -> Java string generator.
|
||||
import unittest
|
||||
|
||||
import java_cpp_strings
|
||||
from util import java_cpp_utils
|
||||
|
||||
|
||||
class _TestStringsParser(unittest.TestCase):
|
||||
@ -36,7 +37,9 @@ const char kAnotherSwitch[] = "another-value";
|
||||
|
||||
// Comment followed by nothing.
|
||||
""".split('\n')
|
||||
strings = java_cpp_strings.StringFileParser(test_data).Parse()
|
||||
string_file_parser = java_cpp_utils.CppConstantParser(
|
||||
java_cpp_strings.StringParserDelegate(), test_data)
|
||||
strings = string_file_parser.Parse()
|
||||
self.assertEqual(2, len(strings))
|
||||
self.assertEqual('A_SWITCH', strings[0].name)
|
||||
self.assertEqual('"a-value"', strings[0].value)
|
||||
@ -82,7 +85,9 @@ const char kConcatenateMultipleStringLiterals[] =
|
||||
"first line"
|
||||
"second line";
|
||||
""".split('\n')
|
||||
strings = java_cpp_strings.StringFileParser(test_data).Parse()
|
||||
string_file_parser = java_cpp_utils.CppConstantParser(
|
||||
java_cpp_strings.StringParserDelegate(), test_data)
|
||||
strings = string_file_parser.Parse()
|
||||
self.assertEqual(9, len(strings))
|
||||
self.assertEqual('A_STRING', strings[0].name)
|
||||
self.assertEqual('"a-value"', strings[0].value)
|
||||
@ -113,7 +118,9 @@ const char kSomeWebViewSwitch[] = "some-webview-switch";
|
||||
const char kWebViewOtherSwitch[] = "webview-other-switch";
|
||||
const char kSwitchWithPluralWebViews[] = "switch-with-plural-webviews";
|
||||
""".split('\n')
|
||||
strings = java_cpp_strings.StringFileParser(test_data).Parse()
|
||||
string_file_parser = java_cpp_utils.CppConstantParser(
|
||||
java_cpp_strings.StringParserDelegate(), test_data)
|
||||
strings = string_file_parser.Parse()
|
||||
self.assertEqual('SOME_WEBVIEW_SWITCH', strings[0].name)
|
||||
self.assertEqual('"some-webview-switch"', strings[0].value)
|
||||
self.assertEqual('WEBVIEW_OTHER_SWITCH', strings[1].name)
|
||||
@ -123,16 +130,10 @@ const char kSwitchWithPluralWebViews[] = "switch-with-plural-webviews";
|
||||
|
||||
def testTemplateParsing(self):
|
||||
test_data = """
|
||||
// Copyright {YEAR} The Chromium Authors. All rights reserved.
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// This file is autogenerated by
|
||||
// {SCRIPT_NAME}
|
||||
// From
|
||||
// {SOURCE_PATH}, and
|
||||
// {TEMPLATE_PATH}
|
||||
|
||||
package my.java.package;
|
||||
|
||||
public any sort of class MyClass {{
|
||||
@ -141,7 +142,7 @@ public any sort of class MyClass {{
|
||||
|
||||
}}
|
||||
""".split('\n')
|
||||
package, class_name = java_cpp_strings.ParseTemplateFile(test_data)
|
||||
package, class_name = java_cpp_utils.ParseTemplateFile(test_data)
|
||||
self.assertEqual('my.java.package', package)
|
||||
self.assertEqual('MyClass', class_name)
|
||||
|
||||
|
@ -13,6 +13,12 @@ def GetScriptName():
|
||||
return os.path.basename(os.path.abspath(sys.argv[0]))
|
||||
|
||||
|
||||
def GetJavaFilePath(java_package, class_name):
|
||||
package_path = java_package.replace('.', os.path.sep)
|
||||
file_name = class_name + '.java'
|
||||
return os.path.join(package_path, file_name)
|
||||
|
||||
|
||||
def KCamelToShouty(s):
|
||||
"""Convert |s| from kCamelCase or CamelCase to SHOUTY_CASE.
|
||||
|
||||
@ -32,3 +38,159 @@ def KCamelToShouty(s):
|
||||
# Add _ between lower -> upper transitions.
|
||||
s = re.sub(r'([^A-Z_0-9])([A-Z])', r'\1_\2', s)
|
||||
return s.upper()
|
||||
|
||||
|
||||
class JavaString(object):
|
||||
def __init__(self, name, value, comments):
|
||||
self.name = KCamelToShouty(name)
|
||||
self.value = value
|
||||
self.comments = '\n'.join(' ' + x for x in comments)
|
||||
|
||||
def Format(self):
|
||||
return '%s\n public static final String %s = %s;' % (
|
||||
self.comments, self.name, self.value)
|
||||
|
||||
|
||||
def ParseTemplateFile(lines):
|
||||
package_re = re.compile(r'^package (.*);')
|
||||
class_re = re.compile(r'.*class (.*) {')
|
||||
package = ''
|
||||
class_name = ''
|
||||
for line in lines:
|
||||
package_line = package_re.match(line)
|
||||
if package_line:
|
||||
package = package_line.groups()[0]
|
||||
class_line = class_re.match(line)
|
||||
if class_line:
|
||||
class_name = class_line.groups()[0]
|
||||
break
|
||||
return package, class_name
|
||||
|
||||
|
||||
# TODO(crbug.com/937282): Work will be needed if we want to annotate specific
|
||||
# constants in the file to be parsed.
|
||||
class CppConstantParser(object):
|
||||
"""Parses C++ constants, retaining their comments.
|
||||
|
||||
The Delegate subclass is responsible for matching and extracting the
|
||||
constant's variable name and value, as well as generating an object to
|
||||
represent the Java representation of this value.
|
||||
"""
|
||||
SINGLE_LINE_COMMENT_RE = re.compile(r'\s*(// [^\n]*)')
|
||||
|
||||
class Delegate(object):
|
||||
def ExtractConstantName(self, line):
|
||||
"""Extracts a constant's name from line or None if not a match."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def ExtractValue(self, line):
|
||||
"""Extracts a constant's value from line or None if not a match."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def CreateJavaConstant(self, name, value, comments):
|
||||
"""Creates an object representing the Java analog of a C++ constant.
|
||||
|
||||
CppConstantParser will not interact with the object created by this
|
||||
method. Instead, it will store this value in a list and return a list of
|
||||
all objects from the Parse() method. In this way, the caller may define
|
||||
whatever class suits their need.
|
||||
|
||||
Args:
|
||||
name: the constant's variable name, as extracted by
|
||||
ExtractConstantName()
|
||||
value: the constant's value, as extracted by ExtractValue()
|
||||
comments: the code comments describing this constant
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def __init__(self, delegate, lines):
|
||||
self._delegate = delegate
|
||||
self._lines = lines
|
||||
self._in_variable = False
|
||||
self._in_comment = False
|
||||
self._package = ''
|
||||
self._current_comments = []
|
||||
self._current_name = ''
|
||||
self._current_value = ''
|
||||
self._constants = []
|
||||
|
||||
def _ExtractVariable(self, line):
|
||||
match = StringFileParser.STRING_RE.match(line)
|
||||
return match.group(1) if match else None
|
||||
|
||||
def _ExtractValue(self, line):
|
||||
match = StringFileParser.VALUE_RE.search(line)
|
||||
return match.group(1) if match else None
|
||||
|
||||
def _Reset(self):
|
||||
self._current_comments = []
|
||||
self._current_name = ''
|
||||
self._current_value = ''
|
||||
self._in_variable = False
|
||||
self._in_comment = False
|
||||
|
||||
def _AppendConstant(self):
|
||||
self._constants.append(
|
||||
self._delegate.CreateJavaConstant(self._current_name,
|
||||
self._current_value,
|
||||
self._current_comments))
|
||||
self._Reset()
|
||||
|
||||
def _ParseValue(self, line):
|
||||
current_value = self._delegate.ExtractValue(line)
|
||||
if current_value is not None:
|
||||
self._current_value = current_value
|
||||
self._AppendConstant()
|
||||
else:
|
||||
self._Reset()
|
||||
|
||||
def _ParseComment(self, line):
|
||||
comment_line = CppConstantParser.SINGLE_LINE_COMMENT_RE.match(line)
|
||||
if comment_line:
|
||||
self._current_comments.append(comment_line.groups()[0])
|
||||
self._in_comment = True
|
||||
self._in_variable = True
|
||||
return True
|
||||
else:
|
||||
self._in_comment = False
|
||||
return False
|
||||
|
||||
def _ParseVariable(self, line):
|
||||
current_name = self._delegate.ExtractConstantName(line)
|
||||
if current_name is not None:
|
||||
self._current_name = current_name
|
||||
current_value = self._delegate.ExtractValue(line)
|
||||
if current_value is not None:
|
||||
self._current_value = current_value
|
||||
self._AppendConstant()
|
||||
else:
|
||||
self._in_variable = True
|
||||
return True
|
||||
else:
|
||||
self._in_variable = False
|
||||
return False
|
||||
|
||||
def _ParseLine(self, line):
|
||||
if not self._in_variable:
|
||||
if not self._ParseVariable(line):
|
||||
self._ParseComment(line)
|
||||
return
|
||||
|
||||
if self._in_comment:
|
||||
if self._ParseComment(line):
|
||||
return
|
||||
if not self._ParseVariable(line):
|
||||
self._Reset()
|
||||
return
|
||||
|
||||
if self._in_variable:
|
||||
self._ParseValue(line)
|
||||
|
||||
def Parse(self):
|
||||
"""Returns a list of objects representing C++ constants.
|
||||
|
||||
Each object in the list was created by Delegate.CreateJavaValue().
|
||||
"""
|
||||
for line in self._lines:
|
||||
self._ParseLine(line)
|
||||
return self._constants
|
||||
|
@ -607,6 +607,81 @@ if (enable_java_templates) {
|
||||
}
|
||||
}
|
||||
|
||||
# Declare a target for generating Java classes with string constants matching
|
||||
# those found in C++ base::Feature declarations, using a python script.
|
||||
#
|
||||
# This target will create a single .srcjar. Adding this target to an
|
||||
# android_library target's srcjar_deps will make the generated java files be
|
||||
# included in that library's final outputs.
|
||||
#
|
||||
# Variables
|
||||
# sources: list of files to be processed by the script. For each
|
||||
# base::Feature in the source files, the script will add a
|
||||
# corresponding Java string for that feature's name to the
|
||||
# specified template file.
|
||||
# Example
|
||||
# java_cpp_features("foo_features") {
|
||||
# sources = [
|
||||
# "src/foo_features.cc",
|
||||
# ]
|
||||
# template = "src/templates/FooFeatures.java.tmpl
|
||||
# }
|
||||
#
|
||||
# foo_features.cc:
|
||||
#
|
||||
# // A feature.
|
||||
# const base::Feature kSomeFeature{"SomeFeature",
|
||||
# base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
#
|
||||
# FooFeatures.java.tmpl
|
||||
#
|
||||
# // Copyright $YEAR The Chromium Authors. All rights reserved.
|
||||
# // Use of this source code is governed by a BSD-style license that can be
|
||||
# // found in the LICENSE file.
|
||||
#
|
||||
# package my.java.package;
|
||||
#
|
||||
# public final class FooFeatures {{
|
||||
# // ...snip...
|
||||
# {NATIVE_STRINGS}
|
||||
# // ...snip...
|
||||
# // Do not instantiate this class.
|
||||
# private FooFeatures() {{}}
|
||||
# }}
|
||||
#
|
||||
# result:
|
||||
# A FooFeatures.java file, defining a class named FooFeatures in the package
|
||||
# my.java.package.
|
||||
template("java_cpp_features") {
|
||||
set_sources_assignment_filter([])
|
||||
action_with_pydeps(target_name) {
|
||||
forward_variables_from(invoker,
|
||||
[
|
||||
"sources",
|
||||
"testonly",
|
||||
"visibility",
|
||||
])
|
||||
|
||||
# The sources aren't compiled so don't check their dependencies.
|
||||
check_includes = false
|
||||
script = "//build/android/gyp/java_cpp_features.py"
|
||||
|
||||
_srcjar_path = "${target_gen_dir}/${target_name}.srcjar"
|
||||
_rebased_srcjar_path = rebase_path(_srcjar_path, root_build_dir)
|
||||
_rebased_sources = rebase_path(invoker.sources, root_build_dir)
|
||||
_rebased_template = rebase_path(invoker.template, root_build_dir)
|
||||
|
||||
args = [
|
||||
"--srcjar=$_rebased_srcjar_path",
|
||||
"--template=$_rebased_template",
|
||||
]
|
||||
args += _rebased_sources
|
||||
sources += [ invoker.template ]
|
||||
|
||||
outputs = [ _srcjar_path ]
|
||||
}
|
||||
}
|
||||
|
||||
# Declare a target for processing a Jinja template.
|
||||
#
|
||||
# Variables
|
||||
|
@ -146,6 +146,10 @@ class
|
||||
}
|
||||
```
|
||||
|
||||
## See also
|
||||
* [Accessing C++ Switches In Java](android_accessing_cpp_switches_in_java.md)
|
||||
* [Accessing C++ Features In Java](android_accessing_cpp_features_in_java.md)
|
||||
|
||||
## Code
|
||||
* [Generator
|
||||
code](https://cs.chromium.org/chromium/src/build/android/gyp/java_cpp_enum.py?dr=C&sq=package:chromium)
|
||||
|
172
docs/android_accessing_cpp_features_in_java.md
Normal file
172
docs/android_accessing_cpp_features_in_java.md
Normal file
@ -0,0 +1,172 @@
|
||||
# Accessing C++ Features In Java
|
||||
|
||||
[TOC]
|
||||
|
||||
## Introduction
|
||||
|
||||
Accessing C++ `base::Features` in Java is implemented via a Python script which
|
||||
analyzes the `*_features.cc` file and generates the corresponding Java class,
|
||||
based on a template file. The template file must be specified in the GN target.
|
||||
This outputs Java String constants which represent the name of the
|
||||
`base::Feature`.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Create a template file (ex. `FooFeatures.java.tmpl`). Change "Copyright
|
||||
2020" to be whatever the year is at the time of writing (as you would for any
|
||||
other file).
|
||||
```java
|
||||
// Copyright 2020 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package org.chromium.foo;
|
||||
|
||||
// Be sure to escape any curly braces in your template by doubling as
|
||||
// follows.
|
||||
/**
|
||||
* Contains features that are specific to the foo project.
|
||||
*/
|
||||
public final class FooFeatures {{
|
||||
|
||||
{NATIVE_FEATURES}
|
||||
|
||||
// Prevents instantiation.
|
||||
private FooFeatures() {{}}
|
||||
}}
|
||||
```
|
||||
|
||||
2. Add a new build target and add it to the `srcjar_deps` of an
|
||||
`android_library` target:
|
||||
|
||||
```gn
|
||||
if (is_android) {
|
||||
import("//build/config/android/rules.gni")
|
||||
}
|
||||
|
||||
if (is_android) {
|
||||
java_cpp_features("java_features_srcjar") {
|
||||
# External code should depend on ":foo_java" instead.
|
||||
visibility = [ ":*" ]
|
||||
sources = [
|
||||
"//base/android/foo_features.cc",
|
||||
]
|
||||
template = "//base/android/java_templates/FooFeatures.java.tmpl"
|
||||
}
|
||||
|
||||
# If there's already an android_library target, you can add
|
||||
# java_features_srcjar to that target's srcjar_deps. Otherwise, the best
|
||||
# practice is to create a new android_library just for this target.
|
||||
android_library("foo_java") {
|
||||
srcjar_deps = [ ":java_features_srcjar" ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. The generated file `out/Default/gen/.../org/chromium/foo/FooFeatures.java`
|
||||
would contain:
|
||||
|
||||
```java
|
||||
// Copyright $YEAR The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package org.chromium.foo;
|
||||
|
||||
// Be sure to escape any curly braces in your template by doubling as
|
||||
// follows.
|
||||
/**
|
||||
* Contains features that are specific to the foo project.
|
||||
*/
|
||||
public final class FooFeatures {
|
||||
|
||||
// This following string constants were inserted by
|
||||
// java_cpp_features.py
|
||||
// From
|
||||
// ../../base/android/foo_features.cc
|
||||
// Into
|
||||
// ../../base/android/java_templates/FooFeatures.java.tmpl
|
||||
|
||||
// Documentation for the C++ Feature is copied here.
|
||||
public static final String SOME_FEATURE = "SomeFeature";
|
||||
|
||||
// ...snip...
|
||||
|
||||
// Prevents instantiation.
|
||||
private FooFeatures() {}
|
||||
}
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
The script only supports limited syntaxes for declaring C++ base::Features. You
|
||||
may see an error like the following during compilation:
|
||||
|
||||
```
|
||||
...
|
||||
org/chromium/foo/FooFeatures.java:41: error: duplicate declaration of field: MY_FEATURE
|
||||
public static final String MY_FEATURE = "MyFeature";
|
||||
```
|
||||
|
||||
This can happen if you've re-declared a feature for mutually-exclsuive build
|
||||
configs (ex. the feature is enabled-by-default for one config, but
|
||||
disabled-by-default for another). Example:
|
||||
|
||||
```c++
|
||||
#if defined(...)
|
||||
const base::Feature kMyFeature{
|
||||
"MyFeature", base::FEATURE_ENABLED_BY_DEFAULT};
|
||||
#else
|
||||
const base::Feature kMyFeature{
|
||||
"MyFeature", base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
#endif
|
||||
```
|
||||
|
||||
The `java_cpp_features` rule doesn't know how to evaluate C++ preprocessor
|
||||
directives, so it generates two identical Java fields (which is what the
|
||||
compilation error is complaining about). Fortunately, the workaround is fairly
|
||||
simple. Rewrite the definition to only use directives around the enabled state:
|
||||
|
||||
```c++
|
||||
const base::Feature kMyFeature{
|
||||
"MyFeature",
|
||||
#if defined(...)
|
||||
base::FEATURE_ENABLED_BY_DEFAULT
|
||||
#else
|
||||
base::FEATURE_DISABLED_BY_DEFAULT
|
||||
#endif
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Checking if a Feature is enabled
|
||||
|
||||
The standard pattern is to create a `FooFeatureList.java` class with an
|
||||
`isEnabled()` method (ex.
|
||||
[`ContentFeatureList`](/content/public/android/java/src/org/chromium/content_public/browser/ContentFeatureList.java)).
|
||||
This should call into C++ (ex.
|
||||
[`content_feature_list`](/content/browser/android/content_feature_list.cc)),
|
||||
where a subset of features are exposed via the `kFeaturesExposedToJava` array.
|
||||
You can either add your `base::Feature` to an existing `feature_list` or create
|
||||
a new `FeatureList` class if no existing one is suitable. Then you can check the
|
||||
enabled state like so:
|
||||
|
||||
```java
|
||||
// It's OK if ContentFeatureList checks FooFeatures.*, so long as
|
||||
// content_feature_list.cc exposes `kMyFeature`.
|
||||
if (ContentFeatureList.isEnabled(FooFeatures.MY_FEATURE)) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
At the moment, `base::Features` must be explicitly exposed to Java this way, in
|
||||
whichever layer needs to access their state. See https://crbug.com/1060097.
|
||||
|
||||
## See also
|
||||
* [Accessing C++ Enums In Java](android_accessing_cpp_enums_in_java.md)
|
||||
* [Accessing C++ Switches In Java](android_accessing_cpp_switches_in_java.md)
|
||||
|
||||
## Code
|
||||
* [Generator code](/build/android/gyp/java_cpp_features.py) and
|
||||
[Tests](/build/android/gyp/java_cpp_features_tests.py)
|
||||
* [GN template](/build/config/android/rules.gni)
|
@ -10,9 +10,11 @@ template file. The template file must be specified in the GN target.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Create a template file (ex. `FooSwitches.java.tmpl`)
|
||||
1. Create a template file (ex. `FooSwitches.java.tmpl`). Change "Copyright
|
||||
2020" to be whatever the year is at the time of writing (as you would for any
|
||||
other file).
|
||||
```java
|
||||
// Copyright $YEAR The Chromium Authors. All rights reserved.
|
||||
// Copyright 2020 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
@ -93,6 +95,10 @@ template file. The template file must be specified in the GN target.
|
||||
}
|
||||
```
|
||||
|
||||
## See also
|
||||
* [Accessing C++ Enums In Java](android_accessing_cpp_enums_in_java.md)
|
||||
* [Accessing C++ Features In Java](android_accessing_cpp_features_in_java.md)
|
||||
|
||||
## Code
|
||||
* [Generator
|
||||
code](https://cs.chromium.org/chromium/src/build/android/gyp/java_cpp_strings.py?dr=C&sq=package:chromium)
|
||||
|
Reference in New Issue
Block a user