build-webapp.py revision 868fa2fe829687343ffae624259930155e16dbd8
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  locale_dir = "/_locales/"
128  for current_locale in locales:
129    pos = current_locale.find(locale_dir)
130    if (pos == -1):
131      raise Exception("Missing locales directory in " + current_locale)
132    subtree = current_locale[pos + len(locale_dir):]
133    pos = subtree.find("/")
134    if (pos == -1):
135      raise Exception("Malformed locale: " + current_locale)
136    locale_id = subtree[:pos]
137    messages = subtree[pos+1:]
138    destination_dir = os.path.join(destination_locales, locale_id)
139    destination_file = os.path.join(destination_dir, messages)
140    os.mkdir(destination_dir, 0775)
141    shutil.copy2(current_locale, destination_file)
142
143  # Create fake plugin files to appease the manifest checker.
144  # It requires that if there is a plugin listed in the manifest that
145  # there be a file in the plugin with that name.
146  names = [
147    'remoting_host_plugin.dll',  # Windows
148    'remoting_host_plugin.plugin',  # Mac
149    'libremoting_host_plugin.ia32.so',  # Linux 32
150    'libremoting_host_plugin.x64.so'  # Linux 64
151  ]
152  pluginName = os.path.basename(plugin)
153
154  for name in names:
155    if name != pluginName:
156      path = os.path.join(destination, name)
157      f = open(path, 'w')
158      f.write("placeholder for %s" % (name))
159      f.close()
160
161  # Copy the plugin. On some platforms (e.g. ChromeOS) plugin compilation may be
162  # disabled, in which case we don't need to copy anything.
163  if plugin:
164    newPluginPath = os.path.join(destination, pluginName)
165    if os.path.isdir(plugin):
166      # On Mac we have a directory.
167      shutil.copytree(plugin, newPluginPath)
168    else:
169      shutil.copy2(plugin, newPluginPath)
170
171    # Strip the linux build.
172    if ((platform.system() == 'Linux') and (buildtype == 'Official')):
173      subprocess.call(["strip", newPluginPath])
174
175  # Patch the files, if necessary. Do this before updating any placeholders
176  # in case any of the diff contexts refer to the placeholders.
177  for patch in patches:
178    patchfile = os.path.join(os.getcwd(), patch)
179    if subprocess.call(['patch', '-d', destination, '-i', patchfile,
180                        '-p1', '-F0', '-s']) != 0:
181      print 'Patch ' + patch + ' failed to apply.'
182      return 1
183
184  # Set the version number in the manifest version.
185  findAndReplace(os.path.join(destination, 'manifest.json'),
186                 'FULL_APP_VERSION',
187                 version)
188
189  # Set the correct mimetype.
190  findAndReplace(os.path.join(destination, 'plugin_settings.js'),
191                 'HOST_PLUGIN_MIMETYPE',
192                 mimetype)
193
194  # Allow host names for google services/apis to be overriden via env vars.
195  oauth2AccountsHost = os.environ.get(
196      'OAUTH2_ACCOUNTS_HOST', 'https://accounts.google.com')
197  oauth2ApiHost = os.environ.get(
198      'OAUTH2_API_HOST', 'https://www.googleapis.com')
199  directoryApiHost = os.environ.get(
200      'DIRECTORY_API_HOST', 'https://www.googleapis.com')
201  oauth2BaseUrl = oauth2AccountsHost + '/o/oauth2'
202  oauth2ApiBaseUrl = oauth2ApiHost + '/oauth2'
203  directoryApiBaseUrl = directoryApiHost + '/chromoting/v1'
204  replaceUrl(destination, 'OAUTH2_BASE_URL', oauth2BaseUrl)
205  replaceUrl(destination, 'OAUTH2_API_BASE_URL', oauth2ApiBaseUrl)
206  replaceUrl(destination, 'DIRECTORY_API_BASE_URL', directoryApiBaseUrl)
207  # Substitute hosts in the manifest's CSP list.
208  findAndReplace(os.path.join(destination, 'manifest.json'),
209                 'OAUTH2_ACCOUNTS_HOST', oauth2AccountsHost)
210  # Ensure we list the API host only once if it's the same for multiple APIs.
211  googleApiHosts = ' '.join(set([oauth2ApiHost, directoryApiHost]))
212  findAndReplace(os.path.join(destination, 'manifest.json'),
213                 'GOOGLE_API_HOSTS', googleApiHosts)
214
215  # WCS and the OAuth trampoline are both hosted on talkgadget. Split them into
216  # separate suffix/prefix variables to allow for wildcards in manifest.json.
217  talkGadgetHostSuffix = os.environ.get(
218      'TALK_GADGET_HOST_SUFFIX', 'talkgadget.google.com')
219  talkGadgetHostPrefix = os.environ.get(
220      'TALK_GADGET_HOST_PREFIX', 'https://chromoting-client.')
221  oauth2RedirectHostPrefix = os.environ.get(
222      'OAUTH2_REDIRECT_HOST_PREFIX', 'https://chromoting-oauth.')
223
224  # Use a wildcard in the manifest.json host specs if the prefixes differ.
225  talkGadgetHostJs = talkGadgetHostPrefix + talkGadgetHostSuffix
226  talkGadgetBaseUrl = talkGadgetHostJs + '/talkgadget/'
227  if talkGadgetHostPrefix == oauth2RedirectHostPrefix:
228    talkGadgetHostJson = talkGadgetHostJs
229  else:
230    talkGadgetHostJson = 'https://*.' + talkGadgetHostSuffix
231
232  # Set the correct OAuth2 redirect URL.
233  oauth2RedirectHostJs = oauth2RedirectHostPrefix + talkGadgetHostSuffix
234  oauth2RedirectHostJson = talkGadgetHostJson
235  oauth2RedirectPath = '/talkgadget/oauth/chrome-remote-desktop'
236  oauth2RedirectBaseUrlJs = oauth2RedirectHostJs + oauth2RedirectPath
237  oauth2RedirectBaseUrlJson = oauth2RedirectHostJson + oauth2RedirectPath
238  if buildtype == 'Official':
239    oauth2RedirectUrlJs = ("'" + oauth2RedirectBaseUrlJs +
240                           "/rel/' + chrome.i18n.getMessage('@@extension_id')")
241    oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/rel/*'
242  else:
243    oauth2RedirectUrlJs = "'" + oauth2RedirectBaseUrlJs + "/dev'"
244    oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/dev*'
245  thirdPartyAuthUrlJs = "'" + oauth2RedirectBaseUrlJs + "/thirdpartyauth'"
246  thirdPartyAuthUrlJson = oauth2RedirectBaseUrlJson + '/thirdpartyauth*'
247  findAndReplace(os.path.join(destination, 'plugin_settings.js'),
248                 "'TALK_GADGET_URL'", "'" + talkGadgetBaseUrl + "'")
249  findAndReplace(os.path.join(destination, 'plugin_settings.js'),
250                 "'OAUTH2_REDIRECT_URL'", oauth2RedirectUrlJs)
251  findAndReplace(os.path.join(destination, 'manifest.json'),
252                 'TALK_GADGET_HOST', talkGadgetHostJson)
253  findAndReplace(os.path.join(destination, 'manifest.json'),
254                 'OAUTH2_REDIRECT_URL', oauth2RedirectUrlJson)
255
256  # Configure xmpp server and directory bot settings in the plugin.
257  xmppServerAddress = os.environ.get(
258      'XMPP_SERVER_ADDRESS', 'talk.google.com:5222')
259  xmppServerUseTls = os.environ.get('XMPP_SERVER_USE_TLS', 'true')
260  directoryBotJid = os.environ.get(
261      'DIRECTORY_BOT_JID', 'remoting@bot.talk.google.com')
262
263  findAndReplace(os.path.join(destination, 'plugin_settings.js'),
264                 "'XMPP_SERVER_ADDRESS'", "'" + xmppServerAddress + "'")
265  findAndReplace(os.path.join(destination, 'plugin_settings.js'),
266                 "Boolean('XMPP_SERVER_USE_TLS')", xmppServerUseTls)
267  findAndReplace(os.path.join(destination, 'plugin_settings.js'),
268                 "'DIRECTORY_BOT_JID'", "'" + directoryBotJid + "'")
269  findAndReplace(os.path.join(destination, 'plugin_settings.js'),
270                 "'THIRD_PARTY_AUTH_REDIRECT_URL'",
271                 thirdPartyAuthUrlJs)
272  findAndReplace(os.path.join(destination, 'manifest.json'),
273                 "THIRD_PARTY_AUTH_REDIRECT_URL",
274                 thirdPartyAuthUrlJson)
275
276  # Set the correct API keys.
277  # For overriding the client ID/secret via env vars, see google_api_keys.py.
278  apiClientId = google_api_keys.GetClientID('REMOTING')
279  apiClientSecret = google_api_keys.GetClientSecret('REMOTING')
280
281  findAndReplace(os.path.join(destination, 'plugin_settings.js'),
282                 "'API_CLIENT_ID'",
283                 "'" + apiClientId + "'")
284  findAndReplace(os.path.join(destination, 'plugin_settings.js'),
285                 "'API_CLIENT_SECRET'",
286                 "'" + apiClientSecret + "'")
287
288  # Use a consistent extension id for unofficial builds.
289  if buildtype != 'Official':
290    manifestKey = '"key": "remotingdevbuild",'
291  else:
292    manifestKey = ''
293  findAndReplace(os.path.join(destination, 'manifest.json'),
294                 'MANIFEST_KEY_FOR_UNOFFICIAL_BUILD', manifestKey)
295
296  # Make the zipfile.
297  createZip(zip_path, destination)
298
299  return 0
300
301
302def main():
303  if len(sys.argv) < 7:
304    print ('Usage: build-webapp.py '
305           '<build-type> <version> <mime-type> <dst> <zip-path> <plugin> '
306           '<other files...> [--patches <patches...>] '
307           '[--locales <locales...>]')
308    return 1
309
310  arg_type = ''
311  files = []
312  locales = []
313  patches = []
314  for arg in sys.argv[7:]:
315    if arg == '--locales' or arg == '--patches':
316      arg_type = arg
317    elif arg_type == '--locales':
318      locales.append(arg)
319    elif arg_type == '--patches':
320      patches.append(arg)
321    else:
322      files.append(arg)
323
324  return buildWebApp(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4],
325                     sys.argv[5], sys.argv[6], files, locales, patches)
326
327
328if __name__ == '__main__':
329  sys.exit(main())
330