diff --git a/lib/modules/manager/nix/__fixtures__/flake.1.lock b/lib/modules/manager/nix/__fixtures__/flake.1.lock new file mode 100644 index 00000000000000..5999137c97311a --- /dev/null +++ b/lib/modules/manager/nix/__fixtures__/flake.1.lock @@ -0,0 +1,7 @@ +{ + "nodes": { + "root": {} + }, + "root": "root", + "version": 7 +} diff --git a/lib/modules/manager/nix/__fixtures__/flake.2.lock b/lib/modules/manager/nix/__fixtures__/flake.2.lock new file mode 100644 index 00000000000000..8bbf80d08a6d05 --- /dev/null +++ b/lib/modules/manager/nix/__fixtures__/flake.2.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1720031269, + "narHash": "sha256-rwz8NJZV+387rnWpTYcXaRNvzUSnnF9aHONoJIYmiUQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9f4128e00b0ae8ec65918efeba59db998750ead6", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/lib/modules/manager/nix/__fixtures__/flake.3.lock b/lib/modules/manager/nix/__fixtures__/flake.3.lock new file mode 100644 index 00000000000000..4e715e4b122a14 --- /dev/null +++ b/lib/modules/manager/nix/__fixtures__/flake.3.lock @@ -0,0 +1,26 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1728650607, + "narHash": "sha256-0lOnVTzRXzpk5uxbHLm3Ti3tyPAvirAIQDfwEUd8arg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "612ee628421ba2c1abca4c99684862f76cb3b089", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/lib/modules/manager/nix/__fixtures__/flake.4.lock b/lib/modules/manager/nix/__fixtures__/flake.4.lock new file mode 100644 index 00000000000000..a11067f50b38fd --- /dev/null +++ b/lib/modules/manager/nix/__fixtures__/flake.4.lock @@ -0,0 +1,44 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1672057183, + "narHash": "sha256-GN7/10DNNvs1FPj9tlZA2qgNdFuYKKuS3qlHTqAxasQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b139e44d78c36c69bcbb825b20dbfa51e7738347", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixpkgs-unstable", + "type": "indirect" + } + }, + "patchelf": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1718457448, + "narHash": "sha256-FSoxTcRZMGHNJh8dNtKOkcUtjhmhU6yQXcZZfUPLhQM=", + "ref": "refs/heads/master", + "rev": "a0f54334df36770b335c051e540ba40afcbf8378", + "revCount": 844, + "type": "git", + "url": "https://github.com/NixOS/patchelf.git" + }, + "original": { + "type": "git", + "url": "https://github.com/NixOS/patchelf.git" + } + }, + "root": { + "inputs": { + "patchelf": "patchelf" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/lib/modules/manager/nix/__fixtures__/flake.5.lock b/lib/modules/manager/nix/__fixtures__/flake.5.lock new file mode 100644 index 00000000000000..9e6349fa1f1baa --- /dev/null +++ b/lib/modules/manager/nix/__fixtures__/flake.5.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "ijq": { + "flake": false, + "locked": { + "lastModified": 1723569650, + "narHash": "sha256-Ho/sAhEUeSug52JALgjrKVUPCBe8+PovbJj/lniKxp8=", + "owner": "~gpanders", + "repo": "ijq", + "rev": "88f0d9ae98942bf49cba302c42b2a0f6e05f9b58", + "type": "sourcehut" + }, + "original": { + "owner": "~gpanders", + "repo": "ijq", + "type": "sourcehut" + } + }, + "root": { + "inputs": { + "ijq": "ijq" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/lib/modules/manager/nix/__fixtures__/flake.6.lock b/lib/modules/manager/nix/__fixtures__/flake.6.lock new file mode 100644 index 00000000000000..3b4e21a8331a78 --- /dev/null +++ b/lib/modules/manager/nix/__fixtures__/flake.6.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "home-manager": { + "flake": false, + "locked": { + "lastModified": 1728650932, + "narHash": "sha256-mGKzqdsRyLnGNl6WjEr7+sghGgBtYHhJQ4mjpgRTCsU=", + "owner": "rycee", + "repo": "home-manager", + "rev": "65ae9c147349829d3df0222151f53f79821c5134", + "type": "gitlab" + }, + "original": { + "owner": "rycee", + "repo": "home-manager", + "type": "gitlab" + } + }, + "root": { + "inputs": { + "home-manager": "home-manager" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/lib/modules/manager/nix/artifacts.spec.ts b/lib/modules/manager/nix/artifacts.spec.ts index 7b741ee82208f4..d9cca2268fdde9 100644 --- a/lib/modules/manager/nix/artifacts.spec.ts +++ b/lib/modules/manager/nix/artifacts.spec.ts @@ -36,17 +36,14 @@ process.env.CONTAINERBASE = 'true'; const config: UpdateArtifactsConfig = {}; const lockMaintenanceConfig = { ...config, isLockFileMaintenance: true }; const updateInputCmd = `nix \ - --extra-experimental-features nix-command \ - --extra-experimental-features flakes \ +--extra-experimental-features 'nix-command flakes' \ flake lock --update-input nixpkgs`; const updateInputTokenCmd = `nix \ - --extra-experimental-features nix-command \ - --extra-experimental-features flakes \ +--extra-experimental-features 'nix-command flakes' \ --extra-access-tokens github.com=token \ flake lock --update-input nixpkgs`; const lockfileMaintenanceCmd = `nix \ - --extra-experimental-features nix-command \ - --extra-experimental-features flakes \ +--extra-experimental-features 'nix-command flakes' \ flake update`; describe('modules/manager/nix/artifacts', () => { @@ -66,7 +63,7 @@ describe('modules/manager/nix/artifacts', () => { it('returns if no flake.lock found', async () => { const execSnapshots = mockExecAll(); const res = await updateArtifacts({ - packageFileName: 'flake.nix', + packageFileName: 'flake.lock', updatedDeps: [], newPackageFileContent: '', config, @@ -86,7 +83,7 @@ describe('modules/manager/nix/artifacts', () => { ); const res = await updateArtifacts({ - packageFileName: 'flake.nix', + packageFileName: 'flake.lock', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: 'some new content', config, @@ -107,7 +104,7 @@ describe('modules/manager/nix/artifacts', () => { fs.readLocalFile.mockResolvedValueOnce('new flake.lock'); const res = await updateArtifacts({ - packageFileName: 'flake.nix', + packageFileName: 'flake.lock', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: 'some new content', config: { ...config, constraints: { python: '3.7' } }, @@ -137,7 +134,7 @@ describe('modules/manager/nix/artifacts', () => { hostRules.find.mockReturnValueOnce({ token: 'token' }); const res = await updateArtifacts({ - packageFileName: 'flake.nix', + packageFileName: 'flake.lock', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: 'some new content', config: { ...config, constraints: { python: '3.7' } }, @@ -167,7 +164,7 @@ describe('modules/manager/nix/artifacts', () => { hostRules.find.mockReturnValueOnce({ token: 'x-access-token:token' }); const res = await updateArtifacts({ - packageFileName: 'flake.nix', + packageFileName: 'flake.lock', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: 'some new content', config: { ...config, constraints: { python: '3.7' } }, @@ -196,7 +193,7 @@ describe('modules/manager/nix/artifacts', () => { fs.readLocalFile.mockResolvedValueOnce('new flake.lock'); const res = await updateArtifacts({ - packageFileName: 'flake.nix', + packageFileName: 'flake.lock', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: '{}', config: { ...config, constraints: { nix: '2.10.0' } }, @@ -241,7 +238,7 @@ describe('modules/manager/nix/artifacts', () => { fs.readLocalFile.mockResolvedValueOnce('new flake.lock'); const res = await updateArtifacts({ - packageFileName: 'flake.nix', + packageFileName: 'flake.lock', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: '{}', config: { ...config, constraints: { nix: '2.10.0' } }, @@ -269,7 +266,7 @@ describe('modules/manager/nix/artifacts', () => { const execSnapshots = mockExecSequence([new Error('exec error')]); const res = await updateArtifacts({ - packageFileName: 'flake.nix', + packageFileName: 'flake.lock', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: '{}', config, @@ -294,7 +291,7 @@ describe('modules/manager/nix/artifacts', () => { fs.readLocalFile.mockResolvedValueOnce('new flake.lock'); const res = await updateArtifacts({ - packageFileName: 'flake.nix', + packageFileName: 'flake.lock', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: '{}', config: lockMaintenanceConfig, @@ -323,7 +320,7 @@ describe('modules/manager/nix/artifacts', () => { fs.readLocalFile.mockResolvedValueOnce('new lock'); const res = await updateArtifacts({ - packageFileName: 'flake.nix', + packageFileName: 'flake.lock', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: 'some new content', config: { diff --git a/lib/modules/manager/nix/artifacts.ts b/lib/modules/manager/nix/artifacts.ts index 411cfb160d6f97..4180f37bbc6f35 100644 --- a/lib/modules/manager/nix/artifacts.ts +++ b/lib/modules/manager/nix/artifacts.ts @@ -22,9 +22,7 @@ export async function updateArtifacts({ return null; } - let cmd = `nix \ - --extra-experimental-features nix-command \ - --extra-experimental-features flakes `; + let cmd = `nix --extra-experimental-features 'nix-command flakes' `; const token = findGithubToken( hostRules.find({ diff --git a/lib/modules/manager/nix/extract.spec.ts b/lib/modules/manager/nix/extract.spec.ts index 9645744c2a9208..b43320f7692dc4 100644 --- a/lib/modules/manager/nix/extract.spec.ts +++ b/lib/modules/manager/nix/extract.spec.ts @@ -1,74 +1,89 @@ import { GitRefsDatasource } from '../../datasource/git-refs'; -import { id as nixpkgsVersioning } from '../../versioning/nixpkgs'; +import { Fixtures } from '../../../../test/fixtures'; import { extractPackageFile } from '.'; +const flake1Lock = Fixtures.get('flake.1.lock'); +const flake2Lock = Fixtures.get('flake.2.lock'); +const flake3Lock = Fixtures.get('flake.3.lock'); +const flake4Lock = Fixtures.get('flake.4.lock'); +const flake5Lock = Fixtures.get('flake.5.lock'); +const flake6Lock = Fixtures.get('flake.6.lock'); + describe('modules/manager/nix/extract', () => { - it('returns null when no nixpkgs', () => { - const content = `{ - inputs = {}; -}`; - const res = extractPackageFile(content); + it('returns null when no inputs', () => { + const res = extractPackageFile(flake1Lock, 'flake.lock'); expect(res).toBeNull(); }); - it('returns nixpkgs', () => { - const content = `{ - inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-21.11"; - }; -}`; - - const res = extractPackageFile(content); + it('returns nixpkgs input', () => { + const res = extractPackageFile(flake2Lock, 'flake.lock'); expect(res?.deps).toEqual([ { depName: 'nixpkgs', - currentValue: 'nixos-21.11', + currentDigest: '9f4128e00b0ae8ec65918efeba59db998750ead6', datasource: GitRefsDatasource.id, packageName: 'https://github.com/NixOS/nixpkgs', - versioning: nixpkgsVersioning, }, ]); }); - it('is case insensitive', () => { - const content = `{ - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-21.11"; - }; -}`; + it('includes nixpkgs with no explicit ref', () => { + const res = extractPackageFile(flake3Lock, 'flake.lock'); - const res = extractPackageFile(content); + expect(res).toMatchObject({ + deps: [ + { + currentDigest: '612ee628421ba2c1abca4c99684862f76cb3b089', + datasource: 'git-refs', + depName: 'nixpkgs', + packageName: 'https://github.com/NixOS/nixpkgs', + }, + ], + }); + }); - expect(res?.deps).toEqual([ - { - depName: 'nixpkgs', - currentValue: 'nixos-21.11', - datasource: GitRefsDatasource.id, - packageName: 'https://github.com/NixOS/nixpkgs', - versioning: nixpkgsVersioning, - }, - ]); + it('includes patchelf from HEAD', () => { + const res = extractPackageFile(flake4Lock, 'flake.lock'); + + expect(res).toMatchObject({ + deps: [ + { + currentDigest: 'a0f54334df36770b335c051e540ba40afcbf8378', + datasource: 'git-refs', + depName: 'patchelf', + packageName: 'https://github.com/NixOS/patchelf.git', + }, + ], + }); }); - it('includes nixpkgs with no explicit ref', () => { - const content = `{ - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs"; - }; -}`; + it('includes ijq from sourcehut without a flake', () => { + const res = extractPackageFile(flake5Lock, 'flake.lock'); + + expect(res).toMatchObject({ + deps: [ + { + currentDigest: '88f0d9ae98942bf49cba302c42b2a0f6e05f9b58', + datasource: 'git-refs', + depName: 'ijq', + packageName: 'https://git.sr.ht/~gpanders/ijq', + }, + ], + }); + }); - const res = extractPackageFile(content); + it('includes home-manager from gitlab', () => { + const res = extractPackageFile(flake6Lock, 'flake.lock'); expect(res).toMatchObject({ deps: [ { - currentValue: undefined, + currentDigest: '65ae9c147349829d3df0222151f53f79821c5134', datasource: 'git-refs', - depName: 'nixpkgs', - packageName: 'https://github.com/NixOS/nixpkgs', - versioning: 'nixpkgs', + depName: 'home-manager', + packageName: 'https://gitlab.com/rycee/home-manager', }, ], }); diff --git a/lib/modules/manager/nix/extract.ts b/lib/modules/manager/nix/extract.ts index 5170e23a07dfe0..a046b72182ff9c 100644 --- a/lib/modules/manager/nix/extract.ts +++ b/lib/modules/manager/nix/extract.ts @@ -1,23 +1,90 @@ -import { regEx } from '../../../util/regex'; import { GitRefsDatasource } from '../../datasource/git-refs'; +import { logger } from '../../../logger'; import { id as nixpkgsVersioning } from '../../versioning/nixpkgs'; import type { PackageDependency, PackageFileContent } from '../types'; +import { InputType, NixFlakeLock } from './types'; -const nixpkgsRegex = regEx(/"github:nixos\/nixpkgs(\/(?[a-z0-9-.]+))?"/i); +// TODO: add support to update nixpkgs branches in flakes.nix using nixpkgsVersioning + +export function extractPackageFile( + content: string, + packageFile: string, +): PackageFileContent | null { + logger.trace(`nix.extractPackageFile(${packageFile})`); -export function extractPackageFile(content: string): PackageFileContent | null { const deps: PackageDependency[] = []; + let lock: NixFlakeLock; + + try { + lock = JSON.parse(content); + } catch { + logger.debug({ packageFile }, `Invalid JSON`); + return null; + } + + if (lock.version !== 7) { + logger.debug({ packageFile }, 'Unsupported flake lock version'); + return null; + } + + for (const depName of Object.keys(lock.nodes ?? {})) { + // the root input is a magic string for the entrypoint and only references other flake inputs + if (depName === 'root') { + continue; + } + + const flakeInput = lock.nodes[depName]; + const flakeLocked = flakeInput.locked; + const flakeOriginal = flakeInput.original; + + if (flakeLocked === undefined || flakeOriginal === undefined) { + logger.debug( + { packageFile }, + `Found empty flake input '${JSON.stringify(flakeInput)}', skipping`, + ); + continue; + } + + // indirect inputs cannot be updated via normal means + if (flakeOriginal.type === InputType.indirect) { + continue; + } - const match = nixpkgsRegex.exec(content); - if (match?.groups) { - const { ref } = match.groups; - deps.push({ - depName: 'nixpkgs', - currentValue: ref, - datasource: GitRefsDatasource.id, - packageName: 'https://github.com/NixOS/nixpkgs', - versioning: nixpkgsVersioning, - }); + if (flakeLocked.type === InputType.github) { + deps.push({ + depName, + currentDigest: flakeLocked.rev, + datasource: GitRefsDatasource.id, + packageName: `https://github.com/${flakeOriginal.owner}/${flakeOriginal.repo}`, + }); + } else if (flakeLocked.type === InputType.gitlab) { + deps.push({ + depName, + currentDigest: flakeLocked.rev, + datasource: GitRefsDatasource.id, + packageName: `https://gitlab.com/${flakeOriginal.owner}/${flakeOriginal.repo}`, + }); + } else if (flakeOriginal.type === InputType.git) { + deps.push({ + depName, + currentDigest: flakeLocked.rev, + datasource: GitRefsDatasource.id, + packageName: flakeOriginal.url, + }); + } else if (flakeLocked.type === InputType.sourcehut) { + deps.push({ + depName, + currentDigest: flakeLocked.rev, + datasource: GitRefsDatasource.id, + packageName: `https://git.sr.ht/${flakeOriginal.owner}/${flakeOriginal.repo}`, + }); + } else { + logger.debug( + { packageFile }, + `Unknown flake.lock type "${flakeLocked.type}", skipping`, + ); + continue; + } } if (deps.length) { diff --git a/lib/modules/manager/nix/index.ts b/lib/modules/manager/nix/index.ts index 7cdfa48cdeb1bd..9444bce8d06616 100644 --- a/lib/modules/manager/nix/index.ts +++ b/lib/modules/manager/nix/index.ts @@ -6,8 +6,8 @@ export { updateArtifacts } from './artifacts'; export const supportsLockFileMaintenance = true; export const defaultConfig = { - fileMatch: ['(^|/)flake\\.nix$'], - commitMessageTopic: 'nixpkgs', + fileMatch: ['(^|/)flake\\.lock'], + commitMessageTopic: 'nix', commitMessageExtra: 'to {{newValue}}', enabled: false, }; diff --git a/lib/modules/manager/nix/readme.md b/lib/modules/manager/nix/readme.md index 3a196a34445c0e..c1357a8519bd20 100644 --- a/lib/modules/manager/nix/readme.md +++ b/lib/modules/manager/nix/readme.md @@ -1,4 +1,4 @@ The [`nix`](https://github.com/NixOS/nix) manager supports: - [`lockFileMaintenance`](../../../configuration-options.md#lockfilemaintenance) updates for `flake.lock` -- [nixpkgs](https://github.com/NixOS/nixpkgs) updates +- input updates for `flake.lock` diff --git a/lib/modules/manager/nix/types.ts b/lib/modules/manager/nix/types.ts new file mode 100644 index 00000000000000..c0d65791e4ce7e --- /dev/null +++ b/lib/modules/manager/nix/types.ts @@ -0,0 +1,39 @@ +export interface NixFlakeLock { + readonly nodes: Record; + readonly root: string; + readonly version: number; +} + +export interface NixInput { + readonly inputs?: Record; + readonly locked?: LockedInput; + readonly original?: OriginalInput; +} + +export interface LockedInput { + readonly lastModified: number; + readonly narHash: string; + readonly owner?: string; + readonly repo?: string; + readonly ref?: string; + readonly rev: string; + readonly revCount: number; + readonly type: InputType; + readonly url?: string; +} + +export interface OriginalInput { + readonly owner?: string; + readonly repo?: string; + readonly ref?: string; + readonly type: InputType; + readonly url?: string; +} + +export enum InputType { + git = 'git', + github = 'github', + gitlab = 'gitlab', + indirect = 'indirect', + sourcehut = 'sourcehut', +}