diff --git a/MANIFEST.in b/MANIFEST.in index a0062f56..3f3d04ac 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,6 +5,7 @@ include CHANGES.md include AUTHORS include requirements.txt include download.py +include install.py include tests/README.md include tests/pytest.ini include tests/inspect_cython_signatures.py diff --git a/install.py b/install.py new file mode 100644 index 00000000..62933116 --- /dev/null +++ b/install.py @@ -0,0 +1,145 @@ +"""Install `dd`, including C extensions wrapping CUDD. + +Running this file will attempt to download +the source code of CUDD from the internet. + +To run this file: + +```shell +python install.py +``` + +This script is a shorthand for defining environment +variables when running `pip install .` for the +source code of the package `dd`. +""" +import argparse as _arg +import os +import re +import shlex as _sh +import subprocess as _sbp +import tempfile as _tmp + +try: + import cython +except ImportError as error: + cython = None + _cython_error = error + + +def _main( + ) -> None: + """Entry point.""" + args, unknown_args = _parse_args() + _assert_cython() + if not _ask_about_downloading(args): + print('Exiting without installing.') + return + with _tmp.NamedTemporaryFile('wt') as fd: + _install_using_pip(fd, unknown_args) + + +def _install_using_pip( + fd, + unknown_args: + list[str] + ) -> None: + """Install `dd` using `pip`. + + The installation includes `dd.cudd`. + + Writes a temporary file to + the file system, used as requirements + file for `pip`. + """ + env = dict(os.environ) + env.update(dict( + DD_FETCH='1', + DD_CUDD='1', + DD_CUDD_ZDD='1', + DD_CUDD_ADD='1')) + cmd_line = f''' + pip install + . + -vvv + --use-pep517 + --no-build-isolation + ''' + cmd = _sh.split(cmd_line) + cmd.extend(unknown_args) + _sbp.run(cmd, env=env) + + +def _parse_args( + ) -> tuple[ + _arg.Namespace, + list[str]]: + """Return command-line arguments.""" + parser = _arg.ArgumentParser(description=''' + Install package `dd` from its source, + including the modules `dd.cudd`, `dd.cudd_zdd`, + and `dd.cudd_add` (which are C extensions + that need `cython` and `gcc` to be compiled). + + Attempts to download the source code of + CUDD from the internet. + + Unknown arguments are passed to `pip install`. + ''') + parser.add_argument( + '-y', '--yes', + action='store_true', + help='go ahead and download CUDD ' + 'source code tarball ' + '(checking its SHA hash) ' + 'without stopping and ' + 'prompting the user ' + 'for whether to do so.') + args, unknown_args = parser.parse_known_args() + return args, unknown_args + + +def _assert_cython( + ) -> None: + """Assert that `cython` is installed. + + Raise `ImportError` if not. + """ + if cython is not None: + return + raise ImportError( + 'Installing `dd.cudd` requires the ' + 'Python package `cython`, which can ' + 'be installed from ' + 'the Python Package Index (PyPI) ' + 'by running `pip install cython`. ' + 'For installing `dd` with only ' + 'its pure-Python modules, ' + 'running `pip install .` suffices, ' + 'no need to run `python install.py`.' + ) from _cython_error + + +def _ask_about_downloading( + args: + _arg.Namespace + ) -> bool: + """Ask whether to download CUDD tarball.""" + if args.yes: + return True + choice = input( + 'This script downloads ' + 'CUDD sources from the internet ' + '(details in the file `download.py`).\n' + 'Proceed?\n' + '(answers: yes / no)\n') + match choice: + case 'yes' | 'no': + return (choice == 'yes') + raise ValueError( + 'Expected `yes` or ' + '`no` as input.') + + +if __name__ == '__main__': + _main()