diff --git a/lib/manager/cargo/__snapshots__/artifacts.spec.ts.snap b/lib/manager/cargo/__snapshots__/artifacts.spec.ts.snap index 7e6b944d3ccdcafd27bc3df998212ac21ff50d00..4c0587bb0020975a44fa35a4ff2bf907f02c1c34 100644 --- a/lib/manager/cargo/__snapshots__/artifacts.spec.ts.snap +++ b/lib/manager/cargo/__snapshots__/artifacts.spec.ts.snap @@ -114,3 +114,26 @@ Array [ }, ] `; + +exports[`.updateArtifacts() returns updated workspace Cargo.lock 1`] = ` +Array [ + Object { + "cmd": "cargo update --manifest-path crates/one/Cargo.toml --package dep1", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; diff --git a/lib/manager/cargo/artifacts.spec.ts b/lib/manager/cargo/artifacts.spec.ts index 537d324b27a17f181a3157c2d7b322e77d5775aa..50bba66fa8bdc5e86aa27422f5cad1525b25f6ad 100644 --- a/lib/manager/cargo/artifacts.spec.ts +++ b/lib/manager/cargo/artifacts.spec.ts @@ -35,6 +35,7 @@ describe('.updateArtifacts()', () => { docker.resetPrefetchedImages(); }); it('returns null if no Cargo.lock found', async () => { + fs.stat.mockRejectedValue(new Error('not found!')); const updatedDeps = ['dep1']; expect( await cargo.updateArtifacts({ @@ -56,9 +57,11 @@ describe('.updateArtifacts()', () => { ).toBeNull(); }); it('returns null if unchanged', async () => { + fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any); fs.readFile.mockResolvedValueOnce('Current Cargo.lock' as any); const execSnapshots = mockExecAll(exec); fs.readFile.mockResolvedValueOnce('Current Cargo.lock' as any); + const updatedDeps = ['dep1']; expect( await cargo.updateArtifacts({ @@ -71,6 +74,7 @@ describe('.updateArtifacts()', () => { expect(execSnapshots).toMatchSnapshot(); }); it('returns updated Cargo.lock', async () => { + fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any); git.getFile.mockResolvedValueOnce('Old Cargo.lock'); const execSnapshots = mockExecAll(exec); fs.readFile.mockResolvedValueOnce('New Cargo.lock' as any); @@ -86,7 +90,28 @@ describe('.updateArtifacts()', () => { expect(execSnapshots).toMatchSnapshot(); }); + it('returns updated workspace Cargo.lock', async () => { + fs.stat.mockRejectedValueOnce(new Error('crates/one/Cargo.lock not found')); + fs.stat.mockRejectedValueOnce(new Error('crates/Cargo.lock not found')); + fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any); + + git.getFile.mockResolvedValueOnce('Old Cargo.lock'); + const execSnapshots = mockExecAll(exec); + fs.readFile.mockResolvedValueOnce('New Cargo.lock' as any); + const updatedDeps = ['dep1']; + expect( + await cargo.updateArtifacts({ + packageFileName: 'crates/one/Cargo.toml', + updatedDeps, + newPackageFileContent: '{}', + config, + }) + ).not.toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + it('returns updated Cargo.lock for lockfile maintenance', async () => { + fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any); git.getFile.mockResolvedValueOnce('Old Cargo.lock'); const execSnapshots = mockExecAll(exec); fs.readFile.mockResolvedValueOnce('New Cargo.lock' as any); @@ -102,6 +127,7 @@ describe('.updateArtifacts()', () => { }); it('returns updated Cargo.lock with docker', async () => { + fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any); jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce(); await setExecConfig({ ...config, binarySource: BinarySource.Docker }); git.getFile.mockResolvedValueOnce('Old Cargo.lock'); @@ -119,6 +145,7 @@ describe('.updateArtifacts()', () => { expect(execSnapshots).toMatchSnapshot(); }); it('catches errors', async () => { + fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any); fs.readFile.mockResolvedValueOnce('Current Cargo.lock' as any); fs.outputFile.mockImplementationOnce(() => { throw new Error('not found'); diff --git a/lib/manager/cargo/artifacts.ts b/lib/manager/cargo/artifacts.ts index 9d08e90dbac3efaf4c51fbf60ab8b1c4e735a65b..857a61b8522376a98d318e10b580dabf7912680e 100644 --- a/lib/manager/cargo/artifacts.ts +++ b/lib/manager/cargo/artifacts.ts @@ -2,7 +2,7 @@ import { quote } from 'shlex'; import { logger } from '../../logger'; import { ExecOptions, exec } from '../../util/exec'; import { - getSiblingFileName, + findLocalSiblingOrParent, readLocalFile, writeLocalFile, } from '../../util/fs'; @@ -64,8 +64,16 @@ export async function updateArtifacts({ return null; } - const lockFileName = getSiblingFileName(packageFileName, 'Cargo.lock'); - const existingLockFileContent = await readLocalFile(lockFileName); + // For standalone package crates, the `Cargo.lock` will be in the same + // directory as `Cargo.toml` (ie. a sibling). For cargo workspaces, it + // will be further up. + const lockFileName = await findLocalSiblingOrParent( + packageFileName, + 'Cargo.lock' + ); + const existingLockFileContent = lockFileName + ? await readLocalFile(lockFileName) + : null; if (!existingLockFileContent) { logger.debug('No Cargo.lock found'); return null; diff --git a/lib/util/fs/index.spec.ts b/lib/util/fs/index.spec.ts index 7614d83516cf6ff1532b162847157ede40755528..81fe579b984dd3468bb237334374b5adf144d3b9 100644 --- a/lib/util/fs/index.spec.ts +++ b/lib/util/fs/index.spec.ts @@ -1,5 +1,13 @@ +import { withDir } from 'tmp-promise'; import { getName } from '../../../test/util'; -import { getSubDirectory, localPathExists, readLocalFile } from '.'; +import { + findLocalSiblingOrParent, + getSubDirectory, + localPathExists, + readLocalFile, + setFsConfig, + writeLocalFile, +} from '.'; describe(getName(__filename), () => { describe('readLocalFile', () => { @@ -32,3 +40,59 @@ describe(getName(__filename), () => { }); }); }); + +describe(getName(__filename), () => { + describe('findLocalSiblingOrParent', () => { + it('returns path for file', async () => { + await withDir( + async (localDir) => { + setFsConfig({ + localDir: localDir.path, + }); + + await writeLocalFile('crates/one/Cargo.toml', ''); + await writeLocalFile('Cargo.lock', ''); + + expect( + await findLocalSiblingOrParent( + 'crates/one/Cargo.toml', + 'Cargo.lock' + ) + ).toBe('Cargo.lock'); + expect( + await findLocalSiblingOrParent( + 'crates/one/Cargo.toml', + 'Cargo.mock' + ) + ).toBeNull(); + + await writeLocalFile('crates/one/Cargo.lock', ''); + + expect( + await findLocalSiblingOrParent( + 'crates/one/Cargo.toml', + 'Cargo.lock' + ) + ).toBe('crates/one/Cargo.lock'); + expect( + await findLocalSiblingOrParent('crates/one', 'Cargo.lock') + ).toBe('Cargo.lock'); + expect( + await findLocalSiblingOrParent( + 'crates/one/Cargo.toml', + 'Cargo.mock' + ) + ).toBeNull(); + }, + { + unsafeCleanup: true, + } + ); + }); + + it('immediately returns null when either path is absolute', async () => { + expect(await findLocalSiblingOrParent('/etc/hosts', 'other')).toBeNull(); + expect(await findLocalSiblingOrParent('other', '/etc/hosts')).toBeNull(); + }); + }); +}); diff --git a/lib/util/fs/index.ts b/lib/util/fs/index.ts index da73e844283bc3b590a64f1d1b624cf516694737..d94cef1f8420aaac0cb40a35f189f7f5c265207a 100644 --- a/lib/util/fs/index.ts +++ b/lib/util/fs/index.ts @@ -1,5 +1,5 @@ import * as fs from 'fs-extra'; -import { join, parse } from 'upath'; +import { isAbsolute, join, parse } from 'upath'; import { RenovateConfig } from '../../config/common'; import { logger } from '../../logger'; @@ -96,3 +96,32 @@ export function localPathExists(pathName: string): Promise<boolean> { .then((s) => !!s) .catch(() => false); } + +/** + * Tries to find `otherFileName` in the directory where + * `existingFileNameWithPath` is, then in its parent directory, then in the + * grandparent, until we reach the top-level directory. All paths + * must be relative to `localDir`. + */ +export async function findLocalSiblingOrParent( + existingFileNameWithPath: string, + otherFileName: string +): Promise<string | null> { + if (isAbsolute(existingFileNameWithPath)) { + return null; + } + if (isAbsolute(otherFileName)) { + return null; + } + + let current = existingFileNameWithPath; + while (current !== '') { + current = getSubDirectory(current); + const candidate = join(current, otherFileName); + if (await localPathExists(candidate)) { + return candidate; + } + } + + return null; +}