Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
setuptools
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jérome Perrin
setuptools
Commits
23e30bce
Commit
23e30bce
authored
Sep 29, 1999
by
Greg Ward
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
New command to generate source distribution based on a manifest file.
parent
0e1de219
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
590 additions
and
0 deletions
+590
-0
command/dist.py
command/dist.py
+590
-0
No files found.
command/dist.py
0 → 100644
View file @
23e30bce
"""distutils.command.dist
Implements the Distutils 'dist' command (create a source distribution)."""
# created 1999/09/22, Greg Ward
__rcsid__
=
"$Id$"
import
sys
,
os
,
string
,
re
import
fnmatch
from
types
import
*
from
glob
import
glob
from
distutils.core
import
Command
from
distutils.text_file
import
TextFile
# Possible modes of operation:
# - require an explicit manifest that lists every single file (presumably
# along with a way to auto-generate the manifest)
# - require an explicit manifest, but allow it to have globs or
# filename patterns of some kind (and also have auto-generation)
# - allow an explict manifest, but automatically augment it at runtime
# with the source files mentioned in 'packages', 'py_modules', and
# 'ext_modules' (and any other such things that might come along)
# I'm liking the third way. Possible gotchas:
# - redundant specification: 'packages' includes 'foo' and manifest
# includes 'foo/*.py'
# - obvious conflict: 'packages' includes 'foo' and manifest
# includes '! foo/*.py' (can't imagine why you'd want this)
# - subtle conflict: 'packages' includes 'foo' and manifest
# includes '! foo/bar.py' (this could well be desired: eg. exclude
# an experimental module from distribution)
# Syntax for the manifest file:
# - if a line is just a Unix-style glob by itself, it's a "simple include
# pattern": go find all files that match and add them to the list
# of files
# - if a line is a glob preceded by "!", then it's a "simple exclude
# pattern": go over the current list of files and exclude any that
# match the glob pattern
# - if a line consists of a directory name followed by zero or more
# glob patterns, then we'll recursively explore that directory tree
# - the glob patterns can be include (no punctuation) or exclude
# (prefixed by "!", no space)
# - if no patterns given or the first pattern is not an include pattern,
# then assume "*" -- ie. find everything (and then start applying
# the rest of the patterns)
# - the patterns are given in order of increasing precedence, ie.
# the *last* one to match a given file applies to it
#
# example (ignoring auto-augmentation!):
# distutils/*.py
# distutils/command/*.py
# ! distutils/bleeding_edge.py
# examples/*.py
# examples/README
#
# smarter way (that *will* include distutils/command/bleeding_edge.py!)
# distutils *.py
# ! distutils/bleeding_edge.py
# examples !*~ !*.py[co] (same as: examples * !*~ !*.py[co])
# test test_* *.txt !*~ !*.py[co]
# README
# setup.py
#
# The actual Distutils manifest (don't need to mention source files,
# README, setup.py -- they're automatically distributed!):
# examples !*~ !*.py[co]
# test !*~ !*.py[co]
# The algorithm that will make it work:
# files = stuff from 'packages', 'py_modules', 'ext_modules',
# plus README, setup.py, ... ?
# foreach pattern in manifest file:
# if simple-include-pattern: # "distutils/*.py"
# files.append (glob (pattern))
# elif simple-exclude-pattern: # "! distutils/foo*"
# xfiles = glob (pattern)
# remove all xfiles from files
# elif recursive-pattern: # "examples" (just a directory name)
# patterns = rest-of-words-on-line
# dir_files = list of all files under dir
# if patterns:
# if patterns[0] is an exclude-pattern:
# insert "*" at patterns[0]
# for file in dir_files:
# for dpattern in reverse (patterns):
# if file matches dpattern:
# if dpattern is an include-pattern:
# files.append (file)
# else:
# nothing, don't include it
# next file
# else:
# files.extend (dir_files) # ie. accept all of them
# Anyways, this is all implemented below -- BUT it is largely untested; I
# know it works for the simple case of distributing the Distutils, but
# haven't tried it on more complicated examples. Undoubtedly doing so will
# reveal bugs and cause delays, so I'm waiting until after I've released
# Distutils 0.1.
# Other things we need to look for in creating a source distribution:
# - make sure there's a README
# - make sure the distribution meta-info is supplied and non-empty
# (*must* have name, version, ((author and author_email) or
# (maintainer and maintainer_email)), url
#
# Frills:
# - make sure the setup script is called "setup.py"
# - make sure the README refers to "setup.py" (ie. has a line matching
# /^\s*python\s+setup\.py/)
# A crazy idea that conflicts with having/requiring 'version' in setup.py:
# - make sure there's a version number in the "main file" (main file
# is __init__.py of first package, or the first module if no packages,
# or the first extension module if no pure Python modules)
# - XXX how do we look for __version__ in an extension module?
# - XXX do we import and look for __version__? or just scan source for
# /^__version__\s*=\s*"[^"]+"/ ?
# - what about 'version_from' as an alternative to 'version' -- then
# we know just where to search for the version -- no guessing about
# what the "main file" is
class
Dist
(
Command
):
options
=
[(
'formats='
,
'f'
,
"formats for source distribution (tar, ztar, gztar, or zip)"
),
(
'manifest='
,
'm'
,
"name of manifest file"
),
]
default_format
=
{
'posix'
:
'gztar'
,
'nt'
:
'zip'
}
exclude_re
=
re
.
compile
(
r'\
s*!
\s*(\
S+)
') # for manifest lines
def set_default_options (self):
self.formats = None
self.manifest = None
def set_final_options (self):
if self.formats is None:
try:
self.formats = [self.default_format[os.name]]
except KeyError:
raise DistutilsPlatformError, \
"don'
t
know
how
to
build
source
distributions
on
" +
\
"
%
s
platform
" % os.name
elif type (self.formats) is StringType:
self.formats = string.split (self.formats, ',')
if self.manifest is None:
self.manifest = "
MANIFEST
"
def run (self):
self.check_metadata ()
self.files = []
self.find_defaults ()
self.read_manifest ()
self.make_distribution ()
def check_metadata (self):
dist = self.distribution
missing = []
for attr in ('name', 'version', 'url'):
if not (hasattr (dist, attr) and getattr (dist, attr)):
missing.append (attr)
if missing:
self.warn ("
missing
required
meta
-
data
:
" +
string.join (missing, "
,
"))
if dist.author:
if not dist.author_email:
self.warn ("
missing
meta
-
data
:
if
'author'
supplied
,
" +
"'author_email'
must
be
supplied
too
")
elif dist.maintainer:
if not dist.maintainer_email:
self.warn ("
missing
meta
-
data
:
if
'maintainer'
supplied
,
" +
"'maintainer_email'
must
be
supplied
too
")
else:
self.warn ("
missing
meta
-
data
:
either
author
(
and
author_email
)
" +
"
or
maintainer
(
and
maintainer_email
)
" +
"
must
be
supplied
")
# check_metadata ()
def find_defaults (self):
standards = ['README', 'setup.py']
for fn in standards:
if os.path.exists (fn):
self.files.append (fn)
else:
self.warn ("
standard
file
%
s
not
found
" % fn)
optional = ['test/test*.py']
for pattern in optional:
files = glob (pattern)
if files:
self.files.extend (files)
if self.distribution.packages or self.distribution.py_modules:
build_py = self.find_peer ('build_py')
build_py.ensure_ready ()
self.files.extend (build_py.get_source_files ())
if self.distribution.ext_modules:
build_ext = self.find_peer ('build_ext')
build_ext.ensure_ready ()
self.files.extend (build_ext.get_source_files ())
def open_manifest (self, filename):
return TextFile (filename,
strip_comments=1,
skip_blanks=1,
join_lines=1,
lstrip_ws=1,
rstrip_ws=1,
collapse_ws=1)
def search_dir (self, dir, patterns):
allfiles = findall (dir)
if patterns:
if patterns[0][0] == "
!
": # starts with an exclude spec?
patterns.insert (0, "
*
")# then accept anything that isn't
# explicitly excluded
act_patterns = [] # "
action
-
patterns
": (include,regexp)
# tuples where include is a boolean
for pattern in patterns:
if pattern[0] == '!':
act_patterns.append
\
((0, re.compile (fnmatch.translate (pattern[1:]))))
else:
act_patterns.append
\
((1, re.compile (fnmatch.translate (pattern))))
act_patterns.reverse()
files = []
for file in allfiles:
for (include,regexp) in act_patterns:
if regexp.match (file):
if include:
files.append (file)
break # continue to next file
else:
files = allfiles
return files
# search_dir ()
def exclude_files (self, pattern):
regexp = re.compile (fnmatch.translate (pattern))
for i in range (len (self.files)-1, -1, -1):
if regexp.match (self.files[i]):
del self.files[i]
def read_manifest (self):
# self.files had better already be defined (and hold the
# "
automatically
found
" files -- Python modules and extensions,
# README, setup script, ...)
assert self.files is not None
manifest = self.open_manifest (self.manifest)
while 1:
pattern = manifest.readline()
if pattern is None: # end of file
break
# Cases:
# 1) simple-include: "
*
.
py
", "
foo
/*
.
py
", "
doc
/*
.
html
", "
FAQ
"
# 2) simple-exclude: same, prefaced by !
# 3) recursive: multi-word line, first word a directory
exclude = self.exclude_re.match (pattern)
if exclude:
pattern = exclude.group (1)
words = string.split (pattern)
assert words # must have something!
if os.name != 'posix':
words[0] = apply (os.path.join, string.split (words[0], '/'))
# First word is a directory, possibly with include/exclude
# patterns making up the rest of the line: it's a recursive
# pattern
if os.path.isdir (words[0]):
if exclude:
file.warn ("
exclude
(
!
)
doesn
't apply to " +
"whole directory trees")
continue
dir_files = self.search_dir (words[0], words[1:])
self.files.extend (dir_files)
# Multiple words in pattern: that'
s
a
no
-
no
unless
the
first
# word is a directory name
elif
len
(
words
)
>
1
:
file
.
warn
(
"can't have multiple words unless first word "
+
"('%s') is a directory name"
%
words
[
0
])
continue
# Single word, no bang: it's a "simple include pattern"
elif
not
exclude
:
matches
=
glob
(
pattern
)
if
matches
:
self
.
files
.
extend
(
matches
)
else
:
manifest
.
warn
(
"no matches for '%s' found"
%
pattern
)
# Single word prefixed with a bang: it's a "simple exclude pattern"
else
:
if
self
.
exclude_files
(
pattern
)
==
0
:
file
.
warn
(
"no files excluded by '%s'"
%
pattern
)
# if/elif/.../else on 'pattern'
# loop over lines of 'manifest'
# read_manifest ()
def
make_release_tree
(
self
,
base_dir
,
files
):
# XXX this is Unix-specific
# First get the list of directories to create
need_dir
=
{}
for
file
in
files
:
need_dir
[
os
.
path
.
join
(
base_dir
,
os
.
path
.
dirname
(
file
))]
=
1
need_dirs
=
need_dir
.
keys
()
need_dirs
.
sort
()
# Now create them
for
dir
in
need_dirs
:
self
.
mkpath
(
dir
)
# And walk over the list of files, making a hard link for
# each one that doesn't already exist in its corresponding
# location under 'base_dir'
self
.
announce
(
"making hard links in %s..."
%
base_dir
)
for
file
in
files
:
dest
=
os
.
path
.
join
(
base_dir
,
file
)
if
not
os
.
path
.
exists
(
dest
):
self
.
execute
(
os
.
link
,
(
file
,
dest
),
"linking %s -> %s"
%
(
file
,
dest
))
# make_release_tree ()
def
make_tarball
(
self
,
base_dir
):
# XXX GNU tar 1.13 has a nifty option to add a prefix directory.
# It's pretty new, though, so we certainly can't require it -- but
# it would be nice to take advantage of it to skip the "create a
# tree of hardlinks" step!
# But I am a lazy bastard, so I require GNU tar anyways.
archive_name
=
base_dir
+
".tar.gz"
self
.
spawn
([
"tar"
,
"-czf"
,
archive_name
,
base_dir
])
def
make_zipfile
(
self
,
base_dir
):
# This assumes the Unix 'zip' utility -- it could be easily recast
# to use pkzip (or whatever the command-line zip creation utility
# on Redmond's archaic CP/M knockoff is nowadays), but I'll let
# someone who can actually test it do that.
self
.
spawn
([
"zip"
,
"-r"
,
base_dir
,
base_dir
])
def
make_distribution
(
self
):
# Don't warn about missing meta-data here -- should be done
# elsewhere.
name
=
self
.
distribution
.
name
or
"UNKNOWN"
version
=
self
.
distribution
.
version
if
version
:
base_dir
=
"%s-%s"
%
(
name
,
version
)
else
:
base_dir
=
name
# Remove any files that match "base_dir" from the fileset -- we
# don't want to go distributing the distribution inside itself!
self
.
exclude_files
(
base_dir
+
"*"
)
self
.
make_release_tree
(
base_dir
,
self
.
files
)
if
'gztar'
in
self
.
formats
:
self
.
make_tarball
(
base_dir
)
if
'zip'
in
self
.
formats
:
self
.
make_zipfile
(
base_dir
)
# class Dist
# ----------------------------------------------------------------------
# Utility functions
def
findall
(
dir
=
os
.
curdir
):
"""Find all files under 'dir' and return the sorted list of full
filenames (relative to 'dir')."""
list
=
[]
stack
=
[
dir
]
pop
=
stack
.
pop
push
=
stack
.
append
while
stack
:
dir
=
pop
()
names
=
os
.
listdir
(
dir
)
for
name
in
names
:
fullname
=
os
.
path
.
join
(
dir
,
name
)
list
.
append
(
fullname
)
if
os
.
path
.
isdir
(
fullname
)
and
not
os
.
path
.
islink
(
fullname
):
push
(
fullname
)
list
.
sort
()
return
list
# ======================================================================
# Here follows some extensive mental masturbation about how to
# make the manifest file and search algorithm even more complex.
# I think this is all gratuitous, really.
# Hmm, something extra: want to apply an exclude pattern over a whole
# subtree without necessarily having to explicitly include files from it,
# ie. it should apply after gathering files by other means (simple
# include pattern)
# . !*~ !*.bak !#*#
# and we also want to prune at certain directories:
# . !RCS !CVS
# which again should apply globally.
#
# possible solution:
# - exclude pattern in a directory applies to all files found under that
# directory
# - subdirectories that match an exclude pattern will be pruned
# - hmmm, to be consistent, subdirectories that match an include
# pattern should be recursively included
# - and this should apply to "simple" patterns too
#
# thus:
#
# examples/
#
# means get everything in examples/ and all subdirs;
#
# examples/ !*~ !#*# !*.py[co]
#
# means get everything under examples/ except files matching those three globs;
#
# ./ !RCS !CVS
#
# means get everything under current dir, but prune RCS/CVS directories;
#
# ./ !*~ !#*# !*.py[co] !RCS !CVS
# ! build/
# ! experimental/
#
# means get everything under the distribution directory except the usual
# excludes at all levels; exclude "build" and "experimental" under the
# distribution dir only.
#
# Do the former examples still work?
#
# distutils/ *.py
# ! distutils/bleeding_edge.py
#
# means all .py files recursively found under distutils, except for the one
# explicitly named.
#
# distutils/ *.py !bleeding_edge.py
#
# means the same, except bleeding_edge.py will be excluded wherever it's
# found -- thus this can exclude up to one file per directory under
# distutils.
#
# distutils/*.py
# ! distutils/bleeding_edge.py
#
# gets exactly distutils/*.py, minus the one explicitly mentioned exclude, and
#
# distutils/*.py
# distutils/ !bleeding_edge.py
#
# coincidentally does the same, but only because there can only be one file
# that matches the exclude pattern. Oh, we'd still like
#
# distutils *.py !bleeding*.py
# distutils/bleeding_ledge.py
#
# to include distutils/bleeding_ledge.py -- i.e. it should override the
# earlier exclude pattern by virtue of appearing later in the manifest. Does
# this conflict with the above requirements, ie. that "!RCS" and "!*~" should
# apply everywhere? Hmm, I think it doesn't have to, as long as we're smart
# about it. Consequence:
#
# . !RCS !CVS
# distutils *
#
# will go ahead and include RCS and CVS files under distutils, but
#
# distutils *
# . !RCS !CVS
#
# will do the right thing. Hmmm. I think that's OK, and an inevitable
# consequence of the ability to override exclusions.
# OK, new crack at the search algorithm.
#
# for pattern in manifest:
# if dir-pattern: # ie. first word is a directory (incl. "."!)
# dir = first word on line
# patterns = rest of line
# if patterns:
# for dpattern in patterns:
# if exclude-pattern:
# remove from files anything matching dpattern (including pruning
# subtrees rooted at directories that match dpattern)
# else:
# files.append (recursive_glob (dir, dpattern))
# else:
# files.append (recursive_glob (dir, '*')
#
# elif include-pattern: # it's a "simple include pattern"
# files.append (glob (pattern))
#
# else: # it's a "simple exclude pattern"
# remove from files anything matching pattern
# The two removal algorithms might be a bit tricky:
#
# "remove simple exclude pattern":
# for f in files:
# if f matches pattern:
# delete it
#
# "remove recursive exclude pattern":
# for f in files:
#
# t = tail (f)
# while t:
# if t matches pattern:
# delete current file
# continue
# t = tail (t)
#
# Well, that was an interesting mental exercise. I'm not completely
# convinced it will work, nor am I convinced this level of complexity
# is necessary. If you want to exclude RCS or CVS directories, just
# don't bloody include them!
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment