2017年2月27日 星期一

Python setup.py 簡易研究

最近在公司開發 Python 專案時,有一些需求

  1. 保護 Source code 的方法。網路上其實可以找到很多方法,像是代碼混淆、加密、打包成一個可執行檔以及利用 Cython 將 Python 檔案轉換為 C,然後編繹成 Library 檔案 (Windows 為 pyd 檔案 , Linux 為 so 檔案)。最終採用了最後兩種方法
  2. 大多的專案最終都會有重覆的程式,所以想寫一個共用 Library 供開發使用

上述兩點都可以使用 Python Setup.py 來實現,所以要先了解 Setup.py 怎麼撰寫


一、先來看看 Setup.py Spec 吧


想要了解 Setup.py 是什麼,或是如何寫出一個 Setup.py?
我會請你直接 Google (呵呵)

原因無它,由於 Python 文件檔案做得很好,沒有理由不利用它

這裡挑幾個常用到的名詞做簡單的說明

  • name : 一般填寫 package 的名字即可
  • version : 版本
  • description : 基本描述
  • logn_description : 更加詳細的描述
  • author : 作者
  • author_email : email
  • url : 項目的網站
  • license : 授權的方式
  • keywords : 關鍵字,可作為分類用
  • packages : 告訴 Distutils 需要處理哪些 package (有包含 __init__.py 的資料夾)
  • package_dir : 告訴 Distutils 哪些 package 所對應的相對路徑。比如說 package_dir={'foo', 'lib'},表示路徑最終為 'lib/foo'
  • ext_modules : 是一個包含 Extension 的列表
  • ext_package : 定義 Extension 的相對路徑
  • requires : 定義依賴哪些 package or module
  • provides : 定義可以為哪些 module 提供依賴 (Dependency)
  • scripts : 指定一個可以從 command line 執行的 python 檔案,在安裝時指定 --install-script
  • package_date : 通常用於相關的數據文件或類似於 readme 的文件。比如說 package_data={'':[*.txt'], 'mypkg':['data/*.dat']},表示包含所有資料夾的 txt 檔案和 mypkg/data中所有 dat 檔案
  • data_files : 指定其它的一些文件,如 config 檔案。比如說 data_files=[(bitmaps', ['bm/b1.gif', 'bm/b2.gif'])],表示 'b1.gif' 和 'b1.gif' 將會放在 bitmaps 資料夾中。
  • 另一種方式是寫一個 MANIFEST.in template,將需要包含在分發包的文件寫在裡面

參考上面寫一個簡單的 Setup.py,大概是長成這樣
  1. from distutils.core import setup
  2.  
  3. setup(
  4. name="text",
  5. version="1.0",
  6. author="YOUR_NAME"
  7. author_email="YOUR_NAME@email",
  8. url="http://www.test.org",
  9. packages=['test'],
  10. )


二、Setup.py 有哪些指令可以用


除了上述的方法,其實也可以用 help 指令去了解 Setup.py
  1. $> python setup.py --help
  2.  
  3. Common commands: (see '--help-commands' for more)
  4.  
  5. setup.py build will build the package underneath 'build/'
  6. setup.py install will install the package
  7.  
  8. Global options:
  9. --verbose (-v) run verbosely (default)
  10. --quiet (-q) run quietly (turns verbosity off)
  11. --dry-run (-n) don't actually do anything
  12. --help (-h) show detailed help message
  13. --no-user-cfg ignore pydistutils.cfg in your home directory
  14. --command-packages list of packages that provide distutils commands
  15.  
  16. Information display options (just display information, ignore any commands)
  17. --help-commands list all available commands
  18. --name print package name
  19. --version (-V) print package version
  20. --fullname print -
  21. --author print the author's name
  22. --author-email print the author's email address
  23. --maintainer print the maintainer's name
  24. --maintainer-email print the maintainer's email address
  25. --contact print the maintainer's name if known, else the author's
  26. --contact-email print the maintainer's email address if known, else the
  27. author's
  28. --url print the URL for this package
  29. --license print the license of the package
  30. --licence alias for --license
  31. --description print the package description
  32. --long-description print the long package description
  33. --platforms print the list of platforms
  34. --classifiers print the list of classifiers
  35. --keywords print the list of keywords
  36. --provides print the list of packages/modules provided
  37. --requires print the list of packages/modules required
  38. --obsoletes print the list of packages/modules made obsolete
  39. usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
  40. or: setup.py --help [cmd1 cmd2 ...]
  41. or: setup.py --help-commands
  42. or: setup.py cmd --help

試試 Commands help
  1. $> python setup.py --help-command
  2.  
  3. Standard commands:
  4. build build everything needed to install
  5. build_py "build" pure Python modules (copy to build directory)
  6. build_ext build C/C++ and Cython extensions (compile/link to build directory)
  7. build_clib build C/C++ libraries used by Python extensions
  8. build_scripts "build" scripts (copy and fixup #! line)
  9. clean clean up temporary files from 'build' command
  10. install install everything from build directory
  11. install_lib install all Python modules (extensions and pure Python)
  12. install_headers install C/C++ header files
  13. install_scripts install scripts (Python or otherwise)
  14. install_data install data files
  15. sdist create a source distribution (tarball, zip file, etc.)
  16. register register the distribution with the Python package index
  17. bdist create a built (binary) distribution
  18. bdist_dumb create a "dumb" built distribution
  19. bdist_rpm create an RPM distribution
  20. bdist_wininst create an executable installer for MS Windows
  21. upload upload binary package to PyPI
  22. check perform some checks on the package
  23.  
  24. Extra commands:
  25. saveopts save supplied options to setup.cfg or other config file
  26. develop install package in 'development mode'
  27. upload_docs Upload documentation to PyPI
  28. test run unit tests after in-place build
  29. setopt set an option in setup.cfg or another config file
  30. install_egg_info Install an .egg-info directory for the package
  31. rotate delete older distributions, keeping N newest files
  32. cleanall clean up temporary files from 'build' command
  33. bdist_wheel create a wheel distribution
  34. egg_info create a distribution's .egg-info directory
  35. alias define a shortcut to invoke one or more commands
  36. easy_install Find/get/install Python packages
  37. bdist_egg create an "egg" distribution
  38.  
  39. usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
  40. or: setup.py --help [cmd1 cmd2 ...]
  41. or: setup.py --help-commands
  42. or: setup.py cmd --help

這裡舉例一些常用的指令
  1. python setup.py build # 僅編繹不安裝
  2. python setup.py install # 安裝到 python 安裝目錄的 lib
  3. python setup.py sdist # 產生成壓縮檔案 (zip/tar.gz)。Support pip
  4.  
  5. python setup.py bdist --formats=zip
  6. # 等同於 python setup.py sdist
  7. python setup.py bdist_egg # 產生成一個二進制分發版本,經常用來替代 bdist。Support easy_install
  8. python setup.py bdist_wininst # 生成 NT 平台安裝檔案 (.exe)
  9. python setup.py bdist_rpm # 生成 rpm 檔案
  10.  
  11. python setup.py develop # 編繹並且在適當的位置安裝,然後增加一個鏈結到 python site-packages 中
  12. python setup.py develop -u # 反安裝


三、利用 Cython 將 Python 檔案轉成 C 檔案


Cython 官網文件在這裡,因為我沒有看完,就不多說了

  1. 安裝方法並不困難
    1. pip install Cython
  2. 將原本的 Python 檔案,重新命名副檔名為 *.pyx
  3. 簡單的 Setup.py 可以長這樣
    1. from distutils.core import setup
    2. from Cython.Build import cythonize
    3.  
    4. setup(
    5. name = 'example 1',
    6. ext_modules = cythonize("*.pyx"),
    7. )
  4. 還有使用 Extension 的寫法,像這樣
    1. from distutils.core import setup
    2. from distutils.extension import Extension
    3. from Cython.Build import cythonize
    4.  
    5. ext_modules=[
    6. Extension(
    7. "example",
    8. sources=["example.pyx"]
    9. )
    10. ]
    11.  
    12. setup(
    13. name = "example 2",
    14. ext_modules = cythonize(ext_modules)
    15. )
  5. 執行下列指令,Cython 就會將 *pyx 轉換為 *.c ,經由 gcc 編譯之後就可以得到 library 檔案囉
    1. python setup.py build_ext --inplace


四、來個 Setup.py 範例吧


檔案的架構長這樣

setup_example/
├── Makefile
├── setup.py
└── src
    ├── Hello
    │   ├── Hello.py
    │   └── __init__.py
    ├── Hello_so
    │   ├── Hello_so.pyx
    │   └── __init__.py
    └── __init__.py


4.1 Source code

  • Hello.__init__.py
    1. from __future__ import absolute_import
    2.  
    3. from .Hello import hello_world
    4.  
  • Hello.Hello.py
    1. def hello_world():
    2. print("PY SAY: Hello!!")
    3.  
  • Hello_so.__init__.py
    1. from __future__ import absolute_import
    2.  
    3. from .Hello_so import hello_world
    4.  
  • Hello_so.Hello_so.pyx
    1. def hello_world():
    2. print("SO SAY: Hello!!")
    3.  
  • setup.py
    1. # coding: utf-8
    2. import glob
    3. import os
    4. from distutils.command.clean import clean
    5. from distutils.core import setup
    6.  
    7. from setuptools import find_packages
    8. from setuptools.extension import Extension
    9.  
    10. try:
    11. from Cython.Distutils import build_ext
    12. except (ImportError, ImportWarning):
    13. print("Import Cython failed. Is Cython not installed?")
    14. raise
    15.  
    16. Project_Name = 'setup_example'
    17. Project_PKG_Path = 'src'
    18.  
    19. LONGDESC = """
    20. Example Python Setup.py
    21. =======================
    22.  
    23. How to build
    24. ============
    25.  
    26. $> python setup.py bdist_egg # generate a build .egg file
    27. $> python setup.py bdist_egg --exclude-source-files # generate a build .egg file without source files
    28. $> python setup.py sdist # generate a build source distro
    29. $> python setup.py sdist bdist_egg # generate both
    30.  
    31. How to install
    32. ==============
    33.  
    34. $> python setup.py install
    35. $> pip install dist/'generated-egg'
    36.  
    37. How to uninstall
    38. ================
    39.  
    40. $> pip uninstall {name}
    41.  
    42. """.format(name=Project_Name)
    43.  
    44. Requires = [
    45. 'cython',
    46. ]
    47.  
    48.  
    49. def _searching_dir(current_dir, extension, files=list()):
    50. """ Create a list of all files to be compiled """
    51. for file_name in os.listdir(current_dir):
    52. file_path = os.path.join(current_dir, file_name)
    53. if os.path.isfile(file_path) and file_path.endswith(extension):
    54. files.append(file_path)
    55. elif os.path.isdir(file_path):
    56. _searching_dir(file_path, extension, files)
    57.  
    58. return files
    59.  
    60.  
    61. def _make_extension(ext_path, only_filename=False, includes=list()):
    62. """Generate an Extension object from its dotted name
    63.  
    64. :param ext_path:
    65. :param only_filename:
    66. :param includes: Including path
    67. :return:
    68. """
    69. if only_filename is True:
    70. ext_path = os.path.basename(ext_path)
    71.  
    72. return Extension(
    73. (ext_path.replace(os.path.sep, '.'))[:-4],
    74. [ext_path],
    75. include_dirs=includes + ['.'], # adding the '.' to include_dirs is CRUCIAL!!
    76. # extra_compile_args = ["-O3", "-Wall"],
    77. # extra_link_args = ['-g'],
    78. # libraries = ["dv",],
    79. )
    80.  
    81.  
    82. def _purge(pattern):
    83. for path in glob.glob(pattern):
    84. if os.path.exists(path):
    85. os.remove(path)
    86.  
    87.  
    88. class CleanAll(clean):
    89. def run(self):
    90. # Clean build
    91. clean.run(self)
    92.  
    93. # Clean *.c, *.so generated by pyx file
    94. for file_path in pyx_modules:
    95. _purge(file_path[:-4] + "*.c")
    96. _purge(file_path[:-4] + '*.so')
    97.  
    98.  
    99. if __name__ == '__main__':
    100.  
    101. # build up the set of Extension objects
    102. ext_modules = []
    103. pyx_modules = _searching_dir(Project_PKG_Path, ".pyx", [])
    104. for name in pyx_modules:
    105. ext_modules.append(_make_extension(name))
    106.  
    107. setup(
    108. name=Project_Name,
    109. version="0.0",
    110. requires=Requires,
    111. cmdclass={'build_ext': build_ext, 'cleanall': CleanAll},
    112. description='OnWord Security Common Python Utilities',
    113. long_description=LONGDESC,
    114. author='Your Name',
    115. author_email='YOUR_NAME@example.com',
    116. url='https://www.example.com/',
    117. license='Private',
    118. classifiers=[
    119. 'Intended Audience :: Developers',
    120. 'Operating System :: OS Independent',
    121. 'Programming Language :: Python',
    122. 'Programming Language :: Python :: 2.7',
    123. 'Topic :: Software Development :: Libraries',
    124. 'Topic :: Utilities',
    125. ],
    126. keywords='example',
    127. packages=find_packages(exclude=[], ),
    128. package_dir={'src': 'src'},
    129. package_data={'src': ['*.*']},
    130. zip_safe=False,
    131. ext_modules=ext_modules,
    132. )
    133.  
  • Makefile
    1. # Makefile
    2. #
    3. # @build
    4. # 1. make, or
    5. # 2. make all
    6. #
    7. # @install
    8. # 1. make install
    9. #
    10. # @clean all
    11. # 1. make clean
    12. #
    13. # @Change python or pyinstaller path
    14. #
    15. PYTHON:=python
    16.  
    17. all:
    18. $(PYTHON) setup.py build_ext --inplace
    19.  
    20. .PHONY: all
    21.  
    22. build-clean:
    23. $(PYTHON) setup.py cleanall
    24. rm -rf MANIFEST build dist *.egg-info
    25.  
    26. .PHONY: build-clean
    27.  
    28. sdist: all
    29. $(PYTHON) setup.py sdist
    30.  
    31. .PHONY: sdist
    32.  
    33. install: all
    34. $(PYTHON) setup.py install
    35.  
    36. .PHONY: install
    37.  
    38. clean: build-clean
    39. find . -type f -name "*.py[co]" -delete
    40. find . -type d -name "__pycache__" -delete
    41.  
    42. .PHONY: clean


4.2 安裝到 python lib 中

  1. $> make install

如果沒出現錯誤訊息的話,那麼應該會順利地編譯出 so 檔案


可以用  pip list 看看已安裝的 packages
  1. $> pip list
  2.  
  3. appdirs (1.4.0)
  4. Cython (0.25.2)
  5. packaging (16.8)
  6. pip (9.0.1)
  7. pyparsing (2.1.10)
  8. setup-example (0.0)
  9. setuptools (34.2.0)
  10. six (1.10.0)
  11. wheel (0.29.0)

有看到 setup-example (0.0),接下來就是試用看看囉


4.3 用看看效果如何

  1. from src.Hello import hello_world
  2. from src.Hello_so import hello_world as hello_world_so
  3.  
  4. hello_world()
  5. hello_world_so()
  6.  
結果畫面為

最後試著用 pyinstaller,so 檔也可以成功地打包
順帶一提,反安裝的方法可以用
  1. pip uninstall setup-example


5. Q&A

  • 出現 error: each element of 'ext_modules' option must be an Extension instance or 2-tuple
    解決方法 : 雖然目前還是不明白為什麼,改用 setuptools extension 就沒問題了
    1. + from setuptools.extension import Extension
    2. - from distutils.extension import Extension

5 則留言:

  1. Your info is really amazing with impressive content..Excellent blog with informative concept. Really I feel happy to see this useful blog, Thanks for sharing such a nice blog..
    If you are looking for any python Related information please visit our website Python Training In Pune page!

    回覆刪除

  2. Thanks for sharing.Your information is clear with very good content.This was the exact information i was looking for.keep updating more.If you are the one searching for big Data certification courses then visit our site big data certification bangalore

    回覆刪除
  3. Do you want to get back love of your life and have tried all possible efforts but failed in all? Did you love someone madly but recently had a breakup? Do you want your beloved back? If yes then astrology is the best which recommends various remedies to solve this problem.

    Get your love back astrologer to see a wonderful difference created in your love life. Astrology will work like magic and at the end your love life will be blessed with happiness and passion for each other. Ask your World Famous Indian Astrologer now.

    回覆刪除
  4. Very informative and amazing data..
    Thanks for sharing with us,
    We are again come on your website,
    Thanks and good day,
    Please visit our site,
    buylogo

    回覆刪除
  5. this is really great content thanks for sharing with us keep sharing more helpful content like this...!
    are you guys have an interest in web designing or logo designing then visit us?
    Logo Designers

    回覆刪除