Create and include new file into python package - python

I want to use version for my package base on git describe command. For this, I created setup.py with function get_version(). This function retrieves version from .version file if it exists, else computes new package version and writes it to a new .version file. However, when I call python setup.py sdist, .version is not copying inside .tar archive. This causes error when I'm trying to install package from PyPi repo. How to properly include my .version file "on the fly" into package?
setup.py:
import pathlib
from subprocess import check_output
from setuptools import find_packages, setup
_VERSION_FILE = pathlib.Path(".version") # Add it to .gitignore!
_GIT_COMMAND = "git describe --tags --long --dirty"
_VERSION_FORMAT = "{tag}.dev{commit_count}+{commit_hash}"
def get_version() -> str:
""" Return version from git, write commit to file
"""
if _VERSION_FILE.is_file():
with _VERSION_FILE.open() as f:
return f.readline().strip()
output = check_output(_GIT_COMMAND.split()).decode("utf-8").strip().split("-")
tag, count, commit = output[:3]
dirty = len(output) == 4
if count == "0" and not dirty:
return tag
version = _VERSION_FORMAT.format(tag=tag, commit_count=count, commit_hash=commit)
with _VERSION_FILE.open("w") as f:
print(version, file=f, end="")
return version
_version = get_version()
setup(
name="mypackage",
package_data={
"": [str(_VERSION_FILE)]
},
version=_version,
packages=find_packages(exclude=["tests"]),
)

If you include a file called MANIFEST.in in the same directory as setup.py with include .version inside, this should get the file picked up.

It was a mistake in setup.py. I forgot to add file dumping in if count == "0" and not dirty:. Now it works with MANIFEST.in.

Related

Why is the text not adding in to the .txt file using python?

