diff --git a/setuptools_conda/__main__.py b/setuptools_conda/__main__.py index 30e8c1e..656ac09 100644 --- a/setuptools_conda/__main__.py +++ b/setuptools_conda/__main__.py @@ -1,5 +1,6 @@ from pathlib import Path from subprocess import call, check_output +import shlex import sys import argparse import textwrap @@ -182,7 +183,7 @@ def main(): ) def run_conda_cmd(cmd, **kwargs): - print('[running]:', ' '.join(cmd)) + print('[running]:', *[shlex.quote(arg) for arg in cmd]) # Shell=True is necessary on Windows for calls to conda, otherwise we get # mysterious breakage. But shell=True with a list of args totally changes this # function on unix so we avoid it: @@ -191,15 +192,8 @@ def run_conda_cmd(cmd, **kwargs): sys.exit(rc) return rc - def run(cmd, **kwargs): - print('[running]:', ' '.join(cmd)) - rc = call(cmd, **kwargs) - if rc: - sys.exit(rc) - return rc - def get_output(cmd, **kwargs): - print('[running]:', ' '.join(cmd)) + print('[running]:', *[shlex.quote(arg) for arg in cmd]) return check_output(cmd, shell=WINDOWS, **kwargs).decode('utf8').strip() def getargvalue(argname, args): @@ -220,7 +214,7 @@ def getargvalue(argname, args): return arg.split(f'--{argname}=', 1)[1] def get_project_name(proj): - return get_output([sys.executable, 'setup.py', '--name'], cwd=str(proj)) + return get_output([sys.executable, *setup_py(proj), '--name'], cwd=str(proj)) def get_build_requires(proj, args): arg = 'setup-requires' @@ -253,7 +247,7 @@ def get_run_requires(proj, args, name): if requires is not None: print("Using run requirements from [dist_conda]/setup_requires") return requires - get_output([sys.executable, 'setup.py', 'egg_info'], cwd=str(proj)) + get_output([sys.executable, *setup_py(proj), 'egg_info'], cwd=str(proj)) requires_file = Path(proj, f'{name.replace("-", "_")}.egg-info', 'requires.txt') requires = requires_file.read_text().splitlines() # Ignore extras sections: @@ -336,6 +330,8 @@ def remove_projects(requirements, projects): evaluate_requirements, condify_requirement, split, + setup_py, + run, ) all_build_requires = [] @@ -370,7 +366,10 @@ def remove_projects(requirements, projects): print("\nBuilding...") proj = Path(args.projects[0]) sys.exit( - run([sys.executable, 'setup.py', 'dist_conda'] + setup_args, cwd=str(proj)) + run( + [sys.executable, *setup_py(proj), 'dist_conda'] + setup_args, + cwd=str(proj), + ) ) print("\nGetting run requirements...") diff --git a/setuptools_conda/setuptools_conda.py b/setuptools_conda/setuptools_conda.py index 50fe571..aafd1fb 100644 --- a/setuptools_conda/setuptools_conda.py +++ b/setuptools_conda/setuptools_conda.py @@ -1,7 +1,8 @@ import sys import os import shutil -from subprocess import check_call +from subprocess import call +import shlex from setuptools import Command import json from textwrap import dedent @@ -36,6 +37,38 @@ } +# Command line args that can be used in place of "setup.py" for projects that lack a +# setup.py, runs a minimal setup.py similar to what pip does for projects with no +# setup.py. +_SETUP_PY_STUB = [ + "-c", + 'import sys, setuptools; sys.argv[0] = __file__ = "setup.py"; setuptools.setup()', +] + + +def run(cmd, **kwargs): + print('[running]:', *[shlex.quote(arg) for arg in cmd]) + rc = call(cmd, **kwargs) + if rc: + sys.exit(rc) + return rc + + +def setup_py(project_dir): + """Returns a list of command line arguments to be used in place of ["setup.py"]. If + setup.py exists, then this is just ["setup.py"]. Otherwise, if setup.cfg or + pyproject.toml exists, returns args that pass a code snippet to Python with "-c" to + execute a minimal setup.py calling setuptools.setup(). If none of pyproject.toml, + setup.cfg, or setup.py exists, raises an exception.""" + if Path(project_dir, 'setup.py').exists(): + return ['setup.py'] + elif any(Path(project_dir, s).exists() for s in ['setup.cfg', 'pyproject.toml']): + return _SETUP_PY_STUB + msg = f"""{project_dir} does not look like a python project directory: contains no + setup.py, setup.cfg, or pyproject.toml""" + raise RuntimeError(' '.join(msg.split)) + + # Couldn't figure out how to use PyYAML to produce output looking like conda recipes are # usually formatted: def yaml_lines(obj, indent=2): @@ -542,19 +575,19 @@ def run(self): self.BUILD_DIR, f'{self.NAME}=={self.VERSION}', ] - check_call(cmd) + run(cmd) else: # Run sdist or bdist_wheel to make a source tarball or wheel in the recipe # dir: - cmd = [sys.executable, 'setup.py'] + cmd = [sys.executable, *setup_py('.')] if self.from_wheel: cmd += ['bdist_wheel'] else: cmd += ['sdist', '--formats=gztar'] cmd += ['--dist-dir=' + self.BUILD_DIR] - check_call(cmd) + run(cmd) if self.from_wheel or self.from_downloaded_wheel: dist = [p for p in os.listdir(self.BUILD_DIR) if p.endswith('.whl')][0] @@ -627,7 +660,7 @@ def run(self): environ = os.environ.copy() environ['CONDA_BLD_PATH'] = os.path.abspath(self.CONDA_BLD_PATH) - check_call( + run( ['conda-build', '--no-test', self.RECIPE_DIR] + channel_args, env=environ, )