#!/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 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) os.rename(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 replaceUrl(destination, url_name, url_value): """Updates a URL in both plugin_settings.js and manifest.json.""" findAndReplace(os.path.join(destination, 'plugin_settings.js'), "'" + url_name + "'", "'" + url_value + "'") findAndReplace(os.path.join(destination, 'manifest.json'), url_name, url_value) def buildWebApp(buildtype, version, mimetype, destination, zip_path, plugin, files, locales, patches): """Does the main work of building the webapp directory and zipfile. Args: buildtype: the type of build ("Official" or "Dev") mimetype: A string with mimetype of plugin. 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|. plugin: A string with path to the binary plugin for this webapp. files: An array of strings listing the paths for resources to include in this webapp. locales: An array of strings listing locales, which are copied, along with their directory structure from the _locales directory down. patches: An array of strings listing patch files to be applied to the webapp directory. Paths in the patch file should be relative to the remoting/webapp directory, for example a/main.html. Since 'git diff -p' works relative to the src/ directory, patches obtained this way will need to be edited. """ # Ensure a fresh directory. try: shutil.rmtree(destination) except OSError: if os.path.exists(destination): raise else: pass os.mkdir(destination, 0775) # Use symlinks on linux and mac for faster compile/edit cycle. # # On Windows Vista platform.system() can return 'Microsoft' with some # versions of Python, see http://bugs.python.org/issue1082 # should_symlink = platform.system() not in ['Windows', 'Microsoft'] # # TODO(ajwong): Pending decision on http://crbug.com/27185 we may not be # able to load symlinked resources. should_symlink = False # Copy all the files. for current_file in files: destination_file = os.path.join(destination, os.path.basename(current_file)) destination_dir = os.path.dirname(destination_file) if not os.path.exists(destination_dir): os.makedirs(destination_dir, 0775) if should_symlink: # TODO(ajwong): Detect if we're vista or higher. Then use win32file # to create a symlink in that case. targetname = os.path.relpath(os.path.realpath(current_file), os.path.realpath(destination_file)) os.symlink(targetname, destination_file) 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); # Create fake plugin files to appease the manifest checker. # It requires that if there is a plugin listed in the manifest that # there be a file in the plugin with that name. names = [ 'remoting_host_plugin.dll', # Windows 'remoting_host_plugin.plugin', # Mac 'libremoting_host_plugin.ia32.so', # Linux 32 'libremoting_host_plugin.x64.so' # Linux 64 ] pluginName = os.path.basename(plugin) for name in names: if name != pluginName: path = os.path.join(destination, name) f = open(path, 'w') f.write("placeholder for %s" % (name)) f.close() # Copy the plugin. On some platforms (e.g. ChromeOS) plugin compilation may be # disabled, in which case we don't need to copy anything. if plugin: newPluginPath = os.path.join(destination, pluginName) if os.path.isdir(plugin): # On Mac we have a directory. shutil.copytree(plugin, newPluginPath) else: shutil.copy2(plugin, newPluginPath) # Strip the linux build. if ((platform.system() == 'Linux') and (buildtype == 'Official')): subprocess.call(["strip", newPluginPath]) # Patch the files, if necessary. Do this before updating any placeholders # in case any of the diff contexts refer to the placeholders. for patch in patches: patchfile = os.path.join(os.getcwd(), patch) if subprocess.call(['patch', '-d', destination, '-i', patchfile, '-p1', '-F0', '-s']) != 0: print 'Patch ' + patch + ' failed to apply.' return 1 # Set the version number in the manifest version. findAndReplace(os.path.join(destination, 'manifest.json'), 'FULL_APP_VERSION', version) # Set the correct mimetype. findAndReplace(os.path.join(destination, 'plugin_settings.js'), 'HOST_PLUGIN_MIMETYPE', mimetype) # 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') oauth2BaseUrl = oauth2AccountsHost + '/o/oauth2' oauth2ApiBaseUrl = oauth2ApiHost + '/oauth2' directoryApiBaseUrl = directoryApiHost + '/chromoting/v1' replaceUrl(destination, 'OAUTH2_BASE_URL', oauth2BaseUrl) replaceUrl(destination, 'OAUTH2_API_BASE_URL', oauth2ApiBaseUrl) replaceUrl(destination, 'DIRECTORY_API_BASE_URL', directoryApiBaseUrl) # Substitute hosts in the manifest's CSP list. findAndReplace(os.path.join(destination, 'manifest.json'), 'OAUTH2_ACCOUNTS_HOST', oauth2AccountsHost) # Ensure we list the API host only once if it's the same for multiple APIs. googleApiHosts = ' '.join(set([oauth2ApiHost, directoryApiHost])) findAndReplace(os.path.join(destination, 'manifest.json'), 'GOOGLE_API_HOSTS', googleApiHosts) # 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*' findAndReplace(os.path.join(destination, 'plugin_settings.js'), "'TALK_GADGET_URL'", "'" + talkGadgetBaseUrl + "'") findAndReplace(os.path.join(destination, 'plugin_settings.js'), "'OAUTH2_REDIRECT_URL'", oauth2RedirectUrlJs) findAndReplace(os.path.join(destination, 'manifest.json'), 'TALK_GADGET_HOST', talkGadgetHostJson) findAndReplace(os.path.join(destination, 'manifest.json'), 'OAUTH2_REDIRECT_URL', oauth2RedirectUrlJson) # Configure xmpp server and directory bot settings in the plugin. xmppServerAddress = os.environ.get( 'XMPP_SERVER_ADDRESS', 'talk.google.com:5222') xmppServerUseTls = os.environ.get('XMPP_SERVER_USE_TLS', 'true') directoryBotJid = os.environ.get( 'DIRECTORY_BOT_JID', 'remoting@bot.talk.google.com') findAndReplace(os.path.join(destination, 'plugin_settings.js'), "'XMPP_SERVER_ADDRESS'", "'" + xmppServerAddress + "'") findAndReplace(os.path.join(destination, 'plugin_settings.js'), "Boolean('XMPP_SERVER_USE_TLS')", xmppServerUseTls) findAndReplace(os.path.join(destination, 'plugin_settings.js'), "'DIRECTORY_BOT_JID'", "'" + directoryBotJid + "'") findAndReplace(os.path.join(destination, 'plugin_settings.js'), "'THIRD_PARTY_AUTH_REDIRECT_URL'", thirdPartyAuthUrlJs) findAndReplace(os.path.join(destination, 'manifest.json'), "THIRD_PARTY_AUTH_REDIRECT_URL", thirdPartyAuthUrlJson) # 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 = google_api_keys.GetClientID('REMOTING_IDENTITY_API') findAndReplace(os.path.join(destination, 'plugin_settings.js'), "'API_CLIENT_ID'", "'" + apiClientId + "'") findAndReplace(os.path.join(destination, 'plugin_settings.js'), "'API_CLIENT_SECRET'", "'" + apiClientSecret + "'") findAndReplace(os.path.join(destination, 'manifest.json'), '"REMOTING_IDENTITY_API_CLIENT_ID"', '"' + apiClientIdV2 + '"') # Use a consistent extension id for unofficial builds. if buildtype != 'Official': manifestKey = '"key": "remotingdevbuild",' else: manifestKey = '' findAndReplace(os.path.join(destination, 'manifest.json'), 'MANIFEST_KEY_FOR_UNOFFICIAL_BUILD', manifestKey) # Make the zipfile. createZip(zip_path, destination) return 0 def main(): if len(sys.argv) < 7: print ('Usage: build-webapp.py ' '<build-type> <version> <mime-type> <dst> <zip-path> <plugin> ' '<other files...> [--patches <patches...>] ' '[--locales <locales...>]') return 1 arg_type = '' files = [] locales = [] patches = [] for arg in sys.argv[7:]: if arg == '--locales' or arg == '--patches': arg_type = arg elif arg_type == '--locales': locales.append(arg) elif arg_type == '--patches': patches.append(arg) else: files.append(arg) return buildWebApp(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6], files, locales, patches) if __name__ == '__main__': sys.exit(main())