newt/deprepo/deprepo.go (236 lines of code) (raw):

/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ // deprepo: Package for resolving repo dependencies. package deprepo import ( "fmt" "sort" "strings" log "github.com/sirupsen/logrus" "mynewt.apache.org/newt/newt/newtutil" "mynewt.apache.org/newt/newt/repo" "mynewt.apache.org/newt/util" ) // [repo-name] => repo type RepoMap map[string]*repo.Repo // [repo-name] => repo-version type VersionMap map[string]newtutil.RepoVersion // [repo-name] => requirements-for-key-repo type RequirementMap map[string]newtutil.RepoVersion type ConflictEntry struct { Dependent RVPair DependeeVer newtutil.RepoVersion } // Indicates dependencies on different versions of the same repo. type Conflict struct { DependeeName string Entries []ConflictEntry } func (c *Conflict) SortEntries() { sort.Slice(c.Entries, func(i int, j int) bool { ci := c.Entries[i] cj := c.Entries[j] x := CompareRVPairs(ci.Dependent, cj.Dependent) if x != 0 { return x < 0 } return newtutil.CompareRepoVersions(ci.DependeeVer, cj.DependeeVer) < 0 }) } // Returns a sorted slice of all constituent repo names. func (vm VersionMap) SortedNames() []string { names := make([]string, 0, len(vm)) for name, _ := range vm { names = append(names, name) } sort.Strings(names) return names } // Returns a slice of all constituent repos, sorted by name. func (rm RepoMap) Sorted() []*repo.Repo { names := make([]string, 0, len(rm)) for n, _ := range rm { names = append(names, n) } sort.Strings(names) repos := make([]*repo.Repo, len(names)) for i, n := range names { repos[i] = rm[n] } return repos } func (vm VersionMap) String() string { s := "" names := vm.SortedNames() for _, name := range names { ver := vm[name] if len(s) > 0 { s += "\n" } s += fmt.Sprintf("%s:%s", name, ver.String()) } return s } // Builds a repo dependency graph from the repo requirements expressed in the // `project.yml` file. func BuildDepGraph(repos RepoMap, rootReqs RequirementMap) (DepGraph, error) { dg := DepGraph{} // First, add the hard dependencies expressed in `project.yml`. for repoName, verReq := range rootReqs { repo := repos[repoName] normalizedReq, err := repo.NormalizeVerReq(verReq) if err != nil { return nil, err } if err := dg.AddRootDep(repoName, normalizedReq); err != nil { return nil, err } } // Add inter-repo dependencies to the graph. for _, r := range repos.Sorted() { nvers, err := r.NormalizedVersions() if err != nil { return nil, err } for _, v := range nvers { deps := r.DepsForVersion(v) reqMap := RequirementMap{} for _, d := range deps { depRepo := repos[d.Name] verReqs, err := depRepo.NormalizeVerReq(d.VerReqs) if err != nil { return nil, err } reqMap[d.Name] = verReqs } if err := dg.AddRepoVer(r.Name(), v, reqMap); err != nil { return nil, err } } } return dg, nil } // PruneDepGraph removes all entries from a depgraph that aren't in the given // repo slice. This is necessary when the user wants to upgrade only a subset // of repos in the project. func PruneDepGraph(dg DepGraph, keep []*repo.Repo) { for k, _ := range dg { // The empty string indicates a `project.yml` requirement. Always // keep these. if k.Name != "" { found := false for _, r := range keep { if k.Name == r.Name() { found = true break } } if !found { delete(dg, k) } } } } // Produces an error describing the specified set of repo conflicts. func ConflictError(conflicts []Conflict) error { s := "" for _, c := range conflicts { if s != "" { s += "\n\n" } s += fmt.Sprintf(" Installation of repo \"%s\" is blocked:", c.DependeeName) lines := []string{} for _, e := range c.Entries { dependee := RVPair{ Name: c.DependeeName, Ver: e.DependeeVer, } lines = append(lines, fmt.Sprintf("\n %30s requires %s", e.Dependent.String(), dependee.String())) } sort.Strings(lines) s += strings.Join(lines, "") } return util.NewNewtError("Repository conflicts:\n" + s) } // ResolveRepoDeps calculates the set of repo versions a project should be // upgraded to. // // dg: The project's repo dependency graph. This includes root dependencies // (i.e., dependencies expressed in `project.yml`). // // Returns a version map on success; a set of conflicts on failure. func ResolveRepoDeps(dg DepGraph) (VersionMap, []Conflict) { // Represents an entry in the working set. type WSNode struct { highPrio bool // If true, always "wins" without conflict. dependent RVPair // Repo that expresses this dependency. dependee RVPair // Repo that is depended on. } ws := map[string]WSNode{} // Working set (key=dependent). cm := map[string]*Conflict{} // Conflict map (key=dependee). visited := map[string]struct{}{} // Tracks which nodes have been visited. // Updates (or inserts a new) conflict object into the conflict map (cm). addConflict := func(dependent RVPair, dependee RVPair) { c := cm[dependee.Name] if c == nil { wsn := ws[dependee.Name] c = &Conflict{ DependeeName: repoNameString(dependee.Name), Entries: []ConflictEntry{ ConflictEntry{ Dependent: RVPair{ Name: repoNameString(wsn.dependent.Name), Ver: wsn.dependent.Ver, }, DependeeVer: wsn.dependee.Ver, }, }, } cm[dependee.Name] = c } c.Entries = append(c.Entries, ConflictEntry{ Dependent: dependent, DependeeVer: dependee.Ver, }) } // Attempts to add a single node to the working set. In case of a // conflict, the conflict map is updated instead. addWsNode := func(dependent RVPair, dependee RVPair) { old, ok := ws[dependee.Name] if !ok { // This is the first time we've seen this repo. ws[dependee.Name] = WSNode{ highPrio: false, dependent: dependent, dependee: dependee, } return } if newtutil.CompareRepoVersions(old.dependee.Ver, dependee.Ver) == 0 { // We have already seen this exact dependency. Ignore the // duplicate. return } // There is already a dependency for a different version of this repo. // Handle the conflict. if old.highPrio { // Root commit dependencies take priority over all others. log.Debugf("discarding repo dependency in favor "+ "of root override: dep=%s->%s root=%s->%s", dependent.String(), dependee.String(), old.dependent.String(), old.dependee.String()) } else { addConflict(dependent, dependee) } } // Adds one entry to the working set (ws) for each of the given repo's // dependencies. visit := func(rvp RVPair) { nodes := dg[rvp] for _, node := range nodes { addWsNode(rvp, node) } visited[rvp.Name] = struct{}{} } // Insert all the root dependencies (dependencies in `project.yml`) into // the working set. A root dependency is "high priority" if it points to a // git commit rather than a version number. rootDeps := dg[RVPair{}] for _, dep := range rootDeps { ws[dep.Name] = WSNode{ highPrio: dep.Ver.Commit != "", dependent: RVPair{Name: rootDependencyName}, dependee: dep, } } // Repeatedly iterate through the working set, visiting each unvisited // node. Stop looping when an iteration produces no new nodes. for len(visited) < len(ws) { // Iterate the working set in a consistent order. The order doesn't // matter for well-defined projects. For invalid projects, different // iteration orders can result in different conflicts being reported. names := make([]string, 0, len(ws)) for name, _ := range ws { names = append(names, name) } sort.Strings(names) for _, name := range names { wsn := ws[name] if _, ok := visited[name]; !ok { visit(wsn.dependee) } } } // It is an error if any conflicts were detected. if len(cm) > 0 { var cs []Conflict for _, c := range cm { c.SortEntries() cs = append(cs, *c) } sort.Slice(cs, func(i int, j int) bool { return strings.Compare(cs[i].DependeeName, cs[j].DependeeName) < 0 }) return nil, cs } // The working set now contains the target version of each repo in the // project. vm := VersionMap{} for name, wsn := range ws { vm[name] = wsn.dependee.Ver } return vm, nil }