Added unit test for HostDaemonFacade.
[chromium-blink-merge.git] / remoting / webapp / build-webapp.py
blobfb10150c125d7d14c5fec51d1d7252f366058038
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.
6 """Creates a directory with with the unpacked contents of the remoting webapp.
8 The directory will contain a copy-of or a link-to to all remoting webapp
9 resources. This includes HTML/JS and any plugin binaries. The script also
10 massages resulting files appropriately with host plugin data. Finally,
11 a zip archive for all of the above is produced.
12 """
14 # Python 2.5 compatibility
15 from __future__ import with_statement
17 import argparse
18 import io
19 import os
20 import platform
21 import re
22 import shutil
23 import subprocess
24 import sys
25 import time
26 import zipfile
28 # Update the module path, assuming that this script is in src/remoting/webapp,
29 # and that the google_api_keys module is in src/google_apis. Note that
30 # sys.path[0] refers to the directory containing this script.
31 if __name__ == '__main__':
32 sys.path.append(
33 os.path.abspath(os.path.join(sys.path[0], '../../google_apis')))
34 import google_api_keys
37 def findAndReplace(filepath, findString, replaceString):
38 """Does a search and replace on the contents of a file."""
39 oldFilename = os.path.basename(filepath) + '.old'
40 oldFilepath = os.path.join(os.path.dirname(filepath), oldFilename)
41 os.rename(filepath, oldFilepath)
42 with open(oldFilepath) as input:
43 with open(filepath, 'w') as output:
44 for s in input:
45 output.write(s.replace(findString, replaceString))
46 os.remove(oldFilepath)
49 def createZip(zip_path, directory):
50 """Creates a zipfile at zip_path for the given directory."""
51 zipfile_base = os.path.splitext(os.path.basename(zip_path))[0]
52 zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
53 for (root, dirs, files) in os.walk(directory):
54 for f in files:
55 full_path = os.path.join(root, f)
56 rel_path = os.path.relpath(full_path, directory)
57 zip.write(full_path, os.path.join(zipfile_base, rel_path))
58 zip.close()
61 def replaceString(destination, placeholder, value):
62 findAndReplace(os.path.join(destination, 'plugin_settings.js'),
63 "'" + placeholder + "'", "'" + value + "'")
66 def replaceBool(destination, placeholder, value):
67 # Look for a "!!" in the source code so the expession we're
68 # replacing looks like a boolean to the compiler. A single "!"
69 # would satisfy the compiler but might confused human readers.
70 findAndReplace(os.path.join(destination, 'plugin_settings.js'),
71 "!!'" + placeholder + "'", 'true' if value else 'false')
74 def parseBool(boolStr):
75 """Tries to parse a string as a boolean value.
77 Returns a bool on success; raises ValueError on failure.
78 """
79 lower = boolStr.tolower()
80 if lower in ['0', 'false']: return False
81 if lower in ['1', 'true']: return True
82 raise ValueError('not a boolean string {!r}'.format(boolStr))
85 def getenvBool(name, defaultValue):
86 """Gets an environment value as a boolean."""
87 rawValue = os.environ.get(name)
88 if rawValue is None:
89 return defaultValue
90 try:
91 return parseBool(rawValue)
92 except ValueError:
93 raise Exception('Value of ${} must be boolean!'.format(name))
96 def processJinjaTemplate(input_file, include_paths, output_file, context):
97 jinja2_path = os.path.normpath(
98 os.path.join(os.path.abspath(__file__),
99 '../../../third_party/jinja2'))
100 sys.path.append(os.path.split(jinja2_path)[0])
101 import jinja2
102 (template_path, template_name) = os.path.split(input_file)
103 include_paths = [template_path] + include_paths
104 env = jinja2.Environment(loader=jinja2.FileSystemLoader(include_paths))
105 template = env.get_template(template_name)
106 rendered = template.render(context)
107 io.open(output_file, 'w', encoding='utf-8').write(rendered)
109 def buildWebApp(buildtype, version, destination, zip_path,
110 manifest_template, webapp_type, appid, app_client_id, app_name,
111 app_description, app_capabilities, manifest_key, files,
112 locales_listfile, jinja_paths, service_environment, use_gcd):
113 """Does the main work of building the webapp directory and zipfile.
115 Args:
116 buildtype: the type of build ("Official", "Release" or "Dev").
117 destination: A string with path to directory where the webapp will be
118 written.
119 zipfile: A string with path to the zipfile to create containing the
120 contents of |destination|.
121 manifest_template: jinja2 template file for manifest.
122 webapp_type: webapp type ("v1", "v2", "v2_pnacl" or "app_remoting").
123 appid: A string with the Remoting Application Id (only used for app
124 remoting webapps). If supplied, it defaults to using the
125 test API server.
126 app_client_id: The OAuth2 client ID for the webapp.
127 app_name: A string with the name of the application.
128 app_description: A string with the description of the application.
129 app_capabilities: A set of strings naming the capabilities that should be
130 enabled for this application.
131 manifest_key: The manifest key for the webapp.
132 files: An array of strings listing the paths for resources to include
133 in this webapp.
134 locales_listfile: The name of a file containing a list of locales, one per
135 line, which are copied, along with their directory structure, from
136 the _locales directory down.
137 jinja_paths: An array of paths to search for {%include} directives in
138 addition to the directory containing the manifest template.
139 service_environment: Used to point the webapp to one of the
140 dev/test/staging/prod/prod-testing environments
141 use_gcd: True if GCD support should be enabled.
144 # Load the locales files from the locales_listfile.
145 if not locales_listfile:
146 raise Exception('You must specify a locales_listfile')
147 locales = []
148 with open(locales_listfile) as input:
149 for s in input:
150 locales.append(s.rstrip())
152 # Ensure a fresh directory.
153 try:
154 shutil.rmtree(destination)
155 except OSError:
156 if os.path.exists(destination):
157 raise
158 else:
159 pass
160 os.mkdir(destination, 0775)
162 if buildtype != 'Official' and buildtype != 'Release' and buildtype != 'Dev':
163 raise Exception('Unknown buildtype: ' + buildtype)
165 jinja_context = {
166 'webapp_type': webapp_type,
167 'buildtype': buildtype,
170 # Copy all the files.
171 for current_file in files:
172 destination_file = os.path.join(destination, os.path.basename(current_file))
174 # Process *.jinja2 files as jinja2 templates
175 if current_file.endswith(".jinja2"):
176 destination_file = destination_file[:-len(".jinja2")]
177 processJinjaTemplate(current_file, jinja_paths,
178 destination_file, jinja_context)
179 else:
180 shutil.copy2(current_file, destination_file)
182 # Copy all the locales, preserving directory structure
183 destination_locales = os.path.join(destination, '_locales')
184 os.mkdir(destination_locales, 0775)
185 remoting_locales = os.path.join(destination, 'remoting_locales')
186 os.mkdir(remoting_locales, 0775)
187 for current_locale in locales:
188 extension = os.path.splitext(current_locale)[1]
189 if extension == '.json':
190 locale_id = os.path.split(os.path.split(current_locale)[0])[1]
191 destination_dir = os.path.join(destination_locales, locale_id)
192 destination_file = os.path.join(destination_dir,
193 os.path.split(current_locale)[1])
194 os.mkdir(destination_dir, 0775)
195 shutil.copy2(current_locale, destination_file)
196 elif extension == '.pak':
197 destination_file = os.path.join(remoting_locales,
198 os.path.split(current_locale)[1])
199 shutil.copy2(current_locale, destination_file)
200 else:
201 raise Exception('Unknown extension: ' + current_locale)
203 # Set client plugin type.
204 # TODO(wez): Use 'native' in app_remoting until b/17441659 is resolved.
205 client_plugin = 'pnacl' if webapp_type == 'v2_pnacl' else 'native'
206 findAndReplace(os.path.join(destination, 'plugin_settings.js'),
207 "'CLIENT_PLUGIN_TYPE'", "'" + client_plugin + "'")
209 # Allow host names for google services/apis to be overriden via env vars.
210 oauth2AccountsHost = os.environ.get(
211 'OAUTH2_ACCOUNTS_HOST', 'https://accounts.google.com')
212 oauth2ApiHost = os.environ.get(
213 'OAUTH2_API_HOST', 'https://www.googleapis.com')
214 directoryApiHost = os.environ.get(
215 'DIRECTORY_API_HOST', 'https://www.googleapis.com')
217 is_app_remoting_webapp = webapp_type == 'app_remoting'
218 is_prod_service_environment = service_environment == 'prod' or \
219 service_environment == 'prod-testing'
220 if is_app_remoting_webapp:
221 appRemotingApiHost = os.environ.get(
222 'APP_REMOTING_API_HOST', None)
223 appRemotingApplicationId = os.environ.get(
224 'APP_REMOTING_APPLICATION_ID', None)
226 # Release/Official builds are special because they are what we will upload
227 # to the web store. The checks below will validate that prod builds are
228 # being generated correctly (no overrides) and with the correct buildtype.
229 # They also verify that folks are not accidentally building dev/test/staging
230 # apps for release (no impersonation) instead of dev.
231 if is_prod_service_environment and buildtype == 'Dev':
232 raise Exception("Prod environment cannot be built for 'dev' builds")
234 if buildtype != 'Dev':
235 if not is_prod_service_environment:
236 raise Exception('Invalid service_environment targeted for '
237 + buildtype + ': ' + service_environment)
238 if 'out/Release' not in destination and 'out\Release' not in destination:
239 raise Exception('Prod builds must be placed in the out/Release folder')
240 if appid != None:
241 raise Exception('Cannot pass in an appid for '
242 + buildtype + ' builds: ' + service_environment)
243 if appRemotingApiHost != None:
244 raise Exception('Cannot set APP_REMOTING_API_HOST env var for '
245 + buildtype + ' builds')
246 if appRemotingApplicationId != None:
247 raise Exception('Cannot set APP_REMOTING_APPLICATION_ID env var for '
248 + buildtype + ' builds')
250 # If an Application ID was set (either from service_environment variable or
251 # from a command line argument), hardcode it, otherwise get it at runtime.
252 effectiveAppId = appRemotingApplicationId or appid
253 if effectiveAppId:
254 appRemotingApplicationId = "'" + effectiveAppId + "'"
255 else:
256 appRemotingApplicationId = "chrome.i18n.getMessage('@@extension_id')"
257 findAndReplace(os.path.join(destination, 'plugin_settings.js'),
258 "'APP_REMOTING_APPLICATION_ID'", appRemotingApplicationId)
260 oauth2BaseUrl = oauth2AccountsHost + '/o/oauth2'
261 oauth2ApiBaseUrl = oauth2ApiHost + '/oauth2'
262 directoryApiBaseUrl = directoryApiHost + '/chromoting/v1'
264 if is_app_remoting_webapp:
265 # Set the apiary endpoint and then set the endpoint version
266 if not appRemotingApiHost:
267 if is_prod_service_environment:
268 appRemotingApiHost = 'https://www.googleapis.com'
269 else:
270 appRemotingApiHost = 'https://www-googleapis-test.sandbox.google.com'
272 if service_environment == 'dev':
273 appRemotingServicePath = '/appremoting/v1beta1_dev'
274 elif service_environment == 'test':
275 appRemotingServicePath = '/appremoting/v1beta1'
276 elif service_environment == 'staging':
277 appRemotingServicePath = '/appremoting/v1beta1_staging'
278 elif service_environment == 'prod':
279 appRemotingServicePath = '/appremoting/v1beta1'
280 elif service_environment == 'prod-testing':
281 appRemotingServicePath = '/appremoting/v1beta1_prod_testing'
282 else:
283 raise Exception('Unknown service environment: ' + service_environment)
284 appRemotingApiBaseUrl = appRemotingApiHost + appRemotingServicePath
285 else:
286 appRemotingApiBaseUrl = ''
288 replaceBool(destination, 'USE_GCD', use_gcd)
289 replaceString(destination, 'OAUTH2_BASE_URL', oauth2BaseUrl)
290 replaceString(destination, 'OAUTH2_API_BASE_URL', oauth2ApiBaseUrl)
291 replaceString(destination, 'DIRECTORY_API_BASE_URL', directoryApiBaseUrl)
292 if is_app_remoting_webapp:
293 replaceString(destination, 'APP_REMOTING_API_BASE_URL',
294 appRemotingApiBaseUrl)
296 # Substitute hosts in the manifest's CSP list.
297 # Ensure we list the API host only once if it's the same for multiple APIs.
298 googleApiHosts = ' '.join(set([oauth2ApiHost, directoryApiHost]))
300 # WCS and the OAuth trampoline are both hosted on talkgadget. Split them into
301 # separate suffix/prefix variables to allow for wildcards in manifest.json.
302 talkGadgetHostSuffix = os.environ.get(
303 'TALK_GADGET_HOST_SUFFIX', 'talkgadget.google.com')
304 talkGadgetHostPrefix = os.environ.get(
305 'TALK_GADGET_HOST_PREFIX', 'https://chromoting-client.')
306 oauth2RedirectHostPrefix = os.environ.get(
307 'OAUTH2_REDIRECT_HOST_PREFIX', 'https://chromoting-oauth.')
309 # Use a wildcard in the manifest.json host specs if the prefixes differ.
310 talkGadgetHostJs = talkGadgetHostPrefix + talkGadgetHostSuffix
311 talkGadgetBaseUrl = talkGadgetHostJs + '/talkgadget/'
312 if talkGadgetHostPrefix == oauth2RedirectHostPrefix:
313 talkGadgetHostJson = talkGadgetHostJs
314 else:
315 talkGadgetHostJson = 'https://*.' + talkGadgetHostSuffix
317 # Set the correct OAuth2 redirect URL.
318 oauth2RedirectHostJs = oauth2RedirectHostPrefix + talkGadgetHostSuffix
319 oauth2RedirectHostJson = talkGadgetHostJson
320 oauth2RedirectPath = '/talkgadget/oauth/chrome-remote-desktop'
321 oauth2RedirectBaseUrlJs = oauth2RedirectHostJs + oauth2RedirectPath
322 oauth2RedirectBaseUrlJson = oauth2RedirectHostJson + oauth2RedirectPath
323 if buildtype == 'Official':
324 oauth2RedirectUrlJs = ("'" + oauth2RedirectBaseUrlJs +
325 "/rel/' + chrome.i18n.getMessage('@@extension_id')")
326 oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/rel/*'
327 else:
328 oauth2RedirectUrlJs = "'" + oauth2RedirectBaseUrlJs + "/dev'"
329 oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/dev*'
330 thirdPartyAuthUrlJs = oauth2RedirectBaseUrlJs + '/thirdpartyauth'
331 thirdPartyAuthUrlJson = oauth2RedirectBaseUrlJson + '/thirdpartyauth*'
332 replaceString(destination, 'TALK_GADGET_URL', talkGadgetBaseUrl)
333 findAndReplace(os.path.join(destination, 'plugin_settings.js'),
334 "'OAUTH2_REDIRECT_URL'", oauth2RedirectUrlJs)
336 # Configure xmpp server and directory bot settings in the plugin.
337 replaceBool(
338 destination, 'XMPP_SERVER_USE_TLS',
339 getenvBool('XMPP_SERVER_USE_TLS', True))
340 xmppServer = os.environ.get('XMPP_SERVER',
341 'talk.google.com:443')
342 replaceString(destination, 'XMPP_SERVER', xmppServer)
343 replaceString(destination, 'DIRECTORY_BOT_JID',
344 os.environ.get('DIRECTORY_BOT_JID',
345 'remoting@bot.talk.google.com'))
346 replaceString(destination, 'THIRD_PARTY_AUTH_REDIRECT_URL',
347 thirdPartyAuthUrlJs)
349 # Set the correct API keys.
350 # For overriding the client ID/secret via env vars, see google_api_keys.py.
351 apiClientId = google_api_keys.GetClientID('REMOTING')
352 apiClientSecret = google_api_keys.GetClientSecret('REMOTING')
354 if is_app_remoting_webapp and buildtype != 'Dev':
355 if not app_client_id:
356 raise Exception('Invalid app_client_id passed in: "' +
357 app_client_id + '"')
358 apiClientIdV2 = app_client_id
359 else:
360 apiClientIdV2 = google_api_keys.GetClientID('REMOTING_IDENTITY_API')
362 replaceString(destination, 'API_CLIENT_ID', apiClientId)
363 replaceString(destination, 'API_CLIENT_SECRET', apiClientSecret)
365 # Write the application capabilities.
366 appCapabilities = ','.join(
367 ['remoting.ClientSession.Capability.' + x for x in app_capabilities])
368 findAndReplace(os.path.join(destination, 'app_capabilities.js'),
369 "'APPLICATION_CAPABILITIES'", appCapabilities)
371 # Use a consistent extension id for dev builds.
372 # AppRemoting builds always use the dev app id - the correct app id gets
373 # written into the manifest later.
374 if is_app_remoting_webapp:
375 if buildtype != 'Dev':
376 if not manifest_key:
377 raise Exception('Invalid manifest_key passed in: "' +
378 manifest_key + '"')
379 manifestKey = '"key": "' + manifest_key + '",'
380 else:
381 manifestKey = '"key": "remotingdevbuild",'
382 elif buildtype != 'Official':
383 # TODO(joedow): Update the chromoting webapp GYP entries to include keys.
384 manifestKey = '"key": "remotingdevbuild",'
385 else:
386 manifestKey = ''
388 # Generate manifest.
389 if manifest_template:
390 context = {
391 'webapp_type': webapp_type,
392 'FULL_APP_VERSION': version,
393 'MANIFEST_KEY_FOR_UNOFFICIAL_BUILD': manifestKey,
394 'OAUTH2_REDIRECT_URL': oauth2RedirectUrlJson,
395 'TALK_GADGET_HOST': talkGadgetHostJson,
396 'THIRD_PARTY_AUTH_REDIRECT_URL': thirdPartyAuthUrlJson,
397 'REMOTING_IDENTITY_API_CLIENT_ID': apiClientIdV2,
398 'OAUTH2_BASE_URL': oauth2BaseUrl,
399 'OAUTH2_API_BASE_URL': oauth2ApiBaseUrl,
400 'DIRECTORY_API_BASE_URL': directoryApiBaseUrl,
401 'APP_REMOTING_API_BASE_URL': appRemotingApiBaseUrl,
402 'OAUTH2_ACCOUNTS_HOST': oauth2AccountsHost,
403 'GOOGLE_API_HOSTS': googleApiHosts,
404 'APP_NAME': app_name,
405 'APP_DESCRIPTION': app_description,
406 'OAUTH_GDRIVE_SCOPE': '',
407 'USE_GCD': use_gcd,
408 'XMPP_SERVER': xmppServer,
410 if 'GOOGLE_DRIVE' in app_capabilities:
411 context['OAUTH_GDRIVE_SCOPE'] = ('https://docs.google.com/feeds/ '
412 'https://www.googleapis.com/auth/drive')
413 processJinjaTemplate(manifest_template,
414 jinja_paths,
415 os.path.join(destination, 'manifest.json'),
416 context)
418 # Make the zipfile.
419 createZip(zip_path, destination)
421 return 0
424 def main():
425 parser = argparse.ArgumentParser()
426 parser.add_argument('buildtype')
427 parser.add_argument('version')
428 parser.add_argument('destination')
429 parser.add_argument('zip_path')
430 parser.add_argument('manifest_template')
431 parser.add_argument('webapp_type')
432 parser.add_argument('files', nargs='*', metavar='file', default=[])
433 parser.add_argument('--app_name', metavar='NAME')
434 parser.add_argument('--app_description', metavar='TEXT')
435 parser.add_argument('--app_capabilities',
436 nargs='*', default=[], metavar='CAPABILITY')
437 parser.add_argument('--appid')
438 parser.add_argument('--app_client_id', default='')
439 parser.add_argument('--manifest_key', default='')
440 parser.add_argument('--locales_listfile', default='', metavar='PATH')
441 parser.add_argument('--jinja_paths', nargs='*', default=[], metavar='PATH')
442 parser.add_argument('--service_environment', default='', metavar='ENV')
443 parser.add_argument('--use_gcd', choices=['0', '1'], default='0')
445 args = parser.parse_args()
446 args.use_gcd = (args.use_gcd != '0')
447 args.app_capabilities = set(args.app_capabilities)
448 return buildWebApp(**vars(args))
451 if __name__ == '__main__':
452 sys.exit(main())