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