setup.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. #!/usr/bin/env python3
  2. from __future__ import print_function
  3. import sys
  4. import os
  5. import platform
  6. import errno
  7. import re
  8. import shutil
  9. import hashlib
  10. from distutils.core import setup
  11. from distutils.extension import Extension
  12. from awlsim.common.version import VERSION_STRING
  13. try:
  14. import py2exe
  15. except ImportError as e:
  16. py2exe = None
  17. try:
  18. if py2exe and "py2exe" in sys.argv:
  19. raise ImportError
  20. from cx_Freeze import setup, Executable
  21. cx_Freeze = True
  22. except ImportError as e:
  23. cx_Freeze = False
  24. def makedirs(path, mode=0o755):
  25. try:
  26. os.makedirs(path, mode)
  27. except OSError as e:
  28. if e.errno == errno.EEXIST:
  29. return
  30. raise
  31. def hashFile(path):
  32. if sys.version_info[0] < 3:
  33. ExpectedException = IOError
  34. else:
  35. ExpectedException = FileNotFoundError
  36. try:
  37. return hashlib.sha1(open(path, "rb").read()).hexdigest()
  38. except ExpectedException as e:
  39. return None
  40. def __fileopIfChanged(fromFile, toFile, fileop):
  41. toFileHash = hashFile(toFile)
  42. if toFileHash is not None:
  43. fromFileHash = hashFile(fromFile)
  44. if toFileHash == fromFileHash:
  45. return False
  46. makedirs(os.path.dirname(toFile))
  47. fileop(fromFile, toFile)
  48. return True
  49. def copyIfChanged(fromFile, toFile):
  50. return __fileopIfChanged(fromFile, toFile, shutil.copy2)
  51. def moveIfChanged(fromFile, toFile):
  52. return __fileopIfChanged(fromFile, toFile, os.rename)
  53. def makeDummyFile(path):
  54. if os.path.isfile(path):
  55. return
  56. print("creating dummy file '%s'" % path)
  57. makedirs(os.path.dirname(path))
  58. fd = open(path, "w")
  59. fd.write("\n")
  60. fd.close()
  61. def pyCythonPatch(fromFile, toFile, basicOnly=False):
  62. print("cython-patch: patching file '%s' to '%s'" %\
  63. (fromFile, toFile))
  64. tmpFile = toFile + ".TMP"
  65. makedirs(os.path.dirname(tmpFile))
  66. infd = open(fromFile, "r")
  67. outfd = open(tmpFile, "w")
  68. for line in infd.readlines():
  69. stripLine = line.strip()
  70. if stripLine.endswith("#<no-cython-patch"):
  71. outfd.write(line)
  72. continue
  73. # Uncomment all lines containing #@cy
  74. if "#@cy" in stripLine:
  75. line = line.replace("#@cy", "")
  76. if line.startswith("#"):
  77. line = line[1:]
  78. if not line.endswith("\n"):
  79. line += "\n"
  80. # Sprinkle magic cdef, as requested by #+cdef
  81. if "#+cdef" in stripLine:
  82. if stripLine.startswith("class"):
  83. line = line.replace("class", "cdef class")
  84. else:
  85. line = line.replace("def", "cdef")
  86. # Comment all lines containing #@nocy
  87. if "#@nocy" in stripLine:
  88. line = "#" + line
  89. if not basicOnly:
  90. # Automagic types
  91. line = re.sub(r'\b_Bool\b', "unsigned char", line)
  92. line = re.sub(r'\bint8_t\b', "signed char", line)
  93. line = re.sub(r'\buint8_t\b', "unsigned char", line)
  94. line = re.sub(r'\bint16_t\b', "signed short", line)
  95. line = re.sub(r'\buint16_t\b', "unsigned short", line)
  96. line = re.sub(r'\bint32_t\b', "signed int", line)
  97. line = re.sub(r'\buint32_t\b', "unsigned int", line)
  98. line = re.sub(r'\bint64_t\b', "signed long long", line)
  99. line = re.sub(r'\buint64_t\b', "unsigned long long", line)
  100. # Remove compat stuff
  101. line = line.replace("absolute_import,", "")
  102. # Patch the import statements
  103. line = re.sub(r'^(\s*from awlsim[0-9a-zA-Z_]*)\.([0-9a-zA-Z_\.]+) import', r'\1_cython.\2 import', line)
  104. line = re.sub(r'^(\s*from awlsim[0-9a-zA-Z_]*)\.([0-9a-zA-Z_\.]+) cimport', r'\1_cython.\2 cimport', line)
  105. line = re.sub(r'^(\s*import awlsim[0-9a-zA-Z_]*)\.', r'\1_cython.', line)
  106. line = re.sub(r'^(\s*cimport awlsim[0-9a-zA-Z_]*)\.', r'\1_cython.', line)
  107. outfd.write(line)
  108. infd.close()
  109. outfd.flush()
  110. outfd.close()
  111. if not moveIfChanged(tmpFile, toFile):
  112. print("(already up to date)")
  113. os.unlink(tmpFile)
  114. class CythonBuildUnit(object):
  115. def __init__(self, cyModName, baseName, fromPy, fromPxd, toDir, toPyx, toPxd):
  116. self.cyModName = cyModName
  117. self.baseName = baseName
  118. self.fromPy = fromPy
  119. self.fromPxd = fromPxd
  120. self.toDir = toDir
  121. self.toPyx = toPyx
  122. self.toPxd = toPxd
  123. cythonBuildUnits = []
  124. def patchCythonModules(buildDir):
  125. for unit in cythonBuildUnits:
  126. makedirs(unit.toDir)
  127. makeDummyFile(os.path.join(unit.toDir, "__init__.py"))
  128. if unit.baseName == "__init__":
  129. # Copy and patch the package __init__.py
  130. toPy = os.path.join(buildDir, *unit.cyModName.split(".")) + ".py"
  131. pyCythonPatch(unit.fromPy, toPy,
  132. basicOnly=True)
  133. else:
  134. # Generate the .pyx
  135. pyCythonPatch(unit.fromPy, unit.toPyx)
  136. # Copy and patch the .pxd, if any
  137. if os.path.isfile(unit.fromPxd):
  138. pyCythonPatch(unit.fromPxd, unit.toPxd)
  139. def registerCythonModule(baseDir, sourceModName):
  140. global ext_modules
  141. global cythonBuildUnits
  142. modDir = os.path.join(baseDir, sourceModName)
  143. # Make path to the cython patch-build-dir
  144. patchDir = os.path.join(baseDir, "build",
  145. "cython_patched.%s-%s-%d.%d" %\
  146. (platform.system().lower(),
  147. platform.machine().lower(),
  148. sys.version_info[0], sys.version_info[1]),
  149. "%s_cython" % sourceModName
  150. )
  151. if not os.path.exists(os.path.join(baseDir, "setup.py")) or\
  152. not os.path.exists(modDir) or\
  153. not os.path.isdir(modDir):
  154. raise Exception("Wrong directory. "
  155. "Execute setup.py from within the awlsim directory.")
  156. # Walk the "awlsim" module
  157. for dirpath, dirnames, filenames in os.walk(modDir):
  158. subpath = os.path.relpath(dirpath, modDir)
  159. if subpath == baseDir:
  160. subpath = ""
  161. if subpath.startswith("gui"):
  162. continue
  163. for filename in filenames:
  164. if not filename.endswith(".py"):
  165. continue
  166. baseName = filename[:-3] # Strip .py
  167. fromPy = os.path.join(dirpath, baseName + ".py")
  168. fromPxd = os.path.join(dirpath, baseName + ".pxd.in")
  169. toDir = os.path.join(patchDir, subpath)
  170. toPyx = os.path.join(toDir, baseName + ".pyx")
  171. toPxd = os.path.join(toDir, baseName + ".pxd")
  172. # Construct the new cython module name
  173. cyModName = [ "%s_cython" % sourceModName ]
  174. if subpath:
  175. cyModName.extend(subpath.split(os.sep))
  176. cyModName.append(baseName)
  177. cyModName = ".".join(cyModName)
  178. # Remember the filenames for the build
  179. unit = CythonBuildUnit(cyModName, baseName, fromPy, fromPxd,
  180. toDir, toPyx, toPxd)
  181. cythonBuildUnits.append(unit)
  182. if baseName != "__init__":
  183. # Create a distutils Extension for the module
  184. ext_modules.append(
  185. Extension(cyModName, [toPyx])
  186. )
  187. def registerCythonModules():
  188. baseDir = os.curdir # Base directory, where setup.py lives.
  189. for filename in os.listdir(baseDir):
  190. if filename == "awlsim" or\
  191. filename.startswith("awlsimhw_"):
  192. registerCythonModule(baseDir, filename)
  193. cmdclass = {}
  194. ext_modules = []
  195. extraKeywords = {}
  196. # Try to build the Cython modules. This might fail.
  197. buildCython = True
  198. try:
  199. if int(os.getenv("NOCYTHON", "0")):
  200. print("Skipping build of CYTHON modules due to "
  201. "NOCYTHON environment variable setting.")
  202. buildCython = False
  203. except ValueError:
  204. pass
  205. if buildCython:
  206. if os.name != "posix":
  207. print("WARNING: Not building CYTHON modules on '%s' platform." %\
  208. os.name)
  209. buildCython = False
  210. if buildCython:
  211. if "bdist_wininst" in sys.argv:
  212. print("WARNING: Omitting CYTHON modules while building "
  213. "Windows installer.")
  214. buildCython = False
  215. if buildCython:
  216. try:
  217. from Cython.Distutils import build_ext as Cython_build_ext
  218. except ImportError as e:
  219. print("WARNING: Could not build the CYTHON modules: "
  220. "%s" % str(e))
  221. print("--> Is Cython installed?")
  222. buildCython = False
  223. if buildCython:
  224. try:
  225. cythonParallelBuild = int(os.getenv("CYTHONPARALLEL", "0"))
  226. except ValueError:
  227. cythonParallelBuild = 0
  228. cythonParallelBuild = bool(cythonParallelBuild == 1 or\
  229. cythonParallelBuild == sys.version_info[0])
  230. if sys.version_info[0] < 3:
  231. # Cython2 build libraries need method pickling
  232. # for parallel build.
  233. def unpickle_method(fname, obj, cls):
  234. # Ignore MRO. We don't seem to inherit methods.
  235. return cls.__dict__[fname].__get__(obj, cls)
  236. def pickle_method(m):
  237. return unpickle_method, (m.im_func.__name__,
  238. m.im_self,
  239. m.im_class)
  240. import copy_reg, types
  241. copy_reg.pickle(types.MethodType, pickle_method, unpickle_method)
  242. def cyBuildWrapper(arg):
  243. # This function does the same thing as the for-loop-body
  244. # inside of Cython's build_ext.build_extensions() method.
  245. # It is called via multiprocessing to build extensions
  246. # in parallel.
  247. # Note that this might break, if Cython's build_extensions()
  248. # is changed and stuff is added to its for loop. Meh.
  249. self, ext = arg
  250. ext.sources = self.cython_sources(ext.sources, ext)
  251. self.build_extension(ext)
  252. # Override Cython's build_ext class.
  253. class MyCythonBuildExt(Cython_build_ext):
  254. def build_extension(self, ext):
  255. assert(not ext.name.endswith("__init__"))
  256. Cython_build_ext.build_extension(self, ext)
  257. def build_extensions(self):
  258. # First patch the files, the run the build
  259. patchCythonModules(self.build_lib)
  260. if cythonParallelBuild:
  261. # Run the parallel build, yay.
  262. self.check_extensions_list(self.extensions)
  263. from multiprocessing.pool import Pool
  264. Pool().map(cyBuildWrapper,
  265. ((self, ext) for ext in self.extensions))
  266. else:
  267. # Run the normal non-parallel build.
  268. Cython_build_ext.build_extensions(self)
  269. cmdclass["build_ext"] = MyCythonBuildExt
  270. registerCythonModules()
  271. # Workaround for mbcs codec bug in distutils
  272. # http://bugs.python.org/issue10945
  273. import codecs
  274. try:
  275. codecs.lookup("mbcs")
  276. except LookupError:
  277. codecs.register(lambda name: codecs.lookup("ascii") if name == "mbcs" else None)
  278. freezeExecutables = [ ("awlsim-gui", None),
  279. ("awlsim-client", None),
  280. ("awlsim-server", None),
  281. ("awlsim-symtab", None),
  282. ("awlsim-test", None),
  283. ("awlsim/coreserver/server.py", "awlsim-server-module"), ]
  284. if py2exe:
  285. extraKeywords["console"] = [ s for s, e in freezeExecutables ]
  286. if cx_Freeze:
  287. executables = []
  288. for script, exe in freezeExecutables:
  289. if exe:
  290. if os.name.lower() in ("nt", "ce"):
  291. exe += ".exe"
  292. executables.append(Executable(script = script,
  293. targetName = exe))
  294. else:
  295. executables.append(Executable(script = script))
  296. extraKeywords["executables"] = executables
  297. extraKeywords["options"] = {
  298. "build_exe" : {
  299. "packages" : [ "awlsimhw_debug",
  300. "awlsimhw_dummy",
  301. "awlsim.library.iec", ],
  302. }
  303. }
  304. setup( name = "awlsim",
  305. version = VERSION_STRING,
  306. description = "S7 AWL/STL Soft-PLC",
  307. license = "GNU General Public License v2 or later",
  308. author = "Michael Buesch",
  309. author_email = "m@bues.ch",
  310. url = "http://bues.ch/h/awlsim",
  311. packages = [ "awlsim",
  312. "awlsim/common",
  313. "awlsim/core",
  314. "awlsim/core/instructions",
  315. "awlsim/core/systemblocks",
  316. "awlsim/coreclient",
  317. "awlsim/coreserver",
  318. "awlsim/gui",
  319. "awlsim/gui/icons",
  320. "awlsim/library",
  321. "awlsim/library/iec",
  322. "awlsimhw_debug",
  323. "awlsimhw_dummy",
  324. "awlsimhw_linuxcnc",
  325. "awlsimhw_pyprofibus",
  326. "awlsimhw_rpigpio", ],
  327. scripts = [ "awlsim-gui",
  328. "awlsim-client",
  329. "awlsim-server",
  330. "awlsim-symtab",
  331. "awlsim-test",
  332. "awlsim-linuxcnc-hal",
  333. "awlsim-win.bat", ],
  334. cmdclass = cmdclass,
  335. ext_modules = ext_modules,
  336. keywords = [ "AWL", "STL", "SPS", "PLC", "Step 7",
  337. "Siemens", "emulator", "simulator",
  338. "PROFIBUS", "LinuxCNC", ],
  339. classifiers = [
  340. "Development Status :: 4 - Beta",
  341. "Environment :: Console",
  342. "Environment :: Win32 (MS Windows)",
  343. "Environment :: X11 Applications",
  344. "Intended Audience :: Developers",
  345. "Intended Audience :: Education",
  346. "Intended Audience :: Information Technology",
  347. "Intended Audience :: Manufacturing",
  348. "Intended Audience :: Science/Research",
  349. "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)",
  350. "Operating System :: Microsoft :: Windows",
  351. "Operating System :: POSIX",
  352. "Operating System :: POSIX :: Linux",
  353. "Programming Language :: Cython",
  354. "Programming Language :: Python",
  355. "Programming Language :: Python :: 2.7",
  356. "Programming Language :: Python :: 3",
  357. "Programming Language :: Python :: Implementation :: CPython",
  358. "Programming Language :: Python :: Implementation :: PyPy",
  359. "Programming Language :: Python :: Implementation :: Jython",
  360. "Programming Language :: Python :: Implementation :: IronPython",
  361. "Topic :: Education",
  362. "Topic :: Home Automation",
  363. "Topic :: Scientific/Engineering",
  364. "Topic :: Software Development",
  365. "Topic :: Software Development :: Interpreters",
  366. "Topic :: Software Development :: Embedded Systems",
  367. "Topic :: Software Development :: Testing",
  368. "Topic :: System :: Emulators",
  369. ],
  370. long_description = open("README.txt").read(),
  371. **extraKeywords
  372. )