From 58ed1f45f5869fd81851315ae23a13e709002db7 Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Mon, 14 Mar 2022 19:57:27 +0100 Subject: [PATCH 1/5] Remove unused sourceHashFunc arg from patchPackagefile --- internal.nix | 12 ++++++------ tests/patch-packagefile.nix | 7 +++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/internal.nix b/internal.nix index f097351..52eb3f9 100644 --- a/internal.nix +++ b/internal.nix @@ -209,8 +209,8 @@ rec { }; # Description: Rewrite all the `github:` references to wildcards. - # Type: Fn -> Path -> Set - patchPackagefile = sourceHashFunc: file: + # Type: Path -> Set + patchPackagefile = file: assert (builtins.typeOf file != "path" && builtins.typeOf file != "string") -> throw "file ${toString file} must be a path or string"; let @@ -233,10 +233,10 @@ rec { content // { inherit devDependencies dependencies; }; # Description: Takes a Path to a package file and returns the patched version as file in the Nix store - # Type: Fn -> Path -> Derivation - patchedPackagefile = sourceHashFunc: file: writeText "package.json" + # Type: Path -> Derivation + patchedPackagefile = file: writeText "package.json" ( - builtins.toJSON (patchPackagefile sourceHashFunc file) + builtins.toJSON (patchPackagefile file) ); # Description: Takes a Path to a lockfile and returns the patched version as file in the Nix store @@ -389,7 +389,7 @@ rec { postPatch = '' ln -sf ${patchedLockfile (sourceHashFunc githubSourceHashMap) packageLockJson} package-lock.json - ln -sf ${patchedPackagefile (sourceHashFunc githubSourceHashMap) packageJson} package.json + ln -sf ${patchedPackagefile packageJson} package.json ''; buildPhase = '' diff --git a/tests/patch-packagefile.nix b/tests/patch-packagefile.nix index dc122ea..7cc1d32 100644 --- a/tests/patch-packagefile.nix +++ b/tests/patch-packagefile.nix @@ -1,19 +1,18 @@ { lib, npmlock2nix, testLib }: let - inherit (testLib) noGithubHashes; i = npmlock2nix.internal; in (testLib.runTests { testTurnsGitHubRefsToWildcards = { - expr = (npmlock2nix.internal.patchPackagefile noGithubHashes ./examples-projects/github-dependency/package.json).dependencies.leftpad; + expr = (npmlock2nix.internal.patchPackagefile ./examples-projects/github-dependency/package.json).dependencies.leftpad; expected = "*"; }; testHandlesBranches = { - expr = (npmlock2nix.internal.patchPackagefile noGithubHashes ./examples-projects/github-dependency-branch/package.json).dependencies.leftpad; + expr = (npmlock2nix.internal.patchPackagefile ./examples-projects/github-dependency-branch/package.json).dependencies.leftpad; expected = "*"; }; testHandlesDevDependencies = { - expr = (npmlock2nix.internal.patchPackagefile noGithubHashes ./examples-projects/github-dev-dependency/package.json).devDependencies.leftpad; + expr = (npmlock2nix.internal.patchPackagefile ./examples-projects/github-dev-dependency/package.json).devDependencies.leftpad; expected = "*"; }; }) From 9551a851b691e7b0fe2266bb3223a5f90393b5df Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Mon, 14 Mar 2022 20:07:40 +0100 Subject: [PATCH 2/5] Convert internal sourceHashFunc argument to more general sourceOptions This allows more flexibility for the future, such as support for: - Source patches - Fetch authentication --- internal.nix | 48 +++++++++++++++++++----------------- tests/lib.nix | 2 +- tests/make-github-source.nix | 6 ++--- tests/make-source.nix | 2 +- tests/patch-lockfile.nix | 20 +++++++-------- 5 files changed, 41 insertions(+), 37 deletions(-) diff --git a/internal.nix b/internal.nix index 52eb3f9..163dcf5 100644 --- a/internal.nix +++ b/internal.nix @@ -90,8 +90,8 @@ rec { # `github:org/repo#revision` into a git fetcher. The fetcher can # receive a hash value by calling 'sourceHashFunc' if a source hash # map has been provided. Otherwise the function yields `null`. - # Type: Fn -> String -> Set -> Path - makeGithubSource = sourceHashFunc: name: dependency: + # Type: { sourceHashFunc :: Fn } -> String -> Set -> Path + makeGithubSource = { sourceHashFunc, ... }: name: dependency: assert !(dependency ? version) -> builtins.throw "version` attribute missing from `${name}`"; assert (lib.hasPrefix "github: " dependency.version) -> builtins.throw "invalid prefix for `version` field of `${name}` expected `github:`, got: `${dependency.version}`."; @@ -128,8 +128,8 @@ rec { dependency ? version && dependency ? integrity && ! (dependency ? resolved) && looksLikeUrl dependency.version; # Description: Turns an npm lockfile dependency into a fetchurl derivation - # Type: Fn -> String -> Set -> Derivation - makeSource = sourceHashFunc: name: dependency: + # Type: { sourceHashFunc :: Fn } -> String -> Set -> Derivation + makeSource = sourceOptions: name: dependency: assert (builtins.typeOf name != "string") -> throw "Name of dependency ${toString name} must be a string"; assert (builtins.typeOf dependency != "set") -> @@ -137,9 +137,9 @@ rec { if dependency ? resolved && dependency ? integrity then dependency // { resolved = "file://" + (toString (fetchurl (makeSourceAttrs name dependency))); } else if dependency ? from && dependency ? version then - makeGithubSource sourceHashFunc name dependency + makeGithubSource sourceOptions name dependency else if shouldUseVersionAsUrl dependency then - makeSource sourceHashFunc name (dependency // { resolved = dependency.version; }) + makeSource sourceOptions name (dependency // { resolved = dependency.version; }) else throw "A valid dependency consists of at least the resolved and integrity field. Missing one or both of them for `${name}`. The object I got looks like this: ${builtins.toJSON dependency}"; # Description: Parses the lock file as json and returns an attribute set @@ -158,7 +158,7 @@ rec { # Description: Turns a github string reference into a store path with a tgz of the reference # Type: Fn -> String -> String -> Path - stringToTgzPath = sourceHashFunc: name: str: + stringToTgzPath = { sourceHashFunc, ... }: name: str: let gitAttrs = parseGitHubRef str; in @@ -170,17 +170,17 @@ rec { }; # Description: Patch the `requires` attributes of a dependency spec to refer to paths in the store - # Type: Fn -> String -> Set -> Set - patchRequires = sourceHashFunc: name: requires: + # Type: { sourceHashFunc :: Fn } -> String -> Set -> Set + patchRequires = sourceOptions: name: requires: let - patchReq = name: version: if lib.hasPrefix "github:" version then stringToTgzPath sourceHashFunc name version else version; + patchReq = name: version: if lib.hasPrefix "github:" version then stringToTgzPath sourceOptions name version else version; in lib.mapAttrs patchReq requires; # Description: Patches a single lockfile dependency (recursively) by replacing the resolved URL with a store path - # Type: Fn -> String -> Set -> Set - patchDependency = sourceHashFunc: name: spec: + # Type: { sourceHashFunc :: Fn } -> String -> Set -> Set + patchDependency = sourceOptions: name: spec: assert (builtins.typeOf name != "string") -> throw "Name of dependency ${toString name} must be a string"; assert (builtins.typeOf spec != "set") -> @@ -188,9 +188,9 @@ rec { let isBundled = spec ? bundled && spec.bundled == true; hasGitHubRequires = spec: (spec ? requires) && (lib.any (x: lib.hasPrefix "github:" x) (lib.attrValues spec.requires)); - patchSource = lib.optionalAttrs (!isBundled) (makeSource sourceHashFunc name spec); - patchRequiresSources = lib.optionalAttrs (hasGitHubRequires spec) { requires = (patchRequires sourceHashFunc name spec.requires); }; - patchDependenciesSources = lib.optionalAttrs (spec ? dependencies) { dependencies = lib.mapAttrs (patchDependency sourceHashFunc) spec.dependencies; }; + patchSource = lib.optionalAttrs (!isBundled) (makeSource sourceOptions name spec); + patchRequiresSources = lib.optionalAttrs (hasGitHubRequires spec) { requires = (patchRequires sourceOptions name spec.requires); }; + patchDependenciesSources = lib.optionalAttrs (spec ? dependencies) { dependencies = lib.mapAttrs (patchDependency sourceOptions) spec.dependencies; }; in # For our purposes we need a dependency with # - `resolved` set to a path in the nix store (`patchSource`) @@ -199,13 +199,13 @@ rec { (spec // patchSource // patchRequiresSources // patchDependenciesSources); # Description: Takes a Path to a lockfile and returns the patched version as attribute set - # Type: Fn -> Path -> Set - patchLockfile = sourceHashFunc: file: + # Type: { sourceHashFunc :: Fn } -> Path -> Set + patchLockfile = sourceOptions: file: assert (builtins.typeOf file != "path" && builtins.typeOf file != "string") -> throw "file ${toString file} must be a path or string"; let content = readLockfile file; in content // { - dependencies = lib.mapAttrs (patchDependency sourceHashFunc) content.dependencies; + dependencies = lib.mapAttrs (patchDependency sourceOptions) content.dependencies; }; # Description: Rewrite all the `github:` references to wildcards. @@ -240,9 +240,9 @@ rec { ); # Description: Takes a Path to a lockfile and returns the patched version as file in the Nix store - # Type: Fn -> Path -> Derivation - patchedLockfile = sourceHashFunc: file: writeText "package-lock.json" - (builtins.toJSON (patchLockfile sourceHashFunc file)); + # Type: { sourceHashFunc :: Fn } -> Path -> Derivation + patchedLockfile = sourceOptions: file: writeText "package-lock.json" + (builtins.toJSON (patchLockfile sourceOptions file)); # Description: Turn a derivation (with name & src attribute) into a directory containing the unpacked sources # Type: Derivation -> Derivation @@ -328,6 +328,10 @@ rec { cleanArgs = builtins.removeAttrs args [ "src" "packageJson" "packageLockJson" "buildInputs" "nativeBuildInputs" "nodejs" "preBuild" "postBuild" "preInstallLinks" "githubSourceHashMap" ]; lockfile = readLockfile packageLockJson; + sourceOptions = { + sourceHashFunc = sourceHashFunc githubSourceHashMap; + }; + preinstall_node_modules = writeTextFile { name = "prepare"; destination = "/node_modules/.hooks/prepare"; @@ -388,7 +392,7 @@ rec { ''; postPatch = '' - ln -sf ${patchedLockfile (sourceHashFunc githubSourceHashMap) packageLockJson} package-lock.json + ln -sf ${patchedLockfile sourceOptions packageLockJson} package-lock.json ln -sf ${patchedPackagefile packageJson} package.json ''; diff --git a/tests/lib.nix b/tests/lib.nix index 3c70678..cf2432e 100644 --- a/tests/lib.nix +++ b/tests/lib.nix @@ -10,7 +10,7 @@ # Reads a given file (either drv, path or string) and returns it's sha256 hash hashFile = filename: builtins.hashString "sha256" (builtins.readFile filename); - noGithubHashes = (_: null); + noSourceOptions = { sourceHashFunc = _: null; }; runTests = tests: let diff --git a/tests/make-github-source.nix b/tests/make-github-source.nix index b282e05..f78e9b9 100644 --- a/tests/make-github-source.nix +++ b/tests/make-github-source.nix @@ -1,6 +1,6 @@ { lib, npmlock2nix, testLib }: let - inherit (testLib) noGithubHashes; + inherit (testLib) noSourceOptions; i = npmlock2nix.internal; testDependency = { @@ -12,7 +12,7 @@ in testSimpleCase = { expr = let - version = (i.makeGithubSource noGithubHashes "leftpad" testDependency).version; + version = (i.makeGithubSource noSourceOptions "leftpad" testDependency).version; in lib.hasPrefix "file:///nix/store" version; expected = true; @@ -21,7 +21,7 @@ in testDropsFrom = { expr = let - dep = i.makeGithubSource noGithubHashes "leftpad" testDependency; + dep = i.makeGithubSource noSourceOptions "leftpad" testDependency; in dep ? from; expected = false; diff --git a/tests/make-source.nix b/tests/make-source.nix index 05eb783..ddf913d 100644 --- a/tests/make-source.nix +++ b/tests/make-source.nix @@ -1,7 +1,7 @@ { testLib, npmlock2nix }: let i = npmlock2nix.internal; - f = builtins.throw "Shouldn't be called"; + f = { sourceHashFunc = builtins.throw "Shouldn't be called"; }; in testLib.runTests { testMakeSourceRegular = { diff --git a/tests/patch-lockfile.nix b/tests/patch-lockfile.nix index 0993673..20a0dec 100644 --- a/tests/patch-lockfile.nix +++ b/tests/patch-lockfile.nix @@ -1,13 +1,13 @@ { npmlock2nix, testLib, lib }: let - inherit (testLib) noGithubHashes; + inherit (testLib) noSourceOptions; in testLib.runTests { testPatchDependencyHandlesGitHubRefsInRequires = { expr = let - libxmljsUrl = (npmlock2nix.internal.patchDependency noGithubHashes "test" { + libxmljsUrl = (npmlock2nix.internal.patchDependency noSourceOptions "test" { version = "github:tmcw/leftpad#db1442a0556c2b133627ffebf455a78a1ced64b9"; from = "github:tmcw/leftpad#db1442a0556c2b133627ffebf455a78a1ced64b9"; integrity = "sha512-8/UvHFG90J4O4QNRzb0jB5Ni1QuvuB7XFTLfDMQnCzAsFemF29VKnNGUESFFcSP/r5WWh/PMe0YRz90+3IqsUA=="; @@ -22,7 +22,7 @@ testLib.runTests { }; testBundledDependenciesAreRetained = { - expr = npmlock2nix.internal.patchDependency noGithubHashes "test" { + expr = npmlock2nix.internal.patchDependency noSourceOptions "test" { bundled = true; integrity = "sha1-hrGk3k+s4YCsVFqD8VA1I9j+0RU="; something = "bar"; @@ -37,12 +37,12 @@ testLib.runTests { }; testPatchLockfileWithoutDependencies = { - expr = (npmlock2nix.internal.patchLockfile noGithubHashes ./examples-projects/no-dependencies/package-lock.json).dependencies; + expr = (npmlock2nix.internal.patchLockfile noSourceOptions ./examples-projects/no-dependencies/package-lock.json).dependencies; expected = { }; }; testPatchDependencyDoesntDropAttributes = { - expr = npmlock2nix.internal.patchDependency noGithubHashes "test" { + expr = npmlock2nix.internal.patchDependency noSourceOptions "test" { a = 1; foo = "something"; resolved = "https://examples.com/something.tgz"; @@ -59,7 +59,7 @@ testLib.runTests { }; testPatchDependencyPatchesDependenciesRecursively = { - expr = npmlock2nix.internal.patchDependency noGithubHashes "test" { + expr = npmlock2nix.internal.patchDependency noSourceOptions "test" { a = 1; foo = "something"; resolved = "https://examples.com/something.tgz"; @@ -85,7 +85,7 @@ testLib.runTests { testPatchLockfileTurnsUrlsIntoStorePaths = { expr = let - deps = (npmlock2nix.internal.patchLockfile noGithubHashes ./examples-projects/single-dependency/package-lock.json).dependencies; + deps = (npmlock2nix.internal.patchLockfile noSourceOptions ./examples-projects/single-dependency/package-lock.json).dependencies; in lib.count (dep: lib.hasPrefix "file:///nix/store/" dep.resolved) (lib.attrValues deps); expected = 1; @@ -94,19 +94,19 @@ testLib.runTests { testPatchLockfileTurnsGitHubUrlsIntoStorePaths = { expr = let - leftpad = (npmlock2nix.internal.patchLockfile noGithubHashes ./examples-projects/github-dependency/package-lock.json).dependencies.leftpad; + leftpad = (npmlock2nix.internal.patchLockfile noSourceOptions ./examples-projects/github-dependency/package-lock.json).dependencies.leftpad; in lib.hasPrefix ("file://" + builtins.storeDir) leftpad.version; expected = true; }; testConvertPatchedLockfileToJSON = { - expr = builtins.typeOf (builtins.toJSON (npmlock2nix.internal.patchLockfile noGithubHashes ./examples-projects/nested-dependencies/package-lock.json)) == "string"; + expr = builtins.typeOf (builtins.toJSON (npmlock2nix.internal.patchLockfile noSourceOptions ./examples-projects/nested-dependencies/package-lock.json)) == "string"; expected = true; }; testPatchedLockFile = { - expr = testLib.hashFile (npmlock2nix.internal.patchedLockfile noGithubHashes ./examples-projects/nested-dependencies/package-lock.json); + expr = testLib.hashFile (npmlock2nix.internal.patchedLockfile noSourceOptions ./examples-projects/nested-dependencies/package-lock.json); expected = "980323c3a53d86ab6886f21882936cfe7c06ac633993f16431d79e3185084414"; }; From a74e464d6adc10f0dd1369dc2d2e9e0b17125620 Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Tue, 15 Mar 2022 03:37:36 +0100 Subject: [PATCH 3/5] Add support for patching dependencies Can be used to patchShebangs for specific packages, which with NPM version 7.0 can't be done globally for all packages anymore Could also be a replacement for preInstallLinks --- API.md | 30 ++++ internal.nix | 150 ++++++++++++++++-- .../source-patching/package-lock.json | 13 ++ .../source-patching/package.json | 11 ++ .../source-patching/shell.nix | 25 +++ tests/integration-tests/default.nix | 9 ++ tests/lib.nix | 5 +- tests/make-source.nix | 5 +- 8 files changed, 230 insertions(+), 18 deletions(-) create mode 100644 tests/examples-projects/source-patching/package-lock.json create mode 100644 tests/examples-projects/source-patching/package.json create mode 100644 tests/examples-projects/source-patching/shell.nix diff --git a/API.md b/API.md index ea50b97..a81f6b5 100644 --- a/API.md +++ b/API.md @@ -16,6 +16,7 @@ The `node_modules` function takes an attribute set with the following attributes - **nodejs** *(default `nixpkgs.nodejs`, which is the Active LTS version)*: Node.js derivation to use - **preInstallLinks** *(default `{}`)*: Map of symlinks to create inside npm dependencies in the `node_modules` output (See [Concepts](#concepts) for details). - **githubSourceHashMap** *(default `{}`)*: Dependency hashes for evaluation in restricted mode (See [Concepts](#concepts) for details). +- **sourceAttrs** *(default `{}`)*: Derivation attributes to apply to sources, allowing patching (See [Concepts](#concepts) for details) #### Notes - You may provide additional arguments accepted by `mkDerivation` all of which are going to be passed on. @@ -114,3 +115,32 @@ npmlock2nix.build { }; } ``` + +### Source derivation attributes + +`node_modules` takes a `sourceAttrs` argument, which allows you to modify the source derivations of individual npm packages you depend on, mainly useful for adding Nix-specific fixes to packages. This could be used for patching interpreter or paths, or to replace vendored binaries with ones provided by Nix. + +The `sourceAttrs` argument expects an attribute set mapping npm package names to a function describing the modifications of that package. Each function receives an attribute set as an argument containing either a `version` attribute if the version is known, or a `github = { org, repo, rev, ref }` attribute if the package is fetched from GitHub. These values can be used to decide what the result of the function should be. The result of this function is passed to a `mkDerivation` call running mainly the [patch phase](https://nixos.org/manual/nixpkgs/stable/#ssec-patch-phase), meaning that [most `mkDerivation` arguments](https://nixos.org/manual/nixpkgs/stable/#chap-stdenv) can be returned from it. Of particular interest are `patches` and `postPatch`, in which `patchShebangs` can be called. Note that `patchShebangs` can only patch shebangs to binaries accessible in the derivation, which you can extend with `buildInputs`. For convenience, the correct version of `nodejs` is always included in `buildInputs`. + +```nix +npmlock2nix.node_modules { + sourceMods = { + # sourceInfo either contains: + # - A version attribute + # - A github = { org, repo, rev, ref } attribute for GitHub sources + package-name = sourceInfo: { + buildInputs = [ somePackage ]; + patches = [ somePatch ]; + postPatch = '' + some script + ''; + # ... + } + + # Example + node-pre-gyp = sourceInfo: { + postPatch = "patchShebangs bin"; + }; + }; +} +``` diff --git a/internal.nix b/internal.nix index 163dcf5..b91111c 100644 --- a/internal.nix +++ b/internal.nix @@ -1,4 +1,4 @@ -{ nodejs-14_x, stdenv, mkShell, lib, fetchurl, writeText, writeTextFile, runCommand, fetchFromGitHub }: +{ nodejs-14_x, jq, openssl, stdenv, mkShell, lib, fetchurl, writeText, writeTextFile, runCommand, fetchFromGitHub }: rec { # Versions >= 15 use npm >= 7, which uses npm lockfile version 2, which we don't support yet # See the assertion in the node_modules function @@ -62,7 +62,7 @@ rec { # hash attribute it will provide the value to `fetchFromGitHub` which will # also work in restricted evaluation. # Type: Set -> Path - buildTgzFromGitHub = { name, org, repo, rev, ref, hash ? null }: + buildTgzFromGitHub = { name, org, repo, rev, ref, hash ? null, sourceOptions ? { } }: let src = if hash != null then @@ -78,20 +78,44 @@ rec { inherit rev ref; allRefs = true; }; + + attrs = { + pname = name; + version = ref; + src = src; + } // lib.optionalAttrs (sourceOptions ? sourceAttrs.${name}) + (sourceOptions.sourceAttrs.${name} { + github = { inherit org repo rev ref; }; + }); in - runCommand - name - { } '' - set +x - tar -C ${src} -czf $out ./ + packTgz sourceOptions.nodejs attrs; + + # Description: Packs a source directory into a .tgz tar archive, allowing the + # source to be modified using derivation attributes. If the source is an + # archive, it gets unpacked first. + # Type: Path -> Set -> Path + packTgz = nodejs: attrs@{ pname, version, src, ... }: stdenv.mkDerivation ({ + name = "${pname}-${version}.tgz"; + phases = "unpackPhase patchPhase installPhase"; + installPhase = '' + runHook preInstall + tar -C . -czf $out ./ + runHook postInstall ''; + } // attrs // { + buildInputs = attrs.buildInputs or [ ] ++ [ + # Allows patchShebangs in postPatch to patch shebangs to nodejs + nodejs + ]; + }); # Description: Turns a dependency with a from field of the format # `github:org/repo#revision` into a git fetcher. The fetcher can # receive a hash value by calling 'sourceHashFunc' if a source hash - # map has been provided. Otherwise the function yields `null`. + # map has been provided. Otherwise the function yields `null`. Patches + # specified with sourceAttrs will be applied # Type: { sourceHashFunc :: Fn } -> String -> Set -> Path - makeGithubSource = { sourceHashFunc, ... }: name: dependency: + makeGithubSource = sourceOptions@{ sourceHashFunc, ... }: name: dependency: assert !(dependency ? version) -> builtins.throw "version` attribute missing from `${name}`"; assert (lib.hasPrefix "github: " dependency.version) -> builtins.throw "invalid prefix for `version` field of `${name}` expected `github:`, got: `${dependency.version}`."; @@ -108,6 +132,7 @@ rec { ref = f.rev; inherit (v) org repo rev; hash = sourceHashFunc { type = "github"; value = v; }; + inherit sourceOptions; }; in (builtins.removeAttrs dependency [ "from" ]) // { @@ -127,6 +152,35 @@ rec { shouldUseVersionAsUrl = dependency: dependency ? version && dependency ? integrity && ! (dependency ? resolved) && looksLikeUrl dependency.version; + # Description: Replaces the `resolved` field of a dependency with a + # prefetched version from the Nix store. Patches specified with sourceAttrs + # will be applied, in which case the `integrity` attribute is set to `null`, + # in order to be recomputer later + # Type: { sourceAttrs :: Fn, nodejs :: Package } -> String -> Set -> Set + makeUrlSource = { sourceAttrs ? { }, nodejs, ... }: name: dependency: + let + src = fetchurl (makeSourceAttrs name dependency); + attrs = { + pname = name; + version = dependency.version; + src = src; + } // sourceAttrs.${name} { + inherit (dependency) version; + }; + tgz = + if sourceAttrs ? ${name} + # If we have modification to this source, unpack the tgz, apply the + # patches and repack the tgz + then packTgz nodejs attrs + else src; + resolved = "file://" + toString tgz; + in + dependency // { inherit resolved; } // lib.optionalAttrs (sourceAttrs ? ${name}) { + # Integrity was tampered with due to the source attributes, so it needs + # to be recalculated, which is done in the node_modules builder + integrity = null; + }; + # Description: Turns an npm lockfile dependency into a fetchurl derivation # Type: { sourceHashFunc :: Fn } -> String -> Set -> Derivation makeSource = sourceOptions: name: dependency: @@ -135,7 +189,7 @@ rec { assert (builtins.typeOf dependency != "set") -> throw "Specification of dependency ${toString name} must be a set"; if dependency ? resolved && dependency ? integrity then - dependency // { resolved = "file://" + (toString (fetchurl (makeSourceAttrs name dependency))); } + makeUrlSource sourceOptions name dependency else if dependency ? from && dependency ? version then makeGithubSource sourceOptions name dependency else if shouldUseVersionAsUrl dependency then @@ -158,7 +212,7 @@ rec { # Description: Turns a github string reference into a store path with a tgz of the reference # Type: Fn -> String -> String -> Path - stringToTgzPath = { sourceHashFunc, ... }: name: str: + stringToTgzPath = sourceOptions@{ sourceHashFunc, ... }: name: str: let gitAttrs = parseGitHubRef str; in @@ -167,6 +221,7 @@ rec { ref = gitAttrs.rev; inherit (gitAttrs) org repo rev; hash = sourceHashFunc { type = "github"; value = gitAttrs; }; + inherit sourceOptions; }; # Description: Patch the `requires` attributes of a dependency spec to refer to paths in the store @@ -316,6 +371,7 @@ rec { , preBuild ? "" , postBuild ? "" , preInstallLinks ? { } # set that describes which files should be linked in a specific packages folder + , sourceAttrs ? { } , githubSourceHashMap ? { } , passthru ? { } , ... @@ -325,13 +381,17 @@ rec { assert (builtins.typeOf preInstallLinks != "set") -> throw "`preInstallLinks` must be an attributeset of attributesets"; let - cleanArgs = builtins.removeAttrs args [ "src" "packageJson" "packageLockJson" "buildInputs" "nativeBuildInputs" "nodejs" "preBuild" "postBuild" "preInstallLinks" "githubSourceHashMap" ]; + cleanArgs = builtins.removeAttrs args [ "src" "packageJson" "packageLockJson" "buildInputs" "nativeBuildInputs" "nodejs" "preBuild" "postBuild" "preInstallLinks" "sourceAttrs" "githubSourceHashMap" ]; lockfile = readLockfile packageLockJson; sourceOptions = { sourceHashFunc = sourceHashFunc githubSourceHashMap; + inherit nodejs sourceAttrs; }; + patchedLockfilePath = patchedLockfile sourceOptions packageLockJson; + patchedPackagefilePath = patchedPackagefile packageJson; + preinstall_node_modules = writeTextFile { name = "prepare"; destination = "/node_modules/.hooks/prepare"; @@ -376,6 +436,8 @@ rec { dontUnpack = true; nativeBuildInputs = nativeBuildInputs ++ [ + jq + openssl nodejs ]; @@ -391,9 +453,65 @@ rec { export HOME=$(mktemp -d) ''; + # A jq filter for finding dependencies with an integrity field of + # `null`, as set at evaluation time by `makeUrlSource`, in the + # package-lock.json file. The output format is a newline separated list + # of entries, where each entry contains a JSON object path of the + # integrity field and the corresponding resolved file path, separated + # by a tab, ready for shell consumption + jqFindNullIntegrity = '' + # Processes dependencies entries as { key, value } pairs + def process(prefix): + (prefix + [ .key ]) as $path | .value | + ( + # If we have an integrity attribute that is null, output an entry + if has("integrity") and .integrity == null + then + [ ($path + ["integrity"] | @json) + , (.resolved | ltrimstr("file://")) + ] + else empty + end + , + # Recurse into .dependencies, this won't be necessary for + # lockfile version 2 + if has("dependencies") + then .dependencies | to_entries[] | process($path + ["dependencies"]) + else empty + end + ); + .dependencies | to_entries[] | process(["dependencies"]) + # Does the newline/tab separated thing, nice for shell consumption + | @tsv + ''; + + # A script for updating specific JSON paths (.path) with specific + # values (.value), as given in a list of objects, of an $original[0] + # JSON value + jqSetIntegrity = '' + reduce .[] as $update + ( $original[0] + ; . * setpath($update | .path; $update | .value) + ) + ''; + + passAsFile = [ "jqFindNullIntegrity" "jqSetIntegrity" ]; + postPatch = '' - ln -sf ${patchedLockfile sourceOptions packageLockJson} package-lock.json - ln -sf ${patchedPackagefile packageJson} package.json + # Patches the lockfile at build time to replace the `"integrity": + # null` entries as set by `makeUrlSource` at eval time. + jq -r -f "$jqFindNullIntegrityPath" ${patchedLockfilePath} | while IFS=$'\t' read jsonpath file; do + + # https://docs.npmjs.com/cli/v8/configuring-npm/package-lock-json#packages + # https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity#tools_for_generating_sri_hashes + hash="sha512-$(openssl dgst -sha512 -binary "$file" | openssl base64 -A)" + + # Constructs a simple { path, value } JSON of the given arguments + jq -c --argjson path "$jsonpath" --arg value "$hash" -n '$ARGS.named' + + done | jq -s --slurpfile original ${patchedLockfilePath} -f "$jqSetIntegrityPath" > package-lock.json + + ln -sf ${patchedPackagefilePath} package.json ''; buildPhase = '' @@ -422,8 +540,8 @@ rec { passthru = passthru // { inherit nodejs; - lockfile = patchedLockfile packageLockJson; - packagesfile = patchedPackagefile packageJson; + lockfile = patchedLockfilePath; + packagesfile = patchedPackagefilePath; }; } // cleanArgs); diff --git a/tests/examples-projects/source-patching/package-lock.json b/tests/examples-projects/source-patching/package-lock.json new file mode 100644 index 0000000..889f3e3 --- /dev/null +++ b/tests/examples-projects/source-patching/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "source-patching", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "custom-hello-world": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/custom-hello-world/-/custom-hello-world-1.0.3.tgz", + "integrity": "sha512-Rinkq1q+uLmgbDLZRNZrlyeK3G9e+o0gbVy6r948kySbau0ymwOPNybu4iIkb4R1gE5aZsQUHgtfCg2oZ4zfbw==" + } + } +} diff --git a/tests/examples-projects/source-patching/package.json b/tests/examples-projects/source-patching/package.json new file mode 100644 index 0000000..d0ea4e4 --- /dev/null +++ b/tests/examples-projects/source-patching/package.json @@ -0,0 +1,11 @@ +{ + "name": "source-patching", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "", + "license": "ISC", + "dependencies": { + "custom-hello-world": "^1.0.0" + } +} diff --git a/tests/examples-projects/source-patching/shell.nix b/tests/examples-projects/source-patching/shell.nix new file mode 100644 index 0000000..a741e19 --- /dev/null +++ b/tests/examples-projects/source-patching/shell.nix @@ -0,0 +1,25 @@ +{ npmlock2nix }: +npmlock2nix.shell { + src = ./.; + node_modules_attrs = { + sourceAttrs = { + custom-hello-world = _: { + patches = builtins.toFile "custom-hello-world.patch" '' + diff --git a/lib/index.js b/lib/index.js + index 1f66513..64391a7 100644 + --- a/lib/index.js + +++ b/lib/index.js + @@ -21,7 +21,7 @@ function generateHelloWorld({ comma, exclamation, lowercase }) { + if (comma) + helloWorldStr += ','; + + - helloWorldStr += ' World'; + + helloWorldStr += ' Nix'; + + if (exclamation) + helloWorldStr += '!'; + ''; + }; + }; + }; +} diff --git a/tests/integration-tests/default.nix b/tests/integration-tests/default.nix index 1813fa1..7aa485b 100644 --- a/tests/integration-tests/default.nix +++ b/tests/integration-tests/default.nix @@ -284,4 +284,13 @@ testLib.makeIntegrationTests { ''; }; }; + + source-patching = { + description = "Source patching works"; + shell = callPackage ../examples-projects/source-patching/shell.nix { }; + command = '' + node -e 'console.log(require("custom-hello-world")({}));' + ''; + expected = "Hello Nix\n"; + }; } diff --git a/tests/lib.nix b/tests/lib.nix index cf2432e..8764b3c 100644 --- a/tests/lib.nix +++ b/tests/lib.nix @@ -10,7 +10,10 @@ # Reads a given file (either drv, path or string) and returns it's sha256 hash hashFile = filename: builtins.hashString "sha256" (builtins.readFile filename); - noSourceOptions = { sourceHashFunc = _: null; }; + noSourceOptions = { + sourceHashFunc = _: null; + nodejs = null; + }; runTests = tests: let diff --git a/tests/make-source.nix b/tests/make-source.nix index ddf913d..dd44426 100644 --- a/tests/make-source.nix +++ b/tests/make-source.nix @@ -1,7 +1,10 @@ { testLib, npmlock2nix }: let i = npmlock2nix.internal; - f = { sourceHashFunc = builtins.throw "Shouldn't be called"; }; + f = { + sourceHashFunc = builtins.throw "Shouldn't be called"; + nodejs = null; + }; in testLib.runTests { testMakeSourceRegular = { From bbb6fd47a820a550568277dc073d487a5b5f73b6 Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Thu, 31 Mar 2022 18:50:08 +0200 Subject: [PATCH 4/5] Collect necessary integrity changes with Nix Instead of jq --- internal.nix | 113 +++++++++++++++++++-------------------- tests/patch-lockfile.nix | 26 ++++----- 2 files changed, 68 insertions(+), 71 deletions(-) diff --git a/internal.nix b/internal.nix index b91111c..ae5ef67 100644 --- a/internal.nix +++ b/internal.nix @@ -234,8 +234,8 @@ rec { # Description: Patches a single lockfile dependency (recursively) by replacing the resolved URL with a store path - # Type: { sourceHashFunc :: Fn } -> String -> Set -> Set - patchDependency = sourceOptions: name: spec: + # Type: List String -> { sourceHashFunc :: Fn } -> String -> Set -> { result :: Set, integrityUpdates :: List { path, file } } + patchDependency = path: sourceOptions: name: spec: assert (builtins.typeOf name != "string") -> throw "Name of dependency ${toString name} must be a string"; assert (builtins.typeOf spec != "set") -> @@ -245,22 +245,38 @@ rec { hasGitHubRequires = spec: (spec ? requires) && (lib.any (x: lib.hasPrefix "github:" x) (lib.attrValues spec.requires)); patchSource = lib.optionalAttrs (!isBundled) (makeSource sourceOptions name spec); patchRequiresSources = lib.optionalAttrs (hasGitHubRequires spec) { requires = (patchRequires sourceOptions name spec.requires); }; - patchDependenciesSources = lib.optionalAttrs (spec ? dependencies) { dependencies = lib.mapAttrs (patchDependency sourceOptions) spec.dependencies; }; - in - # For our purposes we need a dependency with + nestedDependencies = lib.mapAttrs (name: patchDependency (path ++ [ name ]) sourceOptions name) spec.dependencies; + patchDependenciesSources = lib.optionalAttrs (spec ? dependencies) { dependencies = lib.mapAttrs (_: value: value.result) nestedDependencies; }; + nestedIntegrityUpdates = lib.concatMap (value: value.integrityUpdates) (lib.attrValues nestedDependencies); + + # For our purposes we need a dependency with # - `resolved` set to a path in the nix store (`patchSource`) # - All `requires` entries of this dependency that are set to github URLs set to a path in the nix store (`patchRequiresSources`) # - This needs to be done recursively for all `dependencies` in the lockfile (`patchDependenciesSources`) - (spec // patchSource // patchRequiresSources // patchDependenciesSources); + result = spec // patchSource // patchRequiresSources // patchDependenciesSources; + in + { + result = result; + integrityUpdates = lib.optional (result ? resolved && result ? integrity && result.integrity == null) { + inherit path; + file = lib.removePrefix "file://" result.resolved; + }; + }; # Description: Takes a Path to a lockfile and returns the patched version as attribute set - # Type: { sourceHashFunc :: Fn } -> Path -> Set + # Type: { sourceHashFunc :: Fn } -> Path -> { result :: Set, integrityUpdates :: List { path, file } } patchLockfile = sourceOptions: file: assert (builtins.typeOf file != "path" && builtins.typeOf file != "string") -> throw "file ${toString file} must be a path or string"; - let content = readLockfile file; in - content // { - dependencies = lib.mapAttrs (patchDependency sourceOptions) content.dependencies; + let + content = readLockfile file; + dependencies = lib.mapAttrs (name: patchDependency [ name ] sourceOptions name) content.dependencies; + in + { + result = content // { + dependencies = lib.mapAttrs (_: value: value.result) dependencies; + }; + integrityUpdates = lib.concatMap (value: value.integrityUpdates) (lib.attrValues dependencies); }; # Description: Rewrite all the `github:` references to wildcards. @@ -295,9 +311,15 @@ rec { ); # Description: Takes a Path to a lockfile and returns the patched version as file in the Nix store - # Type: { sourceHashFunc :: Fn } -> Path -> Derivation - patchedLockfile = sourceOptions: file: writeText "package-lock.json" - (builtins.toJSON (patchLockfile sourceOptions file)); + # Type: { sourceHashFunc :: Fn } -> Path -> { result :: Derivation, integrityUpdates :: List { path, file } } + patchedLockfile = sourceOptions: file: + let + patched = patchLockfile sourceOptions file; + in + { + result = writeText "package-lock.json" (builtins.toJSON patched.result); + integrityUpdates = patched.integrityUpdates; + }; # Description: Turn a derivation (with name & src attribute) into a directory containing the unpacked sources # Type: Derivation -> Derivation @@ -389,7 +411,7 @@ rec { inherit nodejs sourceAttrs; }; - patchedLockfilePath = patchedLockfile sourceOptions packageLockJson; + patchedLockfile' = patchedLockfile sourceOptions packageLockJson; patchedPackagefilePath = patchedPackagefile packageJson; preinstall_node_modules = writeTextFile { @@ -437,6 +459,7 @@ rec { nativeBuildInputs = nativeBuildInputs ++ [ jq + ] ++ lib.optionals (patchedLockfile'.integrityUpdates != [ ]) [ openssl nodejs ]; @@ -453,38 +476,6 @@ rec { export HOME=$(mktemp -d) ''; - # A jq filter for finding dependencies with an integrity field of - # `null`, as set at evaluation time by `makeUrlSource`, in the - # package-lock.json file. The output format is a newline separated list - # of entries, where each entry contains a JSON object path of the - # integrity field and the corresponding resolved file path, separated - # by a tab, ready for shell consumption - jqFindNullIntegrity = '' - # Processes dependencies entries as { key, value } pairs - def process(prefix): - (prefix + [ .key ]) as $path | .value | - ( - # If we have an integrity attribute that is null, output an entry - if has("integrity") and .integrity == null - then - [ ($path + ["integrity"] | @json) - , (.resolved | ltrimstr("file://")) - ] - else empty - end - , - # Recurse into .dependencies, this won't be necessary for - # lockfile version 2 - if has("dependencies") - then .dependencies | to_entries[] | process($path + ["dependencies"]) - else empty - end - ); - .dependencies | to_entries[] | process(["dependencies"]) - # Does the newline/tab separated thing, nice for shell consumption - | @tsv - ''; - # A script for updating specific JSON paths (.path) with specific # values (.value), as given in a list of objects, of an $original[0] # JSON value @@ -495,21 +486,27 @@ rec { ) ''; - passAsFile = [ "jqFindNullIntegrity" "jqSetIntegrity" ]; + passAsFile = [ "jqSetIntegrity" ]; postPatch = '' # Patches the lockfile at build time to replace the `"integrity": # null` entries as set by `makeUrlSource` at eval time. - jq -r -f "$jqFindNullIntegrityPath" ${patchedLockfilePath} | while IFS=$'\t' read jsonpath file; do - - # https://docs.npmjs.com/cli/v8/configuring-npm/package-lock-json#packages - # https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity#tools_for_generating_sri_hashes - hash="sha512-$(openssl dgst -sha512 -binary "$file" | openssl base64 -A)" - - # Constructs a simple { path, value } JSON of the given arguments - jq -c --argjson path "$jsonpath" --arg value "$hash" -n '$ARGS.named' - - done | jq -s --slurpfile original ${patchedLockfilePath} -f "$jqSetIntegrityPath" > package-lock.json + # integrityUpdates is a list of { file, path } + ${if patchedLockfile'.integrityUpdates == [] then '' + cp ${patchedLockfile'.result} package-lock.json + '' else '' + { + ${lib.concatMapStrings ({ file, path }: '' + # https://docs.npmjs.com/cli/v8/configuring-npm/package-lock-json#packages + # https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity#tools_for_generating_sri_hashes + hash="sha512-$(openssl dgst -sha512 -binary ${lib.escapeShellArg file} | openssl base64 -A)" + + # Constructs a simple { path, value } JSON of the given arguments + jq -c --argjson path ${lib.escapeShellArg (builtins.toJSON path)} --arg value "$hash" -n '$ARGS.named' + '') patchedLockfile'.integrityUpdates} + } | jq -s --slurpfile original ${patchedLockfile'.result} -f "$jqSetIntegrityPath" > package-lock.json + set +x + ''} ln -sf ${patchedPackagefilePath} package.json ''; @@ -540,7 +537,7 @@ rec { passthru = passthru // { inherit nodejs; - lockfile = patchedLockfilePath; + lockfile = patchedLockfile'.result; packagesfile = patchedPackagefilePath; }; } // cleanArgs); diff --git a/tests/patch-lockfile.nix b/tests/patch-lockfile.nix index 20a0dec..2db2051 100644 --- a/tests/patch-lockfile.nix +++ b/tests/patch-lockfile.nix @@ -7,7 +7,7 @@ testLib.runTests { testPatchDependencyHandlesGitHubRefsInRequires = { expr = let - libxmljsUrl = (npmlock2nix.internal.patchDependency noSourceOptions "test" { + libxmljsUrl = (npmlock2nix.internal.patchDependency [ ] noSourceOptions "test" { version = "github:tmcw/leftpad#db1442a0556c2b133627ffebf455a78a1ced64b9"; from = "github:tmcw/leftpad#db1442a0556c2b133627ffebf455a78a1ced64b9"; integrity = "sha512-8/UvHFG90J4O4QNRzb0jB5Ni1QuvuB7XFTLfDMQnCzAsFemF29VKnNGUESFFcSP/r5WWh/PMe0YRz90+3IqsUA=="; @@ -15,19 +15,19 @@ testLib.runTests { libxmljs = "github:znerol/libxmljs#0517e063347ea2532c9fdf38dc47878c628bf0ae"; }; } - ).requires.libxmljs; + ).result.requires.libxmljs; in lib.hasPrefix builtins.storeDir libxmljsUrl; expected = true; }; testBundledDependenciesAreRetained = { - expr = npmlock2nix.internal.patchDependency noSourceOptions "test" { + expr = (npmlock2nix.internal.patchDependency [ ] noSourceOptions "test" { bundled = true; integrity = "sha1-hrGk3k+s4YCsVFqD8VA1I9j+0RU="; something = "bar"; dependencies = { }; - }; + }).result; expected = { bundled = true; integrity = "sha1-hrGk3k+s4YCsVFqD8VA1I9j+0RU="; @@ -37,18 +37,18 @@ testLib.runTests { }; testPatchLockfileWithoutDependencies = { - expr = (npmlock2nix.internal.patchLockfile noSourceOptions ./examples-projects/no-dependencies/package-lock.json).dependencies; + expr = (npmlock2nix.internal.patchLockfile noSourceOptions ./examples-projects/no-dependencies/package-lock.json).result.dependencies; expected = { }; }; testPatchDependencyDoesntDropAttributes = { - expr = npmlock2nix.internal.patchDependency noSourceOptions "test" { + expr = (npmlock2nix.internal.patchDependency [ ] noSourceOptions "test" { a = 1; foo = "something"; resolved = "https://examples.com/something.tgz"; integrity = "sha1-00000000000000000000000+0RU="; dependencies = { }; - }; + }).result; expected = { a = 1; foo = "something"; @@ -59,7 +59,7 @@ testLib.runTests { }; testPatchDependencyPatchesDependenciesRecursively = { - expr = npmlock2nix.internal.patchDependency noSourceOptions "test" { + expr = (npmlock2nix.internal.patchDependency [ ] noSourceOptions "test" { a = 1; foo = "something"; resolved = "https://examples.com/something.tgz"; @@ -68,7 +68,7 @@ testLib.runTests { resolved = "https://examples.com/somethingelse.tgz"; integrity = "sha1-00000000000000000000000+00U="; }; - }; + }).result; expected = { a = 1; @@ -85,7 +85,7 @@ testLib.runTests { testPatchLockfileTurnsUrlsIntoStorePaths = { expr = let - deps = (npmlock2nix.internal.patchLockfile noSourceOptions ./examples-projects/single-dependency/package-lock.json).dependencies; + deps = (npmlock2nix.internal.patchLockfile noSourceOptions ./examples-projects/single-dependency/package-lock.json).result.dependencies; in lib.count (dep: lib.hasPrefix "file:///nix/store/" dep.resolved) (lib.attrValues deps); expected = 1; @@ -94,19 +94,19 @@ testLib.runTests { testPatchLockfileTurnsGitHubUrlsIntoStorePaths = { expr = let - leftpad = (npmlock2nix.internal.patchLockfile noSourceOptions ./examples-projects/github-dependency/package-lock.json).dependencies.leftpad; + leftpad = (npmlock2nix.internal.patchLockfile noSourceOptions ./examples-projects/github-dependency/package-lock.json).result.dependencies.leftpad; in lib.hasPrefix ("file://" + builtins.storeDir) leftpad.version; expected = true; }; testConvertPatchedLockfileToJSON = { - expr = builtins.typeOf (builtins.toJSON (npmlock2nix.internal.patchLockfile noSourceOptions ./examples-projects/nested-dependencies/package-lock.json)) == "string"; + expr = builtins.typeOf (builtins.toJSON (npmlock2nix.internal.patchLockfile noSourceOptions ./examples-projects/nested-dependencies/package-lock.json).result) == "string"; expected = true; }; testPatchedLockFile = { - expr = testLib.hashFile (npmlock2nix.internal.patchedLockfile noSourceOptions ./examples-projects/nested-dependencies/package-lock.json); + expr = testLib.hashFile (npmlock2nix.internal.patchedLockfile noSourceOptions ./examples-projects/nested-dependencies/package-lock.json).result; expected = "980323c3a53d86ab6886f21882936cfe7c06ac633993f16431d79e3185084414"; }; From 60e0c0e3c014d5f1686d67e296c887975ad8cf83 Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Thu, 21 Apr 2022 12:41:44 +0200 Subject: [PATCH 5/5] Change interface from sourceAttrs to sourceOverrides --- API.md | 18 ++--- internal.nix | 66 +++++++++---------- .../source-patching/shell.nix | 6 +- 3 files changed, 42 insertions(+), 48 deletions(-) diff --git a/API.md b/API.md index a81f6b5..7623013 100644 --- a/API.md +++ b/API.md @@ -16,7 +16,7 @@ The `node_modules` function takes an attribute set with the following attributes - **nodejs** *(default `nixpkgs.nodejs`, which is the Active LTS version)*: Node.js derivation to use - **preInstallLinks** *(default `{}`)*: Map of symlinks to create inside npm dependencies in the `node_modules` output (See [Concepts](#concepts) for details). - **githubSourceHashMap** *(default `{}`)*: Dependency hashes for evaluation in restricted mode (See [Concepts](#concepts) for details). -- **sourceAttrs** *(default `{}`)*: Derivation attributes to apply to sources, allowing patching (See [Concepts](#concepts) for details) +- **sourceOverrides** *(default `{}`)*: Derivation attributes to apply to sources, allowing patching (See the [source derivation overrides](#source-derivation-overrides) concept for details) #### Notes - You may provide additional arguments accepted by `mkDerivation` all of which are going to be passed on. @@ -116,31 +116,31 @@ npmlock2nix.build { } ``` -### Source derivation attributes +### Source derivation overrides -`node_modules` takes a `sourceAttrs` argument, which allows you to modify the source derivations of individual npm packages you depend on, mainly useful for adding Nix-specific fixes to packages. This could be used for patching interpreter or paths, or to replace vendored binaries with ones provided by Nix. +`node_modules` takes a `sourceOverrides` argument, which allows you to modify the source derivations of individual npm packages you depend on, mainly useful for adding Nix-specific fixes to packages. This could be used for patching interpreter or paths, or to replace vendored binaries with ones provided by Nix. -The `sourceAttrs` argument expects an attribute set mapping npm package names to a function describing the modifications of that package. Each function receives an attribute set as an argument containing either a `version` attribute if the version is known, or a `github = { org, repo, rev, ref }` attribute if the package is fetched from GitHub. These values can be used to decide what the result of the function should be. The result of this function is passed to a `mkDerivation` call running mainly the [patch phase](https://nixos.org/manual/nixpkgs/stable/#ssec-patch-phase), meaning that [most `mkDerivation` arguments](https://nixos.org/manual/nixpkgs/stable/#chap-stdenv) can be returned from it. Of particular interest are `patches` and `postPatch`, in which `patchShebangs` can be called. Note that `patchShebangs` can only patch shebangs to binaries accessible in the derivation, which you can extend with `buildInputs`. For convenience, the correct version of `nodejs` is always included in `buildInputs`. +The `sourceOverrides` argument expects an attribute set mapping npm package names to a function describing the modifications of that package. Each function receives an attribute set as a first argument, containing either a `version` attribute if the version is known, or a `github = { org, repo, rev, ref }` attribute if the package is fetched from GitHub. These values can be used to have different overrides depending on the version. The function receives another argument which is the derivation of the fetched source, which can be modified using `.overrideAttrs`. The fetched source mainly runs the [patch phase](https://nixos.org/manual/nixpkgs/stable/#ssec-patch-phase), so of particular interest are the `patches` and `postPatch` attributes, in which `patchShebangs` can be called. Note that `patchShebangs` can only patch shebangs to binaries accessible in the derivation, which you can extend with `buildInputs`. For convenience, the correct version of `nodejs` is always included in `buildInputs`. ```nix npmlock2nix.node_modules { - sourceMods = { + sourceOverrides = { # sourceInfo either contains: # - A version attribute # - A github = { org, repo, rev, ref } attribute for GitHub sources - package-name = sourceInfo: { + package-name = sourceInfo: drv: drv.overrideAttrs (old: { buildInputs = [ somePackage ]; patches = [ somePatch ]; postPatch = '' some script ''; # ... - } + }) # Example - node-pre-gyp = sourceInfo: { + node-pre-gyp = sourceInfo: drv: drv.overrideAttrs (old: { postPatch = "patchShebangs bin"; - }; + }); }; } ``` diff --git a/internal.nix b/internal.nix index ae5ef67..0104205 100644 --- a/internal.nix +++ b/internal.nix @@ -79,41 +79,38 @@ rec { allRefs = true; }; - attrs = { - pname = name; - version = ref; - src = src; - } // lib.optionalAttrs (sourceOptions ? sourceAttrs.${name}) - (sourceOptions.sourceAttrs.${name} { - github = { inherit org repo rev ref; }; - }); + sourceInfo = { + github = { inherit org repo rev ref; }; + }; + drv = packTgz sourceOptions.nodejs name ref src; in - packTgz sourceOptions.nodejs attrs; - - # Description: Packs a source directory into a .tgz tar archive, allowing the - # source to be modified using derivation attributes. If the source is an - # archive, it gets unpacked first. - # Type: Path -> Set -> Path - packTgz = nodejs: attrs@{ pname, version, src, ... }: stdenv.mkDerivation ({ + if sourceOptions ? sourceOverrides.${name} + then sourceOptions.sourceOverrides.${name} sourceInfo drv + else drv; + + # Description: Packs a source directory into a .tgz tar archive. If the + # source is an archive, it gets unpacked first. + # Type: Path -> String -> String -> Path -> Path + packTgz = nodejs: pname: version: src: stdenv.mkDerivation { name = "${pname}-${version}.tgz"; phases = "unpackPhase patchPhase installPhase"; + inherit src; + buildInputs = [ + # Allows patchShebangs in postPatch to patch shebangs to nodejs + nodejs + ]; installPhase = '' runHook preInstall tar -C . -czf $out ./ runHook postInstall ''; - } // attrs // { - buildInputs = attrs.buildInputs or [ ] ++ [ - # Allows patchShebangs in postPatch to patch shebangs to nodejs - nodejs - ]; - }); + }; # Description: Turns a dependency with a from field of the format # `github:org/repo#revision` into a git fetcher. The fetcher can # receive a hash value by calling 'sourceHashFunc' if a source hash # map has been provided. Otherwise the function yields `null`. Patches - # specified with sourceAttrs will be applied + # specified with sourceOverrides will be applied # Type: { sourceHashFunc :: Fn } -> String -> Set -> Path makeGithubSource = sourceOptions@{ sourceHashFunc, ... }: name: dependency: assert !(dependency ? version) -> @@ -153,29 +150,26 @@ rec { dependency ? version && dependency ? integrity && ! (dependency ? resolved) && looksLikeUrl dependency.version; # Description: Replaces the `resolved` field of a dependency with a - # prefetched version from the Nix store. Patches specified with sourceAttrs + # prefetched version from the Nix store. Patches specified with sourceOverrides # will be applied, in which case the `integrity` attribute is set to `null`, # in order to be recomputer later - # Type: { sourceAttrs :: Fn, nodejs :: Package } -> String -> Set -> Set - makeUrlSource = { sourceAttrs ? { }, nodejs, ... }: name: dependency: + # Type: { sourceOverrides :: Fn, nodejs :: Package } -> String -> Set -> Set + makeUrlSource = { sourceOverrides ? { }, nodejs, ... }: name: dependency: let src = fetchurl (makeSourceAttrs name dependency); - attrs = { - pname = name; - version = dependency.version; - src = src; - } // sourceAttrs.${name} { + sourceInfo = { inherit (dependency) version; }; + drv = packTgz nodejs name dependency.version src; tgz = - if sourceAttrs ? ${name} + if sourceOverrides ? ${name} # If we have modification to this source, unpack the tgz, apply the # patches and repack the tgz - then packTgz nodejs attrs + then sourceOverrides.${name} sourceInfo drv else src; resolved = "file://" + toString tgz; in - dependency // { inherit resolved; } // lib.optionalAttrs (sourceAttrs ? ${name}) { + dependency // { inherit resolved; } // lib.optionalAttrs (sourceOverrides ? ${name}) { # Integrity was tampered with due to the source attributes, so it needs # to be recalculated, which is done in the node_modules builder integrity = null; @@ -393,7 +387,7 @@ rec { , preBuild ? "" , postBuild ? "" , preInstallLinks ? { } # set that describes which files should be linked in a specific packages folder - , sourceAttrs ? { } + , sourceOverrides ? { } , githubSourceHashMap ? { } , passthru ? { } , ... @@ -403,12 +397,12 @@ rec { assert (builtins.typeOf preInstallLinks != "set") -> throw "`preInstallLinks` must be an attributeset of attributesets"; let - cleanArgs = builtins.removeAttrs args [ "src" "packageJson" "packageLockJson" "buildInputs" "nativeBuildInputs" "nodejs" "preBuild" "postBuild" "preInstallLinks" "sourceAttrs" "githubSourceHashMap" ]; + cleanArgs = builtins.removeAttrs args [ "src" "packageJson" "packageLockJson" "buildInputs" "nativeBuildInputs" "nodejs" "preBuild" "postBuild" "preInstallLinks" "sourceOverrides" "githubSourceHashMap" ]; lockfile = readLockfile packageLockJson; sourceOptions = { sourceHashFunc = sourceHashFunc githubSourceHashMap; - inherit nodejs sourceAttrs; + inherit nodejs sourceOverrides; }; patchedLockfile' = patchedLockfile sourceOptions packageLockJson; diff --git a/tests/examples-projects/source-patching/shell.nix b/tests/examples-projects/source-patching/shell.nix index a741e19..928d6f0 100644 --- a/tests/examples-projects/source-patching/shell.nix +++ b/tests/examples-projects/source-patching/shell.nix @@ -2,8 +2,8 @@ npmlock2nix.shell { src = ./.; node_modules_attrs = { - sourceAttrs = { - custom-hello-world = _: { + sourceOverrides = { + custom-hello-world = sourceInfo: drv: drv.overrideAttrs (old: { patches = builtins.toFile "custom-hello-world.patch" '' diff --git a/lib/index.js b/lib/index.js index 1f66513..64391a7 100644 @@ -19,7 +19,7 @@ npmlock2nix.shell { if (exclamation) helloWorldStr += '!'; ''; - }; + }); }; }; }