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
))
374 def install(recipe_name
):
376 Install a module given the recipe name
378 RECIPES
[recipe_name
]()
382 def init_submodules(src_dir
='.'):
384 Initialize submodules (fetch them from external Git repos)
387 print green("Initializing submodules")
390 local('git submodule update --init --recursive')
395 Install asset dependencies
397 print green("Installing asset dependencies...")
398 for recipe_name
in RECIPES
:
399 print cyan("Installing {0}".format(recipe_name
))
404 def setup_deps(n_env
=None, n_version
=None, src_dir
=None, system_node
=None):
406 Setup (fetch and install) dependencies for Indico assets
409 src_dir
= src_dir
or env
.src_dir
410 n_env
= n_env
or env
.node_env_path
411 system_node
= system_node
.lower() in ('1', 'true') if system_node
is not None else env
.system_node
413 # initialize submodules if they haven't yet been
414 init_submodules(src_dir
)
416 ext_dir
= os
.path
.join(src_dir
, 'ext_modules')
418 _check_present('curl')
420 with
settings(node_env_path
=n_env
or os
.path
.join(ext_dir
, 'node_env'),
421 node_version
=n_version
or env
.node_version
,
422 system_node
=system_node
,
426 if not system_node
and not os
.path
.exists(n_env
):
430 local('npm install -g grunt-cli')
436 def clean_deps(src_dir
=None):
438 Clean up generated files
441 for dtype
in ASSET_TYPES
:
442 _safe_rm('{0}/*'.format(lib_dir(src_dir
or env
.src_dir
, dtype
)), recursive
=True)
446 def cleanup(build_dir
=None, force
=False):
448 Clean up build environment
450 _safe_rm('{0}'.format(build_dir
or env
.build_dir
), recursive
=True, ask
=(not force
))
454 def tarball(src_dir
=None):
456 Create a source Indico distribution (tarball)
459 src_dir
= src_dir
or env
.src_dir
463 setup_deps(n_env
=os
.path
.join(src_dir
, 'ext_modules', 'node_env'),
465 local('python setup.py -q sdist')
469 def egg(py_versions
=None):
471 Create a binary Indico distribution (egg)
474 for py_version
in py_versions
:
475 cmd_dir
= os
.path
.join(env
.pyenv_dir
, 'versions', 'indico-build-{0}'.format(py_version
), 'bin')
476 local('{0} setup.py -q bdist_egg'.format(os
.path
.join(cmd_dir
, 'python')))
477 print green(local('ls -lah dist/', capture
=True))
481 def make_docs(src_dir
=None, build_dir
=None, force
=False):
486 src_dir
= src_dir
or env
.src_dir
487 doc_src_dir
= os
.path
.join(src_dir
, 'doc')
489 if build_dir
is None:
490 target_dir
= os
.path
.join(src_dir
, 'indico', 'htdocs', 'ihelp')
492 target_dir
= os
.path
.join(build_dir
or env
.build_dir
, 'indico', 'htdocs', 'ihelp')
495 print yellow("Checking if docs need to be generated... "),
496 if _find_most_recent(target_dir
) > _find_most_recent(doc_src_dir
):
501 _check_present('pdflatex')
503 print green('Generating documentation')
504 with
lcd(doc_src_dir
):
509 local('rm -rf {0}/*'.format(os
.path
.join(target_dir
, 'html')))
510 local('mv build/html/* {0}'.format(os
.path
.join(target_dir
, 'html')))
512 with
lcd(os
.path
.join('guides', 'build', 'latex')):
513 local('make all-pdf')
514 local('mv *.pdf {0}'.format(os
.path
.join(target_dir
, 'pdf')))
516 print green('Cleaning up')
522 def _check_request_error(r
):
523 if r
.status_code
>= 400:
525 msg
= j
.get('message', DEFAULT_REQUEST_ERROR_MSG
)
526 print red("ERROR: {0} ({1})".format(msg
, r
.status_code
))
530 def _valid_github_credentials(auth
):
531 url
= "https://api.github.com/repos/{0}/{1}".format(env
.github
['org'], env
.github
['repo'])
532 r
= requests
.get(url
, auth
=(env
.github
['user'], auth
))
533 if (r
.status_code
== 401) and (r
.json().get('message') == 'Bad credentials'):
534 print red('Invalid Github credentials for user \'{0}\''.format(env
.github
['user']))
540 def _release_exists(tag_name
, auth
):
541 url
= "https://api.github.com/repos/{0}/{1}/releases".format(env
.github
['org'], env
.github
['repo'])
542 r
= requests
.get(url
, auth
=(env
.github
['user'], auth
))
543 _check_request_error(r
)
545 for release
in parsed
:
546 if release
.get('tag_name') == tag_name
:
547 rel_id
= release
.get('id')
548 return (True, rel_id
, release
)
550 return (False, 0, None)
553 def _asset_exists(rel_id
, name
, auth
):
554 url
= "https://api.github.com/repos/{0}/{1}/releases/{2}/assets" \
555 .format(env
.github
['org'], env
.github
['repo'], rel_id
)
556 r
= requests
.get(url
, auth
=(env
.github
['user'], auth
))
557 _check_request_error(r
)
560 if j
.get('name') == name
:
561 asset_id
= j
.get('id')
562 return (True, asset_id
)
568 def upload_github(build_dir
=None, tag_name
=None, auth_token
=None,
569 overwrite
=None, indico_version
='master'):
571 build_dir
= build_dir
or env
.build_dir
572 auth_token
= auth_token
or env
.github
['auth_token']
574 while (auth_token
is None) or (not _valid_github_credentials(auth_token
)):
575 auth_token
= getpass
.getpass(
576 'Insert the Github password/OAuth token for user \'{0}\': '.format(env
.github
['user']))
578 auth_creds
= (env
.github
['user'], auth_token
)
580 overwrite
= overwrite
or env
.github
['overwrite']
582 # Create a new release
583 tag_name
= tag_name
or indico_version
584 url
= "https://api.github.com/repos/{0}/{1}/releases".format(env
.github
['org'], env
.github
['repo'])
586 'tag_name': tag_name
,
587 'target_commitish': indico_version
,
588 'name': 'Indico {0}'.format(tag_name
),
592 (exists
, rel_id
, release_data
) = _release_exists(tag_name
, auth_token
)
595 if overwrite
is None:
596 overwrite
= _yes_no_input('Release already exists, do you want to overwrite', 'n')
602 # We will need to get a new release id from github
603 r
= requests
.post(url
, auth
=auth_creds
, data
=json
.dumps(payload
))
604 _check_request_error(r
)
605 release_data
= r
.json()
606 release_id
= release_data
.get('id')
608 # Upload binaries to the new release
609 binaries_dir
= os
.path
.join(build_dir
, 'indico', 'dist')
611 # awful way to handle this, but a regex seems like too much
612 url
= release_data
['upload_url'][:-7]
614 for f
in os
.listdir(binaries_dir
):
616 # jump over hidden/system files
617 if f
.startswith('.'):
620 if os
.path
.isfile(os
.path
.join(binaries_dir
, f
)):
621 (exists
, asset_id
) = _asset_exists(release_id
, f
, auth_token
)
624 # delete previous version
625 del_url
= "https://api.github.com/repos/{0}/{1}/releases/assets/{2}" \
626 .format(env
.github
['org'], env
.github
['repo'], asset_id
)
627 r
= requests
.delete(del_url
, auth
=auth_creds
)
628 _check_request_error(r
)
630 with
open(os
.path
.join(binaries_dir
, f
), 'rb') as ff
:
632 extension
= os
.path
.splitext(f
)[1]
634 # upload eggs using zip mime type
635 if extension
== '.gz':
636 headers
= {'Content-Type': 'application/x-gzip'}
637 elif extension
== '.egg':
638 headers
= {'Content-Type': 'application/zip'}
640 headers
['Accept'] = 'application/vnd.github.v3+json'
641 headers
['Content-Length'] = len(data
)
644 print green("Uploading \'{0}\' to Github".format(f
))
645 r
= requests
.post(url
, auth
=auth_creds
, headers
=headers
, data
=data
, params
=params
, verify
=False)
646 _check_request_error(r
)
650 def upload_ssh(build_dir
=None, server_host
=None, server_port
=None,
651 ssh_user
=None, ssh_key
=None, dest_dir
=None):
653 build_dir
= build_dir
or env
.build_dir
654 server_host
= server_host
or env
.ssh
['host']
655 server_port
= server_port
or env
.ssh
['port']
656 ssh_user
= ssh_user
or env
.ssh
['user']
657 ssh_key
= ssh_key
or env
.ssh
['key']
658 dest_dir
= dest_dir
or env
.ssh
['dest_dir']
660 env
.host_string
= server_host
+ ':' + server_port
662 env
.key_filename
= ssh_key
664 binaries_dir
= os
.path
.join(build_dir
, 'indico', 'dist')
665 for f
in os
.listdir(binaries_dir
):
666 if os
.path
.isfile(os
.path
.join(binaries_dir
, f
)):
667 _putl(os
.path
.join(binaries_dir
, f
), dest_dir
)
671 def _package_release(build_dir
, py_versions
, system_node
):
672 # Build source tarball
673 with
settings(system_node
=system_node
):
674 print green('Generating '), cyan('tarball')
677 # Build binaries (EGG)
678 print green('Generating '), cyan('eggs')
683 def package_release(py_versions
=None, build_dir
=None, system_node
=False,
684 indico_version
=None, upstream
=None, tag_name
=None,
685 github_auth
=None, overwrite
=None, ssh_server_host
=None,
686 ssh_server_port
=None, ssh_user
=None, ssh_key
=None,
687 ssh_dest_dir
=None, no_clean
=False, force_clean
=False,
688 upload_to
=None, build_here
=False):
690 Create an Indico release - source and binary distributions
693 DEVELOP_REQUIRES
= ['pojson>=0.4', 'termcolor', 'werkzeug', 'nodeenv', 'fabric',
694 'sphinx', 'repoze.sphinx.autointerface']
696 py_versions
= py_versions
.split('/') if py_versions
else env
.py_versions
697 upload_to
= upload_to
.split('/') if upload_to
else []
699 build_dir
= build_dir
or env
.build_dir
700 upstream
= upstream
or env
.github
['upstream']
702 ssh_server_host
= ssh_server_host
or env
.ssh
['host']
703 ssh_server_port
= ssh_server_port
or env
.ssh
['port']
704 ssh_user
= ssh_user
or env
.ssh
['user']
705 ssh_key
= ssh_key
or env
.ssh
['key']
706 ssh_dest_dir
= ssh_dest_dir
or env
.ssh
['dest_dir']
708 indico_version
= indico_version
or 'master'
710 local('mkdir -p {0}'.format(build_dir
))
712 _check_pyenv(py_versions
)
715 _package_release(os
.path
.dirname(__file__
), py_versions
, system_node
)
718 if os
.path
.exists(os
.path
.join(build_dir
, 'indico')):
719 print yellow("Repository seems to already exist.")
721 local('git fetch {0}'.format(upstream
))
722 local('git reset --hard FETCH_HEAD')
724 local('git clean -df')
726 local('git clone {0}'.format(upstream
))
728 print green("Checking out branch \'{0}\'".format(indico_version
))
729 local('git checkout {0}'.format(indico_version
))
731 _package_release(os
.path
.join(build_dir
, 'indico'), py_versions
, system_node
)
735 upload_github(build_dir
, tag_name
, github_auth
, overwrite
, indico_version
)
737 upload_ssh(build_dir
, ssh_server_host
, ssh_server_port
, ssh_user
, ssh_key
, ssh_dest_dir
)
739 if not build_here
and force_clean
:
740 cleanup(build_dir
, force
=True)