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
27 # Update the module path, assuming that this script is in src/remoting/webapp,
28 # and that the google_api_keys module is in src/google_apis. Note that
29 # sys.path[0] refers to the directory containing this script.
30 if __name__
== '__main__':
32 os
.path
.abspath(os
.path
.join(sys
.path
[0], '../../google_apis')))
33 import google_api_keys
36 def findAndReplace(filepath
, findString
, replaceString
):
37 """Does a search and replace on the contents of a file."""
38 oldFilename
= os
.path
.basename(filepath
) + '.old'
39 oldFilepath
= os
.path
.join(os
.path
.dirname(filepath
), oldFilename
)
40 os
.rename(filepath
, oldFilepath
)
41 with
open(oldFilepath
) as input:
42 with
open(filepath
, 'w') as output
:
44 output
.write(s
.replace(findString
, replaceString
))
45 os
.remove(oldFilepath
)
48 def createZip(zip_path
, directory
):
49 """Creates a zipfile at zip_path for the given directory."""
50 zipfile_base
= os
.path
.splitext(os
.path
.basename(zip_path
))[0]
51 zip = zipfile
.ZipFile(zip_path
, 'w', zipfile
.ZIP_DEFLATED
)
52 for (root
, dirs
, files
) in os
.walk(directory
):
54 full_path
= os
.path
.join(root
, f
)
55 rel_path
= os
.path
.relpath(full_path
, directory
)
56 zip.write(full_path
, os
.path
.join(zipfile_base
, rel_path
))
60 def replaceString(destination
, placeholder
, value
):
61 findAndReplace(os
.path
.join(destination
, 'plugin_settings.js'),
62 "'" + placeholder
+ "'", "'" + value
+ "'")
65 def processJinjaTemplate(input_file
, include_paths
, output_file
, context
):
66 jinja2_path
= os
.path
.normpath(
67 os
.path
.join(os
.path
.abspath(__file__
),
68 '../../../third_party/jinja2'))
69 sys
.path
.append(os
.path
.split(jinja2_path
)[0])
71 (template_path
, template_name
) = os
.path
.split(input_file
)
72 include_paths
= [template_path
] + include_paths
73 env
= jinja2
.Environment(loader
=jinja2
.FileSystemLoader(include_paths
))
74 template
= env
.get_template(template_name
)
75 rendered
= template
.render(context
)
76 io
.open(output_file
, 'w', encoding
='utf-8').write(rendered
)
79 def buildWebApp(buildtype
, version
, destination
, zip_path
,
80 manifest_template
, webapp_type
, app_id
, app_name
,
81 app_description
, files
, locales
, jinja_paths
,
83 """Does the main work of building the webapp directory and zipfile.
86 buildtype: the type of build ("Official", "Release" or "Dev").
87 destination: A string with path to directory where the webapp will be
89 zipfile: A string with path to the zipfile to create containing the
90 contents of |destination|.
91 manifest_template: jinja2 template file for manifest.
92 webapp_type: webapp type ("v1", "v2", "v2_pnacl" or "app_remoting").
93 app_id: A string with the Remoting Application Id (only used for app
94 remoting webapps). If supplied, it defaults to using the
96 app_name: A string with the name of the application.
97 app_description: A string with the description of the application.
98 files: An array of strings listing the paths for resources to include
100 locales: An array of strings listing locales, which are copied, along
101 with their directory structure from the _locales directory down.
102 jinja_paths: An array of paths to search for {%include} directives in
103 addition to the directory containing the manifest template.
104 service_environment: Used to point the webApp to one of the
105 dev/test/staging/prod environments
107 # Ensure a fresh directory.
109 shutil
.rmtree(destination
)
111 if os
.path
.exists(destination
):
115 os
.mkdir(destination
, 0775)
117 if buildtype
!= 'Official' and buildtype
!= 'Release' and buildtype
!= 'Dev':
118 raise Exception('Unknown buildtype: ' + buildtype
)
120 # Use symlinks on linux and mac for faster compile/edit cycle.
122 # On Windows Vista platform.system() can return 'Microsoft' with some
123 # versions of Python, see http://bugs.python.org/issue1082
124 # should_symlink = platform.system() not in ['Windows', 'Microsoft']
126 # TODO(ajwong): Pending decision on http://crbug.com/27185 we may not be
127 # able to load symlinked resources.
128 should_symlink
= False
130 # Copy all the files.
131 for current_file
in files
:
132 destination_file
= os
.path
.join(destination
, os
.path
.basename(current_file
))
133 destination_dir
= os
.path
.dirname(destination_file
)
134 if not os
.path
.exists(destination_dir
):
135 os
.makedirs(destination_dir
, 0775)
138 # TODO(ajwong): Detect if we're vista or higher. Then use win32file
139 # to create a symlink in that case.
140 targetname
= os
.path
.relpath(os
.path
.realpath(current_file
),
141 os
.path
.realpath(destination_file
))
142 os
.symlink(targetname
, destination_file
)
144 shutil
.copy2(current_file
, destination_file
)
146 # Copy all the locales, preserving directory structure
147 destination_locales
= os
.path
.join(destination
, '_locales')
148 os
.mkdir(destination_locales
, 0775)
149 remoting_locales
= os
.path
.join(destination
, 'remoting_locales')
150 os
.mkdir(remoting_locales
, 0775)
151 for current_locale
in locales
:
152 extension
= os
.path
.splitext(current_locale
)[1]
153 if extension
== '.json':
154 locale_id
= os
.path
.split(os
.path
.split(current_locale
)[0])[1]
155 destination_dir
= os
.path
.join(destination_locales
, locale_id
)
156 destination_file
= os
.path
.join(destination_dir
,
157 os
.path
.split(current_locale
)[1])
158 os
.mkdir(destination_dir
, 0775)
159 shutil
.copy2(current_locale
, destination_file
)
160 elif extension
== '.pak':
161 destination_file
= os
.path
.join(remoting_locales
,
162 os
.path
.split(current_locale
)[1])
163 shutil
.copy2(current_locale
, destination_file
)
165 raise Exception('Unknown extension: ' + current_locale
)
167 # Set client plugin type.
168 # TODO(wez): Use 'native' in app_remoting until b/17441659 is resolved.
169 client_plugin
= 'pnacl' if webapp_type
== 'v2_pnacl' else 'native'
170 findAndReplace(os
.path
.join(destination
, 'plugin_settings.js'),
171 "'CLIENT_PLUGIN_TYPE'", "'" + client_plugin
+ "'")
173 # Allow host names for google services/apis to be overriden via env vars.
174 oauth2AccountsHost
= os
.environ
.get(
175 'OAUTH2_ACCOUNTS_HOST', 'https://accounts.google.com')
176 oauth2ApiHost
= os
.environ
.get(
177 'OAUTH2_API_HOST', 'https://www.googleapis.com')
178 directoryApiHost
= os
.environ
.get(
179 'DIRECTORY_API_HOST', 'https://www.googleapis.com')
181 if webapp_type
== 'app_remoting':
182 appRemotingApiHost
= os
.environ
.get(
183 'APP_REMOTING_API_HOST', None)
184 appRemotingApplicationId
= os
.environ
.get(
185 'APP_REMOTING_APPLICATION_ID', None)
187 # Release/Official builds are special because they are what we will upload
188 # to the web store. The checks below will validate that prod builds are
189 # being generated correctly (no overrides) and with the correct buildtype.
190 # They also verify that folks are not accidentally building dev/test/staging
191 # apps for release (no impersonation) instead of dev.
192 if service_environment
== 'prod' and buildtype
== 'Dev':
193 raise Exception("Prod environment cannot be built for 'dev' builds")
195 if buildtype
!= 'Dev':
196 if service_environment
!= 'prod':
197 raise Exception('Invalid service_environment targeted for '
198 + buildtype
+ ': ' + service_environment
)
199 if 'out/Release' not in destination
:
200 raise Exception('Prod builds must be placed in the out/Release folder')
202 raise Exception('Cannot pass in an app_id for '
203 + buildtype
+ ' builds: ' + service_environment
)
204 if appRemotingApiHost
!= None:
205 raise Exception('Cannot set APP_REMOTING_API_HOST env var for '
206 + buildtype
+ ' builds')
207 if appRemotingApplicationId
!= None:
208 raise Exception('Cannot set APP_REMOTING_APPLICATION_ID env var for '
209 + buildtype
+ ' builds')
211 # If an Application ID was set (either from service_environment variable or
212 # from a command line argument), hardcode it, otherwise get it at runtime.
213 effectiveAppId
= appRemotingApplicationId
or app_id
215 appRemotingApplicationId
= "'" + effectiveAppId
+ "'"
217 appRemotingApplicationId
= "chrome.i18n.getMessage('@@extension_id')"
218 findAndReplace(os
.path
.join(destination
, 'plugin_settings.js'),
219 "'APP_REMOTING_APPLICATION_ID'", appRemotingApplicationId
)
221 oauth2BaseUrl
= oauth2AccountsHost
+ '/o/oauth2'
222 oauth2ApiBaseUrl
= oauth2ApiHost
+ '/oauth2'
223 directoryApiBaseUrl
= directoryApiHost
+ '/chromoting/v1'
225 if webapp_type
== 'app_remoting':
226 # Set the apiary endpoint and then set the endpoint version
227 if not appRemotingApiHost
:
228 if service_environment
== 'prod':
229 appRemotingApiHost
= 'https://www.googleapis.com'
231 appRemotingApiHost
= 'https://www-googleapis-test.sandbox.google.com'
233 if service_environment
== 'dev':
234 appRemotingServicePath
= '/appremoting/v1beta1_dev'
235 elif service_environment
== 'test':
236 appRemotingServicePath
= '/appremoting/v1beta1'
237 elif service_environment
== 'staging':
238 appRemotingServicePath
= '/appremoting/v1beta1_staging'
239 elif service_environment
== 'prod':
240 appRemotingServicePath
= '/appremoting/v1beta1'
242 raise Exception('Unknown service environment: ' + service_environment
)
243 appRemotingApiBaseUrl
= appRemotingApiHost
+ appRemotingServicePath
245 appRemotingApiBaseUrl
= ''
247 replaceString(destination
, 'OAUTH2_BASE_URL', oauth2BaseUrl
)
248 replaceString(destination
, 'OAUTH2_API_BASE_URL', oauth2ApiBaseUrl
)
249 replaceString(destination
, 'DIRECTORY_API_BASE_URL', directoryApiBaseUrl
)
250 if webapp_type
== 'app_remoting':
251 replaceString(destination
, 'APP_REMOTING_API_BASE_URL',
252 appRemotingApiBaseUrl
)
254 # Substitute hosts in the manifest's CSP list.
255 # Ensure we list the API host only once if it's the same for multiple APIs.
256 googleApiHosts
= ' '.join(set([oauth2ApiHost
, directoryApiHost
]))
258 # WCS and the OAuth trampoline are both hosted on talkgadget. Split them into
259 # separate suffix/prefix variables to allow for wildcards in manifest.json.
260 talkGadgetHostSuffix
= os
.environ
.get(
261 'TALK_GADGET_HOST_SUFFIX', 'talkgadget.google.com')
262 talkGadgetHostPrefix
= os
.environ
.get(
263 'TALK_GADGET_HOST_PREFIX', 'https://chromoting-client.')
264 oauth2RedirectHostPrefix
= os
.environ
.get(
265 'OAUTH2_REDIRECT_HOST_PREFIX', 'https://chromoting-oauth.')
267 # Use a wildcard in the manifest.json host specs if the prefixes differ.
268 talkGadgetHostJs
= talkGadgetHostPrefix
+ talkGadgetHostSuffix
269 talkGadgetBaseUrl
= talkGadgetHostJs
+ '/talkgadget/'
270 if talkGadgetHostPrefix
== oauth2RedirectHostPrefix
:
271 talkGadgetHostJson
= talkGadgetHostJs
273 talkGadgetHostJson
= 'https://*.' + talkGadgetHostSuffix
275 # Set the correct OAuth2 redirect URL.
276 oauth2RedirectHostJs
= oauth2RedirectHostPrefix
+ talkGadgetHostSuffix
277 oauth2RedirectHostJson
= talkGadgetHostJson
278 oauth2RedirectPath
= '/talkgadget/oauth/chrome-remote-desktop'
279 oauth2RedirectBaseUrlJs
= oauth2RedirectHostJs
+ oauth2RedirectPath
280 oauth2RedirectBaseUrlJson
= oauth2RedirectHostJson
+ oauth2RedirectPath
281 if buildtype
== 'Official':
282 oauth2RedirectUrlJs
= ("'" + oauth2RedirectBaseUrlJs
+
283 "/rel/' + chrome.i18n.getMessage('@@extension_id')")
284 oauth2RedirectUrlJson
= oauth2RedirectBaseUrlJson
+ '/rel/*'
286 oauth2RedirectUrlJs
= "'" + oauth2RedirectBaseUrlJs
+ "/dev'"
287 oauth2RedirectUrlJson
= oauth2RedirectBaseUrlJson
+ '/dev*'
288 thirdPartyAuthUrlJs
= oauth2RedirectBaseUrlJs
+ '/thirdpartyauth'
289 thirdPartyAuthUrlJson
= oauth2RedirectBaseUrlJson
+ '/thirdpartyauth*'
290 replaceString(destination
, 'TALK_GADGET_URL', talkGadgetBaseUrl
)
291 findAndReplace(os
.path
.join(destination
, 'plugin_settings.js'),
292 "'OAUTH2_REDIRECT_URL'", oauth2RedirectUrlJs
)
294 # Configure xmpp server and directory bot settings in the plugin.
295 xmppServerAddress
= os
.environ
.get(
296 'XMPP_SERVER_ADDRESS', 'talk.google.com:5222')
297 xmppServerUseTls
= os
.environ
.get('XMPP_SERVER_USE_TLS', 'true')
298 directoryBotJid
= os
.environ
.get(
299 'DIRECTORY_BOT_JID', 'remoting@bot.talk.google.com')
301 findAndReplace(os
.path
.join(destination
, 'plugin_settings.js'),
302 "Boolean('XMPP_SERVER_USE_TLS')", xmppServerUseTls
)
303 replaceString(destination
, 'XMPP_SERVER_ADDRESS', xmppServerAddress
)
304 replaceString(destination
, 'DIRECTORY_BOT_JID', directoryBotJid
)
305 replaceString(destination
, 'THIRD_PARTY_AUTH_REDIRECT_URL',
308 # Set the correct API keys.
309 # For overriding the client ID/secret via env vars, see google_api_keys.py.
310 apiClientId
= google_api_keys
.GetClientID('REMOTING')
311 apiClientSecret
= google_api_keys
.GetClientSecret('REMOTING')
312 apiClientIdV2
= google_api_keys
.GetClientID('REMOTING_IDENTITY_API')
314 replaceString(destination
, 'API_CLIENT_ID', apiClientId
)
315 replaceString(destination
, 'API_CLIENT_SECRET', apiClientSecret
)
317 # Use a consistent extension id for dev builds.
318 # AppRemoting builds always use the dev app id - the correct app id gets
319 # written into the manifest later.
320 if buildtype
!= 'Official' or webapp_type
== 'app_remoting':
321 manifestKey
= '"key": "remotingdevbuild",'
326 if manifest_template
:
328 'webapp_type': webapp_type
,
329 'FULL_APP_VERSION': version
,
330 'MANIFEST_KEY_FOR_UNOFFICIAL_BUILD': manifestKey
,
331 'OAUTH2_REDIRECT_URL': oauth2RedirectUrlJson
,
332 'TALK_GADGET_HOST': talkGadgetHostJson
,
333 'THIRD_PARTY_AUTH_REDIRECT_URL': thirdPartyAuthUrlJson
,
334 'REMOTING_IDENTITY_API_CLIENT_ID': apiClientIdV2
,
335 'OAUTH2_BASE_URL': oauth2BaseUrl
,
336 'OAUTH2_API_BASE_URL': oauth2ApiBaseUrl
,
337 'DIRECTORY_API_BASE_URL': directoryApiBaseUrl
,
338 'APP_REMOTING_API_BASE_URL': appRemotingApiBaseUrl
,
339 'OAUTH2_ACCOUNTS_HOST': oauth2AccountsHost
,
340 'GOOGLE_API_HOSTS': googleApiHosts
,
341 'APP_NAME': app_name
,
342 'APP_DESCRIPTION': app_description
,
344 processJinjaTemplate(manifest_template
,
346 os
.path
.join(destination
, 'manifest.json'),
350 createZip(zip_path
, destination
)
356 if len(sys
.argv
) < 6:
357 print ('Usage: build-webapp.py '
358 '<build-type> <version> <dst> <zip-path> <manifest_template> '
359 '<webapp_type> <other files...> '
361 '--app_description <description> '
363 '[--locales <locales...>] '
364 '[--jinja_paths <paths...>] '
365 '[--service_environment <service_environment>]')
374 app_description
= None
375 service_environment
= ''
377 for arg
in sys
.argv
[7:]:
378 if arg
in ['--locales',
383 '--service_environment']:
385 elif arg_type
== '--locales':
387 elif arg_type
== '--jinja_paths':
388 jinja_paths
.append(arg
)
389 elif arg_type
== '--appid':
392 elif arg_type
== '--app_name':
395 elif arg_type
== '--app_description':
396 app_description
= arg
398 elif arg_type
== '--service_environment':
399 service_environment
= arg
404 return buildWebApp(sys
.argv
[1], sys
.argv
[2], sys
.argv
[3], sys
.argv
[4],
405 sys
.argv
[5], sys
.argv
[6], app_id
, app_name
,
406 app_description
, files
, locales
, jinja_paths
,
410 if __name__
== '__main__':