| #!/usr/bin/env python |
| # Copyright (c) 2012 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. |
| |
| """Creates a directory with with the unpacked contents of the remoting webapp. |
| |
| The directory will contain a copy-of or a link-to to all remoting webapp |
| resources. This includes HTML/JS and any plugin binaries. The script also |
| massages resulting files appropriately with host plugin data. Finally, |
| a zip archive for all of the above is produced. |
| """ |
| |
| # Python 2.5 compatibility |
| from __future__ import with_statement |
| |
| import argparse |
| import io |
| import os |
| import platform |
| import re |
| import shutil |
| import subprocess |
| import sys |
| import time |
| import zipfile |
| |
| # Update the module path, assuming that this script is in src/remoting/webapp, |
| # and that the google_api_keys module is in src/google_apis. Note that |
| # sys.path[0] refers to the directory containing this script. |
| if __name__ == '__main__': |
| sys.path.append( |
| os.path.abspath(os.path.join(sys.path[0], '../../google_apis'))) |
| import google_api_keys |
| |
| |
| def findAndReplace(filepath, findString, replaceString): |
| """Does a search and replace on the contents of a file.""" |
| oldFilename = os.path.basename(filepath) + '.old' |
| oldFilepath = os.path.join(os.path.dirname(filepath), oldFilename) |
| shutil.move(filepath, oldFilepath) |
| with open(oldFilepath) as input: |
| with open(filepath, 'w') as output: |
| for s in input: |
| output.write(s.replace(findString, replaceString)) |
| os.remove(oldFilepath) |
| |
| |
| def createZip(zip_path, directory): |
| """Creates a zipfile at zip_path for the given directory.""" |
| zipfile_base = os.path.splitext(os.path.basename(zip_path))[0] |
| zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) |
| for (root, dirs, files) in os.walk(directory): |
| for f in files: |
| full_path = os.path.join(root, f) |
| rel_path = os.path.relpath(full_path, directory) |
| zip.write(full_path, os.path.join(zipfile_base, rel_path)) |
| zip.close() |
| |
| |
| def replaceString(destination, placeholder, value): |
| findAndReplace(os.path.join(destination, 'plugin_settings.js'), |
| "'" + placeholder + "'", "'" + value + "'") |
| |
| |
| def replaceBool(destination, placeholder, value): |
| # Look for a "!!" in the source code so the expession we're |
| # replacing looks like a boolean to the compiler. A single "!" |
| # would satisfy the compiler but might confused human readers. |
| findAndReplace(os.path.join(destination, 'plugin_settings.js'), |
| "!!'" + placeholder + "'", 'true' if value else 'false') |
| |
| |
| def parseBool(boolStr): |
| """Tries to parse a string as a boolean value. |
| |
| Returns a bool on success; raises ValueError on failure. |
| """ |
| lower = boolStr.lower() |
| if lower in ['0', 'false']: return False |
| if lower in ['1', 'true']: return True |
| raise ValueError('not a boolean string {!r}'.format(boolStr)) |
| |
| |
| def getenvBool(name, defaultValue): |
| """Gets an environment value as a boolean.""" |
| rawValue = os.environ.get(name) |
| if rawValue is None: |
| return defaultValue |
| try: |
| return parseBool(rawValue) |
| except ValueError: |
| raise Exception('Value of ${} must be boolean!'.format(name)) |
| |
| |
| def processJinjaTemplate(input_file, include_paths, output_file, context): |
| jinja2_path = os.path.normpath( |
| os.path.join(os.path.abspath(__file__), |
| '../../../third_party/jinja2')) |
| sys.path.append(os.path.split(jinja2_path)[0]) |
| import jinja2 |
| (template_path, template_name) = os.path.split(input_file) |
| include_paths = [template_path] + include_paths |
| env = jinja2.Environment(loader=jinja2.FileSystemLoader(include_paths)) |
| template = env.get_template(template_name) |
| rendered = template.render(context) |
| io.open(output_file, 'w', encoding='utf-8').write(rendered) |
| |
| |
| def buildWebApp(buildtype, version, destination, zip_path, |
| manifest_template, appid, app_client_id, app_name, |
| app_description, app_capabilities, manifest_key, files, |
| files_listfile, locales_listfile, jinja_paths, |
| service_environment, use_gcd): |
| """Does the main work of building the webapp directory and zipfile. |
| |
| Args: |
| buildtype: the type of build ("Official", "Release" or "Dev"). |
| destination: A string with path to directory where the webapp will be |
| written. |
| zipfile: A string with path to the zipfile to create containing the |
| contents of |destination|. |
| manifest_template: jinja2 template file for manifest. |
| appid: A string with the Remoting Application Id (only used for app |
| remoting webapps). If supplied, it defaults to using the |
| test API server. |
| app_client_id: The OAuth2 client ID for the webapp. |
| app_name: A string with the name of the application. |
| app_description: A string with the description of the application. |
| app_capabilities: A set of strings naming the capabilities that should be |
| enabled for this application. |
| manifest_key: The manifest key for the webapp. |
| files: An array of strings listing the paths for resources to include |
| in this webapp. |
| files_listfile: The name of a file containing a list of files, one per |
| line, identifying the resources to include in this webapp. |
| This is an alternate to specifying the files directly via |
| the 'files' option. The files listed in this file are |
| appended to the files passed via the 'files' option, if any. |
| locales_listfile: The name of a file containing a list of locales, one per |
| line, which are copied, along with their directory |
| structure, from the _locales directory down. |
| jinja_paths: An array of paths to search for {%include} directives in |
| addition to the directory containing the manifest template. |
| service_environment: Used to point the webapp to the dev/prod environments. |
| use_gcd: True if GCD support should be enabled. |
| """ |
| |
| # Load the locales files from the locales_listfile. |
| if not locales_listfile: |
| raise Exception('You must specify a locales_listfile') |
| locales = [] |
| with open(locales_listfile) as input: |
| for s in input: |
| locales.append(s.rstrip()) |
| |
| # Load the files from the files_listfile. |
| if files_listfile: |
| with open(files_listfile) as input: |
| for s in input: |
| files.append(s.rstrip()) |
| |
| #Ensure a fresh directory. |
| try: |
| shutil.rmtree(destination) |
| except OSError: |
| if os.path.exists(destination): |
| raise |
| else: |
| pass |
| os.makedirs(destination, 0775) |
| |
| if buildtype != 'Official' and buildtype != 'Release' and buildtype != 'Dev': |
| raise Exception('Unknown buildtype: ' + buildtype) |
| |
| jinja_context = { |
| 'buildtype': buildtype, |
| } |
| |
| # Copy all the files. |
| for current_file in files: |
| destination_file = os.path.join(destination, os.path.basename(current_file)) |
| |
| # Process *.jinja2 files as jinja2 templates |
| if current_file.endswith(".jinja2"): |
| destination_file = destination_file[:-len(".jinja2")] |
| processJinjaTemplate(current_file, jinja_paths, |
| destination_file, jinja_context) |
| else: |
| shutil.copy2(current_file, destination_file) |
| |
| # Copy all the locales, preserving directory structure |
| destination_locales = os.path.join(destination, '_locales') |
| os.mkdir(destination_locales, 0775) |
| remoting_locales = os.path.join(destination, 'remoting_locales') |
| os.mkdir(remoting_locales, 0775) |
| for current_locale in locales: |
| extension = os.path.splitext(current_locale)[1] |
| if extension == '.json': |
| locale_id = os.path.split(os.path.split(current_locale)[0])[1] |
| destination_dir = os.path.join(destination_locales, locale_id) |
| destination_file = os.path.join(destination_dir, |
| os.path.split(current_locale)[1]) |
| os.mkdir(destination_dir, 0775) |
| shutil.copy2(current_locale, destination_file) |
| elif extension == '.pak': |
| destination_file = os.path.join(remoting_locales, |
| os.path.split(current_locale)[1]) |
| shutil.copy2(current_locale, destination_file) |
| else: |
| raise Exception('Unknown extension: ' + current_locale) |
| |
| is_prod_service_environment = service_environment == 'prod' |
| |
| # Allow host names for google services/apis to be overriden via env vars. |
| oauth2AccountsHost = os.environ.get( |
| 'OAUTH2_ACCOUNTS_HOST', 'https://accounts.google.com') |
| oauth2ApiHost = os.environ.get( |
| 'OAUTH2_API_HOST', 'https://www.googleapis.com') |
| directoryApiHost = os.environ.get( |
| 'DIRECTORY_API_HOST', 'https://www.googleapis.com') |
| remotingApiHost = os.environ.get( |
| 'REMOTING_API_HOST', 'https://remoting-pa.googleapis.com') |
| |
| oauth2BaseUrl = oauth2AccountsHost + '/o/oauth2' |
| oauth2ApiBaseUrl = oauth2ApiHost + '/oauth2' |
| directoryApiBaseUrl = directoryApiHost + '/chromoting/v1' |
| telemetryApiBaseUrl = remotingApiHost + '/v1/events' |
| |
| replaceBool(destination, 'USE_GCD', use_gcd) |
| replaceString(destination, 'OAUTH2_BASE_URL', oauth2BaseUrl) |
| replaceString(destination, 'OAUTH2_API_BASE_URL', oauth2ApiBaseUrl) |
| replaceString(destination, 'DIRECTORY_API_BASE_URL', directoryApiBaseUrl) |
| replaceString(destination, 'TELEMETRY_API_BASE_URL', telemetryApiBaseUrl) |
| |
| # Substitute hosts in the manifest's CSP list. |
| # Ensure we list the API host only once if it's the same for multiple APIs. |
| googleApiHosts = ' '.join(set([oauth2ApiHost, directoryApiHost])) |
| |
| # WCS and the OAuth trampoline are both hosted on talkgadget. Split them into |
| # separate suffix/prefix variables to allow for wildcards in manifest.json. |
| talkGadgetHostSuffix = os.environ.get( |
| 'TALK_GADGET_HOST_SUFFIX', 'talkgadget.google.com') |
| talkGadgetHostPrefix = os.environ.get( |
| 'TALK_GADGET_HOST_PREFIX', 'https://chromoting-client.') |
| oauth2RedirectHostPrefix = os.environ.get( |
| 'OAUTH2_REDIRECT_HOST_PREFIX', 'https://chromoting-oauth.') |
| |
| # Use a wildcard in the manifest.json host specs if the prefixes differ. |
| talkGadgetHostJs = talkGadgetHostPrefix + talkGadgetHostSuffix |
| talkGadgetBaseUrl = talkGadgetHostJs + '/talkgadget' |
| if talkGadgetHostPrefix == oauth2RedirectHostPrefix: |
| talkGadgetHostJson = talkGadgetHostJs |
| else: |
| talkGadgetHostJson = 'https://*.' + talkGadgetHostSuffix |
| |
| # Set the correct OAuth2 redirect URL. |
| oauth2RedirectHostJs = oauth2RedirectHostPrefix + talkGadgetHostSuffix |
| oauth2RedirectHostJson = talkGadgetHostJson |
| oauth2RedirectPath = '/talkgadget/oauth/chrome-remote-desktop' |
| oauth2RedirectBaseUrlJs = oauth2RedirectHostJs + oauth2RedirectPath |
| oauth2RedirectBaseUrlJson = oauth2RedirectHostJson + oauth2RedirectPath |
| if buildtype == 'Official': |
| oauth2RedirectUrlJs = ("'" + oauth2RedirectBaseUrlJs + |
| "/rel/' + chrome.i18n.getMessage('@@extension_id')") |
| oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/rel/*' |
| else: |
| oauth2RedirectUrlJs = "'" + oauth2RedirectBaseUrlJs + "/dev'" |
| oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/dev*' |
| thirdPartyAuthUrlJs = oauth2RedirectBaseUrlJs + '/thirdpartyauth' |
| thirdPartyAuthUrlJson = oauth2RedirectBaseUrlJson + '/thirdpartyauth*' |
| xmppServer = os.environ.get('XMPP_SERVER', 'talk.google.com:443') |
| |
| replaceString(destination, 'TALK_GADGET_URL', talkGadgetBaseUrl) |
| findAndReplace(os.path.join(destination, 'plugin_settings.js'), |
| "'OAUTH2_REDIRECT_URL'", oauth2RedirectUrlJs) |
| |
| # Configure xmpp server and directory bot settings in the plugin. |
| xmpp_server_user_tls = getenvBool('XMPP_SERVER_USE_TLS', True) |
| if (buildtype != 'Dev' and not xmpp_server_user_tls): |
| raise Exception('TLS can must be enabled in non Dev builds.') |
| |
| replaceBool( |
| destination, 'XMPP_SERVER_USE_TLS', xmpp_server_user_tls) |
| replaceString(destination, 'XMPP_SERVER', xmppServer) |
| replaceString(destination, 'DIRECTORY_BOT_JID', |
| os.environ.get('DIRECTORY_BOT_JID', |
| 'remoting@bot.talk.google.com')) |
| replaceString(destination, 'THIRD_PARTY_AUTH_REDIRECT_URL', |
| thirdPartyAuthUrlJs) |
| |
| # Set the correct API keys. |
| # For overriding the client ID/secret via env vars, see google_api_keys.py. |
| apiClientId = google_api_keys.GetClientID('REMOTING') |
| apiClientSecret = google_api_keys.GetClientSecret('REMOTING') |
| |
| apiClientIdV2 = os.environ.get( |
| 'REMOTING_IDENTITY_API_CLIENT_ID', |
| google_api_keys.GetClientID('REMOTING_IDENTITY_API')) |
| |
| replaceString(destination, 'API_CLIENT_ID', apiClientId) |
| replaceString(destination, 'API_CLIENT_SECRET', apiClientSecret) |
| |
| # Use a fixed key in the app manifest. For dev builds, this ensures that the |
| # app can be run directly from the output directory. For official CRD builds, |
| # it allows QA to test the app without uploading it to Chrome Web Store. |
| manifest_key = 'remotingdevbuild' |
| |
| # Generate manifest. |
| if manifest_template: |
| context = { |
| 'FULL_APP_VERSION': version, |
| 'MANIFEST_KEY': manifest_key, |
| 'OAUTH2_REDIRECT_URL': oauth2RedirectUrlJson, |
| 'TALK_GADGET_HOST': talkGadgetHostJson, |
| 'THIRD_PARTY_AUTH_REDIRECT_URL': thirdPartyAuthUrlJson, |
| 'REMOTING_IDENTITY_API_CLIENT_ID': apiClientIdV2, |
| 'OAUTH2_BASE_URL': oauth2BaseUrl, |
| 'OAUTH2_API_BASE_URL': oauth2ApiBaseUrl, |
| 'DIRECTORY_API_BASE_URL': directoryApiBaseUrl, |
| 'TELEMETRY_API_BASE_URL':telemetryApiBaseUrl , |
| 'CLOUD_PRINT_URL': '', |
| 'OAUTH2_ACCOUNTS_HOST': oauth2AccountsHost, |
| 'GOOGLE_API_HOSTS': googleApiHosts, |
| 'APP_NAME': app_name, |
| 'APP_DESCRIPTION': app_description, |
| 'OAUTH_CLOUD_PRINT_SCOPE': '', |
| 'OAUTH_GDRIVE_SCOPE': '', |
| 'USE_GCD': use_gcd, |
| 'XMPP_SERVER': xmppServer, |
| # An URL match pattern that is added to the |permissions| section of the |
| # manifest in case some URLs are redirected by corporate proxies. |
| 'PROXY_URL' : os.environ.get('PROXY_URL', ''), |
| } |
| if 'CLOUD_PRINT' in app_capabilities: |
| context['OAUTH_CLOUD_PRINT_SCOPE'] = ('"https://www.googleapis.com/auth/cloudprint",') |
| context['CLOUD_PRINT_URL'] = ('"https://www.google.com/cloudprint/*",') |
| if 'GOOGLE_DRIVE' in app_capabilities: |
| context['OAUTH_GDRIVE_SCOPE'] = ('"https://docs.google.com/feeds/", ' |
| '"https://www.googleapis.com/auth/drive",') |
| processJinjaTemplate(manifest_template, |
| jinja_paths, |
| os.path.join(destination, 'manifest.json'), |
| context) |
| |
| # Make the zipfile. |
| createZip(zip_path, destination) |
| |
| return 0 |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('buildtype') |
| parser.add_argument('version') |
| parser.add_argument('destination') |
| parser.add_argument('zip_path') |
| parser.add_argument('manifest_template') |
| parser.add_argument('files', nargs='*', metavar='file', default=[]) |
| parser.add_argument('--app_name', metavar='NAME') |
| parser.add_argument('--app_description', metavar='TEXT') |
| parser.add_argument('--app_capabilities', |
| nargs='*', default=[], metavar='CAPABILITY') |
| parser.add_argument('--appid') |
| parser.add_argument('--app_client_id', default='') |
| parser.add_argument('--manifest_key', default='') |
| parser.add_argument('--files_listfile', default='', metavar='PATH') |
| parser.add_argument('--locales_listfile', default='', metavar='PATH') |
| parser.add_argument('--jinja_paths', nargs='*', default=[], metavar='PATH') |
| parser.add_argument('--service_environment', default='', metavar='ENV') |
| parser.add_argument('--use_gcd', choices=['0', '1'], default='0') |
| |
| args = parser.parse_args() |
| args.use_gcd = (args.use_gcd != '0') |
| args.app_capabilities = set(args.app_capabilities) |
| return buildWebApp(**vars(args)) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |