Skip to content

Commit 137df92

Browse files
authored
fix: replace builtins.appendContext with context-carrying concatenation (#2482)
* fix: replace builtins.appendContext with context-carrying concatenation builtins.appendContext validates that store paths exist locally, which fails when paths come from flake inputs that aren't available on the evaluating machine (e.g. CHaP in cardano-node builds). Instead, preserve string context through parsing by using builtins.substring 0 0 to capture context from input strings and re-attach it via concatenation after builtins.match/split strip it. - parseBlockLines: carry context from input lines to parsed attr values - parseSourceRepositoryPackages: carry context through builtins.split - extractSourceRepoPackageData: use unsafeDiscardStringContext for sha256map attribute name lookups - parseRepositoryBlock: remove addContext helper that used appendContext, use attrs.url directly (context now preserved by parseBlockLines) - call-cabal-project-to-nix: remove appendContext on writeText, export rawCabalProjectContext for use by load-cabal-plan - load-cabal-plan: use rawCabalProjectContext concatenation instead of appendContext Add unit tests verifying context flows through parseBlockLines and parseSourceRepositoryPackages. * Bump head.hackage
1 parent 545ccfe commit 137df92

File tree

5 files changed

+83
-25
lines changed

5 files changed

+83
-25
lines changed

lib/cabal-project-parser.nix

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,19 @@ let
4242
);
4343

