You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
496 lines
17 KiB
496 lines
17 KiB
#!/usr/bin/python
|
|
|
|
# Copyright 2016 Google LLC
|
|
#
|
|
# 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.
|
|
|
|
"""Stand-alone implementation of the Gradle Firebase plugin.
|
|
|
|
Converts the services json file to xml:
|
|
https://googleplex-android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/google-services/src/main/groovy/com/google/gms/googleservices
|
|
"""
|
|
|
|
__author__ = 'Wouter van Oortmerssen'
|
|
|
|
import argparse
|
|
import ctypes
|
|
import json
|
|
import os
|
|
import platform
|
|
import sys
|
|
from xml.etree import ElementTree
|
|
|
|
if platform.system().lower() == 'windows':
|
|
import ctypes.wintypes # pylint: disable=g-import-not-at-top
|
|
|
|
# Map Python 2's unicode method to encode a string as bytes in python 3.
|
|
try:
|
|
unicode('') # See whether unicode class is available (Python < 3)
|
|
except NameError:
|
|
unicode = str # pylint: disable=redefined-builtin,invalid-name
|
|
|
|
# Input filename if it isn't set.
|
|
DEFAULT_INPUT_FILENAME = 'app/google-services.json'
|
|
# Output filename if it isn't set.
|
|
DEFAULT_OUTPUT_FILENAME = 'res/values/googleservices.xml'
|
|
# Input filename for .plist files, if it isn't set.
|
|
DEFAULT_PLIST_INPUT_FILENAME = 'GoogleService-Info.plist'
|
|
# Output filename for .json files, if it isn't set.
|
|
DEFAULT_JSON_OUTPUT_FILENAME = 'google-services-desktop.json'
|
|
|
|
OAUTH_CLIENT_TYPE_ANDROID_APP = 1
|
|
OAUTH_CLIENT_TYPE_WEB = 3
|
|
|
|
|
|
def read_xml_value(xml_node):
|
|
"""Utility method for reading values from the plist XML.
|
|
|
|
Args:
|
|
xml_node: An ElementTree node, that contains a value.
|
|
|
|
Returns:
|
|
The value of the node, or None, if it could not be read.
|
|
"""
|
|
if xml_node.tag == 'string':
|
|
return xml_node.text
|
|
elif xml_node.tag == 'integer':
|
|
return int(xml_node.text)
|
|
elif xml_node.tag == 'real':
|
|
return float(xml_node.text)
|
|
elif xml_node.tag == 'false':
|
|
return 0
|
|
elif xml_node.tag == 'true':
|
|
return 1
|
|
else:
|
|
# other types of input are ignored. (data, dates, arrays, etc.)
|
|
return None
|
|
|
|
|
|
def construct_plist_dictionary(xml_root):
|
|
"""Constructs a dictionary of values based on the contents of a plist file.
|
|
|
|
Args:
|
|
xml_root: An ElementTree node, that represents the root of the xml file
|
|
that is to be parsed. (Which should be a dictionary containing
|
|
key-value pairs of the properties that need to be extracted.)
|
|
|
|
Returns:
|
|
A dictionary, containing key-value pairs for all (supported) entries in the
|
|
node.
|
|
"""
|
|
xml_dict = xml_root.find('dict')
|
|
|
|
if xml_dict is None:
|
|
return None
|
|
|
|
plist_dict = {}
|
|
i = 0
|
|
while i < len(xml_dict):
|
|
if xml_dict[i].tag == 'key':
|
|
key = xml_dict[i].text
|
|
i += 1
|
|
if i < len(xml_dict):
|
|
value = read_xml_value(xml_dict[i])
|
|
if value is not None:
|
|
plist_dict[key] = value
|
|
i += 1
|
|
|
|
return plist_dict
|
|
|
|
|
|
def update_dict_keys(key_map, input_dict):
|
|
"""Creates a dict from input_dict with the same values but new keys.
|
|
|
|
Two dictionaries are passed to this function: the key_map that represents a
|
|
mapping of source keys to destination keys, and the input_dict that is the
|
|
dictionary that is to be duplicated, replacing any key that matches a source
|
|
key with a destination key. Source keys that are not present in the
|
|
input_dict will not have their destination key represented in the result.
|
|
|
|
In other words, if key_map is `{'old': 'new', 'foo': 'bar'}`, and input_dict
|
|
is `{'old': 10}`, the result will be `{'new': 10}`.
|
|
|
|
Args:
|
|
key_map (dict): A dictionary of strings to strings that maps source keys to
|
|
destination keys.
|
|
input_dict (dict): The dictionary of string keys to any value type, which
|
|
is to be duplicated, replacing source keys with the corresponding
|
|
destination keys from key_map.
|
|
|
|
Returns:
|
|
dict: A new dictionary with updated keys.
|
|
"""
|
|
return {
|
|
new_key: input_dict[old_key]
|
|
for (old_key, new_key) in key_map.items()
|
|
if old_key in input_dict
|
|
}
|
|
|
|
|
|
def construct_google_services_json(xml_dict):
|
|
"""Constructs a google services json file from a dictionary.
|
|
|
|
Args:
|
|
xml_dict: A dictionary of all the key/value pairs that are needed for the
|
|
output json file.
|
|
Returns:
|
|
A string representing the output json file.
|
|
"""
|
|
|
|
try:
|
|
json_struct = {
|
|
'project_info':
|
|
update_dict_keys(
|
|
{
|
|
'GCM_SENDER_ID': 'project_number',
|
|
'DATABASE_URL': 'firebase_url',
|
|
'PROJECT_ID': 'project_id',
|
|
'STORAGE_BUCKET': 'storage_bucket'
|
|
}, xml_dict),
|
|
'client': [{
|
|
'client_info': {
|
|
'mobilesdk_app_id': xml_dict['GOOGLE_APP_ID'],
|
|
'android_client_info': {
|
|
'package_name': xml_dict['BUNDLE_ID']
|
|
}
|
|
},
|
|
'oauth_client': [{
|
|
'client_id': xml_dict['CLIENT_ID'],
|
|
}],
|
|
'api_key': [{
|
|
'current_key': xml_dict['API_KEY']
|
|
}],
|
|
'services': {
|
|
'analytics_service': {
|
|
'status': xml_dict['IS_ANALYTICS_ENABLED']
|
|
},
|
|
'appinvite_service': {
|
|
'status': xml_dict['IS_APPINVITE_ENABLED']
|
|
}
|
|
}
|
|
},],
|
|
'configuration_version':
|
|
'1'
|
|
}
|
|
return json.dumps(json_struct, indent=2)
|
|
except KeyError as e:
|
|
sys.stderr.write('Could not find key in plist file: [%s]\n' % (e.args[0]))
|
|
return None
|
|
|
|
|
|
def convert_plist_to_json(plist_string, input_filename):
|
|
"""Converts an input plist string into a .json file and saves it.
|
|
|
|
Args:
|
|
plist_string: The contents of the loaded plist file.
|
|
|
|
input_filename: The file name that the plist data was read from.
|
|
Returns:
|
|
the converted string, or None if there were errors.
|
|
"""
|
|
|
|
try:
|
|
root = ElementTree.fromstring(plist_string)
|
|
except ElementTree.ParseError:
|
|
sys.stderr.write('Error parsing file %s.\n'
|
|
'It does not appear to be valid XML.\n' % (input_filename))
|
|
return None
|
|
|
|
plist_dict = construct_plist_dictionary(root)
|
|
if plist_dict is None:
|
|
sys.stderr.write('In file %s, could not locate a top-level \'dict\' '
|
|
'element.\n'
|
|
'File format should be plist XML, with a top-level '
|
|
'dictionary containing project settings as key-value '
|
|
'pairs.\n' % (input_filename))
|
|
return None
|
|
|
|
json_string = construct_google_services_json(plist_dict)
|
|
return json_string
|
|
|
|
|
|
def gen_string(parent, name, text):
|
|
"""Generate one <string /> element and put into the list of keeps.
|
|
|
|
Args:
|
|
parent: The object that will hold the string.
|
|
name: The name to store the string under.
|
|
text: The text of the string.
|
|
"""
|
|
if text:
|
|
prev = parent.get('tools:keep', '')
|
|
if prev:
|
|
prev += ','
|
|
parent.set('tools:keep', prev + '@string/' + name)
|
|
child = ElementTree.SubElement(parent, 'string', {
|
|
'name': name,
|
|
'translatable': 'false'
|
|
})
|
|
child.text = text
|
|
|
|
|
|
def indent(elem, level=0):
|
|
"""Recurse through XML tree and add indentation.
|
|
|
|
Args:
|
|
elem: The element to recurse over
|
|
level: The current indentation level.
|
|
"""
|
|
i = '\n' + level*' '
|
|
if elem is not None:
|
|
if not elem.text or not elem.text.strip():
|
|
elem.text = i + ' '
|
|
if not elem.tail or not elem.tail.strip():
|
|
elem.tail = i
|
|
for elem in elem:
|
|
indent(elem, level+1)
|
|
if not elem.tail or not elem.tail.strip():
|
|
elem.tail = i
|
|
else:
|
|
if level and (not elem.tail or not elem.tail.strip()):
|
|
elem.tail = i
|
|
|
|
|
|
def argv_as_unicode_win32():
|
|
"""Returns unicode command line arguments on windows.
|
|
"""
|
|
|
|
get_command_line_w = ctypes.cdll.kernel32.GetCommandLineW
|
|
get_command_line_w.restype = ctypes.wintypes.LPCWSTR
|
|
|
|
# CommandLineToArgvW parses the Unicode command line
|
|
command_line_to_argv_w = ctypes.windll.shell32.CommandLineToArgvW
|
|
command_line_to_argv_w.argtypes = [
|
|
ctypes.wintypes.LPCWSTR,
|
|
ctypes.POINTER(ctypes.c_int)
|
|
]
|
|
command_line_to_argv_w.restype = ctypes.POINTER(
|
|
ctypes.wintypes.LPWSTR)
|
|
|
|
argc = ctypes.c_int(0)
|
|
argv = command_line_to_argv_w(get_command_line_w(), argc)
|
|
|
|
# Strip the python executable from the arguments if it exists
|
|
# (It would be listed as the first argument on the windows command line, but
|
|
# not in the arguments to the python script)
|
|
sys_argv_len = len(sys.argv)
|
|
return [unicode(argv[i]) for i in
|
|
range(argc.value - sys_argv_len, argc.value)]
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description=((
|
|
'Converts a Firebase %s into %s similar to the Gradle plugin, or '
|
|
'converts a Firebase %s into a %s suitible for use on desktop apps.' %
|
|
(DEFAULT_INPUT_FILENAME, DEFAULT_OUTPUT_FILENAME,
|
|
DEFAULT_PLIST_INPUT_FILENAME, DEFAULT_JSON_OUTPUT_FILENAME))))
|
|
parser.add_argument('-i', help='Override input file name',
|
|
metavar='FILE', required=False)
|
|
parser.add_argument('-o', help='Override destination file name',
|
|
metavar='FILE', required=False)
|
|
parser.add_argument('-p', help=('Package ID to select within the set of '
|
|
'packages in the input file. If this is '
|
|
'not specified, the first package in the '
|
|
'input file is selected.'))
|
|
parser.add_argument('-l', help=('List all package IDs referenced by the '
|
|
'input file. If this is specified, '
|
|
'the output file is not created.'),
|
|
action='store_true', default=False, required=False)
|
|
parser.add_argument('-f', help=('Print project fields from the input file '
|
|
'in the form \'name=value\\n\' for each '
|
|
'field. If this is specified, the output '
|
|
'is not created.'),
|
|
action='store_true', default=False, required=False)
|
|
parser.add_argument(
|
|
'--plist',
|
|
help=(
|
|
'Specifies a plist file to convert to a JSON configuration file. '
|
|
'If this is enabled, the script will expect a .plist file as input, '
|
|
'which it will convert into %s file. The output file is '
|
|
'*not* suitable for use with Firebase on Android.' %
|
|
(DEFAULT_JSON_OUTPUT_FILENAME)),
|
|
action='store_true',
|
|
default=False,
|
|
required=False)
|
|
|
|
# python 2 on Windows doesn't handle unicode arguments well, so we need to
|
|
# pre-process the command line arguments before trying to parse them.
|
|
if platform.system() == 'Windows':
|
|
sys.argv = argv_as_unicode_win32()
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.plist:
|
|
input_filename = DEFAULT_PLIST_INPUT_FILENAME
|
|
output_filename = DEFAULT_JSON_OUTPUT_FILENAME
|
|
else:
|
|
input_filename = DEFAULT_INPUT_FILENAME
|
|
output_filename = DEFAULT_OUTPUT_FILENAME
|
|
|
|
if args.i:
|
|
# Encode the input string (type unicode) as a normal string (type str)
|
|
# using the 'utf-8' encoding so that it can be worked with the same as
|
|
# input names from other sources (like the defaults).
|
|
input_filename_raw = args.i.encode('utf-8')
|
|
# Decode the filename to a unicode string using the 'utf-8' encoding to
|
|
# properly handle filepaths with unicode characters in them.
|
|
input_filename = input_filename_raw.decode('utf-8')
|
|
|
|
if args.o:
|
|
output_filename = args.o
|
|
|
|
with open(input_filename, 'r') as ifile:
|
|
file_string = ifile.read()
|
|
|
|
json_string = None
|
|
if args.plist:
|
|
json_string = convert_plist_to_json(file_string, input_filename)
|
|
if json_string is None:
|
|
return 1
|
|
jsobj = json.loads(json_string)
|
|
else:
|
|
jsobj = json.loads(file_string)
|
|
|
|
root = ElementTree.Element('resources')
|
|
root.set('xmlns:tools', 'http://schemas.android.com/tools')
|
|
|
|
project_info = jsobj.get('project_info')
|
|
if project_info:
|
|
gen_string(root, 'firebase_database_url', project_info.get('firebase_url'))
|
|
gen_string(root, 'gcm_defaultSenderId', project_info.get('project_number'))
|
|
gen_string(root, 'google_storage_bucket',
|
|
project_info.get('storage_bucket'))
|
|
gen_string(root, 'project_id', project_info.get('project_id'))
|
|
|
|
if args.f:
|
|
if not project_info:
|
|
sys.stderr.write('No project info found in %s.' % input_filename)
|
|
return 1
|
|
for field, value in sorted(project_info.items()):
|
|
sys.stdout.write('%s=%s\n' % (field, value))
|
|
return 0
|
|
|
|
packages = set()
|
|
client_list = jsobj.get('client')
|
|
if client_list:
|
|
# Search for the user specified package in the file.
|
|
selected_package_name = ''
|
|
selected_client = client_list[0]
|
|
find_package_name = args.p
|
|
for client in client_list:
|
|
package_name = client.get('client_info', {}).get(
|
|
'android_client_info', {}).get('package_name', '')
|
|
if not package_name:
|
|
package_name = client.get('oauth_client', {}).get(
|
|
'android_info', {}).get('package_name', '')
|
|
if package_name:
|
|
if not selected_package_name:
|
|
selected_package_name = package_name
|
|
selected_client = client
|
|
if package_name == find_package_name:
|
|
selected_package_name = package_name
|
|
selected_client = client
|
|
packages.add(package_name)
|
|
|
|
if args.p and selected_package_name != find_package_name:
|
|
sys.stderr.write('No packages found in %s which match the package '
|
|
'name %s\n'
|
|
'\n'
|
|
'Found the following:\n'
|
|
'%s\n' % (input_filename, find_package_name,
|
|
'\n'.join(packages)))
|
|
return 1
|
|
|
|
client_api_key = selected_client.get('api_key')
|
|
if client_api_key:
|
|
client_api_key0 = client_api_key[0]
|
|
gen_string(root, 'google_api_key', client_api_key0.get('current_key'))
|
|
gen_string(root, 'google_crash_reporting_api_key',
|
|
client_api_key0.get('current_key'))
|
|
|
|
client_info = selected_client.get('client_info')
|
|
if client_info:
|
|
gen_string(root, 'google_app_id', client_info.get('mobilesdk_app_id'))
|
|
|
|
# Only include the first matching OAuth client ID per type.
|
|
client_id_web_parsed = False
|
|
client_id_android_parsed = False
|
|
|
|
oauth_client_list = selected_client.get('oauth_client')
|
|
if oauth_client_list:
|
|
for oauth_client in oauth_client_list:
|
|
client_type = oauth_client.get('client_type')
|
|
client_id = oauth_client.get('client_id')
|
|
if not (client_type and client_id): continue
|
|
if (client_type == OAUTH_CLIENT_TYPE_WEB and
|
|
not client_id_web_parsed):
|
|
gen_string(root, 'default_web_client_id', client_id)
|
|
client_id_web_parsed = True
|
|
if (client_type == OAUTH_CLIENT_TYPE_ANDROID_APP and
|
|
not client_id_android_parsed):
|
|
gen_string(root, 'default_android_client_id', client_id)
|
|
client_id_android_parsed = True
|
|
|
|
services = selected_client.get('services')
|
|
if services:
|
|
ads_service = services.get('ads_service')
|
|
if ads_service:
|
|
gen_string(root, 'test_banner_ad_unit_id',
|
|
ads_service.get('test_banner_ad_unit_id'))
|
|
gen_string(root, 'test_interstitial_ad_unit_id',
|
|
ads_service.get('test_interstitial_ad_unit_id'))
|
|
analytics_service = services.get('analytics_service')
|
|
if analytics_service:
|
|
analytics_property = analytics_service.get('analytics_property')
|
|
if analytics_property:
|
|
gen_string(root, 'ga_trackingId',
|
|
analytics_property.get('tracking_id'))
|
|
# enable this once we have an example if this service being present
|
|
# in the json data:
|
|
maps_service_enabled = False
|
|
if maps_service_enabled:
|
|
maps_service = services.get('maps_service')
|
|
if maps_service:
|
|
maps_api_key = maps_service.get('api_key')
|
|
if maps_api_key:
|
|
for k in range(0, len(maps_api_key)):
|
|
# generates potentially multiple of these keys, which is
|
|
# the same behavior as the java plugin.
|
|
gen_string(root, 'google_maps_key',
|
|
maps_api_key[k].get('maps_api_key'))
|
|
|
|
tree = ElementTree.ElementTree(root)
|
|
|
|
indent(root)
|
|
|
|
if args.l:
|
|
for package in sorted(packages):
|
|
if package:
|
|
sys.stdout.write(package + '\n')
|
|
else:
|
|
path = os.path.dirname(output_filename)
|
|
|
|
if path and not os.path.exists(path):
|
|
os.makedirs(path)
|
|
|
|
if not args.plist:
|
|
tree.write(output_filename, 'utf-8', True)
|
|
else:
|
|
with open(output_filename, 'w') as ofile:
|
|
ofile.write(json_string)
|
|
|
|
return 0
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|
|
|