2018-11-15 17:42:01 +00:00
|
|
|
const { exec } = require('child-process-promise');
|
|
|
|
const fs = require('fs-extra');
|
|
|
|
const { join } = require('upath');
|
|
|
|
const { isSkipComment } = require('../../util/ignore');
|
|
|
|
const { dependencyPattern } = require('../pip_requirements/extract');
|
|
|
|
|
2019-03-17 14:54:31 +00:00
|
|
|
const pythonVersions = ['python', 'python3', 'python3.7'];
|
|
|
|
let pythonAlias = null;
|
2018-11-15 17:42:01 +00:00
|
|
|
module.exports = {
|
|
|
|
extractPackageFile,
|
|
|
|
extractSetupFile,
|
2019-03-17 14:54:31 +00:00
|
|
|
parsePythonVersion,
|
|
|
|
getPythonAlias,
|
|
|
|
pythonVersions,
|
2018-11-15 17:42:01 +00:00
|
|
|
};
|
|
|
|
|
2019-03-17 14:54:31 +00:00
|
|
|
function parsePythonVersion(str) {
|
|
|
|
const arr = str.split(' ')[1].split('.');
|
|
|
|
return [parseInt(arr[0], 10), parseInt(arr[1], 10)];
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getPythonAlias() {
|
|
|
|
if (pythonAlias) {
|
|
|
|
return pythonAlias;
|
|
|
|
}
|
|
|
|
pythonAlias = pythonVersions[0]; // fallback to 'python'
|
|
|
|
for (const pythonVersion of pythonVersions) {
|
|
|
|
try {
|
|
|
|
const { stdout, stderr } = await exec(`${pythonVersion} --version`);
|
|
|
|
const version = parsePythonVersion(stdout || stderr);
|
|
|
|
// istanbul ignore if
|
|
|
|
if (version[0] >= 3 && version[1] >= 7) {
|
|
|
|
pythonAlias = pythonVersion;
|
|
|
|
}
|
2019-03-17 15:56:40 +00:00
|
|
|
} catch (err) /* istanbul ignore next */ {
|
2019-03-17 14:54:31 +00:00
|
|
|
logger.debug(`${pythonVersion} alias not found`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return pythonAlias;
|
|
|
|
}
|
|
|
|
|
2018-11-15 17:42:01 +00:00
|
|
|
async function extractSetupFile(content, packageFile, config) {
|
|
|
|
const cwd = config.localDir;
|
|
|
|
// extract.py needs setup.py to be written to disk
|
|
|
|
if (!config.gitFs) {
|
|
|
|
const localFileName = join(config.localDir, packageFile);
|
|
|
|
await fs.outputFile(localFileName, content);
|
|
|
|
}
|
|
|
|
let cmd;
|
|
|
|
const args = [join(__dirname, 'extract.py'), packageFile];
|
|
|
|
// istanbul ignore if
|
|
|
|
if (config.binarySource === 'docker') {
|
|
|
|
logger.info('Running python via docker');
|
|
|
|
cmd = 'docker';
|
|
|
|
args.unshift(
|
|
|
|
'run',
|
|
|
|
'-i',
|
|
|
|
'--rm',
|
|
|
|
// volume
|
|
|
|
'-v',
|
|
|
|
`${cwd}:${cwd}`,
|
|
|
|
'-v',
|
|
|
|
`${__dirname}:${__dirname}`,
|
|
|
|
// cwd
|
|
|
|
'-w',
|
|
|
|
cwd,
|
|
|
|
// image
|
|
|
|
'renovate/pip',
|
|
|
|
'python'
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
logger.info('Running python via global command');
|
2019-03-17 14:54:31 +00:00
|
|
|
cmd = await getPythonAlias();
|
2018-11-15 17:42:01 +00:00
|
|
|
}
|
|
|
|
logger.debug({ cmd, args }, 'python command');
|
2019-02-18 10:24:25 +00:00
|
|
|
let stdout;
|
|
|
|
let stderr;
|
|
|
|
try {
|
|
|
|
({ stdout, stderr } = await exec(`${cmd} ${args.join(' ')}`, {
|
|
|
|
cwd,
|
|
|
|
shell: true,
|
|
|
|
timeout: 5000,
|
|
|
|
}));
|
|
|
|
} catch (err) {
|
2019-03-11 10:33:03 +00:00
|
|
|
// istanbul ignore if
|
|
|
|
if (
|
|
|
|
err.message &&
|
|
|
|
err.message.includes('No such file or directory') &&
|
|
|
|
!config.gitFs
|
|
|
|
) {
|
2019-03-12 14:29:43 +00:00
|
|
|
logger.warn(
|
2019-03-11 10:33:03 +00:00
|
|
|
'File not found error when extracting setup.py. Ask your Renovate administrator to enable gitFs and try again'
|
|
|
|
);
|
|
|
|
}
|
2019-02-18 10:24:25 +00:00
|
|
|
throw err;
|
|
|
|
}
|
2018-11-15 17:42:01 +00:00
|
|
|
// istanbul ignore if
|
|
|
|
if (stderr) {
|
2019-02-18 11:20:55 +00:00
|
|
|
stderr = stderr.replace(/.*\n\s*import imp/, '').trim();
|
|
|
|
if (stderr.length) {
|
|
|
|
logger.warn({ stdout, stderr }, 'Error in read setup file');
|
|
|
|
}
|
2018-11-15 17:42:01 +00:00
|
|
|
}
|
|
|
|
return JSON.parse(stdout);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function extractPackageFile(content, packageFile, config) {
|
|
|
|
logger.debug('pip_setup.extractPackageFile()');
|
|
|
|
let setup;
|
|
|
|
try {
|
|
|
|
setup = await extractSetupFile(content, packageFile, config);
|
|
|
|
} catch (err) {
|
2019-03-12 14:29:43 +00:00
|
|
|
logger.warn({ err }, 'Failed to read setup.py file');
|
2018-11-15 17:42:01 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const requires = [];
|
|
|
|
if (setup.install_requires) {
|
|
|
|
requires.push(...setup.install_requires);
|
|
|
|
}
|
|
|
|
if (setup.extras_require) {
|
|
|
|
for (const req of Object.values(setup.extras_require)) {
|
|
|
|
requires.push(...req);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const regex = new RegExp(`^${dependencyPattern}`);
|
|
|
|
const lines = content.split('\n');
|
|
|
|
const deps = requires
|
|
|
|
.map(req => {
|
|
|
|
const lineNumber = lines.findIndex(l => l.includes(req));
|
|
|
|
if (lineNumber === -1) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const rawline = lines[lineNumber];
|
|
|
|
let dep = {};
|
|
|
|
const [, comment] = rawline.split('#').map(part => part.trim());
|
|
|
|
if (isSkipComment(comment)) {
|
|
|
|
dep.skipReason = 'ignored';
|
|
|
|
}
|
|
|
|
regex.lastIndex = 0;
|
|
|
|
const matches = regex.exec(req);
|
|
|
|
if (!matches) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const [, depName, , currentValue] = matches;
|
|
|
|
dep = {
|
|
|
|
...dep,
|
|
|
|
depName,
|
|
|
|
currentValue,
|
|
|
|
lineNumber,
|
2019-02-04 08:41:22 +00:00
|
|
|
datasource: 'pypi',
|
2018-11-15 17:42:01 +00:00
|
|
|
};
|
|
|
|
return dep;
|
|
|
|
})
|
2019-01-30 20:32:38 +00:00
|
|
|
.filter(Boolean)
|
|
|
|
.sort((a, b) =>
|
|
|
|
a.lineNumber === b.lineNumber
|
|
|
|
? (a.depName > b.depName) - (a.depName < b.depName)
|
|
|
|
: a.lineNumber - b.lineNumber
|
|
|
|
);
|
2018-11-15 17:42:01 +00:00
|
|
|
if (!deps.length) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return { deps };
|
|
|
|
}
|