Skip to content

Commit 41b6310

Browse files
authored
Add buildcache creation to distribution cmd (#637)
1 parent 4d99470 commit 41b6310

File tree

2 files changed

+270
-13
lines changed

2 files changed

+270
-13
lines changed

manager/manager_cmds/distribution.py

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
import spack.config
77
import spack.environment
88
import spack.extensions
9+
import spack.llnl.util.filesystem as fs
910
import spack.llnl.util.tty as tty
11+
import spack.spec
1012
import spack.util.path
1113
import spack.util.spack_yaml
1214
from spack.cmd.mirror import create_mirror_for_all_specs, filter_externals
13-
from spack.llnl.util.filesystem import working_dir
1415
from spack.main import SpackCommand
1516
from spack.paths import spack_root
1617

@@ -49,6 +50,23 @@ def add_command(parser, command_dict):
4950
"into the root of the packaged distribution"
5051
),
5152
)
53+
group = subparser.add_mutually_exclusive_group()
54+
group.add_argument(
55+
"--source-only",
56+
action="store_true",
57+
help=(
58+
"Only create a source mirror for packages in the environment. "
59+
"Default is to create a source and binary mirror"
60+
),
61+
)
62+
group.add_argument(
63+
"--binary-only",
64+
action="store_true",
65+
help=(
66+
"Only create a binary mirror for packages in the environment. "
67+
"Default is to create a source and binary mirror"
68+
),
69+
)
5270
command_dict["distribution"] = distribution
5371

5472

@@ -135,7 +153,8 @@ def __init__(self, env, root, includes=None, excludes=None, extra_data=None):
135153
self.path = root
136154
self.package_repos = os.path.join(self.path, "spack_repo")
137155
self.extensions = os.path.join(self.path, "extensions")
138-
self.mirror = os.path.join(self.path, "mirror")
156+
self.source_mirror = os.path.join(self.path, "source-mirror")
157+
self.binary_mirror = os.path.join(self.path, "binary-mirror")
139158
self.bootstrap_mirror = os.path.join(self.path, "bootstrap-mirror")
140159
self.spack_dir = os.path.join(self.path, "spack")
141160

@@ -283,28 +302,46 @@ def configure_source_mirror(self):
283302
# However, this causes issues for packages that are not downloadable,
284303
# so we do a first-shot mirror creation with the original environment active.
285304
with self.environment_to_package:
286-
tty.msg(f"Creating mirror at {self.mirror}....")
305+
tty.msg(f"Creating source mirror at {self.source_mirror}....")
287306
create_mirror_for_all_specs(
288307
mirror_specs=filter_externals(self.environment_to_package.all_specs()),
289-
path=self.mirror,
308+
path=self.source_mirror,
290309
skip_unstable_versions=False,
291310
)
292311

293312
with self.env:
294-
tty.msg(f"Updating mirror at {self.mirror}....")
313+
tty.msg(f"Updating mirror at {self.source_mirror}....")
295314
create_mirror_for_all_specs(
296315
mirror_specs=filter_externals(self.env.all_specs()),
297-
path=self.mirror,
316+
path=self.source_mirror,
298317
skip_unstable_versions=False,
299318
)
300319
mirror_path = os.path.join(
301-
os.path.relpath(self.path, self.env.path), os.path.basename(self.mirror)
320+
os.path.relpath(self.path, self.env.path), os.path.basename(self.source_mirror)
302321
)
303322

304323
mirrorer = SpackCommand("mirror")
305324
tty.msg(f"Adding mirror to env: {self.env.name}....")
306325
with self.env.write_transaction():
307-
mirrorer("add", "internal", mirror_path)
326+
mirrorer("add", "internal-source", mirror_path)
327+
328+
def configure_binary_mirror(self):
329+
cacher = SpackCommand("buildcache")
330+
with self.environment_to_package:
331+
tty.msg(f"Creating binary mirror at {self.binary_mirror}....")
332+
cacher("push", "--unsigned", self.binary_mirror)
333+
cacher("keys", "--install", "--trust")
334+
335+
mirrorer = SpackCommand("mirror")
336+
mirror_path = os.path.join(
337+
os.path.relpath(self.path, self.env.path), os.path.basename(self.binary_mirror)
338+
)
339+
mirror_name = "internal-binary"
340+
341+
with self.env:
342+
tty.msg(f"Adding mirror to env: {self.env.name}....")
343+
with self.env.write_transaction():
344+
mirrorer("add", "--type", "binary", "--unsigned", mirror_name, mirror_path)
308345

