Repository: spack/spack

Issue:
bugfix: prevent ncurses from overriding the environment's CFLAGS (#11819)
This PR attempts to fix the issue of the ncurses build overriding the environment's `CFLAGS`/`CXXFLAGS` (#11819).


Discussion:
Is this just an `nurses` issue, or do you think it's widespread?
I can't say for sure, I'm afraid; I'm but a new user to Spack. This problem manifested when I misused `CFLAGS`/`CXXFLAGS` in `compilers.yaml` to implement an unusual llvm install. I used `spack install --fake llvm` and then extracted a binary release directly to `spack location -i llvm`, abusing `CXXFLAGS` to add `--stdlib=libc++`, set rpaths, etc.

It was a while until I figured out to use `modules` and `extra_rpaths` in `compilers.yaml` to achieve the same results, and to favour libc++ by setting build options when building llvm. Until figuring that out, I ran into an ever-growing series of issues, one of which was that I couldn't always enforce linking with lld by using `CXXFLAGS=-fuse-ld=lld`; some packages seemed to always fallback to using `/usr/bin/ld`.

From said experiences, my impression is that one cannot rely on setting `CFLAGS` and `CXXFLAGS` in `compilers.yaml` to implement a system-wide configuration--there always seemed to be something that eventually broke, despite the `CFLAGS`/`CXXFLAGS` being valid in principle. 

My slightly contrived use-case was a result of wanting to make a small docker image for establishing a spack-based build environment, primarily so as to avoid the common pitfalls of using libc++ on Linux, where all system-installed libraries use libstdc++. 
@tgamblin I'm sending this one up the ladder.
I don't exactly want to be the boy who kicked the hornet's nest here. :-)
But there are a few issues wrt how spack handles compiler flags that
I'd like to bring up.

First, as you've discovered, the main thing is that you never want to
set compiler flags in spack, you only want to append to them.  This
patch fixes that problem.

In this case, the problem came from a compiler module, but it could
just as easily come from setting cflags on the command line.  In that
sense, this issue affects every package that touches flags.

----------

But there's another issue.  It won't break the build, but it's still
pretty significant.  Spack doesn't handle optimization flags for
autotools packages very well, and ncurses is an example.

Here's how autotools handles CFLAGS and CXXFLAGS by default.  If you
don't specify them, then autoconf picks a default for you, normally
`-g -O2`.  (Ncurses actually chooses `-O2` without `-g`, but
whatever.)  But if you specify any flag, then autoconf uses exactly
what you write, no optimization, not unless you add it.

So, setting `CFLAGS=-fPIC` on the command line means exactly that,
NO flag for optimization.  It depends on the compiler family, but for
GNU, this means `-O0`.  So, adding `-fPIC` turns optimization off,
which may not be what you want.

Further, autoconf will outsmart you even more.  If you don't write
CFLAGS on the configure line, then autoconf will look for CFLAGS in
your environment and use that.  And again, if it's set, then it will
use exactly that, so no optimization.  That's the problem with this
patch.  Putting CFLAGS in your environment is just as bad as writing
it on the configure line.

You can see this by running spack install as:

```
spack -d install -v ncurses
```

But there is a third way.  Use `flag_handler()` to insert the flags in
the spack compiler wrapper.  That way, they're there when you run the
compiler, but autoconf doesn't see them, so autoconf will still pick a
default `-O2`.

```
def flag_handler(self, name, flags):
    if name == 'cflags' or name == 'cxxflags':
        flags.append(self.compiler.pic_flag)
    return (flags, None, None)
```

Really, it's either that or add `-O2` yourself or else live with `-O0`.

----------

So, is this widespread?

Yes and no.  Spack gives you the tools to do it right and makes it
easy to do it wrong.

For the append issue, you just have to know that you should append and
not set.  As long as you do that, then any flag handling will work.
But this is not well documented.

For the optimization issue (mostly autoconf specific), you really need
to be pretty familiar with the quirks of autoconf.  And it's very easy
to do it wrong, where wrong means `-O0` and not know it.

----------

Anyway, I should probably open a separate issue for the general
treatment of autoconf flags.  Cmake is easier because there's a
separate `build_type` variant.  But autoconf intermixes compiler flags
like this with optimization flags and spack is still a bit clumsy for
how it handles that.

Well, maybe I kicked the hornet's nest just a little.  :-)

@adamjstewart
There's another issue here.  Why does ncurses need `-fPIC` at all?

There are already configure options for `--with-shared` and
`--with-cxx-shared`.  Those options tell autotools to add `-fPIC`
itself.  Again, if you run `spack -d install -v ncurses`, you can see
that `-fPIC` is added twice.

The only thing that adding `-fPIC` does is build the STATIC libraries
with PIC code, so you could statically link ncurses into another
shared library.  That's a valid use case, but uncommon.  So, I'd
prefer a variant with default off (or remove it completely).

I looked up the commit (4f600111) that added this.  I don't mean to
pick on Adam, he's been an excellent reviewer of my PRs.

But I'd like to know, was there something specific that failed without
adding `-fPIC`?  Or, did you add this because you thought it was
needed for shared libraries in general?

This is not supposed to be necessary for autotools.  Autoconf and
libtool know how to build shared libraries and add `-fPIC` itself.

My apologies, I had a blind spot here.  I claim that the original
`package.py` does not have the overwriting problem that this PR is
trying to fix.

`-fPIC` from `package.py` is put in CFLAGS/CXXFLAGS on the configure
line.  cflags/cxxflags from `compilers.yaml` are inserted via the
spack compiler wrapper.  They're separate, so not overwritten.

So, there's something else going on here and I suggest not patching
anything until we correctly understand the mechanics of what goes
wrong.

Either that or else I'm seriously missing something else, which is certainly
possible. :-)

The issue of overwriting can be a legit problem, but it's less common
than what I first thought and I claim doesn't apply here.

The issue of deleting the optimization flags is real and does apply to
this package.

See #11819
Apologies all, I'm not able to recreate the original scenario that I made this PR to fix, so I've closed my original issue (#11819). I've left this PR open though, due to the optimisation flags issue mentioned by @mwkrentel.
As I mentioned above, I think this is the wrong patch.  I think the
right patch is:

```
     def configure(self, spec, prefix):
         opts = [
-            'CFLAGS={0}'.format(self.compiler.pic_flag),
-            'CXXFLAGS={0}'.format(self.compiler.pic_flag),
             '--with-shared',
             '--with-cxx-shared',
```

Libtool knows how to build shared libraries and will add `-fPIC`
itself.  If you run `spack -d install -v ncurses`, you'll see
that `-fPIC` is added twice.

The only thing that setting cflags does (by whatever means) is add
`-fPIC` to the **static** libraries.  Normally, this is not needed,
and if it is needed then it should be a variant.
> @adamjstewart
> There's another issue here. Why does ncurses need `-fPIC` at all?
> 
> There are already configure options for `--with-shared` and
> `--with-cxx-shared`. Those options tell autotools to add `-fPIC`
> itself. Again, if you run `spack -d install -v ncurses`, you can see
> that `-fPIC` is added twice.
> 
> The only thing that adding `-fPIC` does is build the STATIC libraries
> with PIC code, so you could statically link ncurses into another
> shared library. That's a valid use case, but uncommon. So, I'd
> prefer a variant with default off (or remove it completely).
> 
> I looked up the commit ([4f60011](https://github.com/spack/spack/commit/4f6001112199524910e7754e13564e7f5d107d05)) that added this. I don't mean to
> pick on Adam, he's been an excellent reviewer of my PRs.
> 
> But I'd like to know, was there something specific that failed without
> adding `-fPIC`? Or, did you add this because you thought it was
> needed for shared libraries in general?
> 
> This is not supposed to be necessary for autotools. Autoconf and
> libtool know how to build shared libraries and add `-fPIC` itself.

It's been a long time, but according to #3135, I wasn't able to build `ncurses` with the PGI compilers without adding this flag:
```
ld: ../obj_s/hardscroll.o: relocation R_X86_64_32 against `.rodata' can not be used when making a shared object; recompile with -fPIC
```
This was a long time ago, and I don't claim to be a C expert, so if you can remove that flag and still find a way to build the package with the PGI compilers, then go ahead.

I've also added this to several HDF/NetCDF-related packages in the past for similar reasons. I tried building the package, and it complained that its dependencies should be installed with `-fPIC`. If that just means they require the `+shared` variant, then I made a mistake.
@adamjstewart Ok, that was the piece that I was missing, what was the
use case that failed for you.

In that case, the safer patch would be the `flag_handler` patch I
suggested above.  It keeps the `-fPIC` flags, just moves them to the
spack wrapper.

The problem with CFLAGS on the configure line (as is now) or the
`setup_environment` patch in this PR is that autotools will erase the
`-O2` flag, so you'll build everything unoptimized (at least for GNU,
it depends on compiler family).

Libtool is supposed to know how to build shared libraries and to add
the appropriate flag itself.  So, adding `-fPIC` is not supposed to be
needed.  (If it was, that would say libtool is fundamentally broken
for PGI.)  But there are a lot points in "the matrix" and autotools is
the wild west of build systems, so nothing would surprise me.

If you'd like to commit the `flag_handler` patch, declare victory and
move on, I'd be ok with that.  That would fix the `-O2` problem and
save me a lot of digging into old use cases.  And I do want to be
careful not to break other people's use cases.

I could prepare a separate PR if you like.

Btw, this is not really a C thing, it's more an autotools thing.

At one point, libtool actually was broken for PGI. I had to patch a lot of packages to fix it. But it's been fixed upstream. I no longer use PGI, so do whatever you want.
Sorry, I didn't realize that old libtool was broken for PGI.  In that
case, I'll submit a PR for the `flag_handler` patch in a few days and
declare victory.  That will be safe and fix the optimization problem.

Resolving patch:
diff --git a/var/spack/repos/builtin/packages/ncurses/package.py b/var/spack/repos/builtin/packages/ncurses/package.py
--- a/var/spack/repos/builtin/packages/ncurses/package.py
+++ b/var/spack/repos/builtin/packages/ncurses/package.py
@@ -36,10 +36,14 @@ class Ncurses(AutotoolsPackage):
     def setup_environment(self, spack_env, run_env):
         spack_env.unset('TERMINFO')
 
+    def flag_handler(self, name, flags):
+        if name == 'cflags' or name == 'cxxflags':
+            flags.append(self.compiler.pic_flag)
+
+        return (flags, None, None)
+
     def configure(self, spec, prefix):
         opts = [
-            'CFLAGS={0}'.format(self.compiler.pic_flag),
-            'CXXFLAGS={0}'.format(self.compiler.pic_flag),
             '--with-shared',
             '--with-cxx-shared',
             '--enable-overwrite',


Test patch: