@@ -6,16 +6,42 @@ const set = require('lodash.set');
6
6
const { spawnSync } = require ( 'child_process' ) ;
7
7
const { quote } = require ( 'shell-quote' ) ;
8
8
const { buildImage, getBindPath, getDockerUid } = require ( './docker' ) ;
9
- const { getStripCommand, deleteFiles } = require ( './slim' ) ;
9
+ const { getStripCommand, getStripMode , deleteFiles } = require ( './slim' ) ;
10
10
const {
11
11
checkForAndDeleteMaxCacheVersions,
12
12
md5Path,
13
13
getRequirementsWorkingPath,
14
14
getUserCachePath
15
15
} = require ( './shared' ) ;
16
16
17
- function quote_single ( quoteme ) {
18
- return quote ( [ quoteme ] ) ;
17
+ /**
18
+ * Omit empty commands.
19
+ * In this context, a "command" is a list of arguments. An empty list or falsy value is ommitted.
20
+ * @param {string[][] } many commands to merge.
21
+ * @return {string[][] } a list of valid commands.
22
+ */
23
+ function filterCommands ( commands ) {
24
+ return commands . filter ( ( cmd ) => Boolean ( cmd ) && cmd . length > 0 ) ;
25
+ }
26
+
27
+ /**
28
+ * Render zero or more commands as a single command for a Unix environment.
29
+ * In this context, a "command" is a list of arguments. An empty list or falsy value is ommitted.
30
+ *
31
+ * @param {string[][] } many commands to merge.
32
+ * @return {string[] } a single list of words.
33
+ */
34
+ function mergeCommands ( commands ) {
35
+ const cmds = filterCommands ( commands ) ;
36
+ if ( cmds . length === 0 ) {
37
+ throw new Error ( 'Expected at least one non-empty command' )
38
+ } else if ( cmds . length === 1 ) {
39
+ return cmds [ 0 ] ;
40
+ } else {
41
+ // Quote the arguments in each command and join them all using &&.
42
+ const script = cmds . map ( quote ) . join ( ' && ' ) ;
43
+ return [ "/bin/sh" , "-c" , script ] ;
44
+ }
19
45
}
20
46
21
47
/**
@@ -51,6 +77,25 @@ function installRequirementsFile(
51
77
}
52
78
}
53
79
80
+ function pipAcceptsSystem ( pythonBin ) {
81
+ // Check if pip has Debian's --system option and set it if so
82
+ const pipTestRes = spawnSync ( pythonBin , [
83
+ '-m' ,
84
+ 'pip' ,
85
+ 'help' ,
86
+ 'install'
87
+ ] ) ;
88
+ if ( pipTestRes . error ) {
89
+ if ( pipTestRes . error . code === 'ENOENT' ) {
90
+ throw new Error (
91
+ `${ pythonBin } not found! Try the pythonBin option.`
92
+ ) ;
93
+ }
94
+ throw pipTestRes . error ;
95
+ }
96
+ return pipTestRes . stdout . toString ( ) . indexOf ( '--system' ) >= 0 ;
97
+ }
98
+
54
99
/**
55
100
* Install requirements described from requirements in the targetFolder into that same targetFolder
56
101
* @param {string } targetFolder
@@ -65,15 +110,16 @@ function installRequirements(targetFolder, serverless, options) {
65
110
`Installing requirements from ${ targetRequirementsTxt } ...`
66
111
) ;
67
112
68
- let cmd ;
69
- let cmdOptions ;
70
- let pipCmd = [
113
+ const dockerCmd = [ ] ;
114
+ const pipCmd = [
71
115
options . pythonBin ,
72
116
'-m' ,
73
117
'pip' ,
74
118
'install' ,
75
119
...options . pipCmdExtraArgs
76
120
] ;
121
+ const pipCmds = [ pipCmd ] ;
122
+ const postCmds = [ ] ;
77
123
// Check if we're using the legacy --cache-dir command...
78
124
if ( options . pipCmdExtraArgs . indexOf ( '--cache-dir' ) > - 1 ) {
79
125
if ( options . dockerizePip ) {
@@ -94,8 +140,8 @@ function installRequirements(targetFolder, serverless, options) {
94
140
95
141
if ( ! options . dockerizePip ) {
96
142
// Push our local OS-specific paths for requirements and target directory
97
- pipCmd . push ( '-t' , dockerPathForWin ( options , targetFolder ) ) ;
98
- pipCmd . push ( '-r' , dockerPathForWin ( options , targetRequirementsTxt ) ) ;
143
+ pipCmd . push ( '-t' , dockerPathForWin ( targetFolder ) ,
144
+ '-r' , dockerPathForWin ( targetRequirementsTxt ) ) ;
99
145
// If we want a download cache...
100
146
if ( options . useDownloadCache ) {
101
147
const downloadCacheDir = path . join (
@@ -104,35 +150,17 @@ function installRequirements(targetFolder, serverless, options) {
104
150
) ;
105
151
serverless . cli . log ( `Using download cache directory ${ downloadCacheDir } ` ) ;
106
152
fse . ensureDirSync ( downloadCacheDir ) ;
107
- pipCmd . push ( '--cache-dir' , quote_single ( downloadCacheDir ) ) ;
153
+ pipCmd . push ( '--cache-dir' , downloadCacheDir ) ;
108
154
}
109
155
110
- // Check if pip has Debian's --system option and set it if so
111
- const pipTestRes = spawnSync ( options . pythonBin , [
112
- '-m' ,
113
- 'pip' ,
114
- 'help' ,
115
- 'install'
116
- ] ) ;
117
- if ( pipTestRes . error ) {
118
- if ( pipTestRes . error . code === 'ENOENT' ) {
119
- throw new Error (
120
- `${ options . pythonBin } not found! ` + 'Try the pythonBin option.'
121
- ) ;
122
- }
123
- throw pipTestRes . error ;
124
- }
125
- if ( pipTestRes . stdout . toString ( ) . indexOf ( '--system' ) >= 0 ) {
156
+ if ( pipAcceptsSystem ( options . pythonBin ) ) {
126
157
pipCmd . push ( '--system' ) ;
127
158
}
128
159
}
129
160
// If we are dockerizing pip
130
161
if ( options . dockerizePip ) {
131
- cmd = 'docker' ;
132
-
133
162
// Push docker-specific paths for requirements and target directory
134
- pipCmd . push ( '-t' , '/var/task/' ) ;
135
- pipCmd . push ( '-r' , '/var/task/requirements.txt' ) ;
163
+ pipCmd . push ( '-t' , '/var/task/' , '-r' , '/var/task/requirements.txt' ) ;
136
164
137
165
// Build docker image if required
138
166
let dockerImage ;
@@ -148,28 +176,18 @@ function installRequirements(targetFolder, serverless, options) {
148
176
149
177
// Prepare bind path depending on os platform
150
178
const bindPath = dockerPathForWin (
151
- options ,
152
179
getBindPath ( serverless , targetFolder )
153
180
) ;
154
181
155
- cmdOptions = [ 'run' , '--rm' , '-v' , `${ bindPath } :/var/task:z` ] ;
182
+ dockerCmd . push ( 'docker' , 'run' , '--rm' , '-v' , `${ bindPath } :/var/task:z` ) ;
156
183
if ( options . dockerSsh ) {
157
184
// Mount necessary ssh files to work with private repos
158
- cmdOptions . push (
159
- '-v' ,
160
- quote_single ( `${ process . env . HOME } /.ssh/id_rsa:/root/.ssh/id_rsa:z` )
161
- ) ;
162
- cmdOptions . push (
163
- '-v' ,
164
- quote_single (
165
- `${ process . env . HOME } /.ssh/known_hosts:/root/.ssh/known_hosts:z`
166
- )
167
- ) ;
168
- cmdOptions . push (
169
- '-v' ,
170
- quote_single ( `${ process . env . SSH_AUTH_SOCK } :/tmp/ssh_sock:z` )
185
+ dockerCmd . push (
186
+ '-v' , `${ process . env . HOME } /.ssh/id_rsa:/root/.ssh/id_rsa:z` ,
187
+ '-v' , `${ process . env . HOME } /.ssh/known_hosts:/root/.ssh/known_hosts:z` ,
188
+ '-v' , `${ process . env . SSH_AUTH_SOCK } :/tmp/ssh_sock:z` ,
189
+ '-e' , 'SSH_AUTH_SOCK=/tmp/ssh_sock'
171
190
) ;
172
- cmdOptions . push ( '-e' , 'SSH_AUTH_SOCK=/tmp/ssh_sock' ) ;
173
191
}
174
192
175
193
// If we want a download cache...
@@ -189,104 +207,100 @@ function installRequirements(targetFolder, serverless, options) {
189
207
) ;
190
208
const windowsized = getBindPath ( serverless , downloadCacheDir ) ;
191
209
// And now push it to a volume mount and to pip...
192
- cmdOptions . push (
210
+ dockerCmd . push (
193
211
'-v' ,
194
- quote_single ( `${ windowsized } :${ dockerDownloadCacheDir } :z` )
212
+ `${ windowsized } :${ dockerDownloadCacheDir } :z`
195
213
) ;
196
- pipCmd . push ( '--cache-dir' , quote_single ( dockerDownloadCacheDir ) ) ;
214
+ pipCmd . push ( '--cache-dir' , dockerDownloadCacheDir ) ;
197
215
}
198
216
199
217
if ( options . dockerEnv ) {
200
218
// Add environment variables to docker run cmd
201
219
options . dockerEnv . forEach ( function ( item ) {
202
- cmdOptions . push ( '-e' , item ) ;
220
+ dockerCmd . push ( '-e' , item ) ;
203
221
} ) ;
204
222
}
205
223
206
224
if ( process . platform === 'linux' ) {
207
225
// Use same user so requirements folder is not root and so --cache-dir works
208
- var commands = [ ] ;
209
226
if ( options . useDownloadCache ) {
210
227
// Set the ownership of the download cache dir to root
211
- commands . push ( quote ( [ 'chown' , '-R' , '0:0' , dockerDownloadCacheDir ] ) ) ;
228
+ pipCmds . unshift ( [ 'chown' , '-R' , '0:0' , dockerDownloadCacheDir ] ) ;
212
229
}
213
230
// Install requirements with pip
214
- commands . push ( pipCmd . join ( ' ' ) ) ;
215
231
// Set the ownership of the current folder to user
216
- commands . push (
217
- quote ( [
218
- 'chown' ,
219
- '-R' ,
220
- `${ process . getuid ( ) } :${ process . getgid ( ) } ` ,
221
- '/var/task'
222
- ] )
223
- ) ;
232
+ pipCmds . push ( [ 'chown' , '-R' , `${ process . getuid ( ) } :${ process . getgid ( ) } ` , '/var/task' ] ) ;
224
233
if ( options . useDownloadCache ) {
225
234
// Set the ownership of the download cache dir back to user
226
- commands . push (
227
- quote ( [
235
+ pipCmds . push (
236
+ [
228
237
'chown' ,
229
238
'-R' ,
230
239
`${ process . getuid ( ) } :${ process . getgid ( ) } ` ,
231
240
dockerDownloadCacheDir
232
- ] )
241
+ ]
233
242
) ;
234
243
}
235
- pipCmd = [ '/bin/bash' , '-c' , '"' + commands . join ( ' && ' ) + '"' ] ;
236
244
} else {
237
245
// Use same user so --cache-dir works
238
- cmdOptions . push ( '-u' , quote_single ( getDockerUid ( bindPath ) ) ) ;
246
+ dockerCmd . push ( '-u' , getDockerUid ( bindPath ) ) ;
239
247
}
240
- cmdOptions . push ( dockerImage ) ;
241
- cmdOptions . push ( ...pipCmd ) ;
242
- } else {
243
- cmd = pipCmd [ 0 ] ;
244
- cmdOptions = pipCmd . slice ( 1 ) ;
248
+ dockerCmd . push ( dockerImage ) ;
245
249
}
246
250
247
251
// If enabled slimming, strip so files
248
- if ( options . slim === true || options . slim === 'true' ) {
249
- const preparedPath = dockerPathForWin ( options , targetFolder ) ;
250
- cmdOptions . push ( getStripCommand ( options , preparedPath ) ) ;
252
+ switch ( getStripMode ( options ) ) {
253
+ case 'docker' :
254
+ pipCmds . push ( getStripCommand ( options , '/var/task' ) ) ;
255
+ case 'direct' :
256
+ postCmds . push ( getStripCommand ( options , dockerPathForWin ( targetFolder ) ) ) ;
251
257
}
258
+
252
259
let spawnArgs = { shell : true } ;
253
260
if ( process . env . SLS_DEBUG ) {
254
261
spawnArgs . stdio = 'inherit' ;
255
262
}
256
- const res = spawnSync ( cmd , cmdOptions , spawnArgs ) ;
257
- if ( res . error ) {
258
- if ( res . error . code === 'ENOENT' ) {
259
- if ( options . dockerizePip ) {
260
- throw new Error ( 'docker not found! Please install it.' ) ;
261
- }
262
- throw new Error (
263
- `${ options . pythonBin } not found! Try the pythonBin option.`
264
- ) ;
265
- }
266
- throw res . error ;
267
- }
268
- if ( res . status !== 0 ) {
269
- throw new Error ( res . stderr ) ;
263
+ let mainCmds = [ ] ;
264
+ if ( dockerCmd . length ) {
265
+ dockerCmd . push ( ...mergeCommands ( pipCmds ) ) ;
266
+ mainCmds = [ dockerCmd ] ;
267
+ } else {
268
+ mainCmds = pipCmds ;
270
269
}
270
+ mainCmds . push ( ...postCmds ) ;
271
+
272
+ serverless . cli . log ( `Running ${ quote ( dockerCmd ) } ...` ) ;
273
+
274
+ filterCommands ( mainCmds ) . forEach ( ( [ cmd , ...args ] ) => {
275
+ const res = spawnSync ( cmd , args ) ;
276
+ if ( res . error ) {
277
+ if ( res . error . code === 'ENOENT' ) {
278
+ const advice = cmd . indexOf ( 'python' ) > - 1 ? 'Try the pythonBin option' : 'Please install it' ;
279
+ throw new Error ( `${ cmd } not found! ${ advice } ` ) ;
280
+ }
281
+ throw res . error ;
282
+ }
283
+ if ( res . status !== 0 ) {
284
+ throw new Error ( res . stderr ) ;
285
+ }
286
+ } ) ;
271
287
// If enabled slimming, delete files in slimPatterns
272
288
if ( options . slim === true || options . slim === 'true' ) {
273
289
deleteFiles ( options , targetFolder ) ;
274
290
}
275
291
}
276
292
277
293
/**
278
- * convert path from Windows style to Linux style, if needed
279
- * @param {Object } options
294
+ * Convert path from Windows style to Linux style, if needed.
280
295
* @param {string } path
281
296
* @return {string }
282
297
*/
283
- function dockerPathForWin ( options , path ) {
298
+ function dockerPathForWin ( path ) {
284
299
if ( process . platform === 'win32' ) {
285
- return `" ${ path . replace ( / \\ / g, '/' ) } "` ;
286
- } else if ( process . platform === 'win32' && ! options . dockerizePip ) {
300
+ return path . replace ( / \\ / g, '/' ) ;
301
+ } else {
287
302
return path ;
288
303
}
289
- return quote_single ( path ) ;
290
304
}
291
305
292
306
/** create a filtered requirements.txt without anything from noDeploy
0 commit comments