309346
def configure_bootstrap_mirror(self):
310347
tty.msg(f"Creating bootstrap mirror at {self.bootstrap_mirror}....")
@@ -317,7 +354,7 @@ def configure_bootstrap_mirror(self):
317354
os.path.relpath(self.bootstrap_mirror, self.env.path), "metadata", "binaries"
318355
)
319356
with self.env:
320-
with working_dir(self.env.path):
357+
with fs.working_dir(self.env.path):
321358
with self.env.write_transaction():
322359
strapper(
323360
"add",
@@ -355,15 +392,40 @@ def _write(self, data):
355392
spack.util.spack_yaml.dump(data, outf, default_flow_style=False)
356393

357394

395+
def is_installed(spec):
396+
status = True
397+
for dependency in spec.dependencies(deptype=("link", "run")):
398+
status = is_installed(dependency)
399+
bad_statuses = [spack.spec.InstallStatus.absent, spack.spec.InstallStatus.missing]
400+
return status and spec.install_status() not in bad_statuses
401+
402+
403+
def correct_mirror_args(env, args):
404+
specs_to_check = list(env.concretized_specs())
405+
install_status = [len(specs_to_check)] + [is_installed(x) for _, x in specs_to_check]
406+
has_installed_specs = all(install_status)
407+
if not args.source_only and not has_installed_specs:
408+
tty.warn("Environment contains uninstalled specs, defaulting to source-only package")
409+
if args.binary_only:
410+
tty.die(
411+
"Binary distribution requested, but the environment does not "
412+
"include the necessary installed binary packages"
413+
)
414+
args.source_only = True
415+
416+
358417
def distribution(parser, args):
359418
env = spack.cmd.require_active_env(cmd_name="manager distribution")
419+
correct_mirror_args(env, args)
420+
360421
packager = DistributionPackager(
361422
env,
362423
args.distro_dir,
363424
includes=args.include,
364425
excludes=args.exclude,
365426
extra_data=args.extra_data,
366427
)
428+
367429
with packager:
368430
packager.configure_specs()
369431
packager.configure_includes()
@@ -372,7 +434,10 @@ def distribution(parser, args):
372434
packager.configure_package_settings(filter_externals=args.filter_externals)
373435
packager.filter_excludes()
374436
packager.concretize()
375-
packager.configure_source_mirror()
437+
if not args.binary_only:
438+
packager.configure_source_mirror()
439+
if not args.source_only:
440+
packager.configure_binary_mirror()
376441
packager.configure_bootstrap_mirror()
377442
packager.bundle_spack()
378443
packager.bundle_extra_data()

tests/test_distribution.py

Lines changed: 195 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@
99
import os
1010
from argparse import ArgumentParser
1111

12+
import manager.manager_cmds.distribution as distribution
13+
import pytest
14+
1215
import spack
1316
import spack.cmd.bootstrap as test_bootstrap_parse
17+
import spack.cmd.buildcache as test_buildcache_parse
18+
import spack.cmd.mirror as test_mirror_parse
1419
import spack.environment
15-
import spack.extensions.manager.manager_cmds.distribution as distribution
20+
import spack.spec
1621
import spack.util.spack_yaml
1722

1823

@@ -202,6 +207,152 @@ def test_get_valid_env_scopes(tmpdir):
202207
assert len(scope_names) == 2
203208

204209

210+
class MockArgs:
211+
def __init__(self, source=False, binary=False):
212+
self.source_only = source
213+
self.binary_only = binary
214+
215+
216+
class MockSpec:
217+
def __init__(self, name, status, dependencies=None):
218+
self.name = name
219+
self.status = status
220+
self._dependencies = dependencies or []
221+
222+
def install_status(self):
223+
return self.status
224+
225+
def dependencies(self, *args, **kwargs):
226+
return self._dependencies
227+
228+
229+
class MockEnv:
230+
def __init__(self, specs):
231+
self.specs = [(x, f"{x}.concrete") for x in specs]
232+
233+
def concretized_specs(self):
234+
return self.specs
235+
236+
237+
def test_is_installed_true():
238+
"""
239+
This test verifies that `is_installed` returns False if a package reports its
240+
status as something other than missing or absent.
241+
"""
242+
spec = MockSpec("hdf5", "fake_valid_status")
243+
result = distribution.is_installed(spec)
244+
assert result
245+
246+
247+
def test_is_installed_missing():
248+
"""
249+
This test verifies that `is_installed` returns False if a package reports
250+
its status as missing.
251+
"""
252+
spec = MockSpec("hdf5", spack.spec.InstallStatus.missing)
253+
result = distribution.is_installed(spec)
254+
assert not result
255+
256+
257+
def test_is_installed_absent():
258+
"""
259+
This test verifies that `is_installed` returns False if a package reports
260+
its status as absent.
261+
"""
262+
spec = MockSpec("hdf5", spack.spec.InstallStatus.absent)
263+
result = distribution.is_installed(spec)
264+
assert not result
265+
266+
267+
def test_is_installed_missing_dependency():
268+
"""
269+
This test verifies that `is_installed` returns False if a package is installed but its
270+
depenency is missing.
271+
"""
272+
deps = [
273+
MockSpec("a", spack.spec.InstallStatus.installed),
274+
MockSpec("b", spack.spec.InstallStatus.missing),
275+
]
276+
spec = MockSpec("hdf5", spack.spec.InstallStatus.installed, dependencies=deps)
277+
result = distribution.is_installed(spec)
278+
assert not result
279+
280+
281+
def test_correct_mirror_args_does_not_modify_args_when_source_only(monkeypatch):
282+
"""
283+
This test verifies that `correct_mirror_args` does nothing if source_only was requested.
284+
"""
285+
args = MockArgs(source=True)
286+
env = MockEnv(["hdf5"])
287+
288+
assert args.source_only
289+
assert not args.binary_only
290+
monkeypatch.setattr(distribution, "is_installed", lambda x: False)
291+
distribution.correct_mirror_args(env, args)
292+
assert args.source_only
293+
assert not args.binary_only
294+
295+
296+
def test_correct_mirror_args_sets_source_only_if_no_concretized_specs_in_env():
297+
"""
298+
This test verifies that `correct_mirror_args` reverts to source_only if binaries aren't in env.
299+
"""
300+
args = MockArgs()
301+
env = MockEnv([])
302+
assert not args.source_only
303+
assert not args.binary_only
304+
distribution.correct_mirror_args(env, args)
305+
assert args.source_only
306+
assert not args.binary_only
307+
308+
309+
def test_correct_mirror_args_sets_source_only_if_install_not_verified(monkeypatch):
310+
"""
311+
This test verifies that `correct_mirror_args` reverts to source_only if binaries aren't
312+
verified as correct.
313+
"""
314+
args = MockArgs()
315+
env = MockEnv(["hdf5.error"])
316+
317+
assert not args.source_only
318+
assert not args.binary_only
319+
monkeypatch.setattr(distribution, "is_installed", lambda x: False)
320+
distribution.correct_mirror_args(env, args)
321+
assert args.source_only
322+
assert not args.binary_only
323+
324+
325+
def test_correct_mirror_args_does_no_modification_if_install_verified(monkeypatch):
326+
"""
327+
This test verifies that `correct_mirror_args` does nothing if binaries exist.
328+
"""
329+
args = MockArgs()
330+
env = MockEnv(["hdf5.valid"])
331+
332+
assert not args.source_only
333+
assert not args.binary_only
334+
monkeypatch.setattr(distribution, "is_installed", lambda x: True)
335+
distribution.correct_mirror_args(env, args)
336+
assert not args.source_only
337+
assert not args.binary_only
338+
339+
340+
def test_correct_mirror_args_does_errors_if_binary_only_but_no_binaries_exist(monkeypatch):
341+
"""
342+
This test verifies that `correct_mirror_args` errors out if binary_only
343+
is True and source_only gets defaulted to True.
344+
"""
345+
args = MockArgs(binary=True)
346+
env = MockEnv(["hdf5.error"])
347+
348+
assert not args.source_only
349+
assert args.binary_only
350+
351+
monkeypatch.setattr(distribution, "is_installed", lambda x: False)
352+
with pytest.raises(SystemExit):
353+
distribution.correct_mirror_args(env, args)
354+
355+
205356
def test_DistributionPackager_init_distro_dir(tmpdir):
206357
"""
207358
This test verifies that `init_distro_dir` correctly creates a directory
@@ -578,7 +729,7 @@ def mirror_for_specs(*args, **kwargs):
578729
pkgr.configure_source_mirror()
579730

580731
content = get_manifest(pkgr.env)
581-
expected = {"internal": "../mirror"}
732+
expected = {"internal-source": "../source-mirror"}
582733
assert "mirrors" in content["spack"]
583734
assert content["spack"]["mirrors"] == expected
584735

@@ -631,7 +782,7 @@ def mirror_for_specs(*args, **kwargs):
631782

632783
content = get_manifest(pkgr.env)
633784
assert "mirrors" in content["spack"]
634-
assert content["spack"]["mirrors"] == {"internal": "../mirror"}
785+
assert content["spack"]["mirrors"] == {"internal-source": "../source-mirror"}
635786

636787

637788
def test_DistributionPackager_configure_bootstrap_mirror(tmpdir, monkeypatch):
@@ -670,3 +821,44 @@ def __call__(self, *args, **kwargs):
670821
with pkgr.env:
671822
for call in MockCommand.call_args:
672823
parser.parse_args(call)
824+
825+
826+
def test_DistributionPackager_configure_binary_mirror(tmpdir, monkeypatch):
827+
"""
828+
This test verifies that `configure_binary_mirror` does not construct a call to
829+
`spack buildcache` or `spack mirror` that violates its API.
830+
"""
831+
manifest = os.path.join(tmpdir.strpath, "base-env", "spack.yaml")
832+
create_spack_manifest(manifest)
833+
env = spack.environment.Environment(os.path.dirname(manifest))
834+
root = os.path.join(tmpdir.strpath, "root")
835+
pkgr = distribution.DistributionPackager(env, root)
836+
837+
class MockCommand:
838+
args = []
839+
kwargs = {}
840+
call_args = []
841+
842+
def __init__(self, *args, **kwargs):
843+
self.args += list(args)
844+
self.kwargs.update(kwargs)
845+
846+
def __call__(self, *args, **kwargs):
847+
self.call_args.append(list(args))
848+
self.kwargs.update(kwargs)
849+
850+
monkeypatch.setattr(distribution, "SpackCommand", MockCommand)
851+
pkgr.configure_binary_mirror()
852+
853+
assert MockCommand.args == ["buildcache", "mirror"]
854+
assert MockCommand.kwargs == {}
855+
assert len(MockCommand.call_args) == 3
856+
857+
buildcache_parser = ArgumentParser()
858+
test_buildcache_parse.setup_parser(buildcache_parser)
859+
mirror_parser = ArgumentParser()
860+
test_mirror_parse.setup_parser(mirror_parser)
861+
with pkgr.env:
862+
buildcache_parser.parse_args(MockCommand.call_args[0])
863+
buildcache_parser.parse_args(MockCommand.call_args[1])
864+
mirror_parser.parse_args(MockCommand.call_args[2])

0 commit comments

Comments
 (0)