Skip to content

Commit 4e6d135

Browse files
author
Farley
committed
pip and inject, I think merged good
1 parent defa6f7 commit 4e6d135

File tree

2 files changed

+177
-61
lines changed

2 files changed

+177
-61
lines changed

lib/inject.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const path = require('path');
77
const values = require('lodash.values');
88
const JSZip = require('jszip');
99
const { writeZip, zipFile } = require('./zipTree');
10+
const {getRequirementsWorkingPath, md5Path} = require('./shared');
1011

1112
BbPromise.promisifyAll(fse);
1213

@@ -115,8 +116,13 @@ function injectAllRequirements(funcArtifact) {
115116
)
116117
);
117118
} else {
119+
const reqsFilePath = path.join('.serverless', 'requirements.txt');
120+
const reqChecksum = md5Path(reqsFilePath);
121+
const workingReqsFolder = getRequirementsWorkingPath(
122+
reqChecksum, this.servicePath, this.options);
123+
118124
return injectRequirements(
119-
path.join('.serverless', 'requirements'),
125+
workingReqsFolder,
120126
this.serverless.service.package.artifact || funcArtifact,
121127
this.options
122128
);

lib/pip.js

+170-60
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,47 @@ const set = require('lodash.set');
66
const { spawnSync } = require('child_process');
77
const values = require('lodash.values');
88
const { buildImage, getBindPath, getDockerUid } = require('./docker');
9+
const {
10+
checkForAndDeleteMaxCacheVersions, md5Path,
11+
getRequirementsWorkingPath, getUserCachePath,
12+
} = require('./shared');
913

1014
/**
11-
* Install requirements described in requirementsPath to targetFolder
15+
* Just generate the requirements file in the .serverless folder
1216
* @param {string} requirementsPath
13-
* @param {string} targetFolder
17+
* @param {string} targetFile
1418
* @param {Object} serverless
1519
* @param {string} servicePath
1620
* @param {Object} options
1721
* @return {undefined}
1822
*/
19-
function installRequirements(
23+
function installRequirementsFile(
2024
requirementsPath,
21-
targetFolder,
25+
targetFile,
2226
serverless,
2327
servicePath,
2428
options
2529
) {
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');
3130
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}...`);
3333
} else {
34-
generateRequirementsFile(requirementsPath, dotSlsReqs, options);
34+
generateRequirementsFile(requirementsPath, targetFile, options);
35+
serverless.cli.log(`Generated requirements from ${requirementsPath} in ${targetFile}...`);
3536
}
37+
}
3638

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} ...`);
4050

4151
let cmd;
4252
let cmdOptions;
@@ -45,13 +55,20 @@ function installRequirements(
4555
'-m',
4656
'pip',
4757
'install',
48-
'-t',
49-
dockerPathForWin(options, targetRequirementsFolder),
50-
'-r',
51-
dockerPathForWin(options, dotSlsReqs),
5258
...options.pipCmdExtraArgs
5359
];
5460
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+
5572
// Check if pip has Debian's --system option and set it if so
5673
const pipTestRes = spawnSync(options.pythonBin, [
5774
'-m',
@@ -71,8 +88,13 @@ function installRequirements(
7188
pipCmd.push('--system');
7289
}
7390
}
91+
// If we are dockerizing pip
7492
if (options.dockerizePip) {
7593
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');
7698

7799
// Build docker image if required
78100
let dockerImage;
@@ -87,7 +109,7 @@ function installRequirements(
87109
serverless.cli.log(`Docker Image: ${dockerImage}`);
88110

89111
// Prepare bind path depending on os platform
90-
const bindPath = getBindPath(servicePath);
112+
const bindPath = getBindPath(targetFolder);
91113

92114
cmdOptions = ['run', '--rm', '-v', `"${bindPath}:/var/task:z"`];
93115
if (options.dockerSsh) {
@@ -103,11 +125,22 @@ function installRequirements(
103125
cmdOptions.push('-v', `${process.env.SSH_AUTH_SOCK}:/tmp/ssh_sock:z`);
104126
cmdOptions.push('-e', 'SSH_AUTH_SOCK=/tmp/ssh_sock');
105127
}
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+
106139
if (process.platform === 'linux') {
107140
// Use same user so requirements folder is not root and so --cache-dir works
108141
cmdOptions.push('-u', `${process.getuid()}`);
109142
// const stripCmd = quote([
110-
// 'find', targetRequirementsFolder,
143+
// 'find', targetFolder,
111144
// '-name', '"*.so"',
112145
// '-exec', 'strip', '{}', '\;',
113146
// ]);
@@ -122,7 +155,7 @@ function installRequirements(
122155
cmd = pipCmd[0];
123156
cmdOptions = pipCmd.slice(1);
124157
}
125-
const res = spawnSync(cmd, cmdOptions, { cwd: servicePath, shell: true });
158+
const res = spawnSync(cmd, cmdOptions, { cwd: targetFolder, shell: true });
126159
if (res.error) {
127160
if (res.error.code === 'ENOENT') {
128161
if (options.dockerizePip) {
@@ -139,20 +172,9 @@ function installRequirements(
139172
}
140173
}
141174

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-
155175
/** 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
156178
* @param {string} source requirements
157179
* @param {string} target requirements where results are written
158180
* @param {Object} options
@@ -163,8 +185,13 @@ function generateRequirementsFile(source, target, options) {
163185
.readFileSync(source, { encoding: 'utf-8' })
164186
.split(/\r?\n/);
165187
const filteredRequirements = requirements.filter(req => {
188+
req = req.trim();
189+
if (req.length == 0 || req[0] == '#') {
190+
return false;
191+
}
166192
return !noDeploy.has(req.split(/[=<> \t]/)[0].trim());
167193
});
194+
filteredRequirements.sort(); // Sort them alphabetically
168195
fse.writeFileSync(target, filteredRequirements.join('\n'));
169196
}
170197

@@ -177,15 +204,15 @@ function generateRequirementsFile(source, target, options) {
177204
*/
178205
function copyVendors(vendorFolder, targetFolder, serverless) {
179206
// Create target folder if it does not exist
180-
const targetRequirementsFolder = path.join(targetFolder, 'requirements');
207+
fse.ensureDirSync(targetFolder);
181208

182209
serverless.cli.log(
183210
`Copying vendor libraries from ${vendorFolder} to ${targetRequirementsFolder}...`
184211
);
185212

186213
fse.readdirSync(vendorFolder).map(file => {
187214
let source = path.join(vendorFolder, file);
188-
let dest = path.join(targetRequirementsFolder, file);
215+
let dest = path.join(targetFolder, file);
189216
if (fse.existsSync(dest)) {
190217
rimraf.sync(dest);
191218
}
@@ -194,11 +221,103 @@ function copyVendors(vendorFolder, targetFolder, serverless) {
194221
}
195222

196223
/**
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
198313
* @return {undefined}
199314
*/
200315
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...
202321
if (this.serverless.service.package.individually) {
203322
let doneModules = [];
204323
values(this.serverless.service.functions)
@@ -211,36 +330,27 @@ function installAllRequirements() {
211330
if (!get(f, 'module')) {
212331
set(f, ['module'], '.');
213332
}
333+
// If we didn't already process a module (functions can re-use modules)
214334
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
221337
);
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 );
229342
}
230343
doneModules.push(f.module);
231344
}
232345
});
233346
} 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
240349
);
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);
244354
}
245355
}
246356
}

0 commit comments

Comments
 (0)