I am working on the 'ToDo list' project in python using CLI. I am struggling at the initial step. I have created all the necessaries connecting CLI and python (followed https://trstringer.com/easy-and-nice-python-cli/).
I wanted to add some data to the todo.txt file (already created and added in the same folder).
I used the following code in add.py
def add():
note=input('Enter the todo you want to add: ')
file1=open('todo.txt','w+')
file1.write(note)
file1.close()
And following for main.py
import sys
from .add import *
def main():
args = sys.argv[1:]
if len(args)==0:
print('Enter the commands you needed here is help')
else:
for arg in args:
if arg == '--add':
add()
if __name__ == '__main__':
main()
Set file
from setuptools import setup
setup(
name = 'todo',
version = '0.1.0',
packages = ['todo'],
entry_points = {
'console_scripts': [
'todo = todo.__main__:main'
]
})
Added empty files of init.py. I think I did everything correctly but the input I took from the user is not adding to the todo.txt file. Please help me with this issue. Thank you in advance!!
You need to call the close function, like file1.close() to close the file properly. What you are doing is file1.close which does nothing.

Automate compilation of protobuf specs into python classes in setup.py

I have a python project that uses google protobufs as a message format for communicating over the network. Generating python files from the .proto files is straight-forward using the protoc program. How can I configure my setup.py file for the project so that it automatically calls the protoc command?
In a similar situation, I ended up with this code (setup.py, but written in a way to allow extraction into some external Python module for reuse). Note that I took the generate_proto function and several ideas from the setup.py file of the protobuf source distribution.
from __future__ import print_function
import os
import shutil
import subprocess
import sys
from distutils.command.build_py import build_py as _build_py
from distutils.command.clean import clean as _clean
from distutils.debug import DEBUG
from distutils.dist import Distribution
from distutils.spawn import find_executable
from nose.commands import nosetests as _nosetests
from setuptools import setup
PROTO_FILES = [
'goobuntu/proto/hoststatus.proto',
]
CLEANUP_SUFFIXES = [
# filepath suffixes of files to remove on "clean" subcommand
'_pb2.py',
'.pyc',
'.so',
'.o',
'dependency_links.txt',
'entry_points.txt',
'PKG-INFO',
'top_level.txt',
'SOURCES.txt',
'.coverage',
'protobuf/compiler/__init__.py',
]
CLEANUP_DIRECTORIES = [ # subdirectories to remove on "clean" subcommand
# 'build' # Note: the build subdirectory is removed if --all is set.
'html-coverage',
]
if 'PROTOC' in os.environ and os.path.exists(os.environ['PROTOC']):
protoc = os.environ['PROTOC']
else:
protoc = find_executable('protoc')
def generate_proto(source):
"""Invoke Protocol Compiler to generate python from given source .proto."""
if not os.path.exists(source):
sys.stderr.write('Can\'t find required file: %s\n' % source)
sys.exit(1)
output = source.replace('.proto', '_pb2.py')
if (not os.path.exists(output) or
(os.path.getmtime(source) > os.path.getmtime(output))):
if DEBUG:
print('Generating %s' % output)
if protoc is None:
sys.stderr.write(
'protoc not found. Is protobuf-compiler installed? \n'
'Alternatively, you can point the PROTOC environment variable at a '
'local version.')
sys.exit(1)
protoc_command = [protoc, '-I.', '--python_out=.', source]
if subprocess.call(protoc_command) != 0:
sys.exit(1)
class MyDistribution(Distribution):
# Helper class to add the ability to set a few extra arguments
# in setup():
# protofiles : Protocol buffer definitions that need compiling
# cleansuffixes : Filename suffixes (might be full names) to remove when
# "clean" is called
# cleandirectories : Directories to remove during cleanup
# Also, the class sets the clean, build_py, test and nosetests cmdclass
# options to defaults that compile protobufs, implement test as nosetests
# and enables the nosetests command as well as using our cleanup class.
def __init__(self, attrs=None):
self.protofiles = [] # default to no protobuf files
self.cleansuffixes = ['_pb2.py', '.pyc'] # default to clean generated files
self.cleandirectories = ['html-coverage'] # clean out coverage directory
cmdclass = attrs.get('cmdclass')
if not cmdclass:
cmdclass = {}
# These should actually modify attrs['cmdclass'], as we assigned the
# mutable dict to cmdclass without copying it.
if 'nosetests' not in cmdclass:
cmdclass['nosetests'] = MyNosetests
if 'test' not in cmdclass:
cmdclass['test'] = MyNosetests
if 'build_py' not in cmdclass:
cmdclass['build_py'] = MyBuildPy
if 'clean' not in cmdclass:
cmdclass['clean'] = MyClean
attrs['cmdclass'] = cmdclass
# call parent __init__ in old style class
Distribution.__init__(self, attrs)
class MyClean(_clean):
def run(self):
try:
cleandirectories = self.distribution.cleandirectories
except AttributeError:
sys.stderr.write(
'Error: cleandirectories not defined. MyDistribution not used?')
sys.exit(1)
try:
cleansuffixes = self.distribution.cleansuffixes
except AttributeError:
sys.stderr.write(
'Error: cleansuffixes not defined. MyDistribution not used?')
sys.exit(1)
# Remove build and html-coverage directories if they exist
for directory in cleandirectories:
if os.path.exists(directory):
if DEBUG:
print('Removing directory: "{}"'.format(directory))
shutil.rmtree(directory)
# Delete generated files in code tree.
for dirpath, _, filenames in os.walk('.'):
for filename in filenames:
filepath = os.path.join(dirpath, filename)
for i in cleansuffixes:
if filepath.endswith(i):
if DEBUG:
print('Removing file: "{}"'.format(filepath))
os.remove(filepath)
# _clean is an old-style class, so super() doesn't work
_clean.run(self)
class MyBuildPy(_build_py):
def run(self):
try:
protofiles = self.distribution.protofiles
except AttributeError:
sys.stderr.write(
'Error: protofiles not defined. MyDistribution not used?')
sys.exit(1)
for proto in protofiles:
generate_proto(proto)
# _build_py is an old-style class, so super() doesn't work
_build_py.run(self)
class MyNosetests(_nosetests):
def run(self):
try:
protofiles = self.distribution.protofiles
except AttributeError:
sys.stderr.write(
'Error: protofiles not defined. MyDistribution not used?')
for proto in protofiles:
generate_proto(proto)
# _nosetests is an old-style class, so super() doesn't work
_nosetests.run(self)
setup(
# MyDistribution automatically enables several extensions, including
# the compilation of protobuf files.
distclass=MyDistribution,
...
tests_require=['nose'],
protofiles=PROTO_FILES,
cleansuffixes=CLEANUP_SUFFIXES,
cleandirectories=CLEANUP_DIRECTORIES,
)
Here's the solution that I have used for setup.py. The only thing you need to keep in mind is the version of the protoc compiler is compatible with the installed protobuf version.
'''
# here you can specify the proto folder and
# output folder or input them as parameters
# to script
protoc_command = [
"python", "-m", "grpc_tools.protoc",
f"--proto_path={proto_folder}",
f"--python_out={output_folder}",
f"--grpc_python_out={output_folder}",
]
'''

py2exe/py2app and docx don't work together

Installed docx on Windows 7 here:
D:\Program Files (x86)\Python27\Lib\site-packages as shown below:
Installed docx on OS X at /Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/docx-0.0.2-py2.7.egg-info as shown below:
Following is the sample script (named as docx_example.py), which runs absolutely fine on the python interpreter:
#!/usr/bin/env python
'''
This file makes an docx (Office 2007) file from scratch, showing off most of python-docx's features.
If you need to make documents from scratch, use this file as a basis for your work.
Part of Python's docx module - http://github.com/mikemaccana/python-docx
See LICENSE for licensing information.
'''
from docx import *
if __name__ == '__main__':
# Default set of relationshipships - these are the minimum components of a document
relationships = relationshiplist()
# Make a new document tree - this is the main part of a Word document
document = newdocument()
# This xpath location is where most interesting content lives
docbody = document.xpath('/w:document/w:body', namespaces=nsprefixes)[0]
# Append two headings and a paragraph
docbody.append(heading('''Welcome to Python's docx module''',1) )
docbody.append(heading('Make and edit docx in 200 lines of pure Python',2))
docbody.append(paragraph('The module was created when I was looking for a Python support for MS Word .doc files on PyPI and Stackoverflow. Unfortunately, the only solutions I could find used:'))
# Add a numbered list
for point in ['''COM automation''','''.net or Java''','''Automating OpenOffice or MS Office''']:
docbody.append(paragraph(point,style='ListNumber'))
docbody.append(paragraph('''For those of us who prefer something simpler, I made docx.'''))
docbody.append(heading('Making documents',2))
docbody.append(paragraph('''The docx module has the following features:'''))
# Add some bullets
for point in ['Paragraphs','Bullets','Numbered lists','Multiple levels of headings','Tables','Document Properties']:
docbody.append(paragraph(point,style='ListBullet'))
docbody.append(paragraph('Tables are just lists of lists, like this:'))
# Append a table
docbody.append(table([['A1','A2','A3'],['B1','B2','B3'],['C1','C2','C3']]))
docbody.append(heading('Editing documents',2))
docbody.append(paragraph('Thanks to the awesomeness of the lxml module, we can:'))
for point in ['Search and replace','Extract plain text of document','Add and delete items anywhere within the document']:
docbody.append(paragraph(point,style='ListBullet'))
# Search and replace
print 'Searching for something in a paragraph ...',
if search(docbody, 'the awesomeness'): print 'found it!'
else: print 'nope.'
print 'Searching for something in a heading ...',
if search(docbody, '200 lines'): print 'found it!'
else: print 'nope.'
print 'Replacing ...',
docbody = replace(docbody,'the awesomeness','the goshdarned awesomeness')
print 'done.'
# Add a pagebreak
docbody.append(pagebreak(type='page', orient='portrait'))
docbody.append(heading('Ideas? Questions? Want to contribute?',2))
docbody.append(paragraph('''Email <python.docx#librelist.com>'''))
# Create our properties, contenttypes, and other support files
coreprops = coreproperties(title='Python docx demo',subject='A practical example of making docx from Python',creator='Mike MacCana',keywords=['python','Office Open XML','Word'])
appprops = appproperties()
contenttypes = contenttypes()
websettings = websettings()
wordrelationships = wordrelationships(relationships)
# Save our document
savedocx(document,coreprops,appprops,contenttypes,websettings,wordrelationships,'docx_example.docx')
Following is the setup script (named as docx_setup.py) to create the standalone (.app in Mac OSX and .exe in Windows 7):
import sys,os
# Globals: START
main_script='docx_example'
dist_dir_main_path=os.path.abspath('./docx-bin')
compression_level=2
optimization_level=2
bundle_parameter=1
skip_archive_parameter=False
emulation_parameter=False
module_cross_reference_parameter=False
ascii_parameter=False
includes_list=['lxml.etree','lxml._elementpath','gzip']
# Globals: STOP
# Global Functions: START
def isDarwin():
return sys.platform=='darwin'
def isLinux():
return sys.platform=='linux2'
def isWindows():
return os.name=='nt'
# Global Functions: STOP
if isDarwin():
from setuptools import setup
# Setup distribution directory: START
dist_dir=os.path.abspath('%s/osx' %(dist_dir_main_path))
if os.path.exists(dist_dir):
os.system('rm -rf %s' %(dist_dir))
os.system('mkdir -p %s' %(dist_dir))
# Setup distribution directory: STOP
APP = ['%s.py' %(main_script)]
OPTIONS={'argv_emulation': False,
'dist_dir': dist_dir,
'includes': includes_list
}
print 'Creating standalone now...'
setup(app=APP,options={'py2app': OPTIONS},setup_requires=['py2app'])
os.system('rm -rf build')
os.system('tar -C %s -czf %s/%s.tgz %s.app' %(dist_dir,dist_dir,main_script,main_script))
os.system('rm -rf %s/%s.app' %(dist_dir,main_script))
print 'Re-distributable Standalone file(s) created at %s/%s.zip. Unzip and start using!!!' %(dist_dir,main_script)
elif isWindows():
from distutils.core import setup
import py2exe
# Setup distribution directory: START
dist_dir=os.path.abspath('%s/win' %(dist_dir_main_path))
if os.path.exists(dist_dir):
os.system('rmdir /S /Q %s' %(dist_dir))
os.system('mkdir %s' %(dist_dir))
# Setup distribution directory: STOP
OPTIONS={'compressed': compression_level,
'optimize': optimization_level,
'bundle_files': bundle_parameter,
'dist_dir': dist_dir,
'xref': module_cross_reference_parameter,
'skip_archive': skip_archive_parameter,
'ascii': ascii_parameter,
'custom_boot_script': '',
'includes': includes_list
}
print 'Creating standalone now...'
setup(options = {'py2exe': OPTIONS},zipfile = None,windows=[{'script': '%s.py' %(main_script)}])
print 'Re-distributable Standalone file(s) created in the following location: %s' %(dist_dir)
os.system('rmdir /S /Q build')
Now comes the real problem.
Following is the error posted on Mac OS X console after trying to use the docx_example.app, created using the command python docx_setup.py py2app:
docx_example: Searching for something in a paragraph ... found it!
docx_example: Searching for something in a heading ... found it!
docx_example: Replacing ... done.
docx_example: Traceback (most recent call last):
docx_example: File "/Users/admin/docx-bin/osx/docx_example.app/Contents/Resources/__boot__.py", line 64, in <module>
docx_example: _run('docx_example.py')
docx_example: File "/Users/admin/docx-bin/osx/docx_example.app/Contents/Resources/__boot__.py", line 36, in _run
docx_example: execfile(path, globals(), globals())
docx_example: File "/Users/admin/docx-bin/osx/docx_example.app/Contents/Resources/docx_example.py", line 75, in <module>
docx_example: savedocx(document,coreprops,appprops,contenttypes,websettings,wordrelationships,'docx_example.docx')
docx_example: File "docx.pyc", line 849, in savedocx
docx_example: AssertionError
docx_example: docx_example Error
docx_example Exited with code: 255
Following is the error posted in docx_example.exe.log file in Windows 7 after trying to use the docx_example.exe, created using the command python docx_setup.py py2exe:
Traceback (most recent call last):
File "docx_example.py", line 75, in <module>
File "docx.pyo", line 854, in savedocx
WindowsError: [Error 3] The system cannot find the path specified: 'D:\\docx_example\\docx_example.exe\\template'
As you can see, both OS X and Windows 7 are referring to something similar here. Please help.
i have found a solution
in api.py
From
_thisdir = os.path.split(__file__)[0]
To
_thisdir = 'C:\Python27\Lib\site-packages\docx'
Or whatever your docx file is
What's going on (at least for py2exe) is something similar to this question.
The documentation on data_files is here.
What you basically have to do is change
setup(options = {'py2exe': OPTIONS},zipfile = None,windows=[{'script': '%s.py' %(main_script)}])
to
data_files = [
('template', 'D:/Program Files (x86)/Python27/Lib/site-packages/docx-template/*'),
]
setup(
options={'py2exe': OPTIONS},
zipfile=None,
windows=[{'script': '%s.py' %(main_script)}],
data_files=data_files
)
The exact place where the template files are may be wrong above, so you might need to adjust it.
But there may be several other sets of data_files you need to include. You may want to go about retrieving them programatically with an os.listdir or os.walk type of command.
As mentioned in the other post, you will also have to change
bundle_parameter=1
to
bundle_parameter=2
at the top of the file.
You can solve the entire problem by using this API which is based in python-docx. The advantage of the API is that this one doesnt have the savedoc function so you will not have any other AssertionError.
For the WindowsError: [Error 3] The system cannot find the path specified: 'D:\\docx_example\\docx_example.exe\\template' error you need to edit the api.py file of docx egg folder which is located in the Python folder of the system (in my computer: C:\Python27\Lib\site-packages\python_docx-0.3.0a5-py2.7.egg\docx)
Changing this:
_thisdir = os.path.split(__file__)[0]
_default_docx_path = os.path.join(_thisdir, 'templates', 'default.docx')
To this:
thisdir = os.getcwd()
_default_docx_path = os.path.join(thisdir, 'templates', 'default.docx')
The first one was taking the actual running program and adding it to the path to locate the templates folder.
C:\myfiles\myprogram.exe\templates\default.docx
The solution takes only the path, not the running program.
C:\myfiles\templates\default.docx
Hope it helps!
Instead of changing some library file, I find it easier and cleaner to tell python-docx explicitly where to look for the template, i.e.:
document = Document('whatever/path/you/choose/to/some.docx')
This effectively solves the py2exe and docx path problem.

How do I use cx_freeze?

I've created my setup.py file as instructed but I don't actually.. understand what to do next. Typing "python setup.py build" into the command line just gets a syntax error.
So, what do I do?
setup.py:
from cx_Freeze import setup, Executable
setup(
name = "On Dijkstra's Algorithm",
version = "3.1",
description = "A Dijkstra's Algorithm help tool.",
exectuables = [Executable(script = "Main.py", base = "Win32GUI")])
Add import sys as the new topline
You misspelled "executables" on the last line.
Remove script = on last line.
The code should now look like:
import sys
from cx_Freeze import setup, Executable
setup(
name = "On Dijkstra's Algorithm",
version = "3.1",
description = "A Dijkstra's Algorithm help tool.",
executables = [Executable("Main.py", base = "Win32GUI")])
Use the command prompt (cmd) to run python setup.py build. (Run this command from the folder containing setup.py.) Notice the build parameter we added at the end of the script call.
I'm really not sure what you're doing to get that error, it looks like you're trying to run cx_Freeze on its own, without arguments. So here is a short step-by-step guide on how to do it in windows (Your screenshot looks rather like the windows command line, so I'm assuming that's your platform)
Write your setup.py file. Your script above looks correct so it should work, assuming that your script exists.
Open the command line (Start -> Run -> "cmd")
Go to the location of your setup.py file and run python setup.py build
Notes:
There may be a problem with the name of your script. "Main.py" contains upper case letters, which might cause confusion since windows' file names are not case sensitive, but python is. My approach is to always use lower case for scripts to avoid any conflicts.
Make sure that python is on your PATH (read http://docs.python.org/using/windows.html)1
Make sure are are looking at the new cx_Freeze documentation. Google often seems to bring up the old docs.
I ran into a similar issue. I solved it by setting the Executable options in a variable and then simply calling the variable. Below is a sample setup.py that I use:
from cx_Freeze import setup, Executable
import sys
productName = "ProductName"
if 'bdist_msi' in sys.argv:
sys.argv += ['--initial-target-dir', 'C:\InstallDir\\' + productName]
sys.argv += ['--install-script', 'install.py']
exe = Executable(
script="main.py",
base="Win32GUI",
targetName="Product.exe"
)
setup(
name="Product.exe",
version="1.0",
author="Me",
description="Copyright 2012",
executables=[exe],
scripts=[
'install.py'
]
)
You can change the setup.py code to this:
from cx_freeze import setup, Executable
setup( name = "foo",
version = "1.1",
description = "Description of the app here.",
executables = [Executable("foo.py")]
)
I am sure it will work. I have tried it on both windows 7 as well as ubuntu 12.04
find the cxfreeze script and run it. It will be in the same path as your other python helper scripts, such as pip.
cxfreeze Main.py --target-dir dist
read more at:
http://cx-freeze.readthedocs.org/en/latest/script.html#script
I usually put the calling setup.py command into .bat file to easy recall.
Here is simple code in COMPILE.BAT file:
python setup.py build
#ECHO:
#ECHO . : ` . * F I N I S H E D * . ` : .
#ECHO:
#Pause
And the setup.py is organized to easy customizable parameters that let you set icon, add importe module library:
APP_NAME = "Meme Studio"; ## < Your App's name
Python_File = "app.py"; ## < Main Python file to run
Icon_Path = "./res/iconApp48.ico"; ## < Icon
UseFile = ["LANGUAGE.TXT","THEME.TXT"];
UseAllFolder = True; ## Auto scan folder which is same level with Python_File and append to UseFile.
Import = ["infi","time","webbrowser", "cv2","numpy","PIL","tkinter","math","random","datetime","threading","pathlib","os","sys"]; ## < Your Imported modules (cv2,numpy,PIL,...)
Import+=["pkg_resources","xml","email","urllib","ctypes", "json","logging"]
################################### CX_FREEZE IGNITER ###################################
from os import walk
def dirFolder(folderPath="./"): return next(walk(folderPath), (None, None, []))[1]; # [ Folder ]
def dirFile(folderPath="./"): return next(walk(folderPath), (None, None, []))[2]; # [ File ]
if UseAllFolder: UseFile += dirFolder();
import sys, pkgutil;
from cx_Freeze import setup, Executable;
BasicPackages=["collections","encodings","importlib"] + Import;
def AllPackage(): return [i.name for i in list(pkgutil.iter_modules()) if i.ispkg]; # Return name of all package
#Z=AllPackage();Z.sort();print(Z);
#while True:pass;
def notFound(A,v): # Check if v outside A
try: A.index(v); return False;
except: return True;
build_msi_options = {
'add_to_path': False,
"upgrade_code": "{22a35bac-14af-4159-7e77-3afcc7e2ad2c}",
"target_name": APP_NAME,
"install_icon": Icon_Path,
'initial_target_dir': r'[ProgramFilesFolder]\%s\%s' % ("Picox", APP_NAME)
}
build_exe_options = {
"includes": BasicPackages,
"excludes": [i for i in AllPackage() if notFound(BasicPackages,i)],
"include_files":UseFile,
"zip_include_packages": ["encodings"] ##
}
setup( name = APP_NAME,
options = {"build_exe": build_exe_options},#"bdist_msi": build_msi_options},#,
executables = [Executable(
Python_File,
base='Win32GUI',#Win64GUI
icon=Icon_Path,
targetName=APP_NAME,
copyright="Copyright (C) 2900AD Muc",
)]
);
The modules library list in the code above is minimum for workable opencv + pillow + win32 application.
Example of my project file organize:
========== U P D A T E ==========
Although cx_Freeze is a good way to create setup file. It's really consume disk space if you build multiple different software project that use large library module like opencv, torch, ai... Sometimes, users download installer, and receive false positive virus alert about your .exe file after install.
Thus, you should consider use SFX archive (.exe) your app package and SFX python package separate to share between app project instead.
You can create .bat that launch .py file and then convert .bat file to .exe with microsoft IExpress.exe.
Next, you can change .exe icon to your own icon with Resource Hacker: http://www.angusj.com/resourcehacker/
And then, create SFX archive of your package with PeaZip: https://peazip.github.io/
Finally change the icon.
The Python Package can be pack to .exe and the PATH register can made with .bat that also convertable to .exe.
If you learn more about command in .bat file and make experiments with Resource Hacker & self extract ARC in PeaZip & IExpress, you can group both your app project, python package into one .exe file only. It'll auto install what it need on user machine. Although this way more complex and harder, but then you can custom many install experiences included create desktop shorcut, and install UI, and add to app & feature list, and uninstall ability, and portable app, and serial key require, and custom license agreement, and fast window run box, and many more,..... but the important features you get is non virus false positive block, reduce 200MB to many GB when user install many your python graphic applications.

Can python distutils compile CUDA code?

I have CUDA code which I want to build a dynamic library to Python using distutils. But it seems distutils doesn't recognize ".cu" file even if the "nvcc" compiler is installed. Not sure how to get it done.
Distutils is not able to compile CUDA by default, because it doesn't support using multiple compilers simultaneously. By default, it sets to compiler just based on your platform, not on the type of source code you have.
I have an example project on github that contains some monkey patches into distutils to hack in support for this. The example project is a C++ class that manages a some GPU memory and a CUDA kernel, wrapped in swig, and all compiled with just python setup.py install. The focus is on array operations, so we're also using numpy. All the kernel does for this example project is increment each element in an array by one.
The code is here: https://github.com/rmcgibbo/npcuda-example. Here's the setup.py script. The key to whole code is customize_compiler_for_nvcc().
import os
from os.path import join as pjoin
from setuptools import setup
from distutils.extension import Extension
from distutils.command.build_ext import build_ext
import subprocess
import numpy
def find_in_path(name, path):
"Find a file in a search path"
#adapted fom http://code.activestate.com/recipes/52224-find-a-file-given-a-search-path/
for dir in path.split(os.pathsep):
binpath = pjoin(dir, name)
if os.path.exists(binpath):
return os.path.abspath(binpath)
return None
def locate_cuda():
"""Locate the CUDA environment on the system
Returns a dict with keys 'home', 'nvcc', 'include', and 'lib64'
and values giving the absolute path to each directory.
Starts by looking for the CUDAHOME env variable. If not found, everything
is based on finding 'nvcc' in the PATH.
"""
# first check if the CUDAHOME env variable is in use
if 'CUDAHOME' in os.environ:
home = os.environ['CUDAHOME']
nvcc = pjoin(home, 'bin', 'nvcc')
else:
# otherwise, search the PATH for NVCC
nvcc = find_in_path('nvcc', os.environ['PATH'])
if nvcc is None:
raise EnvironmentError('The nvcc binary could not be '
'located in your $PATH. Either add it to your path, or set $CUDAHOME')
home = os.path.dirname(os.path.dirname(nvcc))
cudaconfig = {'home':home, 'nvcc':nvcc,
'include': pjoin(home, 'include'),
'lib64': pjoin(home, 'lib64')}
for k, v in cudaconfig.iteritems():
if not os.path.exists(v):
raise EnvironmentError('The CUDA %s path could not be located in %s' % (k, v))
return cudaconfig
CUDA = locate_cuda()
# Obtain the numpy include directory. This logic works across numpy versions.
try:
numpy_include = numpy.get_include()
except AttributeError:
numpy_include = numpy.get_numpy_include()
ext = Extension('_gpuadder',
sources=['src/swig_wrap.cpp', 'src/manager.cu'],
library_dirs=[CUDA['lib64']],
libraries=['cudart'],
runtime_library_dirs=[CUDA['lib64']],
# this syntax is specific to this build system
# we're only going to use certain compiler args with nvcc and not with gcc
# the implementation of this trick is in customize_compiler() below
extra_compile_args={'gcc': [],
'nvcc': ['-arch=sm_20', '--ptxas-options=-v', '-c', '--compiler-options', "'-fPIC'"]},
include_dirs = [numpy_include, CUDA['include'], 'src'])
# check for swig
if find_in_path('swig', os.environ['PATH']):
subprocess.check_call('swig -python -c++ -o src/swig_wrap.cpp src/swig.i', shell=True)
else:
raise EnvironmentError('the swig executable was not found in your PATH')
def customize_compiler_for_nvcc(self):
"""inject deep into distutils to customize how the dispatch
to gcc/nvcc works.
If you subclass UnixCCompiler, it's not trivial to get your subclass
injected in, and still have the right customizations (i.e.
distutils.sysconfig.customize_compiler) run on it. So instead of going
the OO route, I have this. Note, it's kindof like a wierd functional
subclassing going on."""
# tell the compiler it can processes .cu
self.src_extensions.append('.cu')
# save references to the default compiler_so and _comple methods
default_compiler_so = self.compiler_so
super = self._compile
# now redefine the _compile method. This gets executed for each
# object but distutils doesn't have the ability to change compilers
# based on source extension: we add it.
def _compile(obj, src, ext, cc_args, extra_postargs, pp_opts):
if os.path.splitext(src)[1] == '.cu':
# use the cuda for .cu files
self.set_executable('compiler_so', CUDA['nvcc'])
# use only a subset of the extra_postargs, which are 1-1 translated
# from the extra_compile_args in the Extension class
postargs = extra_postargs['nvcc']
else:
postargs = extra_postargs['gcc']
super(obj, src, ext, cc_args, postargs, pp_opts)
# reset the default compiler_so, which we might have changed for cuda
self.compiler_so = default_compiler_so
# inject our redefined _compile method into the class
self._compile = _compile
# run the customize_compiler
class custom_build_ext(build_ext):
def build_extensions(self):
customize_compiler_for_nvcc(self.compiler)
build_ext.build_extensions(self)
setup(name='gpuadder',
# random metadata. there's more you can supploy
author='Robert McGibbon',
version='0.1',
# this is necessary so that the swigged python file gets picked up
py_modules=['gpuadder'],
package_dir={'': 'src'},
ext_modules = [ext],
# inject our custom trigger
cmdclass={'build_ext': custom_build_ext},
# since the package has c code, the egg cannot be zipped
zip_safe=False)
As an alternative to distutils/setuptools, you could use scikit-build (along with CMakeLists.txt, pyproject.toml, and setup.cfg/setup.py):
import sys
from pathlib import Path
from skbuild import setup
from setuptools import find_packages
# https://github.com/scikit-build/scikit-build/issues/521#issuecomment-753035688
for i in (Path(__file__).resolve().parent / "_skbuild").rglob("CMakeCache.txt"):
i.write_text(re.sub("^//.*$\n^[^#].*pip-build-env.*$", "", i.read_text(), flags=re.M))
setup(cmake_args=[f"-DPython3_ROOT_DIR={sys.prefix}"],
packages=find_packages(exclude=["tests"]))

Categories

Resources