Improve parsing of quoted flags and variants in specs#41529
Improve parsing of quoted flags and variants in specs#41529
Conversation
|
@cosmicexplorer FYI |
|
@tgamblin This partially or mostly fixes #35756 and #31214. This now works correctly: This now correctly fails: However, The Machine(*) finds one odd case that's still not correct. We see from the input spec that the error happens very early. The But early on, something in spack is incorrectly combining args 3+4 Worse yet, But be careful, remember (*) Sometimes, I like to think of my alter-ego as "The Big, Bad |
|
@mwkrentel: historically we have allowed spaces on either side of the = for variants and flags. So the args come in, the current heuristic doesn’t notice I think we should fix the CLI case, because the user could’ve typed: that’s easy enough — we just match any argv element that starts with What about the “whole thing is a string” case? i.e.: That’s legal at the moment. I’m hesitant to disallow spaces around |
|
If the issue was only the CLI and the shell, then my answer would be: I'm guessing the real problem here is that you also have to parse Is it possible to separate these two cases? Note: the shell treats these two lines as identical: So, if you're trying to tweak the first case to do what you think the I don't know what to suggest. I think I need to know more about what |
|
Playing standardese guy for a second, has spack intentionally supported spaces around Changing it might technically be a breaking change in the software, but I've yet to find anywhere it would be a change in the documented API. |
|
@trws we actually did a similar breaking change in #40344, see the bottom of the pr description too. I don't see the point of allowing white space around it either, and wouldn't consider it very breaking, cause it's pretty ambiguous. I also agree with @mwkrentel in that I know how shell quoting works, I find any tricks that Spack does with |
939075b to
480392d
Compare
I bet you don't want that at all, or at least no user I know wants that, because: spack spec zlib cflags="-O3 -g" +barwill parse as: sys.argv == ["spack", "spec", "zlib", "cflags=-O3 -g", "+bar"]
" ".join(sys.argv) == "spack spec zlib cflags=-O3 -g +bar"And the resulting spec is: This is exactly what this PR is intended to solve. |
|
I think when I tried to do something akin to this before I literally quoted every single argument before joining them, such that we could get some of this, but it caused problems with a whole lot of tests. The problem is we accept both If we wanted to make it no longer ambiguous, my preference would be to actually require each argument to be a "complete" component. Rather like gcc doesn't accept |
c1d7f1e to
7677699
Compare
|
@mwkrentel @trws: see if the current version is sufficient. Tests should be passing now. I have fixed the case @mwkrentel brought up, specifically the case where you pass either of these on the CLI: spack spec zlib cflags= +foo
spack spec zlib cflags="" +fooBoth of those result in the same |
|
On disallowing spaces:
I intentionally supported it from the start, because I don't think (or at least did not think) that the grammar should be sensitive to whitespace. In practice, the grammar is sensitive to whitespace. e.g., you can't put spaces around commas in multi-valued variant values, and we disallowed spaces in version ranges in #40344. So, I'm not unsympathetic to disallowing spaces around flags and variants, but I also don't think it matters much. I went ahead and implemented it, and the only changes that it introduces are the following:
This is because the real problem isn't that we allow or disallow spaces around There is really only one parse that you might care about here - namely if a user accidentally wrote: Spec("zlib cflags= +pic")
So to decide whether you want to disallow spaces, the question is really whether breaking any users who were using spaces (we don't know how many were) is worth returning an error (probably a better outcome) in case 3 above. Regardless of whether you allow spaces, you need the heuristic I introduced here to determine whether you should consider a key-value pair on the CLI to be quoted or not. Without that I think you're stuck with what I consider a far greater evil, which is that you have to write one of these on the CLI: Both of those are anti-user monstrosities while, with the fix I added above for @mwkrentel, disallowing spaces really only affects the corner cases. |
Yes. The grammar was born on the CLI (look at
This PR essentially does the reverse -- which I think is more understandable. Basically there is one heuristic: if any I think this results in the most consistency possible (see above for the corner cases) between the two worlds. Nearly anything you write on the CLI will end up being parsed the same as if you wrote it in a file, and vice versa. I think the only meaningful corner cases at this point are: And you can make anything above behave as though it was in a file by quoting and starting with space: But I don't know why anyone would write these -- maybe if they were writing a bash script or something that accumulates specs in a string. In Python there are much better ways to do these things, so I think it's really confined to CLI corner cases. |
This PR does three things:
- [x] Allow any character to appear in the quoted values of variants and flags.
- [x] Allow easier passing of quoted flags on the command line, e.g. `cflags="-O2 -g"`
- [x] Handle quoting better in spec output, using single quotes around double
quotes and vice versa.
This PR is motivated by ORNL's [tips for launching at scale on
Frontier](https://docs.olcf.ornl.gov/systems/frontier_user_guide.html#tips-for-launching-at-scale).
ORNL recommends using `sbcast --send-libs` to broadcast executables and their libraries
to compute nodes when running large jobs (e.g., 80k ranks). For an executable named
`exe`, `sbcast --send-libs` stores the needed libraries in a directory alongside the
executable called `exe_libs`. ORNL recommends pointing `LD_LIBRARY_PATH` at that
directory so that `exe` will find the local libraries and not overwhelm the filesystem.
Spack's `RPATHs` break this because they're considered with higher precedence than
`LD_LIBRARY_PATH`. So Spack applications will still end up hitting the shared filesystem
when searching for libraries. We can avoid this by injecting some `ldflags` in to the
build, e.g., if were were going to launch, say, `LAMMPS` at scale, we could add another
`RPATH` specifically for use with `sbcast`:
spack install lammps ldflags='-Wl,-rpath=$ORIGIN/lmp_libs'
This will put the `lmp_libs` directory alongside `LAMMPS`'s `lmp` executable first in
the `RPATH`, so it will be searched before any directories on the shared filesystem.
Before this PR, the command above would've errored out for two reasons:
1. `$` wasn't an allowed character in our spec parser.
2. You would've had to double quote the flags to get them to pass through correctly:
spack install lammps ldflags='"-Wl,-rpath=$ORIGIN/lmp_libs"'
This is ugly and I don't think many users will easily figure it out. The behavior was
added in #29282, and it improved parsing of specs passed as a single string, e.g.:
spack install 'lammps ldflags="-Wl,-rpath=$ORIGIN/lmp_libs"'
but a lot of users are naturally going to try to quote arguments *directly* on the
command line, without quoting their entire spec.
needed to quote them, but the error message could be confusing to users. In particular,
if you wrote `cflags="-O2 -g"` on the command line, it would break the flags up, warn,
and tell you that you could fix the issue by writing `cflags="-O2 -g"` even though you
just wrote that. It's telling you to *quote* that value, but the user has to know to
double quote.
There are only two places where we allow arbitrary quoted strings in specs: flags and
variant values, so this PR adds a simpler heuristic to the CLI parser: if an argument in
`sys.argv` starts with `name=...`, then we assume the whole argument is quoted.
This means you can write:
spack install bzip2 cflags="-O2 -g"
directly on the command line, without multiple levels of quoting. This also works:
spack install 'bzip2 cflags="-O2 -g"'
The only place where this heuristic runs into ambiguity is if you attempt to pass
anonymous specs that start with `name=...` as one large string. e.g., this will be
interpreted as one large flag value:
spack find 'cflags="-O2 -g" ~bar +baz'
This sets `cflags` to `"-O2 -g" ~bar +baz`, which is likely not what you wanted. You can
fix this easily by either removing the quotes:
spack find cflags="-O2 -g" ~bar +baz
Or by adding a space at the start, which has the same effect:
spack find ' cflags="-O2 -g" ~bar +baz'
You may wonder why we don't just look for quotes inside of flag arguments, and the
reason is that you *might* want them there. If you are passing arguments like:
spack install zlib cppflags="-D DEBUG_MSG1='quick fox' -D DEBUG_MSG2='lazy dog'"
You *need* the quotes there. So we've opted for one potentially confusing, but easily
fixed outcome vs. limiting what you can put in your quoted strings.
In addition to being more lenient about characters accepted in quoted strings, this PR
fixes up spec formatting a bit. If a variant or flag value has double quotes in it,
we'll print it with single quotes, and vice versa. If a variant or flag value has both
types of quotes in it, we'll surround it with single quotes and escape internal single
quotes. Examples:
zlib cflags='-D FOO="bar"'
zlib cflags="-D FOO='bar'"
zlib cflags='-D FOO=\'bar\' BAR="baz"'
- [x] don't handle normalize argument anymore; nothing uses it.
- [x] NO_QUOTES_NEEDED should have + not * - [x] add some very fine-grained test cases
7677699 to
17143bd
Compare
For what is worth the initial implementation of #34151 was exactly like ☝️ but was changed per #34151 (comment) and preceding discussion in the PR to get 100% backward compatibility. I'd be 👍 if we want to go back and disallow |
Why though? We have packages in mainline (look at mumps) that use this. 2/3 of users have private package repositories. I can't imagine that this isn't pointlessly breaking something. Again, the spaces allowed in the grammar aren't the real issue here. They're not what is preventing us from writing |
|
I'm dropping myself from the discussion cause I feel like I'm a minority, but not before pointing out a few things anyways:
|
|
|
(Performance nitpick) There seems to be a slight slowdown when parsing key/value pairs: we can recover most of it with this diff: Pre-compile a few regexes
diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py
index cb4083ed8f..58c4de3469 100644
--- a/lib/spack/spack/cmd/__init__.py
+++ b/lib/spack/spack/cmd/__init__.py
@@ -154,7 +154,7 @@ def quote_kvp(string: str) -> str:
or ``name==``, and we assume the rest of the argument is the value. This covers the
common cases of passign flags, e.g., ``cflags="-O2 -g"`` on the command line.
"""
- match = re.match(spack.parser.SPLIT_KVP, string)
+ match = spack.parser.SPLIT_KVP.match(string)
if not match:
return string
diff --git a/lib/spack/spack/parser.py b/lib/spack/spack/parser.py
index d8d3deef3c..60a0ec8c38 100644
--- a/lib/spack/spack/parser.py
+++ b/lib/spack/spack/parser.py
@@ -100,7 +100,7 @@
VALUE = r"(?:[a-zA-Z_0-9\-+\*.,:=\~\/\\]+)"
#: Anything that matches this *will* be conservatively quoted in Spack output
-NO_QUOTES_NEEDED = r"^[a-zA-Z0-9,/_.-]+$"
+NO_QUOTES_NEEDED = re.compile(r"^[a-zA-Z0-9,/_.-]+$")
#: quoted values can be *anything* in between quotes, including escaped quotes.
QUOTED_VALUE = r"(?:'(?:[^']|(?<=\\)')*'|\"(?:[^\"]|(?<=\\)\")*\")"
@@ -110,15 +110,15 @@
VERSION_LIST = rf"(?:{VERSION_RANGE}|{VERSION})(?:\s*,\s*(?:{VERSION_RANGE}|{VERSION}))*"
#: Regex with groups to use for splitting (optionally propagated) key-value pairs
-SPLIT_KVP = rf"^({NAME})(==?)(.*)$"
+SPLIT_KVP = re.compile(rf"^({NAME})(==?)(.*)$")
#: Regex to strip quotes. Group 2 will be the unquoted string.
-STRIP_QUOTES = r"^(['\"])(.*)\1$"
+STRIP_QUOTES = re.compile(r"^(['\"])(.*)\1$")
def strip_quotes_and_unescape(string: str) -> str:
"""Remove surrounding single or double quotes from string, if present."""
- match = re.match(STRIP_QUOTES, string)
+ match = STRIP_QUOTES.match(string)
if not match:
return string
@@ -140,7 +140,7 @@ def quote_if_needed(value: str) -> str:
``"``, and control codes.
"""
- if re.match(spack.parser.NO_QUOTES_NEEDED, value):
+ if NO_QUOTES_NEEDED.match(value):
return value
return json.dumps(value) if "'" in value else f"'{value}'"
@@ -456,14 +456,14 @@ def parse(
)
elif self.ctx.accept(TokenType.KEY_VALUE_PAIR):
- match = re.match(SPLIT_KVP, self.ctx.current_token.value)
+ match = SPLIT_KVP.match(self.ctx.current_token.value)
assert match, "SPLIT_KVP and KEY_VALUE_PAIR do not agree."
name, delim, value = match.groups()
initial_spec._add_flag(name, strip_quotes_and_unescape(value), propagate=False)
elif self.ctx.accept(TokenType.PROPAGATED_KEY_VALUE_PAIR):
- match = re.match(SPLIT_KVP, self.ctx.current_token.value)
+ match = SPLIT_KVP.match(self.ctx.current_token.value)
assert match, "SPLIT_KVP and PROPAGATED_KEY_VALUE_PAIR do not agree."
name, delim, value = match.groups()
After the diff the results are: |
|
@tgamblin Returning to these two examples for a minute: I'm still wondering if these two cases should behave differently. Suppose you stripped leading whitespace from every argv token. |
It would basically mean that you have no way to write an anonymous spec with multiple variant values in a single string. Specs don't have to have a name; you can start with a variant. If your script generates specs all in one string, and also quotes things properly internally, you probably don't want our heuristic. Consider: Our heuristic says "hey, that starts with vs: The initial space gives you a way to always force the string to be parsed with its own internal quoting, and avoid our heuristic. Here you get the anonymous spec The space was intended as an escape hatch for people building up specs in strings with their own quoting, and stripping it (or modifying the heuristic) removes the ability to do this. |
|
That's unfortunate. You've convinced me that it's complicated. :-( |
This tempts me as a follow-on to see if a recursive regex to match nested quotes would be better or worse. It would handle that case, but I'm not sure it would really be better, since now the corner case would be if the quotes appear somewhere not touching the equals sign or the beginning of the argument, which might be even more surprising. |
@becker33 and I talked about this and opted for very simple heuristic over "try real hard to analyze internal quoting". Currently the rules are:
I cannot think of a simpler heuristic, and I agree that analyzing internal quotes has the potential to make the heuristic very complicated. I also think that it would be very odd for someone to start a string with Consider that this issue only comes up if you do something like: instead of I find it very hard to believe anyone would type the first one. If they are in the habit of quoting all specs, in most cases the spec is going to start with a name: Both of those parse the same. It is really only the anonymous spec case that is affected by this, and only if they accidentally cram some extra args in quotes with it. So, again: rare, and a lesser evil. If you are assembling specs in strings in the shell (i.e., if you are writing a really complicated bash script that you should be writing in python instead), we will document that you can fix this by starting specs with I think you have a choice between:
|
|
Let me repeat: Remember, this started with |
|
I missed @alalazo's suggestion in #41529 (comment), but added it in #41657. |
These changes were missed on #41529. Adding them here. This adds a small (~5%) performance improvement to Spec parsing. Co-authored-by: Massimiliano Culpo <[email protected]>
|
Should we backport this to v0.21.1? |
I don't think so, b/c it is a breaking change -- |
If you are calling Spack from the python API, you might have written something like this before #41529: ``` find = SpackCommand("find") find('--format={name}', '[email protected]', '+rocm', 'amdgpu_target="gfx90a"') ``` But with the breaking change in #41529, you should write: ``` find = SpackCommand("find") find('--format={name}', 'gromacs', '+rocm', 'amdgpu_target=gfx90a') ``` Note that we don't need quotes in Python strings, and that this is what would come in via argv if you typed a quoted variant on the CLI. The error messages for strings like this are not great -- you get something like this: ``` ==> No package matches the query: gromacs+rocm amdgpu_target="gfx90a" ``` Which doesn't indicate that the issue might be your quoting. This is because we were simply outputting the argv we got, instead of using spec.format() to output the error message. This PR fixes such errors to use `spec.format()` and to look like this: ``` ==> No package matches the query: gromacs+rocm amdgpu_target='"gfx90a"' ``` So users should have an easier time understanding that Spack considers the variant value to contain quotes here. - [x] update ConstraintAction to store parsed Specs - [x] refactor commands to display formatted parsed Specs instead of raw input
If you are calling Spack from the python API, you might have written something like this before #41529: ``` find = SpackCommand("find") find('--format={name}', '[email protected]', '+rocm', 'amdgpu_target="gfx90a"') ``` But with the breaking change in #41529, you should write: ``` find = SpackCommand("find") find('--format={name}', 'gromacs', '+rocm', 'amdgpu_target=gfx90a') ``` Note that we don't need quotes in Python strings, and that this is what would come in via argv if you typed a quoted variant on the CLI. The error messages for strings like this are not great -- you get something like this: ``` ==> No package matches the query: gromacs+rocm amdgpu_target="gfx90a" ``` Which doesn't indicate that the issue might be your quoting. This is because we were simply outputting the argv we got, instead of using spec.format() to output the error message. This PR fixes such errors to use `spec.format()` and to look like this: ``` ==> No package matches the query: gromacs+rocm amdgpu_target='"gfx90a"' ``` So users should have an easier time understanding that Spack considers the variant value to contain quotes here. - [x] update ConstraintAction to store parsed Specs - [x] refactor commands to display formatted parsed Specs instead of raw input
This PR does several things:
- [x] Allow any character to appear in the quoted values of variants and flags.
- [x] Allow easier passing of quoted flags on the command line, e.g. `cflags="-O2 -g"`.
- [x] Handle quoting better in spec output, using single quotes around double
quotes and vice versa.
- [x] Disallow spaces around `=` and `==` when parsing variants and flags.
## Motivation
This PR is motivated by the issues above and by ORNL's
[tips for launching at scale on Frontier](https://docs.olcf.ornl.gov/systems/frontier_user_guide.html#tips-for-launching-at-scale).
ORNL recommends using `sbcast --send-libs` to broadcast executables and their
libraries to compute nodes when running large jobs (e.g., 80k ranks). For an
executable named `exe`, `sbcast --send-libs` stores the needed libraries in a
directory alongside the executable called `exe_libs`. ORNL recommends pointing
`LD_LIBRARY_PATH` at that directory so that `exe` will find the local libraries and
not overwhelm the filesystem.
There are other ways to mitigate this problem:
* You could build with `RUNPATH` using `spack config add config:shared_linking:type:runpath`,
which would make `LD_LIBRARY_PATH` take precedence over Spack's `RUNPATHs`.
I don't recommend this one because `RUNPATH` can cause many other things to go wrong.
* You could use `spack config add config:shared_linking:bind:true`, added in spack#31948, which
will greatly reduce the filesystem load for large jobs by pointing `DT_NEEDED` entries in
ELF *directly* at the needed `.so` files instead of relying on `RPATH` search via soname.
I have not experimented with this at 80,000 ranks, but it should help quite a bit.
* You could use [Spindle](https://github.com/hpc/Spindle) (as LLNL does on its machines)
which should transparently fix this without any changes to your executable and without
any need to use `sbcast` or other tools.
But we want to support the `sbcast` use case as well.
## `sbcast` and Spack
Spack's `RPATHs` break the `sbcast` fix because they're considered with higher precedence
than `LD_LIBRARY_PATH`. So Spack applications will still end up hitting the shared filesystem
when searching for libraries. We can avoid this by injecting some `ldflags` in to the build, e.g.,
if were were going to launch, say, `LAMMPS` at scale, we could add another `RPATH`
specifically for use with `sbcast`:
spack install lammps ldflags='-Wl,-rpath=$ORIGIN/lmp_libs'
This will put the `lmp_libs` directory alongside `LAMMPS`'s `lmp` executable first in the
`RPATH`, so it will be searched before any directories on the shared filesystem.
## Issues with quoting
Before this PR, the command above would've errored out for two reasons:
1. `$` wasn't an allowed character in our spec parser.
2. You would've had to double quote the flags to get them to pass through correctly:
spack install lammps ldflags='"-Wl,-rpath=$ORIGIN/lmp_libs"'
This is ugly and I don't think many users will easily figure it out. The behavior was added in
spack#29282, and it improved parsing of specs passed as a single string, e.g.:
spack install 'lammps ldflags="-Wl,-rpath=$ORIGIN/lmp_libs"'
but a lot of users are naturally going to try to quote arguments *directly* on the command
line, without quoting their entire spec. spack#29282 used a heuristic to detect unquoted flags
and warn the user, but the warning could be confusing. In particular, if you wrote
`cflags="-O2 -g"` on the command line, it would break the flags up, warn, and tell you
that you could fix the issue by writing `cflags="-O2 -g"` even though you just wrote
that. It's telling you to *quote* that value, but the user has to know to double quote.
## New heuristic for quoted arguments from the CLI
There are only two places where we allow arbitrary quoted strings in specs: flags and
variant values, so this PR adds a simpler heuristic to the CLI parser: if an argument in
`sys.argv` starts with `name=...`, then we assume the whole argument is quoted.
This means you can write:
spack install bzip2 cflags="-O2 -g"
directly on the command line, without multiple levels of quoting. This also works:
spack install 'bzip2 cflags="-O2 -g"'
The only place where this heuristic runs into ambiguity is if you attempt to pass
anonymous specs that start with `name=...` as one large string. e.g., this will be
interpreted as one large flag value:
spack find 'cflags="-O2 -g" ~bar +baz'
This sets `cflags` to `"-O2 -g" ~bar +baz`, which is likely not what you wanted. You
can fix this easily by either removing the quotes:
spack find cflags="-O2 -g" ~bar +baz
Or by adding a space at the start, which has the same effect:
spack find ' cflags="-O2 -g" ~bar +baz'
You may wonder why we don't just look for quotes inside of flag arguments, and the
reason is that you *might* want them there. If you are passing arguments like:
spack install zlib cppflags="-D DEBUG_MSG1='quick fox' -D DEBUG_MSG2='lazy dog'"
You *need* the quotes there. So we've opted for one potentially confusing, but easily
fixed outcome vs. limiting what you can put in your quoted strings.
## Quotes in formatted spec output
In addition to being more lenient about characters accepted in quoted strings, this PR fixes
up spec formatting a bit. We now format quoted strings in specs with single quotes, unless
the string has a single quote in it, in which case we JSON-escape the string (i.e., we add
`\` before `"` and `\`).
zlib cflags='-D FOO="bar"'
zlib cflags="-D FOO='bar'"
zlib cflags="-D FOO='bar' BAR=\"baz\""
If you are calling Spack from the python API, you might have written something like this before spack#41529: ``` find = SpackCommand("find") find('--format={name}', '[email protected]', '+rocm', 'amdgpu_target="gfx90a"') ``` But with the breaking change in spack#41529, you should write: ``` find = SpackCommand("find") find('--format={name}', 'gromacs', '+rocm', 'amdgpu_target=gfx90a') ``` Note that we don't need quotes in Python strings, and that this is what would come in via argv if you typed a quoted variant on the CLI. The error messages for strings like this are not great -- you get something like this: ``` ==> No package matches the query: gromacs+rocm amdgpu_target="gfx90a" ``` Which doesn't indicate that the issue might be your quoting. This is because we were simply outputting the argv we got, instead of using spec.format() to output the error message. This PR fixes such errors to use `spec.format()` and to look like this: ``` ==> No package matches the query: gromacs+rocm amdgpu_target='"gfx90a"' ``` So users should have an easier time understanding that Spack considers the variant value to contain quotes here. - [x] update ConstraintAction to store parsed Specs - [x] refactor commands to display formatted parsed Specs instead of raw input
This PR does several things:
- [x] Allow any character to appear in the quoted values of variants and flags.
- [x] Allow easier passing of quoted flags on the command line, e.g. `cflags="-O2 -g"`.
- [x] Handle quoting better in spec output, using single quotes around double
quotes and vice versa.
- [x] Disallow spaces around `=` and `==` when parsing variants and flags.
## Motivation
This PR is motivated by the issues above and by ORNL's
[tips for launching at scale on Frontier](https://docs.olcf.ornl.gov/systems/frontier_user_guide.html#tips-for-launching-at-scale).
ORNL recommends using `sbcast --send-libs` to broadcast executables and their
libraries to compute nodes when running large jobs (e.g., 80k ranks). For an
executable named `exe`, `sbcast --send-libs` stores the needed libraries in a
directory alongside the executable called `exe_libs`. ORNL recommends pointing
`LD_LIBRARY_PATH` at that directory so that `exe` will find the local libraries and
not overwhelm the filesystem.
There are other ways to mitigate this problem:
* You could build with `RUNPATH` using `spack config add config:shared_linking:type:runpath`,
which would make `LD_LIBRARY_PATH` take precedence over Spack's `RUNPATHs`.
I don't recommend this one because `RUNPATH` can cause many other things to go wrong.
* You could use `spack config add config:shared_linking:bind:true`, added in spack#31948, which
will greatly reduce the filesystem load for large jobs by pointing `DT_NEEDED` entries in
ELF *directly* at the needed `.so` files instead of relying on `RPATH` search via soname.
I have not experimented with this at 80,000 ranks, but it should help quite a bit.
* You could use [Spindle](https://github.com/hpc/Spindle) (as LLNL does on its machines)
which should transparently fix this without any changes to your executable and without
any need to use `sbcast` or other tools.
But we want to support the `sbcast` use case as well.
## `sbcast` and Spack
Spack's `RPATHs` break the `sbcast` fix because they're considered with higher precedence
than `LD_LIBRARY_PATH`. So Spack applications will still end up hitting the shared filesystem
when searching for libraries. We can avoid this by injecting some `ldflags` in to the build, e.g.,
if were were going to launch, say, `LAMMPS` at scale, we could add another `RPATH`
specifically for use with `sbcast`:
spack install lammps ldflags='-Wl,-rpath=$ORIGIN/lmp_libs'
This will put the `lmp_libs` directory alongside `LAMMPS`'s `lmp` executable first in the
`RPATH`, so it will be searched before any directories on the shared filesystem.
## Issues with quoting
Before this PR, the command above would've errored out for two reasons:
1. `$` wasn't an allowed character in our spec parser.
2. You would've had to double quote the flags to get them to pass through correctly:
spack install lammps ldflags='"-Wl,-rpath=$ORIGIN/lmp_libs"'
This is ugly and I don't think many users will easily figure it out. The behavior was added in
spack#29282, and it improved parsing of specs passed as a single string, e.g.:
spack install 'lammps ldflags="-Wl,-rpath=$ORIGIN/lmp_libs"'
but a lot of users are naturally going to try to quote arguments *directly* on the command
line, without quoting their entire spec. spack#29282 used a heuristic to detect unquoted flags
and warn the user, but the warning could be confusing. In particular, if you wrote
`cflags="-O2 -g"` on the command line, it would break the flags up, warn, and tell you
that you could fix the issue by writing `cflags="-O2 -g"` even though you just wrote
that. It's telling you to *quote* that value, but the user has to know to double quote.
## New heuristic for quoted arguments from the CLI
There are only two places where we allow arbitrary quoted strings in specs: flags and
variant values, so this PR adds a simpler heuristic to the CLI parser: if an argument in
`sys.argv` starts with `name=...`, then we assume the whole argument is quoted.
This means you can write:
spack install bzip2 cflags="-O2 -g"
directly on the command line, without multiple levels of quoting. This also works:
spack install 'bzip2 cflags="-O2 -g"'
The only place where this heuristic runs into ambiguity is if you attempt to pass
anonymous specs that start with `name=...` as one large string. e.g., this will be
interpreted as one large flag value:
spack find 'cflags="-O2 -g" ~bar +baz'
This sets `cflags` to `"-O2 -g" ~bar +baz`, which is likely not what you wanted. You
can fix this easily by either removing the quotes:
spack find cflags="-O2 -g" ~bar +baz
Or by adding a space at the start, which has the same effect:
spack find ' cflags="-O2 -g" ~bar +baz'
You may wonder why we don't just look for quotes inside of flag arguments, and the
reason is that you *might* want them there. If you are passing arguments like:
spack install zlib cppflags="-D DEBUG_MSG1='quick fox' -D DEBUG_MSG2='lazy dog'"
You *need* the quotes there. So we've opted for one potentially confusing, but easily
fixed outcome vs. limiting what you can put in your quoted strings.
## Quotes in formatted spec output
In addition to being more lenient about characters accepted in quoted strings, this PR fixes
up spec formatting a bit. We now format quoted strings in specs with single quotes, unless
the string has a single quote in it, in which case we JSON-escape the string (i.e., we add
`\` before `"` and `\`).
zlib cflags='-D FOO="bar"'
zlib cflags="-D FOO='bar'"
zlib cflags="-D FOO='bar' BAR=\"baz\""
If you are calling Spack from the python API, you might have written something like this before spack#41529: ``` find = SpackCommand("find") find('--format={name}', '[email protected]', '+rocm', 'amdgpu_target="gfx90a"') ``` But with the breaking change in spack#41529, you should write: ``` find = SpackCommand("find") find('--format={name}', 'gromacs', '+rocm', 'amdgpu_target=gfx90a') ``` Note that we don't need quotes in Python strings, and that this is what would come in via argv if you typed a quoted variant on the CLI. The error messages for strings like this are not great -- you get something like this: ``` ==> No package matches the query: gromacs+rocm amdgpu_target="gfx90a" ``` Which doesn't indicate that the issue might be your quoting. This is because we were simply outputting the argv we got, instead of using spec.format() to output the error message. This PR fixes such errors to use `spec.format()` and to look like this: ``` ==> No package matches the query: gromacs+rocm amdgpu_target='"gfx90a"' ``` So users should have an easier time understanding that Spack considers the variant value to contain quotes here. - [x] update ConstraintAction to store parsed Specs - [x] refactor commands to display formatted parsed Specs instead of raw input


Fixes #35756.
Fixes #31214.
This PR does several things:
cflags="-O2 -g".=and==when parsing variants and flags.Motivation
This PR is motivated by the issues above and by ORNL's tips for launching at scale on Frontier. ORNL recommends using
sbcast --send-libsto broadcast executables and their libraries to compute nodes when running large jobs (e.g., 80k ranks). For an executable namedexe,sbcast --send-libsstores the needed libraries in a directory alongside the executable calledexe_libs. ORNL recommends pointingLD_LIBRARY_PATHat that directory so thatexewill find the local libraries and not overwhelm the filesystem.There are other ways to mitigate this problem:
RUNPATHusingspack config add config:shared_linking:type:runpath, which would makeLD_LIBRARY_PATHtake precedence over Spack'sRUNPATHs. I don't recommend this one becauseRUNPATHcan cause many other things to go wrong.spack config add config:shared_linking:bind:true, added in Experimental binding of shared ELF libraries #31948, which will greatly reduce the filesystem load for large jobs by pointingDT_NEEDEDentries in ELF directly at the needed.sofiles instead of relying onRPATHsearch via soname. I have not experimented with this at 80,000 ranks, but it should help quite a bit.sbcastor other tools.But we want to support the
sbcastuse case as well.sbcastand SpackSpack's
RPATHsbreak thesbcastfix because they're considered with higher precedence thanLD_LIBRARY_PATH. So Spack applications will still end up hitting the shared filesystem when searching for libraries. We can avoid this by injecting someldflagsin to the build, e.g., if were were going to launch, say,LAMMPSat scale, we could add anotherRPATHspecifically for use withsbcast:This will put the
lmp_libsdirectory alongsideLAMMPS'slmpexecutable first in theRPATH, so it will be searched before any directories on the shared filesystem.Issues with quoting
Before this PR, the command above would've errored out for two reasons:
$wasn't an allowed character in our spec parser.You would've had to double quote the flags to get them to pass through correctly:
This is ugly and I don't think many users will easily figure it out. The behavior was added in #29282, and it improved parsing of specs passed as a single string, e.g.:
but a lot of users are naturally going to try to quote arguments directly on the command line, without quoting their entire spec. #29282 used a heuristic to detect unquoted flags and warn the user, but the warning could be confusing. In particular, if you wrote
cflags="-O2 -g"on the command line, it would break the flags up, warn, and tell you that you could fix the issue by writingcflags="-O2 -g"even though you just wrote that. It's telling you to quote that value, but the user has to know to double quote.New heuristic for quoted arguments from the CLI
There are only two places where we allow arbitrary quoted strings in specs: flags and variant values, so this PR adds a simpler heuristic to the CLI parser: if an argument in
sys.argvstarts withname=..., then we assume the whole argument is quoted.This means you can write:
directly on the command line, without multiple levels of quoting. This also works:
The only place where this heuristic runs into ambiguity is if you attempt to pass anonymous specs that start with
name=...as one large string. e.g., this will be interpreted as one large flag value:This sets
cflagsto"-O2 -g" ~bar +baz, which is likely not what you wanted. You can fix this easily by either removing the quotes:Or by adding a space at the start, which has the same effect:
You may wonder why we don't just look for quotes inside of flag arguments, and the reason is that you might want them there. If you are passing arguments like:
You need the quotes there. So we've opted for one potentially confusing, but easily fixed outcome vs. limiting what you can put in your quoted strings.
Quotes in formatted spec output
In addition to being more lenient about characters accepted in quoted strings, this PR fixes up spec formatting a bit. We now format quoted strings in specs with single quotes, unless the string has a single quote in it, in which case we JSON-escape the string (i.e., we add
\before"and\).