build-webapp.py revision d3868032626d59662ff73b372b5d584c1d144c53
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 os 18import platform 19import re 20import shutil 21import subprocess 22import sys 23import time 24import zipfile 25 26# Update the module path, assuming that this script is in src/remoting/webapp, 27# and that the google_api_keys module is in src/google_apis. Note that 28# sys.path[0] refers to the directory containing this script. 29if __name__ == '__main__': 30 sys.path.append( 31 os.path.abspath(os.path.join(sys.path[0], '../../google_apis'))) 32import google_api_keys 33 34def findAndReplace(filepath, findString, replaceString): 35 """Does a search and replace on the contents of a file.""" 36 oldFilename = os.path.basename(filepath) + '.old' 37 oldFilepath = os.path.join(os.path.dirname(filepath), oldFilename) 38 os.rename(filepath, oldFilepath) 39 with open(oldFilepath) as input: 40 with open(filepath, 'w') as output: 41 for s in input: 42 output.write(s.replace(findString, replaceString)) 43 os.remove(oldFilepath) 44 45 46def createZip(zip_path, directory): 47 """Creates a zipfile at zip_path for the given directory.""" 48 zipfile_base = os.path.splitext(os.path.basename(zip_path))[0] 49 zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) 50 for (root, dirs, files) in os.walk(directory): 51 for f in files: 52 full_path = os.path.join(root, f) 53 rel_path = os.path.relpath(full_path, directory) 54 zip.write(full_path, os.path.join(zipfile_base, rel_path)) 55 zip.close() 56 57 58def replaceUrl(destination, url_name, url_value): 59 """Updates a URL in both plugin_settings.js and manifest.json.""" 60 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 61 "'" + url_name + "'", "'" + url_value + "'") 62 findAndReplace(os.path.join(destination, 'manifest.json'), 63 url_name, url_value) 64 65 66def buildWebApp(buildtype, version, mimetype, destination, zip_path, plugin, 67 files, locales, patches): 68 """Does the main work of building the webapp directory and zipfile. 69 70 Args: 71 buildtype: the type of build ("Official" or "Dev") 72 mimetype: A string with mimetype of plugin. 73 destination: A string with path to directory where the webapp will be 74 written. 75 zipfile: A string with path to the zipfile to create containing the 76 contents of |destination|. 77 plugin: A string with path to the binary plugin for this webapp. 78 files: An array of strings listing the paths for resources to include 79 in this webapp. 80 locales: An array of strings listing locales, which are copied, along 81 with their directory structure from the _locales directory down. 82 patches: An array of strings listing patch files to be applied to the 83 webapp directory. Paths in the patch file should be relative to 84 the remoting/webapp directory, for example a/main.html. Since 85 'git diff -p' works relative to the src/ directory, patches 86 obtained this way will need to be edited. 87 """ 88 # Ensure a fresh directory. 89 try: 90 shutil.rmtree(destination) 91 except OSError: 92 if os.path.exists(destination): 93 raise 94 else: 95 pass 96 os.mkdir(destination, 0775) 97 98 # Use symlinks on linux and mac for faster compile/edit cycle. 99 # 100 # On Windows Vista platform.system() can return 'Microsoft' with some 101 # versions of Python, see http://bugs.python.org/issue1082 102 # should_symlink = platform.system() not in ['Windows', 'Microsoft'] 103 # 104 # TODO(ajwong): Pending decision on http://crbug.com/27185 we may not be 105 # able to load symlinked resources. 106 should_symlink = False 107 108 # Copy all the files. 109 for current_file in files: 110 destination_file = os.path.join(destination, os.path.basename(current_file)) 111 destination_dir = os.path.dirname(destination_file) 112 if not os.path.exists(destination_dir): 113 os.makedirs(destination_dir, 0775) 114 115 if should_symlink: 116 # TODO(ajwong): Detect if we're vista or higher. Then use win32file 117 # to create a symlink in that case. 118 targetname = os.path.relpath(os.path.realpath(current_file), 119 os.path.realpath(destination_file)) 120 os.symlink(targetname, destination_file) 121 else: 122 shutil.copy2(current_file, destination_file) 123 124 # Copy all the locales, preserving directory structure 125 destination_locales = os.path.join(destination, "_locales") 126 os.mkdir(destination_locales , 0775) 127 remoting_locales = os.path.join(destination, "remoting_locales") 128 os.mkdir(remoting_locales , 0775) 129 for current_locale in locales: 130 extension = os.path.splitext(current_locale)[1] 131 if extension == '.json': 132 locale_id = os.path.split(os.path.split(current_locale)[0])[1] 133 destination_dir = os.path.join(destination_locales, locale_id) 134 destination_file = os.path.join(destination_dir, 135 os.path.split(current_locale)[1]) 136 os.mkdir(destination_dir, 0775) 137 shutil.copy2(current_locale, destination_file) 138 elif extension == '.pak': 139 destination_file = os.path.join(remoting_locales, 140 os.path.split(current_locale)[1]) 141 shutil.copy2(current_locale, destination_file) 142 else: 143 raise Exception("Unknown extension: " + current_locale); 144 145 # Create fake plugin files to appease the manifest checker. 146 # It requires that if there is a plugin listed in the manifest that 147 # there be a file in the plugin with that name. 148 names = [ 149 'remoting_host_plugin.dll', # Windows 150 'remoting_host_plugin.plugin', # Mac 151 'libremoting_host_plugin.ia32.so', # Linux 32 152 'libremoting_host_plugin.x64.so' # Linux 64 153 ] 154 pluginName = os.path.basename(plugin) 155 156 for name in names: 157 if name != pluginName: 158 path = os.path.join(destination, name) 159 f = open(path, 'w') 160 f.write("placeholder for %s" % (name)) 161 f.close() 162 163 # Copy the plugin. On some platforms (e.g. ChromeOS) plugin compilation may be 164 # disabled, in which case we don't need to copy anything. 165 if plugin: 166 newPluginPath = os.path.join(destination, pluginName) 167 if os.path.isdir(plugin): 168 # On Mac we have a directory. 169 shutil.copytree(plugin, newPluginPath) 170 else: 171 shutil.copy2(plugin, newPluginPath) 172 173 # Strip the linux build. 174 if ((platform.system() == 'Linux') and (buildtype == 'Official')): 175 subprocess.call(["strip", newPluginPath]) 176 177 # Patch the files, if necessary. Do this before updating any placeholders 178 # in case any of the diff contexts refer to the placeholders. 179 for patch in patches: 180 patchfile = os.path.join(os.getcwd(), patch) 181 if subprocess.call(['patch', '-d', destination, '-i', patchfile, 182 '-p1', '-F0', '-s']) != 0: 183 print 'Patch ' + patch + ' failed to apply.' 184 return 1 185 186 # Set the version number in the manifest version. 187 findAndReplace(os.path.join(destination, 'manifest.json'), 188 'FULL_APP_VERSION', 189 version) 190 191 # Set the correct mimetype. 192 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 193 'HOST_PLUGIN_MIMETYPE', 194 mimetype) 195 196 # Allow host names for google services/apis to be overriden via env vars. 197 oauth2AccountsHost = os.environ.get( 198 'OAUTH2_ACCOUNTS_HOST', 'https://accounts.google.com') 199 oauth2ApiHost = os.environ.get( 200 'OAUTH2_API_HOST', 'https://www.googleapis.com') 201 directoryApiHost = os.environ.get( 202 'DIRECTORY_API_HOST', 'https://www.googleapis.com') 203 oauth2BaseUrl = oauth2AccountsHost + '/o/oauth2' 204 oauth2ApiBaseUrl = oauth2ApiHost + '/oauth2' 205 directoryApiBaseUrl = directoryApiHost + '/chromoting/v1' 206 replaceUrl(destination, 'OAUTH2_BASE_URL', oauth2BaseUrl) 207 replaceUrl(destination, 'OAUTH2_API_BASE_URL', oauth2ApiBaseUrl) 208 replaceUrl(destination, 'DIRECTORY_API_BASE_URL', directoryApiBaseUrl) 209 # Substitute hosts in the manifest's CSP list. 210 findAndReplace(os.path.join(destination, 'manifest.json'), 211 'OAUTH2_ACCOUNTS_HOST', oauth2AccountsHost) 212 # Ensure we list the API host only once if it's the same for multiple APIs. 213 googleApiHosts = ' '.join(set([oauth2ApiHost, directoryApiHost])) 214 findAndReplace(os.path.join(destination, 'manifest.json'), 215 'GOOGLE_API_HOSTS', googleApiHosts) 216 217 # WCS and the OAuth trampoline are both hosted on talkgadget. Split them into 218 # separate suffix/prefix variables to allow for wildcards in manifest.json. 219 talkGadgetHostSuffix = os.environ.get( 220 'TALK_GADGET_HOST_SUFFIX', 'talkgadget.google.com') 221 talkGadgetHostPrefix = os.environ.get( 222 'TALK_GADGET_HOST_PREFIX', 'https://chromoting-client.') 223 oauth2RedirectHostPrefix = os.environ.get( 224 'OAUTH2_REDIRECT_HOST_PREFIX', 'https://chromoting-oauth.') 225 226 # Use a wildcard in the manifest.json host specs if the prefixes differ. 227 talkGadgetHostJs = talkGadgetHostPrefix + talkGadgetHostSuffix 228 talkGadgetBaseUrl = talkGadgetHostJs + '/talkgadget/' 229 if talkGadgetHostPrefix == oauth2RedirectHostPrefix: 230 talkGadgetHostJson = talkGadgetHostJs 231 else: 232 talkGadgetHostJson = 'https://*.' + talkGadgetHostSuffix 233 234 # Set the correct OAuth2 redirect URL. 235 oauth2RedirectHostJs = oauth2RedirectHostPrefix + talkGadgetHostSuffix 236 oauth2RedirectHostJson = talkGadgetHostJson 237 oauth2RedirectPath = '/talkgadget/oauth/chrome-remote-desktop' 238 oauth2RedirectBaseUrlJs = oauth2RedirectHostJs + oauth2RedirectPath 239 oauth2RedirectBaseUrlJson = oauth2RedirectHostJson + oauth2RedirectPath 240 if buildtype == 'Official': 241 oauth2RedirectUrlJs = ("'" + oauth2RedirectBaseUrlJs + 242 "/rel/' + chrome.i18n.getMessage('@@extension_id')") 243 oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/rel/*' 244 else: 245 oauth2RedirectUrlJs = "'" + oauth2RedirectBaseUrlJs + "/dev'" 246 oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/dev*' 247 thirdPartyAuthUrlJs = "'" + oauth2RedirectBaseUrlJs + "/thirdpartyauth'" 248 thirdPartyAuthUrlJson = oauth2RedirectBaseUrlJson + '/thirdpartyauth*' 249 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 250 "'TALK_GADGET_URL'", "'" + talkGadgetBaseUrl + "'") 251 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 252 "'OAUTH2_REDIRECT_URL'", oauth2RedirectUrlJs) 253 findAndReplace(os.path.join(destination, 'manifest.json'), 254 'TALK_GADGET_HOST', talkGadgetHostJson) 255 findAndReplace(os.path.join(destination, 'manifest.json'), 256 'OAUTH2_REDIRECT_URL', oauth2RedirectUrlJson) 257 258 # Configure xmpp server and directory bot settings in the plugin. 259 xmppServerAddress = os.environ.get( 260 'XMPP_SERVER_ADDRESS', 'talk.google.com:5222') 261 xmppServerUseTls = os.environ.get('XMPP_SERVER_USE_TLS', 'true') 262 directoryBotJid = os.environ.get( 263 'DIRECTORY_BOT_JID', 'remoting@bot.talk.google.com') 264 265 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 266 "'XMPP_SERVER_ADDRESS'", "'" + xmppServerAddress + "'") 267 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 268 "Boolean('XMPP_SERVER_USE_TLS')", xmppServerUseTls) 269 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 270 "'DIRECTORY_BOT_JID'", "'" + directoryBotJid + "'") 271 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 272 "'THIRD_PARTY_AUTH_REDIRECT_URL'", 273 thirdPartyAuthUrlJs) 274 findAndReplace(os.path.join(destination, 'manifest.json'), 275 "THIRD_PARTY_AUTH_REDIRECT_URL", 276 thirdPartyAuthUrlJson) 277 278 # Set the correct API keys. 279 # For overriding the client ID/secret via env vars, see google_api_keys.py. 280 apiClientId = google_api_keys.GetClientID('REMOTING') 281 apiClientSecret = google_api_keys.GetClientSecret('REMOTING') 282 283 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 284 "'API_CLIENT_ID'", 285 "'" + apiClientId + "'") 286 findAndReplace(os.path.join(destination, 'plugin_settings.js'), 287 "'API_CLIENT_SECRET'", 288 "'" + apiClientSecret + "'") 289 290 # Use a consistent extension id for unofficial builds. 291 if buildtype != 'Official': 292 manifestKey = '"key": "remotingdevbuild",' 293 else: 294 manifestKey = '' 295 findAndReplace(os.path.join(destination, 'manifest.json'), 296 'MANIFEST_KEY_FOR_UNOFFICIAL_BUILD', manifestKey) 297 298 # Make the zipfile. 299 createZip(zip_path, destination) 300 301 return 0 302 303 304def main(): 305 if len(sys.argv) < 7: 306 print ('Usage: build-webapp.py ' 307 '<build-type> <version> <mime-type> <dst> <zip-path> <plugin> ' 308 '<other files...> [--patches <patches...>] ' 309 '[--locales <locales...>]') 310 return 1 311 312 arg_type = '' 313 files = [] 314 locales = [] 315 patches = [] 316 for arg in sys.argv[7:]: 317 if arg == '--locales' or arg == '--patches': 318 arg_type = arg 319 elif arg_type == '--locales': 320 locales.append(arg) 321 elif arg_type == '--patches': 322 patches.append(arg) 323 else: 324 files.append(arg) 325 326 return buildWebApp(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], 327 sys.argv[5], sys.argv[6], files, locales, patches) 328 329 330if __name__ == '__main__': 331 sys.exit(main()) 332