spack/lib/spack/spack/solver/concretize.lp
Massimiliano Culpo 8fc1ccc686
solver: encode % as "build requirement", not as "dependency" (#50011)
This PR fixes the issues with `%` and reused specs, due to https://github.com/spack/spack/issues/49847#issuecomment-2774640234 

It does so by adding another layer of indirection, so that whenever a spec
`foo %bar` is encountered, the `%bar` part is encoded as an
`attr("build_requirement", ...)`.

Then:

1. If `foo` is a node to be built, then the build requirement implies a dependency
2. Otherwise it implies looking e.g. reused specs metadata, and ensure it matches

---------

Signed-off-by: Massimiliano Culpo <massimiliano.culpo@gmail.com>
2025-04-11 11:09:21 -07:00

1910 lines
87 KiB
Prolog

% Copyright Spack Project Developers. See COPYRIGHT file for details.
%
% SPDX-License-Identifier: (Apache-2.0 OR MIT)
%=============================================================================
% This logic program implements Spack's concretizer
%=============================================================================
% ID of the nodes in the "root" link-run sub-DAG
#const min_dupe_id = 0.
#const direct_link_run = 0.
#const direct_build = 1.
% Allow clingo to create nodes
{ attr("node", node(0..X-1, Package)) } :- max_dupes(Package, X), not virtual(Package).
{ attr("virtual_node", node(0..X-1, Package)) } :- max_dupes(Package, X), virtual(Package).
% Integrity constraints on DAG nodes
:- attr("root", PackageNode),
not attr("node", PackageNode),
internal_error("Every root must be a node").
:- attr("version", PackageNode, _),
not attr("node", PackageNode),
not attr("virtual_node", PackageNode),
internal_error("Only nodes and virtual_nodes can have versions").
:- attr("node_version_satisfies", PackageNode, _),
not attr("node", PackageNode),
not attr("virtual_node", PackageNode),
internal_error("Only nodes and virtual_nodes can have version satisfaction").
:- attr("hash", PackageNode, _),
not attr("node", PackageNode),
internal_error("Only nodes can have hashes").
:- attr("node_platform", PackageNode, _),
not attr("node", PackageNode),
internal_error("Only nodes can have platforms").
:- attr("node_os", PackageNode, _), not attr("node", PackageNode),
internal_error("Only nodes can have node_os").
:- attr("node_target", PackageNode, _), not attr("node", PackageNode),
internal_error("Only nodes can have node_target").
:- attr("variant_value", PackageNode, _, _), not attr("node", PackageNode),
internal_error("variant_value true for a non-node").
:- attr("node_flag", PackageNode, _), not attr("node", PackageNode),
internal_error("node_flag assigned for non-node").
:- attr("external_spec_selected", PackageNode, _), not attr("node", PackageNode),
internal_error("external_spec_selected for non-node").
:- attr("depends_on", ParentNode, _, _), not attr("node", ParentNode),
internal_error("non-node depends on something").
:- attr("depends_on", _, ChildNode, _), not attr("node", ChildNode),
internal_error("something depends_on a non-node").
:- attr("virtual_node", VirtualNode), not provider(_, VirtualNode),
internal_error("virtual node with no provider").
:- provider(_, VirtualNode), not attr("virtual_node", VirtualNode),
internal_error("provider with no virtual node").
:- provider(PackageNode, _), not attr("node", PackageNode),
internal_error("provider with no real node").
:- node_has_variant(PackageNode, _, _), not attr("node", PackageNode),
internal_error("node has variant for a non-node").
:- attr("variant_set", PackageNode, _, _), not attr("node", PackageNode),
internal_error("variant_set for a non-node").
:- variant_is_propagated(PackageNode, _), not attr("node", PackageNode),
internal_error("variant_is_propagated for a non-node").
:- attr("root", node(ID, PackageNode)), ID > min_dupe_id,
internal_error("root with a non-minimal duplicate ID").
% Nodes in the "root" unification set cannot depend on non-root nodes if the dependency is "link" or "run"
:- attr("depends_on", node(min_dupe_id, Package), node(ID, _), "link"), ID != min_dupe_id, unification_set("root", node(min_dupe_id, Package)), internal_error("link dependency out of the root unification set").
:- attr("depends_on", node(min_dupe_id, Package), node(ID, _), "run"), ID != min_dupe_id, unification_set("root", node(min_dupe_id, Package)), internal_error("run dependency out of the root unification set").
% Namespaces are statically assigned by a package fact if not otherwise set
error(100, "{0} does not have a namespace", Package) :- attr("node", node(ID, Package)),
not attr("namespace", node(ID, Package), _),
internal_error("A node must have a namespace").
error(100, "{0} cannot come from both {1} and {2} namespaces", Package, NS1, NS2) :- attr("node", node(ID, Package)),
attr("namespace", node(ID, Package), NS1),
attr("namespace", node(ID, Package), NS2),
NS1 != NS2,
internal_error("A node cannot have two namespaces").
attr("namespace", node(ID, Package), Namespace) :- attr("namespace_set", node(ID, Package), Namespace).
attr("namespace", node(ID, Package), Namespace)
:- attr("node", node(ID, Package)),
not attr("namespace_set", node(ID, Package), _),
pkg_fact(Package, namespace(Namespace)).
% Rules on "unification sets", i.e. on sets of nodes allowing a single configuration of any given package
unify(SetID, PackageName) :- unification_set(SetID, node(_, PackageName)).
:- 2 { unification_set(SetID, node(_, PackageName)) }, unify(SetID, PackageName),
internal_error("Cannot have multiple unification sets IDs for one set").
unification_set("root", PackageNode) :- attr("root", PackageNode).
unification_set(SetID, ChildNode) :- attr("depends_on", ParentNode, ChildNode, Type), Type != "build", unification_set(SetID, ParentNode).
unification_set(("build", node(X, Child)), node(X, Child))
:- attr("depends_on", ParentNode, node(X, Child), Type),
Type == "build",
multiple_unification_sets(Child),
unification_set(SetID, ParentNode).
unification_set("generic_build", node(X, Child))
:- attr("depends_on", ParentNode, node(X, Child), Type),
Type == "build",
not multiple_unification_sets(Child),
unification_set(_, ParentNode).
unification_set(SetID, VirtualNode)
:- provider(PackageNode, VirtualNode),
unification_set(SetID, PackageNode).
% Do not allow split dependencies, for now. This ensures that we don't construct graphs where e.g.
% a python extension depends on setuptools@63.4 as a run dependency, but uses e.g. setuptools@68
% as a build dependency.
%
% We'll need to relax the rule before we get to actual cross-compilation
:- depends_on(ParentNode, node(X, Dependency)), depends_on(ParentNode, node(Y, Dependency)), X < Y,
internal_error("Cannot split link/build deptypes for a single edge (yet)").
#defined multiple_unification_sets/1.
#defined runtime/1.
%----
% Rules to break symmetry and speed-up searches
%----
% In the "root" unification set only ID = 0 are allowed
:- unification_set("root", node(ID, _)), ID != 0, internal_error("root unification set has node with non-zero unification set ID").
% In the "root" unification set we allow only packages from the link-run possible subDAG
:- unification_set("root", node(_, Package)), not possible_in_link_run(Package), not virtual(Package), internal_error("package outside possible link/run graph in root unification set").
% Each node must belong to at least one unification set
:- attr("node", PackageNode), not unification_set(_, PackageNode), internal_error("node belongs to no unification set").
% Cannot have a node with an ID, if lower ID of the same package are not used
:- attr("node", node(ID1, Package)),
not attr("node", node(ID2, Package)),
max_dupes(Package, X), ID1=0..X-1, ID2=0..X-1, ID2 < ID1,
internal_error("node skipped id number").
:- attr("virtual_node", node(ID1, Package)),
not attr("virtual_node", node(ID2, Package)),
max_dupes(Package, X), ID1=0..X-1, ID2=0..X-1, ID2 < ID1,
internal_error("virtual node skipped id number").
% Prefer to assign lower ID to virtuals associated with a lower penalty provider
:- not unification_set("root", node(X, Virtual)),
not unification_set("root", node(Y, Virtual)),
X < Y,
provider_weight(_, node(X, Virtual), WeightX),
provider_weight(_, node(Y, Virtual), WeightY),
WeightY < WeightX.
%-----------------------------------------------------------------------------
% Map literal input specs to facts that drive the solve
%-----------------------------------------------------------------------------
% TODO: literals, at the moment, can only influence the "root" unification set. This needs to be extended later.
% Node attributes that have multiple node arguments (usually, only the first argument is a node)
multiple_nodes_attribute("depends_on").
multiple_nodes_attribute("virtual_on_edge").
multiple_nodes_attribute("provider_set").
trigger_condition_holds(TriggerID, node(min_dupe_id, Package)) :-
solve_literal(TriggerID),
pkg_fact(Package, condition_trigger(_, TriggerID)),
literal(TriggerID).
trigger_node(TriggerID, Node, Node) :-
trigger_condition_holds(TriggerID, Node),
literal(TriggerID).
% Since we trigger the existence of literal nodes from a condition, we need to construct the condition_set/2
mentioned_in_literal(Root, Mentioned) :- mentioned_in_literal(TriggerID, Root, Mentioned), solve_literal(TriggerID).
literal_node(Root, node(min_dupe_id, Root)) :- mentioned_in_literal(Root, Root).
1 { literal_node(Root, node(0..Y-1, Mentioned)) : max_dupes(Mentioned, Y) } 1 :-
mentioned_in_literal(Root, Mentioned), Mentioned != Root,
internal_error("must have exactly one condition_set for literals").
1 { build_dependency_of_literal_node(LiteralNode, node(0..Y-1, BuildDependency)) : max_dupes(BuildDependency, Y) } 1 :-
literal_node(Root, LiteralNode),
build(LiteralNode),
attr("build_requirement", LiteralNode, build_requirement("node", BuildDependency)).
condition_set(node(min_dupe_id, Root), LiteralNode) :- literal_node(Root, LiteralNode).
condition_set(LiteralNode, BuildNode) :- build_dependency_of_literal_node(LiteralNode, BuildNode).
% Discriminate between "roots" that have been explicitly requested, and roots that are deduced from "virtual roots"
explicitly_requested_root(node(min_dupe_id, Package)) :-
solve_literal(TriggerID),
trigger_and_effect(Package, TriggerID, EffectID),
imposed_constraint(EffectID, "root", Package).
% Keep track of which nodes are associated with which root DAG
associated_with_root(RootNode, RootNode) :- attr("root", RootNode).
associated_with_root(RootNode, ChildNode) :-
depends_on(ParentNode, ChildNode),
associated_with_root(RootNode, ParentNode).
% We cannot have a node in the root condition set, that is not associated with that root
:- attr("root", RootNode),
condition_set(RootNode, node(X, Package)),
not virtual(Package),
not associated_with_root(RootNode, node(X, Package)),
internal_error("nodes in root condition set must be associated with root").
#defined concretize_everything/0.
#defined literal/1.
% Attributes for node packages which must have a single value
attr_single_value("version").
attr_single_value("node_platform").
attr_single_value("node_os").
attr_single_value("node_target").
% Error when no attribute is selected
error(100, no_value_error, Attribute, Package)
:- attr("node", node(ID, Package)),
attr_single_value(Attribute),
not attr(Attribute, node(ID, Package), _).
% Error when multiple attr need to be selected
error(100, multiple_values_error, Attribute, Package)
:- attr("node", node(ID, Package)),
attr_single_value(Attribute),
2 { attr(Attribute, node(ID, Package), Value) }.
%-----------------------------------------------------------------------------
% Version semantics
%-----------------------------------------------------------------------------
% Versions are declared with a weight and an origin, which indicates where the
% version was declared (e.g. "package_py" or "external").
pkg_fact(Package, version_declared(Version, Weight)) :- pkg_fact(Package, version_declared(Version, Weight, _)).
% We can't emit the same version **with the same weight** from two different sources
:- pkg_fact(Package, version_declared(Version, Weight, Origin1)),
pkg_fact(Package, version_declared(Version, Weight, Origin2)),
Origin1 < Origin2,
internal_error("Two versions with identical weights").
% We cannot use a version declared for an installed package if we end up building it
:- pkg_fact(Package, version_declared(Version, Weight, "installed")),
attr("version", node(ID, Package), Version),
version_weight(node(ID, Package), Weight),
not attr("hash", node(ID, Package), _),
internal_error("Reuse version weight used for built package").
% versions are declared w/priority -- declared with priority implies declared
pkg_fact(Package, version_declared(Version)) :- pkg_fact(Package, version_declared(Version, _)).
% If something is a package, it has only one version and that must be a
% declared version.
% We allow clingo to choose any version(s), and infer an error if there
% is not precisely one version chosen. Error facts are heavily optimized
% against to ensure they cannot be inferred when a non-error solution is
% possible
{ attr("version", node(ID, Package), Version) : pkg_fact(Package, version_declared(Version)) }
:- attr("node", node(ID, Package)).
% A virtual package may or may not have a version, but never has more than one
error(100, "Cannot select a single version for virtual '{0}'", Virtual)
:- attr("virtual_node", node(ID, Virtual)),
2 { attr("version", node(ID, Virtual), Version) }.
% If we select a deprecated version, mark the package as deprecated
attr("deprecated", node(ID, Package), Version) :-
attr("version", node(ID, Package), Version),
not external(node(ID, Package)),
pkg_fact(Package, deprecated_version(Version)).
error(100, "Package '{0}' needs the deprecated version '{1}', and this is not allowed", Package, Version)
:- deprecated_versions_not_allowed(),
attr("version", node(ID, Package), Version),
not external(node(ID, Package)),
not concrete(node(ID, Package)),
pkg_fact(Package, deprecated_version(Version)).
possible_version_weight(node(ID, Package), Weight)
:- attr("version", node(ID, Package), Version),
pkg_fact(Package, version_declared(Version, Weight)).
% we can't use the weight for an external version if we don't use the
% corresponding external spec.
:- attr("version", node(ID, Package), Version),
version_weight(node(ID, Package), Weight),
pkg_fact(Package, version_declared(Version, Weight, "external")),
not external(node(ID, Package)),
internal_error("External weight used for built package").
% we can't use a weight from an installed spec if we are building it
% and vice-versa
:- attr("version", node(ID, Package), Version),
version_weight(node(ID, Package), Weight),
pkg_fact(Package, version_declared(Version, Weight, "installed")),
build(node(ID, Package)),
internal_error("Reuse version weight used for build package").
:- attr("version", node(ID, Package), Version),
version_weight(node(ID, Package), Weight),
not pkg_fact(Package, version_declared(Version, Weight, "installed")),
not pkg_fact(Package, version_declared(Version, Weight, "installed_git_version")),
not build(node(ID, Package)),
internal_error("Build version weight used for reused package").
1 { version_weight(node(ID, Package), Weight) : pkg_fact(Package, version_declared(Version, Weight)) } 1
:- attr("version", node(ID, Package), Version),
attr("node", node(ID, Package)),
internal_error("version weights must exist and be unique").
% node_version_satisfies implies that exactly one of the satisfying versions
% is the package's version, and vice versa.
% While this choice rule appears redundant with the initial choice rule for
% versions, virtual nodes with version constraints require this rule to be
% able to choose versions
{ attr("version", node(ID, Package), Version) : pkg_fact(Package, version_satisfies(Constraint, Version)) }
:- attr("node_version_satisfies", node(ID, Package), Constraint).
% More specific error message if the version cannot satisfy some constraint
% Otherwise covered by `no_version_error` and `versions_conflict_error`.
error(1, "Cannot satisfy '{0}@{1}'", Package, Constraint)
:- attr("node_version_satisfies", node(ID, Package), Constraint),
attr("version", node(ID, Package), Version),
not pkg_fact(Package, version_satisfies(Constraint, Version)).
error(10, "Cannot satisfy '{0}@{1}'", Package, Constraint)
:- attr("node_version_satisfies", node(ID, Package), Constraint),
not attr("version", node(ID, Package), _).
attr("node_version_satisfies", node(ID, Package), Constraint)
:- attr("version", node(ID, Package), Version),
pkg_fact(Package, version_satisfies(Constraint, Version)).
#defined version_satisfies/3.
#defined deprecated_versions_not_allowed/0.
#defined deprecated_version/2.
%-----------------------------------------------------------------------------
% Spec conditions and imposed constraints
%
% Given Spack directives like these:
% depends_on("foo@1.0+bar", when="@2.0+variant")
% provides("mpi@2:", when="@1.9:")
%
% The conditions are `@2.0+variant` and `@1.9:`, and the imposed constraints
% are `@1.0+bar` on `foo` and `@2:` on `mpi`.
%-----------------------------------------------------------------------------
% conditions are specified with `condition_requirement` and hold when
% corresponding spec attributes hold.
% A "condition_set(PackageNode, _)" is the set of nodes on which PackageNode can require / impose conditions
% Currently, for a given node, this is the link-run sub-DAG of PackageNode and its direct build dependencies
condition_set(PackageNode, PackageNode, direct_link_run) :- attr("node", PackageNode).
condition_set(PackageNode, PackageNode, direct_link_run) :- provider(PackageNode, VirtualNode).
condition_set(PackageNode, VirtualNode, direct_link_run) :- provider(PackageNode, VirtualNode).
condition_set(PackageNode, DependencyNode, direct_build) :- condition_set(PackageNode, PackageNode, direct_link_run), attr("depends_on", PackageNode, DependencyNode, "build").
condition_set(PackageNode, DependencyNode, direct_link_run) :- condition_set(PackageNode, PackageNode, direct_link_run), attr("depends_on", PackageNode, DependencyNode, Type), Type != "build".
condition_set(ID, VirtualNode, Type) :- condition_set(ID, PackageNode, Type), provider(PackageNode, VirtualNode).
condition_set(ID, PackageNode) :- condition_set(ID, PackageNode, _).
condition_set(VirtualNode, X) :- provider(PackageNode, VirtualNode), condition_set(PackageNode, X).
condition_packages(ID, A1) :- condition_requirement(ID, _, A1).
condition_packages(ID, A1) :- condition_requirement(ID, _, A1, _).
condition_packages(ID, A1) :- condition_requirement(ID, _, A1, _, _).
condition_packages(ID, A1) :- condition_requirement(ID, _, A1, _, _, _).
trigger_node(ID, node(PackageID, Package), node(PackageID, Package)) :- pkg_fact(Package, trigger_id(ID)), attr("node", node(PackageID, Package)).
trigger_node(ID, node(PackageID, Package), node(VirtualID, Virtual)) :- pkg_fact(Virtual, trigger_id(ID)), provider(node(PackageID, Package), node(VirtualID, Virtual)).
condition_nodes(TriggerID, PackageNode, node(X, A1))
:- condition_packages(TriggerID, A1),
condition_set(PackageNode, node(X, A1)),
not self_build_requirement(PackageNode, node(X, A1)),
trigger_node(TriggerID, PackageNode, _).
cannot_hold(TriggerID, PackageNode)
:- condition_packages(TriggerID, A1),
not condition_set(PackageNode, node(_, A1)),
trigger_node(TriggerID, PackageNode, _).
trigger_condition_holds(ID, RequestorNode) :-
trigger_node(ID, PackageNode, RequestorNode);
attr(Name, node(X, A1)) : condition_requirement(ID, Name, A1), condition_nodes(ID, PackageNode, node(X, A1));
attr(Name, node(X, A1), A2) : condition_requirement(ID, Name, A1, A2), condition_nodes(ID, PackageNode, node(X, A1));
attr(Name, node(X, A1), A2, A3) : condition_requirement(ID, Name, A1, A2, A3), condition_nodes(ID, PackageNode, node(X, A1)), not multiple_nodes_attribute(Name);
attr(Name, node(X, A1), A2, A3, A4) : condition_requirement(ID, Name, A1, A2, A3, A4), condition_nodes(ID, PackageNode, node(X, A1));
% Special cases
attr("depends_on", node(X, A1), node(Y, A2), A3) : condition_requirement(ID, "depends_on", A1, A2, A3), condition_nodes(ID, PackageNode, node(X, A1)), condition_nodes(ID, PackageNode, node(Y, A2));
not cannot_hold(ID, PackageNode).
condition_holds(ConditionID, node(X, Package))
:- pkg_fact(Package, condition_trigger(ConditionID, TriggerID)),
trigger_condition_holds(TriggerID, node(X, Package)).
trigger_and_effect(Package, TriggerID, EffectID)
:- pkg_fact(Package, condition_trigger(ID, TriggerID)),
pkg_fact(Package, condition_effect(ID, EffectID)).
% condition_holds(ID, node(ID, Package)) implies all imposed_constraints, unless do_not_impose(ID, node(ID, Package))
% is derived. This allows imposed constraints to be canceled in special cases.
impose(EffectID, node(X, Package))
:- trigger_and_effect(Package, TriggerID, EffectID),
trigger_node(TriggerID, _, node(X, Package)),
trigger_condition_holds(TriggerID, node(X, Package)),
not do_not_impose(EffectID, node(X, Package)).
imposed_packages(ID, A1) :- imposed_constraint(ID, _, A1).
imposed_packages(ID, A1) :- imposed_constraint(ID, _, A1, _).
imposed_packages(ID, A1) :- imposed_constraint(ID, _, A1, _, _).
imposed_packages(ID, A1) :- imposed_constraint(ID, _, A1, _, _, _).
imposed_packages(ID, A1) :- imposed_constraint(ID, "depends_on", _, A1, _).
imposed_nodes(EffectID, node(NodeID, Package), node(X, A1))
:- pkg_fact(Package, condition_trigger(ID, TriggerID)),
pkg_fact(Package, condition_effect(ID, EffectID)),
imposed_packages(EffectID, A1),
condition_set(node(NodeID, Package), node(X, A1)),
trigger_node(TriggerID, _, node(NodeID, Package)),
% We don't want to add build requirements to imposed nodes, to avoid
% unsat problems when we deal with self-dependencies: gcc@14 %gcc@10
not self_build_requirement(node(NodeID, Package), node(X, A1)).
self_build_requirement(node(X, Package), node(Y, Package)) :- build_requirement(node(X, Package), node(Y, Package)).
imposed_nodes(ConditionID, PackageNode, node(X, A1))
:- imposed_packages(ConditionID, A1),
condition_set(PackageNode, node(X, A1)),
attr("hash", PackageNode, ConditionID).
:- imposed_packages(ID, A1), impose(ID, PackageNode), not condition_set(PackageNode, node(_, A1)),
internal_error("Imposing constraint outside of condition set").
:- imposed_packages(ID, A1), impose(ID, PackageNode), not imposed_nodes(ID, PackageNode, node(_, A1)),
internal_error("Imposing constraint outside of imposed_nodes").
% Conditions that hold impose may impose constraints on other specs
attr(Name, node(X, A1)) :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1), imposed_nodes(ID, PackageNode, node(X, A1)).
attr(Name, node(X, A1), A2) :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1, A2), imposed_nodes(ID, PackageNode, node(X, A1)), not multiple_nodes_attribute(Name).
attr(Name, node(X, A1), A2, A3) :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1, A2, A3), imposed_nodes(ID, PackageNode, node(X, A1)), not multiple_nodes_attribute(Name).
attr(Name, node(X, A1), A2, A3, A4) :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1, A2, A3, A4), imposed_nodes(ID, PackageNode, node(X, A1)).
% Provider set is relevant only for literals, since it's the only place where `^[virtuals=foo] bar`
% might appear in the HEAD of a rule
attr("provider_set", node(min_dupe_id, Provider), node(min_dupe_id, Virtual))
:- solve_literal(TriggerID),
trigger_and_effect(_, TriggerID, EffectID),
impose(EffectID, _),
imposed_constraint(EffectID, "provider_set", Provider, Virtual).
provider(ProviderNode, VirtualNode) :- attr("provider_set", ProviderNode, VirtualNode).
% Here we can't use the condition set because it's a recursive definition, that doesn't define the
% node index, and leads to unsatisfiability. Hence we say that one and only one node index must
% satisfy the dependency.
1 { attr("depends_on", node(X, A1), node(0..Y-1, A2), A3) : max_dupes(A2, Y) } 1
:- impose(ID, node(X, A1)),
imposed_constraint(ID, "depends_on", A1, A2, A3),
internal_error("Build deps must land in exactly one duplicate").
% The rule below accounts for expressions like:
%
% root ^dep %compiler
%
% where "compiler" is a dependency of "dep", but is enforced by a condition imposed by "root"
1 { attr("depends_on", node(A1_DUPE_ID, A1), node(0..Y-1, A2), A3) : max_dupes(A2, Y) } 1
:- impose(ID, RootNode),
unification_set("root", RootNode),
condition_set(RootNode, node(A1_DUPE_ID, A1)),
not self_build_requirement(RootNode, node(A1_DUPE_ID, A1)),
imposed_constraint(ID, "depends_on", A1, A2, A3),
internal_error("Build deps must land in exactly one duplicate").
% If the parent is built, then we have a build_requirement on another node. For concrete nodes,
% or external nodes, we don't since we are trimming their build dependencies.
1 { attr("depends_on", node(X, Parent), node(0..Y-1, BuildDependency), "build") : max_dupes(BuildDependency, Y) } 1
:- attr("build_requirement", node(X, Parent), build_requirement("node", BuildDependency)),
build(node(X, Parent)),
not external(node(X, Parent)).
:- attr("build_requirement", ParentNode, build_requirement("node", BuildDependency)),
concrete(ParentNode),
not attr("concrete_build_dependency", ParentNode, BuildDependency, _).
:- attr("build_requirement", ParentNode, build_requirement("node_version_satisfies", BuildDependency, Constraint)),
attr("concrete_build_dependency", ParentNode, BuildDependency, BuildDependencyHash),
not 1 { pkg_fact(BuildDependency, version_satisfies(Constraint, Version)) : hash_attr(BuildDependencyHash, "version", BuildDependency, Version) } 1.
:- attr("build_requirement", ParentNode, build_requirement("provider_set", BuildDependency, Virtual)),
attr("concrete_build_dependency", ParentNode, BuildDependency, BuildDependencyHash),
attr("virtual_on_build_edge", ParentNode, BuildDependency, Virtual),
not 1 { pkg_fact(BuildDependency, version_satisfies(Constraint, Version)) : hash_attr(BuildDependencyHash, "version", BuildDependency, Version) } 1.
% Asking for gcc@10 %gcc@9 shouldn't give us back an external gcc@10, just because of the hack
% we have on externals
:- attr("build_requirement", node(X, Parent), build_requirement("node", BuildDependency)),
Parent == BuildDependency,
external(node(X, Parent)).
build_requirement(node(X, Parent), node(Y, BuildDependency)) :-
attr("depends_on", node(X, Parent), node(Y, BuildDependency), "build"),
attr("build_requirement", node(X, Parent), build_requirement("node", BuildDependency)).
1 { virtual_build_requirement(ParentNode, node(0..Y-1, Virtual)) : max_dupes(Virtual, Y) } 1
:- attr("dependency_holds", ParentNode, Virtual, "build"),
not attr("dependency_holds", ParentNode, Virtual,"link"),
not attr("dependency_holds", ParentNode, Virtual,"run"),
virtual(Virtual).
attr("virtual_node", VirtualNode) :- virtual_build_requirement(ParentNode, VirtualNode).
build_requirement(ParentNode, ProviderNode) :- virtual_build_requirement(ParentNode, VirtualNode), provider(ProviderNode, VirtualNode).
% From cli we can have literal expressions like:
%
% root %gcc@12.0 ^dep %gcc@11.2
%
% Adding a "build_requirement" is a way to discriminate between the incompatible
% version constraints on "gcc" in the "imposed_constraint".
attr("node_version_satisfies", node(X, BuildDependency), Constraint) :-
attr("build_requirement", ParentNode, build_requirement("node_version_satisfies", BuildDependency, Constraint)),
build_requirement(ParentNode, node(X, BuildDependency)).
1 { attr("provider_set", node(X, BuildDependency), node(0..Y-1, Virtual)) : max_dupes(Virtual, Y) } 1 :-
attr("build_requirement", ParentNode, build_requirement("provider_set", BuildDependency, Virtual)),
build_requirement(ParentNode, node(X, BuildDependency)).
% Reconstruct virtual dependencies for reused specs
attr("virtual_on_edge", node(X, A1), node(Y, A2), Virtual)
:- impose(ID, node(X, A1)),
depends_on(node(X, A1), node(Y, A2)),
imposed_constraint(ID, "virtual_on_edge", A1, A2, Virtual),
not build(node(X, A1)).
virtual_condition_holds(node(Y, A2), Virtual)
:- impose(ID, node(X, A1)),
attr("virtual_on_edge", node(X, A1), node(Y, A2), Virtual),
not build(node(X, A1)).
% we cannot have additional variant values when we are working with concrete specs
:- attr("node", node(ID, Package)),
attr("hash", node(ID, Package), Hash),
attr("variant_value", node(ID, Package), Variant, Value),
not imposed_constraint(Hash, "variant_value", Package, Variant, Value),
internal_error("imposed hash without imposing all variant values").
% we cannot have additional flag values when we are working with concrete specs
:- attr("node", node(ID, Package)),
attr("hash", node(ID, Package), Hash),
attr("node_flag", node(ID, Package), node_flag(FlagType, Flag, _, _)),
not imposed_constraint(Hash, "node_flag", Package, node_flag(FlagType, Flag, _, _)),
internal_error("imposed hash without imposing all flag values").
#defined condition/2.
#defined condition_requirement/3.
#defined condition_requirement/4.
#defined condition_requirement/5.
#defined condition_requirement/6.
#defined imposed_constraint/3.
#defined imposed_constraint/4.
#defined imposed_constraint/5.
#defined imposed_constraint/6.
%-----------------------------------------------------------------------------
% Concrete specs
%-----------------------------------------------------------------------------
% if a package is assigned a hash, it's concrete.
concrete(PackageNode) :- attr("hash", PackageNode, _), attr("node", PackageNode).
:- concrete(PackageNode),
depends_on(PackageNode, DependencyNode),
not concrete(DependencyNode),
not abi_splice_conditions_hold(_, DependencyNode, _, _).
%-----------------------------------------------------------------------------
% Dependency semantics
%-----------------------------------------------------------------------------
% Dependencies of any type imply that one package "depends on" another
depends_on(PackageNode, DependencyNode) :- attr("depends_on", PackageNode, DependencyNode, _).
% a dependency holds if its condition holds and if it is not external or
% concrete. We chop off dependencies for externals, and dependencies of
% concrete specs don't need to be resolved -- they arise from the concrete
% specs themselves.
attr("track_dependencies", Node) :- build(Node), not external(Node).
% If a dependency holds on a package node, there must be one and only one dependency node satisfying it
1 { attr("depends_on", PackageNode, node(0..Y-1, Dependency), Type) : max_dupes(Dependency, Y) } 1
:- attr("dependency_holds", PackageNode, Dependency, Type),
not virtual(Dependency).
% all nodes in the graph must be reachable from some root
% this ensures a user can't say `zlib ^libiconv` (neither of which have any
% dependencies) and get a two-node unconnected graph
needed(PackageNode) :- attr("root", PackageNode).
needed(ChildNode) :- edge_needed(ParentNode, ChildNode).
edge_needed(ParentNode, node(X, Child)) :- depends_on(ParentNode, node(X, Child)), runtime(Child).
edge_needed(ParentNode, ChildNode) :- depends_on(ParentNode, ChildNode) , concrete(ParentNode).
edge_needed(ParentNode, node(X, Child)) :-
depends_on(ParentNode, node(X, Child)),
build(ParentNode),
attr("dependency_holds", ParentNode, Child, _).
virtual_edge_needed(ParentNode, ChildNode, node(X, Virtual)) :-
depends_on(ParentNode, ChildNode),
build(ParentNode),
node_depends_on_virtual(ParentNode, Virtual),
provider(ChildNode, node(X, Virtual)).
virtual_edge_needed(ParentNode, ChildNode, node(X, Virtual)) :-
concrete(ParentNode),
concrete(ChildNode),
provider(ChildNode, node(X, Virtual)),
attr("virtual_on_edge", ParentNode, ChildNode, Virtual).
virtual_edge_needed(ParentNode, ChildNode, node(X, Virtual)) :-
concrete(ParentNode),
abi_splice_conditions_hold(_, ChildNode, _, _),
provider(ChildNode, node(X, Virtual)),
attr("virtual_on_edge", ParentNode, ChildNode, Virtual).
edge_needed(ParentNode, ChildNode) :- virtual_edge_needed(ParentNode, ChildNode, _).
provider_needed(ChildNode, VirtualNode) :- virtual_edge_needed(_, ChildNode, VirtualNode).
provider_needed(ChildNode, VirtualNode) :- attr("virtual_root", VirtualNode), provider(ChildNode, VirtualNode).
error(10, "'{0}' is not a valid dependency for any package in the DAG", Package)
:- attr("node", node(ID, Package)),
not needed(node(ID, Package)).
:- depends_on(ParentNode, ChildNode),
not edge_needed(ParentNode, ChildNode),
build(ParentNode).
:- provider(PackageNode, VirtualNode),
not provider_needed(PackageNode, VirtualNode),
not attr("virtual_root", VirtualNode).
% Extensions depending on each other must all extend the same node (e.g. all Python packages
% depending on each other must depend on the same Python interpreter)
error(100, "{0} and {1} must depend on the same {2}", ExtensionParent, ExtensionChild, ExtendeePackage)
:- depends_on(ExtensionParent, ExtensionChild),
attr("extends", ExtensionParent, ExtendeePackage),
depends_on(ExtensionParent, node(X, ExtendeePackage)),
depends_on(ExtensionChild, node(Y, ExtendeePackage)),
X != Y.
#defined dependency_type/2.
%-----------------------------------------------------------------------------
% Conflicts
%-----------------------------------------------------------------------------
error(1, Msg)
:- attr("node", node(ID, Package)),
pkg_fact(Package, conflict(TriggerID, ConstraintID, Msg)),
% node(ID1, TriggerPackage) is node(ID2, Package) in most, but not all, cases
condition_holds(TriggerID, node(ID1, TriggerPackage)),
condition_holds(ConstraintID, node(ID2, Package)),
unification_set(X, node(ID2, Package)),
unification_set(X, node(ID1, TriggerPackage)),
not external(node(ID, Package)), % ignore conflicts for externals
not attr("hash", node(ID, Package), _). % ignore conflicts for installed packages
%-----------------------------------------------------------------------------
% Virtual dependencies
%-----------------------------------------------------------------------------
% If the provider is set from the command line, its weight is 0
possible_provider_weight(ProviderNode, VirtualNode, 0, "Set on the command line")
:- attr("provider_set", ProviderNode, VirtualNode).
% Enforces all virtuals to be provided, if multiple of them are provided together
error(100, "Package '{0}' needs to provide both '{1}' and '{2}' together, but provides only '{1}'", Package, Virtual1, Virtual2)
:- % This package provides 2 or more virtuals together
condition_holds(ID, node(X, Package)),
pkg_fact(Package, provided_together(ID, SetID, Virtual1)),
pkg_fact(Package, provided_together(ID, SetID, Virtual2)),
Virtual1 != Virtual2,
% One node depends on those virtuals AND on this package
node_depends_on_virtual(ClientNode, Virtual1),
node_depends_on_virtual(ClientNode, Virtual2),
depends_on(ClientNode, node(X, Package)),
% But this package is a provider of only one of them
provider(node(X, Package), node(_, Virtual1)),
not provider(node(X, Package), node(_, Virtual2)).
% if a package depends on a virtual, it's not external and we have a
% provider for that virtual then it depends on the provider
node_depends_on_virtual(PackageNode, Virtual, Type)
:- attr("dependency_holds", PackageNode, Virtual, Type),
virtual(Virtual),
not external(PackageNode).
node_depends_on_virtual(PackageNode, Virtual) :- node_depends_on_virtual(PackageNode, Virtual, Type).
1 { attr("depends_on", PackageNode, ProviderNode, Type) : provider(ProviderNode, node(VirtualID, Virtual)) } 1
:- node_depends_on_virtual(PackageNode, Virtual, Type).
attr("virtual_on_edge", PackageNode, ProviderNode, Virtual)
:- attr("dependency_holds", PackageNode, Virtual, Type),
attr("depends_on", PackageNode, ProviderNode, Type),
provider(ProviderNode, node(_, Virtual)),
not external(PackageNode).
% If a virtual node is in the answer set, it must be either a virtual root,
% or used somewhere
:- attr("virtual_node", node(_, Virtual)),
not attr("virtual_on_incoming_edges", _, Virtual),
not attr("virtual_root", node(_, Virtual)),
internal_error("virtual node does not match incoming edge").
attr("virtual_on_incoming_edges", ProviderNode, Virtual)
:- attr("virtual_on_edge", _, ProviderNode, Virtual).
% This is needed to allow requirement on virtuals,
% when a virtual root is requested
attr("virtual_on_incoming_edges", ProviderNode, Virtual)
:- attr("virtual_root", node(min_dupe_id, Virtual)),
attr("root", ProviderNode),
provider(ProviderNode, node(min_dupe_id, Virtual)).
% dependencies on virtuals also imply that the virtual is a virtual node
1 { attr("virtual_node", node(0..X-1, Virtual)) : max_dupes(Virtual, X) }
:- node_depends_on_virtual(PackageNode, Virtual).
% If there's a virtual node, we must select one and only one provider.
% The provider must be selected among the possible providers.
error(100, "'{0}' cannot be a provider for the '{1}' virtual", Package, Virtual)
:- attr("provider_set", node(X, Package), node(Y, Virtual)),
not virtual_condition_holds( node(X, Package), Virtual).
error(100, "Cannot find valid provider for virtual {0}", Virtual)
:- attr("virtual_node", node(X, Virtual)),
not provider(_, node(X, Virtual)).
error(100, "Cannot select a single provider for virtual '{0}'", Virtual)
:- attr("virtual_node", node(X, Virtual)),
2 { provider(P, node(X, Virtual)) }.
% virtual roots imply virtual nodes, and that one provider is a root
attr("virtual_node", VirtualNode) :- attr("virtual_root", VirtualNode).
% If we asked for a virtual root and we have a provider for that,
% then the provider is the root package.
attr("root", PackageNode) :- attr("virtual_root", VirtualNode), provider(PackageNode, VirtualNode).
% The provider is selected among the nodes for which the virtual condition holds
1 { provider(PackageNode, node(X, Virtual)) :
attr("node", PackageNode), virtual_condition_holds(PackageNode, Virtual) } 1
:- attr("virtual_node", node(X, Virtual)).
% The provider provides the virtual if some provider condition holds.
virtual_condition_holds(node(ProviderID, Provider), Virtual) :- virtual_condition_holds(ID, node(ProviderID, Provider), Virtual).
virtual_condition_holds(ID, node(ProviderID, Provider), Virtual) :-
pkg_fact(Provider, provider_condition(ID, Virtual)),
condition_holds(ID, node(ProviderID, Provider)),
virtual(Virtual).
% If a "provider" condition holds, but this package is not a provider, do not impose the "provider" condition
do_not_impose(EffectID, node(X, Package))
:- virtual_condition_holds(ID, node(X, Package), Virtual),
pkg_fact(Package, condition_effect(ID, EffectID)),
not provider(node(X, Package), node(_, Virtual)).
% Choose the provider among root specs, if possible
:- provider(ProviderNode, node(min_dupe_id, Virtual)),
virtual_condition_holds(_, PossibleProvider, Virtual),
PossibleProvider != ProviderNode,
explicitly_requested_root(PossibleProvider),
not self_build_requirement(PossibleProvider, ProviderNode),
not explicitly_requested_root(ProviderNode),
not language(Virtual),
internal_error("If a root can provide a virtual, it must be the provider").
% A package cannot be the actual provider for a virtual if it does not
% fulfill the conditions to provide that virtual
:- provider(PackageNode, node(VirtualID, Virtual)),
not virtual_condition_holds(PackageNode, Virtual),
internal_error("Virtual when provides not respected").
#defined provided_together/4.
%-----------------------------------------------------------------------------
% Virtual dependency weights
%-----------------------------------------------------------------------------
% A provider has different possible weights depending on its preference. This rule ensures that
% we select the weight, among the possible ones, that minimizes the overall objective function.
1 { provider_weight(DependencyNode, VirtualNode, Weight) :
possible_provider_weight(DependencyNode, VirtualNode, Weight, _) } 1
:- provider(DependencyNode, VirtualNode), internal_error("Package provider weights must be unique").
% Any configured provider has a weight based on index in the preference list
possible_provider_weight(node(ProviderID, Provider), node(VirtualID, Virtual), Weight, "default")
:- provider(node(ProviderID, Provider), node(VirtualID, Virtual)),
default_provider_preference(Virtual, Provider, Weight).
% Any non-configured provider has a default weight of 100
possible_provider_weight(node(ProviderID, Provider), VirtualNode, 100, "fallback")
:- provider(node(ProviderID, Provider), VirtualNode).
% do not warn if generated program contains none of these.
#defined virtual/1.
#defined virtual_condition_holds/2.
#defined external/1.
#defined buildable_false/1.
#defined default_provider_preference/3.
%-----------------------------------------------------------------------------
% External semantics
%-----------------------------------------------------------------------------
% if a package is external its version must be one of the external versions
{ external_version(node(ID, Package), Version, Weight):
pkg_fact(Package, version_declared(Version, Weight, "external")) }
:- external(node(ID, Package)).
error(100, "Attempted to use external for '{0}' which does not satisfy any configured external spec version", Package)
:- external(node(ID, Package)),
not external_version(node(ID, Package), _, _).
error(100, "Attempted to use external for '{0}' which does not satisfy a unique configured external spec version", Package)
:- external(node(ID, Package)),
2 { external_version(node(ID, Package), Version, Weight) }.
version_weight(PackageNode, Weight) :- external_version(PackageNode, Version, Weight).
attr("version", PackageNode, Version) :- external_version(PackageNode, Version, Weight).
% if a package is not buildable, only externals or hashed specs are allowed
external(node(ID, Package))
:- buildable_false(Package),
attr("node", node(ID, Package)),
not attr("hash", node(ID, Package), _).
% a package is a real_node if it is not external
real_node(PackageNode) :- attr("node", PackageNode), not external(PackageNode).
% a package is external if we are using an external spec for it
external(PackageNode) :- attr("external_spec_selected", PackageNode, _).
% we can't use the weight for an external version if we don't use the
% corresponding external spec.
:- attr("version", node(ID, Package), Version),
version_weight(node(ID, Package), Weight),
pkg_fact(Package, version_declared(Version, Weight, "external")),
not external(node(ID, Package)),
internal_error("External weight used for internal spec").
% determine if an external spec has been selected
attr("external_spec_selected", node(ID, Package), LocalIndex) :-
attr("external_conditions_hold", node(ID, Package), LocalIndex),
attr("node", node(ID, Package)),
not attr("hash", node(ID, Package), _).
% Account for compiler annotation on externals
:- not attr("root", ExternalNode),
attr("external_build_requirement", ExternalNode, build_requirement("node", Compiler)),
not node_compiler(_, node(_, Compiler)).
1 { attr("node_version_satisfies", node(X, Compiler), Constraint) : node_compiler(_, node(X, Compiler)) }
:- not attr("root", ExternalNode),
attr("external_build_requirement", ExternalNode, build_requirement("node", Compiler)),
attr("external_build_requirement", ExternalNode, build_requirement("node_version_satisfies", Compiler, Constraint)).
% it cannot happen that a spec is external, but none of the external specs
% conditions hold.
error(100, "Attempted to use external for '{0}' which does not satisfy any configured external spec", Package)
:- external(node(ID, Package)),
not attr("external_conditions_hold", node(ID, Package), _).
%-----------------------------------------------------------------------------
% Config required semantics
%-----------------------------------------------------------------------------
package_in_dag(Node) :- attr("node", Node).
package_in_dag(Node) :- attr("virtual_node", Node).
activate_requirement(node(ID, Package), X) :-
package_in_dag(node(ID, Package)),
requirement_group(Package, X),
not requirement_conditional(Package, X, _).
activate_requirement(node(ID, Package), X) :-
package_in_dag(node(ID, Package)),
requirement_group(Package, X),
condition_holds(Y, node(ID, Package)),
requirement_conditional(Package, X, Y).
requirement_group_satisfied(node(ID, Package), X) :-
1 { condition_holds(Y, node(ID, Package)) : requirement_group_member(Y, Package, X) } 1,
requirement_policy(Package, X, "one_of"),
activate_requirement(node(ID, Package), X),
requirement_group(Package, X).
requirement_weight(node(ID, Package), Group, W) :-
condition_holds(Y, node(ID, Package)),
requirement_has_weight(Y, W),
requirement_group_member(Y, Package, Group),
requirement_policy(Package, Group, "one_of"),
requirement_group_satisfied(node(ID, Package), Group).
{ attr("build_requirement", node(ID, Package), BuildRequirement) : condition_requirement(TriggerID, "build_requirement", Package, BuildRequirement) } :-
pkg_fact(Package, condition_trigger(ConditionID, TriggerID)),
requirement_group_member(ConditionID, Package, Group),
activate_requirement(node(ID, Package), Group),
requirement_group(Package, Group).
requirement_group_satisfied(node(ID, Package), X) :-
1 { condition_holds(Y, node(ID, Package)) : requirement_group_member(Y, Package, X) } ,
requirement_policy(Package, X, "any_of"),
activate_requirement(node(ID, Package), X),
requirement_group(Package, X).
% Do not impose requirements, if the conditional requirement is not active
do_not_impose(EffectID, node(ID, Package)) :-
trigger_condition_holds(TriggerID, node(ID, Package)),
pkg_fact(Package, condition_trigger(ConditionID, TriggerID)),
pkg_fact(Package, condition_effect(ConditionID, EffectID)),
requirement_group_member(ConditionID , Package, RequirementID),
not activate_requirement(node(ID, Package), RequirementID).
% When we have a required provider, we need to ensure that the provider/2 facts respect
% the requirement. This is particularly important for packages that could provide multiple
% virtuals independently
required_provider(Provider, Virtual)
:- requirement_group_member(ConditionID, Virtual, RequirementID),
condition_holds(ConditionID, _),
virtual(Virtual),
pkg_fact(Virtual, condition_effect(ConditionID, EffectID)),
imposed_constraint(EffectID, "node", Provider).
error(1, "Trying to use {0} as a provider for {1}, but {2} is required", Package, Virtual, Provider) :-
provider(node(Y, Package), node(X, Virtual)),
required_provider(Provider, Virtual),
Package != Provider.
% TODO: the following choice rule allows the solver to add compiler
% flags if their only source is from a requirement. This is overly-specific
% and should use a more-generic approach like in https://github.com/spack/spack/pull/37180
{ attr("node_flag", node(ID, Package), NodeFlag) } :-
requirement_group_member(ConditionID, Package, RequirementID),
activate_requirement(node(ID, Package), RequirementID),
pkg_fact(Package, condition_effect(ConditionID, EffectID)),
imposed_constraint(EffectID, "node_flag_set", Package, NodeFlag).
requirement_weight(node(ID, Package), Group, W) :-
W = #min {
Z : requirement_has_weight(Y, Z), condition_holds(Y, node(ID, Package)), requirement_group_member(Y, Package, Group);
% We need this to avoid an annoying warning during the solve
% concretize.lp:1151:5-11: info: tuple ignored:
% #sup@73
10000
},
requirement_policy(Package, Group, "any_of"),
requirement_group_satisfied(node(ID, Package), Group).
error(100, "cannot satisfy a requirement for package '{0}'.", Package) :-
activate_requirement(node(ID, Package), X),
requirement_group(Package, X),
not requirement_message(Package, X, _),
not requirement_group_satisfied(node(ID, Package), X).
error(10, Message) :-
activate_requirement(node(ID, Package), X),
requirement_group(Package, X),
requirement_message(Package, X, Message),
not requirement_group_satisfied(node(ID, Package), X).
#defined requirement_group/2.
#defined requirement_conditional/3.
#defined requirement_message/3.
#defined requirement_group_member/3.
#defined requirement_has_weight/2.
#defined requirement_policy/3.
%-----------------------------------------------------------------------------
% Variant semantics
%-----------------------------------------------------------------------------
% Packages define potentially several definitions for each variant, and depending
% on their attibutes, duplicate nodes for the same package may use different
% definitions. So the variant logic has several jobs:
% A. Associate a variant definition with a node, by VariantID
% B. Associate defaults and attributes (sticky, etc.) for the selected variant ID with the node.
% C. Once these rules are established for a node, select variant value(s) based on them.
% A: Selecting a variant definition
% Variant definitions come from package facts in two ways:
% 1. unconditional variants are always defined on all nodes for a given package
variant_definition(node(NodeID, Package), Name, VariantID) :-
pkg_fact(Package, variant_definition(Name, VariantID)),
attr("node", node(NodeID, Package)).
% 2. conditional variants are only defined if the conditions hold for the node
variant_definition(node(NodeID, Package), Name, VariantID) :-
pkg_fact(Package, variant_condition(Name, VariantID, ConditionID)),
condition_holds(ConditionID, node(NodeID, Package)).
% If there are any definitions for a variant on a node, the variant is "defined".
variant_defined(PackageNode, Name) :- variant_definition(PackageNode, Name, _).
% We must select one definition for each defined variant on a node.
1 {
node_has_variant(PackageNode, Name, VariantID) : variant_definition(PackageNode, Name, VariantID)
} 1 :-
variant_defined(PackageNode, Name).
% Solver must pick the variant definition with the highest id. When conditions hold
% for two or more variant definitions, this prefers the last one defined.
:- node_has_variant(node(NodeID, Package), Name, SelectedVariantID),
variant_definition(node(NodeID, Package), Name, VariantID),
VariantID > SelectedVariantID,
internal_error("If the solver picks a variant descriptor it must use that variant descriptor").
% B: Associating applicable package rules with nodes
% The default value for a variant in a package is what is prescribed:
% 1. On the command line
% 2. In packages.yaml (if there's no command line settings)
% 3. In the package.py file (if there are no settings in packages.yaml and the command line)
% -- Associate the definition's default values with the node
% note that the package.py variant defaults are associated with a particular definition, but
% packages.yaml and CLI are associated with just the variant name.
% Also, settings specified on the CLI apply to all duplicates, but always have
% `min_dupe_id` as their node id.
variant_default_value(node(NodeID, Package), VariantName, Value) :-
node_has_variant(node(NodeID, Package), VariantName, VariantID),
pkg_fact(Package, variant_default_value_from_package_py(VariantID, Value)),
not variant_default_value_from_packages_yaml(Package, VariantName, _),
not attr("variant_default_value_from_cli", node(min_dupe_id, Package), VariantName, _).
variant_default_value(node(NodeID, Package), VariantName, Value) :-
node_has_variant(node(NodeID, Package), VariantName, _),
variant_default_value_from_packages_yaml(Package, VariantName, Value),
not attr("variant_default_value_from_cli", node(min_dupe_id, Package), VariantName, _).
variant_default_value(node(NodeID, Package), VariantName, Value) :-
node_has_variant(node(NodeID, Package), VariantName, _),
attr("variant_default_value_from_cli", node(min_dupe_id, Package), VariantName, Value).
% -- Associate the definition's possible values with the node
variant_possible_value(node(NodeID, Package), VariantName, Value) :-
node_has_variant(node(NodeID, Package), VariantName, VariantID),
pkg_fact(Package, variant_possible_value(VariantID, Value)).
variant_value_from_disjoint_sets(node(NodeID, Package), VariantName, Value1, Set1) :-
node_has_variant(node(NodeID, Package), VariantName, VariantID),
pkg_fact(Package, variant_value_from_disjoint_sets(VariantID, Value1, Set1)).
% -- Associate definition's arity with the node
variant_single_value(node(NodeID, Package), VariantName) :-
node_has_variant(node(NodeID, Package), VariantName, VariantID),
not variant_type(VariantID, "multi").
% C: Determining variant values on each node
% if a variant is sticky, but not set, its value is the default value
attr("variant_selected", node(ID, Package), Variant, Value, VariantType, VariantID) :-
node_has_variant(node(ID, Package), Variant, VariantID),
variant_default_value(node(ID, Package), Variant, Value),
pkg_fact(Package, variant_sticky(VariantID)),
variant_type(VariantID, VariantType),
not attr("variant_set", node(ID, Package), Variant),
build(node(ID, Package)).
% we can choose variant values from all the possible values for the node
{
attr("variant_selected", node(ID, Package), Variant, Value, VariantType, VariantID)
: variant_possible_value(node(ID, Package), Variant, Value)
} :-
attr("node", node(ID, Package)),
node_has_variant(node(ID, Package), Variant, VariantID),
variant_type(VariantID, VariantType),
build(node(ID, Package)).
% variant_selected is only needed for reconstruction on the python side, so we can ignore it here
attr("variant_value", PackageNode, Variant, Value) :-
attr("variant_selected", PackageNode, Variant, Value, VariantType, VariantID).
% a variant cannot be set if it is not a variant on the package
error(100, "Cannot set variant '{0}' for package '{1}' because the variant condition cannot be satisfied for the given spec", Variant, Package)
:- attr("variant_set", node(ID, Package), Variant),
not node_has_variant(node(ID, Package), Variant, _),
build(node(ID, Package)).
% a variant cannot take on a value if it is not a variant of the package
error(100, "Cannot set variant '{0}' for package '{1}' because the variant condition cannot be satisfied for the given spec", Variant, Package)
:- attr("variant_value", node(ID, Package), Variant, _),
not node_has_variant(node(ID, Package), Variant, _),
build(node(ID, Package)).
% at most one variant value for single-valued variants.
error(100, "'{0}' requires conflicting variant values 'Spec({1}={2})' and 'Spec({1}={3})'", Package, Variant, Value1, Value2)
:- attr("node", node(ID, Package)),
node_has_variant(node(ID, Package), Variant, _),
variant_single_value(node(ID, Package), Variant),
attr("variant_value", node(ID, Package), Variant, Value1),
attr("variant_value", node(ID, Package), Variant, Value2),
Value1 < Value2,
build(node(ID, Package)).
error(100, "No valid value for variant '{1}' of package '{0}'", Package, Variant)
:- attr("node", node(ID, Package)),
node_has_variant(node(ID, Package), Variant, _),
build(node(ID, Package)),
not attr("variant_value", node(ID, Package), Variant, _).
% if a variant is set to anything, it is considered 'set'.
attr("variant_set", PackageNode, Variant) :- attr("variant_set", PackageNode, Variant, _).
% A variant cannot have a value that is not also a possible value
% This only applies to packages we need to build -- concrete packages may
% have been built w/different variants from older/different package versions.
error(10, "'Spec({1}={2})' is not a valid value for '{0}' variant '{1}'", Package, Variant, Value)
:- attr("variant_value", node(ID, Package), Variant, Value),
not variant_possible_value(node(ID, Package), Variant, Value),
build(node(ID, Package)).
% Some multi valued variants accept multiple values from disjoint sets. Ensure that we
% respect that constraint and we don't pick values from more than one set at once
error(100, "{0} variant '{1}' cannot have values '{2}' and '{3}' as they come from disjoint value sets", Package, Variant, Value1, Value2)
:- attr("variant_value", node(ID, Package), Variant, Value1),
attr("variant_value", node(ID, Package), Variant, Value2),
variant_value_from_disjoint_sets(node(ID, Package), Variant, Value1, Set1),
variant_value_from_disjoint_sets(node(ID, Package), Variant, Value2, Set2),
Set1 < Set2, % see[1]
build(node(ID, Package)).
:- attr("variant_set", node(ID, Package), Variant, Value),
not attr("variant_value", node(ID, Package), Variant, Value).
internal_error("If a variant is set to a value it must have that value").
% The rules below allow us to prefer default values for variants
% whenever possible. If a variant is set in a spec, or if it is
% specified in an external, we score it as if it was a default value.
variant_not_default(node(ID, Package), Variant, Value)
:- attr("variant_value", node(ID, Package), Variant, Value),
not variant_default_value(node(ID, Package), Variant, Value),
% variants set explicitly on the CLI don't count as non-default
not attr("variant_set", node(ID, Package), Variant, Value),
% variant values forced by propagation don't count as non-default
not propagate(node(ID, Package), variant_value(Variant, Value, _)),
% variants set on externals that we could use don't count as non-default
% this makes spack prefer to use an external over rebuilding with the
% default configuration
not external_with_variant_set(node(ID, Package), Variant, Value),
attr("node", node(ID, Package)).
% A default variant value that is not used
variant_default_not_used(node(ID, Package), Variant, Value)
:- variant_default_value(node(ID, Package), Variant, Value),
node_has_variant(node(ID, Package), Variant, _),
not attr("variant_value", node(ID, Package), Variant, Value),
not propagate(node(ID, Package), variant_value(Variant, _, _)),
% variant set explicitly don't count for this metric
not attr("variant_set", node(ID, Package), Variant, _),
attr("node", node(ID, Package)).
% The variant is set in an external spec
external_with_variant_set(node(NodeID, Package), Variant, Value)
:- attr("variant_value", node(NodeID, Package), Variant, Value),
condition_requirement(TriggerID, "variant_value", Package, Variant, Value),
trigger_and_effect(Package, TriggerID, EffectID),
imposed_constraint(EffectID, "external_conditions_hold", Package, _),
external(node(NodeID, Package)),
attr("node", node(NodeID, Package)).
% Treat 'none' in a special way - it cannot be combined with other
% values even if the variant is multi-valued
error(100, "{0} variant '{1}' cannot have values '{2}' and 'none'", Package, Variant, Value)
:- attr("variant_value", node(X, Package), Variant, Value),
attr("variant_value", node(X, Package), Variant, "none"),
Value != "none",
build(node(X, Package)).
% -- Auto variants
% These don't have to be declared in the package. We allow them to spring into
% existence when assigned a value.
variant_possible_value(PackageNode, Variant, Value)
:- attr("variant_set", PackageNode, Variant, Value), auto_variant(Variant, _).
node_has_variant(PackageNode, Variant, VariantID)
:- attr("variant_set", PackageNode, Variant, _), auto_variant(Variant, VariantID).
variant_single_value(PackageNode, Variant)
:- node_has_variant(PackageNode, Variant, VariantID),
auto_variant(Variant, VariantID),
not variant_type(VariantID, "multi").
% suppress warnings about this atom being unset. It's only set if some
% spec or some package sets it, and without this, clingo will give
% warnings like 'info: atom does not occur in any rule head'.
#defined variant_default_value/3.
#defined variant_default_value_from_packages_yaml/3.
#defined variant_default_value_from_package_py/3.
%-----------------------------------------------------------------------------
% Propagation semantics
%-----------------------------------------------------------------------------
non_default_propagation(variant_value(Name, Value)) :- attr("propagate", RootNode, variant_value(Name, Value)).
% Propagation roots have a corresponding attr("propagate", ...)
propagate(RootNode, PropagatedAttribute) :- attr("propagate", RootNode, PropagatedAttribute), not non_default_propagation(PropagatedAttribute).
propagate(RootNode, PropagatedAttribute, EdgeTypes) :- attr("propagate", RootNode, PropagatedAttribute, EdgeTypes).
% Special case variants, to inject the source node in the propagated attribute
propagate(RootNode, variant_value(Name, Value, RootNode)) :- attr("propagate", RootNode, variant_value(Name, Value)).
% Propagate an attribute along edges to child nodes
propagate(ChildNode, PropagatedAttribute) :-
propagate(ParentNode, PropagatedAttribute),
depends_on(ParentNode, ChildNode).
propagate(ChildNode, PropagatedAttribute, edge_types(DepType1, DepType2)) :-
propagate(ParentNode, PropagatedAttribute, edge_types(DepType1, DepType2)),
depends_on(ParentNode, ChildNode),
1 { attr("depends_on", ParentNode, ChildNode, DepType1); attr("depends_on", ParentNode, ChildNode, DepType2) }.
%-----------------------------------------------------------------------------
% Activation of propagated values
%-----------------------------------------------------------------------------
%----
% Variants
%----
% If a variant is propagated, and can be accepted, set its value
attr("variant_selected", PackageNode, Variant, Value, VariantType, VariantID) :-
propagate(PackageNode, variant_value(Variant, Value, _)),
node_has_variant(PackageNode, Variant, VariantID),
variant_type(VariantID, VariantType),
variant_possible_value(PackageNode, Variant, Value).
% If a variant is propagated, we cannot have extraneous values
variant_is_propagated(PackageNode, Variant) :-
attr("variant_value", PackageNode, Variant, Value),
propagate(PackageNode, variant_value(Variant, Value, _)),
not attr("variant_set", PackageNode, Variant).
:- variant_is_propagated(PackageNode, Variant),
attr("variant_selected", PackageNode, Variant, Value, _, _),
not propagate(PackageNode, variant_value(Variant, Value, _)).
error(100, "{0} and {1} cannot both propagate variant '{2}' to the shared dependency: {3}",
Package1, Package2, Variant, Dependency) :-
% The variant is a singlevalued variant
variant_single_value(node(X, Package1), Variant),
% Dependency is trying to propagate Variant with different values and is not the source package
propagate(node(Z, Dependency), variant_value(Variant, Value1, node(X, Package1))),
propagate(node(Z, Dependency), variant_value(Variant, Value2, node(Y, Package2))),
% Package1 and Package2 and their values are different
Package1 > Package2, Value1 != Value2,
not propagate(node(Z, Dependency), variant_value(Variant, _, node(Z, Dependency))).
% Cannot propagate the same variant from two different packages if one is a dependency of the other
error(100, "{0} and {1} cannot both propagate variant '{2}'", Package1, Package2, Variant) :-
% The variant is a single-valued variant
variant_single_value(node(X, Package1), Variant),
% Package1 and Package2 and their values are different
Package1 != Package2, Value1 != Value2,
% Package2 is set to propagate the value from Package1
propagate(node(Y, Package2), variant_value(Variant, Value2, node(X, Package2))),
propagate(node(Y, Package2), variant_value(Variant, Value1, node(X, Package1))),
variant_is_propagated(node(Y, Package2), Variant).
% Cannot propagate a variant if a different value was set for it in a dependency
error(100, "Cannot propagate the variant '{0}' from the package: {1} because package: {2} is set to exclude it", Variant, Source, Package) :-
% Package has a Variant and Source is propagating Variant
attr("variant_set", node(X, Package), Variant, Value1),
% The packages and values are different
Source != Package, Value1 != Value2,
% The variant is a single-valued variant
variant_single_value(node(X, Package1), Variant),
% A different value is being propagated from somewhere else
propagate(node(X, Package), variant_value(Variant, Value2, node(Y, Source))).
%----
% Flags
%----
% A propagated flag implies:
% 1. The same flag type is not set on this node
% 2. This node has the same compilers as the propagation source
node_compiler(node(X, Package), node(Y, Compiler)) :- node_compiler(node(X, Package), node(Y, Compiler), Language).
node_compiler(node(X, Package), node(Y, Compiler), Language) :-
attr("virtual_on_edge", node(X, Package), node(Y, Compiler), Language),
compiler(Compiler), language(Language).
propagated_flag(node(PackageID, Package), node_flag(FlagType, Flag, FlagGroup, Source), SourceNode) :-
propagate(node(PackageID, Package), node_flag(FlagType, Flag, FlagGroup, Source), _),
not attr("node_flag_set", node(PackageID, Package), node_flag(FlagType, _, _, "literal")),
% Same compilers as propagation source
node_compiler(node(PackageID, Package), CompilerNode, Language) : node_compiler(SourceNode, CompilerNode, Language);
attr("propagate", SourceNode, node_flag(FlagType, Flag, FlagGroup, Source), _),
node(PackageID, Package) != SourceNode,
not runtime(Package).
attr("node_flag", PackageNode, NodeFlag) :- propagated_flag(PackageNode, NodeFlag, _).
% Cannot propagate the same flag from two distinct sources
error(100, "{0} and {1} cannot both propagate compiler flags '{2}' to {3}", Source1, Source2, FlagType, Package) :-
propagated_flag(node(ID, Package), node_flag(FlagType, _, _, _), node(_, Source1)),
propagated_flag(node(ID, Package), node_flag(FlagType, _, _, _), node(_, Source2)),
Source1 < Source2.
%----
% Compiler constraints
%----
% If a node is built, impose constraints on the compiler coming from dependents
attr("node_version_satisfies", node(Y, Compiler), VersionRange) :-
propagate(node(X, Package), node_version_satisfies(Compiler, VersionRange)),
attr("depends_on", node(X, Package), node(Y, Compiler), "build"),
not external(node(X, Package)),
not runtime(Package).
attr("node_version_satisfies", node(X, Runtime), VersionRange) :-
attr("node", node(X, Runtime)),
attr("compatible_runtime", PackageNode, Runtime, VersionRange),
concrete(PackageNode).
% If a compiler package is depended on with type link, it's used as a library
compiler_used_as_a_library(node(X, Child), Hash) :-
concrete(node(X, Child)),
attr("hash", node(X, Child), Hash),
compiler_package(Child), % Used to restrict grounding for this rule
attr("depends_on", _, node(X, Child), "link").
% If a compiler is used for C on a package, it must provide C++ too, if need be, and vice-versa
:- attr("virtual_on_edge", PackageNode, CompilerNode1, "c"),
attr("virtual_on_edge", PackageNode, CompilerNode2, "cxx"),
CompilerNode1 != CompilerNode2.
%-----------------------------------------------------------------------------
% Runtimes
%-----------------------------------------------------------------------------
% Check whether the DAG has any built package
has_built_packages() :- build(X), not external(X).
% "gcc-runtime" is always built
:- concrete(node(X, "gcc-runtime")), has_built_packages().
% The "gcc" linked to "gcc-runtime" must be used by at least another package
:- attr("depends_on", node(X, "gcc-runtime"), node(Y, "gcc"), "build"),
node_compiler(_, node(_, "gcc")),
not 2 { attr("depends_on", PackageNode, node(Y, "gcc"), "build") : attr("node", PackageNode) }.
%-----------------------------------------------------------------------------
% Platform semantics
%-----------------------------------------------------------------------------
% NOTE: Currently we have a single allowed platform per DAG, therefore there is no
% need to have additional optimization criteria. If we ever add cross-platform dags,
% this needs to be changed.
:- 2 { allowed_platform(Platform) }, internal_error("More than one allowed platform detected").
1 { attr("node_platform", PackageNode, Platform) : allowed_platform(Platform) } 1
:- attr("node", PackageNode).
% setting platform on a node is a hard constraint
attr("node_platform", PackageNode, Platform)
:- attr("node", PackageNode), attr("node_platform_set", PackageNode, Platform).
% platform is set if set to anything
attr("node_platform_set", PackageNode) :- attr("node_platform_set", PackageNode, _).
%-----------------------------------------------------------------------------
% OS semantics
%-----------------------------------------------------------------------------
% convert weighted OS declarations to simple one
os(OS) :- os(OS, _).
% one os per node
{ attr("node_os", PackageNode, OS) : os(OS) } :- attr("node", PackageNode).
% can't have a non-buildable OS on a node we need to build
error(100, "Cannot select '{0} os={1}' (operating system '{1}' is not buildable)", Package, OS)
:- build(node(X, Package)),
attr("node_os", node(X, Package), OS),
not buildable_os(OS).
% give OS choice weights according to os declarations
node_os_weight(PackageNode, Weight)
:- attr("node", PackageNode),
attr("node_os", PackageNode, OS),
os(OS, Weight).
% every OS is compatible with itself. We can use `os_compatible` to declare
os_compatible(OS, OS) :- os(OS).
% Transitive compatibility among operating systems
os_compatible(OS1, OS3) :- os_compatible(OS1, OS2), os_compatible(OS2, OS3).
% If an OS is set explicitly respect the value
attr("node_os", PackageNode, OS) :- attr("node_os_set", PackageNode, OS), attr("node", PackageNode).
#defined os_compatible/2.
%-----------------------------------------------------------------------------
% Target semantics
%-----------------------------------------------------------------------------
% Each node has only one target chosen among the known targets
{ attr("node_target", PackageNode, Target) : target(Target) } :- attr("node", PackageNode).
% If a node must satisfy a target constraint, enforce it
error(10, "'{0} target={1}' cannot satisfy constraint 'target={2}'", Package, Target, Constraint)
:- attr("node_target", node(X, Package), Target),
attr("node_target_satisfies", node(X, Package), Constraint),
not target_satisfies(Constraint, Target).
% If a node has a target and the target satisfies a constraint, then the target
% associated with the node satisfies the same constraint
attr("node_target_satisfies", PackageNode, Constraint)
:- attr("node_target", PackageNode, Target), target_satisfies(Constraint, Target).
% If a node has a target, all of its dependencies must be compatible with that target
error(100, "Cannot find compatible targets for {0} and {1}", Package, Dependency)
:- attr("depends_on", node(X, Package), node(Y, Dependency), Type), Type != "build",
attr("node_target", node(X, Package), Target),
not node_target_compatible(node(Y, Dependency), Target).
% Intermediate step for performance reasons
% When the integrity constraint above was formulated including this logic
% we suffered a substantial performance penalty
node_target_compatible(PackageNode, Target)
:- attr("node_target", PackageNode, MyTarget),
target_compatible(Target, MyTarget).
#defined target_satisfies/2.
compiler(Compiler) :- compiler_supports_target(Compiler, _, _).
% Can't use targets on node if the compiler for the node doesn't support them
language("c").
language("cxx").
language("fortran").
language_runtime("fortran-rt").
error(10, "Only external, or concrete, compilers are allowed for the {0} language", Language)
:- provider(ProviderNode, node(_, Language)),
language(Language),
not external(ProviderNode),
not concrete(ProviderNode).
error(10, "{0} compiler '{2}@{3}' incompatible with 'target={1}'", Package, Target, Compiler, Version)
:- attr("node_target", node(X, Package), Target),
node_compiler(node(X, Package), node(Y, Compiler)),
attr("version", node(Y, Compiler), Version),
not compiler_supports_target(Compiler, Version, Target),
build(node(X, Package)).
#defined compiler_supports_target/2.
% if a target is set explicitly, respect it
attr("node_target", PackageNode, Target)
:- attr("node", PackageNode), attr("node_target_set", PackageNode, Target).
node_target_weight(PackageNode, MinWeight)
:- attr("node", PackageNode),
attr("node_target", PackageNode, Target),
target(Target),
MinWeight = #min { Weight : target_weight(Target, Weight) }.
:- attr("node_target", PackageNode, Target), not node_target_weight(PackageNode, _).
% compatibility rules for targets among nodes
node_target_match(ParentNode, DependencyNode)
:- attr("depends_on", ParentNode, DependencyNode, Type), Type != "build",
attr("node_target", ParentNode, Target),
attr("node_target", DependencyNode, Target).
node_target_mismatch(ParentNode, DependencyNode)
:- attr("depends_on", ParentNode, DependencyNode, Type), Type != "build",
not node_target_match(ParentNode, DependencyNode).
% disallow reusing concrete specs that don't have a compatible target
error(100, "'{0} target={1}' is not compatible with this machine", Package, Target)
:- attr("node", node(X, Package)),
attr("node_target", node(X, Package), Target),
not target(Target).
%-----------------------------------------------------------------------------
% Compiler flags
%-----------------------------------------------------------------------------
attr("node_flag", PackageNode, NodeFlag) :- attr("node_flag_set", PackageNode, NodeFlag).
%-----------------------------------------------------------------------------
% Installed Packages
%-----------------------------------------------------------------------------
#defined installed_hash/2.
#defined abi_splice_conditions_hold/4.
% These are the previously concretized attributes of the installed package as
% a hash. It has the general form:
% hash_attr(Hash, Attribute, PackageName, Args*)
#defined hash_attr/3.
#defined hash_attr/4.
#defined hash_attr/5.
#defined hash_attr/6.
#defined hash_attr/7.
{ attr("hash", node(ID, PackageName), Hash): installed_hash(PackageName, Hash) } 1 :-
attr("node", node(ID, PackageName)),
internal_error("Package must resolve to at most 1 hash").
% you can't choose an installed hash for a dev spec
:- attr("hash", PackageNode, Hash), attr("variant_value", PackageNode, "dev_path", _).
% You can't install a hash, if it is not installed
:- attr("hash", node(ID, Package), Hash), not installed_hash(Package, Hash).
% hash_attrs are versions, but can_splice_attr are usually node_version_satisfies
hash_attr(Hash, "node_version_satisfies", PackageName, Constraint) :-
hash_attr(Hash, "version", PackageName, Version),
pkg_fact(PackageName, version_satisfies(Constraint, Version)).
% This recovers the exact semantics for hash reuse hash and depends_on are where
% splices are decided, and virtual_on_edge can result in name-changes, which is
% why they are all treated separately.
imposed_constraint(Hash, Attr, PackageName) :- hash_attr(Hash, Attr, PackageName), Attr != "virtual_node".
imposed_constraint(Hash, Attr, PackageName, A1) :- hash_attr(Hash, Attr, PackageName, A1), Attr != "hash".
imposed_constraint(Hash, Attr, PackageName, A1, A2) :-
hash_attr(Hash, Attr, PackageName, A1, A2),
Attr != "depends_on",
Attr != "virtual_on_edge".
imposed_constraint(Hash, Attr, PackageName, A1, A2, A3) :- hash_attr(Hash, Attr, PackageName, A1, A2, A3).
imposed_constraint(Hash, "hash", PackageName, Hash) :- installed_hash(PackageName, Hash).
% If a compiler is not used as a library, we just enforce "run" dependency, so we
% can get by with a much smaller search space.
avoid_link_dependency(Hash, DepName) :-
hash_attr(Hash, "depends_on", PackageName, DepName, "link"),
not hash_attr(Hash, "depends_on", PackageName, DepName, "run"),
hash_attr(Hash, "hash", DepName, DepHash),
compiler_package(PackageName),
not compiler_used_as_a_library(node(_, PackageName), Hash).
% Without splicing, we simply recover the exact semantics
imposed_constraint(ParentHash, "hash", ChildName, ChildHash) :-
hash_attr(ParentHash, "hash", ChildName, ChildHash),
ChildHash != ParentHash,
not avoid_link_dependency(ParentHash, ChildName),
not abi_splice_conditions_hold(_, _, ChildName, ChildHash).
imposed_constraint(Hash, "depends_on", PackageName, DepName, Type) :-
hash_attr(Hash, "depends_on", PackageName, DepName, Type),
hash_attr(Hash, "hash", DepName, DepHash),
not avoid_link_dependency(Hash, DepName),
not attr("splice_at_hash", _, _, DepName, DepHash).
imposed_constraint(Hash, "virtual_on_edge", PackageName, DepName, VirtName) :-
hash_attr(Hash, "virtual_on_edge", PackageName, DepName, VirtName),
not avoid_link_dependency(Hash, DepName),
not attr("splice_at_hash", _, _, DepName,_).
imposed_constraint(Hash, "virtual_node", VirtName) :-
hash_attr(Hash, "virtual_on_edge", PackageName, DepName, VirtName),
hash_attr(Hash, "virtual_node", VirtName),
not avoid_link_dependency(Hash, DepName),
not attr("splice_at_hash", _, _, DepName,_).
% Rules pertaining to attr("splice_at_hash") and abi_splice_conditions_hold will
% be conditionally loaded from splices.lp
impose(Hash, PackageNode) :- attr("hash", PackageNode, Hash), attr("node", PackageNode).
% If there is not a hash for a package, we build it.
build(PackageNode) :- attr("node", PackageNode), not concrete(PackageNode).
% Minimizing builds is tricky. We want a minimizing criterion
% because we want to reuse what is avaialble, but
% we also want things that are built to stick to *default preferences* from
% the package and from the user. We therefore treat built specs differently and apply
% a different set of optimization criteria to them. Spack's *first* priority is to
% reuse what it *can*, but if it builds something, the built specs will respect
% defaults and preferences. This is implemented by bumping the priority of optimization
% criteria for built specs -- so that they take precedence over the otherwise
% topmost-priority criterion to reuse what is installed.
%
% The priority ranges are:
% 1000+ Optimizations for concretization errors
% 300 - 1000 Highest priority optimizations for valid solutions
% 200 - 299 Shifted priorities for build nodes; correspond to priorities 0 - 99.
% 100 - 199 Unshifted priorities. Currently only includes minimizing #builds and minimizing dupes.
% 0 - 99 Priorities for non-built nodes.
build_priority(PackageNode, 200) :- build(PackageNode), attr("node", PackageNode).
build_priority(PackageNode, 0) :- not build(PackageNode), attr("node", PackageNode).
% don't assign versions from installed packages unless reuse is enabled
% NOTE: that "installed" means the declared version was only included because
% that package happens to be installed, NOT because it was asked for on the
% command line. If the user specifies a hash, the origin will be "spec".
%
% TODO: There's a slight inconsistency with this: if the user concretizes
% and installs `foo ^bar`, for some build dependency `bar`, and then later
% does a `spack install --fresh foo ^bar/abcde` (i.e.,the hash of `bar`, it
% currently *won't* force versions for `bar`'s build dependencies -- `--fresh`
% will instead build the latest bar. When we actually include transitive
% build deps in the solve, consider using them as a preference to resolve this.
:- attr("version", node(ID, Package), Version),
version_weight(node(ID, Package), Weight),
pkg_fact(Package, version_declared(Version, Weight, "installed")),
not optimize_for_reuse().
% This statement, which is a hidden feature of clingo, let us avoid cycles in the DAG
#edge (A, B) : depends_on(A, B).
%-----------------------------------------------------------------
% Optimization to avoid errors
%-----------------------------------------------------------------
% Some errors are handled as rules instead of constraints because
% it allows us to explain why something failed.
#minimize{ 0@1000: #true}.
#minimize{ Weight@1000,Msg: error(Weight, Msg) }.
#minimize{ Weight@1000,Msg,Arg1: error(Weight, Msg, Arg1) }.
#minimize{ Weight@1000,Msg,Arg1,Arg2: error(Weight, Msg, Arg1, Arg2) }.
#minimize{ Weight@1000,Msg,Arg1,Arg2,Arg3: error(Weight, Msg, Arg1, Arg2, Arg3) }.
#minimize{ Weight@1000,Msg,Arg1,Arg2,Arg3,Arg4: error(Weight, Msg, Arg1, Arg2, Arg3, Arg4) }.
%-----------------------------------------------------------------------------
% How to optimize the spec (high to low priority)
%-----------------------------------------------------------------------------
% Each criterion below has:
% 1. an opt_criterion(ID, Name) fact that describes the criterion, and
% 2. a `#minimize{ 0@2 : #true }.` statement that ensures the criterion
% is displayed (clingo doesn't display sums over empty sets by default)
% A condition group specifies one or more specs that must be satisfied.
% Specs declared first are preferred, so we assign increasing weights and
% minimize the weights.
opt_criterion(310, "requirement weight").
#minimize{ 0@310: #true }.
#minimize {
Weight@310,PackageNode,Group
: requirement_weight(PackageNode, Group, Weight)
}.
% Try hard to reuse installed packages (i.e., minimize the number built)
opt_criterion(110, "number of packages to build (vs. reuse)").
#minimize { 0@110: #true }.
#minimize { 1@110,PackageNode : build(PackageNode) }.
opt_criterion(100, "number of nodes from the same package").
#minimize { 0@100: #true }.
#minimize { ID@100,Package : attr("node", node(ID, Package)) }.
#minimize { ID@100,Package : attr("virtual_node", node(ID, Package)) }.
#defined optimize_for_reuse/0.
% Minimize the number of deprecated versions being used
opt_criterion(73, "deprecated versions used").
#minimize{ 0@273: #true }.
#minimize{ 0@73: #true }.
#minimize{
1@73+Priority,PackageNode
: attr("deprecated", PackageNode, _),
not external(PackageNode),
build_priority(PackageNode, Priority)
}.
% Minimize the:
% 1. Version weight
% 2. Number of variants with a non default value, if not set
% for the root package.
opt_criterion(70, "version badness (roots)").
#minimize{ 0@270: #true }.
#minimize{ 0@70: #true }.
#minimize {
Weight@70+Priority,PackageNode
: attr("root", PackageNode),
version_weight(PackageNode, Weight),
build_priority(PackageNode, Priority)
}.
opt_criterion(65, "number of non-default variants (roots)").
#minimize{ 0@265: #true }.
#minimize{ 0@65: #true }.
#minimize {
1@65+Priority,PackageNode,Variant,Value
: variant_not_default(PackageNode, Variant, Value),
attr("root", PackageNode),
build_priority(PackageNode, Priority)
}.
opt_criterion(60, "preferred providers for roots").
#minimize{ 0@260: #true }.
#minimize{ 0@60: #true }.
#minimize{
Weight@60+Priority,ProviderNode,X,Virtual
: provider_weight(ProviderNode, node(X, Virtual), Weight),
attr("root", ProviderNode), not language(Virtual), not language_runtime(Virtual),
build_priority(ProviderNode, Priority)
}.
opt_criterion(55, "default values of variants not being used (roots)").
#minimize{ 0@255: #true }.
#minimize{ 0@55: #true }.
#minimize{
1@55+Priority,PackageNode,Variant,Value
: variant_default_not_used(PackageNode, Variant, Value),
attr("root", PackageNode),
build_priority(PackageNode, Priority)
}.
% Try to use default variants or variants that have been set
opt_criterion(50, "number of non-default variants (non-roots)").
#minimize{ 0@250: #true }.
#minimize{ 0@50: #true }.
#minimize {
1@50+Priority,PackageNode,Variant,Value
: variant_not_default(PackageNode, Variant, Value),
not attr("root", PackageNode),
build_priority(PackageNode, Priority)
}.
% Minimize the weights of the providers, i.e. use as much as
% possible the most preferred providers
opt_criterion(48, "preferred providers (non-roots)").
#minimize{ 0@248: #true }.
#minimize{ 0@48: #true }.
#minimize{
Weight@48+Priority,ProviderNode,X,Virtual
: provider_weight(ProviderNode, node(X, Virtual), Weight),
not attr("root", ProviderNode), not language(Virtual), not language_runtime(Virtual),
build_priority(ProviderNode, Priority)
}.
% Minimize the number of compilers used on nodes
compiler_penalty(PackageNode, C-1) :-
C = #count { CompilerNode : node_compiler(PackageNode, CompilerNode) },
node_compiler(PackageNode, _).
opt_criterion(46, "number of compilers used on the same node").
#minimize{ 0@246: #true }.
#minimize{ 0@46: #true }.
#minimize{
Penalty@46+Priority,PackageNode
: compiler_penalty(PackageNode, Penalty), build_priority(PackageNode, Priority)
}.
% Minimize the ids of the providers, i.e. use as much as
% possible the first providers
opt_criterion(45, "number of duplicate virtuals needed").
#minimize{ 0@245: #true }.
#minimize{ 0@45: #true }.
#minimize{
Weight@45+Priority,ProviderNode,Virtual
: provider(ProviderNode, node(Weight, Virtual)),
build_priority(ProviderNode, Priority)
}.
opt_criterion(40, "preferred compilers").
#minimize{ 0@240: #true }.
#minimize{ 0@40: #true }.
#minimize{
Weight@40+Priority,ProviderNode,X,Virtual
: provider_weight(ProviderNode, node(X, Virtual), Weight),
language(Virtual),
build_priority(ProviderNode, Priority)
}.
opt_criterion(30, "non-preferred OS's").
#minimize{ 0@230: #true }.
#minimize{ 0@30: #true }.
#minimize{
Weight@30+Priority,PackageNode
: node_os_weight(PackageNode, Weight),
build_priority(PackageNode, Priority)
}.
% Choose more recent versions for nodes
opt_criterion(25, "version badness (non roots)").
#minimize{ 0@225: #true }.
#minimize{ 0@25: #true }.
#minimize{
Weight@25+Priority,node(X, Package)
: version_weight(node(X, Package), Weight),
build_priority(node(X, Package), Priority),
not attr("root", node(X, Package)),
not runtime(Package)
}.
% Try to use all the default values of variants
opt_criterion(20, "default values of variants not being used (non-roots)").
#minimize{ 0@220: #true }.
#minimize{ 0@20: #true }.
#minimize{
1@20+Priority,PackageNode,Variant,Value
: variant_default_not_used(PackageNode, Variant, Value),
not attr("root", PackageNode),
build_priority(PackageNode, Priority)
}.
% Minimize the number of mismatches for targets in the DAG, try
% to select the preferred target.
opt_criterion(10, "target mismatches").
#minimize{ 0@210: #true }.
#minimize{ 0@10: #true }.
#minimize{
1@10+Priority,PackageNode,node(ID, Dependency)
: node_target_mismatch(PackageNode, node(ID, Dependency)),
build_priority(node(ID, Dependency), Priority),
not runtime(Dependency)
}.
opt_criterion(5, "non-preferred targets").
#minimize{ 0@205: #true }.
#minimize{ 0@5: #true }.
#minimize{
Weight@5+Priority,node(X, Package)
: node_target_weight(node(X, Package), Weight),
build_priority(node(X, Package), Priority),
not runtime(Package)
}.
opt_criterion(4, "preferred providers (language runtimes)").
#minimize{ 0@204: #true }.
#minimize{ 0@4: #true }.
#minimize{
Weight@4+Priority,ProviderNode,X,Virtual
: provider_weight(ProviderNode, node(X, Virtual), Weight),
language_runtime(Virtual),
build_priority(ProviderNode, Priority)
}.
% Choose more recent versions for runtimes
opt_criterion(3, "version badness (runtimes)").
#minimize{ 0@203: #true }.
#minimize{ 0@3: #true }.
#minimize{
Weight@3,node(X, Package)
: version_weight(node(X, Package), Weight),
runtime(Package)
}.
% Choose best target for runtimes
opt_criterion(2, "non-preferred targets (runtimes)").
#minimize{ 0@202: #true }.
#minimize{ 0@2: #true }.
#minimize{
Weight@2,node(X, Package)
: node_target_weight(node(X, Package), Weight),
runtime(Package)
}.
% Choose more recent versions for nodes
opt_criterion(1, "edge wiring").
#minimize{ 0@201: #true }.
#minimize{ 0@1: #true }.
#minimize{
Weight@1,ParentNode,PackageNode
: version_weight(PackageNode, Weight),
not attr("root", PackageNode),
depends_on(ParentNode, PackageNode)
}.
#minimize{ 0@201: #true }.
#minimize{ 0@1: #true }.
#minimize{
Weight@1,ParentNode,ProviderNode,Virtual
: provider_weight(ProviderNode, Virtual, Weight),
not attr("root", ProviderNode),
depends_on(ParentNode, ProviderNode)
}.
%-----------
% Notes
%-----------
% [1] Clingo ensures a total ordering among all atoms. We rely on that total ordering
% to reduce symmetry in the solution by checking `<` instead of `!=` in symmetric
% cases. These choices are made without loss of generality.