4444
# Parse lines of a source-repository-package block
45-
parseBlockLines = blockLines: (pkgs.lib.foldl' ({name, attrs}: s:
45+
parseBlockLines = blockLines:
46+
let
47+
# builtins.match strips string context. Capture the context from the input
48+
# lines so we can re-attach it to parsed values via string concatenation.
49+
# Using substring+concatenation instead of builtins.appendContext because
50+
# appendContext can fail for store paths that don't exist locally.
51+
contextStr = builtins.substring 0 0 (builtins.concatStringsSep "" blockLines);
52+
in (pkgs.lib.foldl' ({name, attrs}: s:
4653
let
4754
# Look for a new attribute name
4855
pair = builtins.match "([^ :]*): *(.*)" s;
4956
trim = x: let m = builtins.match "(.*[^ \t])[ \t]*" x;
50-
in pkgs.lib.optionalString (m != null) (builtins.head m);
57+
in pkgs.lib.optionalString (m != null) (contextStr + builtins.head m);
5158

5259
# Function to build the next parse state when the attribute name is known
5360
nextState = name: value: {
@@ -67,7 +74,7 @@ let
6774
nextState (builtins.head pair) (trim (builtins.elemAt pair 1))
6875
else
6976
if name != null
70-
then nextState name s # Append another line to the current attribute
77+
then nextState name (contextStr + s) # Append another line to the current attribute
7178
else __trace "Expected attribute but found `${s}`" { inherit name attrs; }
7279
) { name = null; attrs = {}; } (stripComments (unindent blockLines))).attrs;
7380

@@ -82,12 +89,15 @@ let
8289
if builtins.match "[0-9a-f]{40}" repo.tag != null
8390
then "rev"
8491
else "ref";
92+
# Strip context for attribute name lookups
93+
locationNoContext = builtins.unsafeDiscardStringContext repo.location;
94+
tagNoContext = builtins.unsafeDiscardStringContext repo.tag;
8595
in {
8696
url = repo.location;
8797
"${refOrRev}" = repo.tag;
8898
sha256 = repo."--sha256" or (
89-
if sha256map != null && sha256map ? ${repo.location}
90-
then sha256map.${repo.location}.${repo.tag}
99+
if sha256map != null && sha256map ? ${locationNoContext}
100+
then sha256map.${locationNoContext}.${tagNoContext}
91101
else null);
92102
subdirs = if repo ? subdir
93103
then pkgs.lib.filter (x: x != "") (pkgs.lib.splitString " " repo.subdir)
@@ -100,7 +110,7 @@ let
100110
let
101111
x = span (pkgs.lib.strings.hasPrefix (indentation + " ")) (pkgs.lib.splitString "\n" block);
102112
attrs = parseBlockLines x.fst;
103-
overrideSourceRepo = sourceRepo: (source-repo-override.${sourceRepo.url} or (pkgs.lib.id)) sourceRepo;
113+
overrideSourceRepo = sourceRepo: (source-repo-override.${builtins.unsafeDiscardStringContext sourceRepo.url} or (pkgs.lib.id)) sourceRepo;
104114
in
105115
if attrs."type" or "" != "git"
106116
then {
@@ -115,11 +125,15 @@ let
115125
parseSourceRepositoryPackages = cabalProjectFileName: sha256map: source-repo-override: projectFile:
116126
let
117127
splitResult = builtins.split "\n( *)source-repository-package\n" ("\n" + projectFile);
128+
# builtins.split strips string context. Use substring to create a zero-length
129+
# string with the original context, then prepend it to carry context through
130+
# via concatenation (which doesn't validate paths like appendContext does).
131+
contextStr = builtins.substring 0 0 projectFile;
118132
# Construct a list of strings with just the indentation amounts for each map
119133
indentations = builtins.concatLists (builtins.filter builtins.isList splitResult);
120134
blocks = builtins.filter builtins.isString (pkgs.lib.lists.drop 1 splitResult);
121135
in {
122-
initialText = builtins.head splitResult;
136+
initialText = contextStr + builtins.head splitResult;
123137
sourceRepos = pkgs.lib.zipListsWith (parseSourceRepositoryPackageBlock cabalProjectFileName sha256map source-repo-override)
124138
indentations blocks;
125139
};
@@ -136,21 +150,18 @@ let
136150
# The first line will contain the repository name.
137151
x = span (pkgs.lib.strings.hasPrefix " ") (__tail lines);
138152
attrs = parseBlockLines x.fst;
153+
# Strip context for attribute name lookups (context on attrs values
154+
# can cause "not allowed to refer to a store path" errors in attr names)
155+
urlNoContext = builtins.unsafeDiscardStringContext attrs.url;
139156
sha256 = attrs."--sha256" or (
140157
if sha256map != null
141-
then sha256map.${attrs.url} or null
158+
then sha256map.${urlNoContext} or null
142159
else null);
143-
# Find store directory strings and include them in the string context
144-
addContext = s:
145-
let storeDirMatch = builtins.match ".*(${builtins.storeDir}/[^/]+).*" s;
146-
in if storeDirMatch == null
147-
then s
148-
else builtins.appendContext s { ${builtins.head storeDirMatch} = { path = true; }; };
149160
in rec {
150161
# This is `some-name` from the `repository some-name` line in the `cabal.project` file.
151162
name = builtins.unsafeDiscardStringContext (__head lines);
152163
# The $HOME/.cabal/packages/${name} after running `cabal v2-update` to download the repository
153-
repoContents = if inputMap ? ${attrs.url}
164+
repoContents = if inputMap ? ${urlNoContext}
154165
# If there is an input use it to make `file:` url and create a suitable `.cabal/packages/${name}` directory
155166
then evalPackages.runCommand name ({
156167
nativeBuildInputs = [ nix-tools.exes.cabal ] ++ evalPackages.haskell-nix.cabal-issue-8352-workaround;
@@ -160,7 +171,7 @@ let
160171
mkdir -p $HOME/.cabal/packages/${name}
161172
cat <<EOF > $HOME/.cabal/config
162173
repository ${name}
163-
url: file:${inputMap.${attrs.url}}
174+
url: file:${inputMap.${urlNoContext}}
164175
${pkgs.lib.optionalString (attrs ? secure) "secure: ${attrs.secure}"}
165176
${pkgs.lib.optionalString (attrs ? root-keys) "root-keys: ${attrs.root-keys}"}
166177
${pkgs.lib.optionalString (attrs ? key-threshold) "key-threshold: ${attrs.key-threshold}"}
@@ -182,7 +193,7 @@ let
182193
mkdir -p $HOME/.cabal/packages/${name}
183194
cat <<EOF > $HOME/.cabal/config
184195
repository ${name}
185-
url: ${addContext attrs.url}
196+
url: ${attrs.url}
186197
${pkgs.lib.optionalString (attrs ? secure) "secure: ${attrs.secure}"}
187198
${pkgs.lib.optionalString (attrs ? root-keys) "root-keys: ${attrs.root-keys}"}
188199
${pkgs.lib.optionalString (attrs ? key-threshold) "key-threshold: ${attrs.key-threshold}"}

lib/call-cabal-project-to-nix.nix

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,7 @@ let
264264
inherit (repoResult) repos extra-hackages;
265265
makeFixedProjectFile = ''
266266
HOME=$(mktemp -d)
267-
cp -f ${evalPackages.writeText "cabal.project" (
268-
# Add the string context of rawCabalProject to make sure
269-
# any nix store paths are included as build inputs.
270-
builtins.appendContext sourceRepoFixedProjectFile
271-
(builtins.getContext rawCabalProject))} ./cabal.project
267+
cp -f ${evalPackages.writeText "cabal.project" sourceRepoFixedProjectFile} ./cabal.project
272268
chmod +w -R ./cabal.project
273269
'' + pkgs.lib.strings.concatStrings (
274270
map (f: ''
@@ -779,4 +775,8 @@ in {
779775
projectNix = plan-json;
780776
inherit index-state-max src;
781777
inherit (fixedProject) sourceRepos extra-hackages;
778+
# Zero-length string carrying context from rawCabalProject.
779+
# Used in load-cabal-plan.nix to add context to URLs referencing store paths
780+
# without using builtins.appendContext (which fails for non-local paths).
781+
rawCabalProjectContext = builtins.substring 0 0 rawCabalProject;
782782
}

lib/load-cabal-plan.nix

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ let
55
plan-json = builtins.fromJSON (
66
builtins.unsafeDiscardStringContext (
77
builtins.readFile (callProjectResults.projectNix + "/plan.json")));
8-
# Function to add context back to the strings we get from `plan.json`
8+
# Add context back to strings from `plan.json` that reference store paths.
9+
# Uses concatenation with a zero-length context string instead of
10+
# builtins.appendContext (which fails for store paths that don't exist locally).
911
addContext = s:
1012
let storeDirMatch = builtins.match ".*(${builtins.storeDir}/[^/]+).*" s;
1113
in if storeDirMatch == null
1214
then s
13-
else builtins.appendContext s { ${builtins.head storeDirMatch} = { path = true; }; };
15+
else callProjectResults.rawCabalProjectContext + s;
1416
# All the units in the plan indexed by unit ID.
1517
by-id = pkgs.lib.listToAttrs (map (x: { name = x.id; value = x; }) plan-json.install-plan);
1618
# Find the names of all the pre-existing packages used by a list of dependencies

test/cabal.project.local

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ repository head.hackage.ghc.haskell.org
2222
f76d08be13e9a61a377a85e2fb63f4c5435d40f8feb3e12eb05905edb8cdea89
2323
26021a13b401500c8eb2761ca95c61f2d625bfef951b939a8124ed12ecf07329
2424
7541f32a4ccca4f97aea3b22f5e593ba2c0267546016b992dfadcd2fe944e55d
25-
--sha256: sha256-RVMjsE1Lmk3SonOuHbGc2cd7gzJWyDPGehGZteWumS8=
25+
--sha256: sha256-b9w0Zl7/VlvmVToocpMRE6T0O8xf/8H2qDczv8B/iiM=
2626

2727
repository ghcjs-overlay
2828
url: https://raw.githubusercontent.com/input-output-hk/hackage-overlay-ghcjs/ffb32dce467b9a4d27be759fdd2740a6edd09d0b

test/unit.nix

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,51 @@ lib.runTests {
122122
};
123123
};
124124

125+
# Verify that parseBlockLines (called inside parseSourceRepositoryPackageBlock)
126+
# preserves string context on parsed attribute values like `location`.
127+
# This is critical for repository blocks where the URL may contain
128+
# interpolated store paths (e.g. `url: file:${CHaP}`).
129+
testParseBlockLinesPreservesContext =
130+
let
131+
storePath = builtins.toFile "test-context" "test";
132+
storePathStr = builtins.unsafeDiscardStringContext storePath;
133+
# Interpolate a store path into the block to give it context.
134+
# pkgs.lib.splitString (used inside parseSourceRepositoryPackageBlock)
135+
# preserves this context, but builtins.match (used in parseBlockLines)
136+
# would strip it without our fix.
137+
blockWithContext = ''
138+
type: git
139+
location: file:${storePath}
140+
tag: 487eea1c249537d34c27f6143dff2b9d5586c657
141+
--sha256: 077j5j3j86qy1wnabjlrg4dmqy1fv037dyq3xb8ch4ickpxxs123
142+
rest of file
143+
'';
144+
result = haskellLib.parseSourceRepositoryPackageBlock "cabal.project" {} {} "" blockWithContext;
145+
urlContext = builtins.getContext result.sourceRepo.url;
146+
in {
147+
expr = urlContext ? ${storePathStr};
148+
expected = true;
149+
};
150+
151+
# Verify that parseSourceRepositoryPackages preserves string context
152+
# from the project file through builtins.split into initialText.
153+
testParseSourceRepoPackagesPreservesContext =
154+
let
155+
storePath = builtins.toFile "test-context" "test";
156+
storePathStr = builtins.unsafeDiscardStringContext storePath;
157+
projectFile = ''
158+
packages: ./*.cabal
159+
repository test-repo
160+
url: file:${storePath}
161+
secure: True
162+
'';
163+
result = haskellLib.parseSourceRepositoryPackages "cabal.project" {} {} projectFile;
164+
initialTextContext = builtins.getContext result.initialText;
165+
in {
166+
expr = initialTextContext ? ${storePathStr};
167+
expected = true;
168+
};
169+
125170
testParseRepositoryBlock =
126171
let
127172
# The Cabal2Nix output in hackage-to-nix is imported into a lambda

0 commit comments

Comments
 (0)