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