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
)
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 files_listfile
, locales_listfile
, jinja_paths
,
113 service_environment
, use_gcd
):
114 """Does the main work of building the webapp directory and zipfile.
117 buildtype: the type of build ("Official", "Release" or "Dev").
118 destination: A string with path to directory where the webapp will be
120 zipfile: A string with path to the zipfile to create containing the
121 contents of |destination|.
122 manifest_template: jinja2 template file for manifest.
123 webapp_type: webapp type ("v1", "v2", "v2_pnacl" or "app_remoting").
124 appid: A string with the Remoting Application Id (only used for app
125 remoting webapps). If supplied, it defaults to using the
127 app_client_id: The OAuth2 client ID for the webapp.
128 app_name: A string with the name of the application.
129 app_description: A string with the description of the application.
130 app_capabilities: A set of strings naming the capabilities that should be
131 enabled for this application.
132 manifest_key: The manifest key for the webapp.
133 files: An array of strings listing the paths for resources to include
135 files_listfile: The name of a file containing a list of files, one per
136 line, identifying the resources to include in this webapp.
137 This is an alternate to specifying the files directly via
138 the 'files' option. The files listed in this file are
139 appended to the files passed via the 'files' option, if any.
140 locales_listfile: The name of a file containing a list of locales, one per
141 line, which are copied, along with their directory
142 structure, from the _locales directory down.
143 jinja_paths: An array of paths to search for {%include} directives in
144 addition to the directory containing the manifest template.
145 service_environment: Used to point the webapp to one of the
146 dev/test/staging/vendor/prod/prod-testing environments
147 use_gcd: True if GCD support should be enabled.
150 # Load the locales files from the locales_listfile.
151 if not locales_listfile
:
152 raise Exception('You must specify a locales_listfile')
154 with
open(locales_listfile
) as input:
156 locales
.append(s
.rstrip())
158 # Load the files from the files_listfile.
160 with
open(files_listfile
) as input:
162 files
.append(s
.rstrip())
164 # Ensure a fresh directory.
166 shutil
.rmtree(destination
)
168 if os
.path
.exists(destination
):
172 os
.makedirs(destination
, 0775)
174 if buildtype
!= 'Official' and buildtype
!= 'Release' and buildtype
!= 'Dev':
175 raise Exception('Unknown buildtype: ' + buildtype
)
178 'webapp_type': webapp_type
,
179 'buildtype': buildtype
,
182 # Copy all the files.
183 for current_file
in files
:
184 destination_file
= os
.path
.join(destination
, os
.path
.basename(current_file
))
186 # Process *.jinja2 files as jinja2 templates
187 if current_file
.endswith(".jinja2"):
188 destination_file
= destination_file
[:-len(".jinja2")]
189 processJinjaTemplate(current_file
, jinja_paths
,
190 destination_file
, jinja_context
)
192 shutil
.copy2(current_file
, destination_file
)
194 # Copy all the locales, preserving directory structure
195 destination_locales
= os
.path
.join(destination
, '_locales')
196 os
.mkdir(destination_locales
, 0775)
197 remoting_locales
= os
.path
.join(destination
, 'remoting_locales')
198 os
.mkdir(remoting_locales
, 0775)
199 for current_locale
in locales
:
200 extension
= os
.path
.splitext(current_locale
)[1]
201 if extension
== '.json':
202 locale_id
= os
.path
.split(os
.path
.split(current_locale
)[0])[1]
203 destination_dir
= os
.path
.join(destination_locales
, locale_id
)
204 destination_file
= os
.path
.join(destination_dir
,
205 os
.path
.split(current_locale
)[1])
206 os
.mkdir(destination_dir
, 0775)
207 shutil
.copy2(current_locale
, destination_file
)
208 elif extension
== '.pak':
209 destination_file
= os
.path
.join(remoting_locales
,
210 os
.path
.split(current_locale
)[1])
211 shutil
.copy2(current_locale
, destination_file
)
213 raise Exception('Unknown extension: ' + current_locale
)
215 # Set client plugin type.
216 # TODO(wez): Use 'native' in app_remoting until b/17441659 is resolved.
217 client_plugin
= 'pnacl' if webapp_type
== 'v2_pnacl' else 'native'
218 findAndReplace(os
.path
.join(destination
, 'plugin_settings.js'),
219 "'CLIENT_PLUGIN_TYPE'", "'" + client_plugin
+ "'")
221 # Allow host names for google services/apis to be overriden via env vars.
222 oauth2AccountsHost
= os
.environ
.get(
223 'OAUTH2_ACCOUNTS_HOST', 'https://accounts.google.com')
224 oauth2ApiHost
= os
.environ
.get(
225 'OAUTH2_API_HOST', 'https://www.googleapis.com')
226 directoryApiHost
= os
.environ
.get(
227 'DIRECTORY_API_HOST', 'https://www.googleapis.com')
228 remotingApiHost
= os
.environ
.get(
229 'REMOTING_API_HOST', 'https://remoting-pa.googleapis.com')
231 is_app_remoting_webapp
= webapp_type
== 'app_remoting'
232 is_prod_service_environment
= service_environment
== 'vendor' or \
233 service_environment
== 'prod' or \
234 service_environment
== 'prod-testing'
235 if is_app_remoting_webapp
:
236 appRemotingApiHost
= os
.environ
.get(
237 'APP_REMOTING_API_HOST', None)
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/test/staging
245 # apps for 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
)
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
267 appRemotingApplicationId
= "'" + effectiveAppId
+ "'"
269 appRemotingApplicationId
= "chrome.i18n.getMessage('@@extension_id')"
270 findAndReplace(os
.path
.join(destination
, 'plugin_settings.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_webapp
:
279 # Set the apiary endpoint and then set the endpoint version
280 if not appRemotingApiHost
:
281 if is_prod_service_environment
:
282 appRemotingApiHost
= 'https://www.googleapis.com'
284 appRemotingApiHost
= 'https://www-googleapis-test.sandbox.google.com'
286 if service_environment
== 'dev':
287 appRemotingServicePath
= '/appremoting/v1beta1_dev'
288 elif service_environment
== 'test':
289 appRemotingServicePath
= '/appremoting/v1beta1'
290 elif service_environment
== 'staging':
291 appRemotingServicePath
= '/appremoting/v1beta1_staging'
292 elif service_environment
== 'vendor':
293 appRemotingServicePath
= '/appremoting/v1beta1_vendor'
294 elif service_environment
== 'prod':
295 appRemotingServicePath
= '/appremoting/v1beta1'
296 elif service_environment
== 'prod-testing':
297 appRemotingServicePath
= '/appremoting/v1beta1_prod_testing'
299 raise Exception('Unknown service environment: ' + service_environment
)
300 appRemotingApiBaseUrl
= appRemotingApiHost
+ appRemotingServicePath
302 appRemotingApiBaseUrl
= ''
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_webapp
:
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
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/*'
345 oauth2RedirectUrlJs
= "'" + oauth2RedirectBaseUrlJs
+ "/dev'"
346 oauth2RedirectUrlJson
= oauth2RedirectBaseUrlJson
+ '/dev*'
347 thirdPartyAuthUrlJs
= oauth2RedirectBaseUrlJs
+ '/thirdpartyauth'
348 thirdPartyAuthUrlJson
= oauth2RedirectBaseUrlJson
+ '/thirdpartyauth*'
349 replaceString(destination
, 'TALK_GADGET_URL', talkGadgetBaseUrl
)
350 findAndReplace(os
.path
.join(destination
, 'plugin_settings.js'),
351 "'OAUTH2_REDIRECT_URL'", oauth2RedirectUrlJs
)
353 # Configure xmpp server and directory bot settings in the plugin.
355 destination
, 'XMPP_SERVER_USE_TLS',
356 getenvBool('XMPP_SERVER_USE_TLS', True))
357 xmppServer
= os
.environ
.get('XMPP_SERVER', 'talk.google.com:443')
358 replaceString(destination
, 'XMPP_SERVER', xmppServer
)
359 replaceString(destination
, 'DIRECTORY_BOT_JID',
360 os
.environ
.get('DIRECTORY_BOT_JID',
361 'remoting@bot.talk.google.com'))
362 replaceString(destination
, 'THIRD_PARTY_AUTH_REDIRECT_URL',
365 # Set the correct API keys.
366 # For overriding the client ID/secret via env vars, see google_api_keys.py.
367 apiClientId
= google_api_keys
.GetClientID('REMOTING')
368 apiClientSecret
= google_api_keys
.GetClientSecret('REMOTING')
369 apiKey
= google_api_keys
.GetAPIKeyRemoting()
371 if is_app_remoting_webapp
and buildtype
!= 'Dev':
372 if not app_client_id
:
373 raise Exception('Invalid app_client_id passed in: "' +
375 apiClientIdV2
= app_client_id
+ '.apps.googleusercontent.com'
377 apiClientIdV2
= google_api_keys
.GetClientID('REMOTING_IDENTITY_API')
379 replaceString(destination
, 'API_CLIENT_ID', apiClientId
)
380 replaceString(destination
, 'API_CLIENT_SECRET', apiClientSecret
)
381 replaceString(destination
, 'API_KEY', apiKey
)
383 # Write the application capabilities.
384 appCapabilities
= ','.join(
385 ['remoting.ClientSession.Capability.' + x
for x
in app_capabilities
])
386 findAndReplace(os
.path
.join(destination
, 'app_capabilities.js'),
387 "'APPLICATION_CAPABILITIES'", appCapabilities
)
389 # Use a consistent extension id for dev builds.
390 # AppRemoting builds always use the dev app id - the correct app id gets
391 # written into the manifest later.
392 if is_app_remoting_webapp
:
393 if buildtype
!= 'Dev':
395 raise Exception('Invalid manifest_key passed in: "' +
397 manifestKey
= '"key": "' + manifest_key
+ '",'
399 manifestKey
= '"key": "remotingdevbuild",'
400 elif buildtype
!= 'Official':
401 # TODO(joedow): Update the chromoting webapp GYP entries to include keys.
402 manifestKey
= '"key": "remotingdevbuild",'
407 if manifest_template
:
409 'webapp_type': webapp_type
,
410 'FULL_APP_VERSION': version
,
411 'MANIFEST_KEY_FOR_UNOFFICIAL_BUILD': manifestKey
,
412 'OAUTH2_REDIRECT_URL': oauth2RedirectUrlJson
,
413 'TALK_GADGET_HOST': talkGadgetHostJson
,
414 'THIRD_PARTY_AUTH_REDIRECT_URL': thirdPartyAuthUrlJson
,
415 'REMOTING_IDENTITY_API_CLIENT_ID': apiClientIdV2
,
416 'OAUTH2_BASE_URL': oauth2BaseUrl
,
417 'OAUTH2_API_BASE_URL': oauth2ApiBaseUrl
,
418 'DIRECTORY_API_BASE_URL': directoryApiBaseUrl
,
419 'TELEMETRY_API_BASE_URL':telemetryApiBaseUrl
,
420 'APP_REMOTING_API_BASE_URL': appRemotingApiBaseUrl
,
421 'OAUTH2_ACCOUNTS_HOST': oauth2AccountsHost
,
422 'GOOGLE_API_HOSTS': googleApiHosts
,
423 'APP_NAME': app_name
,
424 'APP_DESCRIPTION': app_description
,
425 'OAUTH_GDRIVE_SCOPE': '',
427 'XMPP_SERVER': xmppServer
,
429 if 'GOOGLE_DRIVE' in app_capabilities
:
430 context
['OAUTH_GDRIVE_SCOPE'] = ('"https://docs.google.com/feeds/", '
431 '"https://www.googleapis.com/auth/drive",')
432 processJinjaTemplate(manifest_template
,
434 os
.path
.join(destination
, 'manifest.json'),
438 createZip(zip_path
, destination
)
444 parser
= argparse
.ArgumentParser()
445 parser
.add_argument('buildtype')
446 parser
.add_argument('version')
447 parser
.add_argument('destination')
448 parser
.add_argument('zip_path')
449 parser
.add_argument('manifest_template')
450 parser
.add_argument('webapp_type')
451 parser
.add_argument('files', nargs
='*', metavar
='file', default
=[])
452 parser
.add_argument('--app_name', metavar
='NAME')
453 parser
.add_argument('--app_description', metavar
='TEXT')
454 parser
.add_argument('--app_capabilities',
455 nargs
='*', default
=[], metavar
='CAPABILITY')
456 parser
.add_argument('--appid')
457 parser
.add_argument('--app_client_id', default
='')
458 parser
.add_argument('--manifest_key', default
='')
459 parser
.add_argument('--files_listfile', default
='', metavar
='PATH')
460 parser
.add_argument('--locales_listfile', default
='', metavar
='PATH')
461 parser
.add_argument('--jinja_paths', nargs
='*', default
=[], metavar
='PATH')
462 parser
.add_argument('--service_environment', default
='', metavar
='ENV')
463 parser
.add_argument('--use_gcd', choices
=['0', '1'], default
='0')
465 args
= parser
.parse_args()
466 args
.use_gcd
= (args
.use_gcd
!= '0')
467 args
.app_capabilities
= set(args
.app_capabilities
)
468 return buildWebApp(**vars(args
))
471 if __name__
== '__main__':