From 529a3a3f11fc7a563d0c81c0bb262c3d1cc2bf12 Mon Sep 17 00:00:00 2001 From: Malte Swart <msw@kialo.com> Date: Thu, 10 Feb 2022 18:30:50 +0100 Subject: [PATCH] feat(manager/pip-compile): support basic arguments (#14098) Co-authored-by: Michael Kriese <michael.kriese@visualon.de> --- .../__fixtures__/requirementsNoHeaders.txt | 5 ++ .../requirementsWithExploitingArguments.txt | 9 +++ .../__fixtures__/requirementsWithHashes.txt | 14 +++++ .../requirementsWithUnknownArguments.txt | 9 +++ lib/manager/pip-compile/artifacts.spec.ts | 56 +++++++++++++++++++ lib/manager/pip-compile/artifacts.ts | 49 +++++++++++++++- lib/manager/pip-compile/readme.md | 7 +++ 7 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 lib/manager/pip-compile/__fixtures__/requirementsNoHeaders.txt create mode 100644 lib/manager/pip-compile/__fixtures__/requirementsWithExploitingArguments.txt create mode 100644 lib/manager/pip-compile/__fixtures__/requirementsWithHashes.txt create mode 100644 lib/manager/pip-compile/__fixtures__/requirementsWithUnknownArguments.txt diff --git a/lib/manager/pip-compile/__fixtures__/requirementsNoHeaders.txt b/lib/manager/pip-compile/__fixtures__/requirementsNoHeaders.txt new file mode 100644 index 0000000000..3fc1aa1392 --- /dev/null +++ b/lib/manager/pip-compile/__fixtures__/requirementsNoHeaders.txt @@ -0,0 +1,5 @@ +# simple comment +some-package==0.3.1 +some-other-package==1.0.0 +sphinx +not_semver==1.9 diff --git a/lib/manager/pip-compile/__fixtures__/requirementsWithExploitingArguments.txt b/lib/manager/pip-compile/__fixtures__/requirementsWithExploitingArguments.txt new file mode 100644 index 0000000000..930ea670d1 --- /dev/null +++ b/lib/manager/pip-compile/__fixtures__/requirementsWithExploitingArguments.txt @@ -0,0 +1,9 @@ +# +# This file is autogenerated by pip-compile with python 3.9 +# To update, run: +# +# pip-compile --generate-hashes --$(curl asdf) --output-file=/etc/shadow requirements.in +# +attrs==21.2.0 \ + --hash=sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1 \ + --hash=sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb diff --git a/lib/manager/pip-compile/__fixtures__/requirementsWithHashes.txt b/lib/manager/pip-compile/__fixtures__/requirementsWithHashes.txt new file mode 100644 index 0000000000..38ca9ec1db --- /dev/null +++ b/lib/manager/pip-compile/__fixtures__/requirementsWithHashes.txt @@ -0,0 +1,14 @@ +# +# This file is autogenerated by pip-compile with python 3.9 +# To update, run: +# +# pip-compile --allow-unsafe --generate-hashes --output-file=requirements.txt requirements.in +# +attrs==21.2.0 \ + --hash=sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1 \ + --hash=sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb + +# The following packages are considered to be unsafe in a requirements file: +setuptools==59.2.0 \ + --hash=sha256:157d21de9d055ab9e8ea3186d91e7f4f865e11f42deafa952d90842671fc2576 \ + --hash=sha256:4adde3d1e1c89bde1c643c64d89cdd94cbfd8c75252ee459d4500bccb9c7d05d diff --git a/lib/manager/pip-compile/__fixtures__/requirementsWithUnknownArguments.txt b/lib/manager/pip-compile/__fixtures__/requirementsWithUnknownArguments.txt new file mode 100644 index 0000000000..a2f060d334 --- /dev/null +++ b/lib/manager/pip-compile/__fixtures__/requirementsWithUnknownArguments.txt @@ -0,0 +1,9 @@ +# +# This file is autogenerated by pip-compile with python 3.9 +# To update, run: +# +# pip-compile --generate-hashes --version requirements.in +# +attrs==21.2.0 \ + --hash=sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1 \ + --hash=sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb diff --git a/lib/manager/pip-compile/artifacts.spec.ts b/lib/manager/pip-compile/artifacts.spec.ts index 6b37eb0e07..094a4a1dfc 100644 --- a/lib/manager/pip-compile/artifacts.spec.ts +++ b/lib/manager/pip-compile/artifacts.spec.ts @@ -2,9 +2,11 @@ import { exec as _exec } from 'child_process'; import _fs from 'fs-extra'; import { join } from 'upath'; import { envMock, mockExecAll } from '../../../test/exec-util'; +import { Fixtures } from '../../../test/fixtures'; import { git, mocked } from '../../../test/util'; import { GlobalConfig } from '../../config/global'; import type { RepoGlobalConfig } from '../../config/types'; +import { logger } from '../../logger'; import * as docker from '../../util/exec/docker'; import * as _env from '../../util/exec/env'; import type { StatusResult } from '../../util/git/types'; @@ -160,4 +162,58 @@ describe('manager/pip-compile/artifacts', () => { ).not.toBeNull(); expect(execSnapshots).toMatchSnapshot(); }); + + describe('constructPipCompileCmd()', () => { + it('returns default cmd for garbage', () => { + expect( + pipCompile.constructPipCompileCmd( + Fixtures.get('requirementsNoHeaders.txt'), + 'subdir/requirements.in', + 'subdir/requirements.txt' + ) + ).toBe('pip-compile requirements.in'); + }); + + it('returns extracted common arguments (like those featured in the README)', () => { + expect( + pipCompile.constructPipCompileCmd( + Fixtures.get('requirementsWithHashes.txt'), + 'subdir/requirements.in', + 'subdir/requirements.txt' + ) + ).toBe( + 'pip-compile --allow-unsafe --generate-hashes --output-file=requirements.txt requirements.in' + ); + }); + + it('skips unknown arguments', () => { + expect( + pipCompile.constructPipCompileCmd( + Fixtures.get('requirementsWithUnknownArguments.txt'), + 'subdir/requirements.in', + 'subdir/requirements.txt' + ) + ).toBe('pip-compile --generate-hashes requirements.in'); + expect(logger.trace).toHaveBeenCalledWith( + { argument: '--version' }, + 'pip-compile argument is not (yet) supported' + ); + }); + + it('skips exploitable subcommands and files', () => { + expect( + pipCompile.constructPipCompileCmd( + Fixtures.get('requirementsWithExploitingArguments.txt'), + 'subdir/requirements.in', + 'subdir/requirements.txt' + ) + ).toBe( + 'pip-compile --generate-hashes --output-file=requirements.txt requirements.in' + ); + expect(logger.warn).toHaveBeenCalledWith( + { argument: '--output-file=/etc/shadow' }, + 'pip-compile was previously executed with an unexpected `--output-file` filename' + ); + }); + }); }); diff --git a/lib/manager/pip-compile/artifacts.ts b/lib/manager/pip-compile/artifacts.ts index dd79a9acd2..f2848b7b3c 100644 --- a/lib/manager/pip-compile/artifacts.ts +++ b/lib/manager/pip-compile/artifacts.ts @@ -1,5 +1,5 @@ import is from '@sindresorhus/is'; -import { quote } from 'shlex'; +import { quote, split } from 'shlex'; import upath from 'upath'; import { TEMPORARY_ERROR } from '../../constants/error-messages'; import { logger } from '../../logger'; @@ -40,6 +40,47 @@ function getPipToolsConstraint(config: UpdateArtifactsConfig): string { return ''; } +const constraintLineRegex = regEx( + /^(#.*?\r?\n)+# {4}pip-compile(?<arguments>.*?)\r?\n/ +); + +export function constructPipCompileCmd( + content: string, + inputFileName: string, + outputFileName: string +): string { + const headers = constraintLineRegex.exec(content); + const args = ['pip-compile']; + if (headers) { + logger.debug({ header: headers[0] }, 'Found pip-compile header'); + for (const argument of split(headers.groups.arguments)) { + if (['--allow-unsafe', '--generate-hashes'].includes(argument)) { + args.push(argument); + } else if (argument.startsWith('--output-file=')) { + const file = upath.parse(outputFileName).base; + if (argument !== `--output-file=${file}`) { + // we don't trust the user-supplied output-file argument; use our value here + logger.warn( + { argument }, + 'pip-compile was previously executed with an unexpected `--output-file` filename' + ); + } + args.push(`--output-file=${file}`); + } else if (argument.startsWith('--')) { + logger.trace( + { argument }, + 'pip-compile argument is not (yet) supported' + ); + } else { + // ignore position argument (.in file) + } + } + } + args.push(upath.parse(inputFileName).base); + + return args.map((argument) => quote(argument)).join(' '); +} + export async function updateArtifacts({ packageFileName: inputFileName, newPackageFileContent: newInputContent, @@ -59,7 +100,11 @@ export async function updateArtifacts({ if (config.isLockFileMaintenance) { await deleteLocalFile(outputFileName); } - const cmd = `pip-compile ${quote(upath.parse(inputFileName).base)}`; + const cmd = constructPipCompileCmd( + existingOutput, + inputFileName, + outputFileName + ); const tagConstraint = getPythonConstraint(config); const pipToolsConstraint = getPipToolsConstraint(config); const execOptions: ExecOptions = { diff --git a/lib/manager/pip-compile/readme.md b/lib/manager/pip-compile/readme.md index 42330b6006..6118ab5303 100644 --- a/lib/manager/pip-compile/readme.md +++ b/lib/manager/pip-compile/readme.md @@ -40,3 +40,10 @@ To get Renovate to use another version of Python, add a contraints` rule to the } } ``` + +### `pip-compile` arguments + +Renovate reads the `requirements.txt` file and extracts these `pip-compile` arguments: + +- `--generate-hashes` +- `--allow-unsafe` -- GitLab