Skip to content

Commit 3663538

Browse files
authored
Merge pull request #128 from pypa/feature/refactor-build-scripts
Refactor build scripts
2 parents b16cf40 + 7038cf2 commit 3663538

File tree

1 file changed

+114
-96
lines changed

1 file changed

+114
-96
lines changed

distutils/command/build_scripts.py

Lines changed: 114 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
import tokenize
1414

1515
# check if Python is called on the first line with this expression
16-
first_line_re = re.compile(b'^#!.*python[0-9.]*([ \t].*)?$')
16+
shebang_pattern = re.compile(b'^#!.*python[0-9.]*([ \t].*)?$')
17+
18+
# for Setuptools compatibility
19+
first_line_re = shebang_pattern
1720

1821

1922
class build_scripts(Command):
@@ -33,7 +36,6 @@ def initialize_options(self):
3336
self.scripts = None
3437
self.force = None
3538
self.executable = None
36-
self.outfiles = None
3739

3840
def finalize_options(self):
3941
self.set_undefined_options('build',
@@ -51,103 +53,119 @@ def run(self):
5153
self.copy_scripts()
5254

5355
def copy_scripts(self):
54-
r"""Copy each script listed in 'self.scripts'; if it's marked as a
55-
Python script in the Unix way (first line matches 'first_line_re',
56-
ie. starts with "\#!" and contains "python"), then adjust the first
57-
line to refer to the current Python interpreter as we copy.
56+
"""
57+
Copy each script listed in ``self.scripts``.
58+
59+
If a script is marked as a Python script (first line matches
60+
'shebang_pattern', i.e. starts with ``#!`` and contains
61+
"python"), then adjust in the copy the first line to refer to
62+
the current Python interpreter.
5863
"""
5964
self.mkpath(self.build_dir)
6065
outfiles = []
6166
updated_files = []
6267
for script in self.scripts:
63-
adjust = False
64-
script = convert_path(script)
65-
outfile = os.path.join(self.build_dir, os.path.basename(script))
66-
outfiles.append(outfile)
67-
68-
if not self.force and not newer(script, outfile):
69-
log.debug("not copying %s (up-to-date)", script)
70-
continue
71-
72-
# Always open the file, but ignore failures in dry-run mode --
73-
# that way, we'll get accurate feedback if we can read the
74-
# script.
75-
try:
76-
f = open(script, "rb")
77-
except OSError:
78-
if not self.dry_run:
79-
raise
80-
f = None
81-
else:
82-
encoding, lines = tokenize.detect_encoding(f.readline)
83-
f.seek(0)
84-
first_line = f.readline()
85-
if not first_line:
86-
self.warn("%s is an empty file (skipping)" % script)
87-
continue
88-
89-
match = first_line_re.match(first_line)
90-
if match:
91-
adjust = True
92-
post_interp = match.group(1) or b''
93-
94-
if adjust:
95-
log.info("copying and adjusting %s -> %s", script,
96-
self.build_dir)
97-
updated_files.append(outfile)
98-
if not self.dry_run:
99-
if not sysconfig.python_build:
100-
executable = self.executable
101-
else:
102-
executable = os.path.join(
103-
sysconfig.get_config_var("BINDIR"),
104-
"python%s%s" % (
105-
sysconfig.get_config_var("VERSION"),
106-
sysconfig.get_config_var("EXE")))
107-
executable = os.fsencode(executable)
108-
shebang = b"#!" + executable + post_interp + b"\n"
109-
# Python parser starts to read a script using UTF-8 until
110-
# it gets a #coding:xxx cookie. The shebang has to be the
111-
# first line of a file, the #coding:xxx cookie cannot be
112-
# written before. So the shebang has to be decodable from
113-
# UTF-8.
114-
try:
115-
shebang.decode('utf-8')
116-
except UnicodeDecodeError:
117-
raise ValueError(
118-
"The shebang ({!r}) is not decodable "
119-
"from utf-8".format(shebang))
120-
# If the script is encoded to a custom encoding (use a
121-
# #coding:xxx cookie), the shebang has to be decodable from
122-
# the script encoding too.
123-
try:
124-
shebang.decode(encoding)
125-
except UnicodeDecodeError:
126-
raise ValueError(
127-
"The shebang ({!r}) is not decodable "
128-
"from the script encoding ({})"
129-
.format(shebang, encoding))
130-
with open(outfile, "wb") as outf:
131-
outf.write(shebang)
132-
outf.writelines(f.readlines())
133-
if f:
134-
f.close()
135-
else:
136-
if f:
137-
f.close()
138-
updated_files.append(outfile)
139-
self.copy_file(script, outfile)
140-
141-
if os.name == 'posix':
142-
for file in outfiles:
143-
if self.dry_run:
144-
log.info("changing mode of %s", file)
145-
else:
146-
oldmode = os.stat(file)[ST_MODE] & 0o7777
147-
newmode = (oldmode | 0o555) & 0o7777
148-
if newmode != oldmode:
149-
log.info("changing mode of %s from %o to %o",
150-
file, oldmode, newmode)
151-
os.chmod(file, newmode)
152-
# XXX should we modify self.outfiles?
68+
self._copy_script(script, outfiles, updated_files)
69+
70+
self._change_modes(outfiles)
71+
15372
return outfiles, updated_files
73+
74+
def _copy_script(self, script, outfiles, updated_files):
75+
shebang_match = None
76+
script = convert_path(script)
77+
outfile = os.path.join(self.build_dir, os.path.basename(script))
78+
outfiles.append(outfile)
79+
80+
if not self.force and not newer(script, outfile):
81+
log.debug("not copying %s (up-to-date)", script)
82+
return
83+
84+
# Always open the file, but ignore failures in dry-run mode
85+
# in order to attempt to copy directly.
86+
try:
87+
f = open(script, "rb")
88+
except OSError:
89+
if not self.dry_run:
90+
raise
91+
f = None
92+
else:
93+
encoding, lines = tokenize.detect_encoding(f.readline)
94+
f.seek(0)
95+
first_line = f.readline()
96+
if not first_line:
97+
self.warn("%s is an empty file (skipping)" % script)
98+
return
99+
100+
shebang_match = shebang_pattern.match(first_line)
101+
102+
updated_files.append(outfile)
103+
if shebang_match:
104+
log.info("copying and adjusting %s -> %s", script,
105+
self.build_dir)
106+
if not self.dry_run:
107+
if not sysconfig.python_build:
108+
executable = self.executable
109+
else:
110+
executable = os.path.join(
111+
sysconfig.get_config_var("BINDIR"),
112+
"python%s%s" % (
113+
sysconfig.get_config_var("VERSION"),
114+
sysconfig.get_config_var("EXE")))
115+
executable = os.fsencode(executable)
116+
post_interp = shebang_match.group(1) or b''
117+
shebang = b"#!" + executable + post_interp + b"\n"
118+
self._validate_shebang(shebang, encoding)
119+
with open(outfile, "wb") as outf:
120+
outf.write(shebang)
121+
outf.writelines(f.readlines())
122+
if f:
123+
f.close()
124+
else:
125+
if f:
126+
f.close()
127+
self.copy_file(script, outfile)
128+
129+
def _change_modes(self, outfiles):
130+
if os.name != 'posix':
131+
return
132+
133+
for file in outfiles:
134+
self._change_mode(file)
135+
136+
def _change_mode(self, file):
137+
if self.dry_run:
138+
log.info("changing mode of %s", file)
139+
return
140+
141+
oldmode = os.stat(file)[ST_MODE] & 0o7777
142+
newmode = (oldmode | 0o555) & 0o7777
143+
if newmode != oldmode:
144+
log.info("changing mode of %s from %o to %o",
145+
file, oldmode, newmode)
146+
os.chmod(file, newmode)
147+
148+
@staticmethod
149+
def _validate_shebang(shebang, encoding):
150+
# Python parser starts to read a script using UTF-8 until
151+
# it gets a #coding:xxx cookie. The shebang has to be the
152+
# first line of a file, the #coding:xxx cookie cannot be
153+
# written before. So the shebang has to be decodable from
154+
# UTF-8.
155+
try:
156+
shebang.decode('utf-8')
157+
except UnicodeDecodeError:
158+
raise ValueError(
159+
"The shebang ({!r}) is not decodable "
160+
"from utf-8".format(shebang))
161+
162+
# If the script is encoded to a custom encoding (use a
163+
# #coding:xxx cookie), the shebang has to be decodable from
164+
# the script encoding too.
165+
try:
166+
shebang.decode(encoding)
167+
except UnicodeDecodeError:
168+
raise ValueError(
169+
"The shebang ({!r}) is not decodable "
170+
"from the script encoding ({})"
171+
.format(shebang, encoding))

0 commit comments

Comments
 (0)