1 # This file is part of Indico.
2 # Copyright (C) 2002 - 2015 European Organization for Nuclear Research (CERN).
4 # Indico is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 3 of the
7 # License, or (at your option) any later version.
9 # Indico is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with Indico; if not, see <http://www.gnu.org/licenses/>.
18 fabfile for Indico development operations
29 from contextlib
import contextmanager
32 from fabric
.api
import local
, lcd
, task
, env
33 from fabric
.context_managers
import prefix
, settings
34 from fabric
.colors
import red
, green
, yellow
, cyan
35 from fabric
.contrib
import console
36 from fabric
.operations
import put
, run
39 ASSET_TYPES
= ['js', 'sass', 'css']
42 DEFAULT_REQUEST_ERROR_MSG
= 'UNDEFINED ERROR (no error description from server)'
43 CONF_FILE_NAME
= 'fabfile.conf'
44 SOURCE_DIR
= os
.path
.dirname(__file__
)
46 execfile(CONF_FILE_NAME
, {}, env
)
49 'conf': CONF_FILE_NAME
,
50 'src_dir': SOURCE_DIR
,
51 'ext_dir': os
.path
.join(SOURCE_DIR
, env
.ext_dirname
),
52 'target_dir': os
.path
.join(SOURCE_DIR
, env
.target_dirname
),
53 'node_env_path': os
.path
.join(SOURCE_DIR
, env
.node_env_dirname
)
70 with
prefix('. {0}'.format(os
.path
.join(env
.node_env_path
, 'bin/activate'))):
75 def pyenv_env(version
):
76 cmd_dir
= os
.path
.join(env
.pyenv_dir
, 'versions', 'indico-build-{0}'.format(version
), 'bin')
77 with
prefix('PATH={0}:$PATH'.format(cmd_dir
)):
81 def pyenv_cmd(cmd
, **kwargs
):
82 cmd_dir
= os
.path
.join(env
.pyenv_dir
, 'bin')
83 return local('{0}/pyenv {1}'.format(cmd_dir
, cmd
), **kwargs
)
88 def _yes_no_input(message
, default
):
90 if default
.lower() == 'y':
92 elif default
.lower() == 'n':
94 s
= raw_input(message
+c
) or default
101 def _putl(source_file
, dest_dir
):
103 To be used instead of put, since it doesn't support symbolic links
106 put(source_file
, '/')
107 run("mkdir -p {0}".format(dest_dir
))
108 run("mv -f /{0} {1}".format(os
.path
.basename(source_file
), dest_dir
))
111 def create_node_env():
112 with
settings(warn_only
=True):
113 local('nodeenv -c -n {0} {1}'.format(env
.node_version
, env
.node_env_path
))
116 def lib_dir(src_dir
, dtype
):
117 target_dir
= os
.path
.join(src_dir
, 'indico', 'htdocs')
118 return os
.path
.join(target_dir
, dtype
, 'lib')
121 def _check_pyenv(py_versions
):
123 Check that pyenv and pyenv-virtualenv are installed and set up the
124 compilers/virtual envs in case they do not exist
127 if os
.system('which pyenv'):
128 print red("Can't find pyenv!")
129 print yellow("Are you sure you have installed it?")
131 elif os
.system('which pyenv-virtualenv'):
132 print red("Can't find pyenv-virtualenv!")
133 print yellow("Are you sure you have installed it?")
136 # list available pythonbrew versions
137 av_versions
= list(entry
.strip() for entry
in pyenv_cmd('versions', capture
=True).split('\n')[1:])
139 for py_version
in py_versions
:
140 if (py_version
) not in av_versions
:
141 print green('Installing Python {0}'.format(py_version
))
142 pyenv_cmd('install {0}'.format(py_version
), capture
=True)
144 local("echo \'y\' | pyenv virtualenv {0} indico-build-{0}".format(py_version
))
146 with
pyenv_env(py_version
):
147 local("pip install -r requirements.dev.txt")
150 def _check_present(executable
, message
="Please install it first."):
152 Check that executable exists in $PATH
155 with
settings(warn_only
=True):
156 if local('which {0} > /dev/null && echo $?'.format(executable
), capture
=True) != '0':
157 print red('{0} is not available in this system. {1}'.format(executable
, message
))
161 def _safe_rm(path
, recursive
=False, ask
=True):
163 path
= os
.path
.join(env
.lcwd
, path
)
165 files
= glob
.glob(path
)
167 print yellow("The following files are going to be deleted:\n ") + '\n '.join(files
)
168 if console
.confirm(cyan("Are you sure you want to delete them?")):
169 local('rm {0}{1}'.format('-rf ' if recursive
else '', path
))
171 print red("Delete operation cancelled")
173 local('rm {0}{1}'.format('-rf ' if recursive
else '', path
))
176 def _cp_tree(dfrom
, dto
, exclude
=[]):
178 Simple copy with exclude option
181 dfrom
= os
.path
.join(env
.lcwd
, dfrom
)
183 dto
= os
.path
.join(env
.lcwd
, dto
)
185 print "{0} -> {1}".format(dfrom
, dto
)
187 shutil
.copytree(dfrom
, dto
, ignore
=shutil
.ignore_patterns(*exclude
))
190 def _find_most_recent(path
, cmp=operator
.gt
, maxt
=0):
191 for dirpath
, __
, fnames
in os
.walk(path
):
194 # ignore hidden files and ODTs
195 if fname
.startswith(".") or fname
.endswith(".odt"):
198 mtime
= os
.stat(os
.path
.join(dirpath
, fname
)).st_mtime
204 def _find_least_recent(path
):
205 return _find_most_recent(path
, cmp=operator
.lt
, maxt
=sys
.maxint
)
208 def _install_dependencies(mod_name
, sub_path
, dtype
, dest_subpath
=None):
209 l_dir
= lib_dir(env
.src_dir
, dtype
)
210 dest_dir
= os
.path
.join(l_dir
, dest_subpath
) if dest_subpath
else l_dir
211 local('mkdir -p {0}'.format(dest_dir
))
212 local('cp -R {0} {1}/'.format(
213 os
.path
.join(env
.ext_dir
, mod_name
, sub_path
),
220 def install_angular():
222 Install Angular.js from Git
225 with
lcd(os
.path
.join(env
.ext_dir
, 'angular')):
227 local('grunt clean buildall copy write compress')
228 dest_dir_js
= lib_dir(env
.src_dir
, 'js')
229 dest_dir_css
= lib_dir(env
.src_dir
, 'css')
230 local('mkdir -p {0}'.format(dest_dir_js
))
231 local('cp build/angular.js {0}/'.format(dest_dir_js
))
232 local('cp build/angular-resource.js {0}/'.format(dest_dir_js
))
233 local('cp build/angular-sanitize.js {0}/'.format(dest_dir_js
))
234 local('cp css/angular.css {0}'.format(dest_dir_css
))
237 @recipe('ui-sortable')
238 def install_ui_sortable():
240 Install angular ui-sortable from Git
243 with
lcd(os
.path
.join(env
.ext_dir
, 'ui-sortable')):
244 dest_dir_js
= lib_dir(env
.src_dir
, 'js')
245 local('mkdir -p {0}'.format(dest_dir_js
))
246 local('cp src/sortable.js {0}/'.format(dest_dir_js
))
250 def install_compass():
252 Install compass stylesheets from Git
254 _install_dependencies('compass', 'frameworks/compass/stylesheets/*', 'sass', 'compass')
258 def install_jquery():
260 Install jquery from Git
263 with
lcd(os
.path
.join(env
.ext_dir
, 'jquery')):
266 dest_dir
= lib_dir(env
.src_dir
, 'js')
267 local('mkdir -p {0}'.format(dest_dir
))
268 local('cp dist/jquery.js {0}/'.format(dest_dir
))
272 def install_jqplot():
273 """Install jQPlot from Git"""
274 plugins
= ['axis', 'bar', 'cursor', 'highlighter', 'points', 'text']
275 with
lcd(os
.path
.join(env
.ext_dir
, 'jqplot')):
276 dest_dir_js
= os
.path
.join(lib_dir(env
.src_dir
, 'js'), 'jqplot')
277 dest_dir_css
= lib_dir(env
.src_dir
, 'css')
278 dest_dir_js_core
= os
.path
.join(dest_dir_js
, 'core')
279 dest_dir_js_plugins
= os
.path
.join(dest_dir_js
, 'plugins')
280 local('mkdir -p {0} {1}'.format(dest_dir_js_core
, dest_dir_css
))
281 local('cp src/core/*.js {0}'.format(dest_dir_js_core
))
282 local('cp src/core/*.css {0}'.format(dest_dir_css
))
283 for plugin_name
in plugins
:
284 dest
= os
.path
.join(dest_dir_js_plugins
, plugin_name
)
285 local('mkdir -p {0}'.format(dest
))
286 local('cp src/plugins/{0}/* {1}'.format(plugin_name
, dest
))
289 @recipe('underscore')
290 def install_underscore():
292 Install underscore from Git
294 _install_dependencies('underscore', 'underscore.js', 'js')
297 @recipe('rrule') # rrule.js
300 Install rrule from Git
302 _install_dependencies('rrule', 'lib/rrule.js', 'js')
308 Install qtip2 from Git
311 with
lcd(os
.path
.join(env
.ext_dir
, 'qtip2')):
313 local('grunt --plugins="tips modal viewport svg" init clean concat:dist concat:css concat:libs replace')
314 dest_dir_js
, dest_dir_css
= lib_dir(env
.src_dir
, 'js'), lib_dir(env
.src_dir
, 'css')
315 local('mkdir -p {0} {1}'.format(dest_dir_js
, dest_dir_css
))
316 local('cp dist/jquery.qtip.js {0}/'.format(dest_dir_js
))
317 local('cp dist/jquery.qtip.css {0}/'.format(dest_dir_css
))
320 @recipe('jquery-ui-multiselect')
321 def install_jquery_ui_multiselect():
323 Install jquery ui multiselect widget from Git
326 with
lcd(os
.path
.join(env
.ext_dir
, 'jquery-ui-multiselect')):
327 dest_dir_js
= lib_dir(env
.src_dir
, 'js')
328 dest_dir_css
= lib_dir(env
.src_dir
, 'css')
329 local('mkdir -p {0} {1}'.format(dest_dir_js
, dest_dir_css
))
330 local('cp src/jquery.multiselect.js {0}/'.format(dest_dir_js
))
331 local('cp src/jquery.multiselect.filter.js {0}/'.format(dest_dir_js
))
332 local('cp jquery.multiselect.css {0}/'.format(dest_dir_css
))
333 local('cp jquery.multiselect.filter.css {0}/'.format(dest_dir_css
))
337 def install_mathjax():
339 Install MathJax from Git
342 dest_dir
= os
.path
.join(lib_dir(env
.src_dir
, 'js'), 'mathjax/')
343 mathjax_js
= os
.path
.join(dest_dir
, 'MathJax.js')
345 with
lcd(os
.path
.join(env
.ext_dir
, 'mathjax')):
346 local('rm -rf {0}'.format(os
.path
.join(dest_dir
)))
347 _cp_tree('unpacked/', dest_dir
, exclude
=["AM*", "MML*", "Accessible*", "Safe*"])
348 _cp_tree('images/', os
.path
.join(dest_dir
, 'images'))
349 _cp_tree('fonts/', os
.path
.join(dest_dir
, 'fonts'), exclude
=["png"])
351 with
open(mathjax_js
, 'r') as f
:
353 # Uncomment 'isPacked = true' line
354 data
= re
.sub(r
'//\s*(MathJax\.isPacked\s*=\s*true\s*;)', r
'\1', data
, re
.MULTILINE
)
356 with
open(mathjax_js
, 'w') as f
:
361 def install_pagedown():
363 Install PageDown from Git (mirror!)
365 with
lcd(os
.path
.join(env
.ext_dir
, 'pagedown')):
366 dest_dir
= os
.path
.join(lib_dir(env
.src_dir
, 'js'), 'pagedown/')
367 local('mkdir -p {0}'.format(dest_dir
))
368 local('cp *.js {0}'.format(dest_dir
))
371 @recipe('ZeroClipboard')
372 def install_zeroclipboard():
374 Install ZeroClipboard from Git
376 with
lcd(os
.path
.join(env
.ext_dir
, 'zeroclipboard')):
377 dest_dir
= os
.path
.join(lib_dir(env
.src_dir
, 'js'), 'zeroclipboard/')
378 local('mkdir -p {0}'.format(dest_dir
))
379 local('cp dist/ZeroClipboard.js {0}/'.format(dest_dir
))
380 local('cp dist/ZeroClipboard.min.js {0}/'.format(dest_dir
))
381 local('cp dist/ZeroClipboard.min.map {0}/'.format(dest_dir
))
382 local('cp dist/ZeroClipboard.swf {0}/'.format(dest_dir
))
388 def install(recipe_name
):
390 Install a module given the recipe name
392 RECIPES
[recipe_name
]()
396 def init_submodules(src_dir
='.'):
398 Initialize submodules (fetch them from external Git repos)
401 print green("Initializing submodules")
404 local('git submodule update --init --recursive')
409 Install asset dependencies
411 print green("Installing asset dependencies...")
412 for recipe_name
in RECIPES
:
413 print cyan("Installing {0}".format(recipe_name
))
418 def setup_deps(n_env
=None, n_version
=None, src_dir
=None, system_node
=None):
420 Setup (fetch and install) dependencies for Indico assets
423 src_dir
= src_dir
or env
.src_dir
424 n_env
= n_env
or env
.node_env_path
425 system_node
= system_node
.lower() in ('1', 'true') if system_node
is not None else env
.system_node
427 # initialize submodules if they haven't yet been
428 init_submodules(src_dir
)
430 ext_dir
= os
.path
.join(src_dir
, 'ext_modules')
432 _check_present('curl')
434 with
settings(node_env_path
=n_env
or os
.path
.join(ext_dir
, 'node_env'),
435 node_version
=n_version
or env
.node_version
,
436 system_node
=system_node
,
440 if not system_node
and not os
.path
.exists(n_env
):
444 local('npm install -g grunt-cli')
450 def clean_deps(src_dir
=None):
452 Clean up generated files
455 for dtype
in ASSET_TYPES
:
456 _safe_rm('{0}/*'.format(lib_dir(src_dir
or env
.src_dir
, dtype
)), recursive
=True)
460 def cleanup(build_dir
=None, force
=False):
462 Clean up build environment
464 _safe_rm('{0}'.format(build_dir
or env
.build_dir
), recursive
=True, ask
=(not force
))
468 def tarball(src_dir
=None):
470 Create a source Indico distribution (tarball)
473 src_dir
= src_dir
or env
.src_dir
477 setup_deps(n_env
=os
.path
.join(src_dir
, 'ext_modules', 'node_env'),
479 local('python setup.py -q sdist')
483 def egg(py_versions
=None):
485 Create a binary Indico distribution (egg)
488 for py_version
in py_versions
:
489 cmd_dir
= os
.path
.join(env
.pyenv_dir
, 'versions', 'indico-build-{0}'.format(py_version
), 'bin')
490 local('{0} setup.py -q bdist_egg'.format(os
.path
.join(cmd_dir
, 'python')))
491 print green(local('ls -lah dist/', capture
=True))
495 def make_docs(src_dir
=None, build_dir
=None, force
=False):
500 src_dir
= src_dir
or env
.src_dir
501 doc_src_dir
= os
.path
.join(src_dir
, 'doc')
503 if build_dir
is None:
504 target_dir
= os
.path
.join(src_dir
, 'indico', 'htdocs', 'ihelp')
506 target_dir
= os
.path
.join(build_dir
or env
.build_dir
, 'indico', 'htdocs', 'ihelp')
509 print yellow("Checking if docs need to be generated... "),
510 if _find_most_recent(target_dir
) > _find_most_recent(doc_src_dir
):
515 _check_present('pdflatex')
517 print green('Generating documentation')
518 with
lcd(doc_src_dir
):
523 local('rm -rf {0}/*'.format(os
.path
.join(target_dir
, 'html')))
524 local('mv build/html/* {0}'.format(os
.path
.join(target_dir
, 'html')))
526 with
lcd(os
.path
.join('guides', 'build', 'latex')):
527 local('make all-pdf')
528 local('mv *.pdf {0}'.format(os
.path
.join(target_dir
, 'pdf')))
530 print green('Cleaning up')
536 def _check_request_error(r
):
537 if r
.status_code
>= 400:
539 msg
= j
.get('message', DEFAULT_REQUEST_ERROR_MSG
)
540 print red("ERROR: {0} ({1})".format(msg
, r
.status_code
))
544 def _valid_github_credentials(auth
):
545 url
= "https://api.github.com/repos/{0}/{1}".format(env
.github
['org'], env
.github
['repo'])
546 r
= requests
.get(url
, auth
=(env
.github
['user'], auth
))
547 if (r
.status_code
== 401) and (r
.json().get('message') == 'Bad credentials'):
548 print red('Invalid Github credentials for user \'{0}\''.format(env
.github
['user']))
554 def _release_exists(tag_name
, auth
):
555 url
= "https://api.github.com/repos/{0}/{1}/releases".format(env
.github
['org'], env
.github
['repo'])
556 r
= requests
.get(url
, auth
=(env
.github
['user'], auth
))
557 _check_request_error(r
)
559 for release
in parsed
:
560 if release
.get('tag_name') == tag_name
:
561 rel_id
= release
.get('id')
562 return (True, rel_id
, release
)
564 return (False, 0, None)
567 def _asset_exists(rel_id
, name
, auth
):
568 url
= "https://api.github.com/repos/{0}/{1}/releases/{2}/assets" \
569 .format(env
.github
['org'], env
.github
['repo'], rel_id
)
570 r
= requests
.get(url
, auth
=(env
.github
['user'], auth
))
571 _check_request_error(r
)
574 if j
.get('name') == name
:
575 asset_id
= j
.get('id')
576 return (True, asset_id
)
582 def upload_github(build_dir
=None, tag_name
=None, auth_token
=None,
583 overwrite
=None, indico_version
='master'):
585 build_dir
= build_dir
or env
.build_dir
586 auth_token
= auth_token
or env
.github
['auth_token']
588 while (auth_token
is None) or (not _valid_github_credentials(auth_token
)):
589 auth_token
= getpass
.getpass(
590 'Insert the Github password/OAuth token for user \'{0}\': '.format(env
.github
['user']))
592 auth_creds
= (env
.github
['user'], auth_token
)
594 overwrite
= overwrite
or env
.github
['overwrite']
596 # Create a new release
597 tag_name
= tag_name
or indico_version
598 url
= "https://api.github.com/repos/{0}/{1}/releases".format(env
.github
['org'], env
.github
['repo'])
600 'tag_name': tag_name
,
601 'target_commitish': indico_version
,
602 'name': 'Indico {0}'.format(tag_name
),
606 (exists
, rel_id
, release_data
) = _release_exists(tag_name
, auth_token
)
609 if overwrite
is None:
610 overwrite
= _yes_no_input('Release already exists, do you want to overwrite', 'n')
616 # We will need to get a new release id from github
617 r
= requests
.post(url
, auth
=auth_creds
, data
=json
.dumps(payload
))
618 _check_request_error(r
)
619 release_data
= r
.json()
620 release_id
= release_data
.get('id')
622 # Upload binaries to the new release
623 binaries_dir
= os
.path
.join(build_dir
, 'indico', 'dist')
625 # awful way to handle this, but a regex seems like too much
626 url
= release_data
['upload_url'][:-7]
628 for f
in os
.listdir(binaries_dir
):
630 # jump over hidden/system files
631 if f
.startswith('.'):
634 if os
.path
.isfile(os
.path
.join(binaries_dir
, f
)):
635 (exists
, asset_id
) = _asset_exists(release_id
, f
, auth_token
)
638 # delete previous version
639 del_url
= "https://api.github.com/repos/{0}/{1}/releases/assets/{2}" \
640 .format(env
.github
['org'], env
.github
['repo'], asset_id
)
641 r
= requests
.delete(del_url
, auth
=auth_creds
)
642 _check_request_error(r
)
644 with
open(os
.path
.join(binaries_dir
, f
), 'rb') as ff
:
646 extension
= os
.path
.splitext(f
)[1]
648 # upload eggs using zip mime type
649 if extension
== '.gz':
650 headers
= {'Content-Type': 'application/x-gzip'}
651 elif extension
== '.egg':
652 headers
= {'Content-Type': 'application/zip'}
654 headers
['Accept'] = 'application/vnd.github.v3+json'
655 headers
['Content-Length'] = len(data
)
658 print green("Uploading \'{0}\' to Github".format(f
))
659 r
= requests
.post(url
, auth
=auth_creds
, headers
=headers
, data
=data
, params
=params
, verify
=False)
660 _check_request_error(r
)
664 def upload_ssh(build_dir
=None, server_host
=None, server_port
=None,
665 ssh_user
=None, ssh_key
=None, dest_dir
=None):
667 build_dir
= build_dir
or env
.build_dir
668 server_host
= server_host
or env
.ssh
['host']
669 server_port
= server_port
or env
.ssh
['port']
670 ssh_user
= ssh_user
or env
.ssh
['user']
671 ssh_key
= ssh_key
or env
.ssh
['key']
672 dest_dir
= dest_dir
or env
.ssh
['dest_dir']
674 env
.host_string
= server_host
+ ':' + server_port
676 env
.key_filename
= ssh_key
678 binaries_dir
= os
.path
.join(build_dir
, 'indico', 'dist')
679 for f
in os
.listdir(binaries_dir
):
680 if os
.path
.isfile(os
.path
.join(binaries_dir
, f
)):
681 _putl(os
.path
.join(binaries_dir
, f
), dest_dir
)
685 def _package_release(build_dir
, py_versions
, system_node
):
686 # Build source tarball
687 with
settings(system_node
=system_node
):
688 print green('Generating '), cyan('tarball')
691 # Build binaries (EGG)
692 print green('Generating '), cyan('eggs')
697 def package_release(py_versions
=None, build_dir
=None, system_node
=False,
698 indico_version
=None, upstream
=None, tag_name
=None,
699 github_auth
=None, overwrite
=None, ssh_server_host
=None,
700 ssh_server_port
=None, ssh_user
=None, ssh_key
=None,
701 ssh_dest_dir
=None, no_clean
=False, force_clean
=False,
702 upload_to
=None, build_here
=False):
704 Create an Indico release - source and binary distributions
707 DEVELOP_REQUIRES
= ['pojson>=0.4', 'termcolor', 'werkzeug', 'nodeenv', 'fabric',
708 'sphinx', 'repoze.sphinx.autointerface']
710 py_versions
= py_versions
.split('/') if py_versions
else env
.py_versions
711 upload_to
= upload_to
.split('/') if upload_to
else []
713 build_dir
= build_dir
or env
.build_dir
714 upstream
= upstream
or env
.github
['upstream']
716 ssh_server_host
= ssh_server_host
or env
.ssh
['host']
717 ssh_server_port
= ssh_server_port
or env
.ssh
['port']
718 ssh_user
= ssh_user
or env
.ssh
['user']
719 ssh_key
= ssh_key
or env
.ssh
['key']
720 ssh_dest_dir
= ssh_dest_dir
or env
.ssh
['dest_dir']
722 indico_version
= indico_version
or 'master'
724 local('mkdir -p {0}'.format(build_dir
))
726 _check_pyenv(py_versions
)
729 _package_release(os
.path
.dirname(__file__
), py_versions
, system_node
)
732 if os
.path
.exists(os
.path
.join(build_dir
, 'indico')):
733 print yellow("Repository seems to already exist.")
735 local('git fetch {0}'.format(upstream
))
736 local('git reset --hard FETCH_HEAD')
738 local('git clean -df')
740 local('git clone {0}'.format(upstream
))
742 print green("Checking out branch \'{0}\'".format(indico_version
))
743 local('git checkout {0}'.format(indico_version
))
745 _package_release(os
.path
.join(build_dir
, 'indico'), py_versions
, system_node
)
749 upload_github(build_dir
, tag_name
, github_auth
, overwrite
, indico_version
)
751 upload_ssh(build_dir
, ssh_server_host
, ssh_server_port
, ssh_user
, ssh_key
, ssh_dest_dir
)
753 if not build_here
and force_clean
:
754 cleanup(build_dir
, force
=True)