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