# Python command line tool for compiling py files to exec files on Mac

When I used to use a PC I was familiar with py2exe, the mac equivalent I saw most people talking about was py2app — which compiles python to .app files, not .exec files. I was familiar with compiling C files to execs on mac, and I had heard of cython (but I had never used it before), so I figured I could write something to go from python to cython to an exec. I initially did not write this for anyone else to use — but now I'm thinking about packaging it as a pip module.

This is my first time building a command line tool for executing shell commands and messing with files and directories.

"""(PYCX) PYthon to Cython to eXec, a unix command line util
Usage:
pycx FILES... [-o DIR --show --delete --run]
pycx --help

Options:
FILES              one or more python files to compile
-o --output=DIR    output directory
-s --show          show output from exec compiling
-d --delete        delete the c file after compiling exec
-r --run           run the exec after compiling
-h --help          show this screen.
"""
import os, re
from docopt import docopt

args = docopt(__doc__)

# the two pathnames below tell gcc where python is so that cython can be compiled to an exec
INCLUDES = '/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/include/python3.7m'
LIBRARY  = '/usr/local/Cellar/python/3.7.2_2/Frameworks/Python.framework/Versions/3.7/lib'
HIDEDATA = '&>/dev/null'  # this is used to hide output while compiling C files

for pyFILE in args['FILES']:
if pyFILE.endswith('.py'):  # file must be a python file
path, name = os.path.split(pyFILE)  # split full path to seperate path & filename

cFILE  = re.sub('\.py$', '.c', pyFILE) # name of the file with .c extension & path FILE = re.sub('\.py$', '', name)  # name of the file with no extension or path

PATH   = path + '/' if path is not '' else '.'  # if in current directory, path = '.'
OUTPUT = args['--output'] + '/' if args['--output'] else ''  # blank if no arg given
SHOW   = HIDEDATA if not args['--show'] else ''  # will hide gcc output if SHOW is false
# this command will be used to delete the C file
DELETE = f'find {PATH} -name "{FILE}.c" -type f|xargs rm -f' if args['--delete'] else ''
RUN    = f'./{OUTPUT}{FILE}' if args['--run'] else ''  # command to run the exec

commands = [  # cython to make C file, gcc to compile to exec, and some options
f"cython --embed -o {cFILE} {pyFILE}",  # convert python to cython C file
# compile cython C file to exec file
f"gcc -v -Os -I {INCLUDES} -L {LIBRARY} {cFILE} -o {OUTPUT}{FILE} " + \
# source python & other options -- hide or show, delete C file, run exec
f"-lpython3.7 -lpthread -lm -lutil -ldl {SHOW}", f"{DELETE}", f"{RUN}"
]
for command in commands:
os.system(command)  # execute commands above, excluding blank commands
else:
print(__doc__)  # show the help menu if user doesn't put a python file



Besides modifying INCLUDES and LIBRARY to work with varying path locations, are there any other major problems preventing this from working as a python module? Does this even seem like a tool some people might use?

I recommend that you read PEP 8.

• It's advised against doing import os, re. Instead have one line dedicated to each import.
• You should have an empty line between importing third party libraries and standard library ones.
• In Python it's standard to use snake_case.
• You should only have one space either side of the equals sign when performing assignment.
• Rather than doing path is not '' you should use the != operator.
• '\.py$' is deprecated in Python 3.6, and will become a syntax error in a future version of Python. Instead use r'\.py$'.
• Don't mix ' and " string delimiters. Unless it's to do "'".
• You don't need to use a \ for line breaking as your within brackets.
• You should have the + in front of the next line, rather than the end of the previous line.

• Your variable names are crap. path, name FILE, cFILE, PATH. The reason you need your comments is because of this.
• Don't make a list to then call os.system just call it straight away.
• Use pathlib, it singlehandedly makes your code ridiculously simple.
• Put your code in an if __name__ == '__main__' guard. You don't want to destroy things by accident.

untested:

"""(PYCX) PYthon to Cython to eXec, a unix command line util
Usage:
pycx FILES... [-o DIR --show --delete --run]
pycx --help

Options:
FILES              one or more python files to compile
-o --output=DIR    output directory
-s --show          show output from exec compiling
-d --delete        delete the c file after compiling exec
-r --run           run the exec after compiling
-h --help          show this screen.
"""
import os
from pathlib import Path

from docopt import docopt

INCLUDES = '/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/include/python3.7m'
LIBRARY = '/usr/local/Cellar/python/3.7.2_2/Frameworks/Python.framework/Versions/3.7/lib'
HIDEDATA = '&>/dev/null'

def main(args):
for path in args['FILES']:
path = Path(path)
if path.suffix != '.py':
print(__doc__)
continue

output = Path(args['--output'] or '') / path.stem
c_file = path.parent / path.stem + '.c'

os.system(f'cython --embed -o {c_file} {path}')
os.system(
f'gcc -v -Os -I {INCLUDES} -L {LIBRARY} {c_file} '
f'-o {output} -lpython3.7 -lpthread -lm -lutil -ldl '
+ HIDEDATA if not args['--show'] else ''
)

if args['--delete']:
os.system(
f'find {path.parent} -name "{path.stem}.c" -type f'
f'|xargs rm -f'
)

if args['--run']:
os.system(f'{output}')

if __name__ == '__main__':
main(docopt(__doc__))

• you should use the != operator Eh, not really. I posted a small answer on that in particular. Otherwise this seems sane. – Reinderien Mar 30 at 20:46
• Would c_file = path.parent / (path.stem + '.c') do something different then what you have? It seems like this works better. Thanks for your valuable tips! – Shui Mar 31 at 0:21
• @Reinderien I'd like to point out that that is only talking about the is operator in that line, not the turnery logic too. Hence why it's in the pep8 section of my answer and changed to use pathlib and an or in my code. – Peilonrayz Mar 31 at 12:12

This code is obviated by @Peilonrayz, which has posted a good solution. However, in the future, if you need to do something like

PATH = path + '/' if path is not '' else '.'


you're better off to do

PATH = (path or '.') + '/'


Otherwise, the other suggestion is complete.

• Yes! After editing my code based on his suggestions I was beginning to realize I can do this variable assignments, I'm glad I know this now. Thanks for the tip! – Shui Mar 31 at 0:17