Repository: spack/spack

Issue:
Hard error when a build log contains UTF-8 characters
### Steps to reproduce

Attempt to install the package `cairo+X+ft+pdf` with gcc 11.3.0:
```console
spack install cairo+X+ft+pdf%gcc@11.3.0
```
Note, the error doesn't occur with gcc@7.5.0, which does not generate the warning message with utf8.

I get an error message (see below for the full message):
```console
==> cairo: Executing phase: 'autoreconf'
==> cairo: Executing phase: 'configure'
==> cairo: Executing phase: 'build'
==> cairo: Executing phase: 'install'
==> Error: UnicodeDecodeError: 'ascii' codec can't decode byte 0xce in position 4054028: ordinal not in range(128)
```

Digging through the logs shows that the the configure, build and install steps are successful, which suggests that the error occurs in spack itself. I tested the log files to see if they were valid ascii, and sure enough one of the logs failed at the position in our error message above:
```console
iconv -f ascii spack-build-03-build-out.txt -t ascii -o /dev/null
iconv: illegal input sequence at position 4054028
```

That position in the log file is a non-ascii character `μ`:
```console
show-glyphs-many.c:157:12: warning: implicit conversion from 'cairo_test_status_t' {aka 'enum cairo_test_status'} to 'cairo_status_t' {aka 'enum _cairo_status'} [-Wenum-conversion]                                              
  157 |     status = get_glyph (ctx, scaled_font, "μ", &glyphs[i]);
```

It looks like spack is parsing the logs, expecting ascii, and producing a hard error when an UTF-8 encoded log file is encountered.

### Error message

The package is built with:
`spack --debug --stacktrace -e '/dev/shm/bcumming/balfrin/packages/tools' install  --only-concrete --only=package --no-add /d22qdschn4zv7lmzlb6k3myktg7kfnta `

Truncated output:
```console
lib/spack/spack/package_base.py:1643 ==> [2022-12-22-11:22:45.111843] Applied patch /dev/shm/bcumming/balfrin/spack/var/spack/repos/builtin/packages/cairo/fontconfig.patch
lib/spack/spack/fetch_strategy.py:474 ==> [2022-12-22-11:22:43.850838] Staging archive: /tmp/bcumming/spack-stage/spack-stage-cairo-1.16.0-d22qdschn4zv7lmzlb6k3myktg7kfnta/cairo-1.16.0.tar.xz
lib/spack/spack/util/executable.py:197 ==> [2022-12-22-11:22:43.852133] '/usr/bin/tar' '-oxf' '/tmp/bcumming/spack-stage/spack-stage-cairo-1.16.0-d22qdschn4zv7lmzlb6k3myktg7kfnta/cairo-1.16.0.tar.xz'
lib/spack/spack/stage.py:630 ==> [2022-12-22-11:22:45.107092] Created stage in /tmp/bcumming/spack-stage/spack-stage-cairo-1.16.0-d22qdschn4zv7lmzlb6k3myktg7kfnta
lib/spack/spack/util/executable.py:197 ==> [2022-12-22-11:22:45.108084] '/usr/bin/patch' '-s' '-p' '1' '-i' '/dev/shm/bcumming/balfrin/spack/var/spack/repos/builtin/packages/cairo/fontconfig.patch' '-d' '.'
lib/spack/spack/installer.py:1858 ==> [2022-12-22-11:22:45.112279] cairo: Building cairo-1.16.0-d22qdschn4zv7lmzlb6k3myktg7kfnta [AutotoolsPackage]
lib/spack/spack/installer.py:1969 ==> [2022-12-22-11:22:45.118546] cairo: Executing phase: 'autoreconf'
lib/spack/spack/installer.py:1969 ==> [2022-12-22-11:22:52.324909] cairo: Executing phase: 'configure'
lib/spack/spack/installer.py:1969 ==> [2022-12-22-11:23:05.284728] cairo: Executing phase: 'build'
lib/spack/spack/installer.py:1969 ==> [2022-12-22-11:23:23.981463] cairo: Executing phase: 'install'
lib/spack/spack/error.py:54 ==> [2022-12-22-11:23:25.283265] Error: UnicodeDecodeError: 'ascii' codec can't decode byte 0xce in position 4045650: ordinal not in range(128)

/dev/shm/bcumming/balfrin/spack/lib/spack/spack/build_environment.py:1102, in _setup_pkg_and_run:
       1099        tb_string = traceback.format_exc()
       1100
       1101        # build up some context from the offending package so we can
  >>   1102        # show that, too.
       1103        package_context = get_package_context(tb)
       1104
       1105        logfile = None

See build log for details:
  /tmp/bcumming/spack-stage/spack-stage-cairo-1.16.0-d22qdschn4zv7lmzlb6k3myktg7kfnta/spack-build-out.txt

Traceback (most recent call last):
  File "/dev/shm/bcumming/balfrin/spack/lib/spack/spack/build_environment.py", line 1086, in _setup_pkg_and_run
    return_value = function(pkg, kwargs)
  File "/dev/shm/bcumming/balfrin/spack/lib/spack/spack/installer.py", line 2016, in build_process
    return installer.run()
  File "/dev/shm/bcumming/balfrin/spack/lib/spack/spack/installer.py", line 1878, in run
    self._real_install()
  File "/dev/shm/bcumming/balfrin/spack/lib/spack/spack/installer.py", line 1992, in _real_install
    combine_phase_logs(pkg.phase_log_files, pkg.log_path)
  File "/dev/shm/bcumming/balfrin/spack/lib/spack/spack/installer.py", line 432, in combine_phase_logs
    log_file.write(phase_log.read())
  File "/usr/lib64/python3.6/encodings/ascii.py", line 26, in decode
    return codecs.ascii_decode(input, self.errors)[0]
UnicodeDecodeError: 'ascii' codec can't decode byte 0xce in position 4045650: ordinal not in range(128)
lib/spack/spack/installer.py:1406 ==> [2022-12-22-11:23:25.283521] Flagging cairo-1.16.0-d22qdschn4zv7lmzlb6k3myktg7kfnta as failed: UnicodeDecodeError: 'ascii' codec can't decode byte 0xce in position 4045650: ordinal not in range(128)
lib/spack/spack/environment/environment.py:1510 ==> [2022-12-22-11:23:25.322976] Skip view update, this environment does not maintain a view
lib/spack/spack/main.py:993 ==> [2022-12-22-11:23:25.323167] ChildError: UnicodeDecodeError: 'ascii' codec can't decode byte 0xce in position 4045650: ordinal not in range(128)
make[1]: *** [tools/Makefile:19: tools/generated/.install/d22qdschn4zv7lmzlb6k3myktg7kfnta] Error 1
make[1]: Leaving directory '/dev/shm/bcumming/balfrin/packages'
make: *** [Makefile:47: packages] Error 2
```

### Information on your system

```console
* **Spack:** 0.19.0.dev0 (a8b1314d188149e696eb8e7ba3e4d0de548f1894)
* **Python:** 3.6.15
* **Platform:** linux-sles15-zen3
* **Concretizer:** clingo
```

The error is reproduced when building a spack stack, that builds the package inside an environment with the makefile generator (@haampie knows what I am talking about)
```console
env --ignore-environment PATH=/usr/bin:/bin:`pwd`/spack/bin LANG=en_US.UTF-8 make -j32
```

But the error isn't reproduced when I use spack outside of the `env` and install directly on the command line
```console
spack install cairo+X+ft+pdf%gcc@11.3.0
```

### General information

- [X] I have run `spack debug report` and reported the version of Spack/Python/Platform
- [X] I have searched the issues of this repo and believe this is not a duplicate
- [X] I have run the failing commands in debug mode and reported the output


Discussion:
@johnwparent if I would have to worry about unix only I would just concat text files in binary mode, but with Windows support how do you do it? It's not OK to do that when there's a BOM magic bytes, but since we generate those files in the first place, I suppose it's not a problem? 

Resolving patch:
diff --git a/lib/spack/spack/installer.py b/lib/spack/spack/installer.py
--- a/lib/spack/spack/installer.py
+++ b/lib/spack/spack/installer.py
@@ -460,11 +460,10 @@ def combine_phase_logs(phase_log_files, log_path):
         phase_log_files (list): a list or iterator of logs to combine
         log_path (str): the path to combine them to
     """
-
-    with open(log_path, "w") as log_file:
+    with open(log_path, "bw") as log_file:
         for phase_log_file in phase_log_files:
-            with open(phase_log_file, "r") as phase_log:
-                log_file.write(phase_log.read())
+            with open(phase_log_file, "br") as phase_log:
+                shutil.copyfileobj(phase_log, log_file)
 
 
 def dump_packages(spec, path):


Test patch:
diff --git a/lib/spack/spack/test/installer.py b/lib/spack/spack/test/installer.py
--- a/lib/spack/spack/test/installer.py
+++ b/lib/spack/spack/test/installer.py
@@ -622,7 +622,7 @@ def test_combine_phase_logs(tmpdir):
 
     # This is the output log we will combine them into
     combined_log = os.path.join(str(tmpdir), "combined-out.txt")
-    spack.installer.combine_phase_logs(phase_log_files, combined_log)
+    inst.combine_phase_logs(phase_log_files, combined_log)
     with open(combined_log, "r") as log_file:
         out = log_file.read()
 
@@ -631,6 +631,22 @@ def test_combine_phase_logs(tmpdir):
         assert "Output from %s\n" % log_file in out
 
 
+def test_combine_phase_logs_does_not_care_about_encoding(tmpdir):
+    # this is invalid utf-8 at a minimum
+    data = b"\x00\xF4\xBF\x00\xBF\xBF"
+    input = [str(tmpdir.join("a")), str(tmpdir.join("b"))]
+    output = str(tmpdir.join("c"))
+
+    for path in input:
+        with open(path, "wb") as f:
+            f.write(data)
+
+    inst.combine_phase_logs(input, output)
+
+    with open(output, "rb") as f:
+        assert f.read() == data * 2
+
+
 def test_check_deps_status_install_failure(install_mockery, monkeypatch):
     const_arg = installer_args(["a"], {})
     installer = create_installer(const_arg)