lock.go

Summary

Maintainability
A
0 mins
Test Coverage
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package dep

import (
    "bytes"
    "io"
    "sort"

    "github.com/golang/dep/gps"
    "github.com/golang/dep/gps/verify"
    "github.com/pelletier/go-toml"
    "github.com/pkg/errors"
)

// LockName is the lock file name used by dep.
const LockName = "Gopkg.lock"

// Lock holds lock file data and implements gps.Lock.
type Lock struct {
    SolveMeta SolveMeta
    P         []gps.LockedProject
}

// SolveMeta holds metadata about the solving process that created the lock that
// is not specific to any individual project.
type SolveMeta struct {
    AnalyzerName    string
    AnalyzerVersion int
    SolverName      string
    SolverVersion   int
    InputImports    []string
}

type rawLock struct {
    SolveMeta solveMeta          `toml:"solve-meta"`
    Projects  []rawLockedProject `toml:"projects"`
}

type solveMeta struct {
    AnalyzerName    string   `toml:"analyzer-name"`
    AnalyzerVersion int      `toml:"analyzer-version"`
    SolverName      string   `toml:"solver-name"`
    SolverVersion   int      `toml:"solver-version"`
    InputImports    []string `toml:"input-imports"`
}

type rawLockedProject struct {
    Name      string   `toml:"name"`
    Branch    string   `toml:"branch,omitempty"`
    Revision  string   `toml:"revision"`
    Version   string   `toml:"version,omitempty"`
    Source    string   `toml:"source,omitempty"`
    Packages  []string `toml:"packages"`
    PruneOpts string   `toml:"pruneopts"`
    Digest    string   `toml:"digest"`
}

func readLock(r io.Reader) (*Lock, error) {
    buf := &bytes.Buffer{}
    _, err := buf.ReadFrom(r)
    if err != nil {
        return nil, errors.Wrap(err, "Unable to read byte stream")
    }

    raw := rawLock{}
    err = toml.Unmarshal(buf.Bytes(), &raw)
    if err != nil {
        return nil, errors.Wrap(err, "Unable to parse the lock as TOML")
    }

    return fromRawLock(raw)
}

func fromRawLock(raw rawLock) (*Lock, error) {
    l := &Lock{
        P: make([]gps.LockedProject, 0, len(raw.Projects)),
    }

    l.SolveMeta.AnalyzerName = raw.SolveMeta.AnalyzerName
    l.SolveMeta.AnalyzerVersion = raw.SolveMeta.AnalyzerVersion
    l.SolveMeta.SolverName = raw.SolveMeta.SolverName
    l.SolveMeta.SolverVersion = raw.SolveMeta.SolverVersion
    l.SolveMeta.InputImports = raw.SolveMeta.InputImports

    for _, ld := range raw.Projects {
        r := gps.Revision(ld.Revision)

        var v gps.Version = r
        if ld.Version != "" {
            if ld.Branch != "" {
                return nil, errors.Errorf("lock file specified both a branch (%s) and version (%s) for %s", ld.Branch, ld.Version, ld.Name)
            }
            v = gps.NewVersion(ld.Version).Pair(r)
        } else if ld.Branch != "" {
            v = gps.NewBranch(ld.Branch).Pair(r)
        } else if r == "" {
            return nil, errors.Errorf("lock file has entry for %s, but specifies no branch or version", ld.Name)
        }

        id := gps.ProjectIdentifier{
            ProjectRoot: gps.ProjectRoot(ld.Name),
            Source:      ld.Source,
        }

        var err error
        vp := verify.VerifiableProject{
            LockedProject: gps.NewLockedProject(id, v, ld.Packages),
        }
        if ld.Digest != "" {
            vp.Digest, err = verify.ParseVersionedDigest(ld.Digest)
            if err != nil {
                return nil, err
            }
        }

        po, err := gps.ParsePruneOptions(ld.PruneOpts)
        if err != nil {
            return nil, errors.Errorf("%s in prune options for %s", err.Error(), ld.Name)
        }
        // Add the vendor pruning bit so that gps doesn't get confused
        vp.PruneOpts = po | gps.PruneNestedVendorDirs

        l.P = append(l.P, vp)
    }

    return l, nil
}

