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
.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
)
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 locales_listfile
, jinja_paths
, service_environment
, use_gcd
):
113 """Does the main work of building the webapp directory and zipfile.
116 buildtype: the type of build ("Official", "Release" or "Dev").
117 destination: A string with path to directory where the webapp will be
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
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
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')
148 with
open(locales_listfile
) as input:
150 locales
.append(s
.rstrip())
152 # Ensure a fresh directory.
154 shutil
.rmtree(destination
)
156 if os
.path
.exists(destination
):
160 os
.mkdir(destination
, 0775)
162 if buildtype
!= 'Official' and buildtype
!= 'Release' and buildtype
!= 'Dev':
163 raise Exception('Unknown buildtype: ' + buildtype
)
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
)
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
)
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')
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
254 appRemotingApplicationId
= "'" + effectiveAppId
+ "'"
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'
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'
283 raise Exception('Unknown service environment: ' + service_environment
)
284 appRemotingApiBaseUrl
= appRemotingApiHost
+ appRemotingServicePath
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
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/*'
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.
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',
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: "' +
358 apiClientIdV2
= app_client_id
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':
377 raise Exception('Invalid manifest_key passed in: "' +
379 manifestKey
= '"key": "' + manifest_key
+ '",'
381 manifestKey
= '"key": "remotingdevbuild",'
382 elif buildtype
!= 'Official':
383 # TODO(joedow): Update the chromoting webapp GYP entries to include keys.
384 manifestKey
= '"key": "remotingdevbuild",'
389 if manifest_template
:
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': '',
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
,
415 os
.path
.join(destination
, 'manifest.json'),
419 createZip(zip_path
, destination
)
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__':