diff --git a/.clutterignore b/.clutterignore new file mode 100644 index 0000000000..6292af03f4 --- /dev/null +++ b/.clutterignore @@ -0,0 +1,2 @@ +cctbx_version.txt +setup.py diff --git a/.gitignore b/.gitignore index fad06bedca..a07481a5e2 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,10 @@ git-notifier.log # Ignores examples from cctbx website cctbx_website/examples +# Ignore cctbx version files +cctbx_version* +setup.py + # Ignores copied over from subversion: cctbx/dev cctbx/doc diff --git a/libtbx/auto_build/bootstrap.py b/libtbx/auto_build/bootstrap.py index a2cd3f37c2..e16f05140c 100644 --- a/libtbx/auto_build/bootstrap.py +++ b/libtbx/auto_build/bootstrap.py @@ -1510,6 +1510,11 @@ def run(self): use_ssh=use_git_ssh, verbose=True, reference=reference_repository_path) self.add_step(_indirection()) + # Update version information + if module == 'cctbx_project': + workdir = ['modules', module] + self.add_step(self.shell(command=['python', os.path.join('libtbx', 'version.py')], workdir=workdir)) + # Use dials-2.2 branches for Python 2 if (module == 'dials' or module == 'dxtbx' or module == 'xia2') and not self.python3: workdir = ['modules', module] diff --git a/libtbx/libtbx_refresh.py b/libtbx/libtbx_refresh.py new file mode 100644 index 0000000000..cf441f0fcb --- /dev/null +++ b/libtbx/libtbx_refresh.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, division, print_function + +import os +import shutil + +import libtbx.load_env +from libtbx.version import create_version_files + +# try creating version files again +# this should already be done by bootstrap.py, but try anyway +# this will fail if git is not available +filenames = [] +try: + filenames = create_version_files() +except Exception: + pass + +# copy version file and header to build directory +try: + shutil.copy(filenames[0], abs(libtbx.env.build_path)) + cctbx_include = os.path.join(abs(libtbx.env.build_path), 'include', 'cctbx') + if not os.path.exists(cctbx_include): + os.mkdir(cctbx_include) + shutil.copy(filenames[1], cctbx_include) +except Exception: + pass diff --git a/libtbx/run_tests.py b/libtbx/run_tests.py index d6d138a607..85cfdfa5d7 100644 --- a/libtbx/run_tests.py +++ b/libtbx/run_tests.py @@ -38,6 +38,7 @@ "$D/tst_representation.py", "$D/tst_find_unused_imports.py", "$D/tst_program_template.py", + "$D/tst_version.py", ] # generally failing tests diff --git a/libtbx/tst_version.py b/libtbx/tst_version.py new file mode 100644 index 0000000000..b8ac19ee12 --- /dev/null +++ b/libtbx/tst_version.py @@ -0,0 +1,17 @@ +from __future__ import absolute_import, division, print_function + +from libtbx.version import get_version + +# ============================================================================= +def test_version(): + + assert get_version() is not None + assert get_version(filename='random_nonexisting_filename') is not None + assert get_version(filename='random_nonexisting_filename', fail_with_none=True) is None + +# ============================================================================= +if __name__ == '__main__': + test_version() + +# ============================================================================= +# end diff --git a/libtbx/version.py b/libtbx/version.py new file mode 100644 index 0000000000..04d61574d1 --- /dev/null +++ b/libtbx/version.py @@ -0,0 +1,201 @@ +from __future__ import absolute_import, division, print_function + +import os +import subprocess +import sys +import time + +# ============================================================================= +def create_version_files(git_repo='cctbx_project', basename='cctbx_version', + version=None, setup_template=None): + ''' + Function for creating the files containing the version. This + function is called by bootstrap.py after downloading the git + repository. The development version is the date of the commit and + the commit information from "git describe". Files containing an + official release version can created by providing the version as an + argument + + Parameters + ---------- + git_repo: str + The git repository to be versioned. This is the directory name in + "modules" (e.g. cctbx_project) + basename: str + The basename for the filenames. It is also the name of the defintion + in the C++ header. The ".txt" and ".h" extensions will be added to + the filenames. + version: str + If set, this argument is used as the version + setup_template: str + A template for the setup.py file. There should be a {version} field. + + Returns + ------- + filenames: str + A tuple containing the three filenames. The first is the plain text + file, the second is a C++ header file, and the last is a setup.py + file. These files are located in the git repository. The plain text + and header cab be copied to the build directory in libtbx_refresh.py. + ''' + + if version is None: + # create {y}.{m}.dev{d}+{n}.{h} formatted version + y = None # year + m = None # month + d = None # day + n = None # number of commits since last tag + h = None # g + hash of commit + + path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', git_repo) + if not os.path.isdir(path): + raise RuntimeError('The {path} directory does not exist.'.format(path=path)) + + try: + t = subprocess.check_output(['git', 'log', '-1', '--pretty=%ci'], cwd=path).decode('utf8') + t = t.split()[0].split('-') + y = int(t[0]) + m = int(t[1]) + d = int(t[2]) + except subprocess.CalledProcessError: + t = time.localtime() + y = t.tm_year + m = t.tm_mon + d = t.tm_mday + try: + output = subprocess.check_output(['git', 'describe'], cwd=path).decode('utf8') + output = output.split('-') + n = int(output[-2]) + h = output[-1].strip() + except subprocess.CalledProcessError: + pass + + version = '{y}.{m}.dev{d}'.format(y=y, m=m, d=d) + + # add latest commit information as local version + if n is not None and h is not None: + version += '+{n}.{h}'.format(n=n, h=h) + else: + version += '+unknown' + + # write plain text + txt_filename = os.path.join(path, basename + '.txt') + with open(txt_filename, 'w') as f: + f.write(version) + + # write C++ header + header_template = '''\ +// {basename} version header +// This file is automatically generated + +#ifndef {basename}_H +#define {basename}_H + +#define {basename} "{version}" + +#endif +''' + h_filename = os.path.join(path, basename + '.h') + with open(h_filename, 'w') as f: + f.write(header_template.format(basename=basename.upper(), version=version)) + + # write setup.py + if setup_template is None: + setup_template = '''\ +from setuptools import setup +setup( + name='cctbx-base', + version='{version}', + url='https://github.com/cctbx/cctbx_project', + description='The Computational Crystallography Toolbox (cctbx) is being developed as the open source component of the Phenix system. The goal of the Phenix project is to advance automation of macromolecular structure determination. Phenix depends on the cctbx, but not vice versa. This hierarchical approach enforces a clean design as a reusable library. The cctbx is therefore also useful for small-molecule crystallography and even general scientific applications.', + author='CCTBX developers', + author_email='cctbx@cci.lbl.gov', + maintainer='CCTBX developers', + maintainer_email='cctbx@cci.lbl.gov', + license='BSD-3-Clause-LBNL AND BSD-3-Clause AND BSL-1.0 AND LGPL-2.0-only AND LGPL-2.1-only AND LGPL-3.0-only AND MIT AND LGPL-2.0-or-later WITH WxWindows-exception-3.1' +) +''' + + setup_filename = os.path.join(path, 'setup.py') + with open(setup_filename, 'w') as f: + f.write(setup_template.format(version=version)) + + return (txt_filename, h_filename, setup_filename) + +# ----------------------------------------------------------------------------- +def get_version(filename='cctbx_version.txt', fail_with_none=False): + ''' + Function for returning the version of the current installation + The file containing the version is manually created for official + releases and created by bootstrap.py for development releases. The + version follows calendar versioning, so in the event that the file + cannot be read, a version based on the current date can be returned. + + Parameters + ---------- + filename: str + The filename of the file containing the version number. This can + be a full path. Otherwise, the directory is assumed to be the build + directory. + fail_with_none: bool + If set, any failure to read the version file will return None. + Otherwise, the current date is used to generate the version. + + Returns + ------- + str or None + ''' + import libtbx.load_env + + version = None + path = filename + if not os.path.isabs(path): + path = os.path.join(abs(libtbx.env.build_path), filename) + + try: + with open(path) as f: + version = f.read().strip() + except IOError: + if fail_with_none: + return None + + if version is None: + t = time.localtime() + version = '{y}.{m}.dev{d}+unknown'.format(y=t.tm_year, m=t.tm_mon, d=t.tm_mday) + + return version + +# ============================================================================= +if __name__ == '__main__': + import argparse + parser = argparse.ArgumentParser( + description='Command for generating files containing version information.') + parser.add_argument( + '--git-repo', + help='The name of the git repository in the "modules" directory.', + default='cctbx_project') + parser.add_argument( + '--basename', + help='The base name for the version filenames.', + default='cctbx_version') + parser.add_argument( + '--version', + help='An explicit version to be set.', + default=None) + + namespace = parser.parse_args(sys.argv[1:]) + + filenames = create_version_files( + git_repo=namespace.git_repo, + basename=namespace.basename, + version=namespace.version) + + print('Writing files containing version information for {git_repo}'.\ + format(git_repo=namespace.git_repo)) + print('='*79) + for filename in filenames: + print('Wrote {filename}'.format(filename=filename)) + print('='*79) + +# ============================================================================= +# end