// Projects returns the list of LockedProjects contained in the lock data.
func (l *Lock) Projects() []gps.LockedProject {
    if l == nil || l == (*Lock)(nil) {
        return nil
    }
    return l.P
}

// InputImports reports the list of input imports that were used in generating
// this Lock.
func (l *Lock) InputImports() []string {
    if l == nil || l == (*Lock)(nil) {
        return nil
    }
    return l.SolveMeta.InputImports
}

// HasProjectWithRoot checks if the lock contains a project with the provided
// ProjectRoot.
//
// This check is O(n) in the number of projects.
func (l *Lock) HasProjectWithRoot(root gps.ProjectRoot) bool {
    for _, p := range l.P {
        if p.Ident().ProjectRoot == root {
            return true
        }
    }

    return false
}

func (l *Lock) dup() *Lock {
    l2 := &Lock{
        SolveMeta: l.SolveMeta,
        P:         make([]gps.LockedProject, len(l.P)),
    }

    l2.SolveMeta.InputImports = make([]string, len(l.SolveMeta.InputImports))
    copy(l2.SolveMeta.InputImports, l.SolveMeta.InputImports)
    copy(l2.P, l.P)

    return l2
}

// toRaw converts the manifest into a representation suitable to write to the lock file
func (l *Lock) toRaw() rawLock {
    raw := rawLock{
        SolveMeta: solveMeta{
            AnalyzerName:    l.SolveMeta.AnalyzerName,
            AnalyzerVersion: l.SolveMeta.AnalyzerVersion,
            InputImports:    l.SolveMeta.InputImports,
            SolverName:      l.SolveMeta.SolverName,
            SolverVersion:   l.SolveMeta.SolverVersion,
        },
        Projects: make([]rawLockedProject, 0, len(l.P)),
    }

    sort.Slice(l.P, func(i, j int) bool {
        return l.P[i].Ident().Less(l.P[j].Ident())
    })

    for _, lp := range l.P {
        id := lp.Ident()
        ld := rawLockedProject{
            Name:     string(id.ProjectRoot),
            Source:   id.Source,
            Packages: lp.Packages(),
        }

        v := lp.Version()
        ld.Revision, ld.Branch, ld.Version = gps.VersionComponentStrings(v)

        // This will panic if the lock isn't the expected dynamic type. We can
        // relax this later if it turns out to create real problems, but there's
        // no intended case in which this is untrue, so it's preferable to start
        // by failing hard if those expectations aren't met.
        vp := lp.(verify.VerifiableProject)
        ld.Digest = vp.Digest.String()
        ld.PruneOpts = (vp.PruneOpts & ^gps.PruneNestedVendorDirs).String()

        raw.Projects = append(raw.Projects, ld)
    }

    return raw
}

// MarshalTOML serializes this lock into TOML via an intermediate raw form.
func (l *Lock) MarshalTOML() ([]byte, error) {
    raw := l.toRaw()
    var buf bytes.Buffer
    enc := toml.NewEncoder(&buf).ArraysWithOneElementPerLine(true)
    err := enc.Encode(raw)
    return buf.Bytes(), errors.Wrap(err, "Unable to marshal lock to TOML string")
}

// LockFromSolution converts a gps.Solution to dep's representation of a lock.
// It makes sure that that the provided prune options are set correctly, as the
// solver does not use VerifiableProjects for new selections it makes.
//
// Data is defensively copied wherever necessary to ensure the resulting *Lock
// shares no memory with the input solution.
func LockFromSolution(in gps.Solution, prune gps.CascadingPruneOptions) *Lock {
    p := in.Projects()

    l := &Lock{
        SolveMeta: SolveMeta{
            AnalyzerName:    in.AnalyzerName(),
            AnalyzerVersion: in.AnalyzerVersion(),
            InputImports:    in.InputImports(),
            SolverName:      in.SolverName(),
            SolverVersion:   in.SolverVersion(),
        },
        P: make([]gps.LockedProject, 0, len(p)),
    }

    for _, lp := range p {
        if vp, ok := lp.(verify.VerifiableProject); ok {
            l.P = append(l.P, vp)
        } else {
            l.P = append(l.P, verify.VerifiableProject{
                LockedProject: lp,
                PruneOpts:     prune.PruneOptionsFor(lp.Ident().ProjectRoot),
            })
        }
    }

    return l
}