13
13
import tokenize
14
14
15
15
# 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
17
20
18
21
19
22
class build_scripts (Command ):
@@ -33,7 +36,6 @@ def initialize_options(self):
33
36
self .scripts = None
34
37
self .force = None
35
38
self .executable = None
36
- self .outfiles = None
37
39
38
40
def finalize_options (self ):
39
41
self .set_undefined_options ('build' ,
@@ -51,103 +53,119 @@ def run(self):
51
53
self .copy_scripts ()
52
54
53
55
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.
58
63
"""
59
64
self .mkpath (self .build_dir )
60
65
outfiles = []
61
66
updated_files = []
62
67
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
+
153
72
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