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.
14 # Python 2.5 compatibility
15 from __future__
import with_statement
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__':
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
:
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
):
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
))
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.
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
)
91 return parseBool(rawValue
)
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])
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']:
113 elif webapp_type
in ['v2_pnacl', 'shared_module']:
115 elif webapp_type
is 'app_remoting':
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.
127 buildtype: the type of build ("Official", "Release" or "Dev").
128 destination: A string with path to directory where the webapp will be
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
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
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')
166 with
open(locales_listfile
) as input:
168 locales
.append(s
.rstrip())
170 # Load the files from the files_listfile.
172 with
open(files_listfile
) as input:
174 files
.append(s
.rstrip())
176 # Ensure a fresh directory.
178 shutil
.rmtree(destination
)
180 if os
.path
.exists(destination
):
184 os
.makedirs(destination
, 0775)
186 if buildtype
!= 'Official' and buildtype
!= 'Release' and buildtype
!= 'Dev':
187 raise Exception('Unknown buildtype: ' + buildtype
)
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
)
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
)
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')
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
)
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
285 appRemotingApplicationId
= "'" + effectiveAppId
+ "'"
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'
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'
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
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'
321 raise Exception('Unknown service environment: ' + service_environment
)
322 appRemotingApiBaseUrl
= appRemotingApiHost
+ appRemotingServicePath
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
)
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
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/*'
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.
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',
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: "' +
403 apiClientIdV2
= app_client_id
+ '.apps.googleusercontent.com'
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':
425 raise Exception('Invalid manifest_key passed in: "' +
427 manifestKey
= '"key": "' + manifest_key
+ '",'
429 manifestKey
= '"key": "remotingdevbuild",'
430 elif buildtype
!= 'Official':
431 # TODO(joedow): Update the chromoting webapp GYP entries to include keys.
432 manifestKey
= '"key": "remotingdevbuild",'
437 if manifest_template
:
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': '',
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
,
464 os
.path
.join(destination
, 'manifest.json'),
468 createZip(zip_path
, destination
)
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__':