Home | History | Annotate | Download | only in webapp
      1 #!/usr/bin/env python
      2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """Creates a directory with with the unpacked contents of the remoting webapp.
      7 
      8 The directory will contain a copy-of or a link-to to all remoting webapp
      9 resources.  This includes HTML/JS and any plugin binaries. The script also
     10 massages resulting files appropriately with host plugin data. Finally,
     11 a zip archive for all of the above is produced.
     12 """
     13 
     14 # Python 2.5 compatibility
     15 from __future__ import with_statement
     16 
     17 import io
     18 import os
     19 import platform
     20 import re
     21 import shutil
     22 import subprocess
     23 import sys
     24 import time
     25 import zipfile
     26 
     27 # Update the module path, assuming that this script is in src/remoting/webapp,
     28 # and that the google_api_keys module is in src/google_apis. Note that
     29 # sys.path[0] refers to the directory containing this script.
     30 if __name__ == '__main__':
     31   sys.path.append(
     32       os.path.abspath(os.path.join(sys.path[0], '../../google_apis')))
     33 import google_api_keys
     34 
     35 def findAndReplace(filepath, findString, replaceString):
     36   """Does a search and replace on the contents of a file."""
     37   oldFilename = os.path.basename(filepath) + '.old'
     38   oldFilepath = os.path.join(os.path.dirname(filepath), oldFilename)
     39   os.rename(filepath, oldFilepath)
     40   with open(oldFilepath) as input:
     41     with open(filepath, 'w') as output:
     42       for s in input:
     43         output.write(s.replace(findString, replaceString))
     44   os.remove(oldFilepath)
     45 
     46 
     47 def createZip(zip_path, directory):
     48   """Creates a zipfile at zip_path for the given directory."""
     49   zipfile_base = os.path.splitext(os.path.basename(zip_path))[0]
     50   zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
     51   for (root, dirs, files) in os.walk(directory):
     52     for f in files:
     53       full_path = os.path.join(root, f)
     54       rel_path = os.path.relpath(full_path, directory)
     55       zip.write(full_path, os.path.join(zipfile_base, rel_path))
     56   zip.close()
     57 
     58 
     59 def replaceString(destination, placeholder, value):
     60   findAndReplace(os.path.join(destination, 'plugin_settings.js'),
     61                  "'" + placeholder + "'", "'" + value + "'")
     62 
     63 
     64 def processJinjaTemplate(input_file, output_file, context):
     65   jinja2_path = os.path.normpath(
     66       os.path.join(os.path.abspath(__file__),
     67                    '../../../third_party/jinja2'))
     68   sys.path.append(os.path.split(jinja2_path)[0])
     69   import jinja2
     70   (template_path, template_name) = os.path.split(input_file)
     71   env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_path))
     72   template = env.get_template(template_name)
     73   rendered = template.render(context)
     74   io.open(output_file, 'w', encoding='utf-8').write(rendered)
     75 
     76 
     77 
     78 def buildWebApp(buildtype, version, destination, zip_path,
     79                 manifest_template, webapp_type, files, locales):
     80   """Does the main work of building the webapp directory and zipfile.
     81 
     82   Args:
     83     buildtype: the type of build ("Official" or "Dev").
     84     destination: A string with path to directory where the webapp will be
     85                  written.
     86     zipfile: A string with path to the zipfile to create containing the
     87              contents of |destination|.
     88     manifest_template: jinja2 template file for manifest.
     89     webapp_type: webapp type ("v1", "v2" or "v2_pnacl").
     90     files: An array of strings listing the paths for resources to include
     91            in this webapp.
     92     locales: An array of strings listing locales, which are copied, along
     93              with their directory structure from the _locales directory down.
     94   """
     95   # Ensure a fresh directory.
     96   try:
     97     shutil.rmtree(destination)
     98   except OSError:
     99     if os.path.exists(destination):
    100       raise
    101     else:
    102       pass
    103   os.mkdir(destination, 0775)
    104 
    105   # Use symlinks on linux and mac for faster compile/edit cycle.
    106   #
    107   # On Windows Vista platform.system() can return 'Microsoft' with some
    108   # versions of Python, see http://bugs.python.org/issue1082
    109   # should_symlink = platform.system() not in ['Windows', 'Microsoft']
    110   #
    111   # TODO(ajwong): Pending decision on http://crbug.com/27185 we may not be
    112   # able to load symlinked resources.
    113   should_symlink = False
    114 
    115   # Copy all the files.
    116   for current_file in files:
    117     destination_file = os.path.join(destination, os.path.basename(current_file))
    118     destination_dir = os.path.dirname(destination_file)
    119     if not os.path.exists(destination_dir):
    120       os.makedirs(destination_dir, 0775)
    121 
    122     if should_symlink:
    123       # TODO(ajwong): Detect if we're vista or higher.  Then use win32file
    124       # to create a symlink in that case.
    125       targetname = os.path.relpath(os.path.realpath(current_file),
    126                                    os.path.realpath(destination_file))
    127       os.symlink(targetname, destination_file)
    128     else:
    129       shutil.copy2(current_file, destination_file)
    130 
    131   # Copy all the locales, preserving directory structure
    132   destination_locales = os.path.join(destination, "_locales")
    133   os.mkdir(destination_locales , 0775)
    134   remoting_locales = os.path.join(destination, "remoting_locales")
    135   os.mkdir(remoting_locales , 0775)
    136   for current_locale in locales:
    137     extension = os.path.splitext(current_locale)[1]
    138     if extension == '.json':
    139       locale_id = os.path.split(os.path.split(current_locale)[0])[1]
    140       destination_dir = os.path.join(destination_locales, locale_id)
    141       destination_file = os.path.join(destination_dir,
    142                                       os.path.split(current_locale)[1])
    143       os.mkdir(destination_dir, 0775)
    144       shutil.copy2(current_locale, destination_file)
    145     elif extension == '.pak':
    146       destination_file = os.path.join(remoting_locales,
    147                                       os.path.split(current_locale)[1])
    148       shutil.copy2(current_locale, destination_file)
    149     else:
    150       raise Exception("Unknown extension: " + current_locale);
    151 
    152   # Set client plugin type.
    153   client_plugin = 'pnacl' if webapp_type == 'v2_pnacl' else 'native'
    154   findAndReplace(os.path.join(destination, 'plugin_settings.js'),
    155                  "'CLIENT_PLUGIN_TYPE'", "'" + client_plugin + "'")
    156 
    157   # Allow host names for google services/apis to be overriden via env vars.
    158   oauth2AccountsHost = os.environ.get(
    159       'OAUTH2_ACCOUNTS_HOST', 'https://accounts.google.com')
    160   oauth2ApiHost = os.environ.get(
    161       'OAUTH2_API_HOST', 'https://www.googleapis.com')
    162   directoryApiHost = os.environ.get(
    163       'DIRECTORY_API_HOST', 'https://www.googleapis.com')
    164   oauth2BaseUrl = oauth2AccountsHost + '/o/oauth2'
    165   oauth2ApiBaseUrl = oauth2ApiHost + '/oauth2'
    166   directoryApiBaseUrl = directoryApiHost + '/chromoting/v1'
    167   replaceString(destination, 'OAUTH2_BASE_URL', oauth2BaseUrl)
    168   replaceString(destination, 'OAUTH2_API_BASE_URL', oauth2ApiBaseUrl)
    169   replaceString(destination, 'DIRECTORY_API_BASE_URL', directoryApiBaseUrl)
    170   # Substitute hosts in the manifest's CSP list.
    171   # Ensure we list the API host only once if it's the same for multiple APIs.
    172   googleApiHosts = ' '.join(set([oauth2ApiHost, directoryApiHost]))
    173 
    174   # WCS and the OAuth trampoline are both hosted on talkgadget. Split them into
    175   # separate suffix/prefix variables to allow for wildcards in manifest.json.
    176   talkGadgetHostSuffix = os.environ.get(
    177       'TALK_GADGET_HOST_SUFFIX', 'talkgadget.google.com')
    178   talkGadgetHostPrefix = os.environ.get(
    179       'TALK_GADGET_HOST_PREFIX', 'https://chromoting-client.')
    180   oauth2RedirectHostPrefix = os.environ.get(
    181       'OAUTH2_REDIRECT_HOST_PREFIX', 'https://chromoting-oauth.')
    182 
    183   # Use a wildcard in the manifest.json host specs if the prefixes differ.
    184   talkGadgetHostJs = talkGadgetHostPrefix + talkGadgetHostSuffix
    185   talkGadgetBaseUrl = talkGadgetHostJs + '/talkgadget/'
    186   if talkGadgetHostPrefix == oauth2RedirectHostPrefix:
    187     talkGadgetHostJson = talkGadgetHostJs
    188   else:
    189     talkGadgetHostJson = 'https://*.' + talkGadgetHostSuffix
    190 
    191   # Set the correct OAuth2 redirect URL.
    192   oauth2RedirectHostJs = oauth2RedirectHostPrefix + talkGadgetHostSuffix
    193   oauth2RedirectHostJson = talkGadgetHostJson
    194   oauth2RedirectPath = '/talkgadget/oauth/chrome-remote-desktop'
    195   oauth2RedirectBaseUrlJs = oauth2RedirectHostJs + oauth2RedirectPath
    196   oauth2RedirectBaseUrlJson = oauth2RedirectHostJson + oauth2RedirectPath
    197   if buildtype == 'Official':
    198     oauth2RedirectUrlJs = ("'" + oauth2RedirectBaseUrlJs +
    199                            "/rel/' + chrome.i18n.getMessage('@@extension_id')")
    200     oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/rel/*'
    201   else:
    202     oauth2RedirectUrlJs = "'" + oauth2RedirectBaseUrlJs + "/dev'"
    203     oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/dev*'
    204   thirdPartyAuthUrlJs = oauth2RedirectBaseUrlJs + "/thirdpartyauth"
    205   thirdPartyAuthUrlJson = oauth2RedirectBaseUrlJson + '/thirdpartyauth*'
    206   replaceString(destination, "TALK_GADGET_URL", talkGadgetBaseUrl)
    207   findAndReplace(os.path.join(destination, 'plugin_settings.js'),
    208                  "'OAUTH2_REDIRECT_URL'", oauth2RedirectUrlJs)
    209 
    210   # Configure xmpp server and directory bot settings in the plugin.
    211   xmppServerAddress = os.environ.get(
    212       'XMPP_SERVER_ADDRESS', 'talk.google.com:5222')
    213   xmppServerUseTls = os.environ.get('XMPP_SERVER_USE_TLS', 'true')
    214   directoryBotJid = os.environ.get(
    215       'DIRECTORY_BOT_JID', 'remoting (at] bot.talk.google.com')
    216 
    217   findAndReplace(os.path.join(destination, 'plugin_settings.js'),
    218                  "Boolean('XMPP_SERVER_USE_TLS')", xmppServerUseTls)
    219   replaceString(destination, "XMPP_SERVER_ADDRESS", xmppServerAddress)
    220   replaceString(destination, "DIRECTORY_BOT_JID", directoryBotJid)
    221   replaceString(destination, "THIRD_PARTY_AUTH_REDIRECT_URL",
    222                 thirdPartyAuthUrlJs)
    223 
    224   # Set the correct API keys.
    225   # For overriding the client ID/secret via env vars, see google_api_keys.py.
    226   apiClientId = google_api_keys.GetClientID('REMOTING')
    227   apiClientSecret = google_api_keys.GetClientSecret('REMOTING')
    228   apiClientIdV2 = google_api_keys.GetClientID('REMOTING_IDENTITY_API')
    229 
    230   replaceString(destination, "API_CLIENT_ID", apiClientId)
    231   replaceString(destination, "API_CLIENT_SECRET", apiClientSecret)
    232 
    233   # Use a consistent extension id for unofficial builds.
    234   if buildtype != 'Official':
    235     manifestKey = '"key": "remotingdevbuild",'
    236   else:
    237     manifestKey = ''
    238 
    239   # Generate manifest.
    240   context = {
    241     'webapp_type': webapp_type,
    242     'FULL_APP_VERSION': version,
    243     'MANIFEST_KEY_FOR_UNOFFICIAL_BUILD': manifestKey,
    244     'OAUTH2_REDIRECT_URL': oauth2RedirectUrlJson,
    245     'TALK_GADGET_HOST': talkGadgetHostJson,
    246     'THIRD_PARTY_AUTH_REDIRECT_URL': thirdPartyAuthUrlJson,
    247     'REMOTING_IDENTITY_API_CLIENT_ID': apiClientIdV2,
    248     'OAUTH2_BASE_URL': oauth2BaseUrl,
    249     'OAUTH2_API_BASE_URL': oauth2ApiBaseUrl,
    250     'DIRECTORY_API_BASE_URL': directoryApiBaseUrl,
    251     'OAUTH2_ACCOUNTS_HOST': oauth2AccountsHost,
    252     'GOOGLE_API_HOSTS': googleApiHosts,
    253   }
    254   processJinjaTemplate(manifest_template,
    255                        os.path.join(destination, 'manifest.json'),
    256                        context)
    257 
    258   # Make the zipfile.
    259   createZip(zip_path, destination)
    260 
    261   return 0
    262 
    263 
    264 def main():
    265   if len(sys.argv) < 6:
    266     print ('Usage: build-webapp.py '
    267            '<build-type> <version> <dst> <zip-path> <manifest_template> '
    268            '<webapp_type> <other files...> '
    269            '[--locales <locales...>]')
    270     return 1
    271 
    272   arg_type = ''
    273   files = []
    274   locales = []
    275   for arg in sys.argv[7:]:
    276     if arg in ['--locales']:
    277       arg_type = arg
    278     elif arg_type == '--locales':
    279       locales.append(arg)
    280     else:
    281       files.append(arg)
    282 
    283   return buildWebApp(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4],
    284                      sys.argv[5], sys.argv[6], files, locales)
    285 
    286 
    287 if __name__ == '__main__':
    288   sys.exit(main())
    289