build-webapp.py revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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 8The directory will contain a copy-of or a link-to to all remoting webapp 9resources. This includes HTML/JS and any plugin binaries. The script also 10massages resulting files appropriately with host plugin data. Finally, 11a zip archive for all of the above is produced. 12""" 13 14# Python 2.5 compatibility 15from __future__ import with_statement 16 17import io 18import os 19import platform 20import re 21import shutil 22import subprocess 23import sys 24import time 25import 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. 30if __name__ == '__main__': 31 sys.path.append( 32 os.path.abspath(os.path.join(sys.path[0], '../../google_apis'))) 33import google_api_keys 34 35def 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 47def 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 59def replaceString(destination, placeholder, value): 60 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 61 "'" + placeholder + "'", "'" + value + "'") 62 63 64def 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 78def buildWebApp(buildtype, version, mimetype, destination, zip_path, 79 manifest_template, webapp_type, plugin, 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 mimetype: A string with mimetype of plugin. 85 destination: A string with path to directory where the webapp will be 86 written. 87 zipfile: A string with path to the zipfile to create containing the 88 contents of |destination|. 89 manifest_template: jinja2 template file for manifest. 90 webapp_type: webapp type ("v1", "v2" or "v2_pnacl"). 91 plugin: A string with path to the binary plugin for this webapp. 92 files: An array of strings listing the paths for resources to include 93 in this webapp. 94 locales: An array of strings listing locales, which are copied, along 95 with their directory structure from the _locales directory down. 96 """ 97 # Ensure a fresh directory. 98 try: 99 shutil.rmtree(destination) 100 except OSError: 101 if os.path.exists(destination): 102 raise 103 else: 104 pass 105 os.mkdir(destination, 0775) 106 107 # Use symlinks on linux and mac for faster compile/edit cycle. 108 # 109 # On Windows Vista platform.system() can return 'Microsoft' with some 110 # versions of Python, see http://bugs.python.org/issue1082 111 # should_symlink = platform.system() not in ['Windows', 'Microsoft'] 112 # 113 # TODO(ajwong): Pending decision on http://crbug.com/27185 we may not be 114 # able to load symlinked resources. 115 should_symlink = False 116 117 # Copy all the files. 118 for current_file in files: 119 destination_file = os.path.join(destination, os.path.basename(current_file)) 120 destination_dir = os.path.dirname(destination_file) 121 if not os.path.exists(destination_dir): 122 os.makedirs(destination_dir, 0775) 123 124 if should_symlink: 125 # TODO(ajwong): Detect if we're vista or higher. Then use win32file 126 # to create a symlink in that case. 127 targetname = os.path.relpath(os.path.realpath(current_file), 128 os.path.realpath(destination_file)) 129 os.symlink(targetname, destination_file) 130 else: 131 shutil.copy2(current_file, destination_file) 132 133 # Copy all the locales, preserving directory structure 134 destination_locales = os.path.join(destination, "_locales") 135 os.mkdir(destination_locales , 0775) 136 remoting_locales = os.path.join(destination, "remoting_locales") 137 os.mkdir(remoting_locales , 0775) 138 for current_locale in locales: 139 extension = os.path.splitext(current_locale)[1] 140 if extension == '.json': 141 locale_id = os.path.split(os.path.split(current_locale)[0])[1] 142 destination_dir = os.path.join(destination_locales, locale_id) 143 destination_file = os.path.join(destination_dir, 144 os.path.split(current_locale)[1]) 145 os.mkdir(destination_dir, 0775) 146 shutil.copy2(current_locale, destination_file) 147 elif extension == '.pak': 148 destination_file = os.path.join(remoting_locales, 149 os.path.split(current_locale)[1]) 150 shutil.copy2(current_locale, destination_file) 151 else: 152 raise Exception("Unknown extension: " + current_locale); 153 154 # Create fake plugin files to appease the manifest checker. 155 # It requires that if there is a plugin listed in the manifest that 156 # there be a file in the plugin with that name. 157 names = [ 158 'remoting_host_plugin.dll', # Windows 159 'remoting_host_plugin.plugin', # Mac 160 'libremoting_host_plugin.ia32.so', # Linux 32 161 'libremoting_host_plugin.x64.so' # Linux 64 162 ] 163 pluginName = os.path.basename(plugin) 164 165 for name in names: 166 if name != pluginName: 167 path = os.path.join(destination, name) 168 f = open(path, 'w') 169 f.write("placeholder for %s" % (name)) 170 f.close() 171 172 # Copy the plugin. On some platforms (e.g. ChromeOS) plugin compilation may be 173 # disabled, in which case we don't need to copy anything. 174 if plugin: 175 newPluginPath = os.path.join(destination, pluginName) 176 if os.path.isdir(plugin): 177 # On Mac we have a directory. 178 shutil.copytree(plugin, newPluginPath) 179 else: 180 shutil.copy2(plugin, newPluginPath) 181 182 # Strip the linux build. 183 if ((platform.system() == 'Linux') and (buildtype == 'Official')): 184 subprocess.call(["strip", newPluginPath]) 185 186 # Set the correct mimetype. 187 hostPluginMimeType = os.environ.get( 188 'HOST_PLUGIN_MIMETYPE', 'application/vnd.chromium.remoting-host') 189 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 190 'HOST_PLUGIN_MIMETYPE', hostPluginMimeType) 191 192 # Set client plugin type. 193 client_plugin = 'pnacl' if webapp_type == 'v2_pnacl' else 'native' 194 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 195 "'CLIENT_PLUGIN_TYPE'", "'" + client_plugin + "'") 196 197 # Allow host names for google services/apis to be overriden via env vars. 198 oauth2AccountsHost = os.environ.get( 199 'OAUTH2_ACCOUNTS_HOST', 'https://accounts.google.com') 200 oauth2ApiHost = os.environ.get( 201 'OAUTH2_API_HOST', 'https://www.googleapis.com') 202 directoryApiHost = os.environ.get( 203 'DIRECTORY_API_HOST', 'https://www.googleapis.com') 204 oauth2BaseUrl = oauth2AccountsHost + '/o/oauth2' 205 oauth2ApiBaseUrl = oauth2ApiHost + '/oauth2' 206 directoryApiBaseUrl = directoryApiHost + '/chromoting/v1' 207 replaceString(destination, 'OAUTH2_BASE_URL', oauth2BaseUrl) 208 replaceString(destination, 'OAUTH2_API_BASE_URL', oauth2ApiBaseUrl) 209 replaceString(destination, 'DIRECTORY_API_BASE_URL', directoryApiBaseUrl) 210 # Substitute hosts in the manifest's CSP list. 211 # Ensure we list the API host only once if it's the same for multiple APIs. 212 googleApiHosts = ' '.join(set([oauth2ApiHost, directoryApiHost])) 213 214 # WCS and the OAuth trampoline are both hosted on talkgadget. Split them into 215 # separate suffix/prefix variables to allow for wildcards in manifest.json. 216 talkGadgetHostSuffix = os.environ.get( 217 'TALK_GADGET_HOST_SUFFIX', 'talkgadget.google.com') 218 talkGadgetHostPrefix = os.environ.get( 219 'TALK_GADGET_HOST_PREFIX', 'https://chromoting-client.') 220 oauth2RedirectHostPrefix = os.environ.get( 221 'OAUTH2_REDIRECT_HOST_PREFIX', 'https://chromoting-oauth.') 222 223 # Use a wildcard in the manifest.json host specs if the prefixes differ. 224 talkGadgetHostJs = talkGadgetHostPrefix + talkGadgetHostSuffix 225 talkGadgetBaseUrl = talkGadgetHostJs + '/talkgadget/' 226 if talkGadgetHostPrefix == oauth2RedirectHostPrefix: 227 talkGadgetHostJson = talkGadgetHostJs 228 else: 229 talkGadgetHostJson = 'https://*.' + talkGadgetHostSuffix 230 231 # Set the correct OAuth2 redirect URL. 232 oauth2RedirectHostJs = oauth2RedirectHostPrefix + talkGadgetHostSuffix 233 oauth2RedirectHostJson = talkGadgetHostJson 234 oauth2RedirectPath = '/talkgadget/oauth/chrome-remote-desktop' 235 oauth2RedirectBaseUrlJs = oauth2RedirectHostJs + oauth2RedirectPath 236 oauth2RedirectBaseUrlJson = oauth2RedirectHostJson + oauth2RedirectPath 237 if buildtype == 'Official': 238 oauth2RedirectUrlJs = ("'" + oauth2RedirectBaseUrlJs + 239 "/rel/' + chrome.i18n.getMessage('@@extension_id')") 240 oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/rel/*' 241 else: 242 oauth2RedirectUrlJs = "'" + oauth2RedirectBaseUrlJs + "/dev'" 243 oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/dev*' 244 thirdPartyAuthUrlJs = oauth2RedirectBaseUrlJs + "/thirdpartyauth" 245 thirdPartyAuthUrlJson = oauth2RedirectBaseUrlJson + '/thirdpartyauth*' 246 replaceString(destination, "TALK_GADGET_URL", talkGadgetBaseUrl) 247 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 248 "'OAUTH2_REDIRECT_URL'", oauth2RedirectUrlJs) 249 250 # Configure xmpp server and directory bot settings in the plugin. 251 xmppServerAddress = os.environ.get( 252 'XMPP_SERVER_ADDRESS', 'talk.google.com:5222') 253 xmppServerUseTls = os.environ.get('XMPP_SERVER_USE_TLS', 'true') 254 directoryBotJid = os.environ.get( 255 'DIRECTORY_BOT_JID', 'remoting@bot.talk.google.com') 256 257 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 258 "Boolean('XMPP_SERVER_USE_TLS')", xmppServerUseTls) 259 replaceString(destination, "XMPP_SERVER_ADDRESS", xmppServerAddress) 260 replaceString(destination, "DIRECTORY_BOT_JID", directoryBotJid) 261 replaceString(destination, "THIRD_PARTY_AUTH_REDIRECT_URL", 262 thirdPartyAuthUrlJs) 263 264 # Set the correct API keys. 265 # For overriding the client ID/secret via env vars, see google_api_keys.py. 266 apiClientId = google_api_keys.GetClientID('REMOTING') 267 apiClientSecret = google_api_keys.GetClientSecret('REMOTING') 268 apiClientIdV2 = google_api_keys.GetClientID('REMOTING_IDENTITY_API') 269 270 replaceString(destination, "API_CLIENT_ID", apiClientId) 271 replaceString(destination, "API_CLIENT_SECRET", apiClientSecret) 272 273 # Use a consistent extension id for unofficial builds. 274 if buildtype != 'Official': 275 manifestKey = '"key": "remotingdevbuild",' 276 else: 277 manifestKey = '' 278 279 # Generate manifest. 280 context = { 281 'webapp_type': webapp_type, 282 'FULL_APP_VERSION': version, 283 'MANIFEST_KEY_FOR_UNOFFICIAL_BUILD': manifestKey, 284 'OAUTH2_REDIRECT_URL': oauth2RedirectUrlJson, 285 'TALK_GADGET_HOST': talkGadgetHostJson, 286 'THIRD_PARTY_AUTH_REDIRECT_URL': thirdPartyAuthUrlJson, 287 'REMOTING_IDENTITY_API_CLIENT_ID': apiClientIdV2, 288 'OAUTH2_BASE_URL': oauth2BaseUrl, 289 'OAUTH2_API_BASE_URL': oauth2ApiBaseUrl, 290 'DIRECTORY_API_BASE_URL': directoryApiBaseUrl, 291 'OAUTH2_ACCOUNTS_HOST': oauth2AccountsHost, 292 'GOOGLE_API_HOSTS': googleApiHosts, 293 } 294 processJinjaTemplate(manifest_template, 295 os.path.join(destination, 'manifest.json'), 296 context) 297 298 # Make the zipfile. 299 createZip(zip_path, destination) 300 301 return 0 302 303 304def main(): 305 if len(sys.argv) < 6: 306 print ('Usage: build-webapp.py ' 307 '<build-type> <version> <mime-type> <dst> <zip-path> ' 308 '<manifest_template> <webapp_type> <other files...> ' 309 '[--plugin <plugin>] [--locales <locales...>]') 310 return 1 311 312 arg_type = '' 313 files = [] 314 locales = [] 315 plugin = "" 316 for arg in sys.argv[8:]: 317 if arg in ['--locales', '--plugin']: 318 arg_type = arg 319 elif arg_type == '--locales': 320 locales.append(arg) 321 elif arg_type == '--plugin': 322 plugin = arg 323 arg_type = '' 324 else: 325 files.append(arg) 326 327 return buildWebApp(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], 328 sys.argv[5], sys.argv[6], sys.argv[7], plugin, 329 files, locales) 330 331 332if __name__ == '__main__': 333 sys.exit(main()) 334