Browse Source

Extended setup.py script similar to pycarl one

refactoring
Matthias Volk 7 years ago
parent
commit
feadda0d86
  1. 227
      setup.py
  2. 1
      setup/__init__.py
  3. 90
      setup/config.py
  4. 74
      setup/helper.py

227
setup.py

@ -1,57 +1,21 @@
#!/usr/bin/env python
import os import os
import multiprocessing
import sys import sys
import subprocess import subprocess
import datetime import datetime
import re
from setuptools import setup, Extension from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext from setuptools.command.build_ext import build_ext
from setuptools.command.test import test from setuptools.command.test import test
from distutils.version import StrictVersion
import importlib.util
import setup.helper as setup_helper
from setup.config import SetupConfig
if sys.version_info[0] == 2: if sys.version_info[0] == 2:
sys.exit('Sorry, Python 2.x is not supported') sys.exit('Sorry, Python 2.x is not supported')
def check_storm_compatible(storm_v_major, storm_v_minor, storm_v_patch):
if storm_v_major < 1 or (storm_v_major == 1 and storm_v_minor < 1) or (storm_v_major == 1 and storm_v_minor == 1 and storm_v_patch < 0):
sys.exit('Sorry, Storm version {}.{}.{} is not supported anymore!'.format(storm_v_major, storm_v_minor,
storm_v_patch))
def parse_storm_version(version_string):
"""
Parses the version of storm.
:param version_string:
:return: Version as three-tuple.
"""
elems = version_string.split(".")
if len(elems) != 3:
sys.exit('Storm version string is ill-formed: "{}"'.format(version_string))
return int(elems[0]), int(elems[1]), int(elems[2])
def obtain_version():
"""
Obtains the version as specified in stormpy.
:return: Version of stormpy.
"""
verstr = "unknown"
try:
verstrline = open('lib/stormpy/_version.py', "rt").read()
except EnvironmentError:
pass # Okay, there is no version file.
else:
VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
mo = re.search(VSRE, verstrline, re.M)
if mo:
verstr = mo.group(1)
else:
raise RuntimeError("unable to find version in stormpy/_version.py")
return verstr
# Minimal storm version required
storm_min_version = "1.1.0"
class CMakeExtension(Extension): class CMakeExtension(Extension):
@ -64,61 +28,102 @@ class CMakeExtension(Extension):
class CMakeBuild(build_ext): class CMakeBuild(build_ext):
user_options = build_ext.user_options + [ user_options = build_ext.user_options + [
('storm-dir=', None, 'Path to storm root (binary) location'), ('storm-dir=', None, 'Path to storm root (binary) location'),
('jobs=', 'j', 'Number of jobs to use for compiling'),
('disable-dft', None, 'Disable support for DFTs'),
('disable-pars', None, 'Disable support for parametric models'),
('debug', None, 'Build in Debug mode'), ('debug', None, 'Build in Debug mode'),
('jobs=', 'j', 'Number of jobs to use for compiling'),
] ]
def extdir(self, extname):
config = SetupConfig()
def _extdir(self, extname):
return os.path.abspath(os.path.dirname(self.get_ext_fullpath(extname))) return os.path.abspath(os.path.dirname(self.get_ext_fullpath(extname)))
def run(self): def run(self):
self.conf = None
try: try:
_ = subprocess.check_output(['cmake', '--version']) _ = subprocess.check_output(['cmake', '--version'])
except OSError: except OSError:
raise RuntimeError("CMake must be installed to build the following extensions: " + raise RuntimeError("CMake must be installed to build the following extensions: " +
", ".join(e.name for e in self.extensions)) ", ".join(e.name for e in self.extensions))
# Build cmake version info
build_temp_version = self.build_temp + "-version" build_temp_version = self.build_temp + "-version"
if not os.path.exists(build_temp_version):
os.makedirs(build_temp_version)
setup_helper.ensure_dir_exists(build_temp_version)
# Write config
setup_helper.ensure_dir_exists("build")
self.config.write_config("build/build_config.cfg")
# Check cmake variable values
cmake_args = [] cmake_args = []
if self.storm_dir is not None:
cmake_args = ['-Dstorm_DIR=' + self.storm_dir]
output = subprocess.check_output(['cmake', os.path.abspath("cmake")] + cmake_args, cwd=build_temp_version)
spec = importlib.util.spec_from_file_location("genconfig",
os.path.join(build_temp_version, 'generated/config.py'))
self.conf = importlib.util.module_from_spec(spec)
spec.loader.exec_module(self.conf)
# Check storm version
storm_v_major, storm_v_minor, storm_v_patch = parse_storm_version(self.conf.STORM_VERSION)
check_storm_compatible(storm_v_major, storm_v_minor, storm_v_patch)
# Create dir
lib_path = os.path.join(self.extdir("core"))
if not os.path.exists(lib_path):
os.makedirs(lib_path)
storm_dir = self.config.get_as_string("storm_dir")
if storm_dir:
cmake_args += ['-Dstorm_DIR=' + storm_dir]
_ = subprocess.check_output(['cmake', os.path.abspath("cmake")] + cmake_args, cwd=build_temp_version)
cmake_conf = setup_helper.load_cmake_config(os.path.join(build_temp_version, 'generated/config.py'))
# Set storm directory
if storm_dir == "":
storm_dir = cmake_conf.STORM_DIR
if storm_dir != cmake_conf.STORM_DIR:
print("Stormpy - Warning: Using different storm directory {} instead of given {}!".format(
cmake_conf.STORM_DIR,
storm_dir))
storm_dir = cmake_conf.STORM_DIR
# Check version
storm_version, storm_commit = setup_helper.parse_storm_version(cmake_conf.STORM_VERSION)
if StrictVersion(storm_version) < StrictVersion(storm_min_version):
sys.exit(
'Stormpy - Error: Storm version {} from \'{}\' is not supported anymore!'.format(storm_version,
storm_dir))
# Check additional support
use_dft = cmake_conf.HAVE_STORM_DFT and not self.config.get_as_bool("disable_dft")
use_pars = cmake_conf.HAVE_STORM_PARS and not self.config.get_as_bool("disable_pars")
# Print build info
print("Stormpy - Using storm {} from {}".format(storm_version, storm_dir))
if use_dft:
print("Stormpy - Support for DFTs found and included.")
else:
print("Stormpy - Warning: No support for DFTs!")
if use_pars:
print("Stormpy - Support for parametric models found and included.")
else:
print("Stormpy - Warning: No support for parametric models!")
# Set general cmake build options
build_type = 'Debug' if self.config.get_as_bool("debug") else 'Release'
cmake_args = ['-DPYTHON_EXECUTABLE=' + sys.executable]
cmake_args += ['-DCMAKE_BUILD_TYPE=' + build_type]
if storm_dir is not None:
cmake_args += ['-Dstorm_DIR=' + storm_dir]
if use_dft:
cmake_args += ['-DHAVE_STORM_DFT=ON']
if use_pars:
cmake_args += ['-DHAVE_STORM_PARS=ON']
build_args = ['--config', build_type]
build_args += ['--', '-j{}'.format(self.config.get_as_int("jobs"))]
# Build extensions
for ext in self.extensions: for ext in self.extensions:
setup_helper.ensure_dir_exists(os.path.join(self._extdir(ext.name), ext.subdir))
if ext.name == "core": if ext.name == "core":
with open(os.path.join(self.extdir(ext.name), ext.subdir, "_config.py"), "w") as f:
with open(os.path.join(self._extdir(ext.name), ext.subdir, "_config.py"), "w") as f:
f.write("# Generated from setup.py at {}\n".format(datetime.datetime.now())) f.write("# Generated from setup.py at {}\n".format(datetime.datetime.now()))
f.write("import pycarl\n") f.write("import pycarl\n")
if self.conf.STORM_CLN_EA or self.conf.STORM_CLN_RF:
if cmake_conf.STORM_CLN_EA or cmake_conf.STORM_CLN_RF:
f.write("import pycarl.cln\n") f.write("import pycarl.cln\n")
if not self.conf.STORM_CLN_EA or not self.conf.STORM_CLN_RF:
if not cmake_conf.STORM_CLN_EA or not cmake_conf.STORM_CLN_RF:
f.write("import pycarl.gmp\n") f.write("import pycarl.gmp\n")
if self.conf.STORM_CLN_EA:
if cmake_conf.STORM_CLN_EA:
f.write("Rational = pycarl.cln.Rational\n") f.write("Rational = pycarl.cln.Rational\n")
else: else:
f.write("Rational = pycarl.gmp.Rational\n") f.write("Rational = pycarl.gmp.Rational\n")
if self.conf.STORM_CLN_RF:
if cmake_conf.STORM_CLN_RF:
rfpackage = "cln" rfpackage = "cln"
else: else:
rfpackage = "gmp" rfpackage = "gmp"
@ -128,67 +133,61 @@ class CMakeBuild(build_ext):
f.write("RationalFunction = pycarl.{}.RationalFunction\n".format(rfpackage)) f.write("RationalFunction = pycarl.{}.RationalFunction\n".format(rfpackage))
f.write("FactorizedRationalFunction = pycarl.{}.FactorizedRationalFunction\n".format(rfpackage)) f.write("FactorizedRationalFunction = pycarl.{}.FactorizedRationalFunction\n".format(rfpackage))
f.write("\n") f.write("\n")
f.write("storm_with_pars = {}\n".format(self.conf.HAVE_STORM_PARS))
f.write("storm_with_dft = {}\n".format(self.conf.HAVE_STORM_DFT))
f.write("storm_with_dft = {}\n".format(use_dft))
f.write("storm_with_pars = {}\n".format(use_pars))
elif ext.name == "info": elif ext.name == "info":
with open(os.path.join(self.extdir(ext.name), ext.subdir, "_config.py"), "w") as f:
with open(os.path.join(self._extdir(ext.name), ext.subdir, "_config.py"), "w") as f:
f.write("# Generated from setup.py at {}\n".format(datetime.datetime.now())) f.write("# Generated from setup.py at {}\n".format(datetime.datetime.now()))
f.write("storm_version = \"{}\"\n".format(self.conf.STORM_VERSION))
f.write("storm_cln_ea = {}\n".format(self.conf.STORM_CLN_EA))
f.write("storm_cln_rf = {}".format(self.conf.STORM_CLN_RF))
elif ext.name == "pars":
with open(os.path.join(self.extdir(ext.name), ext.subdir, "_config.py"), "w") as f:
f.write("storm_version = \"{}\"\n".format(storm_version))
f.write("storm_cln_ea = {}\n".format(cmake_conf.STORM_CLN_EA))
f.write("storm_cln_rf = {}".format(cmake_conf.STORM_CLN_RF))
elif ext.name == "dft":
with open(os.path.join(self._extdir(ext.name), ext.subdir, "_config.py"), "w") as f:
f.write("# Generated from setup.py at {}\n".format(datetime.datetime.now())) f.write("# Generated from setup.py at {}\n".format(datetime.datetime.now()))
f.write("storm_with_pars = {}".format(self.conf.HAVE_STORM_PARS))
if not self.conf.HAVE_STORM_PARS:
print("WARNING: storm-pars not found. No support for parametric analysis will be built.")
f.write("storm_with_dft = {}".format(use_dft))
if not use_dft:
print("Stormpy - DFT bindings skipped")
continue continue
elif ext.name == "dft":
with open(os.path.join(self.extdir(ext.name), ext.subdir, "_config.py"), "w") as f:
elif ext.name == "pars":
with open(os.path.join(self._extdir(ext.name), ext.subdir, "_config.py"), "w") as f:
f.write("# Generated from setup.py at {}\n".format(datetime.datetime.now())) f.write("# Generated from setup.py at {}\n".format(datetime.datetime.now()))
f.write("storm_with_dft = {}".format(self.conf.HAVE_STORM_DFT))
if not self.conf.HAVE_STORM_DFT:
print("WARNING: storm-dft not found. No support for DFTs will be built.")
f.write("storm_with_pars = {}".format(use_pars))
if not use_pars:
print("Stormpy - Bindings for parametric models skipped")
continue continue
self.build_extension(ext)
self.build_extension(ext, cmake_args, build_args)
def initialize_options(self): def initialize_options(self):
build_ext.initialize_options(self) build_ext.initialize_options(self)
# Load setup config
self.config.load_from_file("build/build_config.cfg")
# Set default values for custom cmdline flags
self.storm_dir = None self.storm_dir = None
self.debug = False
try:
self.jobs = multiprocessing.cpu_count() if multiprocessing.cpu_count() is not None else 1
except NotImplementedError:
self.jobs = 1
self.disable_dft = None
self.disable_pars = None
self.debug = None
self.jobs = None
def finalize_options(self): def finalize_options(self):
if self.storm_dir is not None:
print('The custom storm directory', self.storm_dir)
build_ext.finalize_options(self) build_ext.finalize_options(self)
def build_extension(self, ext):
extdir = self.extdir(ext.name)
cmake_args = ['-DSTORMPY_LIB_DIR=' + extdir,
'-DPYTHON_EXECUTABLE=' + sys.executable]
build_type = 'Debug' if self.debug else 'Release'
build_args = ['--config', build_type]
build_args += ['--', '-j{}'.format(self.jobs)]
cmake_args += ['-DCMAKE_BUILD_TYPE=' + build_type]
if self.conf.STORM_DIR is not None:
cmake_args += ['-Dstorm_DIR=' + self.conf.STORM_DIR]
if self.conf.HAVE_STORM_PARS:
cmake_args += ['-DHAVE_STORM_PARS=ON']
if self.conf.HAVE_STORM_DFT:
cmake_args += ['-DHAVE_STORM_DFT=ON']
# Update setup config
self.config.update("storm_dir", self.storm_dir)
self.config.update("disable_dft", self.disable_dft)
self.config.update("disable_pars", self.disable_pars)
self.config.update("debug", self.debug)
self.config.update("jobs", self.jobs)
def build_extension(self, ext, general_cmake_args, general_build_args):
extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
cmake_args = ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + os.path.join(extdir, ext.subdir)] + general_cmake_args
build_args = general_build_args
env = os.environ.copy() env = os.environ.copy()
env['CXXFLAGS'] = '{} -DVERSION_INFO=\\"{}\\"'.format(env.get('CXXFLAGS', ''), env['CXXFLAGS'] = '{} -DVERSION_INFO=\\"{}\\"'.format(env.get('CXXFLAGS', ''),
self.distribution.get_version()) self.distribution.get_version())
if not os.path.exists(self.build_temp):
os.makedirs(self.build_temp)
print("CMake args={}".format(cmake_args))
setup_helper.ensure_dir_exists(self.build_temp)
print("Pycarl - CMake args={}".format(cmake_args))
# Call cmake # Call cmake
subprocess.check_call(['cmake', ext.sourcedir] + cmake_args, cwd=self.build_temp, env=env) subprocess.check_call(['cmake', ext.sourcedir] + cmake_args, cwd=self.build_temp, env=env)
subprocess.check_call(['cmake', '--build', '.', '--target', ext.name] + build_args, cwd=self.build_temp) subprocess.check_call(['cmake', '--build', '.', '--target', ext.name] + build_args, cwd=self.build_temp)
@ -204,13 +203,14 @@ class PyTest(test):
setup( setup(
name="stormpy", name="stormpy",
version=obtain_version(),
version=setup_helper.obtain_version(),
author="M. Volk", author="M. Volk",
author_email="matthias.volk@cs.rwth-aachen.de", author_email="matthias.volk@cs.rwth-aachen.de",
maintainer="S. Junges", maintainer="S. Junges",
maintainer_email="sebastian.junges@cs.rwth-aachen.de", maintainer_email="sebastian.junges@cs.rwth-aachen.de",
url="http://moves.rwth-aachen.de", url="http://moves.rwth-aachen.de",
description="stormpy - Python Bindings for Storm", description="stormpy - Python Bindings for Storm",
long_description='',
packages=['stormpy', 'stormpy.info', 'stormpy.logic', 'stormpy.storage', 'stormpy.utility', packages=['stormpy', 'stormpy.info', 'stormpy.logic', 'stormpy.storage', 'stormpy.utility',
'stormpy.pars', 'stormpy.dft'], 'stormpy.pars', 'stormpy.dft'],
package_dir={'': 'lib'}, package_dir={'': 'lib'},
@ -220,12 +220,13 @@ setup(
CMakeExtension('logic', subdir='logic'), CMakeExtension('logic', subdir='logic'),
CMakeExtension('storage', subdir='storage'), CMakeExtension('storage', subdir='storage'),
CMakeExtension('utility', subdir='utility'), CMakeExtension('utility', subdir='utility'),
CMakeExtension('pars', subdir='pars'),
CMakeExtension('dft', subdir='dft'), CMakeExtension('dft', subdir='dft'),
CMakeExtension('pars', subdir='pars'),
], ],
cmdclass={'build_ext': CMakeBuild, 'test': PyTest}, cmdclass={'build_ext': CMakeBuild, 'test': PyTest},
zip_safe=False, zip_safe=False,
install_requires=['pycarl>=2.0.1'], install_requires=['pycarl>=2.0.1'],
setup_requires=['pytest-runner'], setup_requires=['pytest-runner'],
tests_require=['pytest'], tests_require=['pytest'],
python_requires='>=3',
) )

