From 8f02c53039b928bb0de860b376ee53f842829dfa Mon Sep 17 00:00:00 2001 From: Norbert Szulc <norbert@icetek.io> Date: Tue, 20 Feb 2024 18:45:15 +0100 Subject: [PATCH] feat(manager/pip-compile): Allow paths relative to repository root (#27272) Co-authored-by: Rhys Arkins <rhys@arkins.net> --- .../manager/pip-compile/artifacts.spec.ts | 63 ++++++------ lib/modules/manager/pip-compile/artifacts.ts | 39 +++----- .../manager/pip-compile/common.spec.ts | 16 ++- lib/modules/manager/pip-compile/common.ts | 21 ++-- .../manager/pip-compile/extract.spec.ts | 98 +++++++++++++++++-- lib/modules/manager/pip-compile/extract.ts | 25 ++++- lib/modules/manager/pip-compile/utils.spec.ts | 29 ++++++ lib/modules/manager/pip-compile/utils.ts | 45 +++++++++ 8 files changed, 257 insertions(+), 79 deletions(-) create mode 100644 lib/modules/manager/pip-compile/utils.spec.ts diff --git a/lib/modules/manager/pip-compile/artifacts.spec.ts b/lib/modules/manager/pip-compile/artifacts.spec.ts index 2bd1c0e107..ff27f6ae60 100644 --- a/lib/modules/manager/pip-compile/artifacts.spec.ts +++ b/lib/modules/manager/pip-compile/artifacts.spec.ts @@ -11,6 +11,7 @@ import type { StatusResult } from '../../../util/git/types'; import * as _datasource from '../../datasource'; import type { UpdateArtifactsConfig, Upgrade } from '../types'; import { constructPipCompileCmd } from './artifacts'; +import { extractHeaderCommand } from './common'; import { updateArtifacts } from '.'; const datasource = mocked(_datasource); @@ -354,8 +355,10 @@ describe('modules/manager/pip-compile/artifacts', () => { it('throws for garbage', () => { expect(() => constructPipCompileCmd( - Fixtures.get('requirementsNoHeaders.txt'), - 'subdir/requirements.txt', + extractHeaderCommand( + Fixtures.get('requirementsNoHeaders.txt'), + 'subdir/requirements.txt', + ), false, ), ).toThrow(/extract/); @@ -364,8 +367,10 @@ describe('modules/manager/pip-compile/artifacts', () => { it('returns extracted common arguments (like those featured in the README)', () => { expect( constructPipCompileCmd( - Fixtures.get('requirementsWithHashes.txt'), - 'subdir/requirements.txt', + extractHeaderCommand( + Fixtures.get('requirementsWithHashes.txt'), + 'subdir/requirements.txt', + ), false, ), ).toBe( @@ -376,8 +381,10 @@ describe('modules/manager/pip-compile/artifacts', () => { it('returns --no-emit-index-url only once when its in the header and credentials are present in URLs', () => { expect( constructPipCompileCmd( - Fixtures.get('requirementsWithHashes.txt'), - 'subdir/requirements.txt', + extractHeaderCommand( + Fixtures.get('requirementsWithHashes.txt'), + 'subdir/requirements.txt', + ), true, ), ).toBe( @@ -386,41 +393,33 @@ describe('modules/manager/pip-compile/artifacts', () => { }); it('safeguard against index url leak if not explicitly set by an option', () => { - expect( - constructPipCompileCmd(simpleHeader, 'subdir/requirements.txt', false), - ).toBe('pip-compile --no-emit-index-url requirements.in'); - }); - - it('allow explicit --emit-index-url', () => { expect( constructPipCompileCmd( - getCommandInHeader('pip-compile --emit-index-url requirements.in'), - 'subdir/requirements.txt', + extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'), false, ), - ).toBe('pip-compile --emit-index-url requirements.in'); + ).toBe('pip-compile --no-emit-index-url requirements.in'); }); - // TODO(not7cd): remove when relative pahts are supported - it('change --output-file if differs', () => { + it('allow explicit --emit-index-url', () => { expect( constructPipCompileCmd( - getCommandInHeader( - 'pip-compile --output-file=hey.txt requirements.in', + extractHeaderCommand( + getCommandInHeader('pip-compile --emit-index-url requirements.in'), + 'subdir/requirements.txt', ), - 'subdir/requirements.txt', false, ), - ).toBe( - 'pip-compile --no-emit-index-url --output-file=requirements.txt requirements.in', - ); + ).toBe('pip-compile --emit-index-url requirements.in'); }); it('throws on unknown arguments', () => { expect(() => constructPipCompileCmd( - Fixtures.get('requirementsWithUnknownArguments.txt'), - 'subdir/requirements.txt', + extractHeaderCommand( + Fixtures.get('requirementsWithUnknownArguments.txt'), + 'subdir/requirements.txt', + ), false, ), ).toThrow(/supported/); @@ -429,8 +428,10 @@ describe('modules/manager/pip-compile/artifacts', () => { it('throws on custom command', () => { expect(() => constructPipCompileCmd( - Fixtures.get('requirementsCustomCommand.txt'), - 'subdir/requirements.txt', + extractHeaderCommand( + Fixtures.get('requirementsCustomCommand.txt'), + 'subdir/requirements.txt', + ), false, ), ).toThrow(/custom/); @@ -439,10 +440,12 @@ describe('modules/manager/pip-compile/artifacts', () => { it('add --upgrade-package to command if Upgrade[] passed', () => { expect( constructPipCompileCmd( - getCommandInHeader( - 'pip-compile --output-file=requirements.txt requirements.in', + extractHeaderCommand( + getCommandInHeader( + 'pip-compile --output-file=requirements.txt requirements.in', + ), + 'subdir/requirements.txt', ), - 'subdir/requirements.txt', false, [ { depName: 'foo', newVersion: '1.0.2' }, diff --git a/lib/modules/manager/pip-compile/artifacts.ts b/lib/modules/manager/pip-compile/artifacts.ts index abbbc6ff92..cf10b4b338 100644 --- a/lib/modules/manager/pip-compile/artifacts.ts +++ b/lib/modules/manager/pip-compile/artifacts.ts @@ -1,5 +1,4 @@ import { quote } from 'shlex'; -import upath from 'upath'; import { TEMPORARY_ERROR } from '../../../constants/error-messages'; import { logger } from '../../../logger'; import { exec } from '../../../util/exec'; @@ -16,39 +15,22 @@ import { getExecOptions, getRegistryUrlVarsFromPackageFile, } from './common'; +import type { PipCompileArgs } from './types'; +import { inferCommandExecDir } from './utils'; export function constructPipCompileCmd( - content: string, - outputFileName: string, + compileArgs: PipCompileArgs, haveCredentials: boolean, upgradePackages: Upgrade[] = [], ): string { - const compileArgs = extractHeaderCommand(content, outputFileName); if (compileArgs.isCustomCommand) { throw new Error( 'Detected custom command, header modified or set by CUSTOM_COMPILE_COMMAND', ); } - if (compileArgs.outputFile) { - // TODO(not7cd): This file path can be relative like `reqs/main.txt` - const file = upath.parse(outputFileName).base; - if (compileArgs.outputFile !== file) { - // we don't trust the user-supplied output-file argument; - // TODO(not7cd): allow relative paths - logger.warn( - { outputFile: compileArgs.outputFile, actualPath: file }, - 'pip-compile was previously executed with an unexpected `--output-file` filename', - ); - // TODO(not7cd): this shouldn't be changed in extract function - compileArgs.outputFile = file; - compileArgs.argv.forEach((item, i) => { - if (item.startsWith('--output-file=')) { - compileArgs.argv[i] = `--output-file=${quote(file)}`; - } - }); - } - } else { - logger.debug(`pip-compile: implicit output file (${outputFileName})`); + + if (!compileArgs.outputFile) { + logger.debug(`pip-compile: implicit output file`); } // safeguard against index url leak if not explicitly set by an option if ( @@ -96,21 +78,22 @@ export async function updateArtifacts({ if (config.isLockFileMaintenance) { await deleteLocalFile(outputFileName); } + const compileArgs = extractHeaderCommand(existingOutput, outputFileName); + const cwd = inferCommandExecDir(outputFileName, compileArgs.outputFile); const upgradePackages = updatedDeps.filter((dep) => dep.isLockfileUpdate); const packageFile = pipRequirements.extractPackageFile(newInputContent); const registryUrlVars = getRegistryUrlVarsFromPackageFile(packageFile); const cmd = constructPipCompileCmd( - existingOutput, - outputFileName, + compileArgs, registryUrlVars.haveCredentials, upgradePackages, ); const execOptions = await getExecOptions( config, - inputFileName, + cwd, registryUrlVars.environmentVars, ); - logger.trace({ cmd }, 'pip-compile command'); + logger.trace({ cwd, cmd }, 'pip-compile command'); logger.trace({ env: execOptions.extraEnv }, 'pip-compile extra env vars'); await exec(cmd, execOptions); const status = await getRepoStatus(); diff --git a/lib/modules/manager/pip-compile/common.spec.ts b/lib/modules/manager/pip-compile/common.spec.ts index 1a1f31e1d0..37acdadf46 100644 --- a/lib/modules/manager/pip-compile/common.spec.ts +++ b/lib/modules/manager/pip-compile/common.spec.ts @@ -5,6 +5,7 @@ import { extractHeaderCommand, getRegistryUrlVarsFromPackageFile, } from './common'; +import { inferCommandExecDir } from './utils'; jest.mock('../../../util/host-rules', () => mockDeep()); @@ -132,7 +133,7 @@ describe('modules/manager/pip-compile/common', () => { 'returned sourceFiles must not contain options', (argument: string) => { const sourceFiles = extractHeaderCommand( - getCommandInHeader(`pip-compile ${argument}=dd reqs.in`), + getCommandInHeader(`pip-compile ${argument}=reqs.txt reqs.in`), 'reqs.txt', ).sourceFiles; expect(sourceFiles).not.toContainEqual(argument); @@ -148,6 +149,19 @@ describe('modules/manager/pip-compile/common', () => { ), ).toHaveProperty('isCustomCommand', true); }); + + it.each([ + { path: 'reqs.txt', arg: 'reqs.txt', result: '.' }, + { path: 'subdir/reqs.txt', arg: 'subdir/reqs.txt', result: '.' }, + { path: 'subdir/reqs.txt', arg: './subdir/reqs.txt', result: '.' }, + { path: 'subdir/reqs.txt', arg: 'reqs.txt', result: 'subdir' }, + { path: 'subdir/reqs.txt', arg: './reqs.txt', result: 'subdir' }, + ])( + 'infer exec directory (cwd) from output file path and header command', + ({ path, arg, result }) => { + expect(inferCommandExecDir(path, arg)).toEqual(result); + }, + ); }); describe('getRegistryUrlFlagsFromPackageFile()', () => { diff --git a/lib/modules/manager/pip-compile/common.ts b/lib/modules/manager/pip-compile/common.ts index 1847e10af9..c0b29fd57e 100644 --- a/lib/modules/manager/pip-compile/common.ts +++ b/lib/modules/manager/pip-compile/common.ts @@ -1,9 +1,11 @@ import is from '@sindresorhus/is'; import { split } from 'shlex'; +import upath from 'upath'; import { logger } from '../../../logger'; import { isNotNullOrUndefined } from '../../../util/array'; import type { ExecOptions } from '../../../util/exec/types'; import { ensureCacheDir } from '../../../util/fs'; +import { ensureLocalPath } from '../../../util/fs/util'; import * as hostRules from '../../../util/host-rules'; import { regEx } from '../../../util/regex'; import type { PackageFileContent, UpdateArtifactsConfig } from '../types'; @@ -36,13 +38,13 @@ export function getPipToolsConstraint(config: UpdateArtifactsConfig): string { } export async function getExecOptions( config: UpdateArtifactsConfig, - inputFileName: string, + cwd: string, extraEnv: Record<string, string>, ): Promise<ExecOptions> { const constraint = getPythonConstraint(config); const pipToolsConstraint = getPipToolsConstraint(config); const execOptions: ExecOptions = { - cwdFile: inputFileName, + cwd: ensureLocalPath(cwd), docker: {}, toolConstraints: [ { @@ -94,7 +96,9 @@ export function extractHeaderCommand( ): PipCompileArgs { const compileCommand = constraintLineRegex.exec(content); if (compileCommand?.groups === undefined) { - throw new Error(`Failed to extract command from header in ${fileName}`); + throw new Error( + `Failed to extract command from header in ${fileName} ${content}`, + ); } logger.trace( `pip-compile: found header in ${fileName}: \n${compileCommand[0]}`, @@ -102,16 +106,12 @@ export function extractHeaderCommand( const command = compileCommand.groups.command; const argv = [command]; const isCustomCommand = command !== 'pip-compile'; - if (isCustomCommand) { - logger.debug( - `pip-compile: custom command ${command} detected (${fileName})`, - ); - } if (compileCommand.groups.arguments) { argv.push(...split(compileCommand.groups.arguments)); } logger.debug( - `pip-compile: extracted command from header: ${JSON.stringify(argv)}`, + { fileName, argv, isCustomCommand }, + `pip-compile: extracted command from header`, ); const result: PipCompileArgs = { @@ -147,7 +147,7 @@ export function extractHeaderCommand( if (result.outputFile) { throw new Error('Cannot use multiple --output-file options'); } - result.outputFile = value; + result.outputFile = upath.normalize(value); } else if (option === '--index-url') { if (result.indexUrl) { throw new Error('Cannot use multiple --index-url options'); @@ -170,7 +170,6 @@ export function extractHeaderCommand( logger.warn(`pip-compile: option ${arg} not handled`); } - logger.trace( { ...result, diff --git a/lib/modules/manager/pip-compile/extract.spec.ts b/lib/modules/manager/pip-compile/extract.spec.ts index 9e9f55720f..bd45db12ff 100644 --- a/lib/modules/manager/pip-compile/extract.spec.ts +++ b/lib/modules/manager/pip-compile/extract.spec.ts @@ -1,10 +1,20 @@ +import { join } from 'upath'; import { Fixtures } from '../../../../test/fixtures'; import { fs } from '../../../../test/util'; +import { GlobalConfig } from '../../../config/global'; +import type { RepoGlobalConfig } from '../../../config/types'; import { logger } from '../../../logger'; import { extractAllPackageFiles, extractPackageFile } from '.'; jest.mock('../../../util/fs'); +const adminConfig: RepoGlobalConfig = { + // `join` fixes Windows CI + localDir: join('/tmp/github/some/repo'), + cacheDir: join('/tmp/renovate/cache'), + containerbaseDir: join('/tmp/renovate/cache/containerbase'), +}; + function getSimpleRequirementsFile(command: string, deps: string[] = []) { return `# # This file is autogenerated by pip-compile with Python 3.11 @@ -17,6 +27,14 @@ ${deps.join('\n')}`; } describe('modules/manager/pip-compile/extract', () => { + beforeEach(() => { + GlobalConfig.set(adminConfig); + }); + + afterEach(() => { + fs.readLocalFile.mockClear(); + }); + describe('extractPackageFile()', () => { it('returns object for requirements.in', () => { const packageFile = extractPackageFile( @@ -79,7 +97,6 @@ describe('modules/manager/pip-compile/extract', () => { 'requirements3.txt', ]; const packageFiles = await extractAllPackageFiles({}, lockFiles); - expect(packageFiles).toBeDefined(); expect(packageFiles).not.toBeNull(); expect(packageFiles!.pop()).toHaveProperty('lockFiles', lockFiles); }); @@ -101,7 +118,8 @@ describe('modules/manager/pip-compile/extract', () => { const lockFiles = ['foo.txt', 'bar.txt']; const packageFiles = await extractAllPackageFiles({}, lockFiles); - expect(packageFiles).toBeDefined(); + expect(fs.readLocalFile).toHaveBeenCalledTimes(4); + expect(packageFiles).not.toBeNull(); packageFiles!.forEach((packageFile) => { expect(packageFile).not.toHaveProperty('packageFile', 'foo.txt'); }); @@ -119,7 +137,7 @@ describe('modules/manager/pip-compile/extract', () => { const lockFiles = ['requirements.txt']; const packageFiles = await extractAllPackageFiles({}, lockFiles); - expect(packageFiles).toBeDefined(); + expect(packageFiles).not.toBeNull(); packageFiles!.forEach((packageFile) => { expect(packageFile).not.toHaveProperty( 'packageFile', @@ -130,18 +148,24 @@ describe('modules/manager/pip-compile/extract', () => { }); it('return null for malformed files', async () => { + // empty.txt fs.readLocalFile.mockResolvedValueOnce(''); + // noHeader.txt fs.readLocalFile.mockResolvedValueOnce( Fixtures.get('requirementsNoHeaders.txt'), ); + // badSource.txt fs.readLocalFile.mockResolvedValueOnce( getSimpleRequirementsFile( - 'pip-compile --output-file=foo.txt malformed.in empty.in', + 'pip-compile --output-file=badSource.txt malformed.in empty.in', ['foo==1.0.1'], ), ); - fs.readLocalFile.mockResolvedValueOnce('!@#$'); // malformed.in - fs.readLocalFile.mockResolvedValueOnce(''); // empty.in + // malformed.in + fs.readLocalFile.mockResolvedValueOnce('!@#$'); + // empty.in + fs.readLocalFile.mockResolvedValueOnce(''); + // headerOnly.txt fs.readLocalFile.mockResolvedValueOnce( getSimpleRequirementsFile( 'pip-compile --output-file=headerOnly.txt reqs.in', @@ -157,6 +181,68 @@ describe('modules/manager/pip-compile/extract', () => { ]; const packageFiles = await extractAllPackageFiles({}, lockFiles); expect(packageFiles).toBeNull(); + expect(fs.readLocalFile).toHaveBeenCalledTimes(6); + expect(logger.warn).toHaveBeenCalledTimes(2); // malformed.in, noHeader.txt + }); + + it('return null for bad paths', async () => { + // ambigous.txt + fs.readLocalFile.mockResolvedValueOnce( + getSimpleRequirementsFile( + 'pip-compile --output-file=../ambigous.txt reqs.in', + ['foo==1.0.1'], + ), + ); + // badSource.txt + fs.readLocalFile.mockResolvedValueOnce( + getSimpleRequirementsFile( + 'pip-compile --output-file=badSource.txt ../outside.in', + ['foo==1.0.1'], + ), + ); + + const packageFiles = await extractAllPackageFiles({}, [ + 'subdir/ambigous.txt', + 'badSource.txt', + ]); + expect(packageFiles).toBeNull(); + expect(fs.readLocalFile).toHaveBeenCalledTimes(2); + expect(logger.warn).toHaveBeenCalledTimes(2); + }); + + it('return for valid paths', async () => { + // reqs.txt + fs.readLocalFile.mockResolvedValueOnce( + getSimpleRequirementsFile('pip-compile --output-file=reqs.txt reqs.in', [ + 'foo==1.0.1', + ]), + ); + fs.readLocalFile.mockResolvedValueOnce('foo>=1.0.0'); + // absolute/reqs.txt + fs.readLocalFile.mockResolvedValueOnce( + getSimpleRequirementsFile( + 'pip-compile --output-file=./absolute/reqs.txt ./absolute/reqs.in', + ['foo==1.0.1'], + ), + ); + fs.readLocalFile.mockResolvedValueOnce('foo>=1.0.0'); + // relative/reqs.txt + fs.readLocalFile.mockResolvedValueOnce( + getSimpleRequirementsFile( + 'pip-compile --output-file=reqs.txt ../outside.in', + ['foo==1.0.1'], + ), + ); + fs.readLocalFile.mockResolvedValueOnce('foo>=1.0.0'); + const packageFiles = await extractAllPackageFiles({}, [ + 'reqs.txt', + 'absolute/reqs.txt', + 'relative/reqs.txt', + ]); + expect(packageFiles?.map((p) => p.packageFile).sort()).toEqual( + ['reqs.in', 'absolute/reqs.in', 'outside.in'].sort(), + ); + expect(logger.warn).toHaveBeenCalledTimes(0); }); it('return sorted package files', async () => { diff --git a/lib/modules/manager/pip-compile/extract.ts b/lib/modules/manager/pip-compile/extract.ts index e3624ffc68..ed011867f7 100644 --- a/lib/modules/manager/pip-compile/extract.ts +++ b/lib/modules/manager/pip-compile/extract.ts @@ -1,5 +1,7 @@ +import upath from 'upath'; import { logger } from '../../../logger'; import { readLocalFile } from '../../../util/fs'; +import { ensureLocalPath } from '../../../util/fs/util'; import { normalizeDepName } from '../../datasource/pypi/common'; import { extractPackageFile as extractRequirementsFile } from '../pip_requirements/extract'; import { extractPackageFile as extractSetupPyFile } from '../pip_setup'; @@ -10,7 +12,11 @@ import type { PipCompileArgs, SupportedManagers, } from './types'; -import { generateMermaidGraph, sortPackageFiles } from './utils'; +import { + generateMermaidGraph, + inferCommandExecDir, + sortPackageFiles, +} from './utils'; function matchManager(filename: string): SupportedManagers | 'unknown' { if (filename.endsWith('setup.py')) { @@ -63,7 +69,6 @@ export async function extractAllPackageFiles( logger.trace('pip-compile.extractAllPackageFiles()'); const lockFileArgs = new Map<string, PipCompileArgs>(); const depsBetweenFiles: DependencyBetweenFiles[] = []; - // for debugging only ^^^ (for now) const packageFiles = new Map<string, PackageFile>(); for (const fileMatch of fileMatches) { const fileContent = await readLocalFile(fileMatch, 'utf8'); @@ -72,8 +77,10 @@ export async function extractAllPackageFiles( continue; } let compileArgs: PipCompileArgs; + let compileDir: string; try { compileArgs = extractHeaderCommand(fileContent, fileMatch); + compileDir = inferCommandExecDir(fileMatch, compileArgs.outputFile); } catch (error) { logger.warn({ fileMatch }, `pip-compile: ${error.message}`); continue; @@ -95,7 +102,19 @@ export async function extractAllPackageFiles( continue; } - for (const packageFile of compileArgs.sourceFiles) { + for (const relativeSourceFile of compileArgs.sourceFiles) { + const packageFile = upath.normalizeTrim( + upath.join(compileDir, relativeSourceFile), + ); + try { + ensureLocalPath(packageFile); + } catch (error) { + logger.warn( + { fileMatch, packageFile }, + 'pip-compile: Source file path outside of repository', + ); + continue; + } depsBetweenFiles.push({ sourceFile: packageFile, outputFile: fileMatch, diff --git a/lib/modules/manager/pip-compile/utils.spec.ts b/lib/modules/manager/pip-compile/utils.spec.ts new file mode 100644 index 0000000000..3ece0c51d8 --- /dev/null +++ b/lib/modules/manager/pip-compile/utils.spec.ts @@ -0,0 +1,29 @@ +import { inferCommandExecDir } from './utils'; + +describe('modules/manager/pip-compile/utils', () => { + describe('inferCommandExecDir()', () => { + it.each([ + { + fileName: 'subdir/reqs.txt', + outputFile: 'subdir/reqs.txt', + result: '.', + }, + { + fileName: 'subdir/reqs.txt', + outputFile: 'reqs.txt', + result: 'subdir', + }, + ])( + 'returns object on correct options', + ({ fileName, outputFile, result }) => { + expect(inferCommandExecDir(fileName, outputFile)).toEqual(result); + }, + ); + + it('throw if --output-file basename differs from path', () => { + expect(() => + inferCommandExecDir('subdir/requirements.txt', 'hey.txt'), + ).toThrow(/mismatch/); + }); + }); +}); diff --git a/lib/modules/manager/pip-compile/utils.ts b/lib/modules/manager/pip-compile/utils.ts index d87a53e53f..50fcdde658 100644 --- a/lib/modules/manager/pip-compile/utils.ts +++ b/lib/modules/manager/pip-compile/utils.ts @@ -1,4 +1,6 @@ import { Graph } from 'graph-data-structure'; +import upath from 'upath'; +import { logger } from '../../../logger'; import type { PackageFile } from '../types'; import type { DependencyBetweenFiles, PipCompileArgs } from './types'; @@ -50,3 +52,46 @@ export function generateMermaidGraph( }); return `graph TD\n${lockFiles.join('\n')}\n${edges.join('\n')}`; } + +export function inferCommandExecDir( + outputFilePath: string, + outputFileArg: string | undefined, +): string { + if (!outputFileArg) { + // implicit output file is in the same directory where command was executed + return upath.normalize(upath.dirname(outputFilePath)); + } + if (upath.normalize(outputFileArg).startsWith('..')) { + throw new Error( + `Cannot infer command execution directory from path ${outputFileArg}`, + ); + } + if (upath.basename(outputFileArg) !== upath.basename(outputFilePath)) { + throw new Error( + `Output file name mismatch: ${upath.basename(outputFileArg)} vs ${upath.basename(outputFilePath)}`, + ); + } + const outputFileDir = upath.normalize(upath.dirname(outputFileArg)); + let commandExecDir = upath.normalize(upath.dirname(outputFilePath)); + + for (const dir of outputFileDir.split('/').reverse()) { + if (commandExecDir.endsWith(dir)) { + commandExecDir = upath.join(commandExecDir.slice(0, -dir.length), '.'); + // outputFileDir = upath.join(outputFileDir.slice(0, -dir.length), '.'); + } else { + break; + } + } + commandExecDir = upath.normalizeTrim(commandExecDir); + if (commandExecDir !== '.') { + logger.debug( + { + commandExecDir, + outputFileArg, + outputFilePath, + }, + `pip-compile: command was not executed in repository root`, + ); + } + return commandExecDir; +} -- GitLab