##// END OF EJS Templates
Merge pull request #12623 from farisachugthai/setupbase
Matthias Bussonnier -
r26144:2b4bc75a merge
parent child Browse files
Show More
@@ -1,407 +1,408 b''
1 1 # encoding: utf-8
2 2 """
3 3 This module defines the things that are used in setup.py for building IPython
4 4
5 5 This includes:
6 6
7 7 * The basic arguments to setup
8 8 * Functions for finding things like packages, package data, etc.
9 9 * A function for checking dependencies.
10 10 """
11 11
12 12 # Copyright (c) IPython Development Team.
13 13 # Distributed under the terms of the Modified BSD License.
14 14
15
16 import re
17 15 import os
16 import re
18 17 import sys
18 from glob import glob
19 from logging import log
20
21 from setuptools import Command
22 from setuptools.command.build_py import build_py
19 23
20 from distutils import log
21 from distutils.command.build_py import build_py
24 # TODO: Replacement for this?
22 25 from distutils.command.build_scripts import build_scripts
23 from distutils.command.install import install
24 from distutils.command.install_scripts import install_scripts
25 from distutils.cmd import Command
26 from glob import glob
26 from setuptools.command.install import install
27 from setuptools.command.install_scripts import install_scripts
27 28
28 29 from setupext import install_data_ext
29 30
30 31 #-------------------------------------------------------------------------------
31 32 # Useful globals and utility functions
32 33 #-------------------------------------------------------------------------------
33 34
34 35 # A few handy globals
35 36 isfile = os.path.isfile
36 37 pjoin = os.path.join
37 38 repo_root = os.path.dirname(os.path.abspath(__file__))
38 39
39 40 def execfile(fname, globs, locs=None):
40 41 locs = locs or globs
41 42 with open(fname) as f:
42 43 exec(compile(f.read(), fname, "exec"), globs, locs)
43 44
44 45 # A little utility we'll need below, since glob() does NOT allow you to do
45 46 # exclusion on multiple endings!
46 47 def file_doesnt_endwith(test,endings):
47 48 """Return true if test is a file and its name does NOT end with any
48 49 of the strings listed in endings."""
49 50 if not isfile(test):
50 51 return False
51 52 for e in endings:
52 53 if test.endswith(e):
53 54 return False
54 55 return True
55 56
56 57 #---------------------------------------------------------------------------
57 58 # Basic project information
58 59 #---------------------------------------------------------------------------
59 60
60 61 # release.py contains version, authors, license, url, keywords, etc.
61 62 execfile(pjoin(repo_root, 'IPython','core','release.py'), globals())
62 63
63 64 # Create a dict with the basic information
64 65 # This dict is eventually passed to setup after additional keys are added.
65 66 setup_args = dict(
66 67 name = name,
67 68 version = version,
68 69 description = description,
69 70 long_description = long_description,
70 71 author = author,
71 72 author_email = author_email,
72 73 url = url,
73 74 license = license,
74 75 platforms = platforms,
75 76 keywords = keywords,
76 77 classifiers = classifiers,
77 78 cmdclass = {'install_data': install_data_ext},
78 79 project_urls={
79 80 'Documentation': 'https://ipython.readthedocs.io/',
80 81 'Funding' : 'https://numfocus.org/',
81 82 'Source' : 'https://github.com/ipython/ipython',
82 83 'Tracker' : 'https://github.com/ipython/ipython/issues',
83 84 }
84 85 )
85 86
86 87
87 88 #---------------------------------------------------------------------------
88 89 # Find packages
89 90 #---------------------------------------------------------------------------
90 91
91 92 def find_packages():
92 93 """
93 94 Find all of IPython's packages.
94 95 """
95 96 excludes = ['deathrow', 'quarantine']
96 97 packages = []
97 98 for dir,subdirs,files in os.walk('IPython'):
98 99 package = dir.replace(os.path.sep, '.')
99 100 if any(package.startswith('IPython.'+exc) for exc in excludes):
100 101 # package is to be excluded (e.g. deathrow)
101 102 continue
102 103 if '__init__.py' not in files:
103 104 # not a package
104 105 continue
105 106 packages.append(package)
106 107 return packages
107 108
108 109 #---------------------------------------------------------------------------
109 110 # Find package data
110 111 #---------------------------------------------------------------------------
111 112
112 113 def find_package_data():
113 114 """
114 115 Find IPython's package_data.
115 116 """
116 117 # This is not enough for these things to appear in an sdist.
117 118 # We need to muck with the MANIFEST to get this to work
118
119
119 120 package_data = {
120 121 'IPython.core' : ['profile/README*'],
121 122 'IPython.core.tests' : ['*.png', '*.jpg', 'daft_extension/*.py'],
122 123 'IPython.lib.tests' : ['*.wav'],
123 124 'IPython.testing.plugin' : ['*.txt'],
124 125 }
125
126
126 127 return package_data
127 128
128 129
129 130 def check_package_data(package_data):
130 131 """verify that package_data globs make sense"""
131 132 print("checking package data")
132 133 for pkg, data in package_data.items():
133 134 pkg_root = pjoin(*pkg.split('.'))
134 135 for d in data:
135 136 path = pjoin(pkg_root, d)
136 137 if '*' in path:
137 138 assert len(glob(path)) > 0, "No files match pattern %s" % path
138 139 else:
139 140 assert os.path.exists(path), "Missing package data: %s" % path
140 141
141 142
142 143 def check_package_data_first(command):
143 144 """decorator for checking package_data before running a given command
144 145
145 146 Probably only needs to wrap build_py
146 147 """
147 148 class DecoratedCommand(command):
148 149 def run(self):
149 150 check_package_data(self.package_data)
150 151 command.run(self)
151 152 return DecoratedCommand
152 153
153 154
154 155 #---------------------------------------------------------------------------
155 156 # Find data files
156 157 #---------------------------------------------------------------------------
157 158
158 159 def find_data_files():
159 160 """
160 161 Find IPython's data_files.
161 162
162 163 Just man pages at this point.
163 164 """
164 165
165 166 if "freebsd" in sys.platform:
166 167 manpagebase = pjoin('man', 'man1')
167 168 else:
168 169 manpagebase = pjoin('share', 'man', 'man1')
169 170
170 171 # Simple file lists can be made by hand
171 172 manpages = [f for f in glob(pjoin('docs','man','*.1.gz')) if isfile(f)]
172 173 if not manpages:
173 174 # When running from a source tree, the manpages aren't gzipped
174 175 manpages = [f for f in glob(pjoin('docs','man','*.1')) if isfile(f)]
175 176
176 177 # And assemble the entire output list
177 178 data_files = [ (manpagebase, manpages) ]
178 179
179 180 return data_files
180 181
181 182
182 183 # The two functions below are copied from IPython.utils.path, so we don't need
183 184 # to import IPython during setup, which fails on Python 3.
184 185
185 186 def target_outdated(target,deps):
186 187 """Determine whether a target is out of date.
187 188
188 189 target_outdated(target,deps) -> 1/0
189 190
190 191 deps: list of filenames which MUST exist.
191 192 target: single filename which may or may not exist.
192 193
193 194 If target doesn't exist or is older than any file listed in deps, return
194 195 true, otherwise return false.
195 196 """
196 197 try:
197 198 target_time = os.path.getmtime(target)
198 199 except os.error:
199 200 return 1
200 201 for dep in deps:
201 202 dep_time = os.path.getmtime(dep)
202 203 if dep_time > target_time:
203 204 #print "For target",target,"Dep failed:",dep # dbg
204 205 #print "times (dep,tar):",dep_time,target_time # dbg
205 206 return 1
206 207 return 0
207 208
208 209
209 210 def target_update(target,deps,cmd):
210 211 """Update a target with a given command given a list of dependencies.
211 212
212 213 target_update(target,deps,cmd) -> runs cmd if target is outdated.
213 214
214 215 This is just a wrapper around target_outdated() which calls the given
215 216 command if target is outdated."""
216 217
217 218 if target_outdated(target,deps):
218 219 os.system(cmd)
219 220
220 221 #---------------------------------------------------------------------------
221 222 # Find scripts
222 223 #---------------------------------------------------------------------------
223 224
224 225 def find_entry_points():
225 226 """Defines the command line entry points for IPython
226 227
227 228 This always uses setuptools-style entry points. When setuptools is not in
228 229 use, our own build_scripts_entrypt class below parses these and builds
229 230 command line scripts.
230 231
231 232 Each of our entry points gets both a plain name, e.g. ipython, and one
232 suffixed with the Python major version number, e.g. ipython3.
233 suffixed with the Python major version number, e.g. ipython3.
233 234 """
234 235 ep = [
235 236 'ipython%s = IPython:start_ipython',
236 237 'iptest%s = IPython.testing.iptestcontroller:main',
237 238 ]
238 239 suffix = str(sys.version_info[0])
239 240 return [e % '' for e in ep] + [e % suffix for e in ep]
240 241
241 242 script_src = """#!{executable}
242 243 # This script was automatically generated by setup.py
243 244 if __name__ == '__main__':
244 245 from {mod} import {func}
245 246 {func}()
246 247 """
247 248
248 249 class build_scripts_entrypt(build_scripts):
249 250 """Build the command line scripts
250
251
251 252 Parse setuptools style entry points and write simple scripts to run the
252 253 target functions.
253
254
254 255 On Windows, this also creates .cmd wrappers for the scripts so that you can
255 256 easily launch them from a command line.
256 257 """
257 258 def run(self):
258 259 self.mkpath(self.build_dir)
259 260 outfiles = []
260 261 for script in find_entry_points():
261 262 name, entrypt = script.split('=')
262 263 name = name.strip()
263 264 entrypt = entrypt.strip()
264 265 outfile = os.path.join(self.build_dir, name)
265 266 outfiles.append(outfile)
266 267 print('Writing script to', outfile)
267 268
268 269 mod, func = entrypt.split(':')
269 270 with open(outfile, 'w') as f:
270 271 f.write(script_src.format(executable=sys.executable,
271 272 mod=mod, func=func))
272
273
273 274 if sys.platform == 'win32':
274 275 # Write .cmd wrappers for Windows so 'ipython' etc. work at the
275 276 # command line
276 277 cmd_file = os.path.join(self.build_dir, name + '.cmd')
277 278 cmd = r'@"{python}" "%~dp0\{script}" %*\r\n'.format(
278 279 python=sys.executable, script=name)
279 280 log.info("Writing %s wrapper script" % cmd_file)
280 281 with open(cmd_file, 'w') as f:
281 282 f.write(cmd)
282 283
283 284 return outfiles, outfiles
284 285
285 286 class install_lib_symlink(Command):
286 287 user_options = [
287 288 ('install-dir=', 'd', "directory to install to"),
288 289 ]
289 290
290 291 def initialize_options(self):
291 292 self.install_dir = None
292 293
293 294 def finalize_options(self):
294 295 self.set_undefined_options('symlink',
295 296 ('install_lib', 'install_dir'),
296 297 )
297 298
298 299 def run(self):
299 300 if sys.platform == 'win32':
300 301 raise Exception("This doesn't work on Windows.")
301 302 pkg = os.path.join(os.getcwd(), 'IPython')
302 303 dest = os.path.join(self.install_dir, 'IPython')
303 304 if os.path.islink(dest):
304 305 print('removing existing symlink at %s' % dest)
305 306 os.unlink(dest)
306 307 print('symlinking %s -> %s' % (pkg, dest))
307 308 os.symlink(pkg, dest)
308 309
309 310 class unsymlink(install):
310 311 def run(self):
311 312 dest = os.path.join(self.install_lib, 'IPython')
312 313 if os.path.islink(dest):
313 314 print('removing symlink at %s' % dest)
314 315 os.unlink(dest)
315 316 else:
316 317 print('No symlink exists at %s' % dest)
317 318
318 319 class install_symlinked(install):
319 320 def run(self):
320 321 if sys.platform == 'win32':
321 322 raise Exception("This doesn't work on Windows.")
322 323
323 324 # Run all sub-commands (at least those that need to be run)
324 325 for cmd_name in self.get_sub_commands():
325 326 self.run_command(cmd_name)
326
327
327 328 # 'sub_commands': a list of commands this command might have to run to
328 329 # get its work done. See cmd.py for more info.
329 330 sub_commands = [('install_lib_symlink', lambda self:True),
330 331 ('install_scripts_sym', lambda self:True),
331 332 ]
332 333
333 334 class install_scripts_for_symlink(install_scripts):
334 335 """Redefined to get options from 'symlink' instead of 'install'.
335
336
336 337 I love distutils almost as much as I love setuptools.
337 338 """
338 339 def finalize_options(self):
339 340 self.set_undefined_options('build', ('build_scripts', 'build_dir'))
340 341 self.set_undefined_options('symlink',
341 342 ('install_scripts', 'install_dir'),
342 343 ('force', 'force'),
343 344 ('skip_build', 'skip_build'),
344 345 )
345 346
346 347
347 348 #---------------------------------------------------------------------------
348 349 # VCS related
349 350 #---------------------------------------------------------------------------
350 351
351 352
352 353 def git_prebuild(pkg_dir, build_cmd=build_py):
353 354 """Return extended build or sdist command class for recording commit
354 355
355 356 records git commit in IPython.utils._sysinfo.commit
356 357
357 358 for use in IPython.utils.sysinfo.sys_info() calls after installation.
358 359 """
359
360
360 361 class MyBuildPy(build_cmd):
361 362 ''' Subclass to write commit data into installation tree '''
362 363 def run(self):
363 364 # loose as `.dev` is suppose to be invalid
364 365 print("check version number")
365 366 loose_pep440re = re.compile(r'^(\d+)\.(\d+)\.(\d+((a|b|rc)\d+)?)(\.post\d+)?(\.dev\d*)?$')
366 367 if not loose_pep440re.match(version):
367 368 raise ValueError("Version number '%s' is not valid (should match [N!]N(.N)*[{a|b|rc}N][.postN][.devN])" % version)
368 369
369 370
370 371 build_cmd.run(self)
371 372 # this one will only fire for build commands
372 373 if hasattr(self, 'build_lib'):
373 374 self._record_commit(self.build_lib)
374
375
375 376 def make_release_tree(self, base_dir, files):
376 377 # this one will fire for sdist
377 378 build_cmd.make_release_tree(self, base_dir, files)
378 379 self._record_commit(base_dir)
379
380
380 381 def _record_commit(self, base_dir):
381 382 import subprocess
382 383 proc = subprocess.Popen('git rev-parse --short HEAD',
383 384 stdout=subprocess.PIPE,
384 385 stderr=subprocess.PIPE,
385 386 shell=True)
386 387 repo_commit, _ = proc.communicate()
387 388 repo_commit = repo_commit.strip().decode("ascii")
388
389
389 390 out_pth = pjoin(base_dir, pkg_dir, 'utils', '_sysinfo.py')
390 391 if os.path.isfile(out_pth) and not repo_commit:
391 392 # nothing to write, don't clobber
392 393 return
393
394
394 395 print("writing git commit '%s' to %s" % (repo_commit, out_pth))
395
396
396 397 # remove to avoid overwriting original via hard link
397 398 try:
398 399 os.remove(out_pth)
399 400 except (IOError, OSError):
400 401 pass
401 402 with open(out_pth, 'w') as out_file:
402 403 out_file.writelines([
403 404 '# GENERATED BY setup.py\n',
404 405 'commit = u"%s"\n' % repo_commit,
405 406 ])
406 407 return MyBuildPy
407 408
General Comments 0
You need to be logged in to leave comments. Login now