1
setup/__init__.py

@ -0,0 +1 @@
# Intentionally left empty

90
setup/config.py

@ -0,0 +1,90 @@
import configparser
import os
import multiprocessing
class SetupConfig:
"""
Configuration for setup.
"""
def __init__(self):
"""
Create config with default values
"""
self.config = configparser.ConfigParser()
self.config["build_ext"] = self._default_values()
@staticmethod
def _default_values():
"""
Return default values for config.
:return: Dict with default values for build settings.
"""
try:
no_jobs = multiprocessing.cpu_count() if multiprocessing.cpu_count() is not None else 1
except NotImplementedError:
no_jobs = 1
return {
"storm_dir": "",
"disable_dft": False,
"disable_pars": False,
"debug": False,
"jobs": str(no_jobs),
}
def load_from_file(self, path):
"""
Load config from file.
:param path Path to config file.
"""
if os.path.isfile(path):
self.config.read(path)
if not self.config.has_section("build_ext"):
self.config["build_ext"] = self._default_values()
def write_config(self, path):
"""
Save config with build settings.
:param path Path to config file.
"""
with open(path, 'w') as configfile:
self.config.write(configfile)
def get_as_bool(self, name):
"""
Get the boolean value for option name.
:param name: Name of option.
:return Value as bool.
"""
return self.config.getboolean("build_ext", name)
def get_as_int(self, name):
"""
Get the int value for option name.
:param name: Name of option.
:return Value as integer.
"""
return self.config.getint("build_ext", name)
def get_as_string(self, name):
"""
Get the string value for option name.
:param name: Name of option.
:return Value as string.
"""
return self.config.get("build_ext", name)
def update(self, name, value):
"""
Update name with given value if value is not None.
:param name: Name of option.
:param value: New value or None
"""
if value is not None:
assert self.config.has_option("build_ext", name)
self.config.set("build_ext", name, str(value))

74
setup/helper.py

@ -0,0 +1,74 @@
import importlib
import os
import re
import sys
def ensure_dir_exists(path):
"""
Check whether the directory exists and creates it if not.
"""
assert path is not None
try:
os.makedirs(path)
except FileExistsError:
pass
except OSError as exception:
if exception.errno != errno.EEXIST:
raise IOError("Cannot create directory: " + path)
except BaseException:
raise IOError("Path " + path + " seems not valid")
def parse_storm_version(version_string):
"""
Parses the version of storm.
:param version_string: String containing version information.
:return: Tuple (version, commit)
"""
split = version_string.split('-')
version = split[0]
commit = ""
if len(split) > 1:
commit = split[1]
return version, commit
def obtain_version():
"""
Obtains the version as specified in stormpy.
:return: Version
"""
verstr = "unknown"
try:
verstrline = open('lib/stormpy/_version.py', "rt").read()
except EnvironmentError:
pass # Okay, there is no version file.
else:
VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
mo = re.search(VSRE, verstrline, re.M)
if mo:
verstr = mo.group(1)
else:
raise RuntimeError("unable to find version in stormpy/_version.py")
return verstr
def load_cmake_config(path):
"""
Load cmake config.
:param path: Path.
:return: Configuration.
"""
if sys.version_info[1] >= 5:
# Method for Python >= 3.5
spec = importlib.util.spec_from_file_location("genconfig", path)
conf = importlib.util.module_from_spec(spec)
spec.loader.exec_module(conf)
return conf
else:
# Deprecated method for Python <= 3.4
from importlib.machinery import SourceFileLoader
return SourceFileLoader("genconfig", path).load_module()
Loading…
Cancel
Save