75 lines
3.1 KiB
Go
Executable File
75 lines
3.1 KiB
Go
Executable File
// Sub-Dependency Solver.
|
|
//
|
|
// In the original Source Mage, SUB_DEPENDS expresses the idea that
|
|
// "spell A needs spell B built with feature X". Sorcery-Go encodes that
|
|
// requirement on the dag.Edge.Features field and the Solver checks whether the
|
|
// currently-active Essence for the dependency satisfies the requested feature set.
|
|
//
|
|
// If not, the Solver returns a ReForge recommendation that the Cast pipeline
|
|
// uses to enqueue a fresh variant of the dependency (with the new flag) before
|
|
// continuing the parent build.
|
|
package dag
|
|
|
|
import "fmt"
|
|
|
|
// ReForge is a recommendation produced by the Solver when an existing
|
|
// Essence does not satisfy the requested sub-depends features.
|
|
type ReForge struct {
|
|
Spell string
|
|
NeededBy string
|
|
MissingFlags []string
|
|
}
|
|
|
|
// Solver is the feature-aware dependency negotiator.
|
|
//
|
|
// It relies on a FeatureLookup callback to ask the Tomb whether a given
|
|
// spell's current Essence variant exposes a feature. This keeps the dag
|
|
// package free of import cycles with pkg/tomb.
|
|
type Solver struct {
|
|
Lookup func(spell, feature string) (present bool, err error)
|
|
}
|
|
|
|
// Solve inspects every required edge of `spell` and returns the list of
|
|
// re-forge recommendations. An empty list means the existing variants are
|
|
// already compatible and the cast can proceed.
|
|
func (s *Solver) Solve(spell string, g *Graph) ([]ReForge, error) {
|
|
return s.solveVisited(spell, g, make(map[string]struct{}))
|
|
}
|
|
|
|
// solveVisited is the recursive implementation with cycle detection.
|
|
// The visited map prevents infinite recursion on circular dependency graphs.
|
|
func (s *Solver) solveVisited(spell string, g *Graph, visited map[string]struct{}) ([]ReForge, error) {
|
|
if _, seen := visited[spell]; seen {
|
|
return nil, nil // cycle detected — skip, don't recurse further
|
|
}
|
|
visited[spell] = struct{}{}
|
|
|
|
root, ok := g.Nodes[spell]
|
|
if !ok {
|
|
return nil, ErrSpellNotFound
|
|
}
|
|
var out []ReForge
|
|
for _, edge := range root.Edges {
|
|
if edge.Type == OptionalDep && !edge.Enabled {
|
|
continue
|
|
}
|
|
for _, feature := range edge.Features {
|
|
present, err := s.Lookup(edge.Target.Name, feature)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("solver: lookup %s/%s: %w", edge.Target.Name, feature, err)
|
|
}
|
|
if !present {
|
|
out = append(out, ReForge{
|
|
Spell: edge.Target.Name,
|
|
NeededBy: spell,
|
|
MissingFlags: []string{feature},
|
|
})
|
|
}
|
|
}
|
|
// Recurse into children so a transitive missing flag also triggers.
|
|
if more, err := s.solveVisited(edge.Target.Name, g, visited); err == nil {
|
|
out = append(out, more...)
|
|
}
|
|
}
|
|
return out, nil
|
|
} |