Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / remoting / webapp / build-webapp.py
blobca0fbd24db21f25c982f6c198d72cd435aebf255
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.lower()
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)
110 def buildWebApp(buildtype, version, destination, zip_path,
111 manifest_template, webapp_type, appid, app_client_id, app_name,
112 app_description, app_capabilities, manifest_key, files,
113 files_listfile, locales_listfile, jinja_paths,
114 service_environment, use_gcd):
115 """Does the main work of building the webapp directory and zipfile.
117 Args:
118 buildtype: the type of build ("Official", "Release" or "Dev").
119 destination: A string with path to directory where the webapp will be
120 written.
121 zipfile: A string with path to the zipfile to create containing the
122 contents of |destination|.
123 manifest_template: jinja2 template file for manifest.
124 webapp_type: webapp type:
125 For DesktopRemoting: "desktop"
126 For AppRemoting: "app_remoting" or "shared_module"
127 appid: A string with the Remoting Application Id (only used for app
128 remoting webapps). If supplied, it defaults to using the
129 test API server.
130 app_client_id: The OAuth2 client ID for the webapp.
131 app_name: A string with the name of the application.
132 app_description: A string with the description of the application.
133 app_capabilities: A set of strings naming the capabilities that should be
134 enabled for this application.
135 manifest_key: The manifest key for the webapp.
136 files: An array of strings listing the paths for resources to include
137 in this webapp.
138 files_listfile: The name of a file containing a list of files, one per
139 line, identifying the resources to include in this webapp.
140 This is an alternate to specifying the files directly via
141 the 'files' option. The files listed in this file are
142 appended to the files passed via the 'files' option, if any.
143 locales_listfile: The name of a file containing a list of locales, one per
144 line, which are copied, along with their directory
145 structure, from the _locales directory down.
146 jinja_paths: An array of paths to search for {%include} directives in
147 addition to the directory containing the manifest template.
148 service_environment: Used to point the webapp to the dev/prod environments.
149 use_gcd: True if GCD support should be enabled.
152 # Load the locales files from the locales_listfile.
153 if not locales_listfile:
154 raise Exception('You must specify a locales_listfile')
155 locales = []
156 with open(locales_listfile) as input:
157 for s in input:
158 locales.append(s.rstrip())
160 # Load the files from the files_listfile.
161 if files_listfile:
162 with open(files_listfile) as input:
163 for s in input:
164 files.append(s.rstrip())
166 # Ensure a fresh directory.
167 try:
168 shutil.rmtree(destination)
169 except OSError:
170 if os.path.exists(destination):
171 raise
172 else:
173 pass
174 os.makedirs(destination, 0775)
176 if buildtype != 'Official' and buildtype != 'Release' and buildtype != 'Dev':
177 raise Exception('Unknown buildtype: ' + buildtype)
179 jinja_context = {
180 'webapp_type': webapp_type,
181 'buildtype': buildtype,
184 # Copy all the files.
185 for current_file in files:
186 destination_file = os.path.join(destination, os.path.basename(current_file))
188 # Process *.jinja2 files as jinja2 templates
189 if current_file.endswith(".jinja2"):
190 destination_file = destination_file[:-len(".jinja2")]
191 processJinjaTemplate(current_file, jinja_paths,
192 destination_file, jinja_context)
193 else:
194 shutil.copy2(current_file, destination_file)
196 # Copy all the locales, preserving directory structure
197 destination_locales = os.path.join(destination, '_locales')
198 os.mkdir(destination_locales, 0775)
199 remoting_locales = os.path.join(destination, 'remoting_locales')
200 os.mkdir(remoting_locales, 0775)
201 for current_locale in locales:
202 extension = os.path.splitext(current_locale)[1]
203 if extension == '.json':
204 locale_id = os.path.split(os.path.split(current_locale)[0])[1]
205 destination_dir = os.path.join(destination_locales, locale_id)
206 destination_file = os.path.join(destination_dir,
207 os.path.split(current_locale)[1])
208 os.mkdir(destination_dir, 0775)
209 shutil.copy2(current_locale, destination_file)
210 elif extension == '.pak':
211 destination_file = os.path.join(remoting_locales,
212 os.path.split(current_locale)[1])
213 shutil.copy2(current_locale, destination_file)
214 else:
215 raise Exception('Unknown extension: ' + current_locale)
217 is_app_remoting_webapp = webapp_type == 'app_remoting'
218 is_app_remoting_shared_module = webapp_type == 'shared_module'
219 is_app_remoting = is_app_remoting_webapp or is_app_remoting_shared_module
220 is_prod_service_environment = service_environment == 'prod'
221 is_desktop_remoting = not is_app_remoting
223 # Allow host names for google services/apis to be overriden via env vars.
224 oauth2AccountsHost = os.environ.get(
225 'OAUTH2_ACCOUNTS_HOST', 'https://accounts.google.com')
226 oauth2ApiHost = os.environ.get(
227 'OAUTH2_API_HOST', 'https://www.googleapis.com')
228 directoryApiHost = os.environ.get(
229 'DIRECTORY_API_HOST', 'https://www.googleapis.com')
230 remotingApiHost = os.environ.get(
231 'REMOTING_API_HOST', 'https://remoting-pa.googleapis.com')
233 if is_app_remoting:
234 appRemotingApiHost = os.environ.get(
235 'APP_REMOTING_API_HOST', None)
237 if is_app_remoting_webapp:
238 appRemotingApplicationId = os.environ.get(
239 'APP_REMOTING_APPLICATION_ID', None)
241 # Release/Official builds are special because they are what we will upload
242 # to the web store. The checks below will validate that prod builds are
243 # being generated correctly (no overrides) and with the correct buildtype.
244 # They also verify that folks are not accidentally building dev apps for
245 # Release (no impersonation) instead of Dev.
246 if is_prod_service_environment and buildtype == 'Dev':
247 raise Exception("Prod environment cannot be built for 'dev' builds")
249 if buildtype != 'Dev':
250 if not is_prod_service_environment:
251 raise Exception('Invalid service_environment targeted for '
252 + buildtype + ': ' + service_environment)
253 if appid != None:
254 raise Exception('Cannot pass in an appid for '
255 + buildtype + ' builds: ' + service_environment)
256 if appRemotingApiHost != None:
257 raise Exception('Cannot set APP_REMOTING_API_HOST env var for '
258 + buildtype + ' builds')
259 if appRemotingApplicationId != None:
260 raise Exception('Cannot set APP_REMOTING_APPLICATION_ID env var for '
261 + buildtype + ' builds')
263 # If an Application ID was set (either from service_environment variable or
264 # from a command line argument), hardcode it, otherwise get it at runtime.
265 effectiveAppId = appRemotingApplicationId or appid
266 if effectiveAppId:
267 appRemotingApplicationId = "'" + effectiveAppId + "'"
268 else:
269 appRemotingApplicationId = "chrome.i18n.getMessage('@@extension_id')"
270 findAndReplace(os.path.join(destination, 'arv_main.js'),
271 "'APP_REMOTING_APPLICATION_ID'", appRemotingApplicationId)
273 oauth2BaseUrl = oauth2AccountsHost + '/o/oauth2'
274 oauth2ApiBaseUrl = oauth2ApiHost + '/oauth2'
275 directoryApiBaseUrl = directoryApiHost + '/chromoting/v1'
276 telemetryApiBaseUrl = remotingApiHost + '/v1/events'
278 if is_app_remoting:
279 # Set the base endpoint url first and then set the endpoint version.
280 if not appRemotingApiHost:
281 if is_prod_service_environment:
282 appRemotingApiHost = 'https://www.googleapis.com'
283 else:
284 appRemotingApiHost = 'https://www-googleapis-test.sandbox.google.com'
286 # TODO(garykac) Currently, the shared module is always set up for the
287 # dev service_environment. Update build so that the dev environment can
288 # be controlled by the app stub rather than hard-coded into the shared
289 # module.
290 if service_environment == 'dev' or is_app_remoting_shared_module:
291 appRemotingServicePath = '/appremoting/v1beta1_dev'
292 elif service_environment == 'prod':
293 appRemotingServicePath = '/appremoting/v1beta1'
294 else:
295 raise Exception('Unknown service environment: ' + service_environment)
296 appRemotingApiBaseUrl = appRemotingApiHost + appRemotingServicePath
297 else:
298 appRemotingApiBaseUrl = ''
300 # TODO(garykac) replaceString (et al.) implictly update plugin_settings.js,
301 # which doesn't exist for the app stub. We need to move app-specific
302 # AppRemoting options into arv_main.js.
303 if not is_app_remoting_webapp:
304 replaceBool(destination, 'USE_GCD', use_gcd)
305 replaceString(destination, 'OAUTH2_BASE_URL', oauth2BaseUrl)
306 replaceString(destination, 'OAUTH2_API_BASE_URL', oauth2ApiBaseUrl)
307 replaceString(destination, 'DIRECTORY_API_BASE_URL', directoryApiBaseUrl)
308 replaceString(destination, 'TELEMETRY_API_BASE_URL', telemetryApiBaseUrl)
309 if is_app_remoting:
310 replaceString(destination, 'APP_REMOTING_API_BASE_URL',
311 appRemotingApiBaseUrl)
313 # Substitute hosts in the manifest's CSP list.
314 # Ensure we list the API host only once if it's the same for multiple APIs.
315 googleApiHosts = ' '.join(set([oauth2ApiHost, directoryApiHost]))
317 # WCS and the OAuth trampoline are both hosted on talkgadget. Split them into
318 # separate suffix/prefix variables to allow for wildcards in manifest.json.
319 talkGadgetHostSuffix = os.environ.get(
320 'TALK_GADGET_HOST_SUFFIX', 'talkgadget.google.com')
321 talkGadgetHostPrefix = os.environ.get(
322 'TALK_GADGET_HOST_PREFIX', 'https://chromoting-client.')
323 oauth2RedirectHostPrefix = os.environ.get(
324 'OAUTH2_REDIRECT_HOST_PREFIX', 'https://chromoting-oauth.')
326 # Use a wildcard in the manifest.json host specs if the prefixes differ.
327 talkGadgetHostJs = talkGadgetHostPrefix + talkGadgetHostSuffix
328 talkGadgetBaseUrl = talkGadgetHostJs + '/talkgadget'
329 if talkGadgetHostPrefix == oauth2RedirectHostPrefix:
330 talkGadgetHostJson = talkGadgetHostJs
331 else:
332 talkGadgetHostJson = 'https://*.' + talkGadgetHostSuffix
334 # Set the correct OAuth2 redirect URL.
335 oauth2RedirectHostJs = oauth2RedirectHostPrefix + talkGadgetHostSuffix
336 oauth2RedirectHostJson = talkGadgetHostJson
337 oauth2RedirectPath = '/talkgadget/oauth/chrome-remote-desktop'
338 oauth2RedirectBaseUrlJs = oauth2RedirectHostJs + oauth2RedirectPath
339 oauth2RedirectBaseUrlJson = oauth2RedirectHostJson + oauth2RedirectPath
340 if buildtype == 'Official':
341 oauth2RedirectUrlJs = ("'" + oauth2RedirectBaseUrlJs +
342 "/rel/' + chrome.i18n.getMessage('@@extension_id')")
343 oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/rel/*'
344 else:
345 oauth2RedirectUrlJs = "'" + oauth2RedirectBaseUrlJs + "/dev'"
346 oauth2RedirectUrlJson = oauth2RedirectBaseUrlJson + '/dev*'
347 thirdPartyAuthUrlJs = oauth2RedirectBaseUrlJs + '/thirdpartyauth'
348 thirdPartyAuthUrlJson = oauth2RedirectBaseUrlJson + '/thirdpartyauth*'
349 xmppServer = os.environ.get('XMPP_SERVER', 'talk.google.com:443')
351 if not is_app_remoting_webapp:
352 replaceString(destination, 'TALK_GADGET_URL', talkGadgetBaseUrl)
353 findAndReplace(os.path.join(destination, 'plugin_settings.js'),
354 "'OAUTH2_REDIRECT_URL'", oauth2RedirectUrlJs)
356 # Configure xmpp server and directory bot settings in the plugin.
357 xmpp_server_user_tls = getenvBool('XMPP_SERVER_USE_TLS', True)
358 if (buildtype != 'Dev' and not xmpp_server_user_tls):
359 raise Exception('TLS can must be enabled in non Dev builds.')
361 replaceBool(
362 destination, 'XMPP_SERVER_USE_TLS', xmpp_server_user_tls)
363 replaceString(destination, 'XMPP_SERVER', xmppServer)
364 replaceString(destination, 'DIRECTORY_BOT_JID',
365 os.environ.get('DIRECTORY_BOT_JID',
366 'remoting@bot.talk.google.com'))
367 replaceString(destination, 'THIRD_PARTY_AUTH_REDIRECT_URL',
368 thirdPartyAuthUrlJs)
370 # Set the correct API keys.
371 # For overriding the client ID/secret via env vars, see google_api_keys.py.
372 apiClientId = google_api_keys.GetClientID('REMOTING')
373 apiClientSecret = google_api_keys.GetClientSecret('REMOTING')
374 apiKey = google_api_keys.GetAPIKeyRemoting()
376 if is_app_remoting_webapp and buildtype != 'Dev':
377 if not app_client_id:
378 raise Exception('Invalid app_client_id passed in: "' +
379 app_client_id + '"')
380 apiClientIdV2 = app_client_id + '.apps.googleusercontent.com'
381 else:
382 apiClientIdV2 = os.environ.get(
383 'REMOTING_IDENTITY_API_CLIENT_ID',
384 google_api_keys.GetClientID('REMOTING_IDENTITY_API'))
386 if not is_app_remoting_webapp:
387 replaceString(destination, 'API_CLIENT_ID', apiClientId)
388 replaceString(destination, 'API_CLIENT_SECRET', apiClientSecret)
389 replaceString(destination, 'API_KEY', apiKey)
391 # Write the application capabilities.
392 if is_app_remoting_webapp:
393 appCapabilities = ','.join(
394 ['remoting.ClientSession.Capability.' + x for x in app_capabilities])
395 findAndReplace(os.path.join(destination, 'arv_main.js'),
396 "'APPLICATION_CAPABILITIES'", appCapabilities)
398 # Official AppRemoting builds get the key from the gyp/gn build file. All
399 # other builds use a fixed key. For dev builds, this ensures that the app
400 # can be run directly from the output directory. For official CRD builds,
401 # it allows QA to test the app without uploading it to Chrome Web Store.
402 if is_app_remoting_webapp and buildtype != 'Dev':
403 if not manifest_key:
404 raise Exception('No manifest_key passed in')
405 else:
406 manifest_key = 'remotingdevbuild'
408 # Generate manifest.
409 if manifest_template:
410 context = {
411 'webapp_type': webapp_type,
412 'FULL_APP_VERSION': version,
413 'MANIFEST_KEY': manifest_key,
414 'OAUTH2_REDIRECT_URL': oauth2RedirectUrlJson,
415 'TALK_GADGET_HOST': talkGadgetHostJson,
416 'THIRD_PARTY_AUTH_REDIRECT_URL': thirdPartyAuthUrlJson,
417 'REMOTING_IDENTITY_API_CLIENT_ID': apiClientIdV2,
418 'OAUTH2_BASE_URL': oauth2BaseUrl,
419 'OAUTH2_API_BASE_URL': oauth2ApiBaseUrl,
420 'DIRECTORY_API_BASE_URL': directoryApiBaseUrl,
421 'TELEMETRY_API_BASE_URL':telemetryApiBaseUrl ,
422 'APP_REMOTING_API_BASE_URL': appRemotingApiBaseUrl,
423 'CLOUD_PRINT_URL': '',
424 'OAUTH2_ACCOUNTS_HOST': oauth2AccountsHost,
425 'GOOGLE_API_HOSTS': googleApiHosts,
426 'APP_NAME': app_name,
427 'APP_DESCRIPTION': app_description,
428 'OAUTH_CLOUD_PRINT_SCOPE': '',
429 'OAUTH_GDRIVE_SCOPE': '',
430 'USE_GCD': use_gcd,
431 'XMPP_SERVER': xmppServer,
432 # An URL match pattern that is added to the |permissions| section of the
433 # manifest in case some URLs are redirected by corporate proxies.
434 'PROXY_URL' : os.environ.get('PROXY_URL', ''),
436 if 'CLOUD_PRINT' in app_capabilities:
437 context['OAUTH_CLOUD_PRINT_SCOPE'] = ('"https://www.googleapis.com/auth/cloudprint",')
438 context['CLOUD_PRINT_URL'] = ('"https://www.google.com/cloudprint/*",')
439 if 'GOOGLE_DRIVE' in app_capabilities:
440 context['OAUTH_GDRIVE_SCOPE'] = ('"https://docs.google.com/feeds/", '
441 '"https://www.googleapis.com/auth/drive",')
442 processJinjaTemplate(manifest_template,
443 jinja_paths,
444 os.path.join(destination, 'manifest.json'),
445 context)
447 # Make the zipfile.
448 createZip(zip_path, destination)
450 return 0
453 def main():
454 parser = argparse.ArgumentParser()
455 parser.add_argument('buildtype')
456 parser.add_argument('version')
457 parser.add_argument('destination')
458 parser.add_argument('zip_path')
459 parser.add_argument('manifest_template')
460 parser.add_argument('webapp_type')
461 parser.add_argument('files', nargs='*', metavar='file', default=[])
462 parser.add_argument('--app_name', metavar='NAME')
463 parser.add_argument('--app_description', metavar='TEXT')
464 parser.add_argument('--app_capabilities',
465 nargs='*', default=[], metavar='CAPABILITY')
466 parser.add_argument('--appid')
467 parser.add_argument('--app_client_id', default='')
468 parser.add_argument('--manifest_key', default='')
469 parser.add_argument('--files_listfile', default='', metavar='PATH')
470 parser.add_argument('--locales_listfile', default='', metavar='PATH')
471 parser.add_argument('--jinja_paths', nargs='*', default=[], metavar='PATH')
472 parser.add_argument('--service_environment', default='', metavar='ENV')
473 parser.add_argument('--use_gcd', choices=['0', '1'], default='0')
475 args = parser.parse_args()
476 args.use_gcd = (args.use_gcd != '0')
477 args.app_capabilities = set(args.app_capabilities)
478 return buildWebApp(**vars(args))
481 if __name__ == '__main__':
482 sys.exit(main())