def pyx_to_dll(filename, ext=None, force_rebuild=0): """Compile a PYX file to a DLL and return the name of the generated .so or .dll .""" assert os.path.exists(filename) path, name = os.path.split(filename) if not ext: modname, extension = os.path.splitext(name) assert extension == ".pyx", extension ext = Extension(name=modname, sources=[filename]) if DEBUG: quiet = "--verbose" else: quiet = "--quiet" args = [quiet, "build_ext"] if force_rebuild: args.append("--force") dist = Distribution({"script_name": None, "script_args": args}) if not dist.ext_modules: dist.ext_modules = [] dist.ext_modules.append(ext) dist.cmdclass = {'build_ext': build_ext} build = dist.get_command_obj('build') build.build_base = os.path.join(path, "_pyxbld") try: ok = dist.parse_command_line() except DistutilsArgError, msg: raise
def pyx_to_dll(filename, ext = None, force_rebuild = 0): """Compile a PYX file to a DLL and return the name of the generated .so or .dll .""" assert os.path.exists(filename) path, name = os.path.split(filename) if not ext: modname, extension = os.path.splitext(name) assert extension == ".pyx", extension ext = Extension(name=modname, sources=[filename]) if DEBUG: quiet = "--verbose" else: quiet = "--quiet" args = [quiet, "build_ext"] if force_rebuild: args.append("--force") dist = Distribution({"script_name": None, "script_args": args}) if not dist.ext_modules: dist.ext_modules = [] dist.ext_modules.append(ext) dist.cmdclass = {'build_ext': build_ext} build = dist.get_command_obj('build') build.build_base = os.path.join(path, "_pyxbld") try: ok = dist.parse_command_line() except DistutilsArgError, msg: raise
def build_cython_extension(py_or_pyx_file_path, cython_force_rebuild=True): """ Build a cython extension from a `.py` or `.pyx` file - build will be done in a sub-folder named `_pyxbld` in the py_or_pyx_file_path :param py_or_pyx_file_path: (str) path to a `.py` or `.pyx` file :param cython_force_rebuild: (bool) If True the cython extension is rebuild even if it was already build :return: (tuple) cython_extension_module_path, cython_module_c_file_path, cython_build_dir_path """ module_dir = path_dirname(py_or_pyx_file_path) module__cython_name = path_splitext(path_basename(py_or_pyx_file_path))[0] cython_module_c_file_path = path_join(module_dir, module__cython_name + '.c') cython_build_dir_path = path_join(module_dir, '_pyxbld') args = ['--quiet', 'build_ext', '--build-lib', module_dir] if cython_force_rebuild: args.append('--force') dist = Distribution({'script_name': None, 'script_args': args}) dist.ext_modules = [ Extension(name=module__cython_name, sources=[py_or_pyx_file_path]) ] dist.cmdclass = {'build_ext': cython_build_ext} build = dist.get_command_obj('build') build.build_base = cython_build_dir_path try: dist.parse_command_line() except DistutilsArgError as err: raise Err('utils.build_cython_extension', [ 'py_or_pyx_file_path: <{}>'.format(py_or_pyx_file_path), ' DistutilsArgError: <{}>'.format(err) ]) try: obj_build_ext = dist.get_command_obj('build_ext') dist.run_commands() cython_extension_module_path = obj_build_ext.get_outputs()[0] if path_dirname(py_or_pyx_file_path) != module_dir: raise Err('utils.build_cython_extension', [ 'py_or_pyx_file_path: <{}>'.format(py_or_pyx_file_path), ' <module_dir> differs from final <cython_module_dir>', ' module_dir: <{}>'.format(module_dir), ' cython_module_dir: <{}>'.format( path_dirname(py_or_pyx_file_path)) ]) except Exception as err: raise Err('utils.build_cython_extension', [ 'py_or_pyx_file_path: <{}>'.format(py_or_pyx_file_path), ' Exception: <{}>'.format(err) ]) return cython_extension_module_path, cython_module_c_file_path, cython_build_dir_path
def build_so(module_name, sources, setup_args=None): from distutils.dist import Distribution from distutils.errors import DistutilsArgError from distutils.extension import Extension from shutil import copy2 setup_args = generate_setup_args(setup_args) dist = Distribution(setup_args) ext = Extension( name = module_name, sources = sources, include_dirs = [ find_handsome_include_dir() ], extra_compile_args = [ '-std=c++11' ], ) if dist.ext_modules is None: dist.ext_modules = [ ext ] else: dist.ext_modules.append(ext) target_dir, _ = os.path.split(os.path.abspath(__file__)) build = dist.get_command_obj('build') build.build_base = os.path.join(target_dir, 'build') cfgfiles = dist.find_config_files() dist.parse_config_files(cfgfiles) try: ok = dist.parse_command_line() except DistutilsArgError: raise if not ok: raise RuntimeError('Build cannot continue') command = dist.get_command_obj("build_ext") dist.run_commands() so_path = os.path.abspath(command.get_outputs()[0]) _, so_name = os.path.split(so_path) target_path = os.path.join(target_dir, so_name) if os.path.isfile(target_path): os.unlink(target_path) copy2(so_path, target_path) return target_path
def build_cython_extension(py_or_pyx_file_path, cython_force_rebuild=True): """ Build a cython extension from a `.py` or `.pyx` file - build will be done in a sub-folder named `_pyxbld` in the py_or_pyx_file_path :param py_or_pyx_file_path: (str) path to a `.py` or `.pyx` file :param cython_force_rebuild: (bool) If True the cython extension is rebuild even if it was already build :return: (tuple) cython_extension_module_path, cython_module_c_file_path, cython_build_dir_path """ module_dir = path_dirname(py_or_pyx_file_path) module__cython_name = path_splitext(path_basename(py_or_pyx_file_path))[0] cython_module_c_file_path = path_join(module_dir, module__cython_name + '.c') cython_build_dir_path = path_join(module_dir, '_pyxbld') args = ['--quiet', 'build_ext', '--build-lib', module_dir] if cython_force_rebuild: args.append('--force') dist = Distribution({'script_name': None, 'script_args': args}) dist.ext_modules = [Extension(name=module__cython_name, sources=[py_or_pyx_file_path])] dist.cmdclass = {'build_ext': cython_build_ext} build = dist.get_command_obj('build') build.build_base = cython_build_dir_path try: dist.parse_command_line() except DistutilsArgError as err: raise Err('utils.build_cython_extension', [ 'py_or_pyx_file_path: <{}>'.format(py_or_pyx_file_path), ' DistutilsArgError: <{}>'.format(err) ]) try: obj_build_ext = dist.get_command_obj('build_ext') dist.run_commands() cython_extension_module_path = obj_build_ext.get_outputs()[0] if path_dirname(py_or_pyx_file_path) != module_dir: raise Err('utils.build_cython_extension', [ 'py_or_pyx_file_path: <{}>'.format(py_or_pyx_file_path), ' <module_dir> differs from final <cython_module_dir>', ' module_dir: <{}>'.format(module_dir), ' cython_module_dir: <{}>'.format(path_dirname(py_or_pyx_file_path)) ]) except Exception as err: raise Err('utils.build_cython_extension', [ 'py_or_pyx_file_path: <{}>'.format(py_or_pyx_file_path), ' Exception: <{}>'.format(err) ]) return cython_extension_module_path, cython_module_c_file_path, cython_build_dir_path
def _build_impl(self): dist = Distribution({ "script_name": None, "script_args": ["build_ext"] }) dist.ext_modules = cythonize([self.extension]) dist.include_dirs = [] dist.cmdclass = {'build_ext': custom_build_ext} build = dist.get_command_obj('build') # following the convention of cython's pyxbuild and naming # base directory "_pyxbld" build.build_base = join(self.CYMJ_DIR_PATH, 'generated', '_pyxbld_%s' % self.__class__.__name__) dist.parse_command_line() obj_build_ext = dist.get_command_obj("build_ext") dist.run_commands() built_so_file_path, = obj_build_ext.get_outputs() return built_so_file_path
def build(self): dist = Distribution({ "script_name": None, "script_args": ["build_ext"] }) dist.ext_modules = cythonize([self.extension]) dist.include_dirs = [] dist.cmdclass = {'build_ext': custom_build_ext} build = dist.get_command_obj('build') # following the convention of cython's pyxbuild and naming # base directory "_pyxbld" build.build_base = join(self.CYMJ_DIR_PATH, 'generated', '_pyxbld_%s' % self.__class__.__name__) dist.parse_command_line() obj_build_ext = dist.get_command_obj("build_ext") dist.run_commands() so_file_path, = obj_build_ext.get_outputs() return so_file_path
def compile_with_distutils(extension_name, src_filename, extra_objects = [], extra_compiler_flags = [], extra_link_flags = [], print_commands = False): # copied largely from pyxbuild from distutils.dist import Distribution from distutils.extension import Extension compiler_flags = get_compiler_flags(extra_compiler_flags) # don't need -shared in the flags since the default CC on Mac OS # might specify -bundle instead and the two are mutually exclusive linker_flags = get_linker_flags(extra_link_flags, shared=False) ext = Extension(name=extension_name, sources=[src_filename], include_dirs = include_dirs, extra_objects=extra_objects, extra_compile_args=compiler_flags, extra_link_args=linker_flags) script_args = ['build_ext', '--quiet'] setup_args = {"script_name": None, "script_args": script_args, } dist = Distribution(setup_args) if not dist.ext_modules: dist.ext_modules = [] dist.ext_modules.append(ext) # I have no idea how distutils works or why I have to do any of this config_files = dist.find_config_files() try: config_files.remove('setup.cfg') except ValueError: pass dist.parse_config_files(config_files) dist.parse_command_line() obj_build_ext = dist.get_command_obj("build_ext") dist.run_commands() shared_name = obj_build_ext.get_outputs()[0] return shared_name
def c_to_dll(filename, ext = None, force_rebuild = 0, build_in_temp=False, cbuild_dir=None, setup_args={}, reload_support=False, inplace=False): """Compile a C file to a DLL and return the name of the generated .so or .dll .""" assert os.path.exists(filename), "Could not find %s" % os.path.abspath(filename) path, name = os.path.split(os.path.abspath(filename)) if not ext: modname, extension = os.path.splitext(name) assert extension in (".c", ".py"), extension if not HAS_CYTHON: filename = filename[:-len(extension)] + '.c' ext = Extension(name=modname, sources=[filename]) if not cbuild_dir: cbuild_dir = os.path.join(path, "_cbld") package_base_dir = path for package_name in ext.name.split('.')[-2::-1]: package_base_dir, pname = os.path.split(package_base_dir) if pname != package_name: # something is wrong - package path doesn't match file path package_base_dir = None break script_args=setup_args.get("script_args",[]) if DEBUG or "--verbose" in script_args: quiet = "--verbose" else: quiet = "--quiet" args = [quiet, "build_ext"] if force_rebuild: args.append("--force") if inplace and package_base_dir: args.extend(['--build-lib', package_base_dir]) if ext.name == '__init__' or ext.name.endswith('.__init__'): # package => provide __path__ early if not hasattr(ext, 'cython_directives'): ext.cython_directives = {'set_initial_path' : 'SOURCEFILE'} elif 'set_initial_path' not in ext.cython_directives: ext.cython_directives['set_initial_path'] = 'SOURCEFILE' if HAS_CYTHON and build_in_temp: args.append("--pyrex-c-in-temp") sargs = setup_args.copy() sargs.update({ "script_name": None, "script_args": args + script_args, }) # late import, in case setuptools replaced it from distutils.dist import Distribution dist = Distribution(sargs) if not dist.ext_modules: dist.ext_modules = [] dist.ext_modules.append(ext) if HAS_CYTHON: dist.cmdclass = {'build_ext': build_ext} build = dist.get_command_obj('build') build.build_base = cbuild_dir cfgfiles = dist.find_config_files() dist.parse_config_files(cfgfiles) try: ok = dist.parse_command_line() except DistutilsArgError: raise if DEBUG: print("options (after parsing command line):") dist.dump_option_dicts() assert ok try: obj_build_ext = dist.get_command_obj("build_ext") dist.run_commands() so_path = obj_build_ext.get_outputs()[0] if obj_build_ext.inplace: # Python distutils get_outputs()[ returns a wrong so_path # when --inplace ; see http://bugs.python.org/issue5977 # workaround: so_path = os.path.join(os.path.dirname(filename), os.path.basename(so_path)) if reload_support: org_path = so_path timestamp = os.path.getmtime(org_path) global _reloads last_timestamp, last_path, count = _reloads.get(org_path, (None,None,0) ) if last_timestamp == timestamp: so_path = last_path else: basename = os.path.basename(org_path) while count < 100: count += 1 r_path = os.path.join(obj_build_ext.build_lib, basename + '.reload%s'%count) try: import shutil # late import / reload_support is: debugging try: # Try to unlink first --- if the .so file # is mmapped by another process, # overwriting its contents corrupts the # loaded image (on Linux) and crashes the # other process. On Windows, unlinking an # open file just fails. if os.path.isfile(r_path): os.unlink(r_path) except OSError: continue shutil.copy2(org_path, r_path) so_path = r_path except IOError: continue break else: # used up all 100 slots raise ImportError("reload count for %s reached maximum"%org_path) _reloads[org_path]=(timestamp, so_path, count) return so_path except KeyboardInterrupt: sys.exit(1) except (IOError, os.error): exc = sys.exc_info()[1] error = grok_environment_error(exc) if DEBUG: sys.stderr.write(error + "\n") raise
def cython(filename, verbose=0, compile_message=False, use_cache=False, create_local_c_file=False, annotate=True, sage_namespace=True, create_local_so_file=False): r""" Compile a Cython file. This converts a Cython file to a C (or C++ file), and then compiles that. The .c file and the .so file are created in a temporary directory. INPUT: - ``filename`` -- the name of the file to be compiled. Should end with 'pyx'. - ``verbose`` (integer, default 0) -- level of verbosity. A negative value ensures complete silence. - ``compile_message`` (bool, default False) -- if True, print ``'Compiling <filename>...'`` to the standard error. - ``use_cache`` (bool, default False) -- if True, check the temporary build directory to see if there is already a corresponding .so file. If so, and if the .so file is newer than the Cython file, don't recompile, just reuse the .so file. - ``create_local_c_file`` (bool, default False) -- if True, save a copy of the ``.c`` or ``.cpp`` file in the current directory. - ``annotate`` (bool, default True) -- if True, create an html file which annotates the conversion from .pyx to .c. By default this is only created in the temporary directory, but if ``create_local_c_file`` is also True, then save a copy of the .html file in the current directory. - ``sage_namespace`` (bool, default True) -- if True, import ``sage.all``. - ``create_local_so_file`` (bool, default False) -- if True, save a copy of the compiled .so file in the current directory. OUTPUT: a tuple ``(name, dir)`` where ``name`` is the name of the compiled module and ``dir`` is the directory containing the generated files. TESTS: Before :trac:`12975`, it would have been needed to write ``#clang c++``, but upper case ``C++`` has resulted in an error. Using pkgconfig to find the libraries, headers and macros. This is a work around while waiting for :trac:`22461` which will offer a better solution:: sage: code = [ ....: "#clang C++", ....: "from sage.rings.polynomial.multi_polynomial_libsingular cimport MPolynomial_libsingular", ....: "from sage.libs.singular.polynomial cimport singular_polynomial_pow", ....: "def test(MPolynomial_libsingular p):", ....: " singular_polynomial_pow(&p._poly, p._poly, 2, p._parent_ring)"] sage: cython(os.linesep.join(code)) The function ``test`` now manipulates internal C data of polynomials, squaring them:: sage: P.<x,y>=QQ[] sage: test(x) sage: x x^2 Check that compiling C++ code works:: sage: cython("# distutils: language = c++\n"+ ....: "from libcpp.vector cimport vector\n" ....: "cdef vector[int] * v = new vector[int](4)\n") Check that compiling C++ code works when creating a local C file, first moving to a tempdir to avoid clutter. Before :trac:`22113`, the create_local_c_file argument was not tested for C++ code:: sage: import sage.misc.cython sage: d = sage.misc.temporary_file.tmp_dir() sage: os.chdir(d) sage: with open("test.pyx", 'w') as f: ....: _ = f.write("# distutils: language = c++\n" ....: "from libcpp.vector cimport vector\n" ....: "cdef vector[int] * v = new vector[int](4)\n") sage: output = sage.misc.cython.cython("test.pyx", create_local_c_file=True) Accessing a ``.pxd`` file from the current directory works:: sage: import sage.misc.cython sage: d = sage.misc.temporary_file.tmp_dir() sage: os.chdir(d) sage: with open("helper.pxd", 'w') as f: ....: f.write("cdef inline int the_answer(): return 42") sage: cython(''' ....: from helper cimport the_answer ....: print(the_answer()) ....: ''') 42 Warning and error messages generated by Cython are properly handled. Warnings are only shown if verbose >= 0:: sage: code = ''' ....: def test_unreachable(): ....: raise Exception ....: return 42 ....: ''' sage: cython(code, verbose=-1) sage: cython(code, verbose=0) warning: ...:4:4: Unreachable code sage: cython("foo = bar\n") Traceback (most recent call last): ... RuntimeError: Error compiling Cython file: ------------------------------------------------------------ ... foo = bar ^ ------------------------------------------------------------ <BLANKLINE> ...:1:6: undeclared name not builtin: bar sage: cython("cdef extern from 'no_such_header_file': pass") Traceback (most recent call last): ... RuntimeError: ... Sage used to automatically include various ``.pxi`` files. Since :trac:`22805`, we no longer do this. But we make sure to give a useful message in case the ``.pxi`` files were needed:: sage: cython("sig_malloc(0)\n") Traceback (most recent call last): ... RuntimeError: Error compiling Cython file: ------------------------------------------------------------ ... sig_malloc(0) ^ ------------------------------------------------------------ <BLANKLINE> ...:1:0: undeclared name not builtin: sig_malloc <BLANKLINE> NOTE: Sage no longer automatically includes the deprecated files "cdefs.pxi", "signals.pxi" and "stdsage.pxi" in Cython files. You can fix your code by adding "from cysignals.memory cimport sig_malloc". """ if not filename.endswith('pyx'): print("Warning: file (={}) should have extension .pyx".format(filename), file=sys.stderr) # base is the name of the .so module that we create. If we are # creating a local shared object file, we use a more natural # naming convention. If we are not creating a local shared object # file, the main constraint is that it is unique and determined by # the file that we're running Cython on, so that in some cases we # can cache the result (e.g., recompiling the same pyx file during # the same session). if create_local_so_file: base, ext = os.path.splitext(os.path.basename(filename)) else: base = os.path.abspath(filename) base = sanitize(base) # This is the *temporary* directory where we store the pyx file. # This is deleted when Sage exits, which means pyx files must be # rebuilt every time Sage is restarted at present. target_dir = os.path.join(SPYX_TMP, base) # Build directory for Cython/distutils build_dir = os.path.join(target_dir, "build") if os.path.exists(target_dir): # There is already a module here. Maybe we do not have to rebuild? # Find the name. if use_cache: from sage.misc.sageinspect import loadable_module_extension prev_so = [F for F in os.listdir(target_dir) if F.endswith(loadable_module_extension())] if len(prev_so) > 0: prev_so = prev_so[0] # should have length 1 because of deletes below if os.path.getmtime(filename) <= os.path.getmtime('%s/%s'%(target_dir, prev_so)): # We do not have to rebuild. return prev_so[:-len(loadable_module_extension())], target_dir # Delete all ordinary files in target_dir for F in os.listdir(target_dir): G = os.path.join(target_dir, F) if os.path.isdir(G): continue try: os.unlink(G) except OSError: pass else: sage_makedirs(target_dir) if create_local_so_file: name = base else: global sequence_number if base not in sequence_number: sequence_number[base] = 0 name = '%s_%s'%(base, sequence_number[base]) # increment the sequence number so will use a different one next time. sequence_number[base] += 1 if compile_message: print("Compiling {}...".format(filename), file=sys.stderr) sys.stderr.flush() with open(filename) as f: (preparsed, libs, includes, language, additional_source_files, extra_args, libdirs) = _pyx_preparse(f.read()) # New filename with preparsed code. # NOTE: if we ever stop preparsing, we should still copy the # original file to the target directory. pyxfile = os.path.join(target_dir, name + ".pyx") with open(pyxfile, 'w') as f: f.write(preparsed) extra_sources = [] for fname in additional_source_files: fname = fname.replace("$SAGE_SRC", SAGE_SRC) fname = fname.replace("$SAGE_LOCAL", SAGE_LOCAL) extra_sources.append(fname) # Add current working directory to includes. This is needed because # we cythonize from a different directory. See Trac #24764. includes.insert(0, os.getcwd()) # Now do the actual build, directly calling Cython and distutils from Cython.Build import cythonize from Cython.Compiler.Errors import CompileError import Cython.Compiler.Options from distutils.dist import Distribution from distutils.core import Extension from distutils.log import set_verbosity set_verbosity(verbose) Cython.Compiler.Options.annotate = annotate Cython.Compiler.Options.embed_pos_in_docstring = True Cython.Compiler.Options.pre_import = "sage.all" if sage_namespace else None ext = Extension(name, sources=[pyxfile] + extra_sources, libraries=libs, library_dirs=[os.path.join(SAGE_LOCAL, "lib")] + libdirs, extra_compile_args=extra_args, language=language) try: # Change directories to target_dir so that Cython produces the correct # relative path; https://trac.sagemath.org/ticket/24097 with restore_cwd(target_dir): try: ext, = cythonize([ext], aliases=cython_aliases(), include_path=includes, quiet=(verbose <= 0), errors_to_stderr=False, use_listing_file=True) finally: # Read the "listing file" which is the file containing # warning and error messages generated by Cython. try: with open(name + ".lis") as f: cython_messages = f.read() except IOError: cython_messages = "Error compiling Cython file" except CompileError: # Check for names in old_pxi_names for pxd, names in old_pxi_names.items(): for name in names: if re.search(r"\b{}\b".format(name), cython_messages): cython_messages += dedent( """ NOTE: Sage no longer automatically includes the deprecated files "cdefs.pxi", "signals.pxi" and "stdsage.pxi" in Cython files. You can fix your code by adding "from {} cimport {}". """.format(pxd, name)) raise RuntimeError(cython_messages.strip()) if verbose >= 0: sys.stderr.write(cython_messages) sys.stderr.flush() if create_local_c_file: shutil.copy(os.path.join(target_dir, ext.sources[0]), os.curdir) if annotate: shutil.copy(os.path.join(target_dir, name + ".html"), os.curdir) # This emulates running "setup.py build" with the correct options dist = Distribution() dist.ext_modules = [ext] dist.include_dirs = includes buildcmd = dist.get_command_obj("build") buildcmd.build_base = build_dir buildcmd.build_lib = target_dir try: # Capture errors from distutils and its child processes with open(os.path.join(target_dir, name + ".err"), 'w+') as errfile: try: # Redirect stderr to errfile. We use the file descriptor # number "2" instead of "sys.stderr" because we really # want to redirect the messages from GCC. These are sent # to the actual stderr, regardless of what sys.stderr is. sys.stderr.flush() with redirection(2, errfile, close=False): dist.run_command("build") finally: errfile.seek(0) distutils_messages = errfile.read() except Exception as msg: msg = str(msg) + "\n" + distutils_messages raise RuntimeError(msg.strip()) if verbose >= 0: sys.stderr.write(distutils_messages) sys.stderr.flush() if create_local_so_file: # Copy module to current directory from sage.misc.sageinspect import loadable_module_extension shutil.copy(os.path.join(target_dir, name + loadable_module_extension()), os.curdir) return name, target_dir
def cython(filename, verbose=0, compile_message=False, use_cache=False, create_local_c_file=False, annotate=True, sage_namespace=True, create_local_so_file=False): r""" Compile a Cython file. This converts a Cython file to a C (or C++ file), and then compiles that. The .c file and the .so file are created in a temporary directory. INPUT: - ``filename`` -- the name of the file to be compiled. Should end with 'pyx'. - ``verbose`` (integer, default 0) -- level of verbosity. A negative value ensures complete silence. - ``compile_message`` (bool, default False) -- if True, print ``'Compiling <filename>...'`` to the standard error. - ``use_cache`` (bool, default False) -- if True, check the temporary build directory to see if there is already a corresponding .so file. If so, and if the .so file is newer than the Cython file, don't recompile, just reuse the .so file. - ``create_local_c_file`` (bool, default False) -- if True, save a copy of the ``.c`` or ``.cpp`` file in the current directory. - ``annotate`` (bool, default True) -- if True, create an html file which annotates the conversion from .pyx to .c. By default this is only created in the temporary directory, but if ``create_local_c_file`` is also True, then save a copy of the .html file in the current directory. - ``sage_namespace`` (bool, default True) -- if True, import ``sage.all``. - ``create_local_so_file`` (bool, default False) -- if True, save a copy of the compiled .so file in the current directory. OUTPUT: a tuple ``(name, dir)`` where ``name`` is the name of the compiled module and ``dir`` is the directory containing the generated files. TESTS: Before :trac:`12975`, it would have been needed to write ``#clang c++``, but upper case ``C++`` has resulted in an error. Using pkgconfig to find the libraries, headers and macros. This is a work around while waiting for :trac:`22461` which will offer a better solution:: sage: code = [ ....: "#clang C++", ....: "from sage.rings.polynomial.multi_polynomial_libsingular cimport MPolynomial_libsingular", ....: "from sage.libs.singular.polynomial cimport singular_polynomial_pow", ....: "def test(MPolynomial_libsingular p):", ....: " singular_polynomial_pow(&p._poly, p._poly, 2, p._parent_ring)"] sage: cython(os.linesep.join(code)) The function ``test`` now manipulates internal C data of polynomials, squaring them:: sage: P.<x,y>=QQ[] sage: test(x) sage: x x^2 Check that compiling C++ code works:: sage: cython("# distutils: language = c++\n"+ ....: "from libcpp.vector cimport vector\n" ....: "cdef vector[int] * v = new vector[int](4)\n") Check that compiling C++ code works when creating a local C file, first moving to a tempdir to avoid clutter. Before :trac:`22113`, the create_local_c_file argument was not tested for C++ code:: sage: d = sage.misc.temporary_file.tmp_dir() sage: os.chdir(d) sage: with open("test.pyx", 'w') as f: ....: _ = f.write("# distutils: language = c++\n" ....: "from libcpp.vector cimport vector\n" ....: "cdef vector[int] * v = new vector[int](4)\n") sage: output = sage.misc.cython.cython("test.pyx", create_local_c_file=True) Accessing a ``.pxd`` file from the current directory works:: sage: d = sage.misc.temporary_file.tmp_dir() sage: os.chdir(d) sage: with open("helper.pxd", 'w') as f: ....: _ = f.write("cdef inline int the_answer(): return 42") sage: cython(''' ....: from helper cimport the_answer ....: print(the_answer()) ....: ''') 42 Warning and error messages generated by Cython are properly handled. Warnings are only shown if verbose >= 0:: sage: code = ''' ....: def test_unreachable(): ....: raise Exception ....: return 42 ....: ''' sage: cython(code, verbose=-1) sage: cython(code, verbose=0) warning: ...:4:4: Unreachable code sage: cython("foo = bar\n") Traceback (most recent call last): ... RuntimeError: Error compiling Cython file: ------------------------------------------------------------ ... foo = bar ^ ------------------------------------------------------------ <BLANKLINE> ...:1:6: undeclared name not builtin: bar sage: cython("cdef extern from 'no_such_header_file': pass") Traceback (most recent call last): ... RuntimeError: ... """ if not filename.endswith('pyx'): print( "Warning: file (={}) should have extension .pyx".format(filename), file=sys.stderr) # base is the name of the .so module that we create. If we are # creating a local shared object file, we use a more natural # naming convention. If we are not creating a local shared object # file, the main constraint is that it is unique and determined by # the file that we're running Cython on, so that in some cases we # can cache the result (e.g., recompiling the same pyx file during # the same session). if create_local_so_file: base, ext = os.path.splitext(os.path.basename(filename)) else: base = os.path.abspath(filename) base = sanitize(base) # This is the *temporary* directory where we store the pyx file. # This is deleted when Sage exits, which means pyx files must be # rebuilt every time Sage is restarted at present. target_dir = os.path.join(SPYX_TMP, base) # Build directory for Cython/distutils build_dir = os.path.join(target_dir, "build") if os.path.exists(target_dir): # There is already a module here. Maybe we do not have to rebuild? # Find the name. if use_cache: from sage.misc.sageinspect import loadable_module_extension prev_so = [ F for F in os.listdir(target_dir) if F.endswith(loadable_module_extension()) ] if len(prev_so) > 0: prev_so = prev_so[ 0] # should have length 1 because of deletes below if os.path.getmtime(filename) <= os.path.getmtime( '%s/%s' % (target_dir, prev_so)): # We do not have to rebuild. return prev_so[:-len(loadable_module_extension() )], target_dir # Delete all ordinary files in target_dir for F in os.listdir(target_dir): G = os.path.join(target_dir, F) if os.path.isdir(G): continue try: os.unlink(G) except OSError: pass else: sage_makedirs(target_dir) if create_local_so_file: name = base else: global sequence_number if base not in sequence_number: sequence_number[base] = 0 name = '%s_%s' % (base, sequence_number[base]) # increment the sequence number so will use a different one next time. sequence_number[base] += 1 if compile_message: sys.stderr.write("Compiling {}...\n".format(filename)) sys.stderr.flush() # Copy original file to the target directory. pyxfile = os.path.join(target_dir, name + ".pyx") shutil.copy(filename, pyxfile) # Add current working directory to includes. This is needed because # we cythonize from a different directory. See Trac #24764. includes = [os.getcwd()] + sage_include_directories() # Now do the actual build, directly calling Cython and distutils from Cython.Build import cythonize from Cython.Compiler.Errors import CompileError import Cython.Compiler.Options from distutils.dist import Distribution from distutils.core import Extension from distutils.log import set_verbosity set_verbosity(verbose) Cython.Compiler.Options.annotate = annotate Cython.Compiler.Options.embed_pos_in_docstring = True Cython.Compiler.Options.pre_import = "sage.all" if sage_namespace else None ext = Extension( name, sources=[pyxfile], extra_compile_args=["-w"], # no warnings libraries=standard_libs, library_dirs=standard_libdirs) directives = dict(language_level=sys.version_info[0]) try: # Change directories to target_dir so that Cython produces the correct # relative path; https://trac.sagemath.org/ticket/24097 with restore_cwd(target_dir): try: ext, = cythonize([ext], aliases=cython_aliases(), include_path=includes, compiler_directives=directives, quiet=(verbose <= 0), errors_to_stderr=False, use_listing_file=True) finally: # Read the "listing file" which is the file containing # warning and error messages generated by Cython. try: with open(name + ".lis") as f: cython_messages = f.read() except IOError: cython_messages = "Error compiling Cython file" except CompileError: raise RuntimeError(cython_messages.strip()) if verbose >= 0: sys.stderr.write(cython_messages) sys.stderr.flush() if create_local_c_file: shutil.copy(os.path.join(target_dir, ext.sources[0]), os.curdir) if annotate: shutil.copy(os.path.join(target_dir, name + ".html"), os.curdir) # This emulates running "setup.py build" with the correct options dist = Distribution() dist.ext_modules = [ext] dist.include_dirs = includes buildcmd = dist.get_command_obj("build") buildcmd.build_base = build_dir buildcmd.build_lib = target_dir try: # Capture errors from distutils and its child processes with open(os.path.join(target_dir, name + ".err"), 'w+') as errfile: try: # Redirect stderr to errfile. We use the file descriptor # number "2" instead of "sys.stderr" because we really # want to redirect the messages from GCC. These are sent # to the actual stderr, regardless of what sys.stderr is. sys.stderr.flush() with redirection(2, errfile, close=False): dist.run_command("build") finally: errfile.seek(0) distutils_messages = errfile.read() except Exception as msg: msg = str(msg) + "\n" + distutils_messages raise RuntimeError(msg.strip()) if verbose >= 0: sys.stderr.write(distutils_messages) sys.stderr.flush() if create_local_so_file: # Copy module to current directory from sage.misc.sageinspect import loadable_module_extension shutil.copy( os.path.join(target_dir, name + loadable_module_extension()), os.curdir) return name, target_dir
def pyx_to_dll(filename, ext=None, force_rebuild=0, build_in_temp=False, pyxbuild_dir=None, setup_args={}, reload_support=False, inplace=False): """Compile a PYX file to a DLL and return the name of the generated .so or .dll .""" assert os.path.exists( filename), "Could not find %s" % os.path.abspath(filename) path, name = os.path.split(os.path.abspath(filename)) if not ext: modname, extension = os.path.splitext(name) assert extension in (".pyx", ".py"), extension if not HAS_CYTHON: filename = filename[:-len(extension)] + '.c' ext = Extension(name=modname, sources=[filename]) if not pyxbuild_dir: pyxbuild_dir = os.path.join(path, "_pyxbld") package_base_dir = path for package_name in ext.name.split('.')[-2::-1]: package_base_dir, pname = os.path.split(package_base_dir) if pname != package_name: # something is wrong - package path doesn't match file path package_base_dir = None break script_args = setup_args.get("script_args", []) if DEBUG or "--verbose" in script_args: quiet = "--verbose" else: quiet = "--quiet" args = [quiet, "build_ext"] if force_rebuild: args.append("--force") if inplace and package_base_dir: args.extend(['--build-lib', package_base_dir]) if ext.name == '__init__' or ext.name.endswith('.__init__'): # package => provide __path__ early if not hasattr(ext, 'cython_directives'): ext.cython_directives = {'set_initial_path': 'SOURCEFILE'} elif 'set_initial_path' not in ext.cython_directives: ext.cython_directives['set_initial_path'] = 'SOURCEFILE' if HAS_CYTHON and build_in_temp: args.append("--pyrex-c-in-temp") sargs = setup_args.copy() sargs.update({"script_name": None, "script_args": args + script_args}) dist = Distribution(sargs) if not dist.ext_modules: dist.ext_modules = [] dist.ext_modules.append(ext) if HAS_CYTHON: dist.cmdclass = {'build_ext': build_ext} build = dist.get_command_obj('build') build.build_base = pyxbuild_dir cfgfiles = dist.find_config_files() dist.parse_config_files(cfgfiles) try: ok = dist.parse_command_line() except DistutilsArgError: raise if DEBUG: print("options (after parsing command line):") dist.dump_option_dicts() assert ok try: obj_build_ext = dist.get_command_obj("build_ext") dist.run_commands() so_path = obj_build_ext.get_outputs()[0] if obj_build_ext.inplace: # Python distutils get_outputs()[ returns a wrong so_path # when --inplace ; see http://bugs.python.org/issue5977 # workaround: so_path = os.path.join(os.path.dirname(filename), os.path.basename(so_path)) if reload_support: org_path = so_path timestamp = os.path.getmtime(org_path) global _reloads last_timestamp, last_path, count = _reloads.get( org_path, (None, None, 0)) if last_timestamp == timestamp: so_path = last_path else: basename = os.path.basename(org_path) while count < 100: count += 1 r_path = os.path.join(obj_build_ext.build_lib, basename + '.reload%s' % count) try: import shutil # late import / reload_support is: debugging try: # Try to unlink first --- if the .so file # is mmapped by another process, # overwriting its contents corrupts the # loaded image (on Linux) and crashes the # other process. On Windows, unlinking an # open file just fails. if os.path.isfile(r_path): os.unlink(r_path) except OSError: continue shutil.copy2(org_path, r_path) so_path = r_path except IOError: continue break else: # used up all 100 slots raise ImportError("reload count for %s reached maximum" % org_path) _reloads[org_path] = (timestamp, so_path, count) return so_path except KeyboardInterrupt: sys.exit(1) except (IOError, os.error): exc = sys.exc_info()[1] error = grok_environment_error(exc) if DEBUG: sys.stderr.write(error + "\n") raise
def pyx_to_dll( filename, ext=None, force_rebuild=0, build_in_temp=False, pyxbuild_dir=None, setup_args={}, reload_support=False ): """Compile a PYX file to a DLL and return the name of the generated .so or .dll .""" assert os.path.exists(filename), "Could not find %s" % os.path.abspath(filename) path, name = os.path.split(filename) if not ext: modname, extension = os.path.splitext(name) assert extension in (".pyx", ".py"), extension if not HAS_CYTHON: filename = filename[: -len(extension)] + ".c" ext = Extension(name=modname, sources=[filename]) if not pyxbuild_dir: pyxbuild_dir = os.path.join(path, "_pyxbld") script_args = setup_args.get("script_args", []) if DEBUG or "--verbose" in script_args: quiet = "--verbose" else: quiet = "--quiet" args = [quiet, "build_ext"] if force_rebuild: args.append("--force") if HAS_CYTHON and build_in_temp: args.append("--pyrex-c-in-temp") sargs = setup_args.copy() sargs.update({"script_name": None, "script_args": args + script_args}) dist = Distribution(sargs) if not dist.ext_modules: dist.ext_modules = [] dist.ext_modules.append(ext) if HAS_CYTHON: dist.cmdclass = {"build_ext": build_ext} build = dist.get_command_obj("build") build.build_base = pyxbuild_dir config_files = dist.find_config_files() try: config_files.remove("setup.cfg") except ValueError: pass dist.parse_config_files(config_files) cfgfiles = dist.find_config_files() try: cfgfiles.remove("setup.cfg") except ValueError: pass dist.parse_config_files(cfgfiles) try: ok = dist.parse_command_line() except DistutilsArgError: raise if DEBUG: print("options (after parsing command line):") dist.dump_option_dicts() assert ok try: dist.run_commands() obj_build_ext = dist.get_command_obj("build_ext") so_path = obj_build_ext.get_outputs()[0] if obj_build_ext.inplace: # Python distutils get_outputs()[ returns a wrong so_path # when --inplace ; see http://bugs.python.org/issue5977 # workaround: so_path = os.path.join(os.path.dirname(filename), os.path.basename(so_path)) if reload_support: org_path = so_path timestamp = os.path.getmtime(org_path) global _reloads last_timestamp, last_path, count = _reloads.get(org_path, (None, None, 0)) if last_timestamp == timestamp: so_path = last_path else: basename = os.path.basename(org_path) while count < 100: count += 1 r_path = os.path.join(obj_build_ext.build_lib, basename + ".reload%s" % count) try: import shutil # late import / reload_support is: debugging shutil.copy2(org_path, r_path) so_path = r_path except IOError: continue break else: # used up all 100 slots raise ImportError("reload count for %s reached maximum" % org_path) _reloads[org_path] = (timestamp, so_path, count) return so_path except KeyboardInterrupt: sys.exit(1) except (IOError, os.error): exc = sys.exc_info()[1] error = grok_environment_error(exc) if DEBUG: sys.stderr.write(error + "\n") raise
def pyx_to_dll(filename, ext=None, force_rebuild=0, build_in_temp=False, pyxbuild_dir=None, setup_args={}, reload_support=False): """Compile a PYX file to a DLL and return the name of the generated .so or .dll .""" assert os.path.exists( filename), "Could not find %s" % os.path.abspath(filename) path, name = os.path.split(filename) if not ext: modname, extension = os.path.splitext(name) assert extension in (".pyx", ".py"), extension if not HAS_CYTHON: filename = filename[:-len(extension)] + '.c' ext = Extension(name=modname, sources=[filename]) if not pyxbuild_dir: pyxbuild_dir = os.path.join(path, "_pyxbld") script_args = setup_args.get("script_args", []) if DEBUG or "--verbose" in script_args: quiet = "--verbose" else: quiet = "--quiet" args = [quiet, "build_ext"] if force_rebuild: args.append("--force") if HAS_CYTHON and build_in_temp: args.append("--pyrex-c-in-temp") sargs = setup_args.copy() sargs.update({"script_name": None, "script_args": args + script_args}) dist = Distribution(sargs) if not dist.ext_modules: dist.ext_modules = [] dist.ext_modules.append(ext) if HAS_CYTHON: dist.cmdclass = {'build_ext': build_ext} build = dist.get_command_obj('build') build.build_base = pyxbuild_dir config_files = dist.find_config_files() try: config_files.remove('setup.cfg') except ValueError: pass dist.parse_config_files(config_files) cfgfiles = dist.find_config_files() try: cfgfiles.remove('setup.cfg') except ValueError: pass dist.parse_config_files(cfgfiles) try: ok = dist.parse_command_line() except DistutilsArgError: raise if DEBUG: print("options (after parsing command line):") dist.dump_option_dicts() assert ok try: dist.run_commands() obj_build_ext = dist.get_command_obj("build_ext") so_path = obj_build_ext.get_outputs()[0] if obj_build_ext.inplace: # Python distutils get_outputs()[ returns a wrong so_path # when --inplace ; see http://bugs.python.org/issue5977 # workaround: so_path = os.path.join(os.path.dirname(filename), os.path.basename(so_path)) if reload_support: org_path = so_path timestamp = os.path.getmtime(org_path) global _reloads last_timestamp, last_path, count = _reloads.get( org_path, (None, None, 0)) if last_timestamp == timestamp: so_path = last_path else: basename = os.path.basename(org_path) while count < 100: count += 1 r_path = os.path.join(obj_build_ext.build_lib, basename + '.reload%s' % count) try: import shutil # late import / reload_support is: debugging shutil.copy2(org_path, r_path) so_path = r_path except IOError: continue break else: # used up all 100 slots raise ImportError("reload count for %s reached maximum" % org_path) _reloads[org_path] = (timestamp, so_path, count) return so_path except KeyboardInterrupt: sys.exit(1) except (IOError, os.error): exc = sys.exc_info()[1] error = grok_environment_error(exc) if DEBUG: sys.stderr.write(error + "\n") raise
def cython(filename, verbose=0, compile_message=False, use_cache=False, create_local_c_file=False, annotate=True, sage_namespace=True, create_local_so_file=False): r""" Compile a Cython file. This converts a Cython file to a C (or C++ file), and then compiles that. The .c file and the .so file are created in a temporary directory. INPUT: - ``filename`` -- the name of the file to be compiled. Should end with 'pyx'. - ``verbose`` (integer, default 0) -- level of verbosity. - ``compile_message`` (bool, default False) -- if True, print ``'Compiling <filename>...'`` to the standard error. - ``use_cache`` (bool, default False) -- if True, check the temporary build directory to see if there is already a corresponding .so file. If so, and if the .so file is newer than the Cython file, don't recompile, just reuse the .so file. - ``create_local_c_file`` (bool, default False) -- if True, save a copy of the ``.c`` or ``.cpp`` file in the current directory. - ``annotate`` (bool, default True) -- if True, create an html file which annotates the conversion from .pyx to .c. By default this is only created in the temporary directory, but if ``create_local_c_file`` is also True, then save a copy of the .html file in the current directory. - ``sage_namespace`` (bool, default True) -- if True, import ``sage.all``. - ``create_local_so_file`` (bool, default False) -- if True, save a copy of the compiled .so file in the current directory. TESTS: Before :trac:`12975`, it would have been needed to write ``#clang c++``, but upper case ``C++`` has resulted in an error. Using pkgconfig to find the libraries, headers and macros. This is a work around while waiting for :trac:`22461` which will offer a better solution:: sage: code = [ ....: "#clang C++", ....: "from sage.rings.polynomial.multi_polynomial_libsingular cimport MPolynomial_libsingular", ....: "from sage.libs.singular.polynomial cimport singular_polynomial_pow", ....: "def test(MPolynomial_libsingular p):", ....: " singular_polynomial_pow(&p._poly, p._poly, 2, p._parent_ring)"] sage: cython(os.linesep.join(code)) The function ``test`` now manipulates internal C data of polynomials, squaring them:: sage: P.<x,y>=QQ[] sage: test(x) sage: x x^2 Check that compiling C++ code works:: sage: cython("# distutils: language = c++\n"+ ....: "from libcpp.vector cimport vector\n" ....: "cdef vector[int] * v = new vector[int](4)\n") Check that compiling C++ code works when creating a local C file, first moving to a tempdir to avoid clutter. Before :trac:`22113`, the create_local_c_file argument was not tested for C++ code:: sage: import sage.misc.cython sage: d = sage.misc.temporary_file.tmp_dir() sage: os.chdir(d) sage: with open("test.pyx", 'w') as f: ....: _ = f.write("# distutils: language = c++\n" ....: "from libcpp.vector cimport vector\n" ....: "cdef vector[int] * v = new vector[int](4)\n") sage: output = sage.misc.cython.cython("test.pyx", create_local_c_file=True) Sage used to automatically include various ``.pxi`` files. Since :trac:`22805`, we no longer do this. But we make sure to give a useful message in case the ``.pxi`` files were needed:: sage: cython("sig_malloc(0)") Traceback (most recent call last): ... RuntimeError: Error converting ... to C NOTE: Sage no longer automatically includes the deprecated files "cdefs.pxi", "signals.pxi" and "stdsage.pxi" in Cython files. You can fix your code by adding "from cysignals.memory cimport sig_malloc". """ if not filename.endswith('pyx'): print( "Warning: file (={}) should have extension .pyx".format(filename), file=sys.stderr) # base is the name of the .so module that we create. If we are # creating a local shared object file, we use a more natural # naming convention. If we are not creating a local shared object # file, the main constraint is that it is unique and determined by # the file that we're running Cython on, so that in some cases we # can cache the result (e.g., recompiling the same pyx file during # the same session). if create_local_so_file: base, ext = os.path.splitext(os.path.basename(filename)) base = sanitize(base) else: base = sanitize(os.path.abspath(filename)) # This is the *temporary* directory where we store the pyx file. # This is deleted when Sage exits, which means pyx files must be # rebuilt every time Sage is restarted at present. target_dir = os.path.join(SPYX_TMP, base) # Build directory for Cython/distutils build_dir = os.path.join(target_dir, "build") if os.path.exists(target_dir): # There is already a module here. Maybe we do not have to rebuild? # Find the name. if use_cache: from sage.misc.sageinspect import loadable_module_extension prev_so = [ F for F in os.listdir(target_dir) if F.endswith(loadable_module_extension()) ] if len(prev_so) > 0: prev_so = prev_so[ 0] # should have length 1 because of deletes below if os.path.getmtime(filename) <= os.path.getmtime( '%s/%s' % (target_dir, prev_so)): # We do not have to rebuild. return prev_so[:-len(loadable_module_extension() )], target_dir # Delete all ordinary files in target_dir for F in os.listdir(target_dir): G = os.path.join(target_dir, F) if os.path.isdir(G): continue try: os.unlink(G) except OSError: pass else: sage_makedirs(target_dir) if create_local_so_file: name = base else: global sequence_number if base not in sequence_number: sequence_number[base] = 0 name = '%s_%s' % (base, sequence_number[base]) # increment the sequence number so will use a different one next time. sequence_number[base] += 1 if compile_message: print("Compiling {}...".format(filename), file=sys.stderr) with open(filename) as f: (preparsed, libs, includes, language, additional_source_files, extra_args, libdirs) = _pyx_preparse(f.read()) # New filename with preparsed code. # NOTE: if we ever stop preparsing, we should still copy the # original file to the target directory. pyxfile = os.path.join(target_dir, name + ".pyx") with open(pyxfile, 'w') as f: f.write(preparsed) extra_sources = [] for fname in additional_source_files: fname = fname.replace("$SAGE_SRC", SAGE_SRC) fname = fname.replace("$SAGE_LOCAL", SAGE_LOCAL) extra_sources.append(fname) # Now do the actual build, directly calling Cython and distutils from Cython.Build import cythonize from Cython.Compiler.Errors import CompileError import Cython.Compiler.Options from distutils.dist import Distribution from distutils.core import Extension from distutils.log import set_verbosity set_verbosity(verbose) Cython.Compiler.Options.annotate = annotate Cython.Compiler.Options.embed_pos_in_docstring = True Cython.Compiler.Options.pre_import = "sage.all" if sage_namespace else None ext = Extension(name, sources=[pyxfile] + extra_sources, libraries=libs, library_dirs=[os.path.join(SAGE_LOCAL, "lib")] + libdirs, extra_compile_args=extra_args, language=language) orig_cwd = os.getcwd() try: # Change directories to target_dir so that Cython produces the correct # relative path; https://trac.sagemath.org/ticket/24097 os.chdir(target_dir) ext, = cythonize([ext], aliases=cython_aliases(), include_path=includes, quiet=not verbose) except CompileError: # Check for names in old_pxi_names note = '' for pxd, names in old_pxi_names.items(): for name in names: if re.search(r"\b{}\b".format(name), preparsed): note += dedent(""" NOTE: Sage no longer automatically includes the deprecated files "cdefs.pxi", "signals.pxi" and "stdsage.pxi" in Cython files. You can fix your code by adding "from {} cimport {}". """.format(pxd, name)) raise RuntimeError("Error converting {} to C".format(filename) + note) finally: os.chdir(orig_cwd) if create_local_c_file: shutil.copy(os.path.join(target_dir, ext.sources[0]), os.curdir) if annotate: shutil.copy(os.path.join(target_dir, name + ".html"), os.curdir) # This emulates running "setup.py build" with the correct options dist = Distribution() dist.ext_modules = [ext] dist.include_dirs = includes buildcmd = dist.get_command_obj("build") buildcmd.build_base = build_dir buildcmd.build_lib = target_dir dist.run_command("build") if create_local_so_file: # Copy module to current directory from sage.misc.sageinspect import loadable_module_extension shutil.copy( os.path.join(target_dir, name + loadable_module_extension()), os.curdir) return name, target_dir