diff --git a/lib/spack/llnl/util/filesystem.py b/lib/spack/llnl/util/filesystem.py index 4fa86914094..a70babff496 100644 --- a/lib/spack/llnl/util/filesystem.py +++ b/lib/spack/llnl/util/filesystem.py @@ -764,7 +764,7 @@ def copy_tree( files = glob.glob(src) if not files: - raise OSError("No such file or directory: '{0}'".format(src)) + raise OSError("No such file or directory: '{0}'".format(src), errno.ENOENT) # For Windows hard-links and junctions, the source path must exist to make a symlink. Add # all symlinks to this list while traversing the tree, then when finished, make all diff --git a/lib/spack/spack/ci/__init__.py b/lib/spack/spack/ci/__init__.py index a4cec793862..14be3bf4ada 100644 --- a/lib/spack/spack/ci/__init__.py +++ b/lib/spack/spack/ci/__init__.py @@ -24,6 +24,7 @@ import spack import spack.binary_distribution as bindist +import spack.builder import spack.config as cfg import spack.environment as ev import spack.error @@ -613,32 +614,40 @@ def copy_stage_logs_to_artifacts(job_spec: spack.spec.Spec, job_log_dir: str) -> job_spec, and attempts to copy the files into the directory given by job_log_dir. - Args: + Parameters: job_spec: spec associated with spack install log job_log_dir: path into which build log should be copied """ tty.debug(f"job spec: {job_spec}") - - try: - package_metadata_root = pathlib.Path(spack.store.STORE.layout.metadata_path(job_spec)) - except spack.error.SpackError as e: - tty.error(f"Cannot copy logs: {str(e)}") + if not job_spec.concrete: + tty.warn("Cannot copy artifacts for non-concrete specs") return - # Get the package's archived files - archive_files = [] - archive_root = package_metadata_root / "archived-files" - if archive_root.is_dir(): - archive_files = [f for f in archive_root.rglob("*") if f.is_file()] - else: - msg = "Cannot copy package archived files: archived-files must be a directory" - tty.warn(msg) + package_metadata_root = pathlib.Path(spack.store.STORE.layout.metadata_path(job_spec)) + if not os.path.isdir(package_metadata_root): + # Fallback to using the stage directory + job_pkg = job_spec.package + package_metadata_root = pathlib.Path(job_pkg.stage.path) + archive_files = spack.builder.create(job_pkg).archive_files + tty.warn("Package not installed, falling back to use stage dir") + tty.debug(f"stage dir: {package_metadata_root}") + else: + # Get the package's archived files + archive_files = [] + archive_root = package_metadata_root / "archived-files" + if os.path.isdir(archive_root): + archive_files = [str(f) for f in archive_root.rglob("*") if os.path.isfile(f)] + else: + tty.debug(f"No archived files detected at {archive_root}") + + # Try zipped and unzipped versions of the build log build_log_zipped = package_metadata_root / "spack-build-out.txt.gz" + build_log = package_metadata_root / "spack-build-out.txt" build_env_mods = package_metadata_root / "spack-build-env.txt" - for f in [build_log_zipped, build_env_mods, *archive_files]: - copy_files_to_artifacts(str(f), job_log_dir) + for f in [build_log_zipped, build_log, build_env_mods, *archive_files]: + copy_files_to_artifacts(str(f), job_log_dir, compress_artifacts=True) def copy_test_logs_to_artifacts(test_stage, job_test_dir): @@ -651,11 +660,12 @@ def copy_test_logs_to_artifacts(test_stage, job_test_dir): """ tty.debug(f"test stage: {test_stage}") if not os.path.exists(test_stage): - msg = f"Cannot copy test logs: job test stage ({test_stage}) does not exist" - tty.error(msg) + tty.error(f"Cannot copy test logs: job test stage ({test_stage}) does not exist") return - copy_files_to_artifacts(os.path.join(test_stage, "*", "*.txt"), job_test_dir) + copy_files_to_artifacts( + os.path.join(test_stage, "*", "*.txt"), job_test_dir, compress_artifacts=True + ) def download_and_extract_artifacts(url, work_dir) -> str: diff --git a/lib/spack/spack/ci/common.py b/lib/spack/spack/ci/common.py index 68e0d91c17e..78caafcb2ab 100644 --- a/lib/spack/spack/ci/common.py +++ b/lib/spack/spack/ci/common.py @@ -2,9 +2,13 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) import copy +import errno +import glob +import gzip import json import os import re +import shutil import sys import time from collections import deque @@ -25,6 +29,7 @@ import spack.mirrors.mirror import spack.schema import spack.spec +import spack.util.compression as compression import spack.util.spack_yaml as syaml import spack.util.url as url_util import spack.util.web as web_util @@ -40,22 +45,67 @@ _urlopen = web_util.urlopen -def copy_files_to_artifacts(src, artifacts_dir): +def copy_gzipped(glob_or_path: str, dest: str) -> None: + """Copy all of the files in the source glob/path to the destination. + + Args: + glob_or_path: path to file to test + dest: destination path to copy to + """ + + files = glob.glob(glob_or_path) + if not files: + raise OSError("No such file or directory: '{0}'".format(glob_or_path), errno.ENOENT) + if len(files) > 1 and not os.path.isdir(dest): + raise ValueError( + "'{0}' matches multiple files but '{1}' is not a directory".format(glob_or_path, dest) + ) + + def is_gzipped(path): + with open(path, "rb") as fd: + return compression.GZipFileType().matches_magic(fd) + + for src in files: + if is_gzipped(src): + fs.copy(src, dest) + else: + # Compress and copy in one step + src_name = os.path.basename(src) + if os.path.isdir(dest): + zipped = os.path.join(dest, f"{src_name}.gz") + elif not dest.endswith(".gz"): + zipped = f"{dest}.gz" + else: + zipped = dest + + with open(src, "rb") as fin, gzip.open(zipped, "wb") as fout: + shutil.copyfileobj(fin, fout) + + +def copy_files_to_artifacts( + src: str, artifacts_dir: str, *, compress_artifacts: bool = False +) -> None: """ Copy file(s) to the given artifacts directory - Parameters: + Args: src (str): the glob-friendly path expression for the file(s) to copy artifacts_dir (str): the destination directory + compress_artifacts (bool): option to compress copied artifacts using Gzip """ try: - fs.copy(src, artifacts_dir) + + if compress_artifacts: + copy_gzipped(src, artifacts_dir) + else: + fs.copy(src, artifacts_dir) except Exception as err: - msg = ( - f"Unable to copy files ({src}) to artifacts {artifacts_dir} due to " - f"exception: {str(err)}" + tty.warn( + ( + f"Unable to copy files ({src}) to artifacts {artifacts_dir} due to " + f"exception: {str(err)}" + ) ) - tty.warn(msg) def win_quote(quote_str: str) -> str: diff --git a/lib/spack/spack/cmd/ci.py b/lib/spack/spack/cmd/ci.py index e18439a97ea..156cb9410ad 100644 --- a/lib/spack/spack/cmd/ci.py +++ b/lib/spack/spack/cmd/ci.py @@ -453,7 +453,7 @@ def ci_rebuild(args): # Arguments when installing the root from sources deps_install_args = install_args + ["--only=dependencies"] - root_install_args = install_args + ["--only=package"] + root_install_args = install_args + ["--keep-stage", "--only=package"] if cdash_handler: # Add additional arguments to `spack install` for CDash reporting. @@ -493,6 +493,9 @@ def ci_rebuild(args): # Copy logs and archived files from the install metadata (.spack) directory to artifacts now spack_ci.copy_stage_logs_to_artifacts(job_spec, job_log_dir) + # Clear the stage directory + spack.stage.purge() + # If the installation succeeded and we're running stand-alone tests for # the package, run them and copy the output. Failures of any kind should # *not* terminate the build process or preclude creating the build cache.