@@ -6,37 +6,47 @@ const set = require('lodash.set');
6
6
const { spawnSync } = require ( 'child_process' ) ;
7
7
const values = require ( 'lodash.values' ) ;
8
8
const { buildImage, getBindPath, getDockerUid } = require ( './docker' ) ;
9
+ const {
10
+ checkForAndDeleteMaxCacheVersions, md5Path,
11
+ getRequirementsWorkingPath, getUserCachePath,
12
+ } = require ( './shared' ) ;
9
13
10
14
/**
11
- * Install requirements described in requirementsPath to targetFolder
15
+ * Just generate the requirements file in the .serverless folder
12
16
* @param {string } requirementsPath
13
- * @param {string } targetFolder
17
+ * @param {string } targetFile
14
18
* @param {Object } serverless
15
19
* @param {string } servicePath
16
20
* @param {Object } options
17
21
* @return {undefined }
18
22
*/
19
- function installRequirements (
23
+ function installRequirementsFile (
20
24
requirementsPath ,
21
- targetFolder ,
25
+ targetFile ,
22
26
serverless ,
23
27
servicePath ,
24
28
options
25
29
) {
26
- // Create target folder if it does not exist
27
- const targetRequirementsFolder = path . join ( targetFolder , 'requirements' ) ;
28
- fse . ensureDirSync ( targetRequirementsFolder ) ;
29
-
30
- const dotSlsReqs = path . join ( targetFolder , 'requirements.txt' ) ;
31
30
if ( options . usePipenv && fse . existsSync ( path . join ( servicePath , 'Pipfile' ) ) ) {
32
- generateRequirementsFile ( dotSlsReqs , dotSlsReqs , options ) ;
31
+ generateRequirementsFile ( dotSlsReqs , targetFile , options ) ;
32
+ serverless . cli . log ( `Parsed requirements.txt from Pipfile in ${ targetFile } ...` ) ;
33
33
} else {
34
- generateRequirementsFile ( requirementsPath , dotSlsReqs , options ) ;
34
+ generateRequirementsFile ( requirementsPath , targetFile , options ) ;
35
+ serverless . cli . log ( `Generated requirements from ${ requirementsPath } in ${ targetFile } ...` ) ;
35
36
}
37
+ }
36
38
37
- serverless . cli . log (
38
- `Installing requirements of ${ requirementsPath } in ${ targetFolder } ...`
39
- ) ;
39
+ /**
40
+ * Install requirements described from requirements in the targetFolder into that same targetFolder
41
+ * @param {string } targetFolder
42
+ * @param {Object } serverless
43
+ * @param {Object } options
44
+ * @return {undefined }
45
+ */
46
+ function installRequirements ( targetFolder , serverless , options ) {
47
+ const targetRequirementsTxt = path . join ( targetFolder , 'requirements.txt' ) ;
48
+
49
+ serverless . cli . log ( `Installing requirements from ${ targetRequirementsTxt } ...` ) ;
40
50
41
51
let cmd ;
42
52
let cmdOptions ;
@@ -45,13 +55,20 @@ function installRequirements(
45
55
'-m' ,
46
56
'pip' ,
47
57
'install' ,
48
- '-t' ,
49
- dockerPathForWin ( options , targetRequirementsFolder ) ,
50
- '-r' ,
51
- dockerPathForWin ( options , dotSlsReqs ) ,
52
58
...options . pipCmdExtraArgs
53
59
] ;
54
60
if ( ! options . dockerizePip ) {
61
+ // Push our local OS-specific paths for requirements and target directory
62
+ pipCmd . push ( '-t' , targetFolder ) ;
63
+ pipCmd . push ( '-r' , targetRequirementsTxt ) ;
64
+ // If we want a download cache...
65
+ if ( options . useDownloadCache ) {
66
+ const downloadCacheDir = path . join ( getUserCachePath ( options ) , 'downloadCacheslspyc' ) ;
67
+ serverless . cli . log ( `Using download cache directory ${ downloadCacheDir } ` ) ;
68
+ fse . ensureDirSync ( downloadCacheDir ) ;
69
+ pipCmd . push ( '--cache-dir' , downloadCacheDir ) ;
70
+ }
71
+
55
72
// Check if pip has Debian's --system option and set it if so
56
73
const pipTestRes = spawnSync ( options . pythonBin , [
57
74
'-m' ,
@@ -71,8 +88,13 @@ function installRequirements(
71
88
pipCmd . push ( '--system' ) ;
72
89
}
73
90
}
91
+ // If we are dockerizing pip
74
92
if ( options . dockerizePip ) {
75
93
cmd = 'docker' ;
94
+
95
+ // Push docker-specific paths for requirements and target directory
96
+ pipCmd . push ( '-t' , '/var/task/' ) ;
97
+ pipCmd . push ( '-r' , '/var/task/requirements.txt' ) ;
76
98
77
99
// Build docker image if required
78
100
let dockerImage ;
@@ -87,7 +109,7 @@ function installRequirements(
87
109
serverless . cli . log ( `Docker Image: ${ dockerImage } ` ) ;
88
110
89
111
// Prepare bind path depending on os platform
90
- const bindPath = getBindPath ( servicePath ) ;
112
+ const bindPath = getBindPath ( targetFolder ) ;
91
113
92
114
cmdOptions = [ 'run' , '--rm' , '-v' , `"${ bindPath } :/var/task:z"` ] ;
93
115
if ( options . dockerSsh ) {
@@ -103,11 +125,22 @@ function installRequirements(
103
125
cmdOptions . push ( '-v' , `${ process . env . SSH_AUTH_SOCK } :/tmp/ssh_sock:z` ) ;
104
126
cmdOptions . push ( '-e' , 'SSH_AUTH_SOCK=/tmp/ssh_sock' ) ;
105
127
}
128
+
129
+ // If we want a download cache...
130
+ if ( options . useDownloadCache ) {
131
+ const downloadCacheDir = path . join ( getUserCachePath ( options ) , 'downloadCacheslspyc' ) ;
132
+ serverless . cli . log ( `Using download cache directory ${ downloadCacheDir } ` ) ;
133
+ fse . ensureDirSync ( downloadCacheDir ) ;
134
+ // And now push it to a volume mount and to pip...
135
+ cmdOptions . push ( '-v' , `${ downloadCacheDir } :/var/useDownloadCache:z` ) ;
136
+ pipCmd . push ( '--cache-dir' , '/var/useDownloadCache' ) ;
137
+ }
138
+
106
139
if ( process . platform === 'linux' ) {
107
140
// Use same user so requirements folder is not root and so --cache-dir works
108
141
cmdOptions . push ( '-u' , `${ process . getuid ( ) } ` ) ;
109
142
// const stripCmd = quote([
110
- // 'find', targetRequirementsFolder ,
143
+ // 'find', targetFolder ,
111
144
// '-name', '"*.so"',
112
145
// '-exec', 'strip', '{}', '\;',
113
146
// ]);
@@ -122,7 +155,7 @@ function installRequirements(
122
155
cmd = pipCmd [ 0 ] ;
123
156
cmdOptions = pipCmd . slice ( 1 ) ;
124
157
}
125
- const res = spawnSync ( cmd , cmdOptions , { cwd : servicePath , shell : true } ) ;
158
+ const res = spawnSync ( cmd , cmdOptions , { cwd : targetFolder , shell : true } ) ;
126
159
if ( res . error ) {
127
160
if ( res . error . code === 'ENOENT' ) {
128
161
if ( options . dockerizePip ) {
@@ -139,20 +172,9 @@ function installRequirements(
139
172
}
140
173
}
141
174
142
- /**
143
- * convert path from Windows style to Linux style, if needed
144
- * @param {Object } options
145
- * @param {string } path
146
- * @return {string }
147
- */
148
- function dockerPathForWin ( options , path ) {
149
- if ( process . platform === 'win32' && options . dockerizePip ) {
150
- return path . replace ( '\\' , '/' ) ;
151
- }
152
- return path ;
153
- }
154
-
155
175
/** create a filtered requirements.txt without anything from noDeploy
176
+ * then remove all comments and empty lines, and sort the list which
177
+ * assist with matching the static cache
156
178
* @param {string } source requirements
157
179
* @param {string } target requirements where results are written
158
180
* @param {Object } options
@@ -163,8 +185,13 @@ function generateRequirementsFile(source, target, options) {
163
185
. readFileSync ( source , { encoding : 'utf-8' } )
164
186
. split ( / \r ? \n / ) ;
165
187
const filteredRequirements = requirements . filter ( req => {
188
+ req = req . trim ( ) ;
189
+ if ( req . length == 0 || req [ 0 ] == '#' ) {
190
+ return false ;
191
+ }
166
192
return ! noDeploy . has ( req . split ( / [ = < > \t ] / ) [ 0 ] . trim ( ) ) ;
167
193
} ) ;
194
+ filteredRequirements . sort ( ) ; // Sort them alphabetically
168
195
fse . writeFileSync ( target , filteredRequirements . join ( '\n' ) ) ;
169
196
}
170
197
@@ -177,15 +204,15 @@ function generateRequirementsFile(source, target, options) {
177
204
*/
178
205
function copyVendors ( vendorFolder , targetFolder , serverless ) {
179
206
// Create target folder if it does not exist
180
- const targetRequirementsFolder = path . join ( targetFolder , 'requirements' ) ;
207
+ fse . ensureDirSync ( targetFolder ) ;
181
208
182
209
serverless . cli . log (
183
210
`Copying vendor libraries from ${ vendorFolder } to ${ targetRequirementsFolder } ...`
184
211
) ;
185
212
186
213
fse . readdirSync ( vendorFolder ) . map ( file => {
187
214
let source = path . join ( vendorFolder , file ) ;
188
- let dest = path . join ( targetRequirementsFolder , file ) ;
215
+ let dest = path . join ( targetFolder , file ) ;
189
216
if ( fse . existsSync ( dest ) ) {
190
217
rimraf . sync ( dest ) ;
191
218
}
@@ -194,11 +221,103 @@ function copyVendors(vendorFolder, targetFolder, serverless) {
194
221
}
195
222
196
223
/**
197
- * pip install the requirements to the .serverless/requirements directory
224
+ * This evaluates if requirements are actually needed to be installed, but fails
225
+ * gracefully if no req file is found intentionally. It also assists with code
226
+ * re-use for this logic pertaining to individually packaged functions
227
+ * @param {string } servicePath
228
+ * @param {string } modulePath
229
+ * @param {Object } options
230
+ * @param {Object } serverless
231
+ * @return {string }
232
+ */
233
+ function installRequirementsIfNeeded ( servicePath , modulePath , options , serverless ) {
234
+ // Our source requirements, under our service path, and our module path (if specified)
235
+ const fileName = path . join ( servicePath , modulePath , options . fileName ) ;
236
+
237
+ // First, generate the requirements file to our local .serverless folder
238
+ fse . ensureDirSync ( path . join ( servicePath , '.serverless' ) ) ;
239
+ const slsReqsTxt = path . join ( servicePath , '.serverless' , 'requirements.txt' ) ;
240
+
241
+ installRequirementsFile (
242
+ fileName ,
243
+ slsReqsTxt ,
244
+ serverless ,
245
+ servicePath ,
246
+ options
247
+ ) ;
248
+
249
+ // If no requirements file or an empty requirements file, then do nothing
250
+ if ( ! fse . existsSync ( slsReqsTxt ) || fse . statSync ( slsReqsTxt ) . size == 0 ) {
251
+ serverless . cli . log ( `Skipping empty output requirements.txt file from ${ slsReqsTxt } ` ) ;
252
+ return false ;
253
+ }
254
+
255
+ // Copy our requirements to another filename in .serverless (incase of individually packaged)
256
+ if ( modulePath ) {
257
+ destinationFile = path . join ( servicePath , '.serverless' , modulePath + '_requirements.txt' ) ;
258
+ serverless . cli . log ( `Copying from ${ slsReqsTxt } into ${ destinationFile } ...` ) ;
259
+ fse . copySync ( slsReqsTxt , destinationFile ) ;
260
+ }
261
+
262
+ // Then generate our MD5 Sum of this requirements file to determine where it should "go" to and/or pull cache from
263
+ const reqChecksum = md5Path ( slsReqsTxt ) ;
264
+
265
+ // Then figure out where this cache should be, if we're caching, etc
266
+ const workingReqsFolder = getRequirementsWorkingPath ( reqChecksum , servicePath , options ) ;
267
+
268
+ // Check if our static cache is present and is valid
269
+ if ( fse . existsSync ( workingReqsFolder ) ) {
270
+ if ( fse . existsSync ( path . join ( workingReqsFolder , '.completed_requirements' ) ) ) {
271
+ serverless . cli . log ( `Using static cache of requirements found at ${ workingReqsFolder } ...` ) ;
272
+ // We'll "touch" the folder, as to bring it to the start of the FIFO cache
273
+ fse . utimesSync ( workingReqsFolder , new Date ( ) , new Date ( ) ) ;
274
+ return workingReqsFolder ;
275
+ }
276
+ // Remove our old folder if it didn't complete properly, but _just incase_ only remove it if named properly...
277
+ if ( workingReqsFolder . endsWith ( '_slspyc' ) || workingReqsFolder . endsWith ( '.requirements' ) ) {
278
+ rimraf . sync ( workingReqsFolder ) ;
279
+ }
280
+ }
281
+
282
+ // Ensuring the working reqs folder exists
283
+ fse . ensureDirSync ( workingReqsFolder ) ;
284
+
285
+ // Copy our requirements.txt into our working folder...
286
+ fse . copySync ( slsReqsTxt , path . join ( workingReqsFolder , 'requirements.txt' ) ) ;
287
+
288
+ // Then install our requirements from this folder
289
+ installRequirements (
290
+ workingReqsFolder ,
291
+ serverless ,
292
+ options
293
+ ) ;
294
+ if ( options . vendor ) {
295
+ // copy vendor libraries to requirements folder
296
+ copyVendors (
297
+ options . vendor ,
298
+ workingReqsFolder ,
299
+ serverless
300
+ ) ;
301
+ }
302
+
303
+ // Then touch our ".completed_requirements" file so we know we can use this for static cache
304
+ if ( options . useStaticCache ) {
305
+ fse . closeSync ( fse . openSync ( path . join ( workingReqsFolder , '.completed_requirements' ) , 'w' ) ) ;
306
+ }
307
+ return workingReqsFolder ;
308
+ }
309
+
310
+
311
+ /**
312
+ * pip install the requirements to the requirements directory
198
313
* @return {undefined }
199
314
*/
200
315
function installAllRequirements ( ) {
201
- fse . ensureDirSync ( path . join ( this . servicePath , '.serverless' ) ) ;
316
+ // fse.ensureDirSync(path.join(this.servicePath, '.serverless'));
317
+ // First, check and delete cache versions, if enabled
318
+ checkForAndDeleteMaxCacheVersions ( this . options , this . serverless ) ;
319
+
320
+ // Then if we're going to package functions individually...
202
321
if ( this . serverless . service . package . individually ) {
203
322
let doneModules = [ ] ;
204
323
values ( this . serverless . service . functions )
@@ -211,36 +330,27 @@ function installAllRequirements() {
211
330
if ( ! get ( f , 'module' ) ) {
212
331
set ( f , [ 'module' ] , '.' ) ;
213
332
}
333
+ // If we didn't already process a module (functions can re-use modules)
214
334
if ( ! doneModules . includes ( f . module ) ) {
215
- installRequirements (
216
- path . join ( f . module , this . options . fileName ) ,
217
- path . join ( '.serverless' , f . module ) ,
218
- this . serverless ,
219
- this . servicePath ,
220
- this . options
335
+ const reqsInstalledAt = installRequirementsIfNeeded (
336
+ this . servicePath , f . module , this . options , this . serverless
221
337
) ;
222
- if ( f . vendor ) {
223
- // copy vendor libraries to requirements folder
224
- copyVendors (
225
- f . vendor ,
226
- path . join ( '.serverless' , f . module ) ,
227
- this . serverless
228
- ) ;
338
+ // Add symlinks into .serverless for each module so it's easier for injecting and for users to see where reqs are
339
+ let symlinkPath = path . join ( this . servicePath , '.serverless' , join ( [ `${ f . module } ` , '_requirements' ] . join ( '_' ) ) ) ;
340
+ if ( reqsInstalledAt && ! fse . existsSync ( symlinkPath ) ) {
341
+ fse . symlink ( reqsInstalledAt , symlinkPath ) ;
229
342
}
230
343
doneModules . push ( f . module ) ;
231
344
}
232
345
} ) ;
233
346
} else {
234
- installRequirements (
235
- this . options . fileName ,
236
- '.serverless' ,
237
- this . serverless ,
238
- this . servicePath ,
239
- this . options
347
+ const reqsInstalledAt = installRequirementsIfNeeded (
348
+ this . servicePath , '' , this . options , this . serverless
240
349
) ;
241
- if ( this . options . vendor ) {
242
- // copy vendor libraries to requirements folder
243
- copyVendors ( this . options . vendor , '.serverless' , this . serverless ) ;
350
+ // Add symlinks into .serverless for so it's easier for injecting and for users to see where reqs are
351
+ let symlinkPath = path . join ( this . servicePath , '.serverless' , `requirements` ) ;
352
+ if ( reqsInstalledAt && ! fse . existsSync ( symlinkPath ) ) {
353
+ fse . symlink ( reqsInstalledAt , symlinkPath ) ;
244
354
}
245
355
}
246
356
}
0 commit comments