Pipelines: DAG Pruning (#20435)

Pipelines: DAG pruning

During the pipeline generation staging process we check each spec against all configured mirrors to determine whether it is up to date on any of the mirrors.  By default, and with the --prune-dag argument to "spack ci generate", any spec already up to date on at least one remote mirror is omitted from the generated pipeline.  To generate jobs for up to date specs instead of omitting them, use the --no-prune-dag argument.  To speed up the pipeline generation process, pass the --check-index-only argument.  This will cause spack to check only remote buildcache indices and avoid directly fetching any spec.yaml files from mirrors.  The drawback is that if the remote buildcache index is out of date, spec rebuild jobs may be scheduled unnecessarily.

This change removes the final-stage-rebuild-index block from gitlab-ci section of spack.yaml.  Now rebuilding the buildcache index of the mirror specified in the spack.yaml is the default, unless "rebuild-index: False" is set.  Spack assigns the generated rebuild-index job runner attributes from an optional new "service-job-attributes" block, which is also used as the source of runner attributes for another generated non-build job, a no-op job, which spack generates to avoid gitlab errors when DAG pruning results in empty pipelines.
This commit is contained in:
Scott Wittenburg 2021-02-16 09:12:37 -07:00 committed by GitHub
parent 60ee4199f5
commit 428f831899
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 831 additions and 257 deletions

View File

@ -122,9 +122,26 @@ pipeline jobs.
Concretizes the specs in the active environment, stages them (as described in Concretizes the specs in the active environment, stages them (as described in
:ref:`staging_algorithm`), and writes the resulting ``.gitlab-ci.yml`` to disk. :ref:`staging_algorithm`), and writes the resulting ``.gitlab-ci.yml`` to disk.
This sub-command takes two arguments, but the most useful is ``--output-file``, Using ``--prune-dag`` or ``--no-prune-dag`` configures whether or not jobs are
which should be an absolute path (including file name) to the generated generated for specs that are already up to date on the mirror. If enabling
pipeline, if the default (``./.gitlab-ci.yml``) is not desired. DAG pruning using ``--prune-dag``, more information may be required in your
``spack.yaml`` file, see the :ref:`noop_jobs` section below regarding
``service-job-attributes``.
The ``--optimize`` argument is experimental and runs the generated pipeline
document through a series of optimization passes designed to reduce the size
of the generated file.
The ``--dependencies`` is also experimental and disables what in Gitlab is
referred to as DAG scheduling, internally using the ``dependencies`` keyword
rather than ``needs`` to list dependency jobs. The drawback of using this option
is that before any job can begin, all jobs in previous stages must first
complete. The benefit is that Gitlab allows more dependencies to be listed
when using ``dependencies`` instead of ``needs``.
The optional ``--output-file`` argument should be an absolute path (including
file name) to the generated pipeline, and if not given, the default is
``./.gitlab-ci.yml``.
.. _cmd-spack-ci-rebuild: .. _cmd-spack-ci-rebuild:
@ -223,21 +240,6 @@ takes a boolean and determines whether the pipeline uses artifacts to store and
pass along the buildcaches from one stage to the next (the default if you don't pass along the buildcaches from one stage to the next (the default if you don't
provide this option is ``False``). provide this option is ``False``).
The
``final-stage-rebuild-index`` section controls whether an extra job is added to the
end of your pipeline (in a stage by itself) which will regenerate the mirror's
buildcache index. Under normal operation, each pipeline job that rebuilds a package
will re-generate the mirror's buildcache index after the buildcache entry for that
job has been created and pushed to the mirror. Since jobs in the same stage can run in
parallel, there is the possibility that at the end of some stage, the index may not
reflect all the binaries in the buildcache. Adding the ``final-stage-rebuild-index``
section ensures that at the end of the pipeline, the index will be in sync with the
binaries on the mirror. If the mirror lives in an S3 bucket, this job will need to
run on a machine with the Python ``boto3`` module installed, and consequently the
``final-stage-rebuild-index`` needs to specify a list of ``tags`` to pick a runner
satisfying that condition. It can also take an ``image`` key so Docker executor type
runners can pick the right image for the index regeneration job.
The optional ``cdash`` section provides information that will be used by the The optional ``cdash`` section provides information that will be used by the
``spack ci generate`` command (invoked by ``spack ci start``) for reporting ``spack ci generate`` command (invoked by ``spack ci start``) for reporting
to CDash. All the jobs generated from this environment will belong to a to CDash. All the jobs generated from this environment will belong to a
@ -251,6 +253,76 @@ Take a look at the
for the gitlab-ci section of the spack environment file, to see precisely what for the gitlab-ci section of the spack environment file, to see precisely what
syntax is allowed there. syntax is allowed there.
.. _rebuild_index:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Note about rebuilding buildcache index
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
By default, while a pipeline job may rebuild a package, create a buildcache
entry, and push it to the mirror, it does not automatically re-generate the
mirror's buildcache index afterward. Because the index is not needed by the
default rebuild jobs in the pipeline, not updating the index at the end of
each job avoids possible race conditions between simultaneous jobs, and it
avoids the computational expense of regenerating the index. This potentially
saves minutes per job, depending on the number of binary packages in the
mirror. As a result, the default is that the mirror's buildcache index may
not correctly reflect the mirror's contents at the end of a pipeline.
To make sure the buildcache index is up to date at the end of your pipeline,
spack generates a job to update the buildcache index of the target mirror
at the end of each pipeline by default. You can disable this behavior by
adding ``rebuild-index: False`` inside the ``gitlab-ci`` section of your
spack environment. Spack will assign the job any runner attributes found
on the ``service-job-attributes``, if you have provided that in your
``spack.yaml``.
.. _noop_jobs:
^^^^^^^^^^^^^^^^^^^^^^^
Note about "no-op" jobs
^^^^^^^^^^^^^^^^^^^^^^^
If no specs in an environment need to be rebuilt during a given pipeline run
(meaning all are already up to date on the mirror), a single succesful job
(a NO-OP) is still generated to avoid an empty pipeline (which GitLab
considers to be an error). An optional ``service-job-attributes`` section
can be added to your ``spack.yaml`` where you can provide ``tags`` and
``image`` or ``variables`` for the generated NO-OP job. This section also
supports providing ``before_script``, ``script``, and ``after_script``, in
case you want to take some custom actions in the case of any empty pipeline.
Following is an example of this section added to a ``spack.yaml``:
.. code-block:: yaml
spack:
specs:
- openmpi
mirrors:
cloud_gitlab: https://mirror.spack.io
gitlab-ci:
mappings:
- match:
- os=centos8
runner-attributes:
tags:
- custom
- tag
image: spack/centos7
service-job-attributes:
tags: ['custom', 'tag']
image:
name: 'some.image.registry/custom-image:latest'
entrypoint: ['/bin/bash']
script:
- echo "Custom message in a custom script"
The example above illustrates how you can provide the attributes used to run
the NO-OP job in the case of an empty pipeline. The only field for the NO-OP
job that might be generated for you is ``script``, but that will only happen
if you do not provide one yourself.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Assignment of specs to runners Assignment of specs to runners
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -1322,7 +1322,7 @@ def extract_tarball(spec, filename, allow_root=False, unsigned=False,
os.remove(filename) os.remove(filename)
def try_direct_fetch(spec, force=False, full_hash_match=False, mirrors=None): def try_direct_fetch(spec, full_hash_match=False, mirrors=None):
""" """
Try to find the spec directly on the configured mirrors Try to find the spec directly on the configured mirrors
""" """
@ -1360,11 +1360,26 @@ def try_direct_fetch(spec, force=False, full_hash_match=False, mirrors=None):
return found_specs return found_specs
def get_mirrors_for_spec(spec=None, force=False, full_hash_match=False, def get_mirrors_for_spec(spec=None, full_hash_match=False,
mirrors_to_check=None): mirrors_to_check=None, index_only=False):
""" """
Check if concrete spec exists on mirrors and return a list Check if concrete spec exists on mirrors and return a list
indicating the mirrors on which it can be found indicating the mirrors on which it can be found
Args:
spec (Spec): The spec to look for in binary mirrors
full_hash_match (bool): If True, only includes mirrors where the spec
full hash matches the locally computed full hash of the ``spec``
argument. If False, any mirror which has a matching DAG hash
is included in the results.
mirrors_to_check (dict): Optionally override the configured mirrors
with the mirrors in this dictionary.
index_only (bool): Do not attempt direct fetching of ``spec.yaml``
files from remote mirrors, only consider the indices.
Return:
A list of objects, each containing a ``mirror_url`` and ``spec`` key
indicating all mirrors where the spec can be found.
""" """
if spec is None: if spec is None:
return [] return []
@ -1390,10 +1405,9 @@ def filter_candidates(candidate_list):
results = filter_candidates(candidates) results = filter_candidates(candidates)
# Maybe we just didn't have the latest information from the mirror, so # Maybe we just didn't have the latest information from the mirror, so
# try to fetch directly. # try to fetch directly, unless we are only considering the indices.
if not results: if not results and not index_only:
results = try_direct_fetch(spec, results = try_direct_fetch(spec,
force=force,
full_hash_match=full_hash_match, full_hash_match=full_hash_match,
mirrors=mirrors_to_check) mirrors=mirrors_to_check)

View File

@ -202,8 +202,8 @@ def format_root_spec(spec, main_phase, strip_compiler):
# spec.name, spec.version, spec.compiler, spec.architecture) # spec.name, spec.version, spec.compiler, spec.architecture)
def spec_deps_key_label(s): def spec_deps_key(s):
return s.dag_hash(), "%s/%s" % (s.name, s.dag_hash(7)) return '{0}/{1}'.format(s.name, s.dag_hash(7))
def _add_dependency(spec_label, dep_label, deps): def _add_dependency(spec_label, dep_label, deps):
@ -214,8 +214,8 @@ def _add_dependency(spec_label, dep_label, deps):
deps[spec_label].add(dep_label) deps[spec_label].add(dep_label)
def get_spec_dependencies(specs, deps, spec_labels): def get_spec_dependencies(specs, deps, spec_labels, check_index_only=False):
spec_deps_obj = compute_spec_deps(specs) spec_deps_obj = compute_spec_deps(specs, check_index_only=check_index_only)
if spec_deps_obj: if spec_deps_obj:
dependencies = spec_deps_obj['dependencies'] dependencies = spec_deps_obj['dependencies']
@ -225,19 +225,25 @@ def get_spec_dependencies(specs, deps, spec_labels):
spec_labels[entry['label']] = { spec_labels[entry['label']] = {
'spec': Spec(entry['spec']), 'spec': Spec(entry['spec']),
'rootSpec': entry['root_spec'], 'rootSpec': entry['root_spec'],
'needs_rebuild': entry['needs_rebuild'],
} }
for entry in dependencies: for entry in dependencies:
_add_dependency(entry['spec'], entry['depends'], deps) _add_dependency(entry['spec'], entry['depends'], deps)
def stage_spec_jobs(specs): def stage_spec_jobs(specs, check_index_only=False):
"""Take a set of release specs and generate a list of "stages", where the """Take a set of release specs and generate a list of "stages", where the
jobs in any stage are dependent only on jobs in previous stages. This jobs in any stage are dependent only on jobs in previous stages. This
allows us to maximize build parallelism within the gitlab-ci framework. allows us to maximize build parallelism within the gitlab-ci framework.
Arguments: Arguments:
specs (Iterable): Specs to build specs (Iterable): Specs to build
check_index_only (bool): Regardless of whether DAG pruning is enabled,
all configured mirrors are searched to see if binaries for specs
are up to date on those mirrors. This flag limits that search to
the binary cache indices on those mirrors to speed the process up,
even though there is no garantee the index is up to date.
Returns: A tuple of information objects describing the specs, dependencies Returns: A tuple of information objects describing the specs, dependencies
and stages: and stages:
@ -274,7 +280,8 @@ def remove_satisfied_deps(deps, satisfied_list):
deps = {} deps = {}
spec_labels = {} spec_labels = {}
get_spec_dependencies(specs, deps, spec_labels) get_spec_dependencies(
specs, deps, spec_labels, check_index_only=check_index_only)
# Save the original deps, as we need to return them at the end of the # Save the original deps, as we need to return them at the end of the
# function. In the while loop below, the "dependencies" variable is # function. In the while loop below, the "dependencies" variable is
@ -311,12 +318,15 @@ def print_staging_summary(spec_labels, dependencies, stages):
for job in sorted(stage): for job in sorted(stage):
s = spec_labels[job]['spec'] s = spec_labels[job]['spec']
tty.msg(' {0} -> {1}'.format(job, get_spec_string(s))) tty.msg(' [{1}] {0} -> {2}'.format(
job,
'x' if spec_labels[job]['needs_rebuild'] else ' ',
get_spec_string(s)))
stage_index += 1 stage_index += 1
def compute_spec_deps(spec_list): def compute_spec_deps(spec_list, check_index_only=False):
""" """
Computes all the dependencies for the spec(s) and generates a JSON Computes all the dependencies for the spec(s) and generates a JSON
object which provides both a list of unique spec names as well as a object which provides both a list of unique spec names as well as a
@ -386,33 +396,39 @@ def append_dep(s, d):
# root_spec = get_spec_string(spec) # root_spec = get_spec_string(spec)
root_spec = spec root_spec = spec
rkey, rlabel = spec_deps_key_label(spec) rkey = spec_deps_key(spec)
for s in spec.traverse(deptype=all): for s in spec.traverse(deptype=all):
if s.external: if s.external:
tty.msg('Will not stage external pkg: {0}'.format(s)) tty.msg('Will not stage external pkg: {0}'.format(s))
continue continue
skey, slabel = spec_deps_key_label(s) up_to_date_mirrors = bindist.get_mirrors_for_spec(
spec_labels[slabel] = { spec=s, full_hash_match=True, index_only=check_index_only)
skey = spec_deps_key(s)
spec_labels[skey] = {
'spec': get_spec_string(s), 'spec': get_spec_string(s),
'root': root_spec, 'root': root_spec,
'needs_rebuild': not up_to_date_mirrors,
} }
append_dep(rlabel, slabel)
append_dep(rkey, skey)
for d in s.dependencies(deptype=all): for d in s.dependencies(deptype=all):
dkey, dlabel = spec_deps_key_label(d) dkey = spec_deps_key(d)
if d.external: if d.external:
tty.msg('Will not stage external dep: {0}'.format(d)) tty.msg('Will not stage external dep: {0}'.format(d))
continue continue
append_dep(slabel, dlabel) append_dep(skey, dkey)
for spec_label, spec_holder in spec_labels.items(): for spec_label, spec_holder in spec_labels.items():
specs.append({ specs.append({
'label': spec_label, 'label': spec_label,
'spec': spec_holder['spec'], 'spec': spec_holder['spec'],
'root_spec': spec_holder['root'], 'root_spec': spec_holder['root'],
'needs_rebuild': spec_holder['needs_rebuild'],
}) })
deps_json_obj = { deps_json_obj = {
@ -481,22 +497,43 @@ def pkg_name_from_spec_label(spec_label):
def format_job_needs(phase_name, strip_compilers, dep_jobs, def format_job_needs(phase_name, strip_compilers, dep_jobs,
osname, build_group, enable_artifacts_buildcache): osname, build_group, prune_dag, stage_spec_dict,
enable_artifacts_buildcache):
needs_list = [] needs_list = []
for dep_job in dep_jobs: for dep_job in dep_jobs:
needs_list.append({ dep_spec_key = spec_deps_key(dep_job)
'job': get_job_name(phase_name, dep_spec_info = stage_spec_dict[dep_spec_key]
strip_compilers,
dep_job, if not prune_dag or dep_spec_info['needs_rebuild']:
osname, needs_list.append({
build_group), 'job': get_job_name(phase_name,
'artifacts': enable_artifacts_buildcache, strip_compilers,
}) dep_job,
osname,
build_group),
'artifacts': enable_artifacts_buildcache,
})
return needs_list return needs_list
def generate_gitlab_ci_yaml(env, print_summary, output_file, def add_pr_mirror(url):
run_optimizer=False, use_dependencies=False): cfg_scope = cfg.default_modify_scope()
mirrors = cfg.get('mirrors', scope=cfg_scope)
items = [(n, u) for n, u in mirrors.items()]
items.insert(0, ('ci_pr_mirror', url))
cfg.set('mirrors', syaml.syaml_dict(items), scope=cfg_scope)
def remove_pr_mirror():
cfg_scope = cfg.default_modify_scope()
mirrors = cfg.get('mirrors', scope=cfg_scope)
mirrors.pop('ci_pr_mirror')
cfg.set('mirrors', mirrors, scope=cfg_scope)
def generate_gitlab_ci_yaml(env, print_summary, output_file, prune_dag=False,
check_index_only=False, run_optimizer=False,
use_dependencies=False):
# FIXME: What's the difference between one that opens with 'spack' # FIXME: What's the difference between one that opens with 'spack'
# and one that opens with 'env'? This will only handle the former. # and one that opens with 'env'? This will only handle the former.
with spack.concretize.disable_compiler_existence_check(): with spack.concretize.disable_compiler_existence_check():
@ -509,10 +546,6 @@ def generate_gitlab_ci_yaml(env, print_summary, output_file,
gitlab_ci = yaml_root['gitlab-ci'] gitlab_ci = yaml_root['gitlab-ci']
final_job_config = None
if 'final-stage-rebuild-index' in gitlab_ci:
final_job_config = gitlab_ci['final-stage-rebuild-index']
build_group = None build_group = None
enable_cdash_reporting = False enable_cdash_reporting = False
cdash_auth_token = None cdash_auth_token = None
@ -539,6 +572,9 @@ def generate_gitlab_ci_yaml(env, print_summary, output_file,
pr_mirror_url = url_util.join(SPACK_PR_MIRRORS_ROOT_URL, pr_mirror_url = url_util.join(SPACK_PR_MIRRORS_ROOT_URL,
spack_pr_branch) spack_pr_branch)
if 'mirrors' not in yaml_root or len(yaml_root['mirrors'].values()) < 1:
tty.die('spack ci generate requires an env containing a mirror')
ci_mirrors = yaml_root['mirrors'] ci_mirrors = yaml_root['mirrors']
mirror_urls = [url for url in ci_mirrors.values()] mirror_urls = [url for url in ci_mirrors.values()]
@ -546,6 +582,10 @@ def generate_gitlab_ci_yaml(env, print_summary, output_file,
if 'enable-artifacts-buildcache' in gitlab_ci: if 'enable-artifacts-buildcache' in gitlab_ci:
enable_artifacts_buildcache = gitlab_ci['enable-artifacts-buildcache'] enable_artifacts_buildcache = gitlab_ci['enable-artifacts-buildcache']
rebuild_index_enabled = True
if 'rebuild-index' in gitlab_ci and gitlab_ci['rebuild-index'] is False:
rebuild_index_enabled = False
bootstrap_specs = [] bootstrap_specs = []
phases = [] phases = []
if 'bootstrap' in gitlab_ci: if 'bootstrap' in gitlab_ci:
@ -573,19 +613,27 @@ def generate_gitlab_ci_yaml(env, print_summary, output_file,
'strip-compilers': False, 'strip-compilers': False,
}) })
staged_phases = {} # Add this mirror if it's enabled, as some specs might be up to date
for phase in phases: # here and thus not need to be rebuilt.
phase_name = phase['name'] if pr_mirror_url:
with spack.concretize.disable_compiler_existence_check(): add_pr_mirror(pr_mirror_url)
staged_phases[phase_name] = stage_spec_jobs(
env.spec_lists[phase_name])
if print_summary: # Speed up staging by first fetching binary indices from all mirrors
# (including the per-PR mirror we may have just added above).
bindist.binary_index.update()
staged_phases = {}
try:
for phase in phases: for phase in phases:
phase_name = phase['name'] phase_name = phase['name']
tty.msg('Stages for phase "{0}"'.format(phase_name)) with spack.concretize.disable_compiler_existence_check():
phase_stages = staged_phases[phase_name] staged_phases[phase_name] = stage_spec_jobs(
print_staging_summary(*phase_stages) env.spec_lists[phase_name],
check_index_only=check_index_only)
finally:
# Clean up PR mirror if enabled
if pr_mirror_url:
remove_pr_mirror()
all_job_names = [] all_job_names = []
output_object = {} output_object = {}
@ -611,7 +659,8 @@ def generate_gitlab_ci_yaml(env, print_summary, output_file,
stage_id += 1 stage_id += 1
for spec_label in stage_jobs: for spec_label in stage_jobs:
root_spec = spec_labels[spec_label]['rootSpec'] spec_record = spec_labels[spec_label]
root_spec = spec_record['rootSpec']
pkg_name = pkg_name_from_spec_label(spec_label) pkg_name = pkg_name_from_spec_label(spec_label)
release_spec = root_spec[pkg_name] release_spec = root_spec[pkg_name]
@ -678,11 +727,15 @@ def generate_gitlab_ci_yaml(env, print_summary, output_file,
job_dependencies = [] job_dependencies = []
if spec_label in dependencies: if spec_label in dependencies:
if enable_artifacts_buildcache: if enable_artifacts_buildcache:
# Get dependencies transitively, so they're all
# available in the artifacts buildcache.
dep_jobs = [ dep_jobs = [
d for d in release_spec.traverse(deptype=all, d for d in release_spec.traverse(deptype=all,
root=False) root=False)
] ]
else: else:
# In this case, "needs" is only used for scheduling
# purposes, so we only get the direct dependencies.
dep_jobs = [] dep_jobs = []
for dep_label in dependencies[spec_label]: for dep_label in dependencies[spec_label]:
dep_pkg = pkg_name_from_spec_label(dep_label) dep_pkg = pkg_name_from_spec_label(dep_label)
@ -690,10 +743,13 @@ def generate_gitlab_ci_yaml(env, print_summary, output_file,
dep_jobs.append(dep_root[dep_pkg]) dep_jobs.append(dep_root[dep_pkg])
job_dependencies.extend( job_dependencies.extend(
format_job_needs(phase_name, strip_compilers, dep_jobs, format_job_needs(phase_name, strip_compilers,
osname, build_group, dep_jobs, osname, build_group,
prune_dag, spec_labels,
enable_artifacts_buildcache)) enable_artifacts_buildcache))
rebuild_spec = spec_record['needs_rebuild']
# This next section helps gitlab make sure the right # This next section helps gitlab make sure the right
# bootstrapped compiler exists in the artifacts buildcache by # bootstrapped compiler exists in the artifacts buildcache by
# creating an artificial dependency between this spec and its # creating an artificial dependency between this spec and its
@ -709,11 +765,12 @@ def generate_gitlab_ci_yaml(env, print_summary, output_file,
compiler_pkg_spec = compilers.pkg_spec_for_compiler( compiler_pkg_spec = compilers.pkg_spec_for_compiler(
release_spec.compiler) release_spec.compiler)
for bs in bootstrap_specs: for bs in bootstrap_specs:
bs_arch = bs['spec'].architecture c_spec = bs['spec']
bs_arch = c_spec.architecture
bs_arch_family = (bs_arch.target bs_arch_family = (bs_arch.target
.microarchitecture .microarchitecture
.family) .family)
if (bs['spec'].satisfies(compiler_pkg_spec) and if (c_spec.satisfies(compiler_pkg_spec) and
bs_arch_family == spec_arch_family): bs_arch_family == spec_arch_family):
# We found the bootstrap compiler this release spec # We found the bootstrap compiler this release spec
# should be built with, so for DAG scheduling # should be built with, so for DAG scheduling
@ -721,10 +778,24 @@ def generate_gitlab_ci_yaml(env, print_summary, output_file,
# to the jobs "needs". But if artifact buildcache # to the jobs "needs". But if artifact buildcache
# is enabled, we'll have to add all transtive deps # is enabled, we'll have to add all transtive deps
# of the compiler as well. # of the compiler as well.
dep_jobs = [bs['spec']]
# Here we check whether the bootstrapped compiler
# needs to be rebuilt. Until compilers are proper
# dependencies, we artificially force the spec to
# be rebuilt if the compiler targeted to build it
# needs to be rebuilt.
bs_specs, _, _ = staged_phases[bs['phase-name']]
c_spec_key = spec_deps_key(c_spec)
rbld_comp = bs_specs[c_spec_key]['needs_rebuild']
rebuild_spec = rebuild_spec or rbld_comp
# Also update record so dependents do not fail to
# add this spec to their "needs"
spec_record['needs_rebuild'] = rebuild_spec
dep_jobs = [c_spec]
if enable_artifacts_buildcache: if enable_artifacts_buildcache:
dep_jobs = [ dep_jobs = [
d for d in bs['spec'].traverse(deptype=all) d for d in c_spec.traverse(deptype=all)
] ]
job_dependencies.extend( job_dependencies.extend(
@ -733,6 +804,8 @@ def generate_gitlab_ci_yaml(env, print_summary, output_file,
dep_jobs, dep_jobs,
str(bs_arch), str(bs_arch),
build_group, build_group,
prune_dag,
bs_specs,
enable_artifacts_buildcache)) enable_artifacts_buildcache))
else: else:
debug_msg = ''.join([ debug_msg = ''.join([
@ -741,9 +814,14 @@ def generate_gitlab_ci_yaml(env, print_summary, output_file,
'not the compiler required by the spec, or ', 'not the compiler required by the spec, or ',
'because the target arch families of the ', 'because the target arch families of the ',
'spec and the compiler did not match' 'spec and the compiler did not match'
]).format(bs['spec'], release_spec) ]).format(c_spec, release_spec)
tty.debug(debug_msg) tty.debug(debug_msg)
if prune_dag and not rebuild_spec:
continue
job_vars['SPACK_SPEC_NEEDS_REBUILD'] = str(rebuild_spec)
if enable_cdash_reporting: if enable_cdash_reporting:
cdash_build_name = get_cdash_build_name( cdash_build_name = get_cdash_build_name(
release_spec, build_group) release_spec, build_group)
@ -812,11 +890,19 @@ def generate_gitlab_ci_yaml(env, print_summary, output_file,
output_object[job_name] = job_object output_object[job_name] = job_object
job_id += 1 job_id += 1
if print_summary:
for phase in phases:
phase_name = phase['name']
tty.msg('Stages for phase "{0}"'.format(phase_name))
phase_stages = staged_phases[phase_name]
print_staging_summary(*phase_stages)
tty.debug('{0} build jobs generated in {1} stages'.format( tty.debug('{0} build jobs generated in {1} stages'.format(
job_id, stage_id)) job_id, stage_id))
tty.debug('The max_needs_job is {0}, with {1} needs'.format( if job_id > 0:
max_needs_job, max_length_needs)) tty.debug('The max_needs_job is {0}, with {1} needs'.format(
max_needs_job, max_length_needs))
# Use "all_job_names" to populate the build group for this set # Use "all_job_names" to populate the build group for this set
if enable_cdash_reporting and cdash_auth_token: if enable_cdash_reporting and cdash_auth_token:
@ -828,63 +914,94 @@ def generate_gitlab_ci_yaml(env, print_summary, output_file,
else: else:
tty.warn('Unable to populate buildgroup without CDash credentials') tty.warn('Unable to populate buildgroup without CDash credentials')
if final_job_config and not is_pr_pipeline: service_job_config = None
# Add an extra, final job to regenerate the index if 'service-job-attributes' in gitlab_ci:
final_stage = 'stage-rebuild-index' service_job_config = gitlab_ci['service-job-attributes']
final_job = {
'stage': final_stage,
'script': 'spack buildcache update-index --keys -d {0}'.format(
mirror_urls[0]),
'tags': final_job_config['tags'],
'when': 'always'
}
if 'image' in final_job_config:
final_job['image'] = final_job_config['image']
if before_script:
final_job['before_script'] = before_script
if after_script:
final_job['after_script'] = after_script
output_object['rebuild-index'] = final_job
stage_names.append(final_stage)
output_object['stages'] = stage_names default_attrs = [
'image',
'tags',
'variables',
'before_script',
# 'script',
'after_script',
]
# Capture the version of spack used to generate the pipeline, transform it if job_id > 0:
# into a value that can be passed to "git checkout", and save it in a if rebuild_index_enabled and not is_pr_pipeline:
# global yaml variable # Add a final job to regenerate the index
spack_version = spack.main.get_version() final_stage = 'stage-rebuild-index'
version_to_clone = None final_job = {}
v_match = re.match(r"^\d+\.\d+\.\d+$", spack_version)
if v_match: if service_job_config:
version_to_clone = 'v{0}'.format(v_match.group(0)) copy_attributes(default_attrs,
else: service_job_config,
v_match = re.match(r"^[^-]+-[^-]+-([a-f\d]+)$", spack_version) final_job)
final_script = 'spack buildcache update-index --keys'
final_script = '{0} -d {1}'.format(final_script, mirror_urls[0])
final_job['stage'] = final_stage
final_job['script'] = [final_script]
final_job['when'] = 'always'
output_object['rebuild-index'] = final_job
stage_names.append(final_stage)
output_object['stages'] = stage_names
# Capture the version of spack used to generate the pipeline, transform it
# into a value that can be passed to "git checkout", and save it in a
# global yaml variable
spack_version = spack.main.get_version()
version_to_clone = None
v_match = re.match(r"^\d+\.\d+\.\d+$", spack_version)
if v_match: if v_match:
version_to_clone = v_match.group(1) version_to_clone = 'v{0}'.format(v_match.group(0))
else: else:
version_to_clone = spack_version v_match = re.match(r"^[^-]+-[^-]+-([a-f\d]+)$", spack_version)
if v_match:
version_to_clone = v_match.group(1)
else:
version_to_clone = spack_version
output_object['variables'] = { output_object['variables'] = {
'SPACK_VERSION': spack_version, 'SPACK_VERSION': spack_version,
'SPACK_CHECKOUT_VERSION': version_to_clone, 'SPACK_CHECKOUT_VERSION': version_to_clone,
} }
if pr_mirror_url: if pr_mirror_url:
output_object['variables']['SPACK_PR_MIRROR_URL'] = pr_mirror_url output_object['variables']['SPACK_PR_MIRROR_URL'] = pr_mirror_url
sorted_output = {} sorted_output = {}
for output_key, output_value in sorted(output_object.items()): for output_key, output_value in sorted(output_object.items()):
sorted_output[output_key] = output_value sorted_output[output_key] = output_value
# TODO(opadron): remove this or refactor # TODO(opadron): remove this or refactor
if run_optimizer: if run_optimizer:
import spack.ci_optimization as ci_opt import spack.ci_optimization as ci_opt
sorted_output = ci_opt.optimizer(sorted_output) sorted_output = ci_opt.optimizer(sorted_output)
# TODO(opadron): remove this or refactor # TODO(opadron): remove this or refactor
if use_dependencies: if use_dependencies:
import spack.ci_needs_workaround as cinw import spack.ci_needs_workaround as cinw
sorted_output = cinw.needs_to_dependencies(sorted_output) sorted_output = cinw.needs_to_dependencies(sorted_output)
else:
# No jobs were generated
tty.debug('No specs to rebuild, generating no-op job')
noop_job = {}
if service_job_config:
copy_attributes(default_attrs,
service_job_config,
noop_job)
if 'script' not in noop_job:
noop_job['script'] = [
'echo "All specs already up to date, nothing to rebuild."',
]
sorted_output = {'no-specs-to-rebuild': noop_job}
with open(output_file, 'w') as outf: with open(output_file, 'w') as outf:
outf.write(syaml.dump_config(sorted_output, default_flow_style=True)) outf.write(syaml.dump_config(sorted_output, default_flow_style=True))

View File

@ -769,19 +769,14 @@ def buildcache_copy(args):
shutil.copyfile(cdashid_src_path, cdashid_dest_path) shutil.copyfile(cdashid_src_path, cdashid_dest_path)
def buildcache_update_index(args): def update_index(mirror_url, update_keys=False):
"""Update a buildcache index.""" mirror = spack.mirror.MirrorCollection().lookup(mirror_url)
outdir = '.'
if args.mirror_url:
outdir = args.mirror_url
mirror = spack.mirror.MirrorCollection().lookup(outdir)
outdir = url_util.format(mirror.push_url) outdir = url_util.format(mirror.push_url)
bindist.generate_package_index( bindist.generate_package_index(
url_util.join(outdir, bindist.build_cache_relative_path())) url_util.join(outdir, bindist.build_cache_relative_path()))
if args.keys: if update_keys:
keys_url = url_util.join(outdir, keys_url = url_util.join(outdir,
bindist.build_cache_relative_path(), bindist.build_cache_relative_path(),
bindist.build_cache_keys_relative_path()) bindist.build_cache_keys_relative_path())
@ -789,6 +784,15 @@ def buildcache_update_index(args):
bindist.generate_key_index(keys_url) bindist.generate_key_index(keys_url)
def buildcache_update_index(args):
"""Update a buildcache index."""
outdir = '.'
if args.mirror_url:
outdir = args.mirror_url
update_index(outdir, update_keys=args.keys)
def buildcache(parser, args): def buildcache(parser, args):
if args.func: if args.func:
args.func(args) args.func(args)

View File

@ -54,6 +54,26 @@ def setup_parser(subparser):
'--dependencies', action='store_true', default=False, '--dependencies', action='store_true', default=False,
help="(Experimental) disable DAG scheduling; use " help="(Experimental) disable DAG scheduling; use "
' "plain" dependencies.') ' "plain" dependencies.')
prune_group = generate.add_mutually_exclusive_group()
prune_group.add_argument(
'--prune-dag', action='store_true', dest='prune_dag',
default=True, help="""Do not generate jobs for specs already up to
date on the mirror""")
prune_group.add_argument(
'--no-prune-dag', action='store_false', dest='prune_dag',
default=True, help="""Generate jobs for specs already up to date
on the mirror""")
generate.add_argument(
'--check-index-only', action='store_true', dest='index_only',
default=False, help="""Spack always check specs against configured
binary mirrors when generating the pipeline, regardless of whether or not
DAG pruning is enabled. This flag controls whether it might attempt to
fetch remote spec.yaml files directly (ensuring no spec is rebuilt if it is
present on the mirror), or whether it should reduce pipeline generation time
by assuming all remote buildcache indices are up to date and only use those
to determine whether a given spec is up to date on mirrors. In the latter
case, specs might be needlessly rebuilt if remote buildcache indices are out
of date.""")
generate.set_defaults(func=ci_generate) generate.set_defaults(func=ci_generate)
# Check a spec against mirror. Rebuild, create buildcache and push to # Check a spec against mirror. Rebuild, create buildcache and push to
@ -61,6 +81,11 @@ def setup_parser(subparser):
rebuild = subparsers.add_parser('rebuild', help=ci_rebuild.__doc__) rebuild = subparsers.add_parser('rebuild', help=ci_rebuild.__doc__)
rebuild.set_defaults(func=ci_rebuild) rebuild.set_defaults(func=ci_rebuild)
# Rebuild the buildcache index associated with the mirror in the
# active, gitlab-enabled environment.
index = subparsers.add_parser('rebuild-index', help=ci_reindex.__doc__)
index.set_defaults(func=ci_reindex)
def ci_generate(args): def ci_generate(args):
"""Generate jobs file from a spack environment file containing CI info. """Generate jobs file from a spack environment file containing CI info.
@ -75,6 +100,8 @@ def ci_generate(args):
copy_yaml_to = args.copy_to copy_yaml_to = args.copy_to
run_optimizer = args.optimize run_optimizer = args.optimize
use_dependencies = args.dependencies use_dependencies = args.dependencies
prune_dag = args.prune_dag
index_only = args.index_only
if not output_file: if not output_file:
output_file = os.path.abspath(".gitlab-ci.yml") output_file = os.path.abspath(".gitlab-ci.yml")
@ -86,7 +113,8 @@ def ci_generate(args):
# Generate the jobs # Generate the jobs
spack_ci.generate_gitlab_ci_yaml( spack_ci.generate_gitlab_ci_yaml(
env, True, output_file, run_optimizer=run_optimizer, env, True, output_file, prune_dag=prune_dag,
check_index_only=index_only, run_optimizer=run_optimizer,
use_dependencies=use_dependencies) use_dependencies=use_dependencies)
if copy_yaml_to: if copy_yaml_to:
@ -306,8 +334,8 @@ def add_mirror(mirror_name, mirror_url):
# Checks all mirrors for a built spec with a matching full hash # Checks all mirrors for a built spec with a matching full hash
matches = bindist.get_mirrors_for_spec( matches = bindist.get_mirrors_for_spec(
job_spec, force=False, full_hash_match=True, job_spec, full_hash_match=True, mirrors_to_check=mirrors_to_check,
mirrors_to_check=mirrors_to_check) index_only=False)
if matches: if matches:
# Got at full hash match on at least one configured mirror. All # Got at full hash match on at least one configured mirror. All
@ -408,6 +436,22 @@ def add_mirror(mirror_name, mirror_url):
artifact_mirror_url or pr_mirror_url or remote_mirror_url) artifact_mirror_url or pr_mirror_url or remote_mirror_url)
def ci_reindex(args):
"""Rebuild the buildcache index associated with the mirror in the
active, gitlab-enabled environment. """
env = ev.get_env(args, 'ci rebuild-index', required=True)
yaml_root = ev.config_dict(env.yaml)
if 'mirrors' not in yaml_root or len(yaml_root['mirrors'].values()) < 1:
tty.die('spack ci rebuild-index requires an env containing a mirror')
ci_mirrors = yaml_root['mirrors']
mirror_urls = [url for url in ci_mirrors.values()]
remote_mirror_url = mirror_urls[0]
buildcache.update_index(remote_mirror_url, update_keys=True)
def ci(parser, args): def ci(parser, args):
if args.func: if args.func:
args.func(args) args.func(args)

View File

@ -392,7 +392,7 @@ def _try_install_from_binary_cache(pkg, explicit, unsigned=False,
pkg_id = package_id(pkg) pkg_id = package_id(pkg)
tty.debug('Searching for binary cache of {0}'.format(pkg_id)) tty.debug('Searching for binary cache of {0}'.format(pkg_id))
matches = binary_distribution.get_mirrors_for_spec( matches = binary_distribution.get_mirrors_for_spec(
pkg.spec, force=False, full_hash_match=full_hash_match) pkg.spec, full_hash_match=full_hash_match)
if not matches: if not matches:
return False return False

View File

@ -9,6 +9,8 @@
:lines: 13- :lines: 13-
""" """
from llnl.util.lang import union_dicts
image_schema = { image_schema = {
'oneOf': [ 'oneOf': [
{ {
@ -28,127 +30,98 @@
], ],
} }
runner_attributes_schema_items = {
'image': image_schema,
'tags': {
'type': 'array',
'items': {'type': 'string'}
},
'variables': {
'type': 'object',
'patternProperties': {
r'[\w\d\-_\.]+': {
'type': 'string',
},
},
},
'before_script': {
'type': 'array',
'items': {'type': 'string'}
},
'script': {
'type': 'array',
'items': {'type': 'string'}
},
'after_script': {
'type': 'array',
'items': {'type': 'string'}
},
}
runner_selector_schema = {
'type': 'object',
'additionalProperties': False,
'required': ['tags'],
'properties': runner_attributes_schema_items,
}
#: Properties for inclusion in other schemas #: Properties for inclusion in other schemas
properties = { properties = {
'gitlab-ci': { 'gitlab-ci': {
'type': 'object', 'type': 'object',
'additionalProperties': False, 'additionalProperties': False,
'required': ['mappings'], 'required': ['mappings'],
'patternProperties': { 'patternProperties': union_dicts(
'bootstrap': { runner_attributes_schema_items,
'type': 'array', {
'items': { 'bootstrap': {
'anyOf': [ 'type': 'array',
{ 'items': {
'type': 'string', 'anyOf': [
}, { {
'type': 'object',
'additionalProperties': False,
'required': ['name'],
'properties': {
'name': {
'type': 'string',
},
'compiler-agnostic': {
'type': 'boolean',
'default': False,
},
},
},
],
},
},
'mappings': {
'type': 'array',
'items': {
'type': 'object',
'additionalProperties': False,
'required': ['match'],
'properties': {
'match': {
'type': 'array',
'items': {
'type': 'string', 'type': 'string',
}, }, {
}, 'type': 'object',
'runner-attributes': { 'additionalProperties': False,
'type': 'object', 'required': ['name'],
'additionalProperties': True, 'properties': {
'required': ['tags'], 'name': {
'properties': { 'type': 'string',
'image': image_schema, },
'tags': { 'compiler-agnostic': {
'type': 'array', 'type': 'boolean',
'items': {'type': 'string'} 'default': False,
},
'variables': {
'type': 'object',
'patternProperties': {
r'[\w\d\-_\.]+': {
'type': 'string',
},
}, },
}, },
'before_script': { },
'type': 'array', ],
'items': {'type': 'string'} },
}, },
'script': { 'mappings': {
'type': 'array', 'type': 'array',
'items': {'type': 'string'} 'items': {
}, 'type': 'object',
'after_script': { 'additionalProperties': False,
'type': 'array', 'required': ['match'],
'items': {'type': 'string'} 'properties': {
'match': {
'type': 'array',
'items': {
'type': 'string',
}, },
}, },
'runner-attributes': runner_selector_schema,
}, },
}, },
}, },
}, 'enable-artifacts-buildcache': {
'image': image_schema, 'type': 'boolean',
'tags': { 'default': False,
'type': 'array',
'items': {'type': 'string'}
},
'variables': {
'type': 'object',
'patternProperties': {
r'[\w\d\-_\.]+': {
'type': 'string',
},
}, },
}, 'service-job-attributes': runner_selector_schema,
'before_script': { 'rebuild-index': {'type': 'boolean'},
'type': 'array', }
'items': {'type': 'string'} ),
},
'script': {
'type': 'array',
'items': {'type': 'string'}
},
'after_script': {
'type': 'array',
'items': {'type': 'string'}
},
'enable-artifacts-buildcache': {
'type': 'boolean',
'default': False,
},
'final-stage-rebuild-index': {
'type': 'object',
'additionalProperties': False,
'required': ['tags'],
'properties': {
'image': image_schema,
'tags': {
'type': 'array',
'default': [],
'items': {'type': 'string'}
},
},
},
},
}, },
} }

View File

@ -11,6 +11,7 @@
import spack import spack
import spack.ci as ci import spack.ci as ci
import spack.compilers as compilers
import spack.config import spack.config
import spack.environment as ev import spack.environment as ev
import spack.hash_types as ht import spack.hash_types as ht
@ -19,7 +20,7 @@
import spack.repo as repo import spack.repo as repo
from spack.schema.buildcache_spec import schema as spec_yaml_schema from spack.schema.buildcache_spec import schema as spec_yaml_schema
from spack.schema.database_index import schema as db_idx_schema from spack.schema.database_index import schema as db_idx_schema
from spack.spec import Spec from spack.spec import Spec, CompilerSpec
from spack.util.mock_package import MockPackageMultiRepo from spack.util.mock_package import MockPackageMultiRepo
import spack.util.executable as exe import spack.util.executable as exe
import spack.util.spack_yaml as syaml import spack.util.spack_yaml as syaml
@ -31,6 +32,7 @@
mirror_cmd = spack.main.SpackCommand('mirror') mirror_cmd = spack.main.SpackCommand('mirror')
gpg_cmd = spack.main.SpackCommand('gpg') gpg_cmd = spack.main.SpackCommand('gpg')
install_cmd = spack.main.SpackCommand('install') install_cmd = spack.main.SpackCommand('install')
uninstall_cmd = spack.main.SpackCommand('uninstall')
buildcache_cmd = spack.main.SpackCommand('buildcache') buildcache_cmd = spack.main.SpackCommand('buildcache')
git = exe.which('git', required=True) git = exe.which('git', required=True)
@ -77,13 +79,13 @@ def test_specs_staging(config):
spec_a = Spec('a') spec_a = Spec('a')
spec_a.concretize() spec_a.concretize()
spec_a_label = ci.spec_deps_key_label(spec_a)[1] spec_a_label = ci.spec_deps_key(spec_a)
spec_b_label = ci.spec_deps_key_label(spec_a['b'])[1] spec_b_label = ci.spec_deps_key(spec_a['b'])
spec_c_label = ci.spec_deps_key_label(spec_a['c'])[1] spec_c_label = ci.spec_deps_key(spec_a['c'])
spec_d_label = ci.spec_deps_key_label(spec_a['d'])[1] spec_d_label = ci.spec_deps_key(spec_a['d'])
spec_e_label = ci.spec_deps_key_label(spec_a['e'])[1] spec_e_label = ci.spec_deps_key(spec_a['e'])
spec_f_label = ci.spec_deps_key_label(spec_a['f'])[1] spec_f_label = ci.spec_deps_key(spec_a['f'])
spec_g_label = ci.spec_deps_key_label(spec_a['g'])[1] spec_g_label = ci.spec_deps_key(spec_a['g'])
spec_labels, dependencies, stages = ci.stage_spec_jobs([spec_a]) spec_labels, dependencies, stages = ci.stage_spec_jobs([spec_a])
@ -109,6 +111,7 @@ def test_ci_generate_with_env(tmpdir, mutable_mock_env_path, env_deactivate,
install_mockery, mock_packages): install_mockery, mock_packages):
"""Make sure we can get a .gitlab-ci.yml from an environment file """Make sure we can get a .gitlab-ci.yml from an environment file
which has the gitlab-ci, cdash, and mirrors sections.""" which has the gitlab-ci, cdash, and mirrors sections."""
mirror_url = 'https://my.fake.mirror'
filename = str(tmpdir.join('spack.yaml')) filename = str(tmpdir.join('spack.yaml'))
with open(filename, 'w') as f: with open(filename, 'w') as f:
f.write("""\ f.write("""\
@ -128,7 +131,7 @@ def test_ci_generate_with_env(tmpdir, mutable_mock_env_path, env_deactivate,
- matrix: - matrix:
- [$old-gcc-pkgs] - [$old-gcc-pkgs]
mirrors: mirrors:
some-mirror: https://my.fake.mirror some-mirror: {0}
gitlab-ci: gitlab-ci:
bootstrap: bootstrap:
- name: bootstrap - name: bootstrap
@ -140,7 +143,7 @@ def test_ci_generate_with_env(tmpdir, mutable_mock_env_path, env_deactivate,
tags: tags:
- donotcare - donotcare
image: donotcare image: donotcare
final-stage-rebuild-index: service-job-attributes:
image: donotcare image: donotcare
tags: [donotcare] tags: [donotcare]
cdash: cdash:
@ -148,7 +151,7 @@ def test_ci_generate_with_env(tmpdir, mutable_mock_env_path, env_deactivate,
url: https://my.fake.cdash url: https://my.fake.cdash
project: Not used project: Not used
site: Nothing site: Nothing
""") """.format(mirror_url))
with tmpdir.as_cwd(): with tmpdir.as_cwd():
env_cmd('create', 'test', './spack.yaml') env_cmd('create', 'test', './spack.yaml')
outputfile = str(tmpdir.join('.gitlab-ci.yml')) outputfile = str(tmpdir.join('.gitlab-ci.yml'))
@ -170,6 +173,12 @@ def test_ci_generate_with_env(tmpdir, mutable_mock_env_path, env_deactivate,
assert(yaml_contents['stages'][0] == 'stage-0') assert(yaml_contents['stages'][0] == 'stage-0')
assert(yaml_contents['stages'][5] == 'stage-rebuild-index') assert(yaml_contents['stages'][5] == 'stage-rebuild-index')
assert('rebuild-index' in yaml_contents)
rebuild_job = yaml_contents['rebuild-index']
expected = 'spack buildcache update-index --keys -d {0}'.format(
mirror_url)
assert(rebuild_job['script'][0] == expected)
def _validate_needs_graph(yaml_contents, needs_graph, artifacts): def _validate_needs_graph(yaml_contents, needs_graph, artifacts):
for job_name, job_def in yaml_contents.items(): for job_name, job_def in yaml_contents.items():
@ -533,7 +542,7 @@ def test_ci_generate_pkg_with_deps(tmpdir, mutable_mock_env_path,
def test_ci_generate_for_pr_pipeline(tmpdir, mutable_mock_env_path, def test_ci_generate_for_pr_pipeline(tmpdir, mutable_mock_env_path,
env_deactivate, install_mockery, env_deactivate, install_mockery,
mock_packages): mock_packages, monkeypatch):
"""Test that PR pipelines do not include a final stage job for """Test that PR pipelines do not include a final stage job for
rebuilding the mirror index, even if that job is specifically rebuilding the mirror index, even if that job is specifically
configured""" configured"""
@ -558,9 +567,10 @@ def test_ci_generate_for_pr_pipeline(tmpdir, mutable_mock_env_path,
runner-attributes: runner-attributes:
tags: tags:
- donotcare - donotcare
final-stage-rebuild-index: service-job-attributes:
image: donotcare image: donotcare
tags: [donotcare] tags: [donotcare]
rebuild-index: False
""") """)
with tmpdir.as_cwd(): with tmpdir.as_cwd():
@ -569,6 +579,9 @@ def test_ci_generate_for_pr_pipeline(tmpdir, mutable_mock_env_path,
with ev.read('test'): with ev.read('test'):
os.environ['SPACK_IS_PR_PIPELINE'] = 'True' os.environ['SPACK_IS_PR_PIPELINE'] = 'True'
os.environ['SPACK_PR_BRANCH'] = 'fake-test-branch'
monkeypatch.setattr(
ci, 'SPACK_PR_MIRRORS_ROOT_URL', r"file:///fake/mirror")
ci_cmd('generate', '--output-file', outputfile) ci_cmd('generate', '--output-file', outputfile)
with open(outputfile) as f: with open(outputfile) as f:
@ -579,10 +592,17 @@ def test_ci_generate_for_pr_pipeline(tmpdir, mutable_mock_env_path,
assert('rebuild-index' not in yaml_contents) assert('rebuild-index' not in yaml_contents)
for ci_key in yaml_contents.keys():
if ci_key.startswith('(specs) '):
job_object = yaml_contents[ci_key]
job_vars = job_object['variables']
assert('SPACK_IS_PR_PIPELINE' in job_vars)
assert(job_vars['SPACK_IS_PR_PIPELINE'] == 'True')
def test_ci_generate_with_external_pkg(tmpdir, mutable_mock_env_path, def test_ci_generate_with_external_pkg(tmpdir, mutable_mock_env_path,
env_deactivate, install_mockery, env_deactivate, install_mockery,
mock_packages): mock_packages, monkeypatch):
"""Make sure we do not generate jobs for external pkgs""" """Make sure we do not generate jobs for external pkgs"""
filename = str(tmpdir.join('spack.yaml')) filename = str(tmpdir.join('spack.yaml'))
with open(filename, 'w') as f: with open(filename, 'w') as f:
@ -609,6 +629,8 @@ def test_ci_generate_with_external_pkg(tmpdir, mutable_mock_env_path,
outputfile = str(tmpdir.join('.gitlab-ci.yml')) outputfile = str(tmpdir.join('.gitlab-ci.yml'))
with ev.read('test'): with ev.read('test'):
monkeypatch.setattr(
ci, 'SPACK_PR_MIRRORS_ROOT_URL', r"file:///fake/mirror")
ci_cmd('generate', '--output-file', outputfile) ci_cmd('generate', '--output-file', outputfile)
with open(outputfile) as f: with open(outputfile) as f:
@ -712,6 +734,19 @@ def test_push_mirror_contents(tmpdir, mutable_mock_env_path, env_deactivate,
- $packages - $packages
mirrors: mirrors:
test-mirror: {0} test-mirror: {0}
gitlab-ci:
enable-artifacts-buildcache: True
mappings:
- match:
- patchelf
runner-attributes:
tags:
- donotcare
image: donotcare
service-job-attributes:
tags:
- nonbuildtag
image: basicimage
""".format(mirror_url) """.format(mirror_url)
print('spack.yaml:\n{0}\n'.format(spack_yaml_contents)) print('spack.yaml:\n{0}\n'.format(spack_yaml_contents))
@ -739,6 +774,48 @@ def test_push_mirror_contents(tmpdir, mutable_mock_env_path, env_deactivate,
buildcache_path = os.path.join(mirror_dir.strpath, 'build_cache') buildcache_path = os.path.join(mirror_dir.strpath, 'build_cache')
# Now test the --prune-dag (default) option of spack ci generate
mirror_cmd('add', 'test-ci', mirror_url)
outputfile_pruned = str(tmpdir.join('pruned_pipeline.yml'))
ci_cmd('generate', '--output-file', outputfile_pruned)
with open(outputfile_pruned) as f:
contents = f.read()
yaml_contents = syaml.load(contents)
assert('no-specs-to-rebuild' in yaml_contents)
# Make sure there are no other spec jobs or rebuild-index
assert(len(yaml_contents.keys()) == 1)
the_elt = yaml_contents['no-specs-to-rebuild']
assert('tags' in the_elt)
assert('nonbuildtag' in the_elt['tags'])
assert('image' in the_elt)
assert(the_elt['image'] == 'basicimage')
outputfile_not_pruned = str(tmpdir.join('unpruned_pipeline.yml'))
ci_cmd('generate', '--no-prune-dag', '--output-file',
outputfile_not_pruned)
# Test the --no-prune-dag option of spack ci generate
with open(outputfile_not_pruned) as f:
contents = f.read()
yaml_contents = syaml.load(contents)
found_spec_job = False
for ci_key in yaml_contents.keys():
if '(specs) patchelf' in ci_key:
the_elt = yaml_contents[ci_key]
assert('variables' in the_elt)
job_vars = the_elt['variables']
assert('SPACK_SPEC_NEEDS_REBUILD' in job_vars)
assert(job_vars['SPACK_SPEC_NEEDS_REBUILD'] == 'False')
found_spec_job = True
assert(found_spec_job)
mirror_cmd('rm', 'test-ci')
# Test generating buildcache index while we have bin mirror # Test generating buildcache index while we have bin mirror
buildcache_cmd('update-index', '--mirror-url', mirror_url) buildcache_cmd('update-index', '--mirror-url', mirror_url)
index_path = os.path.join(buildcache_path, 'index.json') index_path = os.path.join(buildcache_path, 'index.json')
@ -839,7 +916,7 @@ def test_ci_generate_override_runner_attrs(tmpdir, mutable_mock_env_path,
- custom main step - custom main step
after_script: after_script:
- custom post step one - custom post step one
final-stage-rebuild-index: service-job-attributes:
image: donotcare image: donotcare
tags: [donotcare] tags: [donotcare]
""") """)
@ -851,6 +928,8 @@ def test_ci_generate_override_runner_attrs(tmpdir, mutable_mock_env_path,
with ev.read('test'): with ev.read('test'):
monkeypatch.setattr( monkeypatch.setattr(
spack.main, 'get_version', lambda: '0.15.3-416-12ad69eb1') spack.main, 'get_version', lambda: '0.15.3-416-12ad69eb1')
monkeypatch.setattr(
ci, 'SPACK_PR_MIRRORS_ROOT_URL', r"file:///fake/mirror")
ci_cmd('generate', '--output-file', outputfile) ci_cmd('generate', '--output-file', outputfile)
with open(outputfile) as f: with open(outputfile) as f:
@ -925,3 +1004,270 @@ def test_ci_generate_override_runner_attrs(tmpdir, mutable_mock_env_path,
assert(the_elt['script'][0] == 'main step') assert(the_elt['script'][0] == 'main step')
assert(len(the_elt['after_script']) == 1) assert(len(the_elt['after_script']) == 1)
assert(the_elt['after_script'][0] == 'post step one') assert(the_elt['after_script'][0] == 'post step one')
def test_ci_generate_with_workarounds(tmpdir, mutable_mock_env_path,
env_deactivate, install_mockery,
mock_packages, monkeypatch):
"""Make sure the post-processing cli workarounds do what they should"""
filename = str(tmpdir.join('spack.yaml'))
with open(filename, 'w') as f:
f.write("""\
spack:
specs:
- callpath%gcc@3.0
mirrors:
some-mirror: https://my.fake.mirror
gitlab-ci:
mappings:
- match: ['%gcc@3.0']
runner-attributes:
tags:
- donotcare
image: donotcare
enable-artifacts-buildcache: true
""")
with tmpdir.as_cwd():
env_cmd('create', 'test', './spack.yaml')
outputfile = str(tmpdir.join('.gitlab-ci.yml'))
with ev.read('test'):
monkeypatch.setattr(
ci, 'SPACK_PR_MIRRORS_ROOT_URL', r"file:///fake/mirror")
ci_cmd('generate', '--output-file', outputfile, '--dependencies')
with open(outputfile) as f:
contents = f.read()
yaml_contents = syaml.load(contents)
found_one = False
for ci_key in yaml_contents.keys():
if ci_key.startswith('(specs) '):
found_one = True
job_obj = yaml_contents[ci_key]
assert('needs' not in job_obj)
assert('dependencies' in job_obj)
assert(found_one is True)
@pytest.mark.disable_clean_stage_check
def test_ci_rebuild_index(tmpdir, mutable_mock_env_path, env_deactivate,
install_mockery, mock_packages, mock_fetch,
mock_stage):
working_dir = tmpdir.join('working_dir')
mirror_dir = working_dir.join('mirror')
mirror_url = 'file://{0}'.format(mirror_dir.strpath)
spack_yaml_contents = """
spack:
specs:
- callpath
mirrors:
test-mirror: {0}
gitlab-ci:
mappings:
- match:
- patchelf
runner-attributes:
tags:
- donotcare
image: donotcare
""".format(mirror_url)
filename = str(tmpdir.join('spack.yaml'))
with open(filename, 'w') as f:
f.write(spack_yaml_contents)
with tmpdir.as_cwd():
env_cmd('create', 'test', './spack.yaml')
with ev.read('test'):
spec_map = ci.get_concrete_specs(
'callpath', 'callpath', '', 'FIND_ANY')
concrete_spec = spec_map['callpath']
spec_yaml = concrete_spec.to_yaml(hash=ht.build_hash)
yaml_path = str(tmpdir.join('spec.yaml'))
with open(yaml_path, 'w') as ypfd:
ypfd.write(spec_yaml)
install_cmd('--keep-stage', '-f', yaml_path)
buildcache_cmd('create', '-u', '-a', '-f', '--mirror-url',
mirror_url, 'callpath')
ci_cmd('rebuild-index')
buildcache_path = os.path.join(mirror_dir.strpath, 'build_cache')
index_path = os.path.join(buildcache_path, 'index.json')
with open(index_path) as idx_fd:
index_object = json.load(idx_fd)
validate(index_object, db_idx_schema)
def test_ci_generate_bootstrap_prune_dag(
install_mockery_mutable_config, mock_packages, mock_fetch,
mock_archive, mutable_config, monkeypatch, tmpdir,
mutable_mock_env_path, env_deactivate):
"""Test compiler bootstrapping with DAG pruning. Specifically, make
sure that if we detect the bootstrapped compiler needs to be rebuilt,
we ensure the spec we want to build with that compiler is scheduled
for rebuild as well."""
# Create a temp mirror directory for buildcache usage
mirror_dir = tmpdir.join('mirror_dir')
mirror_url = 'file://{0}'.format(mirror_dir.strpath)
# Install a compiler, because we want to put it in a buildcache
install_cmd('gcc@10.1.0%gcc@4.5.0')
# Put installed compiler in the buildcache
buildcache_cmd('create', '-u', '-a', '-f', '-d', mirror_dir.strpath,
'gcc@10.1.0%gcc@4.5.0')
# Now uninstall the compiler
uninstall_cmd('-y', 'gcc@10.1.0%gcc@4.5.0')
monkeypatch.setattr(spack.concretize.Concretizer,
'check_for_compiler_existence', False)
spack.config.set('config:install_missing_compilers', True)
assert CompilerSpec('gcc@10.1.0') not in compilers.all_compiler_specs()
# Configure the mirror where we put that buildcache w/ the compiler
mirror_cmd('add', 'test-mirror', mirror_url)
install_cmd('--no-check-signature', 'a%gcc@10.1.0')
# Put spec built with installed compiler in the buildcache
buildcache_cmd('create', '-u', '-a', '-f', '-d', mirror_dir.strpath,
'a%gcc@10.1.0')
# Now uninstall the spec
uninstall_cmd('-y', 'a%gcc@10.1.0')
filename = str(tmpdir.join('spack.yaml'))
with open(filename, 'w') as f:
f.write("""\
spack:
definitions:
- bootstrap:
- gcc@10.1.0%gcc@4.5.0
specs:
- a%gcc@10.1.0
mirrors:
atestm: {0}
gitlab-ci:
bootstrap:
- name: bootstrap
compiler-agnostic: true
mappings:
- match:
- arch=test-debian6-x86_64
runner-attributes:
tags:
- donotcare
- match:
- arch=test-debian6-core2
runner-attributes:
tags:
- meh
""".format(mirror_url))
# Without this monkeypatch, pipeline generation process would think that
# nothing in the environment needs rebuilding. With the monkeypatch, the
# process sees the compiler as needing a rebuild, which should then result
# in the specs built with that compiler needing a rebuild too.
def fake_get_mirrors_for_spec(spec=None, full_hash_match=False,
mirrors_to_check=None, index_only=False):
if spec.name == 'gcc':
return []
else:
return [{
'spec': spec,
'mirror_url': mirror_url,
}]
with tmpdir.as_cwd():
env_cmd('create', 'test', './spack.yaml')
outputfile = str(tmpdir.join('.gitlab-ci.yml'))
with ev.read('test'):
monkeypatch.setattr(
ci, 'SPACK_PR_MIRRORS_ROOT_URL', r"file:///fake/mirror")
ci_cmd('generate', '--output-file', outputfile)
with open(outputfile) as of:
yaml_contents = of.read()
original_yaml_contents = syaml.load(yaml_contents)
# without the monkeypatch, everything appears up to date and no
# rebuild jobs are generated.
assert(original_yaml_contents)
assert('no-specs-to-rebuild' in original_yaml_contents)
monkeypatch.setattr(spack.binary_distribution,
'get_mirrors_for_spec',
fake_get_mirrors_for_spec)
ci_cmd('generate', '--output-file', outputfile)
with open(outputfile) as of:
yaml_contents = of.read()
new_yaml_contents = syaml.load(yaml_contents)
assert(new_yaml_contents)
# This 'needs' graph reflects that even though specs 'a' and 'b' do
# not otherwise need to be rebuilt (thanks to DAG pruning), they
# both end up in the generated pipeline because the compiler they
# depend on is bootstrapped, and *does* need to be rebuilt.
needs_graph = {
'(bootstrap) gcc': [],
'(specs) b': [
'(bootstrap) gcc',
],
'(specs) a': [
'(bootstrap) gcc',
'(specs) b',
],
}
_validate_needs_graph(new_yaml_contents, needs_graph, False)
def test_ci_subcommands_without_mirror(tmpdir, mutable_mock_env_path,
env_deactivate, mock_packages,
install_mockery):
"""Make sure we catch if there is not a mirror and report an error"""
filename = str(tmpdir.join('spack.yaml'))
with open(filename, 'w') as f:
f.write("""\
spack:
specs:
- archive-files
gitlab-ci:
mappings:
- match:
- archive-files
runner-attributes:
tags:
- donotcare
image: donotcare
""")
with tmpdir.as_cwd():
env_cmd('create', 'test', './spack.yaml')
outputfile = str(tmpdir.join('.gitlab-ci.yml'))
with ev.read('test'):
# Check the 'generate' subcommand
output = ci_cmd('generate', '--output-file', outputfile,
output=str, fail_on_error=False)
ex = 'spack ci generate requires an env containing a mirror'
assert(ex in output)
# Also check the 'rebuild-index' subcommand
output = ci_cmd('rebuild-index', output=str, fail_on_error=False)
ex = 'spack ci rebuild-index requires an env containing a mirror'
assert(ex in output)

View File

@ -231,7 +231,7 @@ def _spec(spec, preferred_mirrors=None):
def test_try_install_from_binary_cache(install_mockery, mock_packages, def test_try_install_from_binary_cache(install_mockery, mock_packages,
monkeypatch, capsys): monkeypatch, capsys):
"""Tests SystemExit path for_try_install_from_binary_cache.""" """Tests SystemExit path for_try_install_from_binary_cache."""
def _mirrors_for_spec(spec, force, full_hash_match=False): def _mirrors_for_spec(spec, full_hash_match=False):
spec = spack.spec.Spec('mpi').concretized() spec = spack.spec.Spec('mpi').concretized()
return [{ return [{
'mirror_url': 'notused', 'mirror_url': 'notused',

View File

@ -473,18 +473,22 @@ _spack_ci() {
then then
SPACK_COMPREPLY="-h --help" SPACK_COMPREPLY="-h --help"
else else
SPACK_COMPREPLY="generate rebuild" SPACK_COMPREPLY="generate rebuild rebuild-index"
fi fi
} }
_spack_ci_generate() { _spack_ci_generate() {
SPACK_COMPREPLY="-h --help --output-file --copy-to --optimize --dependencies" SPACK_COMPREPLY="-h --help --output-file --copy-to --optimize --dependencies --prune-dag --no-prune-dag --check-index-only"
} }
_spack_ci_rebuild() { _spack_ci_rebuild() {
SPACK_COMPREPLY="-h --help" SPACK_COMPREPLY="-h --help"
} }
_spack_ci_rebuild_index() {
SPACK_COMPREPLY="-h --help"
}
_spack_clean() { _spack_clean() {
if $list_options if $list_options
then then