Skip to content

Commit ef116cb

Browse files
CI: Add an environment option for build cache index update behavior (spack#51817)
* CI: Add an environment option for build cache index update behavior Signed-off-by: Ryan Krattiger <ryan.krattiger@kitware.com> * Remove unused default cleanup-job The default cleanup job was using gitlab specific variables and variables that were unexpanded by gitlab CI. Drop this as it is related to a long since removed legacy feature "artfacts-buildcache" which has been deprecated and removed for a few years. Signed-off-by: Ryan Krattiger <ryan.krattiger@kitware.com> * Add mirror metadata formatting. Display view in pruning Views are optional metadata. Provide a way to conditionally format mirror metadata to print view information if it exists. Signed-off-by: Ryan Krattiger <ryan.krattiger@kitware.com> * Add docs on new environment variable Signed-off-by: Ryan Krattiger <ryan.krattiger@kitware.com> --------- Signed-off-by: Ryan Krattiger <ryan.krattiger@kitware.com>
1 parent 0d99be3 commit ef116cb

File tree

7 files changed

+155
-34
lines changed

7 files changed

+155
-34
lines changed

lib/spack/docs/pipelines.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,3 +704,15 @@ Optional.
704704
Only needed if you want ``spack ci rebuild`` to trust the key you store in this variable, in which case, it will subsequently be used to sign and verify binary packages (when installing or creating build caches).
705705
You could also have already trusted a key Spack knows about, or if no key is present anywhere, Spack will install specs using ``--no-check-signature`` and create build caches using ``-u`` (for unsigned binaries).
706706

707+
``SPACK_CI_BUILDCACHE_VIEW``
708+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
709+
710+
Optional.
711+
Only needed when using a ``buildcache-destination`` mirror that points at a build cache view.
712+
This option affects the behavior the ``reindex`` job (:ref:`rebuild_index`) can have the values ``force`` or ``append`` which mirror behavior described by ref:`cmd-spack-buildcache-update-view`.
713+
The default option is ``append`` because that is what is used by the Spack build farm.
714+
715+
.. warning::
716+
717+
Using the ``append`` option with build cache index views is a non-atomic operation.
718+
It is up to the CI maintainer to ensure that concurrent writes to the build cache are handled appropriately.

lib/spack/spack/ci/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ def rebuild_filter(s: spack.spec.Spec) -> RebuildDecision:
255255
if not spec_locations:
256256
return RebuildDecision(True, "not found anywhere")
257257

258-
urls = ",".join(f"{loc.url}@v{loc.version}" for loc in spec_locations)
258+
urls = ",".join(f"{loc:_url@v_version? (view: _view)}" for loc in spec_locations)
259259
message = f"up-to-date [{urls}]"
260260
return RebuildDecision(False, message)
261261

lib/spack/spack/ci/common.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -570,36 +570,52 @@ def init_pipeline_jobs(self, pipeline: PipelineDag):
570570

571571
# Generate IR from the configs
572572
def generate_ir(self):
573-
"""Generate the IR from the Spack CI configurations."""
573+
"""Generate the IR from the Spack CI configurations.
574+
575+
Generate makes use of special strings that need to be expanded by python format.
576+
577+
env_dir: The concrete environment directory used in downstream jobs
578+
"""
574579

575580
jobs = self.ir["jobs"]
576581

577582
# Implicit job defaults
578583
defaults = [
579584
{
580585
"build-job": {
581-
"script": [
582-
"cd {env_dir}",
583-
"spack env activate --without-view .",
584-
"spack ci rebuild",
585-
]
586+
"script": ["spack env activate --without-view {env_dir}", "spack ci rebuild"]
586587
}
587588
},
588589
{"noop-job": {"script": ['echo "All specs already up to date, nothing to rebuild."']}},
589590
]
590591

592+
pipeline_mirrors = spack.mirrors.mirror.MirrorCollection(binary=True)
593+
buildcache_destination = pipeline_mirrors["buildcache-destination"]
594+
update_index_extra_args = []
595+
if buildcache_destination.push_view:
596+
update_index_extra_args.extend(["--name", buildcache_destination.push_view])
597+
option = os.environ.get("SPACK_CI_BUILDCACHE_VIEW", "append")
598+
if option == "append":
599+
# Running this in CI relies on a guarentee from the calling context that there is
600+
# only a single writter or the build cache view doesn't require a complete view
601+
# after each append.
602+
tty.warn("Using --append to update buildcache-destination mirror index view")
603+
update_index_extra_args.extend(["-y", "--append"])
604+
elif option == "force":
605+
update_index_extra_args.append("--force")
606+
else:
607+
raise SpackCIError(f"Unrecognized value: SPACK_CI_BUILDCACHE_VIEW={option}")
608+
591609
# Job overrides
592610
overrides = [
593611
# Reindex script
594612
{
595613
"reindex-job": {
596-
"script:": ["spack buildcache update-index --keys {index_target_mirror}"]
597-
}
598-
},
599-
# Cleanup script
600-
{
601-
"cleanup-job": {
602-
"script:": ["spack -d mirror destroy {mirror_prefix}/$CI_PIPELINE_ID"]
614+
"script:": [
615+
"spack env activate --without-view {env_dir}",
616+
"spack buildcache update-index --keys "
617+
+ f"{' '.join(update_index_extra_args)} buildcache-destination",
618+
]
603619
}
604620
},
605621
# Add signing job tags

lib/spack/spack/ci/gitlab.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -378,21 +378,35 @@ def main_script_replacements(cmd):
378378
output_object["sign-pkgs"] = signing_job
379379

380380
if options.rebuild_index:
381+
# Create a dummy job that runs as the stage before reindex.
382+
# This job will be used to ensure reindex doesn't run until
383+
# the other build jobs complete.
384+
stage_names.append("stage-wait")
385+
wait_job = spack_ci_ir["jobs"]["noop"]["attributes"]
386+
wait_job["stage"] = "stage-wait"
387+
wait_job["retry"] = 0
388+
wait_job["when"] = "always"
389+
wait_job["script"] = ["echo 'Open the pod bay doors HAL'"]
390+
wait_job["dependencies"] = []
391+
392+
output_object["wait-for-build-jobs"] = wait_job
393+
381394
# Add a final job to regenerate the index
382395
stage_names.append("stage-rebuild-index")
383396
final_job = spack_ci_ir["jobs"]["reindex"]["attributes"]
384397

385398
final_job["stage"] = "stage-rebuild-index"
386-
target_mirror = options.buildcache_destination.push_url
387-
final_job["script"] = unpack_script(
388-
final_job["script"],
389-
op=lambda cmd: cmd.replace("{index_target_mirror}", target_mirror),
390-
)
399+
final_job["script"] = unpack_script(final_job["script"], op=main_script_replacements)
391400

392401
final_job["when"] = "always"
393402
final_job["retry"] = service_job_retries
394403
final_job["interruptible"] = True
395-
final_job["dependencies"] = []
404+
# update-index needs to download generate artifacts
405+
# it also needs to wait until all of the other stages complete.
406+
final_job["needs"] = [
407+
{"job": generate_job_name, "pipeline": f"{generate_pipeline_id}"},
408+
"wait-for-build-jobs",
409+
]
396410

397411
output_object["rebuild-index"] = final_job
398412

lib/spack/spack/test/binary_distribution.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1449,6 +1449,54 @@ def test_mirror_metadata():
14491449
spack.binary_distribution.MirrorMetadata.from_string("https://dummy.io/__v3@@4")
14501450

14511451

1452+
def mirror_metadata_check_format(data, fmt, result):
1453+
assert fmt.format(data) == result.format(data)
1454+
1455+
1456+
def test_mirror_metadata_format():
1457+
mirror_metadata = spack.binary_distribution.MirrorMetadata("https://dummy.io/__v3", 3)
1458+
1459+
# Check pass-through formatting
1460+
mirror_metadata_check_format(mirror_metadata, "{0:_url}", "{0.url}")
1461+
mirror_metadata_check_format(mirror_metadata, "{0:_version}", "{0.version}")
1462+
mirror_metadata_check_format(mirror_metadata, "{0:_view}", "{0.view}")
1463+
1464+
# Empty view
1465+
mirror_metadata_check_format(mirror_metadata, "{0:?_view}", "")
1466+
mirror_metadata_check_format(
1467+
mirror_metadata, "{0:_url?^_view^_version?^_version}", "{0.url}^{0.version}"
1468+
)
1469+
mirror_metadata_check_format(
1470+
mirror_metadata,
1471+
"{0:_url?^_view^_version?^_version?^_view^_version?^_url}",
1472+
"{0.url}^{0.version}^{0.url}",
1473+
)
1474+
1475+
1476+
def test_mirror_metadata_format_with_view():
1477+
mirror_metadata = spack.binary_distribution.MirrorMetadata(
1478+
"https://dummy.io/__v3__@aview", 3, "aview"
1479+
)
1480+
1481+
# Check pass-through formatting
1482+
mirror_metadata_check_format(mirror_metadata, "{0:_url}", "{0.url}")
1483+
mirror_metadata_check_format(mirror_metadata, "{0:_version}", "{0.version}")
1484+
mirror_metadata_check_format(mirror_metadata, "{0:_view}", "{0.view}")
1485+
1486+
# View exists
1487+
mirror_metadata_check_format(mirror_metadata, "{0:?_view}", "{0.view}")
1488+
mirror_metadata_check_format(
1489+
mirror_metadata,
1490+
"{0:_url?^_view^_version?^_version}",
1491+
"{0.url}^{0.view}^{0.version}^{0.version}",
1492+
)
1493+
mirror_metadata_check_format(
1494+
mirror_metadata,
1495+
"{0:_url?^_view^_version?^_version?^_view^_version?^_url}",
1496+
"{0.url}^{0.view}^{0.version}^{0.version}^{0.view}^{0.version}^{0.url}",
1497+
)
1498+
1499+
14521500
def test_mirror_metadata_with_view():
14531501
mirror_metadata = spack.binary_distribution.MirrorMetadata(
14541502
"https://dummy.io/__v3__@aview", 3, "aview"

lib/spack/spack/test/cmd/ci.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,15 @@ def test_ci_generate_with_env(ci_generate_test, tmp_path: pathlib.Path, mock_bin
193193
assert yaml_contents["workflow"]["rules"] == [{"when": "always"}]
194194

195195
assert "stages" in yaml_contents
196-
assert len(yaml_contents["stages"]) == 6
196+
assert len(yaml_contents["stages"]) == 7
197197
assert yaml_contents["stages"][0] == "stage-0"
198-
assert yaml_contents["stages"][5] == "stage-rebuild-index"
198+
assert yaml_contents["stages"][5] == "stage-wait"
199+
assert yaml_contents["stages"][6] == "stage-rebuild-index"
199200

200201
assert "rebuild-index" in yaml_contents
201202
rebuild_job = yaml_contents["rebuild-index"]
202203
assert (
203-
rebuild_job["script"][0] == f"spack buildcache update-index --keys {mirror_url.as_uri()}"
204+
rebuild_job["script"][1] == "spack buildcache update-index --keys buildcache-destination"
204205
)
205206
assert rebuild_job["custom_attribute"] == "custom!"
206207

@@ -332,12 +333,11 @@ def test_ci_generate_with_custom_settings(
332333
"git checkout ${SPACK_REF}",
333334
"popd",
334335
]
335-
assert ci_obj["script"][1].startswith("cd ")
336-
ci_obj["script"][1] = "cd ENV"
336+
assert ci_obj["script"][1].startswith("spack env activate --without-view ")
337+
ci_obj["script"][1] = "spack env activate --without-view ENV"
337338
assert ci_obj["script"] == [
338339
"spack -d ci rebuild",
339-
"cd ENV",
340-
"spack env activate --without-view .",
340+
"spack env activate --without-view ENV",
341341
"spack ci rebuild",
342342
]
343343
assert ci_obj["after_script"] == ["rm -rf /some/path/spack"]
@@ -1674,7 +1674,8 @@ def test_ci_generate_mirror_config(
16741674
with open(tmp_path / ".gitlab-ci.yml", encoding="utf-8") as f:
16751675
pipeline_doc = syaml.load(f)
16761676
assert fst not in pipeline_doc["rebuild-index"]["script"][0]
1677-
assert snd in pipeline_doc["rebuild-index"]["script"][0]
1677+
assert "env activate" in pipeline_doc["rebuild-index"]["script"][0]
1678+
assert "buildcache-destination" in pipeline_doc["rebuild-index"]["script"][1]
16781679

16791680

16801681
def dynamic_mapping_setup(tmp_path: pathlib.Path):
@@ -1855,11 +1856,13 @@ def test_ci_generate_copy_only(
18551856
# Make sure there are only two jobs and two stages
18561857
stages = pipeline_doc["stages"]
18571858
copy_stage = "copy"
1859+
wait_stage = "stage-wait"
18581860
rebuild_index_stage = "stage-rebuild-index"
18591861

1860-
assert len(stages) == 2
1862+
assert len(stages) == 3
18611863
assert stages[0] == copy_stage
1862-
assert stages[1] == rebuild_index_stage
1864+
assert stages[1] == wait_stage
1865+
assert stages[2] == rebuild_index_stage
18631866

18641867
rebuild_index_job = pipeline_doc["rebuild-index"]
18651868
assert rebuild_index_job["stage"] == rebuild_index_stage

lib/spack/spack/url_buildcache.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,10 +1365,7 @@ def __init__(self, url: str, version: int, view: Optional[str] = None):
13651365
self.view = view
13661366

13671367
def __str__(self):
1368-
s = f"{self.url}__v{self.version}"
1369-
if self.view:
1370-
s += f"__{self.view}"
1371-
return s
1368+
return f"{self:_url__v_version?___view}"
13721369

13731370
def __eq__(self, other):
13741371
if not isinstance(other, MirrorMetadata):
@@ -1378,6 +1375,37 @@ def __eq__(self, other):
13781375
def __hash__(self):
13791376
return hash((self.url, self.version, self.view))
13801377

1378+
def __format__(self, format_spec):
1379+
"""Format the mirror metadata
1380+
1381+
Format Spec:
1382+
_url: metadata.url
1383+
_version: metadata.version
1384+
_view: metadata.view
1385+
?: delimiter to wrap conditional printing based on optional view
1386+
1387+
Example
1388+
1389+
f"{meta_data:_url?^_view?@v_version}"
1390+
1391+
Expansion without a view:
1392+
https://my-mirror.com/prefix@v3
1393+
1394+
Expansion with a view:
1395+
https://my-mirror.com/prefix^my-view@v3
1396+
"""
1397+
if not format_spec:
1398+
format_spec = "_url@v3?-_view"
1399+
return
1400+
out = format_spec.replace("_url", self.url)
1401+
out = out.replace("_version", str(self.version))
1402+
out = out.replace("_view", str(self.view))
1403+
parts = out.split("?")
1404+
if self.view:
1405+
return "".join(parts)
1406+
else:
1407+
return "".join(parts[0::2])
1408+
13811409
@classmethod
13821410
def from_string(cls, s: str):
13831411
m = re.match(r"^(.*)__v([0-9]+)(?:__(.*))?$", s)

0 commit comments

Comments
 (0)