PageRenderTime 33ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/betse_setup/symlink.py

https://gitlab.com/dglmoore/betse
Python | 307 lines | 277 code | 5 blank | 25 comment | 2 complexity | f45516d116af3291c7bbfd9eba591f76 MD5 | raw file
  1. #!/usr/bin/env python3
  2. # --------------------( LICENSE )--------------------
  3. # Copyright 2014-2017 by Alexis Pietak & Cecil Curry.
  4. # See "LICENSE" for further details.
  5. '''
  6. BETSE-specific `symlink` subcommands for `setuptools`.
  7. ## Microsoft Windows
  8. Microsoft Windows does _not_ comply with POSIX standards and hence does _not_
  9. generally support symbolic links.
  10. While post-Vista versions of Microsoft Windows _do_ purport to support symbolic
  11. links, the Windows version of the (Ana|Mini)conda Python distribution does _not_
  12. appear to (at least, not reliably). Since this renders symbolic links useless
  13. for standard Windows use, this module assumes Windows to _never_ support
  14. symbolic links regardless of version.
  15. Under Microsoft Windows, this module "fakes" symbolic link-based installation by
  16. artifically prepending the Python-specific `sys.path` list of search dirnames
  17. with the absolute path of the parent directory containing the top-level Python
  18. package -- which largely has the same effect, albeit less resiliently. While
  19. `sys.path` manipulation is (justifiably) frowned upon, no alternatives exist.
  20. '''
  21. # ....................{ IMPORTS }....................
  22. import os
  23. from betse_setup import util
  24. from setuptools.command.install import install
  25. from setuptools.command.install_lib import install_lib
  26. from setuptools.command.install_scripts import install_scripts
  27. from os import path
  28. # ....................{ COMMANDS }....................
  29. def add_setup_commands(metadata: dict, setup_options: dict) -> None:
  30. '''
  31. Add `symlink` subcommands to the passed dictionary of `setuptools` options.
  32. '''
  33. util.add_setup_command_classes(
  34. metadata, setup_options,
  35. symlink, symlink_lib, symlink_scripts, unsymlink)
  36. # ....................{ CLASSES ~ install }....................
  37. class symlink(install):
  38. '''
  39. Editably install (e.g., in a symbolically linked manner) `betse` into the
  40. active Python 3 interpreter *without* performing dependency resolution.
  41. Unlike the default `develop` command, this command is suitable for
  42. system-wide installation.
  43. '''
  44. # ..................{ ATTRIBUTES }..................
  45. description = (
  46. 'install a symlink rather than copy of this package (for development)')
  47. '''
  48. Command description printed when running `./setup.py --help-commands`.
  49. '''
  50. sub_commands = [
  51. ('symlink_lib', None),
  52. ('symlink_scripts', None),
  53. ]
  54. '''
  55. Dictionary mapping command names to either:
  56. * A predicate returning a boolean indicating whether such command should be
  57. run under this run of the current command.
  58. * `None` signifying that such command should always be run.
  59. '''
  60. # ..................{ SUPERCLASS }..................
  61. def finalize_options(self):
  62. '''
  63. Default undefined command-specific options to the options passed to the
  64. current parent command if any (e.g., `install`).
  65. '''
  66. super().finalize_options()
  67. #FIXME: Replicate this functionality for the "install" command as well.
  68. # If the current system is OS X *AND* the OS X-specific Homebrew package
  69. # manager is installed...
  70. if util.is_os_os_x() and util.is_pathable('brew'):
  71. # Absolute path of Homebrew's top-level system-wide cellar
  72. # directory (e.g., "/usr/local/Cellar").
  73. brew_cellar_dir = util.get_command_output('brew', '--cellar')
  74. #print('Here!')
  75. # Absolute path of Homebrew's top-level system-wide directory
  76. # (e.g., "/usr/local").
  77. brew_dir = util.get_command_output('brew', '--prefix')
  78. # Absolute path of Homebrew's top-level system-wide binary
  79. # directory (e.g., "/usr/local/bin").
  80. brew_binary_dir = path.join(brew_dir, 'bin')
  81. # If this directory does not exist, raise an exception.
  82. util.die_unless_dir(brew_binary_dir)
  83. # If the directory to which wrappers will be installed is a Python-
  84. # specific subdirectory of this cellar directory (e.g.,
  85. # "/usr/local/Cellar/python3/3.5.0/Frameworks/Python.framework/Versions/3.5/bin"),
  86. # that subdirectory is unlikely to reside in the current ${PATH},
  87. # in which case wrappers installed to that subdirectory will remain
  88. # inaccessible. Correct this by forcing wrappers to be installed
  89. # to the Homebrew's conventional binary directory instead.
  90. if self.install_scripts.startswith(brew_cellar_dir):
  91. self.install_scripts = brew_binary_dir
  92. print('Detected Homebrew installation directory "{}".'.format(
  93. brew_binary_dir))
  94. def run(self):
  95. '''Run the current command and all subcommands thereof.'''
  96. # If the current operating system is POSIX-incompatible, this system
  97. # does *NOT* support conventional symbolic links. See details above.
  98. if not util.is_os_posix():
  99. # Avoid circular import dependencies.
  100. from betse_setup import build
  101. # Print a non-fatal warning.
  102. util.output_warning(
  103. 'Symbolic links require POSIX compatibility. '
  104. 'Since the current platform is\n'
  105. 'POSIX-incompatible (e.g., Windows), '
  106. 'symbolic links will be faked with black magic.'
  107. )
  108. # Absolute path of the parent directory containing the top-level
  109. # "betse" package.
  110. parent_dirname = util.get_project_dirname()
  111. # print('parent: ' + parent_dirname)
  112. # Prepend the template for subsequently installed entry points by a
  113. # Python statement "faking" symlink-based installation.
  114. build.SCRIPT_TEMPLATE = """
  115. # The current operating system is POSIX-incompatible and hence does *NOT*
  116. # support symlinks. To "fake" symlink-based installation, the standard list of
  117. # search dirnames is prepended by the absolute path of the parent directory of
  118. # the top-level "betse" package. For compatibility with third-party modules,
  119. # the first entry of such list (i.e., the parent directory of this script) is
  120. # preserved by inserting at index 1 rather than 0.
  121. import sys
  122. sys.path.insert(1, {})
  123. """.format(repr(parent_dirname)) + build.SCRIPT_TEMPLATE
  124. # Run all subcommands.
  125. for subcommand_name in self.get_sub_commands():
  126. self.run_command(subcommand_name)
  127. # ....................{ CLASSES ~ install : subcommands }....................
  128. class symlink_lib(install_lib):
  129. '''
  130. Install the symbolic link for `betse`'s current editable installation,
  131. usually to a system-wide `site-packages` directory for the active Python 3
  132. interpreter.
  133. '''
  134. description = "install a symlink to this package's top-level module"
  135. '''
  136. Command description printed when running `./setup.py --help-commands`.
  137. '''
  138. def finalize_options(self):
  139. '''
  140. Default undefined command-specific options to the options passed to the
  141. current parent command if any (e.g., `symlink`).
  142. '''
  143. # Copy attributes from a temporarily instantiated "symlink" object into
  144. # the current object under different attribute names.
  145. self.set_undefined_options(
  146. 'symlink', ('install_lib', 'install_dir'))
  147. # Default all remaining options.
  148. super().finalize_options()
  149. def run(self):
  150. # If the current operating system is POSIX-incompatible, such system
  151. # does *NOT* support conventional symbolic links. Return immediately.
  152. if not util.is_os_posix():
  153. return
  154. # Absolute path of betse's top-level Python package in the current
  155. # directory.
  156. package_dirname = path.join(os.getcwd(), self._setup_options['name'])
  157. # Absolute path of such symbolic link.
  158. symlink_filename = path.join(
  159. self.install_dir,
  160. self._setup_options['name'])
  161. # (Re)create such link.
  162. util.make_symlink(package_dirname, symlink_filename)
  163. class symlink_scripts(install_scripts):
  164. '''
  165. Install all scripts wrapping `betse`'s current editable installation,
  166. usually to a system-wide directory in the current `${PATH}`.
  167. '''
  168. description =\
  169. 'install scripts running this package without dependency checks'
  170. '''
  171. Command description printed when running `./setup.py --help-commands`.
  172. '''
  173. def finalize_options(self):
  174. '''
  175. Default undefined command-specific options to the options passed to the
  176. current parent command if any (e.g., `symlink`).
  177. '''
  178. # Copy attributes from a temporarily instantiated "symlink" object into
  179. # the current object under different attribute names.
  180. self.set_undefined_options(
  181. 'build',
  182. ('build_scripts', 'build_dir'),
  183. )
  184. self.set_undefined_options(
  185. 'symlink',
  186. ('install_scripts', 'install_dir'),
  187. ('force', 'force'),
  188. ('skip_build', 'skip_build'),
  189. )
  190. # Default all remaining options.
  191. super().finalize_options()
  192. # ....................{ UNINSTALLERS }....................
  193. class unsymlink(install):
  194. '''
  195. Editably uninstall (e.g., in a symbolically linked manner) `betse` from
  196. the active Python 3 interpreter.
  197. Attributes
  198. ----------
  199. install_package_dirname : str
  200. Absolute path of the directory to which our Python codebase was
  201. previously installed.
  202. install_wrapper_dirname : str
  203. Absolute path of the directory to which our wrapper scripts were
  204. previously installed.
  205. '''
  206. description =\
  207. 'uninstall all installed symbolic links and scripts for this package'
  208. '''
  209. Command description printed when running `./setup.py --help-commands`.
  210. '''
  211. def initialize_options(self):
  212. '''
  213. Declare option-specific attributes subsequently initialized by
  214. `finalize_options()`.
  215. If this function is *not* defined, the default implementation of this
  216. method raises an inscrutable `distutils` exception. If such attributes
  217. are *not* declared, the subsequent call to
  218. `self.set_undefined_options()` raises an inscrutable `setuptools`
  219. exception. (This is terrible. So much hate.)
  220. '''
  221. super().initialize_options()
  222. self.install_package_dirname = None
  223. self.install_wrapper_dirname = None
  224. def finalize_options(self):
  225. '''
  226. Default undefined command-specific options to the options passed to the
  227. current parent command if any (e.g., `symlink`).
  228. '''
  229. # Copy attributes from a temporarily instantiated "symlink" object into
  230. # the current object under different attribute names.
  231. self.set_undefined_options(
  232. 'symlink',
  233. ('install_lib', 'install_package_dirname'),
  234. ('install_scripts', 'install_wrapper_dirname'),
  235. )
  236. # Default all remaining options.
  237. super().finalize_options()
  238. def run(self):
  239. '''Run the current command and all subcommands thereof.'''
  240. # If the current operating system is POSIX-compatible, such system
  241. # supports symbolic links. In such case, remove the previously installed
  242. # symbolic link.
  243. if util.is_os_posix():
  244. util.remove_symlink(path.join(
  245. self.install_package_dirname,
  246. self._setup_options['name'],
  247. ))
  248. # Remove all installed scripts.
  249. for script_basename, _, _ in util.command_entry_points(self):
  250. util.remove_file(path.join(
  251. self.install_wrapper_dirname, script_basename))