forked from conda/conda-build
-
Notifications
You must be signed in to change notification settings - Fork 0
/
bdist_conda.py
301 lines (252 loc) · 12.2 KB
/
bdist_conda.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
"""
bdist_conda
"""
from __future__ import print_function, division, unicode_literals
import sys
import time
from collections import defaultdict
from distutils.command.install import install
from distutils.errors import DistutilsOptionError, DistutilsGetoptError
from distutils.dist import Distribution
from conda_build.conda_interface import (StringIO, string_types, configparser, PY3,
text_type as unicode)
from conda_build.conda_interface import spec_from_line
from conda_build.metadata import MetaData
from conda_build import api
from conda_build.skeletons import pypi
from conda_build.build import handle_anaconda_upload
from conda_build.config import Config
# TODO: Add support for all the options that conda build has
class CondaDistribution(Distribution):
"""
Distribution subclass that supports bdist_conda options
This class is required if you want to pass any bdist_conda specific
options to setup(). To use, set distclass=CondaDistribution in setup().
**NOTE**: If you use setuptools, you must import setuptools before
importing distutils.commands.bdist_conda.
Options that can be passed to setup() (must include
distclass=CondaDistribution):
- conda_buildnum: The build number. Defaults to 0. Can be overridden on
the command line with the --buildnum flag.
- conda_buildstr: The build string. Default is generated automatically
from the Python version, NumPy version if relevant, and the build
number, like py34_0.
- conda_import_tests: Whether to automatically run import tests. The
default is True, which runs import tests for the all the modules in
"packages". Also allowed are False, which runs no tests, or a list of
module names to be tested on import.
- conda_command_tests: Command line tests to run. Default is True, which
runs ``command --help`` for each ``command`` in the console_scripts and
gui_scripts entry_points. Also allowed are False, which doesn't run any
command tests, or a list of command tests to run.
- conda_binary_relocation: Whether binary files should be made relocatable
(using install_name_tool on OS X or patchelf on Linux). The default is
True. See the "making packages relocatable" section in the conda build
documentation for more information on this.
- conda_preserve_egg_dir: Whether to preserve the egg directory as
installed by setuptools. The default is True if the package depends on
setuptools or has a setuptools entry_points other than console_scripts
and gui_scripts.
Command line options:
--buildnum: Set the build number. Defaults to the conda_buildnum passed to
setup(), or 0. Overrides any conda_buildnum passed to setup().
"""
# Unfortunately, there's no way to warn the users that they need to use
# distclass=CondaDistribution when they try to use a conda option to
# setup(). Distribution.__init__ will just print a warning when it sees an
# attr it doesn't recognize, and then it is discarded.
# attr: default
conda_attrs = {
'conda_buildnum': 0,
'conda_buildstr': None,
'conda_import_tests': True,
'conda_command_tests': True,
'conda_binary_relocation': True,
'conda_preserve_egg_dir': None,
'conda_features': None,
'conda_track_features': None,
}
def __init__(self, attrs=None):
given_attrs = {}
# We need to remove the attrs so that Distribution.__init__ doesn't
# warn about them.
if attrs:
for attr in self.conda_attrs:
if attr in attrs:
given_attrs[attr] = attrs.pop(attr)
if not PY3:
# Distribution is an old-style class in Python 3
Distribution.__init__(self, attrs)
else:
super().__init__(attrs)
for attr in self.conda_attrs:
setattr(self.metadata, attr, given_attrs.get(attr, self.conda_attrs[attr]))
class bdist_conda(install):
description = "create a conda package"
config = Config(build_id="bdist_conda" + "_" + str(int(time.time() * 1000)),
build_is_host=True)
def initialize_options(self):
if not PY3:
# Command is an old-style class in Python 2
install.initialize_options(self)
else:
super().initialize_options()
self.buildnum = None
self.anaconda_upload = False
def finalize_options(self):
opt_dict = self.distribution.get_option_dict('install')
if self.prefix:
raise DistutilsOptionError("--prefix is not allowed")
opt_dict['prefix'] = ("bdist_conda", self.config.host_prefix)
if not PY3:
# Command is an old-style class in Python 2
install.finalize_options(self)
else:
super().finalize_options()
def run(self):
# Make sure the metadata has the conda attributes, even if the
# distclass isn't CondaDistribution. We primarily do this to simplify
# the code below.
metadata = self.distribution.metadata
for attr in CondaDistribution.conda_attrs:
if not hasattr(metadata, attr):
setattr(metadata, attr,
CondaDistribution.conda_attrs[attr])
# The command line takes precedence
if self.buildnum is not None:
metadata.conda_buildnum = self.buildnum
d = defaultdict(dict)
# PyPI allows uppercase letters but conda does not, so we fix the
# name here.
d['package']['name'] = metadata.name.lower()
d['package']['version'] = metadata.version
d['build']['number'] = metadata.conda_buildnum
# MetaData does the auto stuff if the build string is None
d['build']['string'] = metadata.conda_buildstr
d['build']['binary_relocation'] = metadata.conda_binary_relocation
d['build']['preserve_egg_dir'] = metadata.conda_preserve_egg_dir
d['build']['features'] = metadata.conda_features
d['build']['track_features'] = metadata.conda_track_features
# XXX: I'm not really sure if it is correct to combine requires
# and install_requires
d['requirements']['run'] = d['requirements']['build'] = \
[spec_from_line(i) for i in
(metadata.requires or []) +
(getattr(self.distribution, 'install_requires', []) or
[])] + ['python']
if hasattr(self.distribution, 'tests_require'):
# A lot of packages use extras_require['test'], but
# tests_require is the one that is officially supported by
# setuptools.
d['test']['requires'] = [spec_from_line(i) for i in
self.distribution.tests_require or []]
d['about']['home'] = metadata.url
# Don't worry about classifiers. This isn't skeleton pypi. We
# don't need to make this work with random stuff in the wild. If
# someone writes their setup.py wrong and this doesn't work, it's
# their fault.
d['about']['license'] = metadata.license
d['about']['summary'] = metadata.description
# This is similar logic from conda skeleton pypi
entry_points = getattr(self.distribution, 'entry_points', [])
if entry_points:
if isinstance(entry_points, string_types):
# makes sure it is left-shifted
newstr = "\n".join(x.strip() for x in
entry_points.splitlines())
c = configparser.ConfigParser()
entry_points = {}
try:
import six
if six.PY2:
c.readfp(StringIO(newstr))
else:
c.read_file(StringIO(newstr))
except Exception as err:
# This seems to be the best error here
raise DistutilsGetoptError("ERROR: entry-points not understood: " +
str(err) + "\nThe string was" + newstr)
else:
for section in c.sections():
if section in ['console_scripts', 'gui_scripts']:
value = ['%s=%s' % (option, c.get(section, option))
for option in c.options(section)]
entry_points[section] = value
else:
# Make sure setuptools is added as a dependency below
entry_points[section] = None
if not isinstance(entry_points, dict):
raise DistutilsGetoptError("ERROR: Could not add entry points. They were:\n" +
entry_points)
else:
rs = entry_points.get('scripts', [])
cs = entry_points.get('console_scripts', [])
gs = entry_points.get('gui_scripts', [])
# We have *other* kinds of entry-points so we need
# setuptools at run-time
if not rs and not cs and not gs and len(entry_points) > 1:
d['requirements']['run'].append('setuptools')
d['requirements']['build'].append('setuptools')
entry_list = rs + cs + gs
if gs and self.config.platform == 'osx':
d['build']['osx_is_app'] = True
if len(cs + gs) != 0:
d['build']['entry_points'] = entry_list
if metadata.conda_command_tests is True:
d['test']['commands'] = list(map(unicode,
pypi.make_entry_tests(entry_list)))
if 'setuptools' in d['requirements']['run']:
d['build']['preserve_egg_dir'] = True
if metadata.conda_import_tests:
if metadata.conda_import_tests is True:
d['test']['imports'] = ((self.distribution.packages or []) +
(self.distribution.py_modules or []))
else:
d['test']['imports'] = metadata.conda_import_tests
if (metadata.conda_command_tests and not
isinstance(metadata.conda_command_tests,
bool)):
d['test']['commands'] = list(map(unicode, metadata.conda_command_tests))
d = dict(d)
self.config.keep_old_work = True
m = MetaData.fromdict(d, config=self.config)
# Shouldn't fail, but do you really trust the code above?
m.check_fields()
m.config.set_build_id = False
m.config.variant['python'] = ".".join((str(sys.version_info.major),
str(sys.version_info.minor)))
api.build(m, build_only=True, notest=True)
self.config = m.config
# prevent changes in the build ID from here, so that we're working in the same prefix
# Do the install
if not PY3:
# Command is an old-style class in Python 2
install.run(self)
else:
super().run()
output = api.build(m, post=True, notest=True)[0]
api.test(output, config=m.config)
m.config.clean()
if self.anaconda_upload:
class args:
anaconda_upload = self.anaconda_upload
handle_anaconda_upload(output, args)
else:
no_upload_message = """\
# If you want to upload this package to anaconda.org later, type:
#
# $ anaconda upload %s
""" % output
print(no_upload_message)
# Distutils looks for user_options on the class (not instance). It also
# requires that it is an instance of list. So we do this here because we want
# to keep the options from the superclass (and because I don't feel like
# making a metaclass just to make this work).
bdist_conda.user_options.extend([
(str('buildnum='), None, str('''The build number of
the conda package. Defaults to 0, or the conda_buildnum specified in the
setup() function. The command line flag overrides the option to
setup().''')),
(str('anaconda-upload'), None, ("""Upload the finished package to anaconda.org""")),
])
bdist_conda.boolean_options.extend([str('anaconda